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_selections(cx: &mut TestAppContext) {
8219 init_test(cx, |_| {});
8220
8221 {
8222 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8223 cx.set_state(indoc! {"
8224 impl A {
8225
8226 fn b() {}
8227
8228 «fn c() {
8229
8230 }ˇ»
8231 }
8232 "});
8233
8234 cx.update_editor(|editor, window, cx| {
8235 editor.autoindent(&Default::default(), window, cx);
8236 });
8237
8238 cx.assert_editor_state(indoc! {"
8239 impl A {
8240
8241 fn b() {}
8242
8243 «fn c() {
8244
8245 }ˇ»
8246 }
8247 "});
8248 }
8249
8250 {
8251 let mut cx = EditorTestContext::new_multibuffer(
8252 cx,
8253 [indoc! { "
8254 impl A {
8255 «
8256 // a
8257 fn b(){}
8258 »
8259 «
8260 }
8261 fn c(){}
8262 »
8263 "}],
8264 );
8265
8266 let buffer = cx.update_editor(|editor, _, cx| {
8267 let buffer = editor.buffer().update(cx, |buffer, _| {
8268 buffer.all_buffers().iter().next().unwrap().clone()
8269 });
8270 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
8271 buffer
8272 });
8273
8274 cx.run_until_parked();
8275 cx.update_editor(|editor, window, cx| {
8276 editor.select_all(&Default::default(), window, cx);
8277 editor.autoindent(&Default::default(), window, cx)
8278 });
8279 cx.run_until_parked();
8280
8281 cx.update(|_, cx| {
8282 assert_eq!(
8283 buffer.read(cx).text(),
8284 indoc! { "
8285 impl A {
8286
8287 // a
8288 fn b(){}
8289
8290
8291 }
8292 fn c(){}
8293
8294 " }
8295 )
8296 });
8297 }
8298}
8299
8300#[gpui::test]
8301async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
8302 init_test(cx, |_| {});
8303
8304 let mut cx = EditorTestContext::new(cx).await;
8305
8306 let language = Arc::new(Language::new(
8307 LanguageConfig {
8308 brackets: BracketPairConfig {
8309 pairs: vec![
8310 BracketPair {
8311 start: "{".to_string(),
8312 end: "}".to_string(),
8313 close: true,
8314 surround: true,
8315 newline: true,
8316 },
8317 BracketPair {
8318 start: "(".to_string(),
8319 end: ")".to_string(),
8320 close: true,
8321 surround: true,
8322 newline: true,
8323 },
8324 BracketPair {
8325 start: "/*".to_string(),
8326 end: " */".to_string(),
8327 close: true,
8328 surround: true,
8329 newline: true,
8330 },
8331 BracketPair {
8332 start: "[".to_string(),
8333 end: "]".to_string(),
8334 close: false,
8335 surround: false,
8336 newline: true,
8337 },
8338 BracketPair {
8339 start: "\"".to_string(),
8340 end: "\"".to_string(),
8341 close: true,
8342 surround: true,
8343 newline: false,
8344 },
8345 BracketPair {
8346 start: "<".to_string(),
8347 end: ">".to_string(),
8348 close: false,
8349 surround: true,
8350 newline: true,
8351 },
8352 ],
8353 ..Default::default()
8354 },
8355 autoclose_before: "})]".to_string(),
8356 ..Default::default()
8357 },
8358 Some(tree_sitter_rust::LANGUAGE.into()),
8359 ));
8360
8361 cx.language_registry().add(language.clone());
8362 cx.update_buffer(|buffer, cx| {
8363 buffer.set_language(Some(language), cx);
8364 });
8365
8366 cx.set_state(
8367 &r#"
8368 🏀ˇ
8369 εˇ
8370 ❤️ˇ
8371 "#
8372 .unindent(),
8373 );
8374
8375 // autoclose multiple nested brackets at multiple cursors
8376 cx.update_editor(|editor, window, cx| {
8377 editor.handle_input("{", window, cx);
8378 editor.handle_input("{", window, cx);
8379 editor.handle_input("{", window, cx);
8380 });
8381 cx.assert_editor_state(
8382 &"
8383 🏀{{{ˇ}}}
8384 ε{{{ˇ}}}
8385 ❤️{{{ˇ}}}
8386 "
8387 .unindent(),
8388 );
8389
8390 // insert a different closing bracket
8391 cx.update_editor(|editor, window, cx| {
8392 editor.handle_input(")", window, cx);
8393 });
8394 cx.assert_editor_state(
8395 &"
8396 🏀{{{)ˇ}}}
8397 ε{{{)ˇ}}}
8398 ❤️{{{)ˇ}}}
8399 "
8400 .unindent(),
8401 );
8402
8403 // skip over the auto-closed brackets when typing a closing bracket
8404 cx.update_editor(|editor, window, cx| {
8405 editor.move_right(&MoveRight, window, cx);
8406 editor.handle_input("}", window, cx);
8407 editor.handle_input("}", window, cx);
8408 editor.handle_input("}", window, cx);
8409 });
8410 cx.assert_editor_state(
8411 &"
8412 🏀{{{)}}}}ˇ
8413 ε{{{)}}}}ˇ
8414 ❤️{{{)}}}}ˇ
8415 "
8416 .unindent(),
8417 );
8418
8419 // autoclose multi-character pairs
8420 cx.set_state(
8421 &"
8422 ˇ
8423 ˇ
8424 "
8425 .unindent(),
8426 );
8427 cx.update_editor(|editor, window, cx| {
8428 editor.handle_input("/", window, cx);
8429 editor.handle_input("*", window, cx);
8430 });
8431 cx.assert_editor_state(
8432 &"
8433 /*ˇ */
8434 /*ˇ */
8435 "
8436 .unindent(),
8437 );
8438
8439 // one cursor autocloses a multi-character pair, one cursor
8440 // does not autoclose.
8441 cx.set_state(
8442 &"
8443 /ˇ
8444 ˇ
8445 "
8446 .unindent(),
8447 );
8448 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
8449 cx.assert_editor_state(
8450 &"
8451 /*ˇ */
8452 *ˇ
8453 "
8454 .unindent(),
8455 );
8456
8457 // Don't autoclose if the next character isn't whitespace and isn't
8458 // listed in the language's "autoclose_before" section.
8459 cx.set_state("ˇa b");
8460 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8461 cx.assert_editor_state("{ˇa b");
8462
8463 // Don't autoclose if `close` is false for the bracket pair
8464 cx.set_state("ˇ");
8465 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
8466 cx.assert_editor_state("[ˇ");
8467
8468 // Surround with brackets if text is selected
8469 cx.set_state("«aˇ» b");
8470 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8471 cx.assert_editor_state("{«aˇ»} b");
8472
8473 // Autoclose when not immediately after a word character
8474 cx.set_state("a ˇ");
8475 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8476 cx.assert_editor_state("a \"ˇ\"");
8477
8478 // Autoclose pair where the start and end characters are the same
8479 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8480 cx.assert_editor_state("a \"\"ˇ");
8481
8482 // Don't autoclose when immediately after a word character
8483 cx.set_state("aˇ");
8484 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8485 cx.assert_editor_state("a\"ˇ");
8486
8487 // Do autoclose when after a non-word character
8488 cx.set_state("{ˇ");
8489 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8490 cx.assert_editor_state("{\"ˇ\"");
8491
8492 // Non identical pairs autoclose regardless of preceding character
8493 cx.set_state("aˇ");
8494 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8495 cx.assert_editor_state("a{ˇ}");
8496
8497 // Don't autoclose pair if autoclose is disabled
8498 cx.set_state("ˇ");
8499 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8500 cx.assert_editor_state("<ˇ");
8501
8502 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
8503 cx.set_state("«aˇ» b");
8504 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8505 cx.assert_editor_state("<«aˇ»> b");
8506}
8507
8508#[gpui::test]
8509async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
8510 init_test(cx, |settings| {
8511 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8512 });
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: false,
8538 surround: false,
8539 newline: true,
8540 },
8541 ],
8542 ..Default::default()
8543 },
8544 autoclose_before: "})]".to_string(),
8545 ..Default::default()
8546 },
8547 Some(tree_sitter_rust::LANGUAGE.into()),
8548 ));
8549
8550 cx.language_registry().add(language.clone());
8551 cx.update_buffer(|buffer, cx| {
8552 buffer.set_language(Some(language), cx);
8553 });
8554
8555 cx.set_state(
8556 &"
8557 ˇ
8558 ˇ
8559 ˇ
8560 "
8561 .unindent(),
8562 );
8563
8564 // ensure only matching closing brackets are skipped over
8565 cx.update_editor(|editor, window, cx| {
8566 editor.handle_input("}", window, cx);
8567 editor.move_left(&MoveLeft, window, cx);
8568 editor.handle_input(")", window, cx);
8569 editor.move_left(&MoveLeft, window, cx);
8570 });
8571 cx.assert_editor_state(
8572 &"
8573 ˇ)}
8574 ˇ)}
8575 ˇ)}
8576 "
8577 .unindent(),
8578 );
8579
8580 // skip-over closing brackets at multiple cursors
8581 cx.update_editor(|editor, window, cx| {
8582 editor.handle_input(")", window, cx);
8583 editor.handle_input("}", window, cx);
8584 });
8585 cx.assert_editor_state(
8586 &"
8587 )}ˇ
8588 )}ˇ
8589 )}ˇ
8590 "
8591 .unindent(),
8592 );
8593
8594 // ignore non-close brackets
8595 cx.update_editor(|editor, window, cx| {
8596 editor.handle_input("]", window, cx);
8597 editor.move_left(&MoveLeft, window, cx);
8598 editor.handle_input("]", window, cx);
8599 });
8600 cx.assert_editor_state(
8601 &"
8602 )}]ˇ]
8603 )}]ˇ]
8604 )}]ˇ]
8605 "
8606 .unindent(),
8607 );
8608}
8609
8610#[gpui::test]
8611async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8612 init_test(cx, |_| {});
8613
8614 let mut cx = EditorTestContext::new(cx).await;
8615
8616 let html_language = Arc::new(
8617 Language::new(
8618 LanguageConfig {
8619 name: "HTML".into(),
8620 brackets: BracketPairConfig {
8621 pairs: vec![
8622 BracketPair {
8623 start: "<".into(),
8624 end: ">".into(),
8625 close: true,
8626 ..Default::default()
8627 },
8628 BracketPair {
8629 start: "{".into(),
8630 end: "}".into(),
8631 close: true,
8632 ..Default::default()
8633 },
8634 BracketPair {
8635 start: "(".into(),
8636 end: ")".into(),
8637 close: true,
8638 ..Default::default()
8639 },
8640 ],
8641 ..Default::default()
8642 },
8643 autoclose_before: "})]>".into(),
8644 ..Default::default()
8645 },
8646 Some(tree_sitter_html::LANGUAGE.into()),
8647 )
8648 .with_injection_query(
8649 r#"
8650 (script_element
8651 (raw_text) @injection.content
8652 (#set! injection.language "javascript"))
8653 "#,
8654 )
8655 .unwrap(),
8656 );
8657
8658 let javascript_language = Arc::new(Language::new(
8659 LanguageConfig {
8660 name: "JavaScript".into(),
8661 brackets: BracketPairConfig {
8662 pairs: vec![
8663 BracketPair {
8664 start: "/*".into(),
8665 end: " */".into(),
8666 close: true,
8667 ..Default::default()
8668 },
8669 BracketPair {
8670 start: "{".into(),
8671 end: "}".into(),
8672 close: true,
8673 ..Default::default()
8674 },
8675 BracketPair {
8676 start: "(".into(),
8677 end: ")".into(),
8678 close: true,
8679 ..Default::default()
8680 },
8681 ],
8682 ..Default::default()
8683 },
8684 autoclose_before: "})]>".into(),
8685 ..Default::default()
8686 },
8687 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8688 ));
8689
8690 cx.language_registry().add(html_language.clone());
8691 cx.language_registry().add(javascript_language.clone());
8692 cx.executor().run_until_parked();
8693
8694 cx.update_buffer(|buffer, cx| {
8695 buffer.set_language(Some(html_language), cx);
8696 });
8697
8698 cx.set_state(
8699 &r#"
8700 <body>ˇ
8701 <script>
8702 var x = 1;ˇ
8703 </script>
8704 </body>ˇ
8705 "#
8706 .unindent(),
8707 );
8708
8709 // Precondition: different languages are active at different locations.
8710 cx.update_editor(|editor, window, cx| {
8711 let snapshot = editor.snapshot(window, cx);
8712 let cursors = editor.selections.ranges::<usize>(cx);
8713 let languages = cursors
8714 .iter()
8715 .map(|c| snapshot.language_at(c.start).unwrap().name())
8716 .collect::<Vec<_>>();
8717 assert_eq!(
8718 languages,
8719 &["HTML".into(), "JavaScript".into(), "HTML".into()]
8720 );
8721 });
8722
8723 // Angle brackets autoclose in HTML, but not JavaScript.
8724 cx.update_editor(|editor, window, cx| {
8725 editor.handle_input("<", window, cx);
8726 editor.handle_input("a", window, cx);
8727 });
8728 cx.assert_editor_state(
8729 &r#"
8730 <body><aˇ>
8731 <script>
8732 var x = 1;<aˇ
8733 </script>
8734 </body><aˇ>
8735 "#
8736 .unindent(),
8737 );
8738
8739 // Curly braces and parens autoclose in both HTML and JavaScript.
8740 cx.update_editor(|editor, window, cx| {
8741 editor.handle_input(" b=", window, cx);
8742 editor.handle_input("{", window, cx);
8743 editor.handle_input("c", window, cx);
8744 editor.handle_input("(", window, cx);
8745 });
8746 cx.assert_editor_state(
8747 &r#"
8748 <body><a b={c(ˇ)}>
8749 <script>
8750 var x = 1;<a b={c(ˇ)}
8751 </script>
8752 </body><a b={c(ˇ)}>
8753 "#
8754 .unindent(),
8755 );
8756
8757 // Brackets that were already autoclosed are skipped.
8758 cx.update_editor(|editor, window, cx| {
8759 editor.handle_input(")", window, cx);
8760 editor.handle_input("d", window, cx);
8761 editor.handle_input("}", window, cx);
8762 });
8763 cx.assert_editor_state(
8764 &r#"
8765 <body><a b={c()d}ˇ>
8766 <script>
8767 var x = 1;<a b={c()d}ˇ
8768 </script>
8769 </body><a b={c()d}ˇ>
8770 "#
8771 .unindent(),
8772 );
8773 cx.update_editor(|editor, window, cx| {
8774 editor.handle_input(">", window, cx);
8775 });
8776 cx.assert_editor_state(
8777 &r#"
8778 <body><a b={c()d}>ˇ
8779 <script>
8780 var x = 1;<a b={c()d}>ˇ
8781 </script>
8782 </body><a b={c()d}>ˇ
8783 "#
8784 .unindent(),
8785 );
8786
8787 // Reset
8788 cx.set_state(
8789 &r#"
8790 <body>ˇ
8791 <script>
8792 var x = 1;ˇ
8793 </script>
8794 </body>ˇ
8795 "#
8796 .unindent(),
8797 );
8798
8799 cx.update_editor(|editor, window, cx| {
8800 editor.handle_input("<", window, cx);
8801 });
8802 cx.assert_editor_state(
8803 &r#"
8804 <body><ˇ>
8805 <script>
8806 var x = 1;<ˇ
8807 </script>
8808 </body><ˇ>
8809 "#
8810 .unindent(),
8811 );
8812
8813 // When backspacing, the closing angle brackets are removed.
8814 cx.update_editor(|editor, window, cx| {
8815 editor.backspace(&Backspace, window, cx);
8816 });
8817 cx.assert_editor_state(
8818 &r#"
8819 <body>ˇ
8820 <script>
8821 var x = 1;ˇ
8822 </script>
8823 </body>ˇ
8824 "#
8825 .unindent(),
8826 );
8827
8828 // Block comments autoclose in JavaScript, but not HTML.
8829 cx.update_editor(|editor, window, cx| {
8830 editor.handle_input("/", window, cx);
8831 editor.handle_input("*", window, cx);
8832 });
8833 cx.assert_editor_state(
8834 &r#"
8835 <body>/*ˇ
8836 <script>
8837 var x = 1;/*ˇ */
8838 </script>
8839 </body>/*ˇ
8840 "#
8841 .unindent(),
8842 );
8843}
8844
8845#[gpui::test]
8846async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
8847 init_test(cx, |_| {});
8848
8849 let mut cx = EditorTestContext::new(cx).await;
8850
8851 let rust_language = Arc::new(
8852 Language::new(
8853 LanguageConfig {
8854 name: "Rust".into(),
8855 brackets: serde_json::from_value(json!([
8856 { "start": "{", "end": "}", "close": true, "newline": true },
8857 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
8858 ]))
8859 .unwrap(),
8860 autoclose_before: "})]>".into(),
8861 ..Default::default()
8862 },
8863 Some(tree_sitter_rust::LANGUAGE.into()),
8864 )
8865 .with_override_query("(string_literal) @string")
8866 .unwrap(),
8867 );
8868
8869 cx.language_registry().add(rust_language.clone());
8870 cx.update_buffer(|buffer, cx| {
8871 buffer.set_language(Some(rust_language), cx);
8872 });
8873
8874 cx.set_state(
8875 &r#"
8876 let x = ˇ
8877 "#
8878 .unindent(),
8879 );
8880
8881 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
8882 cx.update_editor(|editor, window, cx| {
8883 editor.handle_input("\"", window, cx);
8884 });
8885 cx.assert_editor_state(
8886 &r#"
8887 let x = "ˇ"
8888 "#
8889 .unindent(),
8890 );
8891
8892 // Inserting another quotation mark. The cursor moves across the existing
8893 // automatically-inserted quotation mark.
8894 cx.update_editor(|editor, window, cx| {
8895 editor.handle_input("\"", window, cx);
8896 });
8897 cx.assert_editor_state(
8898 &r#"
8899 let x = ""ˇ
8900 "#
8901 .unindent(),
8902 );
8903
8904 // Reset
8905 cx.set_state(
8906 &r#"
8907 let x = ˇ
8908 "#
8909 .unindent(),
8910 );
8911
8912 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
8913 cx.update_editor(|editor, window, cx| {
8914 editor.handle_input("\"", window, cx);
8915 editor.handle_input(" ", window, cx);
8916 editor.move_left(&Default::default(), window, cx);
8917 editor.handle_input("\\", window, cx);
8918 editor.handle_input("\"", window, cx);
8919 });
8920 cx.assert_editor_state(
8921 &r#"
8922 let x = "\"ˇ "
8923 "#
8924 .unindent(),
8925 );
8926
8927 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8928 // mark. Nothing is inserted.
8929 cx.update_editor(|editor, window, cx| {
8930 editor.move_right(&Default::default(), window, cx);
8931 editor.handle_input("\"", window, cx);
8932 });
8933 cx.assert_editor_state(
8934 &r#"
8935 let x = "\" "ˇ
8936 "#
8937 .unindent(),
8938 );
8939}
8940
8941#[gpui::test]
8942async fn test_surround_with_pair(cx: &mut TestAppContext) {
8943 init_test(cx, |_| {});
8944
8945 let language = Arc::new(Language::new(
8946 LanguageConfig {
8947 brackets: BracketPairConfig {
8948 pairs: vec![
8949 BracketPair {
8950 start: "{".to_string(),
8951 end: "}".to_string(),
8952 close: true,
8953 surround: true,
8954 newline: true,
8955 },
8956 BracketPair {
8957 start: "/* ".to_string(),
8958 end: "*/".to_string(),
8959 close: true,
8960 surround: true,
8961 ..Default::default()
8962 },
8963 ],
8964 ..Default::default()
8965 },
8966 ..Default::default()
8967 },
8968 Some(tree_sitter_rust::LANGUAGE.into()),
8969 ));
8970
8971 let text = r#"
8972 a
8973 b
8974 c
8975 "#
8976 .unindent();
8977
8978 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8979 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8980 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8981 editor
8982 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8983 .await;
8984
8985 editor.update_in(cx, |editor, window, cx| {
8986 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8987 s.select_display_ranges([
8988 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8989 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8990 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8991 ])
8992 });
8993
8994 editor.handle_input("{", window, cx);
8995 editor.handle_input("{", window, cx);
8996 editor.handle_input("{", window, cx);
8997 assert_eq!(
8998 editor.text(cx),
8999 "
9000 {{{a}}}
9001 {{{b}}}
9002 {{{c}}}
9003 "
9004 .unindent()
9005 );
9006 assert_eq!(
9007 editor.selections.display_ranges(cx),
9008 [
9009 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
9010 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
9011 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
9012 ]
9013 );
9014
9015 editor.undo(&Undo, window, cx);
9016 editor.undo(&Undo, window, cx);
9017 editor.undo(&Undo, window, cx);
9018 assert_eq!(
9019 editor.text(cx),
9020 "
9021 a
9022 b
9023 c
9024 "
9025 .unindent()
9026 );
9027 assert_eq!(
9028 editor.selections.display_ranges(cx),
9029 [
9030 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
9031 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
9032 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
9033 ]
9034 );
9035
9036 // Ensure inserting the first character of a multi-byte bracket pair
9037 // doesn't surround the selections with the bracket.
9038 editor.handle_input("/", window, cx);
9039 assert_eq!(
9040 editor.text(cx),
9041 "
9042 /
9043 /
9044 /
9045 "
9046 .unindent()
9047 );
9048 assert_eq!(
9049 editor.selections.display_ranges(cx),
9050 [
9051 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
9052 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
9053 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
9054 ]
9055 );
9056
9057 editor.undo(&Undo, window, cx);
9058 assert_eq!(
9059 editor.text(cx),
9060 "
9061 a
9062 b
9063 c
9064 "
9065 .unindent()
9066 );
9067 assert_eq!(
9068 editor.selections.display_ranges(cx),
9069 [
9070 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
9071 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
9072 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
9073 ]
9074 );
9075
9076 // Ensure inserting the last character of a multi-byte bracket pair
9077 // doesn't surround the selections with the bracket.
9078 editor.handle_input("*", window, cx);
9079 assert_eq!(
9080 editor.text(cx),
9081 "
9082 *
9083 *
9084 *
9085 "
9086 .unindent()
9087 );
9088 assert_eq!(
9089 editor.selections.display_ranges(cx),
9090 [
9091 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
9092 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
9093 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
9094 ]
9095 );
9096 });
9097}
9098
9099#[gpui::test]
9100async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
9101 init_test(cx, |_| {});
9102
9103 let language = Arc::new(Language::new(
9104 LanguageConfig {
9105 brackets: BracketPairConfig {
9106 pairs: vec![BracketPair {
9107 start: "{".to_string(),
9108 end: "}".to_string(),
9109 close: true,
9110 surround: true,
9111 newline: true,
9112 }],
9113 ..Default::default()
9114 },
9115 autoclose_before: "}".to_string(),
9116 ..Default::default()
9117 },
9118 Some(tree_sitter_rust::LANGUAGE.into()),
9119 ));
9120
9121 let text = r#"
9122 a
9123 b
9124 c
9125 "#
9126 .unindent();
9127
9128 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9129 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9130 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9131 editor
9132 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9133 .await;
9134
9135 editor.update_in(cx, |editor, window, cx| {
9136 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9137 s.select_ranges([
9138 Point::new(0, 1)..Point::new(0, 1),
9139 Point::new(1, 1)..Point::new(1, 1),
9140 Point::new(2, 1)..Point::new(2, 1),
9141 ])
9142 });
9143
9144 editor.handle_input("{", window, cx);
9145 editor.handle_input("{", window, cx);
9146 editor.handle_input("_", window, cx);
9147 assert_eq!(
9148 editor.text(cx),
9149 "
9150 a{{_}}
9151 b{{_}}
9152 c{{_}}
9153 "
9154 .unindent()
9155 );
9156 assert_eq!(
9157 editor.selections.ranges::<Point>(cx),
9158 [
9159 Point::new(0, 4)..Point::new(0, 4),
9160 Point::new(1, 4)..Point::new(1, 4),
9161 Point::new(2, 4)..Point::new(2, 4)
9162 ]
9163 );
9164
9165 editor.backspace(&Default::default(), window, cx);
9166 editor.backspace(&Default::default(), window, cx);
9167 assert_eq!(
9168 editor.text(cx),
9169 "
9170 a{}
9171 b{}
9172 c{}
9173 "
9174 .unindent()
9175 );
9176 assert_eq!(
9177 editor.selections.ranges::<Point>(cx),
9178 [
9179 Point::new(0, 2)..Point::new(0, 2),
9180 Point::new(1, 2)..Point::new(1, 2),
9181 Point::new(2, 2)..Point::new(2, 2)
9182 ]
9183 );
9184
9185 editor.delete_to_previous_word_start(&Default::default(), window, cx);
9186 assert_eq!(
9187 editor.text(cx),
9188 "
9189 a
9190 b
9191 c
9192 "
9193 .unindent()
9194 );
9195 assert_eq!(
9196 editor.selections.ranges::<Point>(cx),
9197 [
9198 Point::new(0, 1)..Point::new(0, 1),
9199 Point::new(1, 1)..Point::new(1, 1),
9200 Point::new(2, 1)..Point::new(2, 1)
9201 ]
9202 );
9203 });
9204}
9205
9206#[gpui::test]
9207async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
9208 init_test(cx, |settings| {
9209 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9210 });
9211
9212 let mut cx = EditorTestContext::new(cx).await;
9213
9214 let language = Arc::new(Language::new(
9215 LanguageConfig {
9216 brackets: BracketPairConfig {
9217 pairs: vec![
9218 BracketPair {
9219 start: "{".to_string(),
9220 end: "}".to_string(),
9221 close: true,
9222 surround: true,
9223 newline: true,
9224 },
9225 BracketPair {
9226 start: "(".to_string(),
9227 end: ")".to_string(),
9228 close: true,
9229 surround: true,
9230 newline: true,
9231 },
9232 BracketPair {
9233 start: "[".to_string(),
9234 end: "]".to_string(),
9235 close: false,
9236 surround: true,
9237 newline: true,
9238 },
9239 ],
9240 ..Default::default()
9241 },
9242 autoclose_before: "})]".to_string(),
9243 ..Default::default()
9244 },
9245 Some(tree_sitter_rust::LANGUAGE.into()),
9246 ));
9247
9248 cx.language_registry().add(language.clone());
9249 cx.update_buffer(|buffer, cx| {
9250 buffer.set_language(Some(language), cx);
9251 });
9252
9253 cx.set_state(
9254 &"
9255 {(ˇ)}
9256 [[ˇ]]
9257 {(ˇ)}
9258 "
9259 .unindent(),
9260 );
9261
9262 cx.update_editor(|editor, window, cx| {
9263 editor.backspace(&Default::default(), window, cx);
9264 editor.backspace(&Default::default(), window, cx);
9265 });
9266
9267 cx.assert_editor_state(
9268 &"
9269 ˇ
9270 ˇ]]
9271 ˇ
9272 "
9273 .unindent(),
9274 );
9275
9276 cx.update_editor(|editor, window, cx| {
9277 editor.handle_input("{", window, cx);
9278 editor.handle_input("{", window, cx);
9279 editor.move_right(&MoveRight, window, cx);
9280 editor.move_right(&MoveRight, window, cx);
9281 editor.move_left(&MoveLeft, window, cx);
9282 editor.move_left(&MoveLeft, window, cx);
9283 editor.backspace(&Default::default(), window, cx);
9284 });
9285
9286 cx.assert_editor_state(
9287 &"
9288 {ˇ}
9289 {ˇ}]]
9290 {ˇ}
9291 "
9292 .unindent(),
9293 );
9294
9295 cx.update_editor(|editor, window, cx| {
9296 editor.backspace(&Default::default(), window, cx);
9297 });
9298
9299 cx.assert_editor_state(
9300 &"
9301 ˇ
9302 ˇ]]
9303 ˇ
9304 "
9305 .unindent(),
9306 );
9307}
9308
9309#[gpui::test]
9310async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
9311 init_test(cx, |_| {});
9312
9313 let language = Arc::new(Language::new(
9314 LanguageConfig::default(),
9315 Some(tree_sitter_rust::LANGUAGE.into()),
9316 ));
9317
9318 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
9319 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9320 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9321 editor
9322 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9323 .await;
9324
9325 editor.update_in(cx, |editor, window, cx| {
9326 editor.set_auto_replace_emoji_shortcode(true);
9327
9328 editor.handle_input("Hello ", window, cx);
9329 editor.handle_input(":wave", window, cx);
9330 assert_eq!(editor.text(cx), "Hello :wave".unindent());
9331
9332 editor.handle_input(":", window, cx);
9333 assert_eq!(editor.text(cx), "Hello 👋".unindent());
9334
9335 editor.handle_input(" :smile", window, cx);
9336 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
9337
9338 editor.handle_input(":", window, cx);
9339 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
9340
9341 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
9342 editor.handle_input(":wave", window, cx);
9343 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
9344
9345 editor.handle_input(":", window, cx);
9346 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
9347
9348 editor.handle_input(":1", window, cx);
9349 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
9350
9351 editor.handle_input(":", window, cx);
9352 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
9353
9354 // Ensure shortcode does not get replaced when it is part of a word
9355 editor.handle_input(" Test:wave", window, cx);
9356 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
9357
9358 editor.handle_input(":", window, cx);
9359 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
9360
9361 editor.set_auto_replace_emoji_shortcode(false);
9362
9363 // Ensure shortcode does not get replaced when auto replace is off
9364 editor.handle_input(" :wave", window, cx);
9365 assert_eq!(
9366 editor.text(cx),
9367 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
9368 );
9369
9370 editor.handle_input(":", window, cx);
9371 assert_eq!(
9372 editor.text(cx),
9373 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
9374 );
9375 });
9376}
9377
9378#[gpui::test]
9379async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
9380 init_test(cx, |_| {});
9381
9382 let (text, insertion_ranges) = marked_text_ranges(
9383 indoc! {"
9384 ˇ
9385 "},
9386 false,
9387 );
9388
9389 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
9390 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9391
9392 _ = editor.update_in(cx, |editor, window, cx| {
9393 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
9394
9395 editor
9396 .insert_snippet(&insertion_ranges, snippet, window, cx)
9397 .unwrap();
9398
9399 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
9400 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
9401 assert_eq!(editor.text(cx), expected_text);
9402 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
9403 }
9404
9405 assert(
9406 editor,
9407 cx,
9408 indoc! {"
9409 type «» =•
9410 "},
9411 );
9412
9413 assert!(editor.context_menu_visible(), "There should be a matches");
9414 });
9415}
9416
9417#[gpui::test]
9418async fn test_snippets(cx: &mut TestAppContext) {
9419 init_test(cx, |_| {});
9420
9421 let mut cx = EditorTestContext::new(cx).await;
9422
9423 cx.set_state(indoc! {"
9424 a.ˇ b
9425 a.ˇ b
9426 a.ˇ b
9427 "});
9428
9429 cx.update_editor(|editor, window, cx| {
9430 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
9431 let insertion_ranges = editor
9432 .selections
9433 .all(cx)
9434 .iter()
9435 .map(|s| s.range().clone())
9436 .collect::<Vec<_>>();
9437 editor
9438 .insert_snippet(&insertion_ranges, snippet, window, cx)
9439 .unwrap();
9440 });
9441
9442 cx.assert_editor_state(indoc! {"
9443 a.f(«oneˇ», two, «threeˇ») b
9444 a.f(«oneˇ», two, «threeˇ») b
9445 a.f(«oneˇ», two, «threeˇ») b
9446 "});
9447
9448 // Can't move earlier than the first tab stop
9449 cx.update_editor(|editor, window, cx| {
9450 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9451 });
9452 cx.assert_editor_state(indoc! {"
9453 a.f(«oneˇ», two, «threeˇ») b
9454 a.f(«oneˇ», two, «threeˇ») b
9455 a.f(«oneˇ», two, «threeˇ») b
9456 "});
9457
9458 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9459 cx.assert_editor_state(indoc! {"
9460 a.f(one, «twoˇ», three) b
9461 a.f(one, «twoˇ», three) b
9462 a.f(one, «twoˇ», three) b
9463 "});
9464
9465 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
9466 cx.assert_editor_state(indoc! {"
9467 a.f(«oneˇ», two, «threeˇ») b
9468 a.f(«oneˇ», two, «threeˇ») b
9469 a.f(«oneˇ», two, «threeˇ») b
9470 "});
9471
9472 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9473 cx.assert_editor_state(indoc! {"
9474 a.f(one, «twoˇ», three) b
9475 a.f(one, «twoˇ», three) b
9476 a.f(one, «twoˇ», three) b
9477 "});
9478 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9479 cx.assert_editor_state(indoc! {"
9480 a.f(one, two, three)ˇ b
9481 a.f(one, two, three)ˇ b
9482 a.f(one, two, three)ˇ b
9483 "});
9484
9485 // As soon as the last tab stop is reached, snippet state is gone
9486 cx.update_editor(|editor, window, cx| {
9487 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9488 });
9489 cx.assert_editor_state(indoc! {"
9490 a.f(one, two, three)ˇ b
9491 a.f(one, two, three)ˇ b
9492 a.f(one, two, three)ˇ b
9493 "});
9494}
9495
9496#[gpui::test]
9497async fn test_snippet_indentation(cx: &mut TestAppContext) {
9498 init_test(cx, |_| {});
9499
9500 let mut cx = EditorTestContext::new(cx).await;
9501
9502 cx.update_editor(|editor, window, cx| {
9503 let snippet = Snippet::parse(indoc! {"
9504 /*
9505 * Multiline comment with leading indentation
9506 *
9507 * $1
9508 */
9509 $0"})
9510 .unwrap();
9511 let insertion_ranges = editor
9512 .selections
9513 .all(cx)
9514 .iter()
9515 .map(|s| s.range().clone())
9516 .collect::<Vec<_>>();
9517 editor
9518 .insert_snippet(&insertion_ranges, snippet, window, cx)
9519 .unwrap();
9520 });
9521
9522 cx.assert_editor_state(indoc! {"
9523 /*
9524 * Multiline comment with leading indentation
9525 *
9526 * ˇ
9527 */
9528 "});
9529
9530 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9531 cx.assert_editor_state(indoc! {"
9532 /*
9533 * Multiline comment with leading indentation
9534 *
9535 *•
9536 */
9537 ˇ"});
9538}
9539
9540#[gpui::test]
9541async fn test_document_format_during_save(cx: &mut TestAppContext) {
9542 init_test(cx, |_| {});
9543
9544 let fs = FakeFs::new(cx.executor());
9545 fs.insert_file(path!("/file.rs"), Default::default()).await;
9546
9547 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
9548
9549 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9550 language_registry.add(rust_lang());
9551 let mut fake_servers = language_registry.register_fake_lsp(
9552 "Rust",
9553 FakeLspAdapter {
9554 capabilities: lsp::ServerCapabilities {
9555 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9556 ..Default::default()
9557 },
9558 ..Default::default()
9559 },
9560 );
9561
9562 let buffer = project
9563 .update(cx, |project, cx| {
9564 project.open_local_buffer(path!("/file.rs"), cx)
9565 })
9566 .await
9567 .unwrap();
9568
9569 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9570 let (editor, cx) = cx.add_window_view(|window, cx| {
9571 build_editor_with_project(project.clone(), buffer, window, cx)
9572 });
9573 editor.update_in(cx, |editor, window, cx| {
9574 editor.set_text("one\ntwo\nthree\n", window, cx)
9575 });
9576 assert!(cx.read(|cx| editor.is_dirty(cx)));
9577
9578 cx.executor().start_waiting();
9579 let fake_server = fake_servers.next().await.unwrap();
9580
9581 {
9582 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9583 move |params, _| async move {
9584 assert_eq!(
9585 params.text_document.uri,
9586 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9587 );
9588 assert_eq!(params.options.tab_size, 4);
9589 Ok(Some(vec![lsp::TextEdit::new(
9590 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9591 ", ".to_string(),
9592 )]))
9593 },
9594 );
9595 let save = editor
9596 .update_in(cx, |editor, window, cx| {
9597 editor.save(
9598 SaveOptions {
9599 format: true,
9600 autosave: false,
9601 },
9602 project.clone(),
9603 window,
9604 cx,
9605 )
9606 })
9607 .unwrap();
9608 cx.executor().start_waiting();
9609 save.await;
9610
9611 assert_eq!(
9612 editor.update(cx, |editor, cx| editor.text(cx)),
9613 "one, two\nthree\n"
9614 );
9615 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9616 }
9617
9618 {
9619 editor.update_in(cx, |editor, window, cx| {
9620 editor.set_text("one\ntwo\nthree\n", window, cx)
9621 });
9622 assert!(cx.read(|cx| editor.is_dirty(cx)));
9623
9624 // Ensure we can still save even if formatting hangs.
9625 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9626 move |params, _| async move {
9627 assert_eq!(
9628 params.text_document.uri,
9629 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9630 );
9631 futures::future::pending::<()>().await;
9632 unreachable!()
9633 },
9634 );
9635 let save = editor
9636 .update_in(cx, |editor, window, cx| {
9637 editor.save(
9638 SaveOptions {
9639 format: true,
9640 autosave: false,
9641 },
9642 project.clone(),
9643 window,
9644 cx,
9645 )
9646 })
9647 .unwrap();
9648 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9649 cx.executor().start_waiting();
9650 save.await;
9651 assert_eq!(
9652 editor.update(cx, |editor, cx| editor.text(cx)),
9653 "one\ntwo\nthree\n"
9654 );
9655 }
9656
9657 // Set rust language override and assert overridden tabsize is sent to language server
9658 update_test_language_settings(cx, |settings| {
9659 settings.languages.0.insert(
9660 "Rust".into(),
9661 LanguageSettingsContent {
9662 tab_size: NonZeroU32::new(8),
9663 ..Default::default()
9664 },
9665 );
9666 });
9667
9668 {
9669 editor.update_in(cx, |editor, window, cx| {
9670 editor.set_text("somehting_new\n", window, cx)
9671 });
9672 assert!(cx.read(|cx| editor.is_dirty(cx)));
9673 let _formatting_request_signal = fake_server
9674 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9675 assert_eq!(
9676 params.text_document.uri,
9677 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9678 );
9679 assert_eq!(params.options.tab_size, 8);
9680 Ok(Some(vec![]))
9681 });
9682 let save = editor
9683 .update_in(cx, |editor, window, cx| {
9684 editor.save(
9685 SaveOptions {
9686 format: true,
9687 autosave: false,
9688 },
9689 project.clone(),
9690 window,
9691 cx,
9692 )
9693 })
9694 .unwrap();
9695 cx.executor().start_waiting();
9696 save.await;
9697 }
9698}
9699
9700#[gpui::test]
9701async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
9702 init_test(cx, |settings| {
9703 settings.defaults.ensure_final_newline_on_save = Some(false);
9704 });
9705
9706 let fs = FakeFs::new(cx.executor());
9707 fs.insert_file(path!("/file.txt"), "foo".into()).await;
9708
9709 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
9710
9711 let buffer = project
9712 .update(cx, |project, cx| {
9713 project.open_local_buffer(path!("/file.txt"), cx)
9714 })
9715 .await
9716 .unwrap();
9717
9718 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9719 let (editor, cx) = cx.add_window_view(|window, cx| {
9720 build_editor_with_project(project.clone(), buffer, window, cx)
9721 });
9722 editor.update_in(cx, |editor, window, cx| {
9723 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9724 s.select_ranges([0..0])
9725 });
9726 });
9727 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9728
9729 editor.update_in(cx, |editor, window, cx| {
9730 editor.handle_input("\n", window, cx)
9731 });
9732 cx.run_until_parked();
9733 save(&editor, &project, cx).await;
9734 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9735
9736 editor.update_in(cx, |editor, window, cx| {
9737 editor.undo(&Default::default(), window, cx);
9738 });
9739 save(&editor, &project, cx).await;
9740 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9741
9742 editor.update_in(cx, |editor, window, cx| {
9743 editor.redo(&Default::default(), window, cx);
9744 });
9745 cx.run_until_parked();
9746 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9747
9748 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
9749 let save = editor
9750 .update_in(cx, |editor, window, cx| {
9751 editor.save(
9752 SaveOptions {
9753 format: true,
9754 autosave: false,
9755 },
9756 project.clone(),
9757 window,
9758 cx,
9759 )
9760 })
9761 .unwrap();
9762 cx.executor().start_waiting();
9763 save.await;
9764 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9765 }
9766}
9767
9768#[gpui::test]
9769async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9770 init_test(cx, |_| {});
9771
9772 let cols = 4;
9773 let rows = 10;
9774 let sample_text_1 = sample_text(rows, cols, 'a');
9775 assert_eq!(
9776 sample_text_1,
9777 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9778 );
9779 let sample_text_2 = sample_text(rows, cols, 'l');
9780 assert_eq!(
9781 sample_text_2,
9782 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9783 );
9784 let sample_text_3 = sample_text(rows, cols, 'v');
9785 assert_eq!(
9786 sample_text_3,
9787 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9788 );
9789
9790 let fs = FakeFs::new(cx.executor());
9791 fs.insert_tree(
9792 path!("/a"),
9793 json!({
9794 "main.rs": sample_text_1,
9795 "other.rs": sample_text_2,
9796 "lib.rs": sample_text_3,
9797 }),
9798 )
9799 .await;
9800
9801 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9802 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9803 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9804
9805 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9806 language_registry.add(rust_lang());
9807 let mut fake_servers = language_registry.register_fake_lsp(
9808 "Rust",
9809 FakeLspAdapter {
9810 capabilities: lsp::ServerCapabilities {
9811 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9812 ..Default::default()
9813 },
9814 ..Default::default()
9815 },
9816 );
9817
9818 let worktree = project.update(cx, |project, cx| {
9819 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
9820 assert_eq!(worktrees.len(), 1);
9821 worktrees.pop().unwrap()
9822 });
9823 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9824
9825 let buffer_1 = project
9826 .update(cx, |project, cx| {
9827 project.open_buffer((worktree_id, "main.rs"), cx)
9828 })
9829 .await
9830 .unwrap();
9831 let buffer_2 = project
9832 .update(cx, |project, cx| {
9833 project.open_buffer((worktree_id, "other.rs"), cx)
9834 })
9835 .await
9836 .unwrap();
9837 let buffer_3 = project
9838 .update(cx, |project, cx| {
9839 project.open_buffer((worktree_id, "lib.rs"), cx)
9840 })
9841 .await
9842 .unwrap();
9843
9844 let multi_buffer = cx.new(|cx| {
9845 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9846 multi_buffer.push_excerpts(
9847 buffer_1.clone(),
9848 [
9849 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9850 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9851 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9852 ],
9853 cx,
9854 );
9855 multi_buffer.push_excerpts(
9856 buffer_2.clone(),
9857 [
9858 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9859 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9860 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9861 ],
9862 cx,
9863 );
9864 multi_buffer.push_excerpts(
9865 buffer_3.clone(),
9866 [
9867 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9868 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9869 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9870 ],
9871 cx,
9872 );
9873 multi_buffer
9874 });
9875 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
9876 Editor::new(
9877 EditorMode::full(),
9878 multi_buffer,
9879 Some(project.clone()),
9880 window,
9881 cx,
9882 )
9883 });
9884
9885 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9886 editor.change_selections(
9887 SelectionEffects::scroll(Autoscroll::Next),
9888 window,
9889 cx,
9890 |s| s.select_ranges(Some(1..2)),
9891 );
9892 editor.insert("|one|two|three|", window, cx);
9893 });
9894 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9895 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9896 editor.change_selections(
9897 SelectionEffects::scroll(Autoscroll::Next),
9898 window,
9899 cx,
9900 |s| s.select_ranges(Some(60..70)),
9901 );
9902 editor.insert("|four|five|six|", window, cx);
9903 });
9904 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9905
9906 // First two buffers should be edited, but not the third one.
9907 assert_eq!(
9908 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9909 "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}",
9910 );
9911 buffer_1.update(cx, |buffer, _| {
9912 assert!(buffer.is_dirty());
9913 assert_eq!(
9914 buffer.text(),
9915 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
9916 )
9917 });
9918 buffer_2.update(cx, |buffer, _| {
9919 assert!(buffer.is_dirty());
9920 assert_eq!(
9921 buffer.text(),
9922 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
9923 )
9924 });
9925 buffer_3.update(cx, |buffer, _| {
9926 assert!(!buffer.is_dirty());
9927 assert_eq!(buffer.text(), sample_text_3,)
9928 });
9929 cx.executor().run_until_parked();
9930
9931 cx.executor().start_waiting();
9932 let save = multi_buffer_editor
9933 .update_in(cx, |editor, window, cx| {
9934 editor.save(
9935 SaveOptions {
9936 format: true,
9937 autosave: false,
9938 },
9939 project.clone(),
9940 window,
9941 cx,
9942 )
9943 })
9944 .unwrap();
9945
9946 let fake_server = fake_servers.next().await.unwrap();
9947 fake_server
9948 .server
9949 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
9950 Ok(Some(vec![lsp::TextEdit::new(
9951 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9952 format!("[{} formatted]", params.text_document.uri),
9953 )]))
9954 })
9955 .detach();
9956 save.await;
9957
9958 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
9959 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
9960 assert_eq!(
9961 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9962 uri!(
9963 "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}"
9964 ),
9965 );
9966 buffer_1.update(cx, |buffer, _| {
9967 assert!(!buffer.is_dirty());
9968 assert_eq!(
9969 buffer.text(),
9970 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
9971 )
9972 });
9973 buffer_2.update(cx, |buffer, _| {
9974 assert!(!buffer.is_dirty());
9975 assert_eq!(
9976 buffer.text(),
9977 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
9978 )
9979 });
9980 buffer_3.update(cx, |buffer, _| {
9981 assert!(!buffer.is_dirty());
9982 assert_eq!(buffer.text(), sample_text_3,)
9983 });
9984}
9985
9986#[gpui::test]
9987async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
9988 init_test(cx, |_| {});
9989
9990 let fs = FakeFs::new(cx.executor());
9991 fs.insert_tree(
9992 path!("/dir"),
9993 json!({
9994 "file1.rs": "fn main() { println!(\"hello\"); }",
9995 "file2.rs": "fn test() { println!(\"test\"); }",
9996 "file3.rs": "fn other() { println!(\"other\"); }\n",
9997 }),
9998 )
9999 .await;
10000
10001 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
10002 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10003 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
10004
10005 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10006 language_registry.add(rust_lang());
10007
10008 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
10009 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
10010
10011 // Open three buffers
10012 let buffer_1 = project
10013 .update(cx, |project, cx| {
10014 project.open_buffer((worktree_id, "file1.rs"), cx)
10015 })
10016 .await
10017 .unwrap();
10018 let buffer_2 = project
10019 .update(cx, |project, cx| {
10020 project.open_buffer((worktree_id, "file2.rs"), cx)
10021 })
10022 .await
10023 .unwrap();
10024 let buffer_3 = project
10025 .update(cx, |project, cx| {
10026 project.open_buffer((worktree_id, "file3.rs"), cx)
10027 })
10028 .await
10029 .unwrap();
10030
10031 // Create a multi-buffer with all three buffers
10032 let multi_buffer = cx.new(|cx| {
10033 let mut multi_buffer = MultiBuffer::new(ReadWrite);
10034 multi_buffer.push_excerpts(
10035 buffer_1.clone(),
10036 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
10037 cx,
10038 );
10039 multi_buffer.push_excerpts(
10040 buffer_2.clone(),
10041 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
10042 cx,
10043 );
10044 multi_buffer.push_excerpts(
10045 buffer_3.clone(),
10046 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
10047 cx,
10048 );
10049 multi_buffer
10050 });
10051
10052 let editor = cx.new_window_entity(|window, cx| {
10053 Editor::new(
10054 EditorMode::full(),
10055 multi_buffer,
10056 Some(project.clone()),
10057 window,
10058 cx,
10059 )
10060 });
10061
10062 // Edit only the first buffer
10063 editor.update_in(cx, |editor, window, cx| {
10064 editor.change_selections(
10065 SelectionEffects::scroll(Autoscroll::Next),
10066 window,
10067 cx,
10068 |s| s.select_ranges(Some(10..10)),
10069 );
10070 editor.insert("// edited", window, cx);
10071 });
10072
10073 // Verify that only buffer 1 is dirty
10074 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
10075 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10076 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10077
10078 // Get write counts after file creation (files were created with initial content)
10079 // We expect each file to have been written once during creation
10080 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
10081 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
10082 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
10083
10084 // Perform autosave
10085 let save_task = editor.update_in(cx, |editor, window, cx| {
10086 editor.save(
10087 SaveOptions {
10088 format: true,
10089 autosave: true,
10090 },
10091 project.clone(),
10092 window,
10093 cx,
10094 )
10095 });
10096 save_task.await.unwrap();
10097
10098 // Only the dirty buffer should have been saved
10099 assert_eq!(
10100 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
10101 1,
10102 "Buffer 1 was dirty, so it should have been written once during autosave"
10103 );
10104 assert_eq!(
10105 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
10106 0,
10107 "Buffer 2 was clean, so it should not have been written during autosave"
10108 );
10109 assert_eq!(
10110 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
10111 0,
10112 "Buffer 3 was clean, so it should not have been written during autosave"
10113 );
10114
10115 // Verify buffer states after autosave
10116 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10117 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10118 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10119
10120 // Now perform a manual save (format = true)
10121 let save_task = editor.update_in(cx, |editor, window, cx| {
10122 editor.save(
10123 SaveOptions {
10124 format: true,
10125 autosave: false,
10126 },
10127 project.clone(),
10128 window,
10129 cx,
10130 )
10131 });
10132 save_task.await.unwrap();
10133
10134 // During manual save, clean buffers don't get written to disk
10135 // They just get did_save called for language server notifications
10136 assert_eq!(
10137 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
10138 1,
10139 "Buffer 1 should only have been written once total (during autosave, not manual save)"
10140 );
10141 assert_eq!(
10142 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
10143 0,
10144 "Buffer 2 should not have been written at all"
10145 );
10146 assert_eq!(
10147 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
10148 0,
10149 "Buffer 3 should not have been written at all"
10150 );
10151}
10152
10153async fn setup_range_format_test(
10154 cx: &mut TestAppContext,
10155) -> (
10156 Entity<Project>,
10157 Entity<Editor>,
10158 &mut gpui::VisualTestContext,
10159 lsp::FakeLanguageServer,
10160) {
10161 init_test(cx, |_| {});
10162
10163 let fs = FakeFs::new(cx.executor());
10164 fs.insert_file(path!("/file.rs"), Default::default()).await;
10165
10166 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10167
10168 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10169 language_registry.add(rust_lang());
10170 let mut fake_servers = language_registry.register_fake_lsp(
10171 "Rust",
10172 FakeLspAdapter {
10173 capabilities: lsp::ServerCapabilities {
10174 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
10175 ..lsp::ServerCapabilities::default()
10176 },
10177 ..FakeLspAdapter::default()
10178 },
10179 );
10180
10181 let buffer = project
10182 .update(cx, |project, cx| {
10183 project.open_local_buffer(path!("/file.rs"), cx)
10184 })
10185 .await
10186 .unwrap();
10187
10188 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10189 let (editor, cx) = cx.add_window_view(|window, cx| {
10190 build_editor_with_project(project.clone(), buffer, window, cx)
10191 });
10192
10193 cx.executor().start_waiting();
10194 let fake_server = fake_servers.next().await.unwrap();
10195
10196 (project, editor, cx, fake_server)
10197}
10198
10199#[gpui::test]
10200async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
10201 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10202
10203 editor.update_in(cx, |editor, window, cx| {
10204 editor.set_text("one\ntwo\nthree\n", window, cx)
10205 });
10206 assert!(cx.read(|cx| editor.is_dirty(cx)));
10207
10208 let save = editor
10209 .update_in(cx, |editor, window, cx| {
10210 editor.save(
10211 SaveOptions {
10212 format: true,
10213 autosave: false,
10214 },
10215 project.clone(),
10216 window,
10217 cx,
10218 )
10219 })
10220 .unwrap();
10221 fake_server
10222 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10223 assert_eq!(
10224 params.text_document.uri,
10225 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10226 );
10227 assert_eq!(params.options.tab_size, 4);
10228 Ok(Some(vec![lsp::TextEdit::new(
10229 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10230 ", ".to_string(),
10231 )]))
10232 })
10233 .next()
10234 .await;
10235 cx.executor().start_waiting();
10236 save.await;
10237 assert_eq!(
10238 editor.update(cx, |editor, cx| editor.text(cx)),
10239 "one, two\nthree\n"
10240 );
10241 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10242}
10243
10244#[gpui::test]
10245async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
10246 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10247
10248 editor.update_in(cx, |editor, window, cx| {
10249 editor.set_text("one\ntwo\nthree\n", window, cx)
10250 });
10251 assert!(cx.read(|cx| editor.is_dirty(cx)));
10252
10253 // Test that save still works when formatting hangs
10254 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
10255 move |params, _| async move {
10256 assert_eq!(
10257 params.text_document.uri,
10258 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10259 );
10260 futures::future::pending::<()>().await;
10261 unreachable!()
10262 },
10263 );
10264 let save = editor
10265 .update_in(cx, |editor, window, cx| {
10266 editor.save(
10267 SaveOptions {
10268 format: true,
10269 autosave: false,
10270 },
10271 project.clone(),
10272 window,
10273 cx,
10274 )
10275 })
10276 .unwrap();
10277 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10278 cx.executor().start_waiting();
10279 save.await;
10280 assert_eq!(
10281 editor.update(cx, |editor, cx| editor.text(cx)),
10282 "one\ntwo\nthree\n"
10283 );
10284 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10285}
10286
10287#[gpui::test]
10288async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
10289 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10290
10291 // Buffer starts clean, no formatting should be requested
10292 let save = editor
10293 .update_in(cx, |editor, window, cx| {
10294 editor.save(
10295 SaveOptions {
10296 format: false,
10297 autosave: false,
10298 },
10299 project.clone(),
10300 window,
10301 cx,
10302 )
10303 })
10304 .unwrap();
10305 let _pending_format_request = fake_server
10306 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
10307 panic!("Should not be invoked");
10308 })
10309 .next();
10310 cx.executor().start_waiting();
10311 save.await;
10312 cx.run_until_parked();
10313}
10314
10315#[gpui::test]
10316async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
10317 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10318
10319 // Set Rust language override and assert overridden tabsize is sent to language server
10320 update_test_language_settings(cx, |settings| {
10321 settings.languages.0.insert(
10322 "Rust".into(),
10323 LanguageSettingsContent {
10324 tab_size: NonZeroU32::new(8),
10325 ..Default::default()
10326 },
10327 );
10328 });
10329
10330 editor.update_in(cx, |editor, window, cx| {
10331 editor.set_text("something_new\n", window, cx)
10332 });
10333 assert!(cx.read(|cx| editor.is_dirty(cx)));
10334 let save = editor
10335 .update_in(cx, |editor, window, cx| {
10336 editor.save(
10337 SaveOptions {
10338 format: true,
10339 autosave: false,
10340 },
10341 project.clone(),
10342 window,
10343 cx,
10344 )
10345 })
10346 .unwrap();
10347 fake_server
10348 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10349 assert_eq!(
10350 params.text_document.uri,
10351 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10352 );
10353 assert_eq!(params.options.tab_size, 8);
10354 Ok(Some(Vec::new()))
10355 })
10356 .next()
10357 .await;
10358 save.await;
10359}
10360
10361#[gpui::test]
10362async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
10363 init_test(cx, |settings| {
10364 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
10365 Formatter::LanguageServer { name: None },
10366 )))
10367 });
10368
10369 let fs = FakeFs::new(cx.executor());
10370 fs.insert_file(path!("/file.rs"), Default::default()).await;
10371
10372 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10373
10374 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10375 language_registry.add(Arc::new(Language::new(
10376 LanguageConfig {
10377 name: "Rust".into(),
10378 matcher: LanguageMatcher {
10379 path_suffixes: vec!["rs".to_string()],
10380 ..Default::default()
10381 },
10382 ..LanguageConfig::default()
10383 },
10384 Some(tree_sitter_rust::LANGUAGE.into()),
10385 )));
10386 update_test_language_settings(cx, |settings| {
10387 // Enable Prettier formatting for the same buffer, and ensure
10388 // LSP is called instead of Prettier.
10389 settings.defaults.prettier = Some(PrettierSettings {
10390 allowed: true,
10391 ..PrettierSettings::default()
10392 });
10393 });
10394 let mut fake_servers = language_registry.register_fake_lsp(
10395 "Rust",
10396 FakeLspAdapter {
10397 capabilities: lsp::ServerCapabilities {
10398 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10399 ..Default::default()
10400 },
10401 ..Default::default()
10402 },
10403 );
10404
10405 let buffer = project
10406 .update(cx, |project, cx| {
10407 project.open_local_buffer(path!("/file.rs"), cx)
10408 })
10409 .await
10410 .unwrap();
10411
10412 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10413 let (editor, cx) = cx.add_window_view(|window, cx| {
10414 build_editor_with_project(project.clone(), buffer, window, cx)
10415 });
10416 editor.update_in(cx, |editor, window, cx| {
10417 editor.set_text("one\ntwo\nthree\n", window, cx)
10418 });
10419
10420 cx.executor().start_waiting();
10421 let fake_server = fake_servers.next().await.unwrap();
10422
10423 let format = editor
10424 .update_in(cx, |editor, window, cx| {
10425 editor.perform_format(
10426 project.clone(),
10427 FormatTrigger::Manual,
10428 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10429 window,
10430 cx,
10431 )
10432 })
10433 .unwrap();
10434 fake_server
10435 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10436 assert_eq!(
10437 params.text_document.uri,
10438 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10439 );
10440 assert_eq!(params.options.tab_size, 4);
10441 Ok(Some(vec![lsp::TextEdit::new(
10442 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10443 ", ".to_string(),
10444 )]))
10445 })
10446 .next()
10447 .await;
10448 cx.executor().start_waiting();
10449 format.await;
10450 assert_eq!(
10451 editor.update(cx, |editor, cx| editor.text(cx)),
10452 "one, two\nthree\n"
10453 );
10454
10455 editor.update_in(cx, |editor, window, cx| {
10456 editor.set_text("one\ntwo\nthree\n", window, cx)
10457 });
10458 // Ensure we don't lock if formatting hangs.
10459 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10460 move |params, _| async move {
10461 assert_eq!(
10462 params.text_document.uri,
10463 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10464 );
10465 futures::future::pending::<()>().await;
10466 unreachable!()
10467 },
10468 );
10469 let format = editor
10470 .update_in(cx, |editor, window, cx| {
10471 editor.perform_format(
10472 project,
10473 FormatTrigger::Manual,
10474 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10475 window,
10476 cx,
10477 )
10478 })
10479 .unwrap();
10480 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10481 cx.executor().start_waiting();
10482 format.await;
10483 assert_eq!(
10484 editor.update(cx, |editor, cx| editor.text(cx)),
10485 "one\ntwo\nthree\n"
10486 );
10487}
10488
10489#[gpui::test]
10490async fn test_multiple_formatters(cx: &mut TestAppContext) {
10491 init_test(cx, |settings| {
10492 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
10493 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10494 Formatter::LanguageServer { name: None },
10495 Formatter::CodeActions(
10496 [
10497 ("code-action-1".into(), true),
10498 ("code-action-2".into(), true),
10499 ]
10500 .into_iter()
10501 .collect(),
10502 ),
10503 ])))
10504 });
10505
10506 let fs = FakeFs::new(cx.executor());
10507 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
10508 .await;
10509
10510 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10511 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10512 language_registry.add(rust_lang());
10513
10514 let mut fake_servers = language_registry.register_fake_lsp(
10515 "Rust",
10516 FakeLspAdapter {
10517 capabilities: lsp::ServerCapabilities {
10518 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10519 execute_command_provider: Some(lsp::ExecuteCommandOptions {
10520 commands: vec!["the-command-for-code-action-1".into()],
10521 ..Default::default()
10522 }),
10523 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10524 ..Default::default()
10525 },
10526 ..Default::default()
10527 },
10528 );
10529
10530 let buffer = project
10531 .update(cx, |project, cx| {
10532 project.open_local_buffer(path!("/file.rs"), cx)
10533 })
10534 .await
10535 .unwrap();
10536
10537 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10538 let (editor, cx) = cx.add_window_view(|window, cx| {
10539 build_editor_with_project(project.clone(), buffer, window, cx)
10540 });
10541
10542 cx.executor().start_waiting();
10543
10544 let fake_server = fake_servers.next().await.unwrap();
10545 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10546 move |_params, _| async move {
10547 Ok(Some(vec![lsp::TextEdit::new(
10548 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10549 "applied-formatting\n".to_string(),
10550 )]))
10551 },
10552 );
10553 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10554 move |params, _| async move {
10555 assert_eq!(
10556 params.context.only,
10557 Some(vec!["code-action-1".into(), "code-action-2".into()])
10558 );
10559 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
10560 Ok(Some(vec![
10561 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10562 kind: Some("code-action-1".into()),
10563 edit: Some(lsp::WorkspaceEdit::new(
10564 [(
10565 uri.clone(),
10566 vec![lsp::TextEdit::new(
10567 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10568 "applied-code-action-1-edit\n".to_string(),
10569 )],
10570 )]
10571 .into_iter()
10572 .collect(),
10573 )),
10574 command: Some(lsp::Command {
10575 command: "the-command-for-code-action-1".into(),
10576 ..Default::default()
10577 }),
10578 ..Default::default()
10579 }),
10580 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10581 kind: Some("code-action-2".into()),
10582 edit: Some(lsp::WorkspaceEdit::new(
10583 [(
10584 uri.clone(),
10585 vec![lsp::TextEdit::new(
10586 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10587 "applied-code-action-2-edit\n".to_string(),
10588 )],
10589 )]
10590 .into_iter()
10591 .collect(),
10592 )),
10593 ..Default::default()
10594 }),
10595 ]))
10596 },
10597 );
10598
10599 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
10600 move |params, _| async move { Ok(params) }
10601 });
10602
10603 let command_lock = Arc::new(futures::lock::Mutex::new(()));
10604 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
10605 let fake = fake_server.clone();
10606 let lock = command_lock.clone();
10607 move |params, _| {
10608 assert_eq!(params.command, "the-command-for-code-action-1");
10609 let fake = fake.clone();
10610 let lock = lock.clone();
10611 async move {
10612 lock.lock().await;
10613 fake.server
10614 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
10615 label: None,
10616 edit: lsp::WorkspaceEdit {
10617 changes: Some(
10618 [(
10619 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
10620 vec![lsp::TextEdit {
10621 range: lsp::Range::new(
10622 lsp::Position::new(0, 0),
10623 lsp::Position::new(0, 0),
10624 ),
10625 new_text: "applied-code-action-1-command\n".into(),
10626 }],
10627 )]
10628 .into_iter()
10629 .collect(),
10630 ),
10631 ..Default::default()
10632 },
10633 })
10634 .await
10635 .into_response()
10636 .unwrap();
10637 Ok(Some(json!(null)))
10638 }
10639 }
10640 });
10641
10642 cx.executor().start_waiting();
10643 editor
10644 .update_in(cx, |editor, window, cx| {
10645 editor.perform_format(
10646 project.clone(),
10647 FormatTrigger::Manual,
10648 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10649 window,
10650 cx,
10651 )
10652 })
10653 .unwrap()
10654 .await;
10655 editor.update(cx, |editor, cx| {
10656 assert_eq!(
10657 editor.text(cx),
10658 r#"
10659 applied-code-action-2-edit
10660 applied-code-action-1-command
10661 applied-code-action-1-edit
10662 applied-formatting
10663 one
10664 two
10665 three
10666 "#
10667 .unindent()
10668 );
10669 });
10670
10671 editor.update_in(cx, |editor, window, cx| {
10672 editor.undo(&Default::default(), window, cx);
10673 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10674 });
10675
10676 // Perform a manual edit while waiting for an LSP command
10677 // that's being run as part of a formatting code action.
10678 let lock_guard = command_lock.lock().await;
10679 let format = editor
10680 .update_in(cx, |editor, window, cx| {
10681 editor.perform_format(
10682 project.clone(),
10683 FormatTrigger::Manual,
10684 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10685 window,
10686 cx,
10687 )
10688 })
10689 .unwrap();
10690 cx.run_until_parked();
10691 editor.update(cx, |editor, cx| {
10692 assert_eq!(
10693 editor.text(cx),
10694 r#"
10695 applied-code-action-1-edit
10696 applied-formatting
10697 one
10698 two
10699 three
10700 "#
10701 .unindent()
10702 );
10703
10704 editor.buffer.update(cx, |buffer, cx| {
10705 let ix = buffer.len(cx);
10706 buffer.edit([(ix..ix, "edited\n")], None, cx);
10707 });
10708 });
10709
10710 // Allow the LSP command to proceed. Because the buffer was edited,
10711 // the second code action will not be run.
10712 drop(lock_guard);
10713 format.await;
10714 editor.update_in(cx, |editor, window, cx| {
10715 assert_eq!(
10716 editor.text(cx),
10717 r#"
10718 applied-code-action-1-command
10719 applied-code-action-1-edit
10720 applied-formatting
10721 one
10722 two
10723 three
10724 edited
10725 "#
10726 .unindent()
10727 );
10728
10729 // The manual edit is undone first, because it is the last thing the user did
10730 // (even though the command completed afterwards).
10731 editor.undo(&Default::default(), window, cx);
10732 assert_eq!(
10733 editor.text(cx),
10734 r#"
10735 applied-code-action-1-command
10736 applied-code-action-1-edit
10737 applied-formatting
10738 one
10739 two
10740 three
10741 "#
10742 .unindent()
10743 );
10744
10745 // All the formatting (including the command, which completed after the manual edit)
10746 // is undone together.
10747 editor.undo(&Default::default(), window, cx);
10748 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10749 });
10750}
10751
10752#[gpui::test]
10753async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
10754 init_test(cx, |settings| {
10755 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10756 Formatter::LanguageServer { name: None },
10757 ])))
10758 });
10759
10760 let fs = FakeFs::new(cx.executor());
10761 fs.insert_file(path!("/file.ts"), Default::default()).await;
10762
10763 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10764
10765 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10766 language_registry.add(Arc::new(Language::new(
10767 LanguageConfig {
10768 name: "TypeScript".into(),
10769 matcher: LanguageMatcher {
10770 path_suffixes: vec!["ts".to_string()],
10771 ..Default::default()
10772 },
10773 ..LanguageConfig::default()
10774 },
10775 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10776 )));
10777 update_test_language_settings(cx, |settings| {
10778 settings.defaults.prettier = Some(PrettierSettings {
10779 allowed: true,
10780 ..PrettierSettings::default()
10781 });
10782 });
10783 let mut fake_servers = language_registry.register_fake_lsp(
10784 "TypeScript",
10785 FakeLspAdapter {
10786 capabilities: lsp::ServerCapabilities {
10787 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10788 ..Default::default()
10789 },
10790 ..Default::default()
10791 },
10792 );
10793
10794 let buffer = project
10795 .update(cx, |project, cx| {
10796 project.open_local_buffer(path!("/file.ts"), cx)
10797 })
10798 .await
10799 .unwrap();
10800
10801 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10802 let (editor, cx) = cx.add_window_view(|window, cx| {
10803 build_editor_with_project(project.clone(), buffer, window, cx)
10804 });
10805 editor.update_in(cx, |editor, window, cx| {
10806 editor.set_text(
10807 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10808 window,
10809 cx,
10810 )
10811 });
10812
10813 cx.executor().start_waiting();
10814 let fake_server = fake_servers.next().await.unwrap();
10815
10816 let format = editor
10817 .update_in(cx, |editor, window, cx| {
10818 editor.perform_code_action_kind(
10819 project.clone(),
10820 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10821 window,
10822 cx,
10823 )
10824 })
10825 .unwrap();
10826 fake_server
10827 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
10828 assert_eq!(
10829 params.text_document.uri,
10830 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10831 );
10832 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
10833 lsp::CodeAction {
10834 title: "Organize Imports".to_string(),
10835 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
10836 edit: Some(lsp::WorkspaceEdit {
10837 changes: Some(
10838 [(
10839 params.text_document.uri.clone(),
10840 vec![lsp::TextEdit::new(
10841 lsp::Range::new(
10842 lsp::Position::new(1, 0),
10843 lsp::Position::new(2, 0),
10844 ),
10845 "".to_string(),
10846 )],
10847 )]
10848 .into_iter()
10849 .collect(),
10850 ),
10851 ..Default::default()
10852 }),
10853 ..Default::default()
10854 },
10855 )]))
10856 })
10857 .next()
10858 .await;
10859 cx.executor().start_waiting();
10860 format.await;
10861 assert_eq!(
10862 editor.update(cx, |editor, cx| editor.text(cx)),
10863 "import { a } from 'module';\n\nconst x = a;\n"
10864 );
10865
10866 editor.update_in(cx, |editor, window, cx| {
10867 editor.set_text(
10868 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10869 window,
10870 cx,
10871 )
10872 });
10873 // Ensure we don't lock if code action hangs.
10874 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10875 move |params, _| async move {
10876 assert_eq!(
10877 params.text_document.uri,
10878 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10879 );
10880 futures::future::pending::<()>().await;
10881 unreachable!()
10882 },
10883 );
10884 let format = editor
10885 .update_in(cx, |editor, window, cx| {
10886 editor.perform_code_action_kind(
10887 project,
10888 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10889 window,
10890 cx,
10891 )
10892 })
10893 .unwrap();
10894 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
10895 cx.executor().start_waiting();
10896 format.await;
10897 assert_eq!(
10898 editor.update(cx, |editor, cx| editor.text(cx)),
10899 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
10900 );
10901}
10902
10903#[gpui::test]
10904async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
10905 init_test(cx, |_| {});
10906
10907 let mut cx = EditorLspTestContext::new_rust(
10908 lsp::ServerCapabilities {
10909 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10910 ..Default::default()
10911 },
10912 cx,
10913 )
10914 .await;
10915
10916 cx.set_state(indoc! {"
10917 one.twoˇ
10918 "});
10919
10920 // The format request takes a long time. When it completes, it inserts
10921 // a newline and an indent before the `.`
10922 cx.lsp
10923 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
10924 let executor = cx.background_executor().clone();
10925 async move {
10926 executor.timer(Duration::from_millis(100)).await;
10927 Ok(Some(vec![lsp::TextEdit {
10928 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
10929 new_text: "\n ".into(),
10930 }]))
10931 }
10932 });
10933
10934 // Submit a format request.
10935 let format_1 = cx
10936 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10937 .unwrap();
10938 cx.executor().run_until_parked();
10939
10940 // Submit a second format request.
10941 let format_2 = cx
10942 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10943 .unwrap();
10944 cx.executor().run_until_parked();
10945
10946 // Wait for both format requests to complete
10947 cx.executor().advance_clock(Duration::from_millis(200));
10948 cx.executor().start_waiting();
10949 format_1.await.unwrap();
10950 cx.executor().start_waiting();
10951 format_2.await.unwrap();
10952
10953 // The formatting edits only happens once.
10954 cx.assert_editor_state(indoc! {"
10955 one
10956 .twoˇ
10957 "});
10958}
10959
10960#[gpui::test]
10961async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
10962 init_test(cx, |settings| {
10963 settings.defaults.formatter = Some(SelectedFormatter::Auto)
10964 });
10965
10966 let mut cx = EditorLspTestContext::new_rust(
10967 lsp::ServerCapabilities {
10968 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10969 ..Default::default()
10970 },
10971 cx,
10972 )
10973 .await;
10974
10975 // Set up a buffer white some trailing whitespace and no trailing newline.
10976 cx.set_state(
10977 &[
10978 "one ", //
10979 "twoˇ", //
10980 "three ", //
10981 "four", //
10982 ]
10983 .join("\n"),
10984 );
10985
10986 // Submit a format request.
10987 let format = cx
10988 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10989 .unwrap();
10990
10991 // Record which buffer changes have been sent to the language server
10992 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
10993 cx.lsp
10994 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
10995 let buffer_changes = buffer_changes.clone();
10996 move |params, _| {
10997 buffer_changes.lock().extend(
10998 params
10999 .content_changes
11000 .into_iter()
11001 .map(|e| (e.range.unwrap(), e.text)),
11002 );
11003 }
11004 });
11005
11006 // Handle formatting requests to the language server.
11007 cx.lsp
11008 .set_request_handler::<lsp::request::Formatting, _, _>({
11009 let buffer_changes = buffer_changes.clone();
11010 move |_, _| {
11011 // When formatting is requested, trailing whitespace has already been stripped,
11012 // and the trailing newline has already been added.
11013 assert_eq!(
11014 &buffer_changes.lock()[1..],
11015 &[
11016 (
11017 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
11018 "".into()
11019 ),
11020 (
11021 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
11022 "".into()
11023 ),
11024 (
11025 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
11026 "\n".into()
11027 ),
11028 ]
11029 );
11030
11031 // Insert blank lines between each line of the buffer.
11032 async move {
11033 Ok(Some(vec![
11034 lsp::TextEdit {
11035 range: lsp::Range::new(
11036 lsp::Position::new(1, 0),
11037 lsp::Position::new(1, 0),
11038 ),
11039 new_text: "\n".into(),
11040 },
11041 lsp::TextEdit {
11042 range: lsp::Range::new(
11043 lsp::Position::new(2, 0),
11044 lsp::Position::new(2, 0),
11045 ),
11046 new_text: "\n".into(),
11047 },
11048 ]))
11049 }
11050 }
11051 });
11052
11053 // After formatting the buffer, the trailing whitespace is stripped,
11054 // a newline is appended, and the edits provided by the language server
11055 // have been applied.
11056 format.await.unwrap();
11057 cx.assert_editor_state(
11058 &[
11059 "one", //
11060 "", //
11061 "twoˇ", //
11062 "", //
11063 "three", //
11064 "four", //
11065 "", //
11066 ]
11067 .join("\n"),
11068 );
11069
11070 // Undoing the formatting undoes the trailing whitespace removal, the
11071 // trailing newline, and the LSP edits.
11072 cx.update_buffer(|buffer, cx| buffer.undo(cx));
11073 cx.assert_editor_state(
11074 &[
11075 "one ", //
11076 "twoˇ", //
11077 "three ", //
11078 "four", //
11079 ]
11080 .join("\n"),
11081 );
11082}
11083
11084#[gpui::test]
11085async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
11086 cx: &mut TestAppContext,
11087) {
11088 init_test(cx, |_| {});
11089
11090 cx.update(|cx| {
11091 cx.update_global::<SettingsStore, _>(|settings, cx| {
11092 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11093 settings.auto_signature_help = Some(true);
11094 });
11095 });
11096 });
11097
11098 let mut cx = EditorLspTestContext::new_rust(
11099 lsp::ServerCapabilities {
11100 signature_help_provider: Some(lsp::SignatureHelpOptions {
11101 ..Default::default()
11102 }),
11103 ..Default::default()
11104 },
11105 cx,
11106 )
11107 .await;
11108
11109 let language = Language::new(
11110 LanguageConfig {
11111 name: "Rust".into(),
11112 brackets: BracketPairConfig {
11113 pairs: vec![
11114 BracketPair {
11115 start: "{".to_string(),
11116 end: "}".to_string(),
11117 close: true,
11118 surround: true,
11119 newline: true,
11120 },
11121 BracketPair {
11122 start: "(".to_string(),
11123 end: ")".to_string(),
11124 close: true,
11125 surround: true,
11126 newline: true,
11127 },
11128 BracketPair {
11129 start: "/*".to_string(),
11130 end: " */".to_string(),
11131 close: true,
11132 surround: true,
11133 newline: true,
11134 },
11135 BracketPair {
11136 start: "[".to_string(),
11137 end: "]".to_string(),
11138 close: false,
11139 surround: false,
11140 newline: true,
11141 },
11142 BracketPair {
11143 start: "\"".to_string(),
11144 end: "\"".to_string(),
11145 close: true,
11146 surround: true,
11147 newline: false,
11148 },
11149 BracketPair {
11150 start: "<".to_string(),
11151 end: ">".to_string(),
11152 close: false,
11153 surround: true,
11154 newline: true,
11155 },
11156 ],
11157 ..Default::default()
11158 },
11159 autoclose_before: "})]".to_string(),
11160 ..Default::default()
11161 },
11162 Some(tree_sitter_rust::LANGUAGE.into()),
11163 );
11164 let language = Arc::new(language);
11165
11166 cx.language_registry().add(language.clone());
11167 cx.update_buffer(|buffer, cx| {
11168 buffer.set_language(Some(language), cx);
11169 });
11170
11171 cx.set_state(
11172 &r#"
11173 fn main() {
11174 sampleˇ
11175 }
11176 "#
11177 .unindent(),
11178 );
11179
11180 cx.update_editor(|editor, window, cx| {
11181 editor.handle_input("(", window, cx);
11182 });
11183 cx.assert_editor_state(
11184 &"
11185 fn main() {
11186 sample(ˇ)
11187 }
11188 "
11189 .unindent(),
11190 );
11191
11192 let mocked_response = lsp::SignatureHelp {
11193 signatures: vec![lsp::SignatureInformation {
11194 label: "fn sample(param1: u8, param2: u8)".to_string(),
11195 documentation: None,
11196 parameters: Some(vec![
11197 lsp::ParameterInformation {
11198 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11199 documentation: None,
11200 },
11201 lsp::ParameterInformation {
11202 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11203 documentation: None,
11204 },
11205 ]),
11206 active_parameter: None,
11207 }],
11208 active_signature: Some(0),
11209 active_parameter: Some(0),
11210 };
11211 handle_signature_help_request(&mut cx, mocked_response).await;
11212
11213 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11214 .await;
11215
11216 cx.editor(|editor, _, _| {
11217 let signature_help_state = editor.signature_help_state.popover().cloned();
11218 let signature = signature_help_state.unwrap();
11219 assert_eq!(
11220 signature.signatures[signature.current_signature].label,
11221 "fn sample(param1: u8, param2: u8)"
11222 );
11223 });
11224}
11225
11226#[gpui::test]
11227async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
11228 init_test(cx, |_| {});
11229
11230 cx.update(|cx| {
11231 cx.update_global::<SettingsStore, _>(|settings, cx| {
11232 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11233 settings.auto_signature_help = Some(false);
11234 settings.show_signature_help_after_edits = Some(false);
11235 });
11236 });
11237 });
11238
11239 let mut cx = EditorLspTestContext::new_rust(
11240 lsp::ServerCapabilities {
11241 signature_help_provider: Some(lsp::SignatureHelpOptions {
11242 ..Default::default()
11243 }),
11244 ..Default::default()
11245 },
11246 cx,
11247 )
11248 .await;
11249
11250 let language = Language::new(
11251 LanguageConfig {
11252 name: "Rust".into(),
11253 brackets: BracketPairConfig {
11254 pairs: vec![
11255 BracketPair {
11256 start: "{".to_string(),
11257 end: "}".to_string(),
11258 close: true,
11259 surround: true,
11260 newline: true,
11261 },
11262 BracketPair {
11263 start: "(".to_string(),
11264 end: ")".to_string(),
11265 close: true,
11266 surround: true,
11267 newline: true,
11268 },
11269 BracketPair {
11270 start: "/*".to_string(),
11271 end: " */".to_string(),
11272 close: true,
11273 surround: true,
11274 newline: true,
11275 },
11276 BracketPair {
11277 start: "[".to_string(),
11278 end: "]".to_string(),
11279 close: false,
11280 surround: false,
11281 newline: true,
11282 },
11283 BracketPair {
11284 start: "\"".to_string(),
11285 end: "\"".to_string(),
11286 close: true,
11287 surround: true,
11288 newline: false,
11289 },
11290 BracketPair {
11291 start: "<".to_string(),
11292 end: ">".to_string(),
11293 close: false,
11294 surround: true,
11295 newline: true,
11296 },
11297 ],
11298 ..Default::default()
11299 },
11300 autoclose_before: "})]".to_string(),
11301 ..Default::default()
11302 },
11303 Some(tree_sitter_rust::LANGUAGE.into()),
11304 );
11305 let language = Arc::new(language);
11306
11307 cx.language_registry().add(language.clone());
11308 cx.update_buffer(|buffer, cx| {
11309 buffer.set_language(Some(language), cx);
11310 });
11311
11312 // Ensure that signature_help is not called when no signature help is enabled.
11313 cx.set_state(
11314 &r#"
11315 fn main() {
11316 sampleˇ
11317 }
11318 "#
11319 .unindent(),
11320 );
11321 cx.update_editor(|editor, window, cx| {
11322 editor.handle_input("(", window, cx);
11323 });
11324 cx.assert_editor_state(
11325 &"
11326 fn main() {
11327 sample(ˇ)
11328 }
11329 "
11330 .unindent(),
11331 );
11332 cx.editor(|editor, _, _| {
11333 assert!(editor.signature_help_state.task().is_none());
11334 });
11335
11336 let mocked_response = lsp::SignatureHelp {
11337 signatures: vec![lsp::SignatureInformation {
11338 label: "fn sample(param1: u8, param2: u8)".to_string(),
11339 documentation: None,
11340 parameters: Some(vec![
11341 lsp::ParameterInformation {
11342 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11343 documentation: None,
11344 },
11345 lsp::ParameterInformation {
11346 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11347 documentation: None,
11348 },
11349 ]),
11350 active_parameter: None,
11351 }],
11352 active_signature: Some(0),
11353 active_parameter: Some(0),
11354 };
11355
11356 // Ensure that signature_help is called when enabled afte edits
11357 cx.update(|_, cx| {
11358 cx.update_global::<SettingsStore, _>(|settings, cx| {
11359 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11360 settings.auto_signature_help = Some(false);
11361 settings.show_signature_help_after_edits = Some(true);
11362 });
11363 });
11364 });
11365 cx.set_state(
11366 &r#"
11367 fn main() {
11368 sampleˇ
11369 }
11370 "#
11371 .unindent(),
11372 );
11373 cx.update_editor(|editor, window, cx| {
11374 editor.handle_input("(", window, cx);
11375 });
11376 cx.assert_editor_state(
11377 &"
11378 fn main() {
11379 sample(ˇ)
11380 }
11381 "
11382 .unindent(),
11383 );
11384 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11385 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11386 .await;
11387 cx.update_editor(|editor, _, _| {
11388 let signature_help_state = editor.signature_help_state.popover().cloned();
11389 assert!(signature_help_state.is_some());
11390 let signature = signature_help_state.unwrap();
11391 assert_eq!(
11392 signature.signatures[signature.current_signature].label,
11393 "fn sample(param1: u8, param2: u8)"
11394 );
11395 editor.signature_help_state = SignatureHelpState::default();
11396 });
11397
11398 // Ensure that signature_help is called when auto signature help override is enabled
11399 cx.update(|_, cx| {
11400 cx.update_global::<SettingsStore, _>(|settings, cx| {
11401 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11402 settings.auto_signature_help = Some(true);
11403 settings.show_signature_help_after_edits = Some(false);
11404 });
11405 });
11406 });
11407 cx.set_state(
11408 &r#"
11409 fn main() {
11410 sampleˇ
11411 }
11412 "#
11413 .unindent(),
11414 );
11415 cx.update_editor(|editor, window, cx| {
11416 editor.handle_input("(", window, cx);
11417 });
11418 cx.assert_editor_state(
11419 &"
11420 fn main() {
11421 sample(ˇ)
11422 }
11423 "
11424 .unindent(),
11425 );
11426 handle_signature_help_request(&mut cx, mocked_response).await;
11427 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11428 .await;
11429 cx.editor(|editor, _, _| {
11430 let signature_help_state = editor.signature_help_state.popover().cloned();
11431 assert!(signature_help_state.is_some());
11432 let signature = signature_help_state.unwrap();
11433 assert_eq!(
11434 signature.signatures[signature.current_signature].label,
11435 "fn sample(param1: u8, param2: u8)"
11436 );
11437 });
11438}
11439
11440#[gpui::test]
11441async fn test_signature_help(cx: &mut TestAppContext) {
11442 init_test(cx, |_| {});
11443 cx.update(|cx| {
11444 cx.update_global::<SettingsStore, _>(|settings, cx| {
11445 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11446 settings.auto_signature_help = Some(true);
11447 });
11448 });
11449 });
11450
11451 let mut cx = EditorLspTestContext::new_rust(
11452 lsp::ServerCapabilities {
11453 signature_help_provider: Some(lsp::SignatureHelpOptions {
11454 ..Default::default()
11455 }),
11456 ..Default::default()
11457 },
11458 cx,
11459 )
11460 .await;
11461
11462 // A test that directly calls `show_signature_help`
11463 cx.update_editor(|editor, window, cx| {
11464 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11465 });
11466
11467 let mocked_response = lsp::SignatureHelp {
11468 signatures: vec![lsp::SignatureInformation {
11469 label: "fn sample(param1: u8, param2: u8)".to_string(),
11470 documentation: None,
11471 parameters: Some(vec![
11472 lsp::ParameterInformation {
11473 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11474 documentation: None,
11475 },
11476 lsp::ParameterInformation {
11477 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11478 documentation: None,
11479 },
11480 ]),
11481 active_parameter: None,
11482 }],
11483 active_signature: Some(0),
11484 active_parameter: Some(0),
11485 };
11486 handle_signature_help_request(&mut cx, mocked_response).await;
11487
11488 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11489 .await;
11490
11491 cx.editor(|editor, _, _| {
11492 let signature_help_state = editor.signature_help_state.popover().cloned();
11493 assert!(signature_help_state.is_some());
11494 let signature = signature_help_state.unwrap();
11495 assert_eq!(
11496 signature.signatures[signature.current_signature].label,
11497 "fn sample(param1: u8, param2: u8)"
11498 );
11499 });
11500
11501 // When exiting outside from inside the brackets, `signature_help` is closed.
11502 cx.set_state(indoc! {"
11503 fn main() {
11504 sample(ˇ);
11505 }
11506
11507 fn sample(param1: u8, param2: u8) {}
11508 "});
11509
11510 cx.update_editor(|editor, window, cx| {
11511 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11512 s.select_ranges([0..0])
11513 });
11514 });
11515
11516 let mocked_response = lsp::SignatureHelp {
11517 signatures: Vec::new(),
11518 active_signature: None,
11519 active_parameter: None,
11520 };
11521 handle_signature_help_request(&mut cx, mocked_response).await;
11522
11523 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11524 .await;
11525
11526 cx.editor(|editor, _, _| {
11527 assert!(!editor.signature_help_state.is_shown());
11528 });
11529
11530 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
11531 cx.set_state(indoc! {"
11532 fn main() {
11533 sample(ˇ);
11534 }
11535
11536 fn sample(param1: u8, param2: u8) {}
11537 "});
11538
11539 let mocked_response = lsp::SignatureHelp {
11540 signatures: vec![lsp::SignatureInformation {
11541 label: "fn sample(param1: u8, param2: u8)".to_string(),
11542 documentation: None,
11543 parameters: Some(vec![
11544 lsp::ParameterInformation {
11545 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11546 documentation: None,
11547 },
11548 lsp::ParameterInformation {
11549 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11550 documentation: None,
11551 },
11552 ]),
11553 active_parameter: None,
11554 }],
11555 active_signature: Some(0),
11556 active_parameter: Some(0),
11557 };
11558 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11559 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11560 .await;
11561 cx.editor(|editor, _, _| {
11562 assert!(editor.signature_help_state.is_shown());
11563 });
11564
11565 // Restore the popover with more parameter input
11566 cx.set_state(indoc! {"
11567 fn main() {
11568 sample(param1, param2ˇ);
11569 }
11570
11571 fn sample(param1: u8, param2: u8) {}
11572 "});
11573
11574 let mocked_response = lsp::SignatureHelp {
11575 signatures: vec![lsp::SignatureInformation {
11576 label: "fn sample(param1: u8, param2: u8)".to_string(),
11577 documentation: None,
11578 parameters: Some(vec![
11579 lsp::ParameterInformation {
11580 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11581 documentation: None,
11582 },
11583 lsp::ParameterInformation {
11584 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11585 documentation: None,
11586 },
11587 ]),
11588 active_parameter: None,
11589 }],
11590 active_signature: Some(0),
11591 active_parameter: Some(1),
11592 };
11593 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11594 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11595 .await;
11596
11597 // When selecting a range, the popover is gone.
11598 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
11599 cx.update_editor(|editor, window, cx| {
11600 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11601 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11602 })
11603 });
11604 cx.assert_editor_state(indoc! {"
11605 fn main() {
11606 sample(param1, «ˇparam2»);
11607 }
11608
11609 fn sample(param1: u8, param2: u8) {}
11610 "});
11611 cx.editor(|editor, _, _| {
11612 assert!(!editor.signature_help_state.is_shown());
11613 });
11614
11615 // When unselecting again, the popover is back if within the brackets.
11616 cx.update_editor(|editor, window, cx| {
11617 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11618 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11619 })
11620 });
11621 cx.assert_editor_state(indoc! {"
11622 fn main() {
11623 sample(param1, ˇparam2);
11624 }
11625
11626 fn sample(param1: u8, param2: u8) {}
11627 "});
11628 handle_signature_help_request(&mut cx, mocked_response).await;
11629 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11630 .await;
11631 cx.editor(|editor, _, _| {
11632 assert!(editor.signature_help_state.is_shown());
11633 });
11634
11635 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
11636 cx.update_editor(|editor, window, cx| {
11637 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11638 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
11639 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11640 })
11641 });
11642 cx.assert_editor_state(indoc! {"
11643 fn main() {
11644 sample(param1, ˇparam2);
11645 }
11646
11647 fn sample(param1: u8, param2: u8) {}
11648 "});
11649
11650 let mocked_response = lsp::SignatureHelp {
11651 signatures: vec![lsp::SignatureInformation {
11652 label: "fn sample(param1: u8, param2: u8)".to_string(),
11653 documentation: None,
11654 parameters: Some(vec![
11655 lsp::ParameterInformation {
11656 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11657 documentation: None,
11658 },
11659 lsp::ParameterInformation {
11660 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11661 documentation: None,
11662 },
11663 ]),
11664 active_parameter: None,
11665 }],
11666 active_signature: Some(0),
11667 active_parameter: Some(1),
11668 };
11669 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11670 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11671 .await;
11672 cx.update_editor(|editor, _, cx| {
11673 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
11674 });
11675 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11676 .await;
11677 cx.update_editor(|editor, window, cx| {
11678 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11679 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11680 })
11681 });
11682 cx.assert_editor_state(indoc! {"
11683 fn main() {
11684 sample(param1, «ˇparam2»);
11685 }
11686
11687 fn sample(param1: u8, param2: u8) {}
11688 "});
11689 cx.update_editor(|editor, window, cx| {
11690 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11691 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11692 })
11693 });
11694 cx.assert_editor_state(indoc! {"
11695 fn main() {
11696 sample(param1, ˇparam2);
11697 }
11698
11699 fn sample(param1: u8, param2: u8) {}
11700 "});
11701 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
11702 .await;
11703}
11704
11705#[gpui::test]
11706async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
11707 init_test(cx, |_| {});
11708
11709 let mut cx = EditorLspTestContext::new_rust(
11710 lsp::ServerCapabilities {
11711 signature_help_provider: Some(lsp::SignatureHelpOptions {
11712 ..Default::default()
11713 }),
11714 ..Default::default()
11715 },
11716 cx,
11717 )
11718 .await;
11719
11720 cx.set_state(indoc! {"
11721 fn main() {
11722 overloadedˇ
11723 }
11724 "});
11725
11726 cx.update_editor(|editor, window, cx| {
11727 editor.handle_input("(", window, cx);
11728 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11729 });
11730
11731 // Mock response with 3 signatures
11732 let mocked_response = lsp::SignatureHelp {
11733 signatures: vec![
11734 lsp::SignatureInformation {
11735 label: "fn overloaded(x: i32)".to_string(),
11736 documentation: None,
11737 parameters: Some(vec![lsp::ParameterInformation {
11738 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11739 documentation: None,
11740 }]),
11741 active_parameter: None,
11742 },
11743 lsp::SignatureInformation {
11744 label: "fn overloaded(x: i32, y: i32)".to_string(),
11745 documentation: None,
11746 parameters: Some(vec![
11747 lsp::ParameterInformation {
11748 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11749 documentation: None,
11750 },
11751 lsp::ParameterInformation {
11752 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11753 documentation: None,
11754 },
11755 ]),
11756 active_parameter: None,
11757 },
11758 lsp::SignatureInformation {
11759 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
11760 documentation: None,
11761 parameters: Some(vec![
11762 lsp::ParameterInformation {
11763 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11764 documentation: None,
11765 },
11766 lsp::ParameterInformation {
11767 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11768 documentation: None,
11769 },
11770 lsp::ParameterInformation {
11771 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
11772 documentation: None,
11773 },
11774 ]),
11775 active_parameter: None,
11776 },
11777 ],
11778 active_signature: Some(1),
11779 active_parameter: Some(0),
11780 };
11781 handle_signature_help_request(&mut cx, mocked_response).await;
11782
11783 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11784 .await;
11785
11786 // Verify we have multiple signatures and the right one is selected
11787 cx.editor(|editor, _, _| {
11788 let popover = editor.signature_help_state.popover().cloned().unwrap();
11789 assert_eq!(popover.signatures.len(), 3);
11790 // active_signature was 1, so that should be the current
11791 assert_eq!(popover.current_signature, 1);
11792 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
11793 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
11794 assert_eq!(
11795 popover.signatures[2].label,
11796 "fn overloaded(x: i32, y: i32, z: i32)"
11797 );
11798 });
11799
11800 // Test navigation functionality
11801 cx.update_editor(|editor, window, cx| {
11802 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11803 });
11804
11805 cx.editor(|editor, _, _| {
11806 let popover = editor.signature_help_state.popover().cloned().unwrap();
11807 assert_eq!(popover.current_signature, 2);
11808 });
11809
11810 // Test wrap around
11811 cx.update_editor(|editor, window, cx| {
11812 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11813 });
11814
11815 cx.editor(|editor, _, _| {
11816 let popover = editor.signature_help_state.popover().cloned().unwrap();
11817 assert_eq!(popover.current_signature, 0);
11818 });
11819
11820 // Test previous navigation
11821 cx.update_editor(|editor, window, cx| {
11822 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
11823 });
11824
11825 cx.editor(|editor, _, _| {
11826 let popover = editor.signature_help_state.popover().cloned().unwrap();
11827 assert_eq!(popover.current_signature, 2);
11828 });
11829}
11830
11831#[gpui::test]
11832async fn test_completion_mode(cx: &mut TestAppContext) {
11833 init_test(cx, |_| {});
11834 let mut cx = EditorLspTestContext::new_rust(
11835 lsp::ServerCapabilities {
11836 completion_provider: Some(lsp::CompletionOptions {
11837 resolve_provider: Some(true),
11838 ..Default::default()
11839 }),
11840 ..Default::default()
11841 },
11842 cx,
11843 )
11844 .await;
11845
11846 struct Run {
11847 run_description: &'static str,
11848 initial_state: String,
11849 buffer_marked_text: String,
11850 completion_label: &'static str,
11851 completion_text: &'static str,
11852 expected_with_insert_mode: String,
11853 expected_with_replace_mode: String,
11854 expected_with_replace_subsequence_mode: String,
11855 expected_with_replace_suffix_mode: String,
11856 }
11857
11858 let runs = [
11859 Run {
11860 run_description: "Start of word matches completion text",
11861 initial_state: "before ediˇ after".into(),
11862 buffer_marked_text: "before <edi|> after".into(),
11863 completion_label: "editor",
11864 completion_text: "editor",
11865 expected_with_insert_mode: "before editorˇ after".into(),
11866 expected_with_replace_mode: "before editorˇ after".into(),
11867 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11868 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11869 },
11870 Run {
11871 run_description: "Accept same text at the middle of the word",
11872 initial_state: "before ediˇtor after".into(),
11873 buffer_marked_text: "before <edi|tor> after".into(),
11874 completion_label: "editor",
11875 completion_text: "editor",
11876 expected_with_insert_mode: "before editorˇtor after".into(),
11877 expected_with_replace_mode: "before editorˇ after".into(),
11878 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11879 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11880 },
11881 Run {
11882 run_description: "End of word matches completion text -- cursor at end",
11883 initial_state: "before torˇ after".into(),
11884 buffer_marked_text: "before <tor|> after".into(),
11885 completion_label: "editor",
11886 completion_text: "editor",
11887 expected_with_insert_mode: "before editorˇ after".into(),
11888 expected_with_replace_mode: "before editorˇ after".into(),
11889 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11890 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11891 },
11892 Run {
11893 run_description: "End of word matches completion text -- cursor at start",
11894 initial_state: "before ˇtor after".into(),
11895 buffer_marked_text: "before <|tor> after".into(),
11896 completion_label: "editor",
11897 completion_text: "editor",
11898 expected_with_insert_mode: "before editorˇtor after".into(),
11899 expected_with_replace_mode: "before editorˇ after".into(),
11900 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11901 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11902 },
11903 Run {
11904 run_description: "Prepend text containing whitespace",
11905 initial_state: "pˇfield: bool".into(),
11906 buffer_marked_text: "<p|field>: bool".into(),
11907 completion_label: "pub ",
11908 completion_text: "pub ",
11909 expected_with_insert_mode: "pub ˇfield: bool".into(),
11910 expected_with_replace_mode: "pub ˇ: bool".into(),
11911 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
11912 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
11913 },
11914 Run {
11915 run_description: "Add element to start of list",
11916 initial_state: "[element_ˇelement_2]".into(),
11917 buffer_marked_text: "[<element_|element_2>]".into(),
11918 completion_label: "element_1",
11919 completion_text: "element_1",
11920 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
11921 expected_with_replace_mode: "[element_1ˇ]".into(),
11922 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
11923 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
11924 },
11925 Run {
11926 run_description: "Add element to start of list -- first and second elements are equal",
11927 initial_state: "[elˇelement]".into(),
11928 buffer_marked_text: "[<el|element>]".into(),
11929 completion_label: "element",
11930 completion_text: "element",
11931 expected_with_insert_mode: "[elementˇelement]".into(),
11932 expected_with_replace_mode: "[elementˇ]".into(),
11933 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
11934 expected_with_replace_suffix_mode: "[elementˇ]".into(),
11935 },
11936 Run {
11937 run_description: "Ends with matching suffix",
11938 initial_state: "SubˇError".into(),
11939 buffer_marked_text: "<Sub|Error>".into(),
11940 completion_label: "SubscriptionError",
11941 completion_text: "SubscriptionError",
11942 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
11943 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11944 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11945 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
11946 },
11947 Run {
11948 run_description: "Suffix is a subsequence -- contiguous",
11949 initial_state: "SubˇErr".into(),
11950 buffer_marked_text: "<Sub|Err>".into(),
11951 completion_label: "SubscriptionError",
11952 completion_text: "SubscriptionError",
11953 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
11954 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11955 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11956 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
11957 },
11958 Run {
11959 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
11960 initial_state: "Suˇscrirr".into(),
11961 buffer_marked_text: "<Su|scrirr>".into(),
11962 completion_label: "SubscriptionError",
11963 completion_text: "SubscriptionError",
11964 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
11965 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11966 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11967 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
11968 },
11969 Run {
11970 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
11971 initial_state: "foo(indˇix)".into(),
11972 buffer_marked_text: "foo(<ind|ix>)".into(),
11973 completion_label: "node_index",
11974 completion_text: "node_index",
11975 expected_with_insert_mode: "foo(node_indexˇix)".into(),
11976 expected_with_replace_mode: "foo(node_indexˇ)".into(),
11977 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
11978 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
11979 },
11980 Run {
11981 run_description: "Replace range ends before cursor - should extend to cursor",
11982 initial_state: "before editˇo after".into(),
11983 buffer_marked_text: "before <{ed}>it|o after".into(),
11984 completion_label: "editor",
11985 completion_text: "editor",
11986 expected_with_insert_mode: "before editorˇo after".into(),
11987 expected_with_replace_mode: "before editorˇo after".into(),
11988 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
11989 expected_with_replace_suffix_mode: "before editorˇo after".into(),
11990 },
11991 Run {
11992 run_description: "Uses label for suffix matching",
11993 initial_state: "before ediˇtor after".into(),
11994 buffer_marked_text: "before <edi|tor> after".into(),
11995 completion_label: "editor",
11996 completion_text: "editor()",
11997 expected_with_insert_mode: "before editor()ˇtor after".into(),
11998 expected_with_replace_mode: "before editor()ˇ after".into(),
11999 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
12000 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
12001 },
12002 Run {
12003 run_description: "Case insensitive subsequence and suffix matching",
12004 initial_state: "before EDiˇtoR after".into(),
12005 buffer_marked_text: "before <EDi|toR> after".into(),
12006 completion_label: "editor",
12007 completion_text: "editor",
12008 expected_with_insert_mode: "before editorˇtoR after".into(),
12009 expected_with_replace_mode: "before editorˇ after".into(),
12010 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12011 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12012 },
12013 ];
12014
12015 for run in runs {
12016 let run_variations = [
12017 (LspInsertMode::Insert, run.expected_with_insert_mode),
12018 (LspInsertMode::Replace, run.expected_with_replace_mode),
12019 (
12020 LspInsertMode::ReplaceSubsequence,
12021 run.expected_with_replace_subsequence_mode,
12022 ),
12023 (
12024 LspInsertMode::ReplaceSuffix,
12025 run.expected_with_replace_suffix_mode,
12026 ),
12027 ];
12028
12029 for (lsp_insert_mode, expected_text) in run_variations {
12030 eprintln!(
12031 "run = {:?}, mode = {lsp_insert_mode:.?}",
12032 run.run_description,
12033 );
12034
12035 update_test_language_settings(&mut cx, |settings| {
12036 settings.defaults.completions = Some(CompletionSettings {
12037 lsp_insert_mode,
12038 words: WordsCompletionMode::Disabled,
12039 lsp: true,
12040 lsp_fetch_timeout_ms: 0,
12041 });
12042 });
12043
12044 cx.set_state(&run.initial_state);
12045 cx.update_editor(|editor, window, cx| {
12046 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12047 });
12048
12049 let counter = Arc::new(AtomicUsize::new(0));
12050 handle_completion_request_with_insert_and_replace(
12051 &mut cx,
12052 &run.buffer_marked_text,
12053 vec![(run.completion_label, run.completion_text)],
12054 counter.clone(),
12055 )
12056 .await;
12057 cx.condition(|editor, _| editor.context_menu_visible())
12058 .await;
12059 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12060
12061 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12062 editor
12063 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12064 .unwrap()
12065 });
12066 cx.assert_editor_state(&expected_text);
12067 handle_resolve_completion_request(&mut cx, None).await;
12068 apply_additional_edits.await.unwrap();
12069 }
12070 }
12071}
12072
12073#[gpui::test]
12074async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
12075 init_test(cx, |_| {});
12076 let mut cx = EditorLspTestContext::new_rust(
12077 lsp::ServerCapabilities {
12078 completion_provider: Some(lsp::CompletionOptions {
12079 resolve_provider: Some(true),
12080 ..Default::default()
12081 }),
12082 ..Default::default()
12083 },
12084 cx,
12085 )
12086 .await;
12087
12088 let initial_state = "SubˇError";
12089 let buffer_marked_text = "<Sub|Error>";
12090 let completion_text = "SubscriptionError";
12091 let expected_with_insert_mode = "SubscriptionErrorˇError";
12092 let expected_with_replace_mode = "SubscriptionErrorˇ";
12093
12094 update_test_language_settings(&mut cx, |settings| {
12095 settings.defaults.completions = Some(CompletionSettings {
12096 words: WordsCompletionMode::Disabled,
12097 // set the opposite here to ensure that the action is overriding the default behavior
12098 lsp_insert_mode: LspInsertMode::Insert,
12099 lsp: true,
12100 lsp_fetch_timeout_ms: 0,
12101 });
12102 });
12103
12104 cx.set_state(initial_state);
12105 cx.update_editor(|editor, window, cx| {
12106 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12107 });
12108
12109 let counter = Arc::new(AtomicUsize::new(0));
12110 handle_completion_request_with_insert_and_replace(
12111 &mut cx,
12112 &buffer_marked_text,
12113 vec![(completion_text, completion_text)],
12114 counter.clone(),
12115 )
12116 .await;
12117 cx.condition(|editor, _| editor.context_menu_visible())
12118 .await;
12119 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12120
12121 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12122 editor
12123 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12124 .unwrap()
12125 });
12126 cx.assert_editor_state(&expected_with_replace_mode);
12127 handle_resolve_completion_request(&mut cx, None).await;
12128 apply_additional_edits.await.unwrap();
12129
12130 update_test_language_settings(&mut cx, |settings| {
12131 settings.defaults.completions = Some(CompletionSettings {
12132 words: WordsCompletionMode::Disabled,
12133 // set the opposite here to ensure that the action is overriding the default behavior
12134 lsp_insert_mode: LspInsertMode::Replace,
12135 lsp: true,
12136 lsp_fetch_timeout_ms: 0,
12137 });
12138 });
12139
12140 cx.set_state(initial_state);
12141 cx.update_editor(|editor, window, cx| {
12142 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12143 });
12144 handle_completion_request_with_insert_and_replace(
12145 &mut cx,
12146 &buffer_marked_text,
12147 vec![(completion_text, completion_text)],
12148 counter.clone(),
12149 )
12150 .await;
12151 cx.condition(|editor, _| editor.context_menu_visible())
12152 .await;
12153 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12154
12155 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12156 editor
12157 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
12158 .unwrap()
12159 });
12160 cx.assert_editor_state(&expected_with_insert_mode);
12161 handle_resolve_completion_request(&mut cx, None).await;
12162 apply_additional_edits.await.unwrap();
12163}
12164
12165#[gpui::test]
12166async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
12167 init_test(cx, |_| {});
12168 let mut cx = EditorLspTestContext::new_rust(
12169 lsp::ServerCapabilities {
12170 completion_provider: Some(lsp::CompletionOptions {
12171 resolve_provider: Some(true),
12172 ..Default::default()
12173 }),
12174 ..Default::default()
12175 },
12176 cx,
12177 )
12178 .await;
12179
12180 // scenario: surrounding text matches completion text
12181 let completion_text = "to_offset";
12182 let initial_state = indoc! {"
12183 1. buf.to_offˇsuffix
12184 2. buf.to_offˇsuf
12185 3. buf.to_offˇfix
12186 4. buf.to_offˇ
12187 5. into_offˇensive
12188 6. ˇsuffix
12189 7. let ˇ //
12190 8. aaˇzz
12191 9. buf.to_off«zzzzzˇ»suffix
12192 10. buf.«ˇzzzzz»suffix
12193 11. to_off«ˇzzzzz»
12194
12195 buf.to_offˇsuffix // newest cursor
12196 "};
12197 let completion_marked_buffer = indoc! {"
12198 1. buf.to_offsuffix
12199 2. buf.to_offsuf
12200 3. buf.to_offfix
12201 4. buf.to_off
12202 5. into_offensive
12203 6. suffix
12204 7. let //
12205 8. aazz
12206 9. buf.to_offzzzzzsuffix
12207 10. buf.zzzzzsuffix
12208 11. to_offzzzzz
12209
12210 buf.<to_off|suffix> // newest cursor
12211 "};
12212 let expected = indoc! {"
12213 1. buf.to_offsetˇ
12214 2. buf.to_offsetˇsuf
12215 3. buf.to_offsetˇfix
12216 4. buf.to_offsetˇ
12217 5. into_offsetˇensive
12218 6. to_offsetˇsuffix
12219 7. let to_offsetˇ //
12220 8. aato_offsetˇzz
12221 9. buf.to_offsetˇ
12222 10. buf.to_offsetˇsuffix
12223 11. to_offsetˇ
12224
12225 buf.to_offsetˇ // newest cursor
12226 "};
12227 cx.set_state(initial_state);
12228 cx.update_editor(|editor, window, cx| {
12229 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12230 });
12231 handle_completion_request_with_insert_and_replace(
12232 &mut cx,
12233 completion_marked_buffer,
12234 vec![(completion_text, completion_text)],
12235 Arc::new(AtomicUsize::new(0)),
12236 )
12237 .await;
12238 cx.condition(|editor, _| editor.context_menu_visible())
12239 .await;
12240 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12241 editor
12242 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12243 .unwrap()
12244 });
12245 cx.assert_editor_state(expected);
12246 handle_resolve_completion_request(&mut cx, None).await;
12247 apply_additional_edits.await.unwrap();
12248
12249 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
12250 let completion_text = "foo_and_bar";
12251 let initial_state = indoc! {"
12252 1. ooanbˇ
12253 2. zooanbˇ
12254 3. ooanbˇz
12255 4. zooanbˇz
12256 5. ooanˇ
12257 6. oanbˇ
12258
12259 ooanbˇ
12260 "};
12261 let completion_marked_buffer = indoc! {"
12262 1. ooanb
12263 2. zooanb
12264 3. ooanbz
12265 4. zooanbz
12266 5. ooan
12267 6. oanb
12268
12269 <ooanb|>
12270 "};
12271 let expected = indoc! {"
12272 1. foo_and_barˇ
12273 2. zfoo_and_barˇ
12274 3. foo_and_barˇz
12275 4. zfoo_and_barˇz
12276 5. ooanfoo_and_barˇ
12277 6. oanbfoo_and_barˇ
12278
12279 foo_and_barˇ
12280 "};
12281 cx.set_state(initial_state);
12282 cx.update_editor(|editor, window, cx| {
12283 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12284 });
12285 handle_completion_request_with_insert_and_replace(
12286 &mut cx,
12287 completion_marked_buffer,
12288 vec![(completion_text, completion_text)],
12289 Arc::new(AtomicUsize::new(0)),
12290 )
12291 .await;
12292 cx.condition(|editor, _| editor.context_menu_visible())
12293 .await;
12294 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12295 editor
12296 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12297 .unwrap()
12298 });
12299 cx.assert_editor_state(expected);
12300 handle_resolve_completion_request(&mut cx, None).await;
12301 apply_additional_edits.await.unwrap();
12302
12303 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
12304 // (expects the same as if it was inserted at the end)
12305 let completion_text = "foo_and_bar";
12306 let initial_state = indoc! {"
12307 1. ooˇanb
12308 2. zooˇanb
12309 3. ooˇanbz
12310 4. zooˇanbz
12311
12312 ooˇanb
12313 "};
12314 let completion_marked_buffer = indoc! {"
12315 1. ooanb
12316 2. zooanb
12317 3. ooanbz
12318 4. zooanbz
12319
12320 <oo|anb>
12321 "};
12322 let expected = indoc! {"
12323 1. foo_and_barˇ
12324 2. zfoo_and_barˇ
12325 3. foo_and_barˇz
12326 4. zfoo_and_barˇz
12327
12328 foo_and_barˇ
12329 "};
12330 cx.set_state(initial_state);
12331 cx.update_editor(|editor, window, cx| {
12332 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12333 });
12334 handle_completion_request_with_insert_and_replace(
12335 &mut cx,
12336 completion_marked_buffer,
12337 vec![(completion_text, completion_text)],
12338 Arc::new(AtomicUsize::new(0)),
12339 )
12340 .await;
12341 cx.condition(|editor, _| editor.context_menu_visible())
12342 .await;
12343 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12344 editor
12345 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12346 .unwrap()
12347 });
12348 cx.assert_editor_state(expected);
12349 handle_resolve_completion_request(&mut cx, None).await;
12350 apply_additional_edits.await.unwrap();
12351}
12352
12353// This used to crash
12354#[gpui::test]
12355async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
12356 init_test(cx, |_| {});
12357
12358 let buffer_text = indoc! {"
12359 fn main() {
12360 10.satu;
12361
12362 //
12363 // separate cursors so they open in different excerpts (manually reproducible)
12364 //
12365
12366 10.satu20;
12367 }
12368 "};
12369 let multibuffer_text_with_selections = indoc! {"
12370 fn main() {
12371 10.satuˇ;
12372
12373 //
12374
12375 //
12376
12377 10.satuˇ20;
12378 }
12379 "};
12380 let expected_multibuffer = indoc! {"
12381 fn main() {
12382 10.saturating_sub()ˇ;
12383
12384 //
12385
12386 //
12387
12388 10.saturating_sub()ˇ;
12389 }
12390 "};
12391
12392 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
12393 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
12394
12395 let fs = FakeFs::new(cx.executor());
12396 fs.insert_tree(
12397 path!("/a"),
12398 json!({
12399 "main.rs": buffer_text,
12400 }),
12401 )
12402 .await;
12403
12404 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12405 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12406 language_registry.add(rust_lang());
12407 let mut fake_servers = language_registry.register_fake_lsp(
12408 "Rust",
12409 FakeLspAdapter {
12410 capabilities: lsp::ServerCapabilities {
12411 completion_provider: Some(lsp::CompletionOptions {
12412 resolve_provider: None,
12413 ..lsp::CompletionOptions::default()
12414 }),
12415 ..lsp::ServerCapabilities::default()
12416 },
12417 ..FakeLspAdapter::default()
12418 },
12419 );
12420 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12421 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12422 let buffer = project
12423 .update(cx, |project, cx| {
12424 project.open_local_buffer(path!("/a/main.rs"), cx)
12425 })
12426 .await
12427 .unwrap();
12428
12429 let multi_buffer = cx.new(|cx| {
12430 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
12431 multi_buffer.push_excerpts(
12432 buffer.clone(),
12433 [ExcerptRange::new(0..first_excerpt_end)],
12434 cx,
12435 );
12436 multi_buffer.push_excerpts(
12437 buffer.clone(),
12438 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
12439 cx,
12440 );
12441 multi_buffer
12442 });
12443
12444 let editor = workspace
12445 .update(cx, |_, window, cx| {
12446 cx.new(|cx| {
12447 Editor::new(
12448 EditorMode::Full {
12449 scale_ui_elements_with_buffer_font_size: false,
12450 show_active_line_background: false,
12451 sized_by_content: false,
12452 },
12453 multi_buffer.clone(),
12454 Some(project.clone()),
12455 window,
12456 cx,
12457 )
12458 })
12459 })
12460 .unwrap();
12461
12462 let pane = workspace
12463 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12464 .unwrap();
12465 pane.update_in(cx, |pane, window, cx| {
12466 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
12467 });
12468
12469 let fake_server = fake_servers.next().await.unwrap();
12470
12471 editor.update_in(cx, |editor, window, cx| {
12472 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12473 s.select_ranges([
12474 Point::new(1, 11)..Point::new(1, 11),
12475 Point::new(7, 11)..Point::new(7, 11),
12476 ])
12477 });
12478
12479 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
12480 });
12481
12482 editor.update_in(cx, |editor, window, cx| {
12483 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12484 });
12485
12486 fake_server
12487 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12488 let completion_item = lsp::CompletionItem {
12489 label: "saturating_sub()".into(),
12490 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
12491 lsp::InsertReplaceEdit {
12492 new_text: "saturating_sub()".to_owned(),
12493 insert: lsp::Range::new(
12494 lsp::Position::new(7, 7),
12495 lsp::Position::new(7, 11),
12496 ),
12497 replace: lsp::Range::new(
12498 lsp::Position::new(7, 7),
12499 lsp::Position::new(7, 13),
12500 ),
12501 },
12502 )),
12503 ..lsp::CompletionItem::default()
12504 };
12505
12506 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
12507 })
12508 .next()
12509 .await
12510 .unwrap();
12511
12512 cx.condition(&editor, |editor, _| editor.context_menu_visible())
12513 .await;
12514
12515 editor
12516 .update_in(cx, |editor, window, cx| {
12517 editor
12518 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12519 .unwrap()
12520 })
12521 .await
12522 .unwrap();
12523
12524 editor.update(cx, |editor, cx| {
12525 assert_text_with_selections(editor, expected_multibuffer, cx);
12526 })
12527}
12528
12529#[gpui::test]
12530async fn test_completion(cx: &mut TestAppContext) {
12531 init_test(cx, |_| {});
12532
12533 let mut cx = EditorLspTestContext::new_rust(
12534 lsp::ServerCapabilities {
12535 completion_provider: Some(lsp::CompletionOptions {
12536 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12537 resolve_provider: Some(true),
12538 ..Default::default()
12539 }),
12540 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12541 ..Default::default()
12542 },
12543 cx,
12544 )
12545 .await;
12546 let counter = Arc::new(AtomicUsize::new(0));
12547
12548 cx.set_state(indoc! {"
12549 oneˇ
12550 two
12551 three
12552 "});
12553 cx.simulate_keystroke(".");
12554 handle_completion_request(
12555 indoc! {"
12556 one.|<>
12557 two
12558 three
12559 "},
12560 vec!["first_completion", "second_completion"],
12561 true,
12562 counter.clone(),
12563 &mut cx,
12564 )
12565 .await;
12566 cx.condition(|editor, _| editor.context_menu_visible())
12567 .await;
12568 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12569
12570 let _handler = handle_signature_help_request(
12571 &mut cx,
12572 lsp::SignatureHelp {
12573 signatures: vec![lsp::SignatureInformation {
12574 label: "test signature".to_string(),
12575 documentation: None,
12576 parameters: Some(vec![lsp::ParameterInformation {
12577 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
12578 documentation: None,
12579 }]),
12580 active_parameter: None,
12581 }],
12582 active_signature: None,
12583 active_parameter: None,
12584 },
12585 );
12586 cx.update_editor(|editor, window, cx| {
12587 assert!(
12588 !editor.signature_help_state.is_shown(),
12589 "No signature help was called for"
12590 );
12591 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12592 });
12593 cx.run_until_parked();
12594 cx.update_editor(|editor, _, _| {
12595 assert!(
12596 !editor.signature_help_state.is_shown(),
12597 "No signature help should be shown when completions menu is open"
12598 );
12599 });
12600
12601 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12602 editor.context_menu_next(&Default::default(), window, cx);
12603 editor
12604 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12605 .unwrap()
12606 });
12607 cx.assert_editor_state(indoc! {"
12608 one.second_completionˇ
12609 two
12610 three
12611 "});
12612
12613 handle_resolve_completion_request(
12614 &mut cx,
12615 Some(vec![
12616 (
12617 //This overlaps with the primary completion edit which is
12618 //misbehavior from the LSP spec, test that we filter it out
12619 indoc! {"
12620 one.second_ˇcompletion
12621 two
12622 threeˇ
12623 "},
12624 "overlapping additional edit",
12625 ),
12626 (
12627 indoc! {"
12628 one.second_completion
12629 two
12630 threeˇ
12631 "},
12632 "\nadditional edit",
12633 ),
12634 ]),
12635 )
12636 .await;
12637 apply_additional_edits.await.unwrap();
12638 cx.assert_editor_state(indoc! {"
12639 one.second_completionˇ
12640 two
12641 three
12642 additional edit
12643 "});
12644
12645 cx.set_state(indoc! {"
12646 one.second_completion
12647 twoˇ
12648 threeˇ
12649 additional edit
12650 "});
12651 cx.simulate_keystroke(" ");
12652 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12653 cx.simulate_keystroke("s");
12654 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12655
12656 cx.assert_editor_state(indoc! {"
12657 one.second_completion
12658 two sˇ
12659 three sˇ
12660 additional edit
12661 "});
12662 handle_completion_request(
12663 indoc! {"
12664 one.second_completion
12665 two s
12666 three <s|>
12667 additional edit
12668 "},
12669 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12670 true,
12671 counter.clone(),
12672 &mut cx,
12673 )
12674 .await;
12675 cx.condition(|editor, _| editor.context_menu_visible())
12676 .await;
12677 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12678
12679 cx.simulate_keystroke("i");
12680
12681 handle_completion_request(
12682 indoc! {"
12683 one.second_completion
12684 two si
12685 three <si|>
12686 additional edit
12687 "},
12688 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12689 true,
12690 counter.clone(),
12691 &mut cx,
12692 )
12693 .await;
12694 cx.condition(|editor, _| editor.context_menu_visible())
12695 .await;
12696 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12697
12698 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12699 editor
12700 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12701 .unwrap()
12702 });
12703 cx.assert_editor_state(indoc! {"
12704 one.second_completion
12705 two sixth_completionˇ
12706 three sixth_completionˇ
12707 additional edit
12708 "});
12709
12710 apply_additional_edits.await.unwrap();
12711
12712 update_test_language_settings(&mut cx, |settings| {
12713 settings.defaults.show_completions_on_input = Some(false);
12714 });
12715 cx.set_state("editorˇ");
12716 cx.simulate_keystroke(".");
12717 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12718 cx.simulate_keystrokes("c l o");
12719 cx.assert_editor_state("editor.cloˇ");
12720 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12721 cx.update_editor(|editor, window, cx| {
12722 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12723 });
12724 handle_completion_request(
12725 "editor.<clo|>",
12726 vec!["close", "clobber"],
12727 true,
12728 counter.clone(),
12729 &mut cx,
12730 )
12731 .await;
12732 cx.condition(|editor, _| editor.context_menu_visible())
12733 .await;
12734 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
12735
12736 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12737 editor
12738 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12739 .unwrap()
12740 });
12741 cx.assert_editor_state("editor.clobberˇ");
12742 handle_resolve_completion_request(&mut cx, None).await;
12743 apply_additional_edits.await.unwrap();
12744}
12745
12746#[gpui::test]
12747async fn test_completion_reuse(cx: &mut TestAppContext) {
12748 init_test(cx, |_| {});
12749
12750 let mut cx = EditorLspTestContext::new_rust(
12751 lsp::ServerCapabilities {
12752 completion_provider: Some(lsp::CompletionOptions {
12753 trigger_characters: Some(vec![".".to_string()]),
12754 ..Default::default()
12755 }),
12756 ..Default::default()
12757 },
12758 cx,
12759 )
12760 .await;
12761
12762 let counter = Arc::new(AtomicUsize::new(0));
12763 cx.set_state("objˇ");
12764 cx.simulate_keystroke(".");
12765
12766 // Initial completion request returns complete results
12767 let is_incomplete = false;
12768 handle_completion_request(
12769 "obj.|<>",
12770 vec!["a", "ab", "abc"],
12771 is_incomplete,
12772 counter.clone(),
12773 &mut cx,
12774 )
12775 .await;
12776 cx.run_until_parked();
12777 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12778 cx.assert_editor_state("obj.ˇ");
12779 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12780
12781 // Type "a" - filters existing completions
12782 cx.simulate_keystroke("a");
12783 cx.run_until_parked();
12784 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12785 cx.assert_editor_state("obj.aˇ");
12786 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12787
12788 // Type "b" - filters existing completions
12789 cx.simulate_keystroke("b");
12790 cx.run_until_parked();
12791 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12792 cx.assert_editor_state("obj.abˇ");
12793 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12794
12795 // Type "c" - filters existing completions
12796 cx.simulate_keystroke("c");
12797 cx.run_until_parked();
12798 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12799 cx.assert_editor_state("obj.abcˇ");
12800 check_displayed_completions(vec!["abc"], &mut cx);
12801
12802 // Backspace to delete "c" - filters existing completions
12803 cx.update_editor(|editor, window, cx| {
12804 editor.backspace(&Backspace, window, cx);
12805 });
12806 cx.run_until_parked();
12807 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12808 cx.assert_editor_state("obj.abˇ");
12809 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12810
12811 // Moving cursor to the left dismisses menu.
12812 cx.update_editor(|editor, window, cx| {
12813 editor.move_left(&MoveLeft, window, cx);
12814 });
12815 cx.run_until_parked();
12816 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12817 cx.assert_editor_state("obj.aˇb");
12818 cx.update_editor(|editor, _, _| {
12819 assert_eq!(editor.context_menu_visible(), false);
12820 });
12821
12822 // Type "b" - new request
12823 cx.simulate_keystroke("b");
12824 let is_incomplete = false;
12825 handle_completion_request(
12826 "obj.<ab|>a",
12827 vec!["ab", "abc"],
12828 is_incomplete,
12829 counter.clone(),
12830 &mut cx,
12831 )
12832 .await;
12833 cx.run_until_parked();
12834 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12835 cx.assert_editor_state("obj.abˇb");
12836 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12837
12838 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
12839 cx.update_editor(|editor, window, cx| {
12840 editor.backspace(&Backspace, window, cx);
12841 });
12842 let is_incomplete = false;
12843 handle_completion_request(
12844 "obj.<a|>b",
12845 vec!["a", "ab", "abc"],
12846 is_incomplete,
12847 counter.clone(),
12848 &mut cx,
12849 )
12850 .await;
12851 cx.run_until_parked();
12852 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12853 cx.assert_editor_state("obj.aˇb");
12854 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12855
12856 // Backspace to delete "a" - dismisses menu.
12857 cx.update_editor(|editor, window, cx| {
12858 editor.backspace(&Backspace, window, cx);
12859 });
12860 cx.run_until_parked();
12861 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12862 cx.assert_editor_state("obj.ˇb");
12863 cx.update_editor(|editor, _, _| {
12864 assert_eq!(editor.context_menu_visible(), false);
12865 });
12866}
12867
12868#[gpui::test]
12869async fn test_word_completion(cx: &mut TestAppContext) {
12870 let lsp_fetch_timeout_ms = 10;
12871 init_test(cx, |language_settings| {
12872 language_settings.defaults.completions = Some(CompletionSettings {
12873 words: WordsCompletionMode::Fallback,
12874 lsp: true,
12875 lsp_fetch_timeout_ms: 10,
12876 lsp_insert_mode: LspInsertMode::Insert,
12877 });
12878 });
12879
12880 let mut cx = EditorLspTestContext::new_rust(
12881 lsp::ServerCapabilities {
12882 completion_provider: Some(lsp::CompletionOptions {
12883 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12884 ..lsp::CompletionOptions::default()
12885 }),
12886 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12887 ..lsp::ServerCapabilities::default()
12888 },
12889 cx,
12890 )
12891 .await;
12892
12893 let throttle_completions = Arc::new(AtomicBool::new(false));
12894
12895 let lsp_throttle_completions = throttle_completions.clone();
12896 let _completion_requests_handler =
12897 cx.lsp
12898 .server
12899 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
12900 let lsp_throttle_completions = lsp_throttle_completions.clone();
12901 let cx = cx.clone();
12902 async move {
12903 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
12904 cx.background_executor()
12905 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
12906 .await;
12907 }
12908 Ok(Some(lsp::CompletionResponse::Array(vec![
12909 lsp::CompletionItem {
12910 label: "first".into(),
12911 ..lsp::CompletionItem::default()
12912 },
12913 lsp::CompletionItem {
12914 label: "last".into(),
12915 ..lsp::CompletionItem::default()
12916 },
12917 ])))
12918 }
12919 });
12920
12921 cx.set_state(indoc! {"
12922 oneˇ
12923 two
12924 three
12925 "});
12926 cx.simulate_keystroke(".");
12927 cx.executor().run_until_parked();
12928 cx.condition(|editor, _| editor.context_menu_visible())
12929 .await;
12930 cx.update_editor(|editor, window, cx| {
12931 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12932 {
12933 assert_eq!(
12934 completion_menu_entries(&menu),
12935 &["first", "last"],
12936 "When LSP server is fast to reply, no fallback word completions are used"
12937 );
12938 } else {
12939 panic!("expected completion menu to be open");
12940 }
12941 editor.cancel(&Cancel, window, cx);
12942 });
12943 cx.executor().run_until_parked();
12944 cx.condition(|editor, _| !editor.context_menu_visible())
12945 .await;
12946
12947 throttle_completions.store(true, atomic::Ordering::Release);
12948 cx.simulate_keystroke(".");
12949 cx.executor()
12950 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
12951 cx.executor().run_until_parked();
12952 cx.condition(|editor, _| editor.context_menu_visible())
12953 .await;
12954 cx.update_editor(|editor, _, _| {
12955 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12956 {
12957 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
12958 "When LSP server is slow, document words can be shown instead, if configured accordingly");
12959 } else {
12960 panic!("expected completion menu to be open");
12961 }
12962 });
12963}
12964
12965#[gpui::test]
12966async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
12967 init_test(cx, |language_settings| {
12968 language_settings.defaults.completions = Some(CompletionSettings {
12969 words: WordsCompletionMode::Enabled,
12970 lsp: true,
12971 lsp_fetch_timeout_ms: 0,
12972 lsp_insert_mode: LspInsertMode::Insert,
12973 });
12974 });
12975
12976 let mut cx = EditorLspTestContext::new_rust(
12977 lsp::ServerCapabilities {
12978 completion_provider: Some(lsp::CompletionOptions {
12979 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12980 ..lsp::CompletionOptions::default()
12981 }),
12982 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12983 ..lsp::ServerCapabilities::default()
12984 },
12985 cx,
12986 )
12987 .await;
12988
12989 let _completion_requests_handler =
12990 cx.lsp
12991 .server
12992 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12993 Ok(Some(lsp::CompletionResponse::Array(vec![
12994 lsp::CompletionItem {
12995 label: "first".into(),
12996 ..lsp::CompletionItem::default()
12997 },
12998 lsp::CompletionItem {
12999 label: "last".into(),
13000 ..lsp::CompletionItem::default()
13001 },
13002 ])))
13003 });
13004
13005 cx.set_state(indoc! {"ˇ
13006 first
13007 last
13008 second
13009 "});
13010 cx.simulate_keystroke(".");
13011 cx.executor().run_until_parked();
13012 cx.condition(|editor, _| editor.context_menu_visible())
13013 .await;
13014 cx.update_editor(|editor, _, _| {
13015 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13016 {
13017 assert_eq!(
13018 completion_menu_entries(&menu),
13019 &["first", "last", "second"],
13020 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
13021 );
13022 } else {
13023 panic!("expected completion menu to be open");
13024 }
13025 });
13026}
13027
13028#[gpui::test]
13029async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
13030 init_test(cx, |language_settings| {
13031 language_settings.defaults.completions = Some(CompletionSettings {
13032 words: WordsCompletionMode::Disabled,
13033 lsp: true,
13034 lsp_fetch_timeout_ms: 0,
13035 lsp_insert_mode: LspInsertMode::Insert,
13036 });
13037 });
13038
13039 let mut cx = EditorLspTestContext::new_rust(
13040 lsp::ServerCapabilities {
13041 completion_provider: Some(lsp::CompletionOptions {
13042 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13043 ..lsp::CompletionOptions::default()
13044 }),
13045 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13046 ..lsp::ServerCapabilities::default()
13047 },
13048 cx,
13049 )
13050 .await;
13051
13052 let _completion_requests_handler =
13053 cx.lsp
13054 .server
13055 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
13056 panic!("LSP completions should not be queried when dealing with word completions")
13057 });
13058
13059 cx.set_state(indoc! {"ˇ
13060 first
13061 last
13062 second
13063 "});
13064 cx.update_editor(|editor, window, cx| {
13065 editor.show_word_completions(&ShowWordCompletions, window, cx);
13066 });
13067 cx.executor().run_until_parked();
13068 cx.condition(|editor, _| editor.context_menu_visible())
13069 .await;
13070 cx.update_editor(|editor, _, _| {
13071 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13072 {
13073 assert_eq!(
13074 completion_menu_entries(&menu),
13075 &["first", "last", "second"],
13076 "`ShowWordCompletions` action should show word completions"
13077 );
13078 } else {
13079 panic!("expected completion menu to be open");
13080 }
13081 });
13082
13083 cx.simulate_keystroke("l");
13084 cx.executor().run_until_parked();
13085 cx.condition(|editor, _| editor.context_menu_visible())
13086 .await;
13087 cx.update_editor(|editor, _, _| {
13088 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13089 {
13090 assert_eq!(
13091 completion_menu_entries(&menu),
13092 &["last"],
13093 "After showing word completions, further editing should filter them and not query the LSP"
13094 );
13095 } else {
13096 panic!("expected completion menu to be open");
13097 }
13098 });
13099}
13100
13101#[gpui::test]
13102async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
13103 init_test(cx, |language_settings| {
13104 language_settings.defaults.completions = Some(CompletionSettings {
13105 words: WordsCompletionMode::Fallback,
13106 lsp: false,
13107 lsp_fetch_timeout_ms: 0,
13108 lsp_insert_mode: LspInsertMode::Insert,
13109 });
13110 });
13111
13112 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13113
13114 cx.set_state(indoc! {"ˇ
13115 0_usize
13116 let
13117 33
13118 4.5f32
13119 "});
13120 cx.update_editor(|editor, window, cx| {
13121 editor.show_completions(&ShowCompletions::default(), window, cx);
13122 });
13123 cx.executor().run_until_parked();
13124 cx.condition(|editor, _| editor.context_menu_visible())
13125 .await;
13126 cx.update_editor(|editor, window, cx| {
13127 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13128 {
13129 assert_eq!(
13130 completion_menu_entries(&menu),
13131 &["let"],
13132 "With no digits in the completion query, no digits should be in the word completions"
13133 );
13134 } else {
13135 panic!("expected completion menu to be open");
13136 }
13137 editor.cancel(&Cancel, window, cx);
13138 });
13139
13140 cx.set_state(indoc! {"3ˇ
13141 0_usize
13142 let
13143 3
13144 33.35f32
13145 "});
13146 cx.update_editor(|editor, window, cx| {
13147 editor.show_completions(&ShowCompletions::default(), window, cx);
13148 });
13149 cx.executor().run_until_parked();
13150 cx.condition(|editor, _| editor.context_menu_visible())
13151 .await;
13152 cx.update_editor(|editor, _, _| {
13153 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13154 {
13155 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
13156 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
13157 } else {
13158 panic!("expected completion menu to be open");
13159 }
13160 });
13161}
13162
13163fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
13164 let position = || lsp::Position {
13165 line: params.text_document_position.position.line,
13166 character: params.text_document_position.position.character,
13167 };
13168 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13169 range: lsp::Range {
13170 start: position(),
13171 end: position(),
13172 },
13173 new_text: text.to_string(),
13174 }))
13175}
13176
13177#[gpui::test]
13178async fn test_multiline_completion(cx: &mut TestAppContext) {
13179 init_test(cx, |_| {});
13180
13181 let fs = FakeFs::new(cx.executor());
13182 fs.insert_tree(
13183 path!("/a"),
13184 json!({
13185 "main.ts": "a",
13186 }),
13187 )
13188 .await;
13189
13190 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13191 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13192 let typescript_language = Arc::new(Language::new(
13193 LanguageConfig {
13194 name: "TypeScript".into(),
13195 matcher: LanguageMatcher {
13196 path_suffixes: vec!["ts".to_string()],
13197 ..LanguageMatcher::default()
13198 },
13199 line_comments: vec!["// ".into()],
13200 ..LanguageConfig::default()
13201 },
13202 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13203 ));
13204 language_registry.add(typescript_language.clone());
13205 let mut fake_servers = language_registry.register_fake_lsp(
13206 "TypeScript",
13207 FakeLspAdapter {
13208 capabilities: lsp::ServerCapabilities {
13209 completion_provider: Some(lsp::CompletionOptions {
13210 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13211 ..lsp::CompletionOptions::default()
13212 }),
13213 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13214 ..lsp::ServerCapabilities::default()
13215 },
13216 // Emulate vtsls label generation
13217 label_for_completion: Some(Box::new(|item, _| {
13218 let text = if let Some(description) = item
13219 .label_details
13220 .as_ref()
13221 .and_then(|label_details| label_details.description.as_ref())
13222 {
13223 format!("{} {}", item.label, description)
13224 } else if let Some(detail) = &item.detail {
13225 format!("{} {}", item.label, detail)
13226 } else {
13227 item.label.clone()
13228 };
13229 let len = text.len();
13230 Some(language::CodeLabel {
13231 text,
13232 runs: Vec::new(),
13233 filter_range: 0..len,
13234 })
13235 })),
13236 ..FakeLspAdapter::default()
13237 },
13238 );
13239 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13240 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13241 let worktree_id = workspace
13242 .update(cx, |workspace, _window, cx| {
13243 workspace.project().update(cx, |project, cx| {
13244 project.worktrees(cx).next().unwrap().read(cx).id()
13245 })
13246 })
13247 .unwrap();
13248 let _buffer = project
13249 .update(cx, |project, cx| {
13250 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
13251 })
13252 .await
13253 .unwrap();
13254 let editor = workspace
13255 .update(cx, |workspace, window, cx| {
13256 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
13257 })
13258 .unwrap()
13259 .await
13260 .unwrap()
13261 .downcast::<Editor>()
13262 .unwrap();
13263 let fake_server = fake_servers.next().await.unwrap();
13264
13265 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
13266 let multiline_label_2 = "a\nb\nc\n";
13267 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
13268 let multiline_description = "d\ne\nf\n";
13269 let multiline_detail_2 = "g\nh\ni\n";
13270
13271 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
13272 move |params, _| async move {
13273 Ok(Some(lsp::CompletionResponse::Array(vec![
13274 lsp::CompletionItem {
13275 label: multiline_label.to_string(),
13276 text_edit: gen_text_edit(¶ms, "new_text_1"),
13277 ..lsp::CompletionItem::default()
13278 },
13279 lsp::CompletionItem {
13280 label: "single line label 1".to_string(),
13281 detail: Some(multiline_detail.to_string()),
13282 text_edit: gen_text_edit(¶ms, "new_text_2"),
13283 ..lsp::CompletionItem::default()
13284 },
13285 lsp::CompletionItem {
13286 label: "single line label 2".to_string(),
13287 label_details: Some(lsp::CompletionItemLabelDetails {
13288 description: Some(multiline_description.to_string()),
13289 detail: None,
13290 }),
13291 text_edit: gen_text_edit(¶ms, "new_text_2"),
13292 ..lsp::CompletionItem::default()
13293 },
13294 lsp::CompletionItem {
13295 label: multiline_label_2.to_string(),
13296 detail: Some(multiline_detail_2.to_string()),
13297 text_edit: gen_text_edit(¶ms, "new_text_3"),
13298 ..lsp::CompletionItem::default()
13299 },
13300 lsp::CompletionItem {
13301 label: "Label with many spaces and \t but without newlines".to_string(),
13302 detail: Some(
13303 "Details with many spaces and \t but without newlines".to_string(),
13304 ),
13305 text_edit: gen_text_edit(¶ms, "new_text_4"),
13306 ..lsp::CompletionItem::default()
13307 },
13308 ])))
13309 },
13310 );
13311
13312 editor.update_in(cx, |editor, window, cx| {
13313 cx.focus_self(window);
13314 editor.move_to_end(&MoveToEnd, window, cx);
13315 editor.handle_input(".", window, cx);
13316 });
13317 cx.run_until_parked();
13318 completion_handle.next().await.unwrap();
13319
13320 editor.update(cx, |editor, _| {
13321 assert!(editor.context_menu_visible());
13322 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13323 {
13324 let completion_labels = menu
13325 .completions
13326 .borrow()
13327 .iter()
13328 .map(|c| c.label.text.clone())
13329 .collect::<Vec<_>>();
13330 assert_eq!(
13331 completion_labels,
13332 &[
13333 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
13334 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
13335 "single line label 2 d e f ",
13336 "a b c g h i ",
13337 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
13338 ],
13339 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
13340 );
13341
13342 for completion in menu
13343 .completions
13344 .borrow()
13345 .iter() {
13346 assert_eq!(
13347 completion.label.filter_range,
13348 0..completion.label.text.len(),
13349 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
13350 );
13351 }
13352 } else {
13353 panic!("expected completion menu to be open");
13354 }
13355 });
13356}
13357
13358#[gpui::test]
13359async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
13360 init_test(cx, |_| {});
13361 let mut cx = EditorLspTestContext::new_rust(
13362 lsp::ServerCapabilities {
13363 completion_provider: Some(lsp::CompletionOptions {
13364 trigger_characters: Some(vec![".".to_string()]),
13365 ..Default::default()
13366 }),
13367 ..Default::default()
13368 },
13369 cx,
13370 )
13371 .await;
13372 cx.lsp
13373 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13374 Ok(Some(lsp::CompletionResponse::Array(vec![
13375 lsp::CompletionItem {
13376 label: "first".into(),
13377 ..Default::default()
13378 },
13379 lsp::CompletionItem {
13380 label: "last".into(),
13381 ..Default::default()
13382 },
13383 ])))
13384 });
13385 cx.set_state("variableˇ");
13386 cx.simulate_keystroke(".");
13387 cx.executor().run_until_parked();
13388
13389 cx.update_editor(|editor, _, _| {
13390 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13391 {
13392 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
13393 } else {
13394 panic!("expected completion menu to be open");
13395 }
13396 });
13397
13398 cx.update_editor(|editor, window, cx| {
13399 editor.move_page_down(&MovePageDown::default(), window, cx);
13400 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13401 {
13402 assert!(
13403 menu.selected_item == 1,
13404 "expected PageDown to select the last item from the context menu"
13405 );
13406 } else {
13407 panic!("expected completion menu to stay open after PageDown");
13408 }
13409 });
13410
13411 cx.update_editor(|editor, window, cx| {
13412 editor.move_page_up(&MovePageUp::default(), window, cx);
13413 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13414 {
13415 assert!(
13416 menu.selected_item == 0,
13417 "expected PageUp to select the first item from the context menu"
13418 );
13419 } else {
13420 panic!("expected completion menu to stay open after PageUp");
13421 }
13422 });
13423}
13424
13425#[gpui::test]
13426async fn test_as_is_completions(cx: &mut TestAppContext) {
13427 init_test(cx, |_| {});
13428 let mut cx = EditorLspTestContext::new_rust(
13429 lsp::ServerCapabilities {
13430 completion_provider: Some(lsp::CompletionOptions {
13431 ..Default::default()
13432 }),
13433 ..Default::default()
13434 },
13435 cx,
13436 )
13437 .await;
13438 cx.lsp
13439 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13440 Ok(Some(lsp::CompletionResponse::Array(vec![
13441 lsp::CompletionItem {
13442 label: "unsafe".into(),
13443 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13444 range: lsp::Range {
13445 start: lsp::Position {
13446 line: 1,
13447 character: 2,
13448 },
13449 end: lsp::Position {
13450 line: 1,
13451 character: 3,
13452 },
13453 },
13454 new_text: "unsafe".to_string(),
13455 })),
13456 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
13457 ..Default::default()
13458 },
13459 ])))
13460 });
13461 cx.set_state("fn a() {}\n nˇ");
13462 cx.executor().run_until_parked();
13463 cx.update_editor(|editor, window, cx| {
13464 editor.show_completions(
13465 &ShowCompletions {
13466 trigger: Some("\n".into()),
13467 },
13468 window,
13469 cx,
13470 );
13471 });
13472 cx.executor().run_until_parked();
13473
13474 cx.update_editor(|editor, window, cx| {
13475 editor.confirm_completion(&Default::default(), window, cx)
13476 });
13477 cx.executor().run_until_parked();
13478 cx.assert_editor_state("fn a() {}\n unsafeˇ");
13479}
13480
13481#[gpui::test]
13482async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
13483 init_test(cx, |_| {});
13484 let language =
13485 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
13486 let mut cx = EditorLspTestContext::new(
13487 language,
13488 lsp::ServerCapabilities {
13489 completion_provider: Some(lsp::CompletionOptions {
13490 ..lsp::CompletionOptions::default()
13491 }),
13492 ..lsp::ServerCapabilities::default()
13493 },
13494 cx,
13495 )
13496 .await;
13497
13498 cx.set_state(
13499 "#ifndef BAR_H
13500#define BAR_H
13501
13502#include <stdbool.h>
13503
13504int fn_branch(bool do_branch1, bool do_branch2);
13505
13506#endif // BAR_H
13507ˇ",
13508 );
13509 cx.executor().run_until_parked();
13510 cx.update_editor(|editor, window, cx| {
13511 editor.handle_input("#", window, cx);
13512 });
13513 cx.executor().run_until_parked();
13514 cx.update_editor(|editor, window, cx| {
13515 editor.handle_input("i", window, cx);
13516 });
13517 cx.executor().run_until_parked();
13518 cx.update_editor(|editor, window, cx| {
13519 editor.handle_input("n", window, cx);
13520 });
13521 cx.executor().run_until_parked();
13522 cx.assert_editor_state(
13523 "#ifndef BAR_H
13524#define BAR_H
13525
13526#include <stdbool.h>
13527
13528int fn_branch(bool do_branch1, bool do_branch2);
13529
13530#endif // BAR_H
13531#inˇ",
13532 );
13533
13534 cx.lsp
13535 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13536 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13537 is_incomplete: false,
13538 item_defaults: None,
13539 items: vec![lsp::CompletionItem {
13540 kind: Some(lsp::CompletionItemKind::SNIPPET),
13541 label_details: Some(lsp::CompletionItemLabelDetails {
13542 detail: Some("header".to_string()),
13543 description: None,
13544 }),
13545 label: " include".to_string(),
13546 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13547 range: lsp::Range {
13548 start: lsp::Position {
13549 line: 8,
13550 character: 1,
13551 },
13552 end: lsp::Position {
13553 line: 8,
13554 character: 1,
13555 },
13556 },
13557 new_text: "include \"$0\"".to_string(),
13558 })),
13559 sort_text: Some("40b67681include".to_string()),
13560 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13561 filter_text: Some("include".to_string()),
13562 insert_text: Some("include \"$0\"".to_string()),
13563 ..lsp::CompletionItem::default()
13564 }],
13565 })))
13566 });
13567 cx.update_editor(|editor, window, cx| {
13568 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13569 });
13570 cx.executor().run_until_parked();
13571 cx.update_editor(|editor, window, cx| {
13572 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
13573 });
13574 cx.executor().run_until_parked();
13575 cx.assert_editor_state(
13576 "#ifndef BAR_H
13577#define BAR_H
13578
13579#include <stdbool.h>
13580
13581int fn_branch(bool do_branch1, bool do_branch2);
13582
13583#endif // BAR_H
13584#include \"ˇ\"",
13585 );
13586
13587 cx.lsp
13588 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13589 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13590 is_incomplete: true,
13591 item_defaults: None,
13592 items: vec![lsp::CompletionItem {
13593 kind: Some(lsp::CompletionItemKind::FILE),
13594 label: "AGL/".to_string(),
13595 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13596 range: lsp::Range {
13597 start: lsp::Position {
13598 line: 8,
13599 character: 10,
13600 },
13601 end: lsp::Position {
13602 line: 8,
13603 character: 11,
13604 },
13605 },
13606 new_text: "AGL/".to_string(),
13607 })),
13608 sort_text: Some("40b67681AGL/".to_string()),
13609 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
13610 filter_text: Some("AGL/".to_string()),
13611 insert_text: Some("AGL/".to_string()),
13612 ..lsp::CompletionItem::default()
13613 }],
13614 })))
13615 });
13616 cx.update_editor(|editor, window, cx| {
13617 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13618 });
13619 cx.executor().run_until_parked();
13620 cx.update_editor(|editor, window, cx| {
13621 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
13622 });
13623 cx.executor().run_until_parked();
13624 cx.assert_editor_state(
13625 r##"#ifndef BAR_H
13626#define BAR_H
13627
13628#include <stdbool.h>
13629
13630int fn_branch(bool do_branch1, bool do_branch2);
13631
13632#endif // BAR_H
13633#include "AGL/ˇ"##,
13634 );
13635
13636 cx.update_editor(|editor, window, cx| {
13637 editor.handle_input("\"", window, cx);
13638 });
13639 cx.executor().run_until_parked();
13640 cx.assert_editor_state(
13641 r##"#ifndef BAR_H
13642#define BAR_H
13643
13644#include <stdbool.h>
13645
13646int fn_branch(bool do_branch1, bool do_branch2);
13647
13648#endif // BAR_H
13649#include "AGL/"ˇ"##,
13650 );
13651}
13652
13653#[gpui::test]
13654async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
13655 init_test(cx, |_| {});
13656
13657 let mut cx = EditorLspTestContext::new_rust(
13658 lsp::ServerCapabilities {
13659 completion_provider: Some(lsp::CompletionOptions {
13660 trigger_characters: Some(vec![".".to_string()]),
13661 resolve_provider: Some(true),
13662 ..Default::default()
13663 }),
13664 ..Default::default()
13665 },
13666 cx,
13667 )
13668 .await;
13669
13670 cx.set_state("fn main() { let a = 2ˇ; }");
13671 cx.simulate_keystroke(".");
13672 let completion_item = lsp::CompletionItem {
13673 label: "Some".into(),
13674 kind: Some(lsp::CompletionItemKind::SNIPPET),
13675 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13676 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13677 kind: lsp::MarkupKind::Markdown,
13678 value: "```rust\nSome(2)\n```".to_string(),
13679 })),
13680 deprecated: Some(false),
13681 sort_text: Some("Some".to_string()),
13682 filter_text: Some("Some".to_string()),
13683 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13684 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13685 range: lsp::Range {
13686 start: lsp::Position {
13687 line: 0,
13688 character: 22,
13689 },
13690 end: lsp::Position {
13691 line: 0,
13692 character: 22,
13693 },
13694 },
13695 new_text: "Some(2)".to_string(),
13696 })),
13697 additional_text_edits: Some(vec![lsp::TextEdit {
13698 range: lsp::Range {
13699 start: lsp::Position {
13700 line: 0,
13701 character: 20,
13702 },
13703 end: lsp::Position {
13704 line: 0,
13705 character: 22,
13706 },
13707 },
13708 new_text: "".to_string(),
13709 }]),
13710 ..Default::default()
13711 };
13712
13713 let closure_completion_item = completion_item.clone();
13714 let counter = Arc::new(AtomicUsize::new(0));
13715 let counter_clone = counter.clone();
13716 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13717 let task_completion_item = closure_completion_item.clone();
13718 counter_clone.fetch_add(1, atomic::Ordering::Release);
13719 async move {
13720 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13721 is_incomplete: true,
13722 item_defaults: None,
13723 items: vec![task_completion_item],
13724 })))
13725 }
13726 });
13727
13728 cx.condition(|editor, _| editor.context_menu_visible())
13729 .await;
13730 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
13731 assert!(request.next().await.is_some());
13732 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13733
13734 cx.simulate_keystrokes("S o m");
13735 cx.condition(|editor, _| editor.context_menu_visible())
13736 .await;
13737 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
13738 assert!(request.next().await.is_some());
13739 assert!(request.next().await.is_some());
13740 assert!(request.next().await.is_some());
13741 request.close();
13742 assert!(request.next().await.is_none());
13743 assert_eq!(
13744 counter.load(atomic::Ordering::Acquire),
13745 4,
13746 "With the completions menu open, only one LSP request should happen per input"
13747 );
13748}
13749
13750#[gpui::test]
13751async fn test_toggle_comment(cx: &mut TestAppContext) {
13752 init_test(cx, |_| {});
13753 let mut cx = EditorTestContext::new(cx).await;
13754 let language = Arc::new(Language::new(
13755 LanguageConfig {
13756 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13757 ..Default::default()
13758 },
13759 Some(tree_sitter_rust::LANGUAGE.into()),
13760 ));
13761 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13762
13763 // If multiple selections intersect a line, the line is only toggled once.
13764 cx.set_state(indoc! {"
13765 fn a() {
13766 «//b();
13767 ˇ»// «c();
13768 //ˇ» d();
13769 }
13770 "});
13771
13772 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13773
13774 cx.assert_editor_state(indoc! {"
13775 fn a() {
13776 «b();
13777 c();
13778 ˇ» d();
13779 }
13780 "});
13781
13782 // The comment prefix is inserted at the same column for every line in a
13783 // selection.
13784 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13785
13786 cx.assert_editor_state(indoc! {"
13787 fn a() {
13788 // «b();
13789 // c();
13790 ˇ»// d();
13791 }
13792 "});
13793
13794 // If a selection ends at the beginning of a line, that line is not toggled.
13795 cx.set_selections_state(indoc! {"
13796 fn a() {
13797 // b();
13798 «// c();
13799 ˇ» // d();
13800 }
13801 "});
13802
13803 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13804
13805 cx.assert_editor_state(indoc! {"
13806 fn a() {
13807 // b();
13808 «c();
13809 ˇ» // d();
13810 }
13811 "});
13812
13813 // If a selection span a single line and is empty, the line is toggled.
13814 cx.set_state(indoc! {"
13815 fn a() {
13816 a();
13817 b();
13818 ˇ
13819 }
13820 "});
13821
13822 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13823
13824 cx.assert_editor_state(indoc! {"
13825 fn a() {
13826 a();
13827 b();
13828 //•ˇ
13829 }
13830 "});
13831
13832 // If a selection span multiple lines, empty lines are not toggled.
13833 cx.set_state(indoc! {"
13834 fn a() {
13835 «a();
13836
13837 c();ˇ»
13838 }
13839 "});
13840
13841 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13842
13843 cx.assert_editor_state(indoc! {"
13844 fn a() {
13845 // «a();
13846
13847 // c();ˇ»
13848 }
13849 "});
13850
13851 // If a selection includes multiple comment prefixes, all lines are uncommented.
13852 cx.set_state(indoc! {"
13853 fn a() {
13854 «// a();
13855 /// b();
13856 //! c();ˇ»
13857 }
13858 "});
13859
13860 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13861
13862 cx.assert_editor_state(indoc! {"
13863 fn a() {
13864 «a();
13865 b();
13866 c();ˇ»
13867 }
13868 "});
13869}
13870
13871#[gpui::test]
13872async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
13873 init_test(cx, |_| {});
13874 let mut cx = EditorTestContext::new(cx).await;
13875 let language = Arc::new(Language::new(
13876 LanguageConfig {
13877 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13878 ..Default::default()
13879 },
13880 Some(tree_sitter_rust::LANGUAGE.into()),
13881 ));
13882 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13883
13884 let toggle_comments = &ToggleComments {
13885 advance_downwards: false,
13886 ignore_indent: true,
13887 };
13888
13889 // If multiple selections intersect a line, the line is only toggled once.
13890 cx.set_state(indoc! {"
13891 fn a() {
13892 // «b();
13893 // c();
13894 // ˇ» d();
13895 }
13896 "});
13897
13898 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13899
13900 cx.assert_editor_state(indoc! {"
13901 fn a() {
13902 «b();
13903 c();
13904 ˇ» d();
13905 }
13906 "});
13907
13908 // The comment prefix is inserted at the beginning of each line
13909 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13910
13911 cx.assert_editor_state(indoc! {"
13912 fn a() {
13913 // «b();
13914 // c();
13915 // ˇ» d();
13916 }
13917 "});
13918
13919 // If a selection ends at the beginning of a line, that line is not toggled.
13920 cx.set_selections_state(indoc! {"
13921 fn a() {
13922 // b();
13923 // «c();
13924 ˇ»// d();
13925 }
13926 "});
13927
13928 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13929
13930 cx.assert_editor_state(indoc! {"
13931 fn a() {
13932 // b();
13933 «c();
13934 ˇ»// d();
13935 }
13936 "});
13937
13938 // If a selection span a single line and is empty, the line is toggled.
13939 cx.set_state(indoc! {"
13940 fn a() {
13941 a();
13942 b();
13943 ˇ
13944 }
13945 "});
13946
13947 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13948
13949 cx.assert_editor_state(indoc! {"
13950 fn a() {
13951 a();
13952 b();
13953 //ˇ
13954 }
13955 "});
13956
13957 // If a selection span multiple lines, empty lines are not toggled.
13958 cx.set_state(indoc! {"
13959 fn a() {
13960 «a();
13961
13962 c();ˇ»
13963 }
13964 "});
13965
13966 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13967
13968 cx.assert_editor_state(indoc! {"
13969 fn a() {
13970 // «a();
13971
13972 // c();ˇ»
13973 }
13974 "});
13975
13976 // If a selection includes multiple comment prefixes, all lines are uncommented.
13977 cx.set_state(indoc! {"
13978 fn a() {
13979 // «a();
13980 /// b();
13981 //! c();ˇ»
13982 }
13983 "});
13984
13985 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13986
13987 cx.assert_editor_state(indoc! {"
13988 fn a() {
13989 «a();
13990 b();
13991 c();ˇ»
13992 }
13993 "});
13994}
13995
13996#[gpui::test]
13997async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
13998 init_test(cx, |_| {});
13999
14000 let language = Arc::new(Language::new(
14001 LanguageConfig {
14002 line_comments: vec!["// ".into()],
14003 ..Default::default()
14004 },
14005 Some(tree_sitter_rust::LANGUAGE.into()),
14006 ));
14007
14008 let mut cx = EditorTestContext::new(cx).await;
14009
14010 cx.language_registry().add(language.clone());
14011 cx.update_buffer(|buffer, cx| {
14012 buffer.set_language(Some(language), cx);
14013 });
14014
14015 let toggle_comments = &ToggleComments {
14016 advance_downwards: true,
14017 ignore_indent: false,
14018 };
14019
14020 // Single cursor on one line -> advance
14021 // Cursor moves horizontally 3 characters as well on non-blank line
14022 cx.set_state(indoc!(
14023 "fn a() {
14024 ˇdog();
14025 cat();
14026 }"
14027 ));
14028 cx.update_editor(|editor, window, cx| {
14029 editor.toggle_comments(toggle_comments, window, cx);
14030 });
14031 cx.assert_editor_state(indoc!(
14032 "fn a() {
14033 // dog();
14034 catˇ();
14035 }"
14036 ));
14037
14038 // Single selection on one line -> don't advance
14039 cx.set_state(indoc!(
14040 "fn a() {
14041 «dog()ˇ»;
14042 cat();
14043 }"
14044 ));
14045 cx.update_editor(|editor, window, cx| {
14046 editor.toggle_comments(toggle_comments, window, cx);
14047 });
14048 cx.assert_editor_state(indoc!(
14049 "fn a() {
14050 // «dog()ˇ»;
14051 cat();
14052 }"
14053 ));
14054
14055 // Multiple cursors on one line -> advance
14056 cx.set_state(indoc!(
14057 "fn a() {
14058 ˇdˇog();
14059 cat();
14060 }"
14061 ));
14062 cx.update_editor(|editor, window, cx| {
14063 editor.toggle_comments(toggle_comments, window, cx);
14064 });
14065 cx.assert_editor_state(indoc!(
14066 "fn a() {
14067 // dog();
14068 catˇ(ˇ);
14069 }"
14070 ));
14071
14072 // Multiple cursors on one line, with selection -> don't advance
14073 cx.set_state(indoc!(
14074 "fn a() {
14075 ˇdˇog«()ˇ»;
14076 cat();
14077 }"
14078 ));
14079 cx.update_editor(|editor, window, cx| {
14080 editor.toggle_comments(toggle_comments, window, cx);
14081 });
14082 cx.assert_editor_state(indoc!(
14083 "fn a() {
14084 // ˇdˇog«()ˇ»;
14085 cat();
14086 }"
14087 ));
14088
14089 // Single cursor on one line -> advance
14090 // Cursor moves to column 0 on blank line
14091 cx.set_state(indoc!(
14092 "fn a() {
14093 ˇdog();
14094
14095 cat();
14096 }"
14097 ));
14098 cx.update_editor(|editor, window, cx| {
14099 editor.toggle_comments(toggle_comments, window, cx);
14100 });
14101 cx.assert_editor_state(indoc!(
14102 "fn a() {
14103 // dog();
14104 ˇ
14105 cat();
14106 }"
14107 ));
14108
14109 // Single cursor on one line -> advance
14110 // Cursor starts and ends at column 0
14111 cx.set_state(indoc!(
14112 "fn a() {
14113 ˇ dog();
14114 cat();
14115 }"
14116 ));
14117 cx.update_editor(|editor, window, cx| {
14118 editor.toggle_comments(toggle_comments, window, cx);
14119 });
14120 cx.assert_editor_state(indoc!(
14121 "fn a() {
14122 // dog();
14123 ˇ cat();
14124 }"
14125 ));
14126}
14127
14128#[gpui::test]
14129async fn test_toggle_block_comment(cx: &mut TestAppContext) {
14130 init_test(cx, |_| {});
14131
14132 let mut cx = EditorTestContext::new(cx).await;
14133
14134 let html_language = Arc::new(
14135 Language::new(
14136 LanguageConfig {
14137 name: "HTML".into(),
14138 block_comment: Some(BlockCommentConfig {
14139 start: "<!-- ".into(),
14140 prefix: "".into(),
14141 end: " -->".into(),
14142 tab_size: 0,
14143 }),
14144 ..Default::default()
14145 },
14146 Some(tree_sitter_html::LANGUAGE.into()),
14147 )
14148 .with_injection_query(
14149 r#"
14150 (script_element
14151 (raw_text) @injection.content
14152 (#set! injection.language "javascript"))
14153 "#,
14154 )
14155 .unwrap(),
14156 );
14157
14158 let javascript_language = Arc::new(Language::new(
14159 LanguageConfig {
14160 name: "JavaScript".into(),
14161 line_comments: vec!["// ".into()],
14162 ..Default::default()
14163 },
14164 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
14165 ));
14166
14167 cx.language_registry().add(html_language.clone());
14168 cx.language_registry().add(javascript_language.clone());
14169 cx.update_buffer(|buffer, cx| {
14170 buffer.set_language(Some(html_language), cx);
14171 });
14172
14173 // Toggle comments for empty selections
14174 cx.set_state(
14175 &r#"
14176 <p>A</p>ˇ
14177 <p>B</p>ˇ
14178 <p>C</p>ˇ
14179 "#
14180 .unindent(),
14181 );
14182 cx.update_editor(|editor, window, cx| {
14183 editor.toggle_comments(&ToggleComments::default(), window, cx)
14184 });
14185 cx.assert_editor_state(
14186 &r#"
14187 <!-- <p>A</p>ˇ -->
14188 <!-- <p>B</p>ˇ -->
14189 <!-- <p>C</p>ˇ -->
14190 "#
14191 .unindent(),
14192 );
14193 cx.update_editor(|editor, window, cx| {
14194 editor.toggle_comments(&ToggleComments::default(), window, cx)
14195 });
14196 cx.assert_editor_state(
14197 &r#"
14198 <p>A</p>ˇ
14199 <p>B</p>ˇ
14200 <p>C</p>ˇ
14201 "#
14202 .unindent(),
14203 );
14204
14205 // Toggle comments for mixture of empty and non-empty selections, where
14206 // multiple selections occupy a given line.
14207 cx.set_state(
14208 &r#"
14209 <p>A«</p>
14210 <p>ˇ»B</p>ˇ
14211 <p>C«</p>
14212 <p>ˇ»D</p>ˇ
14213 "#
14214 .unindent(),
14215 );
14216
14217 cx.update_editor(|editor, window, cx| {
14218 editor.toggle_comments(&ToggleComments::default(), window, cx)
14219 });
14220 cx.assert_editor_state(
14221 &r#"
14222 <!-- <p>A«</p>
14223 <p>ˇ»B</p>ˇ -->
14224 <!-- <p>C«</p>
14225 <p>ˇ»D</p>ˇ -->
14226 "#
14227 .unindent(),
14228 );
14229 cx.update_editor(|editor, window, cx| {
14230 editor.toggle_comments(&ToggleComments::default(), window, cx)
14231 });
14232 cx.assert_editor_state(
14233 &r#"
14234 <p>A«</p>
14235 <p>ˇ»B</p>ˇ
14236 <p>C«</p>
14237 <p>ˇ»D</p>ˇ
14238 "#
14239 .unindent(),
14240 );
14241
14242 // Toggle comments when different languages are active for different
14243 // selections.
14244 cx.set_state(
14245 &r#"
14246 ˇ<script>
14247 ˇvar x = new Y();
14248 ˇ</script>
14249 "#
14250 .unindent(),
14251 );
14252 cx.executor().run_until_parked();
14253 cx.update_editor(|editor, window, cx| {
14254 editor.toggle_comments(&ToggleComments::default(), window, cx)
14255 });
14256 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
14257 // Uncommenting and commenting from this position brings in even more wrong artifacts.
14258 cx.assert_editor_state(
14259 &r#"
14260 <!-- ˇ<script> -->
14261 // ˇvar x = new Y();
14262 <!-- ˇ</script> -->
14263 "#
14264 .unindent(),
14265 );
14266}
14267
14268#[gpui::test]
14269fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
14270 init_test(cx, |_| {});
14271
14272 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14273 let multibuffer = cx.new(|cx| {
14274 let mut multibuffer = MultiBuffer::new(ReadWrite);
14275 multibuffer.push_excerpts(
14276 buffer.clone(),
14277 [
14278 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
14279 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
14280 ],
14281 cx,
14282 );
14283 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
14284 multibuffer
14285 });
14286
14287 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
14288 editor.update_in(cx, |editor, window, cx| {
14289 assert_eq!(editor.text(cx), "aaaa\nbbbb");
14290 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14291 s.select_ranges([
14292 Point::new(0, 0)..Point::new(0, 0),
14293 Point::new(1, 0)..Point::new(1, 0),
14294 ])
14295 });
14296
14297 editor.handle_input("X", window, cx);
14298 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
14299 assert_eq!(
14300 editor.selections.ranges(cx),
14301 [
14302 Point::new(0, 1)..Point::new(0, 1),
14303 Point::new(1, 1)..Point::new(1, 1),
14304 ]
14305 );
14306
14307 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
14308 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14309 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
14310 });
14311 editor.backspace(&Default::default(), window, cx);
14312 assert_eq!(editor.text(cx), "Xa\nbbb");
14313 assert_eq!(
14314 editor.selections.ranges(cx),
14315 [Point::new(1, 0)..Point::new(1, 0)]
14316 );
14317
14318 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14319 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
14320 });
14321 editor.backspace(&Default::default(), window, cx);
14322 assert_eq!(editor.text(cx), "X\nbb");
14323 assert_eq!(
14324 editor.selections.ranges(cx),
14325 [Point::new(0, 1)..Point::new(0, 1)]
14326 );
14327 });
14328}
14329
14330#[gpui::test]
14331fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
14332 init_test(cx, |_| {});
14333
14334 let markers = vec![('[', ']').into(), ('(', ')').into()];
14335 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
14336 indoc! {"
14337 [aaaa
14338 (bbbb]
14339 cccc)",
14340 },
14341 markers.clone(),
14342 );
14343 let excerpt_ranges = markers.into_iter().map(|marker| {
14344 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
14345 ExcerptRange::new(context.clone())
14346 });
14347 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
14348 let multibuffer = cx.new(|cx| {
14349 let mut multibuffer = MultiBuffer::new(ReadWrite);
14350 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
14351 multibuffer
14352 });
14353
14354 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
14355 editor.update_in(cx, |editor, window, cx| {
14356 let (expected_text, selection_ranges) = marked_text_ranges(
14357 indoc! {"
14358 aaaa
14359 bˇbbb
14360 bˇbbˇb
14361 cccc"
14362 },
14363 true,
14364 );
14365 assert_eq!(editor.text(cx), expected_text);
14366 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14367 s.select_ranges(selection_ranges)
14368 });
14369
14370 editor.handle_input("X", window, cx);
14371
14372 let (expected_text, expected_selections) = marked_text_ranges(
14373 indoc! {"
14374 aaaa
14375 bXˇbbXb
14376 bXˇbbXˇb
14377 cccc"
14378 },
14379 false,
14380 );
14381 assert_eq!(editor.text(cx), expected_text);
14382 assert_eq!(editor.selections.ranges(cx), expected_selections);
14383
14384 editor.newline(&Newline, window, cx);
14385 let (expected_text, expected_selections) = marked_text_ranges(
14386 indoc! {"
14387 aaaa
14388 bX
14389 ˇbbX
14390 b
14391 bX
14392 ˇbbX
14393 ˇb
14394 cccc"
14395 },
14396 false,
14397 );
14398 assert_eq!(editor.text(cx), expected_text);
14399 assert_eq!(editor.selections.ranges(cx), expected_selections);
14400 });
14401}
14402
14403#[gpui::test]
14404fn test_refresh_selections(cx: &mut TestAppContext) {
14405 init_test(cx, |_| {});
14406
14407 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14408 let mut excerpt1_id = None;
14409 let multibuffer = cx.new(|cx| {
14410 let mut multibuffer = MultiBuffer::new(ReadWrite);
14411 excerpt1_id = multibuffer
14412 .push_excerpts(
14413 buffer.clone(),
14414 [
14415 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14416 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14417 ],
14418 cx,
14419 )
14420 .into_iter()
14421 .next();
14422 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14423 multibuffer
14424 });
14425
14426 let editor = cx.add_window(|window, cx| {
14427 let mut editor = build_editor(multibuffer.clone(), window, cx);
14428 let snapshot = editor.snapshot(window, cx);
14429 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14430 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
14431 });
14432 editor.begin_selection(
14433 Point::new(2, 1).to_display_point(&snapshot),
14434 true,
14435 1,
14436 window,
14437 cx,
14438 );
14439 assert_eq!(
14440 editor.selections.ranges(cx),
14441 [
14442 Point::new(1, 3)..Point::new(1, 3),
14443 Point::new(2, 1)..Point::new(2, 1),
14444 ]
14445 );
14446 editor
14447 });
14448
14449 // Refreshing selections is a no-op when excerpts haven't changed.
14450 _ = editor.update(cx, |editor, window, cx| {
14451 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14452 assert_eq!(
14453 editor.selections.ranges(cx),
14454 [
14455 Point::new(1, 3)..Point::new(1, 3),
14456 Point::new(2, 1)..Point::new(2, 1),
14457 ]
14458 );
14459 });
14460
14461 multibuffer.update(cx, |multibuffer, cx| {
14462 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14463 });
14464 _ = editor.update(cx, |editor, window, cx| {
14465 // Removing an excerpt causes the first selection to become degenerate.
14466 assert_eq!(
14467 editor.selections.ranges(cx),
14468 [
14469 Point::new(0, 0)..Point::new(0, 0),
14470 Point::new(0, 1)..Point::new(0, 1)
14471 ]
14472 );
14473
14474 // Refreshing selections will relocate the first selection to the original buffer
14475 // location.
14476 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14477 assert_eq!(
14478 editor.selections.ranges(cx),
14479 [
14480 Point::new(0, 1)..Point::new(0, 1),
14481 Point::new(0, 3)..Point::new(0, 3)
14482 ]
14483 );
14484 assert!(editor.selections.pending_anchor().is_some());
14485 });
14486}
14487
14488#[gpui::test]
14489fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
14490 init_test(cx, |_| {});
14491
14492 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14493 let mut excerpt1_id = None;
14494 let multibuffer = cx.new(|cx| {
14495 let mut multibuffer = MultiBuffer::new(ReadWrite);
14496 excerpt1_id = multibuffer
14497 .push_excerpts(
14498 buffer.clone(),
14499 [
14500 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14501 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14502 ],
14503 cx,
14504 )
14505 .into_iter()
14506 .next();
14507 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14508 multibuffer
14509 });
14510
14511 let editor = cx.add_window(|window, cx| {
14512 let mut editor = build_editor(multibuffer.clone(), window, cx);
14513 let snapshot = editor.snapshot(window, cx);
14514 editor.begin_selection(
14515 Point::new(1, 3).to_display_point(&snapshot),
14516 false,
14517 1,
14518 window,
14519 cx,
14520 );
14521 assert_eq!(
14522 editor.selections.ranges(cx),
14523 [Point::new(1, 3)..Point::new(1, 3)]
14524 );
14525 editor
14526 });
14527
14528 multibuffer.update(cx, |multibuffer, cx| {
14529 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14530 });
14531 _ = editor.update(cx, |editor, window, cx| {
14532 assert_eq!(
14533 editor.selections.ranges(cx),
14534 [Point::new(0, 0)..Point::new(0, 0)]
14535 );
14536
14537 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
14538 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14539 assert_eq!(
14540 editor.selections.ranges(cx),
14541 [Point::new(0, 3)..Point::new(0, 3)]
14542 );
14543 assert!(editor.selections.pending_anchor().is_some());
14544 });
14545}
14546
14547#[gpui::test]
14548async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
14549 init_test(cx, |_| {});
14550
14551 let language = Arc::new(
14552 Language::new(
14553 LanguageConfig {
14554 brackets: BracketPairConfig {
14555 pairs: vec![
14556 BracketPair {
14557 start: "{".to_string(),
14558 end: "}".to_string(),
14559 close: true,
14560 surround: true,
14561 newline: true,
14562 },
14563 BracketPair {
14564 start: "/* ".to_string(),
14565 end: " */".to_string(),
14566 close: true,
14567 surround: true,
14568 newline: true,
14569 },
14570 ],
14571 ..Default::default()
14572 },
14573 ..Default::default()
14574 },
14575 Some(tree_sitter_rust::LANGUAGE.into()),
14576 )
14577 .with_indents_query("")
14578 .unwrap(),
14579 );
14580
14581 let text = concat!(
14582 "{ }\n", //
14583 " x\n", //
14584 " /* */\n", //
14585 "x\n", //
14586 "{{} }\n", //
14587 );
14588
14589 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
14590 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14591 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14592 editor
14593 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
14594 .await;
14595
14596 editor.update_in(cx, |editor, window, cx| {
14597 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14598 s.select_display_ranges([
14599 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
14600 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
14601 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
14602 ])
14603 });
14604 editor.newline(&Newline, window, cx);
14605
14606 assert_eq!(
14607 editor.buffer().read(cx).read(cx).text(),
14608 concat!(
14609 "{ \n", // Suppress rustfmt
14610 "\n", //
14611 "}\n", //
14612 " x\n", //
14613 " /* \n", //
14614 " \n", //
14615 " */\n", //
14616 "x\n", //
14617 "{{} \n", //
14618 "}\n", //
14619 )
14620 );
14621 });
14622}
14623
14624#[gpui::test]
14625fn test_highlighted_ranges(cx: &mut TestAppContext) {
14626 init_test(cx, |_| {});
14627
14628 let editor = cx.add_window(|window, cx| {
14629 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
14630 build_editor(buffer.clone(), window, cx)
14631 });
14632
14633 _ = editor.update(cx, |editor, window, cx| {
14634 struct Type1;
14635 struct Type2;
14636
14637 let buffer = editor.buffer.read(cx).snapshot(cx);
14638
14639 let anchor_range =
14640 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
14641
14642 editor.highlight_background::<Type1>(
14643 &[
14644 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
14645 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
14646 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
14647 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
14648 ],
14649 |_| Hsla::red(),
14650 cx,
14651 );
14652 editor.highlight_background::<Type2>(
14653 &[
14654 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
14655 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
14656 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
14657 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
14658 ],
14659 |_| Hsla::green(),
14660 cx,
14661 );
14662
14663 let snapshot = editor.snapshot(window, cx);
14664 let mut highlighted_ranges = editor.background_highlights_in_range(
14665 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
14666 &snapshot,
14667 cx.theme(),
14668 );
14669 // Enforce a consistent ordering based on color without relying on the ordering of the
14670 // highlight's `TypeId` which is non-executor.
14671 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
14672 assert_eq!(
14673 highlighted_ranges,
14674 &[
14675 (
14676 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
14677 Hsla::red(),
14678 ),
14679 (
14680 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14681 Hsla::red(),
14682 ),
14683 (
14684 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
14685 Hsla::green(),
14686 ),
14687 (
14688 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
14689 Hsla::green(),
14690 ),
14691 ]
14692 );
14693 assert_eq!(
14694 editor.background_highlights_in_range(
14695 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
14696 &snapshot,
14697 cx.theme(),
14698 ),
14699 &[(
14700 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14701 Hsla::red(),
14702 )]
14703 );
14704 });
14705}
14706
14707#[gpui::test]
14708async fn test_following(cx: &mut TestAppContext) {
14709 init_test(cx, |_| {});
14710
14711 let fs = FakeFs::new(cx.executor());
14712 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14713
14714 let buffer = project.update(cx, |project, cx| {
14715 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
14716 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
14717 });
14718 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
14719 let follower = cx.update(|cx| {
14720 cx.open_window(
14721 WindowOptions {
14722 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
14723 gpui::Point::new(px(0.), px(0.)),
14724 gpui::Point::new(px(10.), px(80.)),
14725 ))),
14726 ..Default::default()
14727 },
14728 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
14729 )
14730 .unwrap()
14731 });
14732
14733 let is_still_following = Rc::new(RefCell::new(true));
14734 let follower_edit_event_count = Rc::new(RefCell::new(0));
14735 let pending_update = Rc::new(RefCell::new(None));
14736 let leader_entity = leader.root(cx).unwrap();
14737 let follower_entity = follower.root(cx).unwrap();
14738 _ = follower.update(cx, {
14739 let update = pending_update.clone();
14740 let is_still_following = is_still_following.clone();
14741 let follower_edit_event_count = follower_edit_event_count.clone();
14742 |_, window, cx| {
14743 cx.subscribe_in(
14744 &leader_entity,
14745 window,
14746 move |_, leader, event, window, cx| {
14747 leader.read(cx).add_event_to_update_proto(
14748 event,
14749 &mut update.borrow_mut(),
14750 window,
14751 cx,
14752 );
14753 },
14754 )
14755 .detach();
14756
14757 cx.subscribe_in(
14758 &follower_entity,
14759 window,
14760 move |_, _, event: &EditorEvent, _window, _cx| {
14761 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
14762 *is_still_following.borrow_mut() = false;
14763 }
14764
14765 if let EditorEvent::BufferEdited = event {
14766 *follower_edit_event_count.borrow_mut() += 1;
14767 }
14768 },
14769 )
14770 .detach();
14771 }
14772 });
14773
14774 // Update the selections only
14775 _ = leader.update(cx, |leader, window, cx| {
14776 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14777 s.select_ranges([1..1])
14778 });
14779 });
14780 follower
14781 .update(cx, |follower, window, cx| {
14782 follower.apply_update_proto(
14783 &project,
14784 pending_update.borrow_mut().take().unwrap(),
14785 window,
14786 cx,
14787 )
14788 })
14789 .unwrap()
14790 .await
14791 .unwrap();
14792 _ = follower.update(cx, |follower, _, cx| {
14793 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
14794 });
14795 assert!(*is_still_following.borrow());
14796 assert_eq!(*follower_edit_event_count.borrow(), 0);
14797
14798 // Update the scroll position only
14799 _ = leader.update(cx, |leader, window, cx| {
14800 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14801 });
14802 follower
14803 .update(cx, |follower, window, cx| {
14804 follower.apply_update_proto(
14805 &project,
14806 pending_update.borrow_mut().take().unwrap(),
14807 window,
14808 cx,
14809 )
14810 })
14811 .unwrap()
14812 .await
14813 .unwrap();
14814 assert_eq!(
14815 follower
14816 .update(cx, |follower, _, cx| follower.scroll_position(cx))
14817 .unwrap(),
14818 gpui::Point::new(1.5, 3.5)
14819 );
14820 assert!(*is_still_following.borrow());
14821 assert_eq!(*follower_edit_event_count.borrow(), 0);
14822
14823 // Update the selections and scroll position. The follower's scroll position is updated
14824 // via autoscroll, not via the leader's exact scroll position.
14825 _ = leader.update(cx, |leader, window, cx| {
14826 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14827 s.select_ranges([0..0])
14828 });
14829 leader.request_autoscroll(Autoscroll::newest(), cx);
14830 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14831 });
14832 follower
14833 .update(cx, |follower, window, cx| {
14834 follower.apply_update_proto(
14835 &project,
14836 pending_update.borrow_mut().take().unwrap(),
14837 window,
14838 cx,
14839 )
14840 })
14841 .unwrap()
14842 .await
14843 .unwrap();
14844 _ = follower.update(cx, |follower, _, cx| {
14845 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
14846 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
14847 });
14848 assert!(*is_still_following.borrow());
14849
14850 // Creating a pending selection that precedes another selection
14851 _ = leader.update(cx, |leader, window, cx| {
14852 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14853 s.select_ranges([1..1])
14854 });
14855 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
14856 });
14857 follower
14858 .update(cx, |follower, window, cx| {
14859 follower.apply_update_proto(
14860 &project,
14861 pending_update.borrow_mut().take().unwrap(),
14862 window,
14863 cx,
14864 )
14865 })
14866 .unwrap()
14867 .await
14868 .unwrap();
14869 _ = follower.update(cx, |follower, _, cx| {
14870 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
14871 });
14872 assert!(*is_still_following.borrow());
14873
14874 // Extend the pending selection so that it surrounds another selection
14875 _ = leader.update(cx, |leader, window, cx| {
14876 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
14877 });
14878 follower
14879 .update(cx, |follower, window, cx| {
14880 follower.apply_update_proto(
14881 &project,
14882 pending_update.borrow_mut().take().unwrap(),
14883 window,
14884 cx,
14885 )
14886 })
14887 .unwrap()
14888 .await
14889 .unwrap();
14890 _ = follower.update(cx, |follower, _, cx| {
14891 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
14892 });
14893
14894 // Scrolling locally breaks the follow
14895 _ = follower.update(cx, |follower, window, cx| {
14896 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
14897 follower.set_scroll_anchor(
14898 ScrollAnchor {
14899 anchor: top_anchor,
14900 offset: gpui::Point::new(0.0, 0.5),
14901 },
14902 window,
14903 cx,
14904 );
14905 });
14906 assert!(!(*is_still_following.borrow()));
14907}
14908
14909#[gpui::test]
14910async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
14911 init_test(cx, |_| {});
14912
14913 let fs = FakeFs::new(cx.executor());
14914 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14915 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14916 let pane = workspace
14917 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14918 .unwrap();
14919
14920 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14921
14922 let leader = pane.update_in(cx, |_, window, cx| {
14923 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
14924 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
14925 });
14926
14927 // Start following the editor when it has no excerpts.
14928 let mut state_message =
14929 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14930 let workspace_entity = workspace.root(cx).unwrap();
14931 let follower_1 = cx
14932 .update_window(*workspace.deref(), |_, window, cx| {
14933 Editor::from_state_proto(
14934 workspace_entity,
14935 ViewId {
14936 creator: CollaboratorId::PeerId(PeerId::default()),
14937 id: 0,
14938 },
14939 &mut state_message,
14940 window,
14941 cx,
14942 )
14943 })
14944 .unwrap()
14945 .unwrap()
14946 .await
14947 .unwrap();
14948
14949 let update_message = Rc::new(RefCell::new(None));
14950 follower_1.update_in(cx, {
14951 let update = update_message.clone();
14952 |_, window, cx| {
14953 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
14954 leader.read(cx).add_event_to_update_proto(
14955 event,
14956 &mut update.borrow_mut(),
14957 window,
14958 cx,
14959 );
14960 })
14961 .detach();
14962 }
14963 });
14964
14965 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
14966 (
14967 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
14968 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
14969 )
14970 });
14971
14972 // Insert some excerpts.
14973 leader.update(cx, |leader, cx| {
14974 leader.buffer.update(cx, |multibuffer, cx| {
14975 multibuffer.set_excerpts_for_path(
14976 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
14977 buffer_1.clone(),
14978 vec![
14979 Point::row_range(0..3),
14980 Point::row_range(1..6),
14981 Point::row_range(12..15),
14982 ],
14983 0,
14984 cx,
14985 );
14986 multibuffer.set_excerpts_for_path(
14987 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
14988 buffer_2.clone(),
14989 vec![Point::row_range(0..6), Point::row_range(8..12)],
14990 0,
14991 cx,
14992 );
14993 });
14994 });
14995
14996 // Apply the update of adding the excerpts.
14997 follower_1
14998 .update_in(cx, |follower, window, cx| {
14999 follower.apply_update_proto(
15000 &project,
15001 update_message.borrow().clone().unwrap(),
15002 window,
15003 cx,
15004 )
15005 })
15006 .await
15007 .unwrap();
15008 assert_eq!(
15009 follower_1.update(cx, |editor, cx| editor.text(cx)),
15010 leader.update(cx, |editor, cx| editor.text(cx))
15011 );
15012 update_message.borrow_mut().take();
15013
15014 // Start following separately after it already has excerpts.
15015 let mut state_message =
15016 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
15017 let workspace_entity = workspace.root(cx).unwrap();
15018 let follower_2 = cx
15019 .update_window(*workspace.deref(), |_, window, cx| {
15020 Editor::from_state_proto(
15021 workspace_entity,
15022 ViewId {
15023 creator: CollaboratorId::PeerId(PeerId::default()),
15024 id: 0,
15025 },
15026 &mut state_message,
15027 window,
15028 cx,
15029 )
15030 })
15031 .unwrap()
15032 .unwrap()
15033 .await
15034 .unwrap();
15035 assert_eq!(
15036 follower_2.update(cx, |editor, cx| editor.text(cx)),
15037 leader.update(cx, |editor, cx| editor.text(cx))
15038 );
15039
15040 // Remove some excerpts.
15041 leader.update(cx, |leader, cx| {
15042 leader.buffer.update(cx, |multibuffer, cx| {
15043 let excerpt_ids = multibuffer.excerpt_ids();
15044 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
15045 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
15046 });
15047 });
15048
15049 // Apply the update of removing the excerpts.
15050 follower_1
15051 .update_in(cx, |follower, window, cx| {
15052 follower.apply_update_proto(
15053 &project,
15054 update_message.borrow().clone().unwrap(),
15055 window,
15056 cx,
15057 )
15058 })
15059 .await
15060 .unwrap();
15061 follower_2
15062 .update_in(cx, |follower, window, cx| {
15063 follower.apply_update_proto(
15064 &project,
15065 update_message.borrow().clone().unwrap(),
15066 window,
15067 cx,
15068 )
15069 })
15070 .await
15071 .unwrap();
15072 update_message.borrow_mut().take();
15073 assert_eq!(
15074 follower_1.update(cx, |editor, cx| editor.text(cx)),
15075 leader.update(cx, |editor, cx| editor.text(cx))
15076 );
15077}
15078
15079#[gpui::test]
15080async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15081 init_test(cx, |_| {});
15082
15083 let mut cx = EditorTestContext::new(cx).await;
15084 let lsp_store =
15085 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
15086
15087 cx.set_state(indoc! {"
15088 ˇfn func(abc def: i32) -> u32 {
15089 }
15090 "});
15091
15092 cx.update(|_, cx| {
15093 lsp_store.update(cx, |lsp_store, cx| {
15094 lsp_store
15095 .update_diagnostics(
15096 LanguageServerId(0),
15097 lsp::PublishDiagnosticsParams {
15098 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
15099 version: None,
15100 diagnostics: vec![
15101 lsp::Diagnostic {
15102 range: lsp::Range::new(
15103 lsp::Position::new(0, 11),
15104 lsp::Position::new(0, 12),
15105 ),
15106 severity: Some(lsp::DiagnosticSeverity::ERROR),
15107 ..Default::default()
15108 },
15109 lsp::Diagnostic {
15110 range: lsp::Range::new(
15111 lsp::Position::new(0, 12),
15112 lsp::Position::new(0, 15),
15113 ),
15114 severity: Some(lsp::DiagnosticSeverity::ERROR),
15115 ..Default::default()
15116 },
15117 lsp::Diagnostic {
15118 range: lsp::Range::new(
15119 lsp::Position::new(0, 25),
15120 lsp::Position::new(0, 28),
15121 ),
15122 severity: Some(lsp::DiagnosticSeverity::ERROR),
15123 ..Default::default()
15124 },
15125 ],
15126 },
15127 None,
15128 DiagnosticSourceKind::Pushed,
15129 &[],
15130 cx,
15131 )
15132 .unwrap()
15133 });
15134 });
15135
15136 executor.run_until_parked();
15137
15138 cx.update_editor(|editor, window, cx| {
15139 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15140 });
15141
15142 cx.assert_editor_state(indoc! {"
15143 fn func(abc def: i32) -> ˇu32 {
15144 }
15145 "});
15146
15147 cx.update_editor(|editor, window, cx| {
15148 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15149 });
15150
15151 cx.assert_editor_state(indoc! {"
15152 fn func(abc ˇdef: i32) -> u32 {
15153 }
15154 "});
15155
15156 cx.update_editor(|editor, window, cx| {
15157 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15158 });
15159
15160 cx.assert_editor_state(indoc! {"
15161 fn func(abcˇ def: i32) -> u32 {
15162 }
15163 "});
15164
15165 cx.update_editor(|editor, window, cx| {
15166 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15167 });
15168
15169 cx.assert_editor_state(indoc! {"
15170 fn func(abc def: i32) -> ˇu32 {
15171 }
15172 "});
15173}
15174
15175#[gpui::test]
15176async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15177 init_test(cx, |_| {});
15178
15179 let mut cx = EditorTestContext::new(cx).await;
15180
15181 let diff_base = r#"
15182 use some::mod;
15183
15184 const A: u32 = 42;
15185
15186 fn main() {
15187 println!("hello");
15188
15189 println!("world");
15190 }
15191 "#
15192 .unindent();
15193
15194 // Edits are modified, removed, modified, added
15195 cx.set_state(
15196 &r#"
15197 use some::modified;
15198
15199 ˇ
15200 fn main() {
15201 println!("hello there");
15202
15203 println!("around the");
15204 println!("world");
15205 }
15206 "#
15207 .unindent(),
15208 );
15209
15210 cx.set_head_text(&diff_base);
15211 executor.run_until_parked();
15212
15213 cx.update_editor(|editor, window, cx| {
15214 //Wrap around the bottom of the buffer
15215 for _ in 0..3 {
15216 editor.go_to_next_hunk(&GoToHunk, window, cx);
15217 }
15218 });
15219
15220 cx.assert_editor_state(
15221 &r#"
15222 ˇuse some::modified;
15223
15224
15225 fn main() {
15226 println!("hello there");
15227
15228 println!("around the");
15229 println!("world");
15230 }
15231 "#
15232 .unindent(),
15233 );
15234
15235 cx.update_editor(|editor, window, cx| {
15236 //Wrap around the top of the buffer
15237 for _ in 0..2 {
15238 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15239 }
15240 });
15241
15242 cx.assert_editor_state(
15243 &r#"
15244 use some::modified;
15245
15246
15247 fn main() {
15248 ˇ println!("hello there");
15249
15250 println!("around the");
15251 println!("world");
15252 }
15253 "#
15254 .unindent(),
15255 );
15256
15257 cx.update_editor(|editor, window, cx| {
15258 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15259 });
15260
15261 cx.assert_editor_state(
15262 &r#"
15263 use some::modified;
15264
15265 ˇ
15266 fn main() {
15267 println!("hello there");
15268
15269 println!("around the");
15270 println!("world");
15271 }
15272 "#
15273 .unindent(),
15274 );
15275
15276 cx.update_editor(|editor, window, cx| {
15277 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15278 });
15279
15280 cx.assert_editor_state(
15281 &r#"
15282 ˇuse some::modified;
15283
15284
15285 fn main() {
15286 println!("hello there");
15287
15288 println!("around the");
15289 println!("world");
15290 }
15291 "#
15292 .unindent(),
15293 );
15294
15295 cx.update_editor(|editor, window, cx| {
15296 for _ in 0..2 {
15297 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15298 }
15299 });
15300
15301 cx.assert_editor_state(
15302 &r#"
15303 use some::modified;
15304
15305
15306 fn main() {
15307 ˇ println!("hello there");
15308
15309 println!("around the");
15310 println!("world");
15311 }
15312 "#
15313 .unindent(),
15314 );
15315
15316 cx.update_editor(|editor, window, cx| {
15317 editor.fold(&Fold, window, cx);
15318 });
15319
15320 cx.update_editor(|editor, window, cx| {
15321 editor.go_to_next_hunk(&GoToHunk, window, cx);
15322 });
15323
15324 cx.assert_editor_state(
15325 &r#"
15326 ˇuse some::modified;
15327
15328
15329 fn main() {
15330 println!("hello there");
15331
15332 println!("around the");
15333 println!("world");
15334 }
15335 "#
15336 .unindent(),
15337 );
15338}
15339
15340#[test]
15341fn test_split_words() {
15342 fn split(text: &str) -> Vec<&str> {
15343 split_words(text).collect()
15344 }
15345
15346 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
15347 assert_eq!(split("hello_world"), &["hello_", "world"]);
15348 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
15349 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
15350 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
15351 assert_eq!(split("helloworld"), &["helloworld"]);
15352
15353 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
15354}
15355
15356#[gpui::test]
15357async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
15358 init_test(cx, |_| {});
15359
15360 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
15361 let mut assert = |before, after| {
15362 let _state_context = cx.set_state(before);
15363 cx.run_until_parked();
15364 cx.update_editor(|editor, window, cx| {
15365 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
15366 });
15367 cx.run_until_parked();
15368 cx.assert_editor_state(after);
15369 };
15370
15371 // Outside bracket jumps to outside of matching bracket
15372 assert("console.logˇ(var);", "console.log(var)ˇ;");
15373 assert("console.log(var)ˇ;", "console.logˇ(var);");
15374
15375 // Inside bracket jumps to inside of matching bracket
15376 assert("console.log(ˇvar);", "console.log(varˇ);");
15377 assert("console.log(varˇ);", "console.log(ˇvar);");
15378
15379 // When outside a bracket and inside, favor jumping to the inside bracket
15380 assert(
15381 "console.log('foo', [1, 2, 3]ˇ);",
15382 "console.log(ˇ'foo', [1, 2, 3]);",
15383 );
15384 assert(
15385 "console.log(ˇ'foo', [1, 2, 3]);",
15386 "console.log('foo', [1, 2, 3]ˇ);",
15387 );
15388
15389 // Bias forward if two options are equally likely
15390 assert(
15391 "let result = curried_fun()ˇ();",
15392 "let result = curried_fun()()ˇ;",
15393 );
15394
15395 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
15396 assert(
15397 indoc! {"
15398 function test() {
15399 console.log('test')ˇ
15400 }"},
15401 indoc! {"
15402 function test() {
15403 console.logˇ('test')
15404 }"},
15405 );
15406}
15407
15408#[gpui::test]
15409async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
15410 init_test(cx, |_| {});
15411
15412 let fs = FakeFs::new(cx.executor());
15413 fs.insert_tree(
15414 path!("/a"),
15415 json!({
15416 "main.rs": "fn main() { let a = 5; }",
15417 "other.rs": "// Test file",
15418 }),
15419 )
15420 .await;
15421 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15422
15423 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15424 language_registry.add(Arc::new(Language::new(
15425 LanguageConfig {
15426 name: "Rust".into(),
15427 matcher: LanguageMatcher {
15428 path_suffixes: vec!["rs".to_string()],
15429 ..Default::default()
15430 },
15431 brackets: BracketPairConfig {
15432 pairs: vec![BracketPair {
15433 start: "{".to_string(),
15434 end: "}".to_string(),
15435 close: true,
15436 surround: true,
15437 newline: true,
15438 }],
15439 disabled_scopes_by_bracket_ix: Vec::new(),
15440 },
15441 ..Default::default()
15442 },
15443 Some(tree_sitter_rust::LANGUAGE.into()),
15444 )));
15445 let mut fake_servers = language_registry.register_fake_lsp(
15446 "Rust",
15447 FakeLspAdapter {
15448 capabilities: lsp::ServerCapabilities {
15449 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15450 first_trigger_character: "{".to_string(),
15451 more_trigger_character: None,
15452 }),
15453 ..Default::default()
15454 },
15455 ..Default::default()
15456 },
15457 );
15458
15459 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15460
15461 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15462
15463 let worktree_id = workspace
15464 .update(cx, |workspace, _, cx| {
15465 workspace.project().update(cx, |project, cx| {
15466 project.worktrees(cx).next().unwrap().read(cx).id()
15467 })
15468 })
15469 .unwrap();
15470
15471 let buffer = project
15472 .update(cx, |project, cx| {
15473 project.open_local_buffer(path!("/a/main.rs"), cx)
15474 })
15475 .await
15476 .unwrap();
15477 let editor_handle = workspace
15478 .update(cx, |workspace, window, cx| {
15479 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
15480 })
15481 .unwrap()
15482 .await
15483 .unwrap()
15484 .downcast::<Editor>()
15485 .unwrap();
15486
15487 cx.executor().start_waiting();
15488 let fake_server = fake_servers.next().await.unwrap();
15489
15490 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
15491 |params, _| async move {
15492 assert_eq!(
15493 params.text_document_position.text_document.uri,
15494 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
15495 );
15496 assert_eq!(
15497 params.text_document_position.position,
15498 lsp::Position::new(0, 21),
15499 );
15500
15501 Ok(Some(vec![lsp::TextEdit {
15502 new_text: "]".to_string(),
15503 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15504 }]))
15505 },
15506 );
15507
15508 editor_handle.update_in(cx, |editor, window, cx| {
15509 window.focus(&editor.focus_handle(cx));
15510 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15511 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
15512 });
15513 editor.handle_input("{", window, cx);
15514 });
15515
15516 cx.executor().run_until_parked();
15517
15518 buffer.update(cx, |buffer, _| {
15519 assert_eq!(
15520 buffer.text(),
15521 "fn main() { let a = {5}; }",
15522 "No extra braces from on type formatting should appear in the buffer"
15523 )
15524 });
15525}
15526
15527#[gpui::test(iterations = 20, seeds(31))]
15528async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
15529 init_test(cx, |_| {});
15530
15531 let mut cx = EditorLspTestContext::new_rust(
15532 lsp::ServerCapabilities {
15533 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15534 first_trigger_character: ".".to_string(),
15535 more_trigger_character: None,
15536 }),
15537 ..Default::default()
15538 },
15539 cx,
15540 )
15541 .await;
15542
15543 cx.update_buffer(|buffer, _| {
15544 // This causes autoindent to be async.
15545 buffer.set_sync_parse_timeout(Duration::ZERO)
15546 });
15547
15548 cx.set_state("fn c() {\n d()ˇ\n}\n");
15549 cx.simulate_keystroke("\n");
15550 cx.run_until_parked();
15551
15552 let buffer_cloned =
15553 cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap().clone());
15554 let mut request =
15555 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
15556 let buffer_cloned = buffer_cloned.clone();
15557 async move {
15558 buffer_cloned.update(&mut cx, |buffer, _| {
15559 assert_eq!(
15560 buffer.text(),
15561 "fn c() {\n d()\n .\n}\n",
15562 "OnTypeFormatting should triggered after autoindent applied"
15563 )
15564 })?;
15565
15566 Ok(Some(vec![]))
15567 }
15568 });
15569
15570 cx.simulate_keystroke(".");
15571 cx.run_until_parked();
15572
15573 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
15574 assert!(request.next().await.is_some());
15575 request.close();
15576 assert!(request.next().await.is_none());
15577}
15578
15579#[gpui::test]
15580async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
15581 init_test(cx, |_| {});
15582
15583 let fs = FakeFs::new(cx.executor());
15584 fs.insert_tree(
15585 path!("/a"),
15586 json!({
15587 "main.rs": "fn main() { let a = 5; }",
15588 "other.rs": "// Test file",
15589 }),
15590 )
15591 .await;
15592
15593 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15594
15595 let server_restarts = Arc::new(AtomicUsize::new(0));
15596 let closure_restarts = Arc::clone(&server_restarts);
15597 let language_server_name = "test language server";
15598 let language_name: LanguageName = "Rust".into();
15599
15600 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15601 language_registry.add(Arc::new(Language::new(
15602 LanguageConfig {
15603 name: language_name.clone(),
15604 matcher: LanguageMatcher {
15605 path_suffixes: vec!["rs".to_string()],
15606 ..Default::default()
15607 },
15608 ..Default::default()
15609 },
15610 Some(tree_sitter_rust::LANGUAGE.into()),
15611 )));
15612 let mut fake_servers = language_registry.register_fake_lsp(
15613 "Rust",
15614 FakeLspAdapter {
15615 name: language_server_name,
15616 initialization_options: Some(json!({
15617 "testOptionValue": true
15618 })),
15619 initializer: Some(Box::new(move |fake_server| {
15620 let task_restarts = Arc::clone(&closure_restarts);
15621 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
15622 task_restarts.fetch_add(1, atomic::Ordering::Release);
15623 futures::future::ready(Ok(()))
15624 });
15625 })),
15626 ..Default::default()
15627 },
15628 );
15629
15630 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15631 let _buffer = project
15632 .update(cx, |project, cx| {
15633 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
15634 })
15635 .await
15636 .unwrap();
15637 let _fake_server = fake_servers.next().await.unwrap();
15638 update_test_language_settings(cx, |language_settings| {
15639 language_settings.languages.0.insert(
15640 language_name.clone(),
15641 LanguageSettingsContent {
15642 tab_size: NonZeroU32::new(8),
15643 ..Default::default()
15644 },
15645 );
15646 });
15647 cx.executor().run_until_parked();
15648 assert_eq!(
15649 server_restarts.load(atomic::Ordering::Acquire),
15650 0,
15651 "Should not restart LSP server on an unrelated change"
15652 );
15653
15654 update_test_project_settings(cx, |project_settings| {
15655 project_settings.lsp.insert(
15656 "Some other server name".into(),
15657 LspSettings {
15658 binary: None,
15659 settings: None,
15660 initialization_options: Some(json!({
15661 "some other init value": false
15662 })),
15663 enable_lsp_tasks: false,
15664 },
15665 );
15666 });
15667 cx.executor().run_until_parked();
15668 assert_eq!(
15669 server_restarts.load(atomic::Ordering::Acquire),
15670 0,
15671 "Should not restart LSP server on an unrelated LSP settings change"
15672 );
15673
15674 update_test_project_settings(cx, |project_settings| {
15675 project_settings.lsp.insert(
15676 language_server_name.into(),
15677 LspSettings {
15678 binary: None,
15679 settings: None,
15680 initialization_options: Some(json!({
15681 "anotherInitValue": false
15682 })),
15683 enable_lsp_tasks: false,
15684 },
15685 );
15686 });
15687 cx.executor().run_until_parked();
15688 assert_eq!(
15689 server_restarts.load(atomic::Ordering::Acquire),
15690 1,
15691 "Should restart LSP server on a related LSP settings change"
15692 );
15693
15694 update_test_project_settings(cx, |project_settings| {
15695 project_settings.lsp.insert(
15696 language_server_name.into(),
15697 LspSettings {
15698 binary: None,
15699 settings: None,
15700 initialization_options: Some(json!({
15701 "anotherInitValue": false
15702 })),
15703 enable_lsp_tasks: false,
15704 },
15705 );
15706 });
15707 cx.executor().run_until_parked();
15708 assert_eq!(
15709 server_restarts.load(atomic::Ordering::Acquire),
15710 1,
15711 "Should not restart LSP server on a related LSP settings change that is the same"
15712 );
15713
15714 update_test_project_settings(cx, |project_settings| {
15715 project_settings.lsp.insert(
15716 language_server_name.into(),
15717 LspSettings {
15718 binary: None,
15719 settings: None,
15720 initialization_options: None,
15721 enable_lsp_tasks: false,
15722 },
15723 );
15724 });
15725 cx.executor().run_until_parked();
15726 assert_eq!(
15727 server_restarts.load(atomic::Ordering::Acquire),
15728 2,
15729 "Should restart LSP server on another related LSP settings change"
15730 );
15731}
15732
15733#[gpui::test]
15734async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
15735 init_test(cx, |_| {});
15736
15737 let mut cx = EditorLspTestContext::new_rust(
15738 lsp::ServerCapabilities {
15739 completion_provider: Some(lsp::CompletionOptions {
15740 trigger_characters: Some(vec![".".to_string()]),
15741 resolve_provider: Some(true),
15742 ..Default::default()
15743 }),
15744 ..Default::default()
15745 },
15746 cx,
15747 )
15748 .await;
15749
15750 cx.set_state("fn main() { let a = 2ˇ; }");
15751 cx.simulate_keystroke(".");
15752 let completion_item = lsp::CompletionItem {
15753 label: "some".into(),
15754 kind: Some(lsp::CompletionItemKind::SNIPPET),
15755 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15756 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15757 kind: lsp::MarkupKind::Markdown,
15758 value: "```rust\nSome(2)\n```".to_string(),
15759 })),
15760 deprecated: Some(false),
15761 sort_text: Some("fffffff2".to_string()),
15762 filter_text: Some("some".to_string()),
15763 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15764 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15765 range: lsp::Range {
15766 start: lsp::Position {
15767 line: 0,
15768 character: 22,
15769 },
15770 end: lsp::Position {
15771 line: 0,
15772 character: 22,
15773 },
15774 },
15775 new_text: "Some(2)".to_string(),
15776 })),
15777 additional_text_edits: Some(vec![lsp::TextEdit {
15778 range: lsp::Range {
15779 start: lsp::Position {
15780 line: 0,
15781 character: 20,
15782 },
15783 end: lsp::Position {
15784 line: 0,
15785 character: 22,
15786 },
15787 },
15788 new_text: "".to_string(),
15789 }]),
15790 ..Default::default()
15791 };
15792
15793 let closure_completion_item = completion_item.clone();
15794 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15795 let task_completion_item = closure_completion_item.clone();
15796 async move {
15797 Ok(Some(lsp::CompletionResponse::Array(vec![
15798 task_completion_item,
15799 ])))
15800 }
15801 });
15802
15803 request.next().await;
15804
15805 cx.condition(|editor, _| editor.context_menu_visible())
15806 .await;
15807 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15808 editor
15809 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15810 .unwrap()
15811 });
15812 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
15813
15814 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
15815 let task_completion_item = completion_item.clone();
15816 async move { Ok(task_completion_item) }
15817 })
15818 .next()
15819 .await
15820 .unwrap();
15821 apply_additional_edits.await.unwrap();
15822 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
15823}
15824
15825#[gpui::test]
15826async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
15827 init_test(cx, |_| {});
15828
15829 let mut cx = EditorLspTestContext::new_rust(
15830 lsp::ServerCapabilities {
15831 completion_provider: Some(lsp::CompletionOptions {
15832 trigger_characters: Some(vec![".".to_string()]),
15833 resolve_provider: Some(true),
15834 ..Default::default()
15835 }),
15836 ..Default::default()
15837 },
15838 cx,
15839 )
15840 .await;
15841
15842 cx.set_state("fn main() { let a = 2ˇ; }");
15843 cx.simulate_keystroke(".");
15844
15845 let item1 = lsp::CompletionItem {
15846 label: "method id()".to_string(),
15847 filter_text: Some("id".to_string()),
15848 detail: None,
15849 documentation: None,
15850 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15851 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15852 new_text: ".id".to_string(),
15853 })),
15854 ..lsp::CompletionItem::default()
15855 };
15856
15857 let item2 = lsp::CompletionItem {
15858 label: "other".to_string(),
15859 filter_text: Some("other".to_string()),
15860 detail: None,
15861 documentation: None,
15862 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15863 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15864 new_text: ".other".to_string(),
15865 })),
15866 ..lsp::CompletionItem::default()
15867 };
15868
15869 let item1 = item1.clone();
15870 cx.set_request_handler::<lsp::request::Completion, _, _>({
15871 let item1 = item1.clone();
15872 move |_, _, _| {
15873 let item1 = item1.clone();
15874 let item2 = item2.clone();
15875 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
15876 }
15877 })
15878 .next()
15879 .await;
15880
15881 cx.condition(|editor, _| editor.context_menu_visible())
15882 .await;
15883 cx.update_editor(|editor, _, _| {
15884 let context_menu = editor.context_menu.borrow_mut();
15885 let context_menu = context_menu
15886 .as_ref()
15887 .expect("Should have the context menu deployed");
15888 match context_menu {
15889 CodeContextMenu::Completions(completions_menu) => {
15890 let completions = completions_menu.completions.borrow_mut();
15891 assert_eq!(
15892 completions
15893 .iter()
15894 .map(|completion| &completion.label.text)
15895 .collect::<Vec<_>>(),
15896 vec!["method id()", "other"]
15897 )
15898 }
15899 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15900 }
15901 });
15902
15903 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
15904 let item1 = item1.clone();
15905 move |_, item_to_resolve, _| {
15906 let item1 = item1.clone();
15907 async move {
15908 if item1 == item_to_resolve {
15909 Ok(lsp::CompletionItem {
15910 label: "method id()".to_string(),
15911 filter_text: Some("id".to_string()),
15912 detail: Some("Now resolved!".to_string()),
15913 documentation: Some(lsp::Documentation::String("Docs".to_string())),
15914 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15915 range: lsp::Range::new(
15916 lsp::Position::new(0, 22),
15917 lsp::Position::new(0, 22),
15918 ),
15919 new_text: ".id".to_string(),
15920 })),
15921 ..lsp::CompletionItem::default()
15922 })
15923 } else {
15924 Ok(item_to_resolve)
15925 }
15926 }
15927 }
15928 })
15929 .next()
15930 .await
15931 .unwrap();
15932 cx.run_until_parked();
15933
15934 cx.update_editor(|editor, window, cx| {
15935 editor.context_menu_next(&Default::default(), window, cx);
15936 });
15937
15938 cx.update_editor(|editor, _, _| {
15939 let context_menu = editor.context_menu.borrow_mut();
15940 let context_menu = context_menu
15941 .as_ref()
15942 .expect("Should have the context menu deployed");
15943 match context_menu {
15944 CodeContextMenu::Completions(completions_menu) => {
15945 let completions = completions_menu.completions.borrow_mut();
15946 assert_eq!(
15947 completions
15948 .iter()
15949 .map(|completion| &completion.label.text)
15950 .collect::<Vec<_>>(),
15951 vec!["method id() Now resolved!", "other"],
15952 "Should update first completion label, but not second as the filter text did not match."
15953 );
15954 }
15955 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15956 }
15957 });
15958}
15959
15960#[gpui::test]
15961async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
15962 init_test(cx, |_| {});
15963 let mut cx = EditorLspTestContext::new_rust(
15964 lsp::ServerCapabilities {
15965 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
15966 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
15967 completion_provider: Some(lsp::CompletionOptions {
15968 resolve_provider: Some(true),
15969 ..Default::default()
15970 }),
15971 ..Default::default()
15972 },
15973 cx,
15974 )
15975 .await;
15976 cx.set_state(indoc! {"
15977 struct TestStruct {
15978 field: i32
15979 }
15980
15981 fn mainˇ() {
15982 let unused_var = 42;
15983 let test_struct = TestStruct { field: 42 };
15984 }
15985 "});
15986 let symbol_range = cx.lsp_range(indoc! {"
15987 struct TestStruct {
15988 field: i32
15989 }
15990
15991 «fn main»() {
15992 let unused_var = 42;
15993 let test_struct = TestStruct { field: 42 };
15994 }
15995 "});
15996 let mut hover_requests =
15997 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
15998 Ok(Some(lsp::Hover {
15999 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
16000 kind: lsp::MarkupKind::Markdown,
16001 value: "Function documentation".to_string(),
16002 }),
16003 range: Some(symbol_range),
16004 }))
16005 });
16006
16007 // Case 1: Test that code action menu hide hover popover
16008 cx.dispatch_action(Hover);
16009 hover_requests.next().await;
16010 cx.condition(|editor, _| editor.hover_state.visible()).await;
16011 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
16012 move |_, _, _| async move {
16013 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
16014 lsp::CodeAction {
16015 title: "Remove unused variable".to_string(),
16016 kind: Some(CodeActionKind::QUICKFIX),
16017 edit: Some(lsp::WorkspaceEdit {
16018 changes: Some(
16019 [(
16020 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
16021 vec![lsp::TextEdit {
16022 range: lsp::Range::new(
16023 lsp::Position::new(5, 4),
16024 lsp::Position::new(5, 27),
16025 ),
16026 new_text: "".to_string(),
16027 }],
16028 )]
16029 .into_iter()
16030 .collect(),
16031 ),
16032 ..Default::default()
16033 }),
16034 ..Default::default()
16035 },
16036 )]))
16037 },
16038 );
16039 cx.update_editor(|editor, window, cx| {
16040 editor.toggle_code_actions(
16041 &ToggleCodeActions {
16042 deployed_from: None,
16043 quick_launch: false,
16044 },
16045 window,
16046 cx,
16047 );
16048 });
16049 code_action_requests.next().await;
16050 cx.run_until_parked();
16051 cx.condition(|editor, _| editor.context_menu_visible())
16052 .await;
16053 cx.update_editor(|editor, _, _| {
16054 assert!(
16055 !editor.hover_state.visible(),
16056 "Hover popover should be hidden when code action menu is shown"
16057 );
16058 // Hide code actions
16059 editor.context_menu.take();
16060 });
16061
16062 // Case 2: Test that code completions hide hover popover
16063 cx.dispatch_action(Hover);
16064 hover_requests.next().await;
16065 cx.condition(|editor, _| editor.hover_state.visible()).await;
16066 let counter = Arc::new(AtomicUsize::new(0));
16067 let mut completion_requests =
16068 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16069 let counter = counter.clone();
16070 async move {
16071 counter.fetch_add(1, atomic::Ordering::Release);
16072 Ok(Some(lsp::CompletionResponse::Array(vec![
16073 lsp::CompletionItem {
16074 label: "main".into(),
16075 kind: Some(lsp::CompletionItemKind::FUNCTION),
16076 detail: Some("() -> ()".to_string()),
16077 ..Default::default()
16078 },
16079 lsp::CompletionItem {
16080 label: "TestStruct".into(),
16081 kind: Some(lsp::CompletionItemKind::STRUCT),
16082 detail: Some("struct TestStruct".to_string()),
16083 ..Default::default()
16084 },
16085 ])))
16086 }
16087 });
16088 cx.update_editor(|editor, window, cx| {
16089 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
16090 });
16091 completion_requests.next().await;
16092 cx.condition(|editor, _| editor.context_menu_visible())
16093 .await;
16094 cx.update_editor(|editor, _, _| {
16095 assert!(
16096 !editor.hover_state.visible(),
16097 "Hover popover should be hidden when completion menu is shown"
16098 );
16099 });
16100}
16101
16102#[gpui::test]
16103async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
16104 init_test(cx, |_| {});
16105
16106 let mut cx = EditorLspTestContext::new_rust(
16107 lsp::ServerCapabilities {
16108 completion_provider: Some(lsp::CompletionOptions {
16109 trigger_characters: Some(vec![".".to_string()]),
16110 resolve_provider: Some(true),
16111 ..Default::default()
16112 }),
16113 ..Default::default()
16114 },
16115 cx,
16116 )
16117 .await;
16118
16119 cx.set_state("fn main() { let a = 2ˇ; }");
16120 cx.simulate_keystroke(".");
16121
16122 let unresolved_item_1 = lsp::CompletionItem {
16123 label: "id".to_string(),
16124 filter_text: Some("id".to_string()),
16125 detail: None,
16126 documentation: None,
16127 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16128 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16129 new_text: ".id".to_string(),
16130 })),
16131 ..lsp::CompletionItem::default()
16132 };
16133 let resolved_item_1 = lsp::CompletionItem {
16134 additional_text_edits: Some(vec![lsp::TextEdit {
16135 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
16136 new_text: "!!".to_string(),
16137 }]),
16138 ..unresolved_item_1.clone()
16139 };
16140 let unresolved_item_2 = lsp::CompletionItem {
16141 label: "other".to_string(),
16142 filter_text: Some("other".to_string()),
16143 detail: None,
16144 documentation: None,
16145 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16146 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16147 new_text: ".other".to_string(),
16148 })),
16149 ..lsp::CompletionItem::default()
16150 };
16151 let resolved_item_2 = lsp::CompletionItem {
16152 additional_text_edits: Some(vec![lsp::TextEdit {
16153 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
16154 new_text: "??".to_string(),
16155 }]),
16156 ..unresolved_item_2.clone()
16157 };
16158
16159 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
16160 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
16161 cx.lsp
16162 .server
16163 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
16164 let unresolved_item_1 = unresolved_item_1.clone();
16165 let resolved_item_1 = resolved_item_1.clone();
16166 let unresolved_item_2 = unresolved_item_2.clone();
16167 let resolved_item_2 = resolved_item_2.clone();
16168 let resolve_requests_1 = resolve_requests_1.clone();
16169 let resolve_requests_2 = resolve_requests_2.clone();
16170 move |unresolved_request, _| {
16171 let unresolved_item_1 = unresolved_item_1.clone();
16172 let resolved_item_1 = resolved_item_1.clone();
16173 let unresolved_item_2 = unresolved_item_2.clone();
16174 let resolved_item_2 = resolved_item_2.clone();
16175 let resolve_requests_1 = resolve_requests_1.clone();
16176 let resolve_requests_2 = resolve_requests_2.clone();
16177 async move {
16178 if unresolved_request == unresolved_item_1 {
16179 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
16180 Ok(resolved_item_1.clone())
16181 } else if unresolved_request == unresolved_item_2 {
16182 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
16183 Ok(resolved_item_2.clone())
16184 } else {
16185 panic!("Unexpected completion item {unresolved_request:?}")
16186 }
16187 }
16188 }
16189 })
16190 .detach();
16191
16192 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16193 let unresolved_item_1 = unresolved_item_1.clone();
16194 let unresolved_item_2 = unresolved_item_2.clone();
16195 async move {
16196 Ok(Some(lsp::CompletionResponse::Array(vec![
16197 unresolved_item_1,
16198 unresolved_item_2,
16199 ])))
16200 }
16201 })
16202 .next()
16203 .await;
16204
16205 cx.condition(|editor, _| editor.context_menu_visible())
16206 .await;
16207 cx.update_editor(|editor, _, _| {
16208 let context_menu = editor.context_menu.borrow_mut();
16209 let context_menu = context_menu
16210 .as_ref()
16211 .expect("Should have the context menu deployed");
16212 match context_menu {
16213 CodeContextMenu::Completions(completions_menu) => {
16214 let completions = completions_menu.completions.borrow_mut();
16215 assert_eq!(
16216 completions
16217 .iter()
16218 .map(|completion| &completion.label.text)
16219 .collect::<Vec<_>>(),
16220 vec!["id", "other"]
16221 )
16222 }
16223 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
16224 }
16225 });
16226 cx.run_until_parked();
16227
16228 cx.update_editor(|editor, window, cx| {
16229 editor.context_menu_next(&ContextMenuNext, window, cx);
16230 });
16231 cx.run_until_parked();
16232 cx.update_editor(|editor, window, cx| {
16233 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
16234 });
16235 cx.run_until_parked();
16236 cx.update_editor(|editor, window, cx| {
16237 editor.context_menu_next(&ContextMenuNext, window, cx);
16238 });
16239 cx.run_until_parked();
16240 cx.update_editor(|editor, window, cx| {
16241 editor
16242 .compose_completion(&ComposeCompletion::default(), window, cx)
16243 .expect("No task returned")
16244 })
16245 .await
16246 .expect("Completion failed");
16247 cx.run_until_parked();
16248
16249 cx.update_editor(|editor, _, cx| {
16250 assert_eq!(
16251 resolve_requests_1.load(atomic::Ordering::Acquire),
16252 1,
16253 "Should always resolve once despite multiple selections"
16254 );
16255 assert_eq!(
16256 resolve_requests_2.load(atomic::Ordering::Acquire),
16257 1,
16258 "Should always resolve once after multiple selections and applying the completion"
16259 );
16260 assert_eq!(
16261 editor.text(cx),
16262 "fn main() { let a = ??.other; }",
16263 "Should use resolved data when applying the completion"
16264 );
16265 });
16266}
16267
16268#[gpui::test]
16269async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
16270 init_test(cx, |_| {});
16271
16272 let item_0 = lsp::CompletionItem {
16273 label: "abs".into(),
16274 insert_text: Some("abs".into()),
16275 data: Some(json!({ "very": "special"})),
16276 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
16277 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
16278 lsp::InsertReplaceEdit {
16279 new_text: "abs".to_string(),
16280 insert: lsp::Range::default(),
16281 replace: lsp::Range::default(),
16282 },
16283 )),
16284 ..lsp::CompletionItem::default()
16285 };
16286 let items = iter::once(item_0.clone())
16287 .chain((11..51).map(|i| lsp::CompletionItem {
16288 label: format!("item_{}", i),
16289 insert_text: Some(format!("item_{}", i)),
16290 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
16291 ..lsp::CompletionItem::default()
16292 }))
16293 .collect::<Vec<_>>();
16294
16295 let default_commit_characters = vec!["?".to_string()];
16296 let default_data = json!({ "default": "data"});
16297 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
16298 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
16299 let default_edit_range = lsp::Range {
16300 start: lsp::Position {
16301 line: 0,
16302 character: 5,
16303 },
16304 end: lsp::Position {
16305 line: 0,
16306 character: 5,
16307 },
16308 };
16309
16310 let mut cx = EditorLspTestContext::new_rust(
16311 lsp::ServerCapabilities {
16312 completion_provider: Some(lsp::CompletionOptions {
16313 trigger_characters: Some(vec![".".to_string()]),
16314 resolve_provider: Some(true),
16315 ..Default::default()
16316 }),
16317 ..Default::default()
16318 },
16319 cx,
16320 )
16321 .await;
16322
16323 cx.set_state("fn main() { let a = 2ˇ; }");
16324 cx.simulate_keystroke(".");
16325
16326 let completion_data = default_data.clone();
16327 let completion_characters = default_commit_characters.clone();
16328 let completion_items = items.clone();
16329 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16330 let default_data = completion_data.clone();
16331 let default_commit_characters = completion_characters.clone();
16332 let items = completion_items.clone();
16333 async move {
16334 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16335 items,
16336 item_defaults: Some(lsp::CompletionListItemDefaults {
16337 data: Some(default_data.clone()),
16338 commit_characters: Some(default_commit_characters.clone()),
16339 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
16340 default_edit_range,
16341 )),
16342 insert_text_format: Some(default_insert_text_format),
16343 insert_text_mode: Some(default_insert_text_mode),
16344 }),
16345 ..lsp::CompletionList::default()
16346 })))
16347 }
16348 })
16349 .next()
16350 .await;
16351
16352 let resolved_items = Arc::new(Mutex::new(Vec::new()));
16353 cx.lsp
16354 .server
16355 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
16356 let closure_resolved_items = resolved_items.clone();
16357 move |item_to_resolve, _| {
16358 let closure_resolved_items = closure_resolved_items.clone();
16359 async move {
16360 closure_resolved_items.lock().push(item_to_resolve.clone());
16361 Ok(item_to_resolve)
16362 }
16363 }
16364 })
16365 .detach();
16366
16367 cx.condition(|editor, _| editor.context_menu_visible())
16368 .await;
16369 cx.run_until_parked();
16370 cx.update_editor(|editor, _, _| {
16371 let menu = editor.context_menu.borrow_mut();
16372 match menu.as_ref().expect("should have the completions menu") {
16373 CodeContextMenu::Completions(completions_menu) => {
16374 assert_eq!(
16375 completions_menu
16376 .entries
16377 .borrow()
16378 .iter()
16379 .map(|mat| mat.string.clone())
16380 .collect::<Vec<String>>(),
16381 items
16382 .iter()
16383 .map(|completion| completion.label.clone())
16384 .collect::<Vec<String>>()
16385 );
16386 }
16387 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
16388 }
16389 });
16390 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
16391 // with 4 from the end.
16392 assert_eq!(
16393 *resolved_items.lock(),
16394 [&items[0..16], &items[items.len() - 4..items.len()]]
16395 .concat()
16396 .iter()
16397 .cloned()
16398 .map(|mut item| {
16399 if item.data.is_none() {
16400 item.data = Some(default_data.clone());
16401 }
16402 item
16403 })
16404 .collect::<Vec<lsp::CompletionItem>>(),
16405 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
16406 );
16407 resolved_items.lock().clear();
16408
16409 cx.update_editor(|editor, window, cx| {
16410 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
16411 });
16412 cx.run_until_parked();
16413 // Completions that have already been resolved are skipped.
16414 assert_eq!(
16415 *resolved_items.lock(),
16416 items[items.len() - 17..items.len() - 4]
16417 .iter()
16418 .cloned()
16419 .map(|mut item| {
16420 if item.data.is_none() {
16421 item.data = Some(default_data.clone());
16422 }
16423 item
16424 })
16425 .collect::<Vec<lsp::CompletionItem>>()
16426 );
16427 resolved_items.lock().clear();
16428}
16429
16430#[gpui::test]
16431async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
16432 init_test(cx, |_| {});
16433
16434 let mut cx = EditorLspTestContext::new(
16435 Language::new(
16436 LanguageConfig {
16437 matcher: LanguageMatcher {
16438 path_suffixes: vec!["jsx".into()],
16439 ..Default::default()
16440 },
16441 overrides: [(
16442 "element".into(),
16443 LanguageConfigOverride {
16444 completion_query_characters: Override::Set(['-'].into_iter().collect()),
16445 ..Default::default()
16446 },
16447 )]
16448 .into_iter()
16449 .collect(),
16450 ..Default::default()
16451 },
16452 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16453 )
16454 .with_override_query("(jsx_self_closing_element) @element")
16455 .unwrap(),
16456 lsp::ServerCapabilities {
16457 completion_provider: Some(lsp::CompletionOptions {
16458 trigger_characters: Some(vec![":".to_string()]),
16459 ..Default::default()
16460 }),
16461 ..Default::default()
16462 },
16463 cx,
16464 )
16465 .await;
16466
16467 cx.lsp
16468 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16469 Ok(Some(lsp::CompletionResponse::Array(vec![
16470 lsp::CompletionItem {
16471 label: "bg-blue".into(),
16472 ..Default::default()
16473 },
16474 lsp::CompletionItem {
16475 label: "bg-red".into(),
16476 ..Default::default()
16477 },
16478 lsp::CompletionItem {
16479 label: "bg-yellow".into(),
16480 ..Default::default()
16481 },
16482 ])))
16483 });
16484
16485 cx.set_state(r#"<p class="bgˇ" />"#);
16486
16487 // Trigger completion when typing a dash, because the dash is an extra
16488 // word character in the 'element' scope, which contains the cursor.
16489 cx.simulate_keystroke("-");
16490 cx.executor().run_until_parked();
16491 cx.update_editor(|editor, _, _| {
16492 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16493 {
16494 assert_eq!(
16495 completion_menu_entries(&menu),
16496 &["bg-blue", "bg-red", "bg-yellow"]
16497 );
16498 } else {
16499 panic!("expected completion menu to be open");
16500 }
16501 });
16502
16503 cx.simulate_keystroke("l");
16504 cx.executor().run_until_parked();
16505 cx.update_editor(|editor, _, _| {
16506 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16507 {
16508 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
16509 } else {
16510 panic!("expected completion menu to be open");
16511 }
16512 });
16513
16514 // When filtering completions, consider the character after the '-' to
16515 // be the start of a subword.
16516 cx.set_state(r#"<p class="yelˇ" />"#);
16517 cx.simulate_keystroke("l");
16518 cx.executor().run_until_parked();
16519 cx.update_editor(|editor, _, _| {
16520 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16521 {
16522 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
16523 } else {
16524 panic!("expected completion menu to be open");
16525 }
16526 });
16527}
16528
16529fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
16530 let entries = menu.entries.borrow();
16531 entries.iter().map(|mat| mat.string.clone()).collect()
16532}
16533
16534#[gpui::test]
16535async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
16536 init_test(cx, |settings| {
16537 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
16538 Formatter::Prettier,
16539 )))
16540 });
16541
16542 let fs = FakeFs::new(cx.executor());
16543 fs.insert_file(path!("/file.ts"), Default::default()).await;
16544
16545 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
16546 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16547
16548 language_registry.add(Arc::new(Language::new(
16549 LanguageConfig {
16550 name: "TypeScript".into(),
16551 matcher: LanguageMatcher {
16552 path_suffixes: vec!["ts".to_string()],
16553 ..Default::default()
16554 },
16555 ..Default::default()
16556 },
16557 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
16558 )));
16559 update_test_language_settings(cx, |settings| {
16560 settings.defaults.prettier = Some(PrettierSettings {
16561 allowed: true,
16562 ..PrettierSettings::default()
16563 });
16564 });
16565
16566 let test_plugin = "test_plugin";
16567 let _ = language_registry.register_fake_lsp(
16568 "TypeScript",
16569 FakeLspAdapter {
16570 prettier_plugins: vec![test_plugin],
16571 ..Default::default()
16572 },
16573 );
16574
16575 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
16576 let buffer = project
16577 .update(cx, |project, cx| {
16578 project.open_local_buffer(path!("/file.ts"), cx)
16579 })
16580 .await
16581 .unwrap();
16582
16583 let buffer_text = "one\ntwo\nthree\n";
16584 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16585 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16586 editor.update_in(cx, |editor, window, cx| {
16587 editor.set_text(buffer_text, window, cx)
16588 });
16589
16590 editor
16591 .update_in(cx, |editor, window, cx| {
16592 editor.perform_format(
16593 project.clone(),
16594 FormatTrigger::Manual,
16595 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16596 window,
16597 cx,
16598 )
16599 })
16600 .unwrap()
16601 .await;
16602 assert_eq!(
16603 editor.update(cx, |editor, cx| editor.text(cx)),
16604 buffer_text.to_string() + prettier_format_suffix,
16605 "Test prettier formatting was not applied to the original buffer text",
16606 );
16607
16608 update_test_language_settings(cx, |settings| {
16609 settings.defaults.formatter = Some(SelectedFormatter::Auto)
16610 });
16611 let format = editor.update_in(cx, |editor, window, cx| {
16612 editor.perform_format(
16613 project.clone(),
16614 FormatTrigger::Manual,
16615 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16616 window,
16617 cx,
16618 )
16619 });
16620 format.await.unwrap();
16621 assert_eq!(
16622 editor.update(cx, |editor, cx| editor.text(cx)),
16623 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
16624 "Autoformatting (via test prettier) was not applied to the original buffer text",
16625 );
16626}
16627
16628#[gpui::test]
16629async fn test_addition_reverts(cx: &mut TestAppContext) {
16630 init_test(cx, |_| {});
16631 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16632 let base_text = indoc! {r#"
16633 struct Row;
16634 struct Row1;
16635 struct Row2;
16636
16637 struct Row4;
16638 struct Row5;
16639 struct Row6;
16640
16641 struct Row8;
16642 struct Row9;
16643 struct Row10;"#};
16644
16645 // When addition hunks are not adjacent to carets, no hunk revert is performed
16646 assert_hunk_revert(
16647 indoc! {r#"struct Row;
16648 struct Row1;
16649 struct Row1.1;
16650 struct Row1.2;
16651 struct Row2;ˇ
16652
16653 struct Row4;
16654 struct Row5;
16655 struct Row6;
16656
16657 struct Row8;
16658 ˇstruct Row9;
16659 struct Row9.1;
16660 struct Row9.2;
16661 struct Row9.3;
16662 struct Row10;"#},
16663 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16664 indoc! {r#"struct Row;
16665 struct Row1;
16666 struct Row1.1;
16667 struct Row1.2;
16668 struct Row2;ˇ
16669
16670 struct Row4;
16671 struct Row5;
16672 struct Row6;
16673
16674 struct Row8;
16675 ˇstruct Row9;
16676 struct Row9.1;
16677 struct Row9.2;
16678 struct Row9.3;
16679 struct Row10;"#},
16680 base_text,
16681 &mut cx,
16682 );
16683 // Same for selections
16684 assert_hunk_revert(
16685 indoc! {r#"struct Row;
16686 struct Row1;
16687 struct Row2;
16688 struct Row2.1;
16689 struct Row2.2;
16690 «ˇ
16691 struct Row4;
16692 struct» Row5;
16693 «struct Row6;
16694 ˇ»
16695 struct Row9.1;
16696 struct Row9.2;
16697 struct Row9.3;
16698 struct Row8;
16699 struct Row9;
16700 struct Row10;"#},
16701 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16702 indoc! {r#"struct Row;
16703 struct Row1;
16704 struct Row2;
16705 struct Row2.1;
16706 struct Row2.2;
16707 «ˇ
16708 struct Row4;
16709 struct» Row5;
16710 «struct Row6;
16711 ˇ»
16712 struct Row9.1;
16713 struct Row9.2;
16714 struct Row9.3;
16715 struct Row8;
16716 struct Row9;
16717 struct Row10;"#},
16718 base_text,
16719 &mut cx,
16720 );
16721
16722 // When carets and selections intersect the addition hunks, those are reverted.
16723 // Adjacent carets got merged.
16724 assert_hunk_revert(
16725 indoc! {r#"struct Row;
16726 ˇ// something on the top
16727 struct Row1;
16728 struct Row2;
16729 struct Roˇw3.1;
16730 struct Row2.2;
16731 struct Row2.3;ˇ
16732
16733 struct Row4;
16734 struct ˇRow5.1;
16735 struct Row5.2;
16736 struct «Rowˇ»5.3;
16737 struct Row5;
16738 struct Row6;
16739 ˇ
16740 struct Row9.1;
16741 struct «Rowˇ»9.2;
16742 struct «ˇRow»9.3;
16743 struct Row8;
16744 struct Row9;
16745 «ˇ// something on bottom»
16746 struct Row10;"#},
16747 vec![
16748 DiffHunkStatusKind::Added,
16749 DiffHunkStatusKind::Added,
16750 DiffHunkStatusKind::Added,
16751 DiffHunkStatusKind::Added,
16752 DiffHunkStatusKind::Added,
16753 ],
16754 indoc! {r#"struct Row;
16755 ˇstruct Row1;
16756 struct Row2;
16757 ˇ
16758 struct Row4;
16759 ˇstruct Row5;
16760 struct Row6;
16761 ˇ
16762 ˇstruct Row8;
16763 struct Row9;
16764 ˇstruct Row10;"#},
16765 base_text,
16766 &mut cx,
16767 );
16768}
16769
16770#[gpui::test]
16771async fn test_modification_reverts(cx: &mut TestAppContext) {
16772 init_test(cx, |_| {});
16773 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16774 let base_text = indoc! {r#"
16775 struct Row;
16776 struct Row1;
16777 struct Row2;
16778
16779 struct Row4;
16780 struct Row5;
16781 struct Row6;
16782
16783 struct Row8;
16784 struct Row9;
16785 struct Row10;"#};
16786
16787 // Modification hunks behave the same as the addition ones.
16788 assert_hunk_revert(
16789 indoc! {r#"struct Row;
16790 struct Row1;
16791 struct Row33;
16792 ˇ
16793 struct Row4;
16794 struct Row5;
16795 struct Row6;
16796 ˇ
16797 struct Row99;
16798 struct Row9;
16799 struct Row10;"#},
16800 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16801 indoc! {r#"struct Row;
16802 struct Row1;
16803 struct Row33;
16804 ˇ
16805 struct Row4;
16806 struct Row5;
16807 struct Row6;
16808 ˇ
16809 struct Row99;
16810 struct Row9;
16811 struct Row10;"#},
16812 base_text,
16813 &mut cx,
16814 );
16815 assert_hunk_revert(
16816 indoc! {r#"struct Row;
16817 struct Row1;
16818 struct Row33;
16819 «ˇ
16820 struct Row4;
16821 struct» Row5;
16822 «struct Row6;
16823 ˇ»
16824 struct Row99;
16825 struct Row9;
16826 struct Row10;"#},
16827 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16828 indoc! {r#"struct Row;
16829 struct Row1;
16830 struct Row33;
16831 «ˇ
16832 struct Row4;
16833 struct» Row5;
16834 «struct Row6;
16835 ˇ»
16836 struct Row99;
16837 struct Row9;
16838 struct Row10;"#},
16839 base_text,
16840 &mut cx,
16841 );
16842
16843 assert_hunk_revert(
16844 indoc! {r#"ˇstruct Row1.1;
16845 struct Row1;
16846 «ˇstr»uct Row22;
16847
16848 struct ˇRow44;
16849 struct Row5;
16850 struct «Rˇ»ow66;ˇ
16851
16852 «struˇ»ct Row88;
16853 struct Row9;
16854 struct Row1011;ˇ"#},
16855 vec![
16856 DiffHunkStatusKind::Modified,
16857 DiffHunkStatusKind::Modified,
16858 DiffHunkStatusKind::Modified,
16859 DiffHunkStatusKind::Modified,
16860 DiffHunkStatusKind::Modified,
16861 DiffHunkStatusKind::Modified,
16862 ],
16863 indoc! {r#"struct Row;
16864 ˇstruct Row1;
16865 struct Row2;
16866 ˇ
16867 struct Row4;
16868 ˇstruct Row5;
16869 struct Row6;
16870 ˇ
16871 struct Row8;
16872 ˇstruct Row9;
16873 struct Row10;ˇ"#},
16874 base_text,
16875 &mut cx,
16876 );
16877}
16878
16879#[gpui::test]
16880async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
16881 init_test(cx, |_| {});
16882 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16883 let base_text = indoc! {r#"
16884 one
16885
16886 two
16887 three
16888 "#};
16889
16890 cx.set_head_text(base_text);
16891 cx.set_state("\nˇ\n");
16892 cx.executor().run_until_parked();
16893 cx.update_editor(|editor, _window, cx| {
16894 editor.expand_selected_diff_hunks(cx);
16895 });
16896 cx.executor().run_until_parked();
16897 cx.update_editor(|editor, window, cx| {
16898 editor.backspace(&Default::default(), window, cx);
16899 });
16900 cx.run_until_parked();
16901 cx.assert_state_with_diff(
16902 indoc! {r#"
16903
16904 - two
16905 - threeˇ
16906 +
16907 "#}
16908 .to_string(),
16909 );
16910}
16911
16912#[gpui::test]
16913async fn test_deletion_reverts(cx: &mut TestAppContext) {
16914 init_test(cx, |_| {});
16915 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16916 let base_text = indoc! {r#"struct Row;
16917struct Row1;
16918struct Row2;
16919
16920struct Row4;
16921struct Row5;
16922struct Row6;
16923
16924struct Row8;
16925struct Row9;
16926struct Row10;"#};
16927
16928 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
16929 assert_hunk_revert(
16930 indoc! {r#"struct Row;
16931 struct Row2;
16932
16933 ˇstruct Row4;
16934 struct Row5;
16935 struct Row6;
16936 ˇ
16937 struct Row8;
16938 struct Row10;"#},
16939 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16940 indoc! {r#"struct Row;
16941 struct Row2;
16942
16943 ˇstruct Row4;
16944 struct Row5;
16945 struct Row6;
16946 ˇ
16947 struct Row8;
16948 struct Row10;"#},
16949 base_text,
16950 &mut cx,
16951 );
16952 assert_hunk_revert(
16953 indoc! {r#"struct Row;
16954 struct Row2;
16955
16956 «ˇstruct Row4;
16957 struct» Row5;
16958 «struct Row6;
16959 ˇ»
16960 struct Row8;
16961 struct Row10;"#},
16962 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16963 indoc! {r#"struct Row;
16964 struct Row2;
16965
16966 «ˇstruct Row4;
16967 struct» Row5;
16968 «struct Row6;
16969 ˇ»
16970 struct Row8;
16971 struct Row10;"#},
16972 base_text,
16973 &mut cx,
16974 );
16975
16976 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
16977 assert_hunk_revert(
16978 indoc! {r#"struct Row;
16979 ˇstruct Row2;
16980
16981 struct Row4;
16982 struct Row5;
16983 struct Row6;
16984
16985 struct Row8;ˇ
16986 struct Row10;"#},
16987 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16988 indoc! {r#"struct Row;
16989 struct Row1;
16990 ˇstruct Row2;
16991
16992 struct Row4;
16993 struct Row5;
16994 struct Row6;
16995
16996 struct Row8;ˇ
16997 struct Row9;
16998 struct Row10;"#},
16999 base_text,
17000 &mut cx,
17001 );
17002 assert_hunk_revert(
17003 indoc! {r#"struct Row;
17004 struct Row2«ˇ;
17005 struct Row4;
17006 struct» Row5;
17007 «struct Row6;
17008
17009 struct Row8;ˇ»
17010 struct Row10;"#},
17011 vec![
17012 DiffHunkStatusKind::Deleted,
17013 DiffHunkStatusKind::Deleted,
17014 DiffHunkStatusKind::Deleted,
17015 ],
17016 indoc! {r#"struct Row;
17017 struct Row1;
17018 struct Row2«ˇ;
17019
17020 struct Row4;
17021 struct» Row5;
17022 «struct Row6;
17023
17024 struct Row8;ˇ»
17025 struct Row9;
17026 struct Row10;"#},
17027 base_text,
17028 &mut cx,
17029 );
17030}
17031
17032#[gpui::test]
17033async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
17034 init_test(cx, |_| {});
17035
17036 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
17037 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
17038 let base_text_3 =
17039 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
17040
17041 let text_1 = edit_first_char_of_every_line(base_text_1);
17042 let text_2 = edit_first_char_of_every_line(base_text_2);
17043 let text_3 = edit_first_char_of_every_line(base_text_3);
17044
17045 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
17046 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
17047 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
17048
17049 let multibuffer = cx.new(|cx| {
17050 let mut multibuffer = MultiBuffer::new(ReadWrite);
17051 multibuffer.push_excerpts(
17052 buffer_1.clone(),
17053 [
17054 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17055 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17056 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17057 ],
17058 cx,
17059 );
17060 multibuffer.push_excerpts(
17061 buffer_2.clone(),
17062 [
17063 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17064 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17065 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17066 ],
17067 cx,
17068 );
17069 multibuffer.push_excerpts(
17070 buffer_3.clone(),
17071 [
17072 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17073 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17074 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17075 ],
17076 cx,
17077 );
17078 multibuffer
17079 });
17080
17081 let fs = FakeFs::new(cx.executor());
17082 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
17083 let (editor, cx) = cx
17084 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
17085 editor.update_in(cx, |editor, _window, cx| {
17086 for (buffer, diff_base) in [
17087 (buffer_1.clone(), base_text_1),
17088 (buffer_2.clone(), base_text_2),
17089 (buffer_3.clone(), base_text_3),
17090 ] {
17091 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
17092 editor
17093 .buffer
17094 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
17095 }
17096 });
17097 cx.executor().run_until_parked();
17098
17099 editor.update_in(cx, |editor, window, cx| {
17100 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}");
17101 editor.select_all(&SelectAll, window, cx);
17102 editor.git_restore(&Default::default(), window, cx);
17103 });
17104 cx.executor().run_until_parked();
17105
17106 // When all ranges are selected, all buffer hunks are reverted.
17107 editor.update(cx, |editor, cx| {
17108 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");
17109 });
17110 buffer_1.update(cx, |buffer, _| {
17111 assert_eq!(buffer.text(), base_text_1);
17112 });
17113 buffer_2.update(cx, |buffer, _| {
17114 assert_eq!(buffer.text(), base_text_2);
17115 });
17116 buffer_3.update(cx, |buffer, _| {
17117 assert_eq!(buffer.text(), base_text_3);
17118 });
17119
17120 editor.update_in(cx, |editor, window, cx| {
17121 editor.undo(&Default::default(), window, cx);
17122 });
17123
17124 editor.update_in(cx, |editor, window, cx| {
17125 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17126 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
17127 });
17128 editor.git_restore(&Default::default(), window, cx);
17129 });
17130
17131 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
17132 // but not affect buffer_2 and its related excerpts.
17133 editor.update(cx, |editor, cx| {
17134 assert_eq!(
17135 editor.text(cx),
17136 "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}"
17137 );
17138 });
17139 buffer_1.update(cx, |buffer, _| {
17140 assert_eq!(buffer.text(), base_text_1);
17141 });
17142 buffer_2.update(cx, |buffer, _| {
17143 assert_eq!(
17144 buffer.text(),
17145 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
17146 );
17147 });
17148 buffer_3.update(cx, |buffer, _| {
17149 assert_eq!(
17150 buffer.text(),
17151 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
17152 );
17153 });
17154
17155 fn edit_first_char_of_every_line(text: &str) -> String {
17156 text.split('\n')
17157 .map(|line| format!("X{}", &line[1..]))
17158 .collect::<Vec<_>>()
17159 .join("\n")
17160 }
17161}
17162
17163#[gpui::test]
17164async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
17165 init_test(cx, |_| {});
17166
17167 let cols = 4;
17168 let rows = 10;
17169 let sample_text_1 = sample_text(rows, cols, 'a');
17170 assert_eq!(
17171 sample_text_1,
17172 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
17173 );
17174 let sample_text_2 = sample_text(rows, cols, 'l');
17175 assert_eq!(
17176 sample_text_2,
17177 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
17178 );
17179 let sample_text_3 = sample_text(rows, cols, 'v');
17180 assert_eq!(
17181 sample_text_3,
17182 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
17183 );
17184
17185 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
17186 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
17187 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
17188
17189 let multi_buffer = cx.new(|cx| {
17190 let mut multibuffer = MultiBuffer::new(ReadWrite);
17191 multibuffer.push_excerpts(
17192 buffer_1.clone(),
17193 [
17194 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17195 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17196 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17197 ],
17198 cx,
17199 );
17200 multibuffer.push_excerpts(
17201 buffer_2.clone(),
17202 [
17203 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17204 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17205 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17206 ],
17207 cx,
17208 );
17209 multibuffer.push_excerpts(
17210 buffer_3.clone(),
17211 [
17212 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17213 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17214 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17215 ],
17216 cx,
17217 );
17218 multibuffer
17219 });
17220
17221 let fs = FakeFs::new(cx.executor());
17222 fs.insert_tree(
17223 "/a",
17224 json!({
17225 "main.rs": sample_text_1,
17226 "other.rs": sample_text_2,
17227 "lib.rs": sample_text_3,
17228 }),
17229 )
17230 .await;
17231 let project = Project::test(fs, ["/a".as_ref()], cx).await;
17232 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17233 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17234 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17235 Editor::new(
17236 EditorMode::full(),
17237 multi_buffer,
17238 Some(project.clone()),
17239 window,
17240 cx,
17241 )
17242 });
17243 let multibuffer_item_id = workspace
17244 .update(cx, |workspace, window, cx| {
17245 assert!(
17246 workspace.active_item(cx).is_none(),
17247 "active item should be None before the first item is added"
17248 );
17249 workspace.add_item_to_active_pane(
17250 Box::new(multi_buffer_editor.clone()),
17251 None,
17252 true,
17253 window,
17254 cx,
17255 );
17256 let active_item = workspace
17257 .active_item(cx)
17258 .expect("should have an active item after adding the multi buffer");
17259 assert!(
17260 !active_item.is_singleton(cx),
17261 "A multi buffer was expected to active after adding"
17262 );
17263 active_item.item_id()
17264 })
17265 .unwrap();
17266 cx.executor().run_until_parked();
17267
17268 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17269 editor.change_selections(
17270 SelectionEffects::scroll(Autoscroll::Next),
17271 window,
17272 cx,
17273 |s| s.select_ranges(Some(1..2)),
17274 );
17275 editor.open_excerpts(&OpenExcerpts, window, cx);
17276 });
17277 cx.executor().run_until_parked();
17278 let first_item_id = workspace
17279 .update(cx, |workspace, window, cx| {
17280 let active_item = workspace
17281 .active_item(cx)
17282 .expect("should have an active item after navigating into the 1st buffer");
17283 let first_item_id = active_item.item_id();
17284 assert_ne!(
17285 first_item_id, multibuffer_item_id,
17286 "Should navigate into the 1st buffer and activate it"
17287 );
17288 assert!(
17289 active_item.is_singleton(cx),
17290 "New active item should be a singleton buffer"
17291 );
17292 assert_eq!(
17293 active_item
17294 .act_as::<Editor>(cx)
17295 .expect("should have navigated into an editor for the 1st buffer")
17296 .read(cx)
17297 .text(cx),
17298 sample_text_1
17299 );
17300
17301 workspace
17302 .go_back(workspace.active_pane().downgrade(), window, cx)
17303 .detach_and_log_err(cx);
17304
17305 first_item_id
17306 })
17307 .unwrap();
17308 cx.executor().run_until_parked();
17309 workspace
17310 .update(cx, |workspace, _, cx| {
17311 let active_item = workspace
17312 .active_item(cx)
17313 .expect("should have an active item after navigating back");
17314 assert_eq!(
17315 active_item.item_id(),
17316 multibuffer_item_id,
17317 "Should navigate back to the multi buffer"
17318 );
17319 assert!(!active_item.is_singleton(cx));
17320 })
17321 .unwrap();
17322
17323 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17324 editor.change_selections(
17325 SelectionEffects::scroll(Autoscroll::Next),
17326 window,
17327 cx,
17328 |s| s.select_ranges(Some(39..40)),
17329 );
17330 editor.open_excerpts(&OpenExcerpts, window, cx);
17331 });
17332 cx.executor().run_until_parked();
17333 let second_item_id = workspace
17334 .update(cx, |workspace, window, cx| {
17335 let active_item = workspace
17336 .active_item(cx)
17337 .expect("should have an active item after navigating into the 2nd buffer");
17338 let second_item_id = active_item.item_id();
17339 assert_ne!(
17340 second_item_id, multibuffer_item_id,
17341 "Should navigate away from the multibuffer"
17342 );
17343 assert_ne!(
17344 second_item_id, first_item_id,
17345 "Should navigate into the 2nd buffer and activate it"
17346 );
17347 assert!(
17348 active_item.is_singleton(cx),
17349 "New active item should be a singleton buffer"
17350 );
17351 assert_eq!(
17352 active_item
17353 .act_as::<Editor>(cx)
17354 .expect("should have navigated into an editor")
17355 .read(cx)
17356 .text(cx),
17357 sample_text_2
17358 );
17359
17360 workspace
17361 .go_back(workspace.active_pane().downgrade(), window, cx)
17362 .detach_and_log_err(cx);
17363
17364 second_item_id
17365 })
17366 .unwrap();
17367 cx.executor().run_until_parked();
17368 workspace
17369 .update(cx, |workspace, _, cx| {
17370 let active_item = workspace
17371 .active_item(cx)
17372 .expect("should have an active item after navigating back from the 2nd buffer");
17373 assert_eq!(
17374 active_item.item_id(),
17375 multibuffer_item_id,
17376 "Should navigate back from the 2nd buffer to the multi buffer"
17377 );
17378 assert!(!active_item.is_singleton(cx));
17379 })
17380 .unwrap();
17381
17382 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17383 editor.change_selections(
17384 SelectionEffects::scroll(Autoscroll::Next),
17385 window,
17386 cx,
17387 |s| s.select_ranges(Some(70..70)),
17388 );
17389 editor.open_excerpts(&OpenExcerpts, window, cx);
17390 });
17391 cx.executor().run_until_parked();
17392 workspace
17393 .update(cx, |workspace, window, cx| {
17394 let active_item = workspace
17395 .active_item(cx)
17396 .expect("should have an active item after navigating into the 3rd buffer");
17397 let third_item_id = active_item.item_id();
17398 assert_ne!(
17399 third_item_id, multibuffer_item_id,
17400 "Should navigate into the 3rd buffer and activate it"
17401 );
17402 assert_ne!(third_item_id, first_item_id);
17403 assert_ne!(third_item_id, second_item_id);
17404 assert!(
17405 active_item.is_singleton(cx),
17406 "New active item should be a singleton buffer"
17407 );
17408 assert_eq!(
17409 active_item
17410 .act_as::<Editor>(cx)
17411 .expect("should have navigated into an editor")
17412 .read(cx)
17413 .text(cx),
17414 sample_text_3
17415 );
17416
17417 workspace
17418 .go_back(workspace.active_pane().downgrade(), window, cx)
17419 .detach_and_log_err(cx);
17420 })
17421 .unwrap();
17422 cx.executor().run_until_parked();
17423 workspace
17424 .update(cx, |workspace, _, cx| {
17425 let active_item = workspace
17426 .active_item(cx)
17427 .expect("should have an active item after navigating back from the 3rd buffer");
17428 assert_eq!(
17429 active_item.item_id(),
17430 multibuffer_item_id,
17431 "Should navigate back from the 3rd buffer to the multi buffer"
17432 );
17433 assert!(!active_item.is_singleton(cx));
17434 })
17435 .unwrap();
17436}
17437
17438#[gpui::test]
17439async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17440 init_test(cx, |_| {});
17441
17442 let mut cx = EditorTestContext::new(cx).await;
17443
17444 let diff_base = r#"
17445 use some::mod;
17446
17447 const A: u32 = 42;
17448
17449 fn main() {
17450 println!("hello");
17451
17452 println!("world");
17453 }
17454 "#
17455 .unindent();
17456
17457 cx.set_state(
17458 &r#"
17459 use some::modified;
17460
17461 ˇ
17462 fn main() {
17463 println!("hello there");
17464
17465 println!("around the");
17466 println!("world");
17467 }
17468 "#
17469 .unindent(),
17470 );
17471
17472 cx.set_head_text(&diff_base);
17473 executor.run_until_parked();
17474
17475 cx.update_editor(|editor, window, cx| {
17476 editor.go_to_next_hunk(&GoToHunk, window, cx);
17477 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17478 });
17479 executor.run_until_parked();
17480 cx.assert_state_with_diff(
17481 r#"
17482 use some::modified;
17483
17484
17485 fn main() {
17486 - println!("hello");
17487 + ˇ println!("hello there");
17488
17489 println!("around the");
17490 println!("world");
17491 }
17492 "#
17493 .unindent(),
17494 );
17495
17496 cx.update_editor(|editor, window, cx| {
17497 for _ in 0..2 {
17498 editor.go_to_next_hunk(&GoToHunk, window, cx);
17499 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17500 }
17501 });
17502 executor.run_until_parked();
17503 cx.assert_state_with_diff(
17504 r#"
17505 - use some::mod;
17506 + ˇuse some::modified;
17507
17508
17509 fn main() {
17510 - println!("hello");
17511 + println!("hello there");
17512
17513 + println!("around the");
17514 println!("world");
17515 }
17516 "#
17517 .unindent(),
17518 );
17519
17520 cx.update_editor(|editor, window, cx| {
17521 editor.go_to_next_hunk(&GoToHunk, window, cx);
17522 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17523 });
17524 executor.run_until_parked();
17525 cx.assert_state_with_diff(
17526 r#"
17527 - use some::mod;
17528 + use some::modified;
17529
17530 - const A: u32 = 42;
17531 ˇ
17532 fn main() {
17533 - println!("hello");
17534 + println!("hello there");
17535
17536 + println!("around the");
17537 println!("world");
17538 }
17539 "#
17540 .unindent(),
17541 );
17542
17543 cx.update_editor(|editor, window, cx| {
17544 editor.cancel(&Cancel, window, cx);
17545 });
17546
17547 cx.assert_state_with_diff(
17548 r#"
17549 use some::modified;
17550
17551 ˇ
17552 fn main() {
17553 println!("hello there");
17554
17555 println!("around the");
17556 println!("world");
17557 }
17558 "#
17559 .unindent(),
17560 );
17561}
17562
17563#[gpui::test]
17564async fn test_diff_base_change_with_expanded_diff_hunks(
17565 executor: BackgroundExecutor,
17566 cx: &mut TestAppContext,
17567) {
17568 init_test(cx, |_| {});
17569
17570 let mut cx = EditorTestContext::new(cx).await;
17571
17572 let diff_base = r#"
17573 use some::mod1;
17574 use some::mod2;
17575
17576 const A: u32 = 42;
17577 const B: u32 = 42;
17578 const C: u32 = 42;
17579
17580 fn main() {
17581 println!("hello");
17582
17583 println!("world");
17584 }
17585 "#
17586 .unindent();
17587
17588 cx.set_state(
17589 &r#"
17590 use some::mod2;
17591
17592 const A: u32 = 42;
17593 const C: u32 = 42;
17594
17595 fn main(ˇ) {
17596 //println!("hello");
17597
17598 println!("world");
17599 //
17600 //
17601 }
17602 "#
17603 .unindent(),
17604 );
17605
17606 cx.set_head_text(&diff_base);
17607 executor.run_until_parked();
17608
17609 cx.update_editor(|editor, window, cx| {
17610 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17611 });
17612 executor.run_until_parked();
17613 cx.assert_state_with_diff(
17614 r#"
17615 - use some::mod1;
17616 use some::mod2;
17617
17618 const A: u32 = 42;
17619 - const B: u32 = 42;
17620 const C: u32 = 42;
17621
17622 fn main(ˇ) {
17623 - println!("hello");
17624 + //println!("hello");
17625
17626 println!("world");
17627 + //
17628 + //
17629 }
17630 "#
17631 .unindent(),
17632 );
17633
17634 cx.set_head_text("new diff base!");
17635 executor.run_until_parked();
17636 cx.assert_state_with_diff(
17637 r#"
17638 - new diff base!
17639 + use some::mod2;
17640 +
17641 + const A: u32 = 42;
17642 + const C: u32 = 42;
17643 +
17644 + fn main(ˇ) {
17645 + //println!("hello");
17646 +
17647 + println!("world");
17648 + //
17649 + //
17650 + }
17651 "#
17652 .unindent(),
17653 );
17654}
17655
17656#[gpui::test]
17657async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
17658 init_test(cx, |_| {});
17659
17660 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17661 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17662 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17663 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17664 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
17665 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
17666
17667 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
17668 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
17669 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
17670
17671 let multi_buffer = cx.new(|cx| {
17672 let mut multibuffer = MultiBuffer::new(ReadWrite);
17673 multibuffer.push_excerpts(
17674 buffer_1.clone(),
17675 [
17676 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17677 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17678 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17679 ],
17680 cx,
17681 );
17682 multibuffer.push_excerpts(
17683 buffer_2.clone(),
17684 [
17685 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17686 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17687 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17688 ],
17689 cx,
17690 );
17691 multibuffer.push_excerpts(
17692 buffer_3.clone(),
17693 [
17694 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17695 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17696 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17697 ],
17698 cx,
17699 );
17700 multibuffer
17701 });
17702
17703 let editor =
17704 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17705 editor
17706 .update(cx, |editor, _window, cx| {
17707 for (buffer, diff_base) in [
17708 (buffer_1.clone(), file_1_old),
17709 (buffer_2.clone(), file_2_old),
17710 (buffer_3.clone(), file_3_old),
17711 ] {
17712 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
17713 editor
17714 .buffer
17715 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
17716 }
17717 })
17718 .unwrap();
17719
17720 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17721 cx.run_until_parked();
17722
17723 cx.assert_editor_state(
17724 &"
17725 ˇaaa
17726 ccc
17727 ddd
17728
17729 ggg
17730 hhh
17731
17732
17733 lll
17734 mmm
17735 NNN
17736
17737 qqq
17738 rrr
17739
17740 uuu
17741 111
17742 222
17743 333
17744
17745 666
17746 777
17747
17748 000
17749 !!!"
17750 .unindent(),
17751 );
17752
17753 cx.update_editor(|editor, window, cx| {
17754 editor.select_all(&SelectAll, window, cx);
17755 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17756 });
17757 cx.executor().run_until_parked();
17758
17759 cx.assert_state_with_diff(
17760 "
17761 «aaa
17762 - bbb
17763 ccc
17764 ddd
17765
17766 ggg
17767 hhh
17768
17769
17770 lll
17771 mmm
17772 - nnn
17773 + NNN
17774
17775 qqq
17776 rrr
17777
17778 uuu
17779 111
17780 222
17781 333
17782
17783 + 666
17784 777
17785
17786 000
17787 !!!ˇ»"
17788 .unindent(),
17789 );
17790}
17791
17792#[gpui::test]
17793async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
17794 init_test(cx, |_| {});
17795
17796 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
17797 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
17798
17799 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
17800 let multi_buffer = cx.new(|cx| {
17801 let mut multibuffer = MultiBuffer::new(ReadWrite);
17802 multibuffer.push_excerpts(
17803 buffer.clone(),
17804 [
17805 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
17806 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
17807 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
17808 ],
17809 cx,
17810 );
17811 multibuffer
17812 });
17813
17814 let editor =
17815 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17816 editor
17817 .update(cx, |editor, _window, cx| {
17818 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
17819 editor
17820 .buffer
17821 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
17822 })
17823 .unwrap();
17824
17825 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17826 cx.run_until_parked();
17827
17828 cx.update_editor(|editor, window, cx| {
17829 editor.expand_all_diff_hunks(&Default::default(), window, cx)
17830 });
17831 cx.executor().run_until_parked();
17832
17833 // When the start of a hunk coincides with the start of its excerpt,
17834 // the hunk is expanded. When the start of a a hunk is earlier than
17835 // the start of its excerpt, the hunk is not expanded.
17836 cx.assert_state_with_diff(
17837 "
17838 ˇaaa
17839 - bbb
17840 + BBB
17841
17842 - ddd
17843 - eee
17844 + DDD
17845 + EEE
17846 fff
17847
17848 iii
17849 "
17850 .unindent(),
17851 );
17852}
17853
17854#[gpui::test]
17855async fn test_edits_around_expanded_insertion_hunks(
17856 executor: BackgroundExecutor,
17857 cx: &mut TestAppContext,
17858) {
17859 init_test(cx, |_| {});
17860
17861 let mut cx = EditorTestContext::new(cx).await;
17862
17863 let diff_base = r#"
17864 use some::mod1;
17865 use some::mod2;
17866
17867 const A: u32 = 42;
17868
17869 fn main() {
17870 println!("hello");
17871
17872 println!("world");
17873 }
17874 "#
17875 .unindent();
17876 executor.run_until_parked();
17877 cx.set_state(
17878 &r#"
17879 use some::mod1;
17880 use some::mod2;
17881
17882 const A: u32 = 42;
17883 const B: u32 = 42;
17884 const C: u32 = 42;
17885 ˇ
17886
17887 fn main() {
17888 println!("hello");
17889
17890 println!("world");
17891 }
17892 "#
17893 .unindent(),
17894 );
17895
17896 cx.set_head_text(&diff_base);
17897 executor.run_until_parked();
17898
17899 cx.update_editor(|editor, window, cx| {
17900 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17901 });
17902 executor.run_until_parked();
17903
17904 cx.assert_state_with_diff(
17905 r#"
17906 use some::mod1;
17907 use some::mod2;
17908
17909 const A: u32 = 42;
17910 + const B: u32 = 42;
17911 + const C: u32 = 42;
17912 + ˇ
17913
17914 fn main() {
17915 println!("hello");
17916
17917 println!("world");
17918 }
17919 "#
17920 .unindent(),
17921 );
17922
17923 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
17924 executor.run_until_parked();
17925
17926 cx.assert_state_with_diff(
17927 r#"
17928 use some::mod1;
17929 use some::mod2;
17930
17931 const A: u32 = 42;
17932 + const B: u32 = 42;
17933 + const C: u32 = 42;
17934 + const D: u32 = 42;
17935 + ˇ
17936
17937 fn main() {
17938 println!("hello");
17939
17940 println!("world");
17941 }
17942 "#
17943 .unindent(),
17944 );
17945
17946 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
17947 executor.run_until_parked();
17948
17949 cx.assert_state_with_diff(
17950 r#"
17951 use some::mod1;
17952 use some::mod2;
17953
17954 const A: u32 = 42;
17955 + const B: u32 = 42;
17956 + const C: u32 = 42;
17957 + const D: u32 = 42;
17958 + const E: u32 = 42;
17959 + ˇ
17960
17961 fn main() {
17962 println!("hello");
17963
17964 println!("world");
17965 }
17966 "#
17967 .unindent(),
17968 );
17969
17970 cx.update_editor(|editor, window, cx| {
17971 editor.delete_line(&DeleteLine, window, cx);
17972 });
17973 executor.run_until_parked();
17974
17975 cx.assert_state_with_diff(
17976 r#"
17977 use some::mod1;
17978 use some::mod2;
17979
17980 const A: u32 = 42;
17981 + const B: u32 = 42;
17982 + const C: u32 = 42;
17983 + const D: u32 = 42;
17984 + const E: u32 = 42;
17985 ˇ
17986 fn main() {
17987 println!("hello");
17988
17989 println!("world");
17990 }
17991 "#
17992 .unindent(),
17993 );
17994
17995 cx.update_editor(|editor, window, cx| {
17996 editor.move_up(&MoveUp, window, cx);
17997 editor.delete_line(&DeleteLine, window, cx);
17998 editor.move_up(&MoveUp, window, cx);
17999 editor.delete_line(&DeleteLine, window, cx);
18000 editor.move_up(&MoveUp, window, cx);
18001 editor.delete_line(&DeleteLine, window, cx);
18002 });
18003 executor.run_until_parked();
18004 cx.assert_state_with_diff(
18005 r#"
18006 use some::mod1;
18007 use some::mod2;
18008
18009 const A: u32 = 42;
18010 + const B: u32 = 42;
18011 ˇ
18012 fn main() {
18013 println!("hello");
18014
18015 println!("world");
18016 }
18017 "#
18018 .unindent(),
18019 );
18020
18021 cx.update_editor(|editor, window, cx| {
18022 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
18023 editor.delete_line(&DeleteLine, window, cx);
18024 });
18025 executor.run_until_parked();
18026 cx.assert_state_with_diff(
18027 r#"
18028 ˇ
18029 fn main() {
18030 println!("hello");
18031
18032 println!("world");
18033 }
18034 "#
18035 .unindent(),
18036 );
18037}
18038
18039#[gpui::test]
18040async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
18041 init_test(cx, |_| {});
18042
18043 let mut cx = EditorTestContext::new(cx).await;
18044 cx.set_head_text(indoc! { "
18045 one
18046 two
18047 three
18048 four
18049 five
18050 "
18051 });
18052 cx.set_state(indoc! { "
18053 one
18054 ˇthree
18055 five
18056 "});
18057 cx.run_until_parked();
18058 cx.update_editor(|editor, window, cx| {
18059 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18060 });
18061 cx.assert_state_with_diff(
18062 indoc! { "
18063 one
18064 - two
18065 ˇthree
18066 - four
18067 five
18068 "}
18069 .to_string(),
18070 );
18071 cx.update_editor(|editor, window, cx| {
18072 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18073 });
18074
18075 cx.assert_state_with_diff(
18076 indoc! { "
18077 one
18078 ˇthree
18079 five
18080 "}
18081 .to_string(),
18082 );
18083
18084 cx.set_state(indoc! { "
18085 one
18086 ˇTWO
18087 three
18088 four
18089 five
18090 "});
18091 cx.run_until_parked();
18092 cx.update_editor(|editor, window, cx| {
18093 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18094 });
18095
18096 cx.assert_state_with_diff(
18097 indoc! { "
18098 one
18099 - two
18100 + ˇTWO
18101 three
18102 four
18103 five
18104 "}
18105 .to_string(),
18106 );
18107 cx.update_editor(|editor, window, cx| {
18108 editor.move_up(&Default::default(), window, cx);
18109 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18110 });
18111 cx.assert_state_with_diff(
18112 indoc! { "
18113 one
18114 ˇTWO
18115 three
18116 four
18117 five
18118 "}
18119 .to_string(),
18120 );
18121}
18122
18123#[gpui::test]
18124async fn test_edits_around_expanded_deletion_hunks(
18125 executor: BackgroundExecutor,
18126 cx: &mut TestAppContext,
18127) {
18128 init_test(cx, |_| {});
18129
18130 let mut cx = EditorTestContext::new(cx).await;
18131
18132 let diff_base = r#"
18133 use some::mod1;
18134 use some::mod2;
18135
18136 const A: u32 = 42;
18137 const B: u32 = 42;
18138 const C: u32 = 42;
18139
18140
18141 fn main() {
18142 println!("hello");
18143
18144 println!("world");
18145 }
18146 "#
18147 .unindent();
18148 executor.run_until_parked();
18149 cx.set_state(
18150 &r#"
18151 use some::mod1;
18152 use some::mod2;
18153
18154 ˇconst B: u32 = 42;
18155 const C: u32 = 42;
18156
18157
18158 fn main() {
18159 println!("hello");
18160
18161 println!("world");
18162 }
18163 "#
18164 .unindent(),
18165 );
18166
18167 cx.set_head_text(&diff_base);
18168 executor.run_until_parked();
18169
18170 cx.update_editor(|editor, window, cx| {
18171 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18172 });
18173 executor.run_until_parked();
18174
18175 cx.assert_state_with_diff(
18176 r#"
18177 use some::mod1;
18178 use some::mod2;
18179
18180 - const A: u32 = 42;
18181 ˇconst B: u32 = 42;
18182 const C: u32 = 42;
18183
18184
18185 fn main() {
18186 println!("hello");
18187
18188 println!("world");
18189 }
18190 "#
18191 .unindent(),
18192 );
18193
18194 cx.update_editor(|editor, window, cx| {
18195 editor.delete_line(&DeleteLine, window, cx);
18196 });
18197 executor.run_until_parked();
18198 cx.assert_state_with_diff(
18199 r#"
18200 use some::mod1;
18201 use some::mod2;
18202
18203 - const A: u32 = 42;
18204 - const B: u32 = 42;
18205 ˇconst C: u32 = 42;
18206
18207
18208 fn main() {
18209 println!("hello");
18210
18211 println!("world");
18212 }
18213 "#
18214 .unindent(),
18215 );
18216
18217 cx.update_editor(|editor, window, cx| {
18218 editor.delete_line(&DeleteLine, window, cx);
18219 });
18220 executor.run_until_parked();
18221 cx.assert_state_with_diff(
18222 r#"
18223 use some::mod1;
18224 use some::mod2;
18225
18226 - const A: u32 = 42;
18227 - const B: u32 = 42;
18228 - const C: u32 = 42;
18229 ˇ
18230
18231 fn main() {
18232 println!("hello");
18233
18234 println!("world");
18235 }
18236 "#
18237 .unindent(),
18238 );
18239
18240 cx.update_editor(|editor, window, cx| {
18241 editor.handle_input("replacement", window, cx);
18242 });
18243 executor.run_until_parked();
18244 cx.assert_state_with_diff(
18245 r#"
18246 use some::mod1;
18247 use some::mod2;
18248
18249 - const A: u32 = 42;
18250 - const B: u32 = 42;
18251 - const C: u32 = 42;
18252 -
18253 + replacementˇ
18254
18255 fn main() {
18256 println!("hello");
18257
18258 println!("world");
18259 }
18260 "#
18261 .unindent(),
18262 );
18263}
18264
18265#[gpui::test]
18266async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18267 init_test(cx, |_| {});
18268
18269 let mut cx = EditorTestContext::new(cx).await;
18270
18271 let base_text = r#"
18272 one
18273 two
18274 three
18275 four
18276 five
18277 "#
18278 .unindent();
18279 executor.run_until_parked();
18280 cx.set_state(
18281 &r#"
18282 one
18283 two
18284 fˇour
18285 five
18286 "#
18287 .unindent(),
18288 );
18289
18290 cx.set_head_text(&base_text);
18291 executor.run_until_parked();
18292
18293 cx.update_editor(|editor, window, cx| {
18294 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18295 });
18296 executor.run_until_parked();
18297
18298 cx.assert_state_with_diff(
18299 r#"
18300 one
18301 two
18302 - three
18303 fˇour
18304 five
18305 "#
18306 .unindent(),
18307 );
18308
18309 cx.update_editor(|editor, window, cx| {
18310 editor.backspace(&Backspace, window, cx);
18311 editor.backspace(&Backspace, window, cx);
18312 });
18313 executor.run_until_parked();
18314 cx.assert_state_with_diff(
18315 r#"
18316 one
18317 two
18318 - threeˇ
18319 - four
18320 + our
18321 five
18322 "#
18323 .unindent(),
18324 );
18325}
18326
18327#[gpui::test]
18328async fn test_edit_after_expanded_modification_hunk(
18329 executor: BackgroundExecutor,
18330 cx: &mut TestAppContext,
18331) {
18332 init_test(cx, |_| {});
18333
18334 let mut cx = EditorTestContext::new(cx).await;
18335
18336 let diff_base = r#"
18337 use some::mod1;
18338 use some::mod2;
18339
18340 const A: u32 = 42;
18341 const B: u32 = 42;
18342 const C: u32 = 42;
18343 const D: u32 = 42;
18344
18345
18346 fn main() {
18347 println!("hello");
18348
18349 println!("world");
18350 }"#
18351 .unindent();
18352
18353 cx.set_state(
18354 &r#"
18355 use some::mod1;
18356 use some::mod2;
18357
18358 const A: u32 = 42;
18359 const B: u32 = 42;
18360 const C: u32 = 43ˇ
18361 const D: u32 = 42;
18362
18363
18364 fn main() {
18365 println!("hello");
18366
18367 println!("world");
18368 }"#
18369 .unindent(),
18370 );
18371
18372 cx.set_head_text(&diff_base);
18373 executor.run_until_parked();
18374 cx.update_editor(|editor, window, cx| {
18375 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18376 });
18377 executor.run_until_parked();
18378
18379 cx.assert_state_with_diff(
18380 r#"
18381 use some::mod1;
18382 use some::mod2;
18383
18384 const A: u32 = 42;
18385 const B: u32 = 42;
18386 - const C: u32 = 42;
18387 + const C: u32 = 43ˇ
18388 const D: u32 = 42;
18389
18390
18391 fn main() {
18392 println!("hello");
18393
18394 println!("world");
18395 }"#
18396 .unindent(),
18397 );
18398
18399 cx.update_editor(|editor, window, cx| {
18400 editor.handle_input("\nnew_line\n", window, cx);
18401 });
18402 executor.run_until_parked();
18403
18404 cx.assert_state_with_diff(
18405 r#"
18406 use some::mod1;
18407 use some::mod2;
18408
18409 const A: u32 = 42;
18410 const B: u32 = 42;
18411 - const C: u32 = 42;
18412 + const C: u32 = 43
18413 + new_line
18414 + ˇ
18415 const D: u32 = 42;
18416
18417
18418 fn main() {
18419 println!("hello");
18420
18421 println!("world");
18422 }"#
18423 .unindent(),
18424 );
18425}
18426
18427#[gpui::test]
18428async fn test_stage_and_unstage_added_file_hunk(
18429 executor: BackgroundExecutor,
18430 cx: &mut TestAppContext,
18431) {
18432 init_test(cx, |_| {});
18433
18434 let mut cx = EditorTestContext::new(cx).await;
18435 cx.update_editor(|editor, _, cx| {
18436 editor.set_expand_all_diff_hunks(cx);
18437 });
18438
18439 let working_copy = r#"
18440 ˇfn main() {
18441 println!("hello, world!");
18442 }
18443 "#
18444 .unindent();
18445
18446 cx.set_state(&working_copy);
18447 executor.run_until_parked();
18448
18449 cx.assert_state_with_diff(
18450 r#"
18451 + ˇfn main() {
18452 + println!("hello, world!");
18453 + }
18454 "#
18455 .unindent(),
18456 );
18457 cx.assert_index_text(None);
18458
18459 cx.update_editor(|editor, window, cx| {
18460 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18461 });
18462 executor.run_until_parked();
18463 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
18464 cx.assert_state_with_diff(
18465 r#"
18466 + ˇfn main() {
18467 + println!("hello, world!");
18468 + }
18469 "#
18470 .unindent(),
18471 );
18472
18473 cx.update_editor(|editor, window, cx| {
18474 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18475 });
18476 executor.run_until_parked();
18477 cx.assert_index_text(None);
18478}
18479
18480async fn setup_indent_guides_editor(
18481 text: &str,
18482 cx: &mut TestAppContext,
18483) -> (BufferId, EditorTestContext) {
18484 init_test(cx, |_| {});
18485
18486 let mut cx = EditorTestContext::new(cx).await;
18487
18488 let buffer_id = cx.update_editor(|editor, window, cx| {
18489 editor.set_text(text, window, cx);
18490 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
18491
18492 buffer_ids[0]
18493 });
18494
18495 (buffer_id, cx)
18496}
18497
18498fn assert_indent_guides(
18499 range: Range<u32>,
18500 expected: Vec<IndentGuide>,
18501 active_indices: Option<Vec<usize>>,
18502 cx: &mut EditorTestContext,
18503) {
18504 let indent_guides = cx.update_editor(|editor, window, cx| {
18505 let snapshot = editor.snapshot(window, cx).display_snapshot;
18506 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
18507 editor,
18508 MultiBufferRow(range.start)..MultiBufferRow(range.end),
18509 true,
18510 &snapshot,
18511 cx,
18512 );
18513
18514 indent_guides.sort_by(|a, b| {
18515 a.depth.cmp(&b.depth).then(
18516 a.start_row
18517 .cmp(&b.start_row)
18518 .then(a.end_row.cmp(&b.end_row)),
18519 )
18520 });
18521 indent_guides
18522 });
18523
18524 if let Some(expected) = active_indices {
18525 let active_indices = cx.update_editor(|editor, window, cx| {
18526 let snapshot = editor.snapshot(window, cx).display_snapshot;
18527 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
18528 });
18529
18530 assert_eq!(
18531 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
18532 expected,
18533 "Active indent guide indices do not match"
18534 );
18535 }
18536
18537 assert_eq!(indent_guides, expected, "Indent guides do not match");
18538}
18539
18540fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
18541 IndentGuide {
18542 buffer_id,
18543 start_row: MultiBufferRow(start_row),
18544 end_row: MultiBufferRow(end_row),
18545 depth,
18546 tab_size: 4,
18547 settings: IndentGuideSettings {
18548 enabled: true,
18549 line_width: 1,
18550 active_line_width: 1,
18551 ..Default::default()
18552 },
18553 }
18554}
18555
18556#[gpui::test]
18557async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
18558 let (buffer_id, mut cx) = setup_indent_guides_editor(
18559 &"
18560 fn main() {
18561 let a = 1;
18562 }"
18563 .unindent(),
18564 cx,
18565 )
18566 .await;
18567
18568 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18569}
18570
18571#[gpui::test]
18572async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
18573 let (buffer_id, mut cx) = setup_indent_guides_editor(
18574 &"
18575 fn main() {
18576 let a = 1;
18577 let b = 2;
18578 }"
18579 .unindent(),
18580 cx,
18581 )
18582 .await;
18583
18584 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
18585}
18586
18587#[gpui::test]
18588async fn test_indent_guide_nested(cx: &mut TestAppContext) {
18589 let (buffer_id, mut cx) = setup_indent_guides_editor(
18590 &"
18591 fn main() {
18592 let a = 1;
18593 if a == 3 {
18594 let b = 2;
18595 } else {
18596 let c = 3;
18597 }
18598 }"
18599 .unindent(),
18600 cx,
18601 )
18602 .await;
18603
18604 assert_indent_guides(
18605 0..8,
18606 vec![
18607 indent_guide(buffer_id, 1, 6, 0),
18608 indent_guide(buffer_id, 3, 3, 1),
18609 indent_guide(buffer_id, 5, 5, 1),
18610 ],
18611 None,
18612 &mut cx,
18613 );
18614}
18615
18616#[gpui::test]
18617async fn test_indent_guide_tab(cx: &mut TestAppContext) {
18618 let (buffer_id, mut cx) = setup_indent_guides_editor(
18619 &"
18620 fn main() {
18621 let a = 1;
18622 let b = 2;
18623 let c = 3;
18624 }"
18625 .unindent(),
18626 cx,
18627 )
18628 .await;
18629
18630 assert_indent_guides(
18631 0..5,
18632 vec![
18633 indent_guide(buffer_id, 1, 3, 0),
18634 indent_guide(buffer_id, 2, 2, 1),
18635 ],
18636 None,
18637 &mut cx,
18638 );
18639}
18640
18641#[gpui::test]
18642async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
18643 let (buffer_id, mut cx) = setup_indent_guides_editor(
18644 &"
18645 fn main() {
18646 let a = 1;
18647
18648 let c = 3;
18649 }"
18650 .unindent(),
18651 cx,
18652 )
18653 .await;
18654
18655 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
18656}
18657
18658#[gpui::test]
18659async fn test_indent_guide_complex(cx: &mut TestAppContext) {
18660 let (buffer_id, mut cx) = setup_indent_guides_editor(
18661 &"
18662 fn main() {
18663 let a = 1;
18664
18665 let c = 3;
18666
18667 if a == 3 {
18668 let b = 2;
18669 } else {
18670 let c = 3;
18671 }
18672 }"
18673 .unindent(),
18674 cx,
18675 )
18676 .await;
18677
18678 assert_indent_guides(
18679 0..11,
18680 vec![
18681 indent_guide(buffer_id, 1, 9, 0),
18682 indent_guide(buffer_id, 6, 6, 1),
18683 indent_guide(buffer_id, 8, 8, 1),
18684 ],
18685 None,
18686 &mut cx,
18687 );
18688}
18689
18690#[gpui::test]
18691async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
18692 let (buffer_id, mut cx) = setup_indent_guides_editor(
18693 &"
18694 fn main() {
18695 let a = 1;
18696
18697 let c = 3;
18698
18699 if a == 3 {
18700 let b = 2;
18701 } else {
18702 let c = 3;
18703 }
18704 }"
18705 .unindent(),
18706 cx,
18707 )
18708 .await;
18709
18710 assert_indent_guides(
18711 1..11,
18712 vec![
18713 indent_guide(buffer_id, 1, 9, 0),
18714 indent_guide(buffer_id, 6, 6, 1),
18715 indent_guide(buffer_id, 8, 8, 1),
18716 ],
18717 None,
18718 &mut cx,
18719 );
18720}
18721
18722#[gpui::test]
18723async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
18724 let (buffer_id, mut cx) = setup_indent_guides_editor(
18725 &"
18726 fn main() {
18727 let a = 1;
18728
18729 let c = 3;
18730
18731 if a == 3 {
18732 let b = 2;
18733 } else {
18734 let c = 3;
18735 }
18736 }"
18737 .unindent(),
18738 cx,
18739 )
18740 .await;
18741
18742 assert_indent_guides(
18743 1..10,
18744 vec![
18745 indent_guide(buffer_id, 1, 9, 0),
18746 indent_guide(buffer_id, 6, 6, 1),
18747 indent_guide(buffer_id, 8, 8, 1),
18748 ],
18749 None,
18750 &mut cx,
18751 );
18752}
18753
18754#[gpui::test]
18755async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
18756 let (buffer_id, mut cx) = setup_indent_guides_editor(
18757 &"
18758 fn main() {
18759 if a {
18760 b(
18761 c,
18762 d,
18763 )
18764 } else {
18765 e(
18766 f
18767 )
18768 }
18769 }"
18770 .unindent(),
18771 cx,
18772 )
18773 .await;
18774
18775 assert_indent_guides(
18776 0..11,
18777 vec![
18778 indent_guide(buffer_id, 1, 10, 0),
18779 indent_guide(buffer_id, 2, 5, 1),
18780 indent_guide(buffer_id, 7, 9, 1),
18781 indent_guide(buffer_id, 3, 4, 2),
18782 indent_guide(buffer_id, 8, 8, 2),
18783 ],
18784 None,
18785 &mut cx,
18786 );
18787
18788 cx.update_editor(|editor, window, cx| {
18789 editor.fold_at(MultiBufferRow(2), window, cx);
18790 assert_eq!(
18791 editor.display_text(cx),
18792 "
18793 fn main() {
18794 if a {
18795 b(⋯
18796 )
18797 } else {
18798 e(
18799 f
18800 )
18801 }
18802 }"
18803 .unindent()
18804 );
18805 });
18806
18807 assert_indent_guides(
18808 0..11,
18809 vec![
18810 indent_guide(buffer_id, 1, 10, 0),
18811 indent_guide(buffer_id, 2, 5, 1),
18812 indent_guide(buffer_id, 7, 9, 1),
18813 indent_guide(buffer_id, 8, 8, 2),
18814 ],
18815 None,
18816 &mut cx,
18817 );
18818}
18819
18820#[gpui::test]
18821async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
18822 let (buffer_id, mut cx) = setup_indent_guides_editor(
18823 &"
18824 block1
18825 block2
18826 block3
18827 block4
18828 block2
18829 block1
18830 block1"
18831 .unindent(),
18832 cx,
18833 )
18834 .await;
18835
18836 assert_indent_guides(
18837 1..10,
18838 vec![
18839 indent_guide(buffer_id, 1, 4, 0),
18840 indent_guide(buffer_id, 2, 3, 1),
18841 indent_guide(buffer_id, 3, 3, 2),
18842 ],
18843 None,
18844 &mut cx,
18845 );
18846}
18847
18848#[gpui::test]
18849async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
18850 let (buffer_id, mut cx) = setup_indent_guides_editor(
18851 &"
18852 block1
18853 block2
18854 block3
18855
18856 block1
18857 block1"
18858 .unindent(),
18859 cx,
18860 )
18861 .await;
18862
18863 assert_indent_guides(
18864 0..6,
18865 vec![
18866 indent_guide(buffer_id, 1, 2, 0),
18867 indent_guide(buffer_id, 2, 2, 1),
18868 ],
18869 None,
18870 &mut cx,
18871 );
18872}
18873
18874#[gpui::test]
18875async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
18876 let (buffer_id, mut cx) = setup_indent_guides_editor(
18877 &"
18878 function component() {
18879 \treturn (
18880 \t\t\t
18881 \t\t<div>
18882 \t\t\t<abc></abc>
18883 \t\t</div>
18884 \t)
18885 }"
18886 .unindent(),
18887 cx,
18888 )
18889 .await;
18890
18891 assert_indent_guides(
18892 0..8,
18893 vec![
18894 indent_guide(buffer_id, 1, 6, 0),
18895 indent_guide(buffer_id, 2, 5, 1),
18896 indent_guide(buffer_id, 4, 4, 2),
18897 ],
18898 None,
18899 &mut cx,
18900 );
18901}
18902
18903#[gpui::test]
18904async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
18905 let (buffer_id, mut cx) = setup_indent_guides_editor(
18906 &"
18907 function component() {
18908 \treturn (
18909 \t
18910 \t\t<div>
18911 \t\t\t<abc></abc>
18912 \t\t</div>
18913 \t)
18914 }"
18915 .unindent(),
18916 cx,
18917 )
18918 .await;
18919
18920 assert_indent_guides(
18921 0..8,
18922 vec![
18923 indent_guide(buffer_id, 1, 6, 0),
18924 indent_guide(buffer_id, 2, 5, 1),
18925 indent_guide(buffer_id, 4, 4, 2),
18926 ],
18927 None,
18928 &mut cx,
18929 );
18930}
18931
18932#[gpui::test]
18933async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
18934 let (buffer_id, mut cx) = setup_indent_guides_editor(
18935 &"
18936 block1
18937
18938
18939
18940 block2
18941 "
18942 .unindent(),
18943 cx,
18944 )
18945 .await;
18946
18947 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18948}
18949
18950#[gpui::test]
18951async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
18952 let (buffer_id, mut cx) = setup_indent_guides_editor(
18953 &"
18954 def a:
18955 \tb = 3
18956 \tif True:
18957 \t\tc = 4
18958 \t\td = 5
18959 \tprint(b)
18960 "
18961 .unindent(),
18962 cx,
18963 )
18964 .await;
18965
18966 assert_indent_guides(
18967 0..6,
18968 vec![
18969 indent_guide(buffer_id, 1, 5, 0),
18970 indent_guide(buffer_id, 3, 4, 1),
18971 ],
18972 None,
18973 &mut cx,
18974 );
18975}
18976
18977#[gpui::test]
18978async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
18979 let (buffer_id, mut cx) = setup_indent_guides_editor(
18980 &"
18981 fn main() {
18982 let a = 1;
18983 }"
18984 .unindent(),
18985 cx,
18986 )
18987 .await;
18988
18989 cx.update_editor(|editor, window, cx| {
18990 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18991 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18992 });
18993 });
18994
18995 assert_indent_guides(
18996 0..3,
18997 vec![indent_guide(buffer_id, 1, 1, 0)],
18998 Some(vec![0]),
18999 &mut cx,
19000 );
19001}
19002
19003#[gpui::test]
19004async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
19005 let (buffer_id, mut cx) = setup_indent_guides_editor(
19006 &"
19007 fn main() {
19008 if 1 == 2 {
19009 let a = 1;
19010 }
19011 }"
19012 .unindent(),
19013 cx,
19014 )
19015 .await;
19016
19017 cx.update_editor(|editor, window, cx| {
19018 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19019 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
19020 });
19021 });
19022
19023 assert_indent_guides(
19024 0..4,
19025 vec![
19026 indent_guide(buffer_id, 1, 3, 0),
19027 indent_guide(buffer_id, 2, 2, 1),
19028 ],
19029 Some(vec![1]),
19030 &mut cx,
19031 );
19032
19033 cx.update_editor(|editor, window, cx| {
19034 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19035 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
19036 });
19037 });
19038
19039 assert_indent_guides(
19040 0..4,
19041 vec![
19042 indent_guide(buffer_id, 1, 3, 0),
19043 indent_guide(buffer_id, 2, 2, 1),
19044 ],
19045 Some(vec![1]),
19046 &mut cx,
19047 );
19048
19049 cx.update_editor(|editor, window, cx| {
19050 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19051 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
19052 });
19053 });
19054
19055 assert_indent_guides(
19056 0..4,
19057 vec![
19058 indent_guide(buffer_id, 1, 3, 0),
19059 indent_guide(buffer_id, 2, 2, 1),
19060 ],
19061 Some(vec![0]),
19062 &mut cx,
19063 );
19064}
19065
19066#[gpui::test]
19067async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
19068 let (buffer_id, mut cx) = setup_indent_guides_editor(
19069 &"
19070 fn main() {
19071 let a = 1;
19072
19073 let b = 2;
19074 }"
19075 .unindent(),
19076 cx,
19077 )
19078 .await;
19079
19080 cx.update_editor(|editor, window, cx| {
19081 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19082 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
19083 });
19084 });
19085
19086 assert_indent_guides(
19087 0..5,
19088 vec![indent_guide(buffer_id, 1, 3, 0)],
19089 Some(vec![0]),
19090 &mut cx,
19091 );
19092}
19093
19094#[gpui::test]
19095async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
19096 let (buffer_id, mut cx) = setup_indent_guides_editor(
19097 &"
19098 def m:
19099 a = 1
19100 pass"
19101 .unindent(),
19102 cx,
19103 )
19104 .await;
19105
19106 cx.update_editor(|editor, window, cx| {
19107 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19108 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
19109 });
19110 });
19111
19112 assert_indent_guides(
19113 0..3,
19114 vec![indent_guide(buffer_id, 1, 2, 0)],
19115 Some(vec![0]),
19116 &mut cx,
19117 );
19118}
19119
19120#[gpui::test]
19121async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
19122 init_test(cx, |_| {});
19123 let mut cx = EditorTestContext::new(cx).await;
19124 let text = indoc! {
19125 "
19126 impl A {
19127 fn b() {
19128 0;
19129 3;
19130 5;
19131 6;
19132 7;
19133 }
19134 }
19135 "
19136 };
19137 let base_text = indoc! {
19138 "
19139 impl A {
19140 fn b() {
19141 0;
19142 1;
19143 2;
19144 3;
19145 4;
19146 }
19147 fn c() {
19148 5;
19149 6;
19150 7;
19151 }
19152 }
19153 "
19154 };
19155
19156 cx.update_editor(|editor, window, cx| {
19157 editor.set_text(text, window, cx);
19158
19159 editor.buffer().update(cx, |multibuffer, cx| {
19160 let buffer = multibuffer.as_singleton().unwrap();
19161 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
19162
19163 multibuffer.set_all_diff_hunks_expanded(cx);
19164 multibuffer.add_diff(diff, cx);
19165
19166 buffer.read(cx).remote_id()
19167 })
19168 });
19169 cx.run_until_parked();
19170
19171 cx.assert_state_with_diff(
19172 indoc! { "
19173 impl A {
19174 fn b() {
19175 0;
19176 - 1;
19177 - 2;
19178 3;
19179 - 4;
19180 - }
19181 - fn c() {
19182 5;
19183 6;
19184 7;
19185 }
19186 }
19187 ˇ"
19188 }
19189 .to_string(),
19190 );
19191
19192 let mut actual_guides = cx.update_editor(|editor, window, cx| {
19193 editor
19194 .snapshot(window, cx)
19195 .buffer_snapshot
19196 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
19197 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
19198 .collect::<Vec<_>>()
19199 });
19200 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
19201 assert_eq!(
19202 actual_guides,
19203 vec![
19204 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
19205 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
19206 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
19207 ]
19208 );
19209}
19210
19211#[gpui::test]
19212async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19213 init_test(cx, |_| {});
19214 let mut cx = EditorTestContext::new(cx).await;
19215
19216 let diff_base = r#"
19217 a
19218 b
19219 c
19220 "#
19221 .unindent();
19222
19223 cx.set_state(
19224 &r#"
19225 ˇA
19226 b
19227 C
19228 "#
19229 .unindent(),
19230 );
19231 cx.set_head_text(&diff_base);
19232 cx.update_editor(|editor, window, cx| {
19233 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19234 });
19235 executor.run_until_parked();
19236
19237 let both_hunks_expanded = r#"
19238 - a
19239 + ˇA
19240 b
19241 - c
19242 + C
19243 "#
19244 .unindent();
19245
19246 cx.assert_state_with_diff(both_hunks_expanded.clone());
19247
19248 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19249 let snapshot = editor.snapshot(window, cx);
19250 let hunks = editor
19251 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19252 .collect::<Vec<_>>();
19253 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19254 let buffer_id = hunks[0].buffer_id;
19255 hunks
19256 .into_iter()
19257 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19258 .collect::<Vec<_>>()
19259 });
19260 assert_eq!(hunk_ranges.len(), 2);
19261
19262 cx.update_editor(|editor, _, cx| {
19263 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19264 });
19265 executor.run_until_parked();
19266
19267 let second_hunk_expanded = r#"
19268 ˇA
19269 b
19270 - c
19271 + C
19272 "#
19273 .unindent();
19274
19275 cx.assert_state_with_diff(second_hunk_expanded);
19276
19277 cx.update_editor(|editor, _, cx| {
19278 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19279 });
19280 executor.run_until_parked();
19281
19282 cx.assert_state_with_diff(both_hunks_expanded.clone());
19283
19284 cx.update_editor(|editor, _, cx| {
19285 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19286 });
19287 executor.run_until_parked();
19288
19289 let first_hunk_expanded = r#"
19290 - a
19291 + ˇA
19292 b
19293 C
19294 "#
19295 .unindent();
19296
19297 cx.assert_state_with_diff(first_hunk_expanded);
19298
19299 cx.update_editor(|editor, _, cx| {
19300 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19301 });
19302 executor.run_until_parked();
19303
19304 cx.assert_state_with_diff(both_hunks_expanded);
19305
19306 cx.set_state(
19307 &r#"
19308 ˇA
19309 b
19310 "#
19311 .unindent(),
19312 );
19313 cx.run_until_parked();
19314
19315 // TODO this cursor position seems bad
19316 cx.assert_state_with_diff(
19317 r#"
19318 - ˇa
19319 + A
19320 b
19321 "#
19322 .unindent(),
19323 );
19324
19325 cx.update_editor(|editor, window, cx| {
19326 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19327 });
19328
19329 cx.assert_state_with_diff(
19330 r#"
19331 - ˇa
19332 + A
19333 b
19334 - c
19335 "#
19336 .unindent(),
19337 );
19338
19339 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19340 let snapshot = editor.snapshot(window, cx);
19341 let hunks = editor
19342 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19343 .collect::<Vec<_>>();
19344 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19345 let buffer_id = hunks[0].buffer_id;
19346 hunks
19347 .into_iter()
19348 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19349 .collect::<Vec<_>>()
19350 });
19351 assert_eq!(hunk_ranges.len(), 2);
19352
19353 cx.update_editor(|editor, _, cx| {
19354 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19355 });
19356 executor.run_until_parked();
19357
19358 cx.assert_state_with_diff(
19359 r#"
19360 - ˇa
19361 + A
19362 b
19363 "#
19364 .unindent(),
19365 );
19366}
19367
19368#[gpui::test]
19369async fn test_toggle_deletion_hunk_at_start_of_file(
19370 executor: BackgroundExecutor,
19371 cx: &mut TestAppContext,
19372) {
19373 init_test(cx, |_| {});
19374 let mut cx = EditorTestContext::new(cx).await;
19375
19376 let diff_base = r#"
19377 a
19378 b
19379 c
19380 "#
19381 .unindent();
19382
19383 cx.set_state(
19384 &r#"
19385 ˇb
19386 c
19387 "#
19388 .unindent(),
19389 );
19390 cx.set_head_text(&diff_base);
19391 cx.update_editor(|editor, window, cx| {
19392 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19393 });
19394 executor.run_until_parked();
19395
19396 let hunk_expanded = r#"
19397 - a
19398 ˇb
19399 c
19400 "#
19401 .unindent();
19402
19403 cx.assert_state_with_diff(hunk_expanded.clone());
19404
19405 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19406 let snapshot = editor.snapshot(window, cx);
19407 let hunks = editor
19408 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19409 .collect::<Vec<_>>();
19410 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19411 let buffer_id = hunks[0].buffer_id;
19412 hunks
19413 .into_iter()
19414 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19415 .collect::<Vec<_>>()
19416 });
19417 assert_eq!(hunk_ranges.len(), 1);
19418
19419 cx.update_editor(|editor, _, cx| {
19420 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19421 });
19422 executor.run_until_parked();
19423
19424 let hunk_collapsed = r#"
19425 ˇb
19426 c
19427 "#
19428 .unindent();
19429
19430 cx.assert_state_with_diff(hunk_collapsed);
19431
19432 cx.update_editor(|editor, _, cx| {
19433 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19434 });
19435 executor.run_until_parked();
19436
19437 cx.assert_state_with_diff(hunk_expanded.clone());
19438}
19439
19440#[gpui::test]
19441async fn test_display_diff_hunks(cx: &mut TestAppContext) {
19442 init_test(cx, |_| {});
19443
19444 let fs = FakeFs::new(cx.executor());
19445 fs.insert_tree(
19446 path!("/test"),
19447 json!({
19448 ".git": {},
19449 "file-1": "ONE\n",
19450 "file-2": "TWO\n",
19451 "file-3": "THREE\n",
19452 }),
19453 )
19454 .await;
19455
19456 fs.set_head_for_repo(
19457 path!("/test/.git").as_ref(),
19458 &[
19459 ("file-1".into(), "one\n".into()),
19460 ("file-2".into(), "two\n".into()),
19461 ("file-3".into(), "three\n".into()),
19462 ],
19463 "deadbeef",
19464 );
19465
19466 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
19467 let mut buffers = vec![];
19468 for i in 1..=3 {
19469 let buffer = project
19470 .update(cx, |project, cx| {
19471 let path = format!(path!("/test/file-{}"), i);
19472 project.open_local_buffer(path, cx)
19473 })
19474 .await
19475 .unwrap();
19476 buffers.push(buffer);
19477 }
19478
19479 let multibuffer = cx.new(|cx| {
19480 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
19481 multibuffer.set_all_diff_hunks_expanded(cx);
19482 for buffer in &buffers {
19483 let snapshot = buffer.read(cx).snapshot();
19484 multibuffer.set_excerpts_for_path(
19485 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
19486 buffer.clone(),
19487 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
19488 DEFAULT_MULTIBUFFER_CONTEXT,
19489 cx,
19490 );
19491 }
19492 multibuffer
19493 });
19494
19495 let editor = cx.add_window(|window, cx| {
19496 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
19497 });
19498 cx.run_until_parked();
19499
19500 let snapshot = editor
19501 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19502 .unwrap();
19503 let hunks = snapshot
19504 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
19505 .map(|hunk| match hunk {
19506 DisplayDiffHunk::Unfolded {
19507 display_row_range, ..
19508 } => display_row_range,
19509 DisplayDiffHunk::Folded { .. } => unreachable!(),
19510 })
19511 .collect::<Vec<_>>();
19512 assert_eq!(
19513 hunks,
19514 [
19515 DisplayRow(2)..DisplayRow(4),
19516 DisplayRow(7)..DisplayRow(9),
19517 DisplayRow(12)..DisplayRow(14),
19518 ]
19519 );
19520}
19521
19522#[gpui::test]
19523async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
19524 init_test(cx, |_| {});
19525
19526 let mut cx = EditorTestContext::new(cx).await;
19527 cx.set_head_text(indoc! { "
19528 one
19529 two
19530 three
19531 four
19532 five
19533 "
19534 });
19535 cx.set_index_text(indoc! { "
19536 one
19537 two
19538 three
19539 four
19540 five
19541 "
19542 });
19543 cx.set_state(indoc! {"
19544 one
19545 TWO
19546 ˇTHREE
19547 FOUR
19548 five
19549 "});
19550 cx.run_until_parked();
19551 cx.update_editor(|editor, window, cx| {
19552 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19553 });
19554 cx.run_until_parked();
19555 cx.assert_index_text(Some(indoc! {"
19556 one
19557 TWO
19558 THREE
19559 FOUR
19560 five
19561 "}));
19562 cx.set_state(indoc! { "
19563 one
19564 TWO
19565 ˇTHREE-HUNDRED
19566 FOUR
19567 five
19568 "});
19569 cx.run_until_parked();
19570 cx.update_editor(|editor, window, cx| {
19571 let snapshot = editor.snapshot(window, cx);
19572 let hunks = editor
19573 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19574 .collect::<Vec<_>>();
19575 assert_eq!(hunks.len(), 1);
19576 assert_eq!(
19577 hunks[0].status(),
19578 DiffHunkStatus {
19579 kind: DiffHunkStatusKind::Modified,
19580 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
19581 }
19582 );
19583
19584 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19585 });
19586 cx.run_until_parked();
19587 cx.assert_index_text(Some(indoc! {"
19588 one
19589 TWO
19590 THREE-HUNDRED
19591 FOUR
19592 five
19593 "}));
19594}
19595
19596#[gpui::test]
19597fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
19598 init_test(cx, |_| {});
19599
19600 let editor = cx.add_window(|window, cx| {
19601 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
19602 build_editor(buffer, window, cx)
19603 });
19604
19605 let render_args = Arc::new(Mutex::new(None));
19606 let snapshot = editor
19607 .update(cx, |editor, window, cx| {
19608 let snapshot = editor.buffer().read(cx).snapshot(cx);
19609 let range =
19610 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
19611
19612 struct RenderArgs {
19613 row: MultiBufferRow,
19614 folded: bool,
19615 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
19616 }
19617
19618 let crease = Crease::inline(
19619 range,
19620 FoldPlaceholder::test(),
19621 {
19622 let toggle_callback = render_args.clone();
19623 move |row, folded, callback, _window, _cx| {
19624 *toggle_callback.lock() = Some(RenderArgs {
19625 row,
19626 folded,
19627 callback,
19628 });
19629 div()
19630 }
19631 },
19632 |_row, _folded, _window, _cx| div(),
19633 );
19634
19635 editor.insert_creases(Some(crease), cx);
19636 let snapshot = editor.snapshot(window, cx);
19637 let _div =
19638 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
19639 snapshot
19640 })
19641 .unwrap();
19642
19643 let render_args = render_args.lock().take().unwrap();
19644 assert_eq!(render_args.row, MultiBufferRow(1));
19645 assert!(!render_args.folded);
19646 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19647
19648 cx.update_window(*editor, |_, window, cx| {
19649 (render_args.callback)(true, window, cx)
19650 })
19651 .unwrap();
19652 let snapshot = editor
19653 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19654 .unwrap();
19655 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
19656
19657 cx.update_window(*editor, |_, window, cx| {
19658 (render_args.callback)(false, window, cx)
19659 })
19660 .unwrap();
19661 let snapshot = editor
19662 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19663 .unwrap();
19664 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19665}
19666
19667#[gpui::test]
19668async fn test_input_text(cx: &mut TestAppContext) {
19669 init_test(cx, |_| {});
19670 let mut cx = EditorTestContext::new(cx).await;
19671
19672 cx.set_state(
19673 &r#"ˇone
19674 two
19675
19676 three
19677 fourˇ
19678 five
19679
19680 siˇx"#
19681 .unindent(),
19682 );
19683
19684 cx.dispatch_action(HandleInput(String::new()));
19685 cx.assert_editor_state(
19686 &r#"ˇone
19687 two
19688
19689 three
19690 fourˇ
19691 five
19692
19693 siˇx"#
19694 .unindent(),
19695 );
19696
19697 cx.dispatch_action(HandleInput("AAAA".to_string()));
19698 cx.assert_editor_state(
19699 &r#"AAAAˇone
19700 two
19701
19702 three
19703 fourAAAAˇ
19704 five
19705
19706 siAAAAˇx"#
19707 .unindent(),
19708 );
19709}
19710
19711#[gpui::test]
19712async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
19713 init_test(cx, |_| {});
19714
19715 let mut cx = EditorTestContext::new(cx).await;
19716 cx.set_state(
19717 r#"let foo = 1;
19718let foo = 2;
19719let foo = 3;
19720let fooˇ = 4;
19721let foo = 5;
19722let foo = 6;
19723let foo = 7;
19724let foo = 8;
19725let foo = 9;
19726let foo = 10;
19727let foo = 11;
19728let foo = 12;
19729let foo = 13;
19730let foo = 14;
19731let foo = 15;"#,
19732 );
19733
19734 cx.update_editor(|e, window, cx| {
19735 assert_eq!(
19736 e.next_scroll_position,
19737 NextScrollCursorCenterTopBottom::Center,
19738 "Default next scroll direction is center",
19739 );
19740
19741 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19742 assert_eq!(
19743 e.next_scroll_position,
19744 NextScrollCursorCenterTopBottom::Top,
19745 "After center, next scroll direction should be top",
19746 );
19747
19748 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19749 assert_eq!(
19750 e.next_scroll_position,
19751 NextScrollCursorCenterTopBottom::Bottom,
19752 "After top, next scroll direction should be bottom",
19753 );
19754
19755 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19756 assert_eq!(
19757 e.next_scroll_position,
19758 NextScrollCursorCenterTopBottom::Center,
19759 "After bottom, scrolling should start over",
19760 );
19761
19762 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19763 assert_eq!(
19764 e.next_scroll_position,
19765 NextScrollCursorCenterTopBottom::Top,
19766 "Scrolling continues if retriggered fast enough"
19767 );
19768 });
19769
19770 cx.executor()
19771 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
19772 cx.executor().run_until_parked();
19773 cx.update_editor(|e, _, _| {
19774 assert_eq!(
19775 e.next_scroll_position,
19776 NextScrollCursorCenterTopBottom::Center,
19777 "If scrolling is not triggered fast enough, it should reset"
19778 );
19779 });
19780}
19781
19782#[gpui::test]
19783async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
19784 init_test(cx, |_| {});
19785 let mut cx = EditorLspTestContext::new_rust(
19786 lsp::ServerCapabilities {
19787 definition_provider: Some(lsp::OneOf::Left(true)),
19788 references_provider: Some(lsp::OneOf::Left(true)),
19789 ..lsp::ServerCapabilities::default()
19790 },
19791 cx,
19792 )
19793 .await;
19794
19795 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
19796 let go_to_definition = cx
19797 .lsp
19798 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19799 move |params, _| async move {
19800 if empty_go_to_definition {
19801 Ok(None)
19802 } else {
19803 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
19804 uri: params.text_document_position_params.text_document.uri,
19805 range: lsp::Range::new(
19806 lsp::Position::new(4, 3),
19807 lsp::Position::new(4, 6),
19808 ),
19809 })))
19810 }
19811 },
19812 );
19813 let references = cx
19814 .lsp
19815 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
19816 Ok(Some(vec![lsp::Location {
19817 uri: params.text_document_position.text_document.uri,
19818 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
19819 }]))
19820 });
19821 (go_to_definition, references)
19822 };
19823
19824 cx.set_state(
19825 &r#"fn one() {
19826 let mut a = ˇtwo();
19827 }
19828
19829 fn two() {}"#
19830 .unindent(),
19831 );
19832 set_up_lsp_handlers(false, &mut cx);
19833 let navigated = cx
19834 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19835 .await
19836 .expect("Failed to navigate to definition");
19837 assert_eq!(
19838 navigated,
19839 Navigated::Yes,
19840 "Should have navigated to definition from the GetDefinition response"
19841 );
19842 cx.assert_editor_state(
19843 &r#"fn one() {
19844 let mut a = two();
19845 }
19846
19847 fn «twoˇ»() {}"#
19848 .unindent(),
19849 );
19850
19851 let editors = cx.update_workspace(|workspace, _, cx| {
19852 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19853 });
19854 cx.update_editor(|_, _, test_editor_cx| {
19855 assert_eq!(
19856 editors.len(),
19857 1,
19858 "Initially, only one, test, editor should be open in the workspace"
19859 );
19860 assert_eq!(
19861 test_editor_cx.entity(),
19862 editors.last().expect("Asserted len is 1").clone()
19863 );
19864 });
19865
19866 set_up_lsp_handlers(true, &mut cx);
19867 let navigated = cx
19868 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19869 .await
19870 .expect("Failed to navigate to lookup references");
19871 assert_eq!(
19872 navigated,
19873 Navigated::Yes,
19874 "Should have navigated to references as a fallback after empty GoToDefinition response"
19875 );
19876 // We should not change the selections in the existing file,
19877 // if opening another milti buffer with the references
19878 cx.assert_editor_state(
19879 &r#"fn one() {
19880 let mut a = two();
19881 }
19882
19883 fn «twoˇ»() {}"#
19884 .unindent(),
19885 );
19886 let editors = cx.update_workspace(|workspace, _, cx| {
19887 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19888 });
19889 cx.update_editor(|_, _, test_editor_cx| {
19890 assert_eq!(
19891 editors.len(),
19892 2,
19893 "After falling back to references search, we open a new editor with the results"
19894 );
19895 let references_fallback_text = editors
19896 .into_iter()
19897 .find(|new_editor| *new_editor != test_editor_cx.entity())
19898 .expect("Should have one non-test editor now")
19899 .read(test_editor_cx)
19900 .text(test_editor_cx);
19901 assert_eq!(
19902 references_fallback_text, "fn one() {\n let mut a = two();\n}",
19903 "Should use the range from the references response and not the GoToDefinition one"
19904 );
19905 });
19906}
19907
19908#[gpui::test]
19909async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
19910 init_test(cx, |_| {});
19911 cx.update(|cx| {
19912 let mut editor_settings = EditorSettings::get_global(cx).clone();
19913 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
19914 EditorSettings::override_global(editor_settings, cx);
19915 });
19916 let mut cx = EditorLspTestContext::new_rust(
19917 lsp::ServerCapabilities {
19918 definition_provider: Some(lsp::OneOf::Left(true)),
19919 references_provider: Some(lsp::OneOf::Left(true)),
19920 ..lsp::ServerCapabilities::default()
19921 },
19922 cx,
19923 )
19924 .await;
19925 let original_state = r#"fn one() {
19926 let mut a = ˇtwo();
19927 }
19928
19929 fn two() {}"#
19930 .unindent();
19931 cx.set_state(&original_state);
19932
19933 let mut go_to_definition = cx
19934 .lsp
19935 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19936 move |_, _| async move { Ok(None) },
19937 );
19938 let _references = cx
19939 .lsp
19940 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
19941 panic!("Should not call for references with no go to definition fallback")
19942 });
19943
19944 let navigated = cx
19945 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19946 .await
19947 .expect("Failed to navigate to lookup references");
19948 go_to_definition
19949 .next()
19950 .await
19951 .expect("Should have called the go_to_definition handler");
19952
19953 assert_eq!(
19954 navigated,
19955 Navigated::No,
19956 "Should have navigated to references as a fallback after empty GoToDefinition response"
19957 );
19958 cx.assert_editor_state(&original_state);
19959 let editors = cx.update_workspace(|workspace, _, cx| {
19960 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19961 });
19962 cx.update_editor(|_, _, _| {
19963 assert_eq!(
19964 editors.len(),
19965 1,
19966 "After unsuccessful fallback, no other editor should have been opened"
19967 );
19968 });
19969}
19970
19971#[gpui::test]
19972async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
19973 init_test(cx, |_| {});
19974
19975 let language = Arc::new(Language::new(
19976 LanguageConfig::default(),
19977 Some(tree_sitter_rust::LANGUAGE.into()),
19978 ));
19979
19980 let text = r#"
19981 #[cfg(test)]
19982 mod tests() {
19983 #[test]
19984 fn runnable_1() {
19985 let a = 1;
19986 }
19987
19988 #[test]
19989 fn runnable_2() {
19990 let a = 1;
19991 let b = 2;
19992 }
19993 }
19994 "#
19995 .unindent();
19996
19997 let fs = FakeFs::new(cx.executor());
19998 fs.insert_file("/file.rs", Default::default()).await;
19999
20000 let project = Project::test(fs, ["/a".as_ref()], cx).await;
20001 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20002 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20003 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
20004 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
20005
20006 let editor = cx.new_window_entity(|window, cx| {
20007 Editor::new(
20008 EditorMode::full(),
20009 multi_buffer,
20010 Some(project.clone()),
20011 window,
20012 cx,
20013 )
20014 });
20015
20016 editor.update_in(cx, |editor, window, cx| {
20017 let snapshot = editor.buffer().read(cx).snapshot(cx);
20018 editor.tasks.insert(
20019 (buffer.read(cx).remote_id(), 3),
20020 RunnableTasks {
20021 templates: vec![],
20022 offset: snapshot.anchor_before(43),
20023 column: 0,
20024 extra_variables: HashMap::default(),
20025 context_range: BufferOffset(43)..BufferOffset(85),
20026 },
20027 );
20028 editor.tasks.insert(
20029 (buffer.read(cx).remote_id(), 8),
20030 RunnableTasks {
20031 templates: vec![],
20032 offset: snapshot.anchor_before(86),
20033 column: 0,
20034 extra_variables: HashMap::default(),
20035 context_range: BufferOffset(86)..BufferOffset(191),
20036 },
20037 );
20038
20039 // Test finding task when cursor is inside function body
20040 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20041 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
20042 });
20043 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
20044 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
20045
20046 // Test finding task when cursor is on function name
20047 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20048 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
20049 });
20050 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
20051 assert_eq!(row, 8, "Should find task when cursor is on function name");
20052 });
20053}
20054
20055#[gpui::test]
20056async fn test_folding_buffers(cx: &mut TestAppContext) {
20057 init_test(cx, |_| {});
20058
20059 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
20060 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
20061 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
20062
20063 let fs = FakeFs::new(cx.executor());
20064 fs.insert_tree(
20065 path!("/a"),
20066 json!({
20067 "first.rs": sample_text_1,
20068 "second.rs": sample_text_2,
20069 "third.rs": sample_text_3,
20070 }),
20071 )
20072 .await;
20073 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20074 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20075 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20076 let worktree = project.update(cx, |project, cx| {
20077 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20078 assert_eq!(worktrees.len(), 1);
20079 worktrees.pop().unwrap()
20080 });
20081 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20082
20083 let buffer_1 = project
20084 .update(cx, |project, cx| {
20085 project.open_buffer((worktree_id, "first.rs"), cx)
20086 })
20087 .await
20088 .unwrap();
20089 let buffer_2 = project
20090 .update(cx, |project, cx| {
20091 project.open_buffer((worktree_id, "second.rs"), cx)
20092 })
20093 .await
20094 .unwrap();
20095 let buffer_3 = project
20096 .update(cx, |project, cx| {
20097 project.open_buffer((worktree_id, "third.rs"), cx)
20098 })
20099 .await
20100 .unwrap();
20101
20102 let multi_buffer = cx.new(|cx| {
20103 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20104 multi_buffer.push_excerpts(
20105 buffer_1.clone(),
20106 [
20107 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20108 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20109 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20110 ],
20111 cx,
20112 );
20113 multi_buffer.push_excerpts(
20114 buffer_2.clone(),
20115 [
20116 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20117 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20118 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20119 ],
20120 cx,
20121 );
20122 multi_buffer.push_excerpts(
20123 buffer_3.clone(),
20124 [
20125 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20126 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20127 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20128 ],
20129 cx,
20130 );
20131 multi_buffer
20132 });
20133 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20134 Editor::new(
20135 EditorMode::full(),
20136 multi_buffer.clone(),
20137 Some(project.clone()),
20138 window,
20139 cx,
20140 )
20141 });
20142
20143 assert_eq!(
20144 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20145 "\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",
20146 );
20147
20148 multi_buffer_editor.update(cx, |editor, cx| {
20149 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
20150 });
20151 assert_eq!(
20152 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20153 "\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",
20154 "After folding the first buffer, its text should not be displayed"
20155 );
20156
20157 multi_buffer_editor.update(cx, |editor, cx| {
20158 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
20159 });
20160 assert_eq!(
20161 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20162 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
20163 "After folding the second buffer, its text should not be displayed"
20164 );
20165
20166 multi_buffer_editor.update(cx, |editor, cx| {
20167 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
20168 });
20169 assert_eq!(
20170 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20171 "\n\n\n\n\n",
20172 "After folding the third buffer, its text should not be displayed"
20173 );
20174
20175 // Emulate selection inside the fold logic, that should work
20176 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20177 editor
20178 .snapshot(window, cx)
20179 .next_line_boundary(Point::new(0, 4));
20180 });
20181
20182 multi_buffer_editor.update(cx, |editor, cx| {
20183 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
20184 });
20185 assert_eq!(
20186 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20187 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
20188 "After unfolding the second buffer, its text should be displayed"
20189 );
20190
20191 // Typing inside of buffer 1 causes that buffer to be unfolded.
20192 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20193 assert_eq!(
20194 multi_buffer
20195 .read(cx)
20196 .snapshot(cx)
20197 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
20198 .collect::<String>(),
20199 "bbbb"
20200 );
20201 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
20202 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
20203 });
20204 editor.handle_input("B", window, cx);
20205 });
20206
20207 assert_eq!(
20208 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20209 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
20210 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
20211 );
20212
20213 multi_buffer_editor.update(cx, |editor, cx| {
20214 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
20215 });
20216 assert_eq!(
20217 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20218 "\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",
20219 "After unfolding the all buffers, all original text should be displayed"
20220 );
20221}
20222
20223#[gpui::test]
20224async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
20225 init_test(cx, |_| {});
20226
20227 let sample_text_1 = "1111\n2222\n3333".to_string();
20228 let sample_text_2 = "4444\n5555\n6666".to_string();
20229 let sample_text_3 = "7777\n8888\n9999".to_string();
20230
20231 let fs = FakeFs::new(cx.executor());
20232 fs.insert_tree(
20233 path!("/a"),
20234 json!({
20235 "first.rs": sample_text_1,
20236 "second.rs": sample_text_2,
20237 "third.rs": sample_text_3,
20238 }),
20239 )
20240 .await;
20241 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20242 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20243 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20244 let worktree = project.update(cx, |project, cx| {
20245 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20246 assert_eq!(worktrees.len(), 1);
20247 worktrees.pop().unwrap()
20248 });
20249 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20250
20251 let buffer_1 = project
20252 .update(cx, |project, cx| {
20253 project.open_buffer((worktree_id, "first.rs"), cx)
20254 })
20255 .await
20256 .unwrap();
20257 let buffer_2 = project
20258 .update(cx, |project, cx| {
20259 project.open_buffer((worktree_id, "second.rs"), cx)
20260 })
20261 .await
20262 .unwrap();
20263 let buffer_3 = project
20264 .update(cx, |project, cx| {
20265 project.open_buffer((worktree_id, "third.rs"), cx)
20266 })
20267 .await
20268 .unwrap();
20269
20270 let multi_buffer = cx.new(|cx| {
20271 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20272 multi_buffer.push_excerpts(
20273 buffer_1.clone(),
20274 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20275 cx,
20276 );
20277 multi_buffer.push_excerpts(
20278 buffer_2.clone(),
20279 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20280 cx,
20281 );
20282 multi_buffer.push_excerpts(
20283 buffer_3.clone(),
20284 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20285 cx,
20286 );
20287 multi_buffer
20288 });
20289
20290 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20291 Editor::new(
20292 EditorMode::full(),
20293 multi_buffer,
20294 Some(project.clone()),
20295 window,
20296 cx,
20297 )
20298 });
20299
20300 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
20301 assert_eq!(
20302 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20303 full_text,
20304 );
20305
20306 multi_buffer_editor.update(cx, |editor, cx| {
20307 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
20308 });
20309 assert_eq!(
20310 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20311 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
20312 "After folding the first buffer, its text should not be displayed"
20313 );
20314
20315 multi_buffer_editor.update(cx, |editor, cx| {
20316 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
20317 });
20318
20319 assert_eq!(
20320 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20321 "\n\n\n\n\n\n7777\n8888\n9999",
20322 "After folding the second buffer, its text should not be displayed"
20323 );
20324
20325 multi_buffer_editor.update(cx, |editor, cx| {
20326 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
20327 });
20328 assert_eq!(
20329 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20330 "\n\n\n\n\n",
20331 "After folding the third buffer, its text should not be displayed"
20332 );
20333
20334 multi_buffer_editor.update(cx, |editor, cx| {
20335 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
20336 });
20337 assert_eq!(
20338 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20339 "\n\n\n\n4444\n5555\n6666\n\n",
20340 "After unfolding the second buffer, its text should be displayed"
20341 );
20342
20343 multi_buffer_editor.update(cx, |editor, cx| {
20344 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
20345 });
20346 assert_eq!(
20347 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20348 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
20349 "After unfolding the first buffer, its text should be displayed"
20350 );
20351
20352 multi_buffer_editor.update(cx, |editor, cx| {
20353 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
20354 });
20355 assert_eq!(
20356 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20357 full_text,
20358 "After unfolding all buffers, all original text should be displayed"
20359 );
20360}
20361
20362#[gpui::test]
20363async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
20364 init_test(cx, |_| {});
20365
20366 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
20367
20368 let fs = FakeFs::new(cx.executor());
20369 fs.insert_tree(
20370 path!("/a"),
20371 json!({
20372 "main.rs": sample_text,
20373 }),
20374 )
20375 .await;
20376 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20377 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20378 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20379 let worktree = project.update(cx, |project, cx| {
20380 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20381 assert_eq!(worktrees.len(), 1);
20382 worktrees.pop().unwrap()
20383 });
20384 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20385
20386 let buffer_1 = project
20387 .update(cx, |project, cx| {
20388 project.open_buffer((worktree_id, "main.rs"), cx)
20389 })
20390 .await
20391 .unwrap();
20392
20393 let multi_buffer = cx.new(|cx| {
20394 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20395 multi_buffer.push_excerpts(
20396 buffer_1.clone(),
20397 [ExcerptRange::new(
20398 Point::new(0, 0)
20399 ..Point::new(
20400 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
20401 0,
20402 ),
20403 )],
20404 cx,
20405 );
20406 multi_buffer
20407 });
20408 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20409 Editor::new(
20410 EditorMode::full(),
20411 multi_buffer,
20412 Some(project.clone()),
20413 window,
20414 cx,
20415 )
20416 });
20417
20418 let selection_range = Point::new(1, 0)..Point::new(2, 0);
20419 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20420 enum TestHighlight {}
20421 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
20422 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
20423 editor.highlight_text::<TestHighlight>(
20424 vec![highlight_range.clone()],
20425 HighlightStyle::color(Hsla::green()),
20426 cx,
20427 );
20428 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20429 s.select_ranges(Some(highlight_range))
20430 });
20431 });
20432
20433 let full_text = format!("\n\n{sample_text}");
20434 assert_eq!(
20435 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20436 full_text,
20437 );
20438}
20439
20440#[gpui::test]
20441async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
20442 init_test(cx, |_| {});
20443 cx.update(|cx| {
20444 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
20445 "keymaps/default-linux.json",
20446 cx,
20447 )
20448 .unwrap();
20449 cx.bind_keys(default_key_bindings);
20450 });
20451
20452 let (editor, cx) = cx.add_window_view(|window, cx| {
20453 let multi_buffer = MultiBuffer::build_multi(
20454 [
20455 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
20456 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
20457 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
20458 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
20459 ],
20460 cx,
20461 );
20462 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
20463
20464 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
20465 // fold all but the second buffer, so that we test navigating between two
20466 // adjacent folded buffers, as well as folded buffers at the start and
20467 // end the multibuffer
20468 editor.fold_buffer(buffer_ids[0], cx);
20469 editor.fold_buffer(buffer_ids[2], cx);
20470 editor.fold_buffer(buffer_ids[3], cx);
20471
20472 editor
20473 });
20474 cx.simulate_resize(size(px(1000.), px(1000.)));
20475
20476 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
20477 cx.assert_excerpts_with_selections(indoc! {"
20478 [EXCERPT]
20479 ˇ[FOLDED]
20480 [EXCERPT]
20481 a1
20482 b1
20483 [EXCERPT]
20484 [FOLDED]
20485 [EXCERPT]
20486 [FOLDED]
20487 "
20488 });
20489 cx.simulate_keystroke("down");
20490 cx.assert_excerpts_with_selections(indoc! {"
20491 [EXCERPT]
20492 [FOLDED]
20493 [EXCERPT]
20494 ˇa1
20495 b1
20496 [EXCERPT]
20497 [FOLDED]
20498 [EXCERPT]
20499 [FOLDED]
20500 "
20501 });
20502 cx.simulate_keystroke("down");
20503 cx.assert_excerpts_with_selections(indoc! {"
20504 [EXCERPT]
20505 [FOLDED]
20506 [EXCERPT]
20507 a1
20508 ˇb1
20509 [EXCERPT]
20510 [FOLDED]
20511 [EXCERPT]
20512 [FOLDED]
20513 "
20514 });
20515 cx.simulate_keystroke("down");
20516 cx.assert_excerpts_with_selections(indoc! {"
20517 [EXCERPT]
20518 [FOLDED]
20519 [EXCERPT]
20520 a1
20521 b1
20522 ˇ[EXCERPT]
20523 [FOLDED]
20524 [EXCERPT]
20525 [FOLDED]
20526 "
20527 });
20528 cx.simulate_keystroke("down");
20529 cx.assert_excerpts_with_selections(indoc! {"
20530 [EXCERPT]
20531 [FOLDED]
20532 [EXCERPT]
20533 a1
20534 b1
20535 [EXCERPT]
20536 ˇ[FOLDED]
20537 [EXCERPT]
20538 [FOLDED]
20539 "
20540 });
20541 for _ in 0..5 {
20542 cx.simulate_keystroke("down");
20543 cx.assert_excerpts_with_selections(indoc! {"
20544 [EXCERPT]
20545 [FOLDED]
20546 [EXCERPT]
20547 a1
20548 b1
20549 [EXCERPT]
20550 [FOLDED]
20551 [EXCERPT]
20552 ˇ[FOLDED]
20553 "
20554 });
20555 }
20556
20557 cx.simulate_keystroke("up");
20558 cx.assert_excerpts_with_selections(indoc! {"
20559 [EXCERPT]
20560 [FOLDED]
20561 [EXCERPT]
20562 a1
20563 b1
20564 [EXCERPT]
20565 ˇ[FOLDED]
20566 [EXCERPT]
20567 [FOLDED]
20568 "
20569 });
20570 cx.simulate_keystroke("up");
20571 cx.assert_excerpts_with_selections(indoc! {"
20572 [EXCERPT]
20573 [FOLDED]
20574 [EXCERPT]
20575 a1
20576 b1
20577 ˇ[EXCERPT]
20578 [FOLDED]
20579 [EXCERPT]
20580 [FOLDED]
20581 "
20582 });
20583 cx.simulate_keystroke("up");
20584 cx.assert_excerpts_with_selections(indoc! {"
20585 [EXCERPT]
20586 [FOLDED]
20587 [EXCERPT]
20588 a1
20589 ˇb1
20590 [EXCERPT]
20591 [FOLDED]
20592 [EXCERPT]
20593 [FOLDED]
20594 "
20595 });
20596 cx.simulate_keystroke("up");
20597 cx.assert_excerpts_with_selections(indoc! {"
20598 [EXCERPT]
20599 [FOLDED]
20600 [EXCERPT]
20601 ˇa1
20602 b1
20603 [EXCERPT]
20604 [FOLDED]
20605 [EXCERPT]
20606 [FOLDED]
20607 "
20608 });
20609 for _ in 0..5 {
20610 cx.simulate_keystroke("up");
20611 cx.assert_excerpts_with_selections(indoc! {"
20612 [EXCERPT]
20613 ˇ[FOLDED]
20614 [EXCERPT]
20615 a1
20616 b1
20617 [EXCERPT]
20618 [FOLDED]
20619 [EXCERPT]
20620 [FOLDED]
20621 "
20622 });
20623 }
20624}
20625
20626#[gpui::test]
20627async fn test_edit_prediction_text(cx: &mut TestAppContext) {
20628 init_test(cx, |_| {});
20629
20630 // Simple insertion
20631 assert_highlighted_edits(
20632 "Hello, world!",
20633 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
20634 true,
20635 cx,
20636 |highlighted_edits, cx| {
20637 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
20638 assert_eq!(highlighted_edits.highlights.len(), 1);
20639 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
20640 assert_eq!(
20641 highlighted_edits.highlights[0].1.background_color,
20642 Some(cx.theme().status().created_background)
20643 );
20644 },
20645 )
20646 .await;
20647
20648 // Replacement
20649 assert_highlighted_edits(
20650 "This is a test.",
20651 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
20652 false,
20653 cx,
20654 |highlighted_edits, cx| {
20655 assert_eq!(highlighted_edits.text, "That is a test.");
20656 assert_eq!(highlighted_edits.highlights.len(), 1);
20657 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
20658 assert_eq!(
20659 highlighted_edits.highlights[0].1.background_color,
20660 Some(cx.theme().status().created_background)
20661 );
20662 },
20663 )
20664 .await;
20665
20666 // Multiple edits
20667 assert_highlighted_edits(
20668 "Hello, world!",
20669 vec![
20670 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
20671 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
20672 ],
20673 false,
20674 cx,
20675 |highlighted_edits, cx| {
20676 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
20677 assert_eq!(highlighted_edits.highlights.len(), 2);
20678 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
20679 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
20680 assert_eq!(
20681 highlighted_edits.highlights[0].1.background_color,
20682 Some(cx.theme().status().created_background)
20683 );
20684 assert_eq!(
20685 highlighted_edits.highlights[1].1.background_color,
20686 Some(cx.theme().status().created_background)
20687 );
20688 },
20689 )
20690 .await;
20691
20692 // Multiple lines with edits
20693 assert_highlighted_edits(
20694 "First line\nSecond line\nThird line\nFourth line",
20695 vec![
20696 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
20697 (
20698 Point::new(2, 0)..Point::new(2, 10),
20699 "New third line".to_string(),
20700 ),
20701 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
20702 ],
20703 false,
20704 cx,
20705 |highlighted_edits, cx| {
20706 assert_eq!(
20707 highlighted_edits.text,
20708 "Second modified\nNew third line\nFourth updated line"
20709 );
20710 assert_eq!(highlighted_edits.highlights.len(), 3);
20711 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
20712 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
20713 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
20714 for highlight in &highlighted_edits.highlights {
20715 assert_eq!(
20716 highlight.1.background_color,
20717 Some(cx.theme().status().created_background)
20718 );
20719 }
20720 },
20721 )
20722 .await;
20723}
20724
20725#[gpui::test]
20726async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
20727 init_test(cx, |_| {});
20728
20729 // Deletion
20730 assert_highlighted_edits(
20731 "Hello, world!",
20732 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
20733 true,
20734 cx,
20735 |highlighted_edits, cx| {
20736 assert_eq!(highlighted_edits.text, "Hello, world!");
20737 assert_eq!(highlighted_edits.highlights.len(), 1);
20738 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
20739 assert_eq!(
20740 highlighted_edits.highlights[0].1.background_color,
20741 Some(cx.theme().status().deleted_background)
20742 );
20743 },
20744 )
20745 .await;
20746
20747 // Insertion
20748 assert_highlighted_edits(
20749 "Hello, world!",
20750 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
20751 true,
20752 cx,
20753 |highlighted_edits, cx| {
20754 assert_eq!(highlighted_edits.highlights.len(), 1);
20755 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
20756 assert_eq!(
20757 highlighted_edits.highlights[0].1.background_color,
20758 Some(cx.theme().status().created_background)
20759 );
20760 },
20761 )
20762 .await;
20763}
20764
20765async fn assert_highlighted_edits(
20766 text: &str,
20767 edits: Vec<(Range<Point>, String)>,
20768 include_deletions: bool,
20769 cx: &mut TestAppContext,
20770 assertion_fn: impl Fn(HighlightedText, &App),
20771) {
20772 let window = cx.add_window(|window, cx| {
20773 let buffer = MultiBuffer::build_simple(text, cx);
20774 Editor::new(EditorMode::full(), buffer, None, window, cx)
20775 });
20776 let cx = &mut VisualTestContext::from_window(*window, cx);
20777
20778 let (buffer, snapshot) = window
20779 .update(cx, |editor, _window, cx| {
20780 (
20781 editor.buffer().clone(),
20782 editor.buffer().read(cx).snapshot(cx),
20783 )
20784 })
20785 .unwrap();
20786
20787 let edits = edits
20788 .into_iter()
20789 .map(|(range, edit)| {
20790 (
20791 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
20792 edit,
20793 )
20794 })
20795 .collect::<Vec<_>>();
20796
20797 let text_anchor_edits = edits
20798 .clone()
20799 .into_iter()
20800 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
20801 .collect::<Vec<_>>();
20802
20803 let edit_preview = window
20804 .update(cx, |_, _window, cx| {
20805 buffer
20806 .read(cx)
20807 .as_singleton()
20808 .unwrap()
20809 .read(cx)
20810 .preview_edits(text_anchor_edits.into(), cx)
20811 })
20812 .unwrap()
20813 .await;
20814
20815 cx.update(|_window, cx| {
20816 let highlighted_edits = edit_prediction_edit_text(
20817 &snapshot.as_singleton().unwrap().2,
20818 &edits,
20819 &edit_preview,
20820 include_deletions,
20821 cx,
20822 );
20823 assertion_fn(highlighted_edits, cx)
20824 });
20825}
20826
20827#[track_caller]
20828fn assert_breakpoint(
20829 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
20830 path: &Arc<Path>,
20831 expected: Vec<(u32, Breakpoint)>,
20832) {
20833 if expected.len() == 0usize {
20834 assert!(!breakpoints.contains_key(path), "{}", path.display());
20835 } else {
20836 let mut breakpoint = breakpoints
20837 .get(path)
20838 .unwrap()
20839 .into_iter()
20840 .map(|breakpoint| {
20841 (
20842 breakpoint.row,
20843 Breakpoint {
20844 message: breakpoint.message.clone(),
20845 state: breakpoint.state,
20846 condition: breakpoint.condition.clone(),
20847 hit_condition: breakpoint.hit_condition.clone(),
20848 },
20849 )
20850 })
20851 .collect::<Vec<_>>();
20852
20853 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
20854
20855 assert_eq!(expected, breakpoint);
20856 }
20857}
20858
20859fn add_log_breakpoint_at_cursor(
20860 editor: &mut Editor,
20861 log_message: &str,
20862 window: &mut Window,
20863 cx: &mut Context<Editor>,
20864) {
20865 let (anchor, bp) = editor
20866 .breakpoints_at_cursors(window, cx)
20867 .first()
20868 .and_then(|(anchor, bp)| {
20869 if let Some(bp) = bp {
20870 Some((*anchor, bp.clone()))
20871 } else {
20872 None
20873 }
20874 })
20875 .unwrap_or_else(|| {
20876 let cursor_position: Point = editor.selections.newest(cx).head();
20877
20878 let breakpoint_position = editor
20879 .snapshot(window, cx)
20880 .display_snapshot
20881 .buffer_snapshot
20882 .anchor_before(Point::new(cursor_position.row, 0));
20883
20884 (breakpoint_position, Breakpoint::new_log(&log_message))
20885 });
20886
20887 editor.edit_breakpoint_at_anchor(
20888 anchor,
20889 bp,
20890 BreakpointEditAction::EditLogMessage(log_message.into()),
20891 cx,
20892 );
20893}
20894
20895#[gpui::test]
20896async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
20897 init_test(cx, |_| {});
20898
20899 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20900 let fs = FakeFs::new(cx.executor());
20901 fs.insert_tree(
20902 path!("/a"),
20903 json!({
20904 "main.rs": sample_text,
20905 }),
20906 )
20907 .await;
20908 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20909 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20910 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20911
20912 let fs = FakeFs::new(cx.executor());
20913 fs.insert_tree(
20914 path!("/a"),
20915 json!({
20916 "main.rs": sample_text,
20917 }),
20918 )
20919 .await;
20920 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20921 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20922 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20923 let worktree_id = workspace
20924 .update(cx, |workspace, _window, cx| {
20925 workspace.project().update(cx, |project, cx| {
20926 project.worktrees(cx).next().unwrap().read(cx).id()
20927 })
20928 })
20929 .unwrap();
20930
20931 let buffer = project
20932 .update(cx, |project, cx| {
20933 project.open_buffer((worktree_id, "main.rs"), cx)
20934 })
20935 .await
20936 .unwrap();
20937
20938 let (editor, cx) = cx.add_window_view(|window, cx| {
20939 Editor::new(
20940 EditorMode::full(),
20941 MultiBuffer::build_from_buffer(buffer, cx),
20942 Some(project.clone()),
20943 window,
20944 cx,
20945 )
20946 });
20947
20948 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20949 let abs_path = project.read_with(cx, |project, cx| {
20950 project
20951 .absolute_path(&project_path, cx)
20952 .map(|path_buf| Arc::from(path_buf.to_owned()))
20953 .unwrap()
20954 });
20955
20956 // assert we can add breakpoint on the first line
20957 editor.update_in(cx, |editor, window, cx| {
20958 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20959 editor.move_to_end(&MoveToEnd, window, cx);
20960 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20961 });
20962
20963 let breakpoints = editor.update(cx, |editor, cx| {
20964 editor
20965 .breakpoint_store()
20966 .as_ref()
20967 .unwrap()
20968 .read(cx)
20969 .all_source_breakpoints(cx)
20970 .clone()
20971 });
20972
20973 assert_eq!(1, breakpoints.len());
20974 assert_breakpoint(
20975 &breakpoints,
20976 &abs_path,
20977 vec![
20978 (0, Breakpoint::new_standard()),
20979 (3, Breakpoint::new_standard()),
20980 ],
20981 );
20982
20983 editor.update_in(cx, |editor, window, cx| {
20984 editor.move_to_beginning(&MoveToBeginning, window, cx);
20985 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20986 });
20987
20988 let breakpoints = editor.update(cx, |editor, cx| {
20989 editor
20990 .breakpoint_store()
20991 .as_ref()
20992 .unwrap()
20993 .read(cx)
20994 .all_source_breakpoints(cx)
20995 .clone()
20996 });
20997
20998 assert_eq!(1, breakpoints.len());
20999 assert_breakpoint(
21000 &breakpoints,
21001 &abs_path,
21002 vec![(3, Breakpoint::new_standard())],
21003 );
21004
21005 editor.update_in(cx, |editor, window, cx| {
21006 editor.move_to_end(&MoveToEnd, window, cx);
21007 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21008 });
21009
21010 let breakpoints = editor.update(cx, |editor, cx| {
21011 editor
21012 .breakpoint_store()
21013 .as_ref()
21014 .unwrap()
21015 .read(cx)
21016 .all_source_breakpoints(cx)
21017 .clone()
21018 });
21019
21020 assert_eq!(0, breakpoints.len());
21021 assert_breakpoint(&breakpoints, &abs_path, vec![]);
21022}
21023
21024#[gpui::test]
21025async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
21026 init_test(cx, |_| {});
21027
21028 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
21029
21030 let fs = FakeFs::new(cx.executor());
21031 fs.insert_tree(
21032 path!("/a"),
21033 json!({
21034 "main.rs": sample_text,
21035 }),
21036 )
21037 .await;
21038 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21039 let (workspace, cx) =
21040 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21041
21042 let worktree_id = workspace.update(cx, |workspace, cx| {
21043 workspace.project().update(cx, |project, cx| {
21044 project.worktrees(cx).next().unwrap().read(cx).id()
21045 })
21046 });
21047
21048 let buffer = project
21049 .update(cx, |project, cx| {
21050 project.open_buffer((worktree_id, "main.rs"), cx)
21051 })
21052 .await
21053 .unwrap();
21054
21055 let (editor, cx) = cx.add_window_view(|window, cx| {
21056 Editor::new(
21057 EditorMode::full(),
21058 MultiBuffer::build_from_buffer(buffer, cx),
21059 Some(project.clone()),
21060 window,
21061 cx,
21062 )
21063 });
21064
21065 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
21066 let abs_path = project.read_with(cx, |project, cx| {
21067 project
21068 .absolute_path(&project_path, cx)
21069 .map(|path_buf| Arc::from(path_buf.to_owned()))
21070 .unwrap()
21071 });
21072
21073 editor.update_in(cx, |editor, window, cx| {
21074 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
21075 });
21076
21077 let breakpoints = editor.update(cx, |editor, cx| {
21078 editor
21079 .breakpoint_store()
21080 .as_ref()
21081 .unwrap()
21082 .read(cx)
21083 .all_source_breakpoints(cx)
21084 .clone()
21085 });
21086
21087 assert_breakpoint(
21088 &breakpoints,
21089 &abs_path,
21090 vec![(0, Breakpoint::new_log("hello world"))],
21091 );
21092
21093 // Removing a log message from a log breakpoint should remove it
21094 editor.update_in(cx, |editor, window, cx| {
21095 add_log_breakpoint_at_cursor(editor, "", window, cx);
21096 });
21097
21098 let breakpoints = editor.update(cx, |editor, cx| {
21099 editor
21100 .breakpoint_store()
21101 .as_ref()
21102 .unwrap()
21103 .read(cx)
21104 .all_source_breakpoints(cx)
21105 .clone()
21106 });
21107
21108 assert_breakpoint(&breakpoints, &abs_path, vec![]);
21109
21110 editor.update_in(cx, |editor, window, cx| {
21111 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21112 editor.move_to_end(&MoveToEnd, window, cx);
21113 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21114 // Not adding a log message to a standard breakpoint shouldn't remove it
21115 add_log_breakpoint_at_cursor(editor, "", window, cx);
21116 });
21117
21118 let breakpoints = editor.update(cx, |editor, cx| {
21119 editor
21120 .breakpoint_store()
21121 .as_ref()
21122 .unwrap()
21123 .read(cx)
21124 .all_source_breakpoints(cx)
21125 .clone()
21126 });
21127
21128 assert_breakpoint(
21129 &breakpoints,
21130 &abs_path,
21131 vec![
21132 (0, Breakpoint::new_standard()),
21133 (3, Breakpoint::new_standard()),
21134 ],
21135 );
21136
21137 editor.update_in(cx, |editor, window, cx| {
21138 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
21139 });
21140
21141 let breakpoints = editor.update(cx, |editor, cx| {
21142 editor
21143 .breakpoint_store()
21144 .as_ref()
21145 .unwrap()
21146 .read(cx)
21147 .all_source_breakpoints(cx)
21148 .clone()
21149 });
21150
21151 assert_breakpoint(
21152 &breakpoints,
21153 &abs_path,
21154 vec![
21155 (0, Breakpoint::new_standard()),
21156 (3, Breakpoint::new_log("hello world")),
21157 ],
21158 );
21159
21160 editor.update_in(cx, |editor, window, cx| {
21161 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
21162 });
21163
21164 let breakpoints = editor.update(cx, |editor, cx| {
21165 editor
21166 .breakpoint_store()
21167 .as_ref()
21168 .unwrap()
21169 .read(cx)
21170 .all_source_breakpoints(cx)
21171 .clone()
21172 });
21173
21174 assert_breakpoint(
21175 &breakpoints,
21176 &abs_path,
21177 vec![
21178 (0, Breakpoint::new_standard()),
21179 (3, Breakpoint::new_log("hello Earth!!")),
21180 ],
21181 );
21182}
21183
21184/// This also tests that Editor::breakpoint_at_cursor_head is working properly
21185/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
21186/// or when breakpoints were placed out of order. This tests for a regression too
21187#[gpui::test]
21188async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
21189 init_test(cx, |_| {});
21190
21191 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
21192 let fs = FakeFs::new(cx.executor());
21193 fs.insert_tree(
21194 path!("/a"),
21195 json!({
21196 "main.rs": sample_text,
21197 }),
21198 )
21199 .await;
21200 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21201 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21202 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21203
21204 let fs = FakeFs::new(cx.executor());
21205 fs.insert_tree(
21206 path!("/a"),
21207 json!({
21208 "main.rs": sample_text,
21209 }),
21210 )
21211 .await;
21212 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21213 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21214 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21215 let worktree_id = workspace
21216 .update(cx, |workspace, _window, cx| {
21217 workspace.project().update(cx, |project, cx| {
21218 project.worktrees(cx).next().unwrap().read(cx).id()
21219 })
21220 })
21221 .unwrap();
21222
21223 let buffer = project
21224 .update(cx, |project, cx| {
21225 project.open_buffer((worktree_id, "main.rs"), cx)
21226 })
21227 .await
21228 .unwrap();
21229
21230 let (editor, cx) = cx.add_window_view(|window, cx| {
21231 Editor::new(
21232 EditorMode::full(),
21233 MultiBuffer::build_from_buffer(buffer, cx),
21234 Some(project.clone()),
21235 window,
21236 cx,
21237 )
21238 });
21239
21240 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
21241 let abs_path = project.read_with(cx, |project, cx| {
21242 project
21243 .absolute_path(&project_path, cx)
21244 .map(|path_buf| Arc::from(path_buf.to_owned()))
21245 .unwrap()
21246 });
21247
21248 // assert we can add breakpoint on the first line
21249 editor.update_in(cx, |editor, window, cx| {
21250 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21251 editor.move_to_end(&MoveToEnd, window, cx);
21252 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21253 editor.move_up(&MoveUp, window, cx);
21254 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21255 });
21256
21257 let breakpoints = editor.update(cx, |editor, cx| {
21258 editor
21259 .breakpoint_store()
21260 .as_ref()
21261 .unwrap()
21262 .read(cx)
21263 .all_source_breakpoints(cx)
21264 .clone()
21265 });
21266
21267 assert_eq!(1, breakpoints.len());
21268 assert_breakpoint(
21269 &breakpoints,
21270 &abs_path,
21271 vec![
21272 (0, Breakpoint::new_standard()),
21273 (2, Breakpoint::new_standard()),
21274 (3, Breakpoint::new_standard()),
21275 ],
21276 );
21277
21278 editor.update_in(cx, |editor, window, cx| {
21279 editor.move_to_beginning(&MoveToBeginning, window, cx);
21280 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21281 editor.move_to_end(&MoveToEnd, window, cx);
21282 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21283 // Disabling a breakpoint that doesn't exist should do nothing
21284 editor.move_up(&MoveUp, window, cx);
21285 editor.move_up(&MoveUp, window, cx);
21286 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21287 });
21288
21289 let breakpoints = editor.update(cx, |editor, cx| {
21290 editor
21291 .breakpoint_store()
21292 .as_ref()
21293 .unwrap()
21294 .read(cx)
21295 .all_source_breakpoints(cx)
21296 .clone()
21297 });
21298
21299 let disable_breakpoint = {
21300 let mut bp = Breakpoint::new_standard();
21301 bp.state = BreakpointState::Disabled;
21302 bp
21303 };
21304
21305 assert_eq!(1, breakpoints.len());
21306 assert_breakpoint(
21307 &breakpoints,
21308 &abs_path,
21309 vec![
21310 (0, disable_breakpoint.clone()),
21311 (2, Breakpoint::new_standard()),
21312 (3, disable_breakpoint.clone()),
21313 ],
21314 );
21315
21316 editor.update_in(cx, |editor, window, cx| {
21317 editor.move_to_beginning(&MoveToBeginning, window, cx);
21318 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
21319 editor.move_to_end(&MoveToEnd, window, cx);
21320 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
21321 editor.move_up(&MoveUp, window, cx);
21322 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21323 });
21324
21325 let breakpoints = editor.update(cx, |editor, cx| {
21326 editor
21327 .breakpoint_store()
21328 .as_ref()
21329 .unwrap()
21330 .read(cx)
21331 .all_source_breakpoints(cx)
21332 .clone()
21333 });
21334
21335 assert_eq!(1, breakpoints.len());
21336 assert_breakpoint(
21337 &breakpoints,
21338 &abs_path,
21339 vec![
21340 (0, Breakpoint::new_standard()),
21341 (2, disable_breakpoint),
21342 (3, Breakpoint::new_standard()),
21343 ],
21344 );
21345}
21346
21347#[gpui::test]
21348async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
21349 init_test(cx, |_| {});
21350 let capabilities = lsp::ServerCapabilities {
21351 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
21352 prepare_provider: Some(true),
21353 work_done_progress_options: Default::default(),
21354 })),
21355 ..Default::default()
21356 };
21357 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21358
21359 cx.set_state(indoc! {"
21360 struct Fˇoo {}
21361 "});
21362
21363 cx.update_editor(|editor, _, cx| {
21364 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21365 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21366 editor.highlight_background::<DocumentHighlightRead>(
21367 &[highlight_range],
21368 |theme| theme.colors().editor_document_highlight_read_background,
21369 cx,
21370 );
21371 });
21372
21373 let mut prepare_rename_handler = cx
21374 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
21375 move |_, _, _| async move {
21376 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
21377 start: lsp::Position {
21378 line: 0,
21379 character: 7,
21380 },
21381 end: lsp::Position {
21382 line: 0,
21383 character: 10,
21384 },
21385 })))
21386 },
21387 );
21388 let prepare_rename_task = cx
21389 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21390 .expect("Prepare rename was not started");
21391 prepare_rename_handler.next().await.unwrap();
21392 prepare_rename_task.await.expect("Prepare rename failed");
21393
21394 let mut rename_handler =
21395 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21396 let edit = lsp::TextEdit {
21397 range: lsp::Range {
21398 start: lsp::Position {
21399 line: 0,
21400 character: 7,
21401 },
21402 end: lsp::Position {
21403 line: 0,
21404 character: 10,
21405 },
21406 },
21407 new_text: "FooRenamed".to_string(),
21408 };
21409 Ok(Some(lsp::WorkspaceEdit::new(
21410 // Specify the same edit twice
21411 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
21412 )))
21413 });
21414 let rename_task = cx
21415 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21416 .expect("Confirm rename was not started");
21417 rename_handler.next().await.unwrap();
21418 rename_task.await.expect("Confirm rename failed");
21419 cx.run_until_parked();
21420
21421 // Despite two edits, only one is actually applied as those are identical
21422 cx.assert_editor_state(indoc! {"
21423 struct FooRenamedˇ {}
21424 "});
21425}
21426
21427#[gpui::test]
21428async fn test_rename_without_prepare(cx: &mut TestAppContext) {
21429 init_test(cx, |_| {});
21430 // These capabilities indicate that the server does not support prepare rename.
21431 let capabilities = lsp::ServerCapabilities {
21432 rename_provider: Some(lsp::OneOf::Left(true)),
21433 ..Default::default()
21434 };
21435 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21436
21437 cx.set_state(indoc! {"
21438 struct Fˇoo {}
21439 "});
21440
21441 cx.update_editor(|editor, _window, cx| {
21442 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21443 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21444 editor.highlight_background::<DocumentHighlightRead>(
21445 &[highlight_range],
21446 |theme| theme.colors().editor_document_highlight_read_background,
21447 cx,
21448 );
21449 });
21450
21451 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21452 .expect("Prepare rename was not started")
21453 .await
21454 .expect("Prepare rename failed");
21455
21456 let mut rename_handler =
21457 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21458 let edit = lsp::TextEdit {
21459 range: lsp::Range {
21460 start: lsp::Position {
21461 line: 0,
21462 character: 7,
21463 },
21464 end: lsp::Position {
21465 line: 0,
21466 character: 10,
21467 },
21468 },
21469 new_text: "FooRenamed".to_string(),
21470 };
21471 Ok(Some(lsp::WorkspaceEdit::new(
21472 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
21473 )))
21474 });
21475 let rename_task = cx
21476 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21477 .expect("Confirm rename was not started");
21478 rename_handler.next().await.unwrap();
21479 rename_task.await.expect("Confirm rename failed");
21480 cx.run_until_parked();
21481
21482 // Correct range is renamed, as `surrounding_word` is used to find it.
21483 cx.assert_editor_state(indoc! {"
21484 struct FooRenamedˇ {}
21485 "});
21486}
21487
21488#[gpui::test]
21489async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
21490 init_test(cx, |_| {});
21491 let mut cx = EditorTestContext::new(cx).await;
21492
21493 let language = Arc::new(
21494 Language::new(
21495 LanguageConfig::default(),
21496 Some(tree_sitter_html::LANGUAGE.into()),
21497 )
21498 .with_brackets_query(
21499 r#"
21500 ("<" @open "/>" @close)
21501 ("</" @open ">" @close)
21502 ("<" @open ">" @close)
21503 ("\"" @open "\"" @close)
21504 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
21505 "#,
21506 )
21507 .unwrap(),
21508 );
21509 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21510
21511 cx.set_state(indoc! {"
21512 <span>ˇ</span>
21513 "});
21514 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21515 cx.assert_editor_state(indoc! {"
21516 <span>
21517 ˇ
21518 </span>
21519 "});
21520
21521 cx.set_state(indoc! {"
21522 <span><span></span>ˇ</span>
21523 "});
21524 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21525 cx.assert_editor_state(indoc! {"
21526 <span><span></span>
21527 ˇ</span>
21528 "});
21529
21530 cx.set_state(indoc! {"
21531 <span>ˇ
21532 </span>
21533 "});
21534 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21535 cx.assert_editor_state(indoc! {"
21536 <span>
21537 ˇ
21538 </span>
21539 "});
21540}
21541
21542#[gpui::test(iterations = 10)]
21543async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
21544 init_test(cx, |_| {});
21545
21546 let fs = FakeFs::new(cx.executor());
21547 fs.insert_tree(
21548 path!("/dir"),
21549 json!({
21550 "a.ts": "a",
21551 }),
21552 )
21553 .await;
21554
21555 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
21556 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21557 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21558
21559 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21560 language_registry.add(Arc::new(Language::new(
21561 LanguageConfig {
21562 name: "TypeScript".into(),
21563 matcher: LanguageMatcher {
21564 path_suffixes: vec!["ts".to_string()],
21565 ..Default::default()
21566 },
21567 ..Default::default()
21568 },
21569 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
21570 )));
21571 let mut fake_language_servers = language_registry.register_fake_lsp(
21572 "TypeScript",
21573 FakeLspAdapter {
21574 capabilities: lsp::ServerCapabilities {
21575 code_lens_provider: Some(lsp::CodeLensOptions {
21576 resolve_provider: Some(true),
21577 }),
21578 execute_command_provider: Some(lsp::ExecuteCommandOptions {
21579 commands: vec!["_the/command".to_string()],
21580 ..lsp::ExecuteCommandOptions::default()
21581 }),
21582 ..lsp::ServerCapabilities::default()
21583 },
21584 ..FakeLspAdapter::default()
21585 },
21586 );
21587
21588 let editor = workspace
21589 .update(cx, |workspace, window, cx| {
21590 workspace.open_abs_path(
21591 PathBuf::from(path!("/dir/a.ts")),
21592 OpenOptions::default(),
21593 window,
21594 cx,
21595 )
21596 })
21597 .unwrap()
21598 .await
21599 .unwrap()
21600 .downcast::<Editor>()
21601 .unwrap();
21602 cx.executor().run_until_parked();
21603
21604 let fake_server = fake_language_servers.next().await.unwrap();
21605
21606 let buffer = editor.update(cx, |editor, cx| {
21607 editor
21608 .buffer()
21609 .read(cx)
21610 .as_singleton()
21611 .expect("have opened a single file by path")
21612 });
21613
21614 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
21615 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
21616 drop(buffer_snapshot);
21617 let actions = cx
21618 .update_window(*workspace, |_, window, cx| {
21619 project.code_actions(&buffer, anchor..anchor, window, cx)
21620 })
21621 .unwrap();
21622
21623 fake_server
21624 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21625 Ok(Some(vec![
21626 lsp::CodeLens {
21627 range: lsp::Range::default(),
21628 command: Some(lsp::Command {
21629 title: "Code lens command".to_owned(),
21630 command: "_the/command".to_owned(),
21631 arguments: None,
21632 }),
21633 data: None,
21634 },
21635 lsp::CodeLens {
21636 range: lsp::Range::default(),
21637 command: Some(lsp::Command {
21638 title: "Command not in capabilities".to_owned(),
21639 command: "not in capabilities".to_owned(),
21640 arguments: None,
21641 }),
21642 data: None,
21643 },
21644 lsp::CodeLens {
21645 range: lsp::Range {
21646 start: lsp::Position {
21647 line: 1,
21648 character: 1,
21649 },
21650 end: lsp::Position {
21651 line: 1,
21652 character: 1,
21653 },
21654 },
21655 command: Some(lsp::Command {
21656 title: "Command not in range".to_owned(),
21657 command: "_the/command".to_owned(),
21658 arguments: None,
21659 }),
21660 data: None,
21661 },
21662 ]))
21663 })
21664 .next()
21665 .await;
21666
21667 let actions = actions.await.unwrap();
21668 assert_eq!(
21669 actions.len(),
21670 1,
21671 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
21672 );
21673 let action = actions[0].clone();
21674 let apply = project.update(cx, |project, cx| {
21675 project.apply_code_action(buffer.clone(), action, true, cx)
21676 });
21677
21678 // Resolving the code action does not populate its edits. In absence of
21679 // edits, we must execute the given command.
21680 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
21681 |mut lens, _| async move {
21682 let lens_command = lens.command.as_mut().expect("should have a command");
21683 assert_eq!(lens_command.title, "Code lens command");
21684 lens_command.arguments = Some(vec![json!("the-argument")]);
21685 Ok(lens)
21686 },
21687 );
21688
21689 // While executing the command, the language server sends the editor
21690 // a `workspaceEdit` request.
21691 fake_server
21692 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
21693 let fake = fake_server.clone();
21694 move |params, _| {
21695 assert_eq!(params.command, "_the/command");
21696 let fake = fake.clone();
21697 async move {
21698 fake.server
21699 .request::<lsp::request::ApplyWorkspaceEdit>(
21700 lsp::ApplyWorkspaceEditParams {
21701 label: None,
21702 edit: lsp::WorkspaceEdit {
21703 changes: Some(
21704 [(
21705 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
21706 vec![lsp::TextEdit {
21707 range: lsp::Range::new(
21708 lsp::Position::new(0, 0),
21709 lsp::Position::new(0, 0),
21710 ),
21711 new_text: "X".into(),
21712 }],
21713 )]
21714 .into_iter()
21715 .collect(),
21716 ),
21717 ..lsp::WorkspaceEdit::default()
21718 },
21719 },
21720 )
21721 .await
21722 .into_response()
21723 .unwrap();
21724 Ok(Some(json!(null)))
21725 }
21726 }
21727 })
21728 .next()
21729 .await;
21730
21731 // Applying the code lens command returns a project transaction containing the edits
21732 // sent by the language server in its `workspaceEdit` request.
21733 let transaction = apply.await.unwrap();
21734 assert!(transaction.0.contains_key(&buffer));
21735 buffer.update(cx, |buffer, cx| {
21736 assert_eq!(buffer.text(), "Xa");
21737 buffer.undo(cx);
21738 assert_eq!(buffer.text(), "a");
21739 });
21740
21741 let actions_after_edits = cx
21742 .update_window(*workspace, |_, window, cx| {
21743 project.code_actions(&buffer, anchor..anchor, window, cx)
21744 })
21745 .unwrap()
21746 .await
21747 .unwrap();
21748 assert_eq!(
21749 actions, actions_after_edits,
21750 "For the same selection, same code lens actions should be returned"
21751 );
21752
21753 let _responses =
21754 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21755 panic!("No more code lens requests are expected");
21756 });
21757 editor.update_in(cx, |editor, window, cx| {
21758 editor.select_all(&SelectAll, window, cx);
21759 });
21760 cx.executor().run_until_parked();
21761 let new_actions = cx
21762 .update_window(*workspace, |_, window, cx| {
21763 project.code_actions(&buffer, anchor..anchor, window, cx)
21764 })
21765 .unwrap()
21766 .await
21767 .unwrap();
21768 assert_eq!(
21769 actions, new_actions,
21770 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
21771 );
21772}
21773
21774#[gpui::test]
21775async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
21776 init_test(cx, |_| {});
21777
21778 let fs = FakeFs::new(cx.executor());
21779 let main_text = r#"fn main() {
21780println!("1");
21781println!("2");
21782println!("3");
21783println!("4");
21784println!("5");
21785}"#;
21786 let lib_text = "mod foo {}";
21787 fs.insert_tree(
21788 path!("/a"),
21789 json!({
21790 "lib.rs": lib_text,
21791 "main.rs": main_text,
21792 }),
21793 )
21794 .await;
21795
21796 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21797 let (workspace, cx) =
21798 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21799 let worktree_id = workspace.update(cx, |workspace, cx| {
21800 workspace.project().update(cx, |project, cx| {
21801 project.worktrees(cx).next().unwrap().read(cx).id()
21802 })
21803 });
21804
21805 let expected_ranges = vec![
21806 Point::new(0, 0)..Point::new(0, 0),
21807 Point::new(1, 0)..Point::new(1, 1),
21808 Point::new(2, 0)..Point::new(2, 2),
21809 Point::new(3, 0)..Point::new(3, 3),
21810 ];
21811
21812 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21813 let editor_1 = workspace
21814 .update_in(cx, |workspace, window, cx| {
21815 workspace.open_path(
21816 (worktree_id, "main.rs"),
21817 Some(pane_1.downgrade()),
21818 true,
21819 window,
21820 cx,
21821 )
21822 })
21823 .unwrap()
21824 .await
21825 .downcast::<Editor>()
21826 .unwrap();
21827 pane_1.update(cx, |pane, cx| {
21828 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21829 open_editor.update(cx, |editor, cx| {
21830 assert_eq!(
21831 editor.display_text(cx),
21832 main_text,
21833 "Original main.rs text on initial open",
21834 );
21835 assert_eq!(
21836 editor
21837 .selections
21838 .all::<Point>(cx)
21839 .into_iter()
21840 .map(|s| s.range())
21841 .collect::<Vec<_>>(),
21842 vec![Point::zero()..Point::zero()],
21843 "Default selections on initial open",
21844 );
21845 })
21846 });
21847 editor_1.update_in(cx, |editor, window, cx| {
21848 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21849 s.select_ranges(expected_ranges.clone());
21850 });
21851 });
21852
21853 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
21854 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
21855 });
21856 let editor_2 = workspace
21857 .update_in(cx, |workspace, window, cx| {
21858 workspace.open_path(
21859 (worktree_id, "main.rs"),
21860 Some(pane_2.downgrade()),
21861 true,
21862 window,
21863 cx,
21864 )
21865 })
21866 .unwrap()
21867 .await
21868 .downcast::<Editor>()
21869 .unwrap();
21870 pane_2.update(cx, |pane, cx| {
21871 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21872 open_editor.update(cx, |editor, cx| {
21873 assert_eq!(
21874 editor.display_text(cx),
21875 main_text,
21876 "Original main.rs text on initial open in another panel",
21877 );
21878 assert_eq!(
21879 editor
21880 .selections
21881 .all::<Point>(cx)
21882 .into_iter()
21883 .map(|s| s.range())
21884 .collect::<Vec<_>>(),
21885 vec![Point::zero()..Point::zero()],
21886 "Default selections on initial open in another panel",
21887 );
21888 })
21889 });
21890
21891 editor_2.update_in(cx, |editor, window, cx| {
21892 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
21893 });
21894
21895 let _other_editor_1 = workspace
21896 .update_in(cx, |workspace, window, cx| {
21897 workspace.open_path(
21898 (worktree_id, "lib.rs"),
21899 Some(pane_1.downgrade()),
21900 true,
21901 window,
21902 cx,
21903 )
21904 })
21905 .unwrap()
21906 .await
21907 .downcast::<Editor>()
21908 .unwrap();
21909 pane_1
21910 .update_in(cx, |pane, window, cx| {
21911 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
21912 })
21913 .await
21914 .unwrap();
21915 drop(editor_1);
21916 pane_1.update(cx, |pane, cx| {
21917 pane.active_item()
21918 .unwrap()
21919 .downcast::<Editor>()
21920 .unwrap()
21921 .update(cx, |editor, cx| {
21922 assert_eq!(
21923 editor.display_text(cx),
21924 lib_text,
21925 "Other file should be open and active",
21926 );
21927 });
21928 assert_eq!(pane.items().count(), 1, "No other editors should be open");
21929 });
21930
21931 let _other_editor_2 = workspace
21932 .update_in(cx, |workspace, window, cx| {
21933 workspace.open_path(
21934 (worktree_id, "lib.rs"),
21935 Some(pane_2.downgrade()),
21936 true,
21937 window,
21938 cx,
21939 )
21940 })
21941 .unwrap()
21942 .await
21943 .downcast::<Editor>()
21944 .unwrap();
21945 pane_2
21946 .update_in(cx, |pane, window, cx| {
21947 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
21948 })
21949 .await
21950 .unwrap();
21951 drop(editor_2);
21952 pane_2.update(cx, |pane, cx| {
21953 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21954 open_editor.update(cx, |editor, cx| {
21955 assert_eq!(
21956 editor.display_text(cx),
21957 lib_text,
21958 "Other file should be open and active in another panel too",
21959 );
21960 });
21961 assert_eq!(
21962 pane.items().count(),
21963 1,
21964 "No other editors should be open in another pane",
21965 );
21966 });
21967
21968 let _editor_1_reopened = workspace
21969 .update_in(cx, |workspace, window, cx| {
21970 workspace.open_path(
21971 (worktree_id, "main.rs"),
21972 Some(pane_1.downgrade()),
21973 true,
21974 window,
21975 cx,
21976 )
21977 })
21978 .unwrap()
21979 .await
21980 .downcast::<Editor>()
21981 .unwrap();
21982 let _editor_2_reopened = workspace
21983 .update_in(cx, |workspace, window, cx| {
21984 workspace.open_path(
21985 (worktree_id, "main.rs"),
21986 Some(pane_2.downgrade()),
21987 true,
21988 window,
21989 cx,
21990 )
21991 })
21992 .unwrap()
21993 .await
21994 .downcast::<Editor>()
21995 .unwrap();
21996 pane_1.update(cx, |pane, cx| {
21997 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21998 open_editor.update(cx, |editor, cx| {
21999 assert_eq!(
22000 editor.display_text(cx),
22001 main_text,
22002 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
22003 );
22004 assert_eq!(
22005 editor
22006 .selections
22007 .all::<Point>(cx)
22008 .into_iter()
22009 .map(|s| s.range())
22010 .collect::<Vec<_>>(),
22011 expected_ranges,
22012 "Previous editor in the 1st panel had selections and should get them restored on reopen",
22013 );
22014 })
22015 });
22016 pane_2.update(cx, |pane, cx| {
22017 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22018 open_editor.update(cx, |editor, cx| {
22019 assert_eq!(
22020 editor.display_text(cx),
22021 r#"fn main() {
22022⋯rintln!("1");
22023⋯intln!("2");
22024⋯ntln!("3");
22025println!("4");
22026println!("5");
22027}"#,
22028 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
22029 );
22030 assert_eq!(
22031 editor
22032 .selections
22033 .all::<Point>(cx)
22034 .into_iter()
22035 .map(|s| s.range())
22036 .collect::<Vec<_>>(),
22037 vec![Point::zero()..Point::zero()],
22038 "Previous editor in the 2nd pane had no selections changed hence should restore none",
22039 );
22040 })
22041 });
22042}
22043
22044#[gpui::test]
22045async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
22046 init_test(cx, |_| {});
22047
22048 let fs = FakeFs::new(cx.executor());
22049 let main_text = r#"fn main() {
22050println!("1");
22051println!("2");
22052println!("3");
22053println!("4");
22054println!("5");
22055}"#;
22056 let lib_text = "mod foo {}";
22057 fs.insert_tree(
22058 path!("/a"),
22059 json!({
22060 "lib.rs": lib_text,
22061 "main.rs": main_text,
22062 }),
22063 )
22064 .await;
22065
22066 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22067 let (workspace, cx) =
22068 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22069 let worktree_id = workspace.update(cx, |workspace, cx| {
22070 workspace.project().update(cx, |project, cx| {
22071 project.worktrees(cx).next().unwrap().read(cx).id()
22072 })
22073 });
22074
22075 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22076 let editor = workspace
22077 .update_in(cx, |workspace, window, cx| {
22078 workspace.open_path(
22079 (worktree_id, "main.rs"),
22080 Some(pane.downgrade()),
22081 true,
22082 window,
22083 cx,
22084 )
22085 })
22086 .unwrap()
22087 .await
22088 .downcast::<Editor>()
22089 .unwrap();
22090 pane.update(cx, |pane, cx| {
22091 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22092 open_editor.update(cx, |editor, cx| {
22093 assert_eq!(
22094 editor.display_text(cx),
22095 main_text,
22096 "Original main.rs text on initial open",
22097 );
22098 })
22099 });
22100 editor.update_in(cx, |editor, window, cx| {
22101 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
22102 });
22103
22104 cx.update_global(|store: &mut SettingsStore, cx| {
22105 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
22106 s.restore_on_file_reopen = Some(false);
22107 });
22108 });
22109 editor.update_in(cx, |editor, window, cx| {
22110 editor.fold_ranges(
22111 vec![
22112 Point::new(1, 0)..Point::new(1, 1),
22113 Point::new(2, 0)..Point::new(2, 2),
22114 Point::new(3, 0)..Point::new(3, 3),
22115 ],
22116 false,
22117 window,
22118 cx,
22119 );
22120 });
22121 pane.update_in(cx, |pane, window, cx| {
22122 pane.close_all_items(&CloseAllItems::default(), window, cx)
22123 })
22124 .await
22125 .unwrap();
22126 pane.update(cx, |pane, _| {
22127 assert!(pane.active_item().is_none());
22128 });
22129 cx.update_global(|store: &mut SettingsStore, cx| {
22130 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
22131 s.restore_on_file_reopen = Some(true);
22132 });
22133 });
22134
22135 let _editor_reopened = workspace
22136 .update_in(cx, |workspace, window, cx| {
22137 workspace.open_path(
22138 (worktree_id, "main.rs"),
22139 Some(pane.downgrade()),
22140 true,
22141 window,
22142 cx,
22143 )
22144 })
22145 .unwrap()
22146 .await
22147 .downcast::<Editor>()
22148 .unwrap();
22149 pane.update(cx, |pane, cx| {
22150 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22151 open_editor.update(cx, |editor, cx| {
22152 assert_eq!(
22153 editor.display_text(cx),
22154 main_text,
22155 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
22156 );
22157 })
22158 });
22159}
22160
22161#[gpui::test]
22162async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
22163 struct EmptyModalView {
22164 focus_handle: gpui::FocusHandle,
22165 }
22166 impl EventEmitter<DismissEvent> for EmptyModalView {}
22167 impl Render for EmptyModalView {
22168 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
22169 div()
22170 }
22171 }
22172 impl Focusable for EmptyModalView {
22173 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
22174 self.focus_handle.clone()
22175 }
22176 }
22177 impl workspace::ModalView for EmptyModalView {}
22178 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
22179 EmptyModalView {
22180 focus_handle: cx.focus_handle(),
22181 }
22182 }
22183
22184 init_test(cx, |_| {});
22185
22186 let fs = FakeFs::new(cx.executor());
22187 let project = Project::test(fs, [], cx).await;
22188 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22189 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
22190 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22191 let editor = cx.new_window_entity(|window, cx| {
22192 Editor::new(
22193 EditorMode::full(),
22194 buffer,
22195 Some(project.clone()),
22196 window,
22197 cx,
22198 )
22199 });
22200 workspace
22201 .update(cx, |workspace, window, cx| {
22202 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
22203 })
22204 .unwrap();
22205 editor.update_in(cx, |editor, window, cx| {
22206 editor.open_context_menu(&OpenContextMenu, window, cx);
22207 assert!(editor.mouse_context_menu.is_some());
22208 });
22209 workspace
22210 .update(cx, |workspace, window, cx| {
22211 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
22212 })
22213 .unwrap();
22214 cx.read(|cx| {
22215 assert!(editor.read(cx).mouse_context_menu.is_none());
22216 });
22217}
22218
22219#[gpui::test]
22220async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
22221 init_test(cx, |_| {});
22222
22223 let fs = FakeFs::new(cx.executor());
22224 fs.insert_file(path!("/file.html"), Default::default())
22225 .await;
22226
22227 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
22228
22229 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22230 let html_language = Arc::new(Language::new(
22231 LanguageConfig {
22232 name: "HTML".into(),
22233 matcher: LanguageMatcher {
22234 path_suffixes: vec!["html".to_string()],
22235 ..LanguageMatcher::default()
22236 },
22237 brackets: BracketPairConfig {
22238 pairs: vec![BracketPair {
22239 start: "<".into(),
22240 end: ">".into(),
22241 close: true,
22242 ..Default::default()
22243 }],
22244 ..Default::default()
22245 },
22246 ..Default::default()
22247 },
22248 Some(tree_sitter_html::LANGUAGE.into()),
22249 ));
22250 language_registry.add(html_language);
22251 let mut fake_servers = language_registry.register_fake_lsp(
22252 "HTML",
22253 FakeLspAdapter {
22254 capabilities: lsp::ServerCapabilities {
22255 completion_provider: Some(lsp::CompletionOptions {
22256 resolve_provider: Some(true),
22257 ..Default::default()
22258 }),
22259 ..Default::default()
22260 },
22261 ..Default::default()
22262 },
22263 );
22264
22265 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22266 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22267
22268 let worktree_id = workspace
22269 .update(cx, |workspace, _window, cx| {
22270 workspace.project().update(cx, |project, cx| {
22271 project.worktrees(cx).next().unwrap().read(cx).id()
22272 })
22273 })
22274 .unwrap();
22275 project
22276 .update(cx, |project, cx| {
22277 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
22278 })
22279 .await
22280 .unwrap();
22281 let editor = workspace
22282 .update(cx, |workspace, window, cx| {
22283 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
22284 })
22285 .unwrap()
22286 .await
22287 .unwrap()
22288 .downcast::<Editor>()
22289 .unwrap();
22290
22291 let fake_server = fake_servers.next().await.unwrap();
22292 editor.update_in(cx, |editor, window, cx| {
22293 editor.set_text("<ad></ad>", window, cx);
22294 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22295 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
22296 });
22297 let Some((buffer, _)) = editor
22298 .buffer
22299 .read(cx)
22300 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
22301 else {
22302 panic!("Failed to get buffer for selection position");
22303 };
22304 let buffer = buffer.read(cx);
22305 let buffer_id = buffer.remote_id();
22306 let opening_range =
22307 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
22308 let closing_range =
22309 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
22310 let mut linked_ranges = HashMap::default();
22311 linked_ranges.insert(
22312 buffer_id,
22313 vec![(opening_range.clone(), vec![closing_range.clone()])],
22314 );
22315 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
22316 });
22317 let mut completion_handle =
22318 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
22319 Ok(Some(lsp::CompletionResponse::Array(vec![
22320 lsp::CompletionItem {
22321 label: "head".to_string(),
22322 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22323 lsp::InsertReplaceEdit {
22324 new_text: "head".to_string(),
22325 insert: lsp::Range::new(
22326 lsp::Position::new(0, 1),
22327 lsp::Position::new(0, 3),
22328 ),
22329 replace: lsp::Range::new(
22330 lsp::Position::new(0, 1),
22331 lsp::Position::new(0, 3),
22332 ),
22333 },
22334 )),
22335 ..Default::default()
22336 },
22337 ])))
22338 });
22339 editor.update_in(cx, |editor, window, cx| {
22340 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
22341 });
22342 cx.run_until_parked();
22343 completion_handle.next().await.unwrap();
22344 editor.update(cx, |editor, _| {
22345 assert!(
22346 editor.context_menu_visible(),
22347 "Completion menu should be visible"
22348 );
22349 });
22350 editor.update_in(cx, |editor, window, cx| {
22351 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
22352 });
22353 cx.executor().run_until_parked();
22354 editor.update(cx, |editor, cx| {
22355 assert_eq!(editor.text(cx), "<head></head>");
22356 });
22357}
22358
22359#[gpui::test]
22360async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
22361 init_test(cx, |_| {});
22362
22363 let fs = FakeFs::new(cx.executor());
22364 fs.insert_tree(
22365 path!("/root"),
22366 json!({
22367 "a": {
22368 "main.rs": "fn main() {}",
22369 },
22370 "foo": {
22371 "bar": {
22372 "external_file.rs": "pub mod external {}",
22373 }
22374 }
22375 }),
22376 )
22377 .await;
22378
22379 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
22380 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22381 language_registry.add(rust_lang());
22382 let _fake_servers = language_registry.register_fake_lsp(
22383 "Rust",
22384 FakeLspAdapter {
22385 ..FakeLspAdapter::default()
22386 },
22387 );
22388 let (workspace, cx) =
22389 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22390 let worktree_id = workspace.update(cx, |workspace, cx| {
22391 workspace.project().update(cx, |project, cx| {
22392 project.worktrees(cx).next().unwrap().read(cx).id()
22393 })
22394 });
22395
22396 let assert_language_servers_count =
22397 |expected: usize, context: &str, cx: &mut VisualTestContext| {
22398 project.update(cx, |project, cx| {
22399 let current = project
22400 .lsp_store()
22401 .read(cx)
22402 .as_local()
22403 .unwrap()
22404 .language_servers
22405 .len();
22406 assert_eq!(expected, current, "{context}");
22407 });
22408 };
22409
22410 assert_language_servers_count(
22411 0,
22412 "No servers should be running before any file is open",
22413 cx,
22414 );
22415 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22416 let main_editor = workspace
22417 .update_in(cx, |workspace, window, cx| {
22418 workspace.open_path(
22419 (worktree_id, "main.rs"),
22420 Some(pane.downgrade()),
22421 true,
22422 window,
22423 cx,
22424 )
22425 })
22426 .unwrap()
22427 .await
22428 .downcast::<Editor>()
22429 .unwrap();
22430 pane.update(cx, |pane, cx| {
22431 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22432 open_editor.update(cx, |editor, cx| {
22433 assert_eq!(
22434 editor.display_text(cx),
22435 "fn main() {}",
22436 "Original main.rs text on initial open",
22437 );
22438 });
22439 assert_eq!(open_editor, main_editor);
22440 });
22441 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
22442
22443 let external_editor = workspace
22444 .update_in(cx, |workspace, window, cx| {
22445 workspace.open_abs_path(
22446 PathBuf::from("/root/foo/bar/external_file.rs"),
22447 OpenOptions::default(),
22448 window,
22449 cx,
22450 )
22451 })
22452 .await
22453 .expect("opening external file")
22454 .downcast::<Editor>()
22455 .expect("downcasted external file's open element to editor");
22456 pane.update(cx, |pane, cx| {
22457 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22458 open_editor.update(cx, |editor, cx| {
22459 assert_eq!(
22460 editor.display_text(cx),
22461 "pub mod external {}",
22462 "External file is open now",
22463 );
22464 });
22465 assert_eq!(open_editor, external_editor);
22466 });
22467 assert_language_servers_count(
22468 1,
22469 "Second, external, *.rs file should join the existing server",
22470 cx,
22471 );
22472
22473 pane.update_in(cx, |pane, window, cx| {
22474 pane.close_active_item(&CloseActiveItem::default(), window, cx)
22475 })
22476 .await
22477 .unwrap();
22478 pane.update_in(cx, |pane, window, cx| {
22479 pane.navigate_backward(window, cx);
22480 });
22481 cx.run_until_parked();
22482 pane.update(cx, |pane, cx| {
22483 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22484 open_editor.update(cx, |editor, cx| {
22485 assert_eq!(
22486 editor.display_text(cx),
22487 "pub mod external {}",
22488 "External file is open now",
22489 );
22490 });
22491 });
22492 assert_language_servers_count(
22493 1,
22494 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
22495 cx,
22496 );
22497
22498 cx.update(|_, cx| {
22499 workspace::reload(cx);
22500 });
22501 assert_language_servers_count(
22502 1,
22503 "After reloading the worktree with local and external files opened, only one project should be started",
22504 cx,
22505 );
22506}
22507
22508#[gpui::test]
22509async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
22510 init_test(cx, |_| {});
22511
22512 let mut cx = EditorTestContext::new(cx).await;
22513 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22514 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22515
22516 // test cursor move to start of each line on tab
22517 // for `if`, `elif`, `else`, `while`, `with` and `for`
22518 cx.set_state(indoc! {"
22519 def main():
22520 ˇ for item in items:
22521 ˇ while item.active:
22522 ˇ if item.value > 10:
22523 ˇ continue
22524 ˇ elif item.value < 0:
22525 ˇ break
22526 ˇ else:
22527 ˇ with item.context() as ctx:
22528 ˇ yield count
22529 ˇ else:
22530 ˇ log('while else')
22531 ˇ else:
22532 ˇ log('for else')
22533 "});
22534 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22535 cx.assert_editor_state(indoc! {"
22536 def main():
22537 ˇfor item in items:
22538 ˇwhile item.active:
22539 ˇif item.value > 10:
22540 ˇcontinue
22541 ˇelif item.value < 0:
22542 ˇbreak
22543 ˇelse:
22544 ˇwith item.context() as ctx:
22545 ˇyield count
22546 ˇelse:
22547 ˇlog('while else')
22548 ˇelse:
22549 ˇlog('for else')
22550 "});
22551 // test relative indent is preserved when tab
22552 // for `if`, `elif`, `else`, `while`, `with` and `for`
22553 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22554 cx.assert_editor_state(indoc! {"
22555 def main():
22556 ˇfor item in items:
22557 ˇwhile item.active:
22558 ˇif item.value > 10:
22559 ˇcontinue
22560 ˇelif item.value < 0:
22561 ˇbreak
22562 ˇelse:
22563 ˇwith item.context() as ctx:
22564 ˇyield count
22565 ˇelse:
22566 ˇlog('while else')
22567 ˇelse:
22568 ˇlog('for else')
22569 "});
22570
22571 // test cursor move to start of each line on tab
22572 // for `try`, `except`, `else`, `finally`, `match` and `def`
22573 cx.set_state(indoc! {"
22574 def main():
22575 ˇ try:
22576 ˇ fetch()
22577 ˇ except ValueError:
22578 ˇ handle_error()
22579 ˇ else:
22580 ˇ match value:
22581 ˇ case _:
22582 ˇ finally:
22583 ˇ def status():
22584 ˇ return 0
22585 "});
22586 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22587 cx.assert_editor_state(indoc! {"
22588 def main():
22589 ˇtry:
22590 ˇfetch()
22591 ˇexcept ValueError:
22592 ˇhandle_error()
22593 ˇelse:
22594 ˇmatch value:
22595 ˇcase _:
22596 ˇfinally:
22597 ˇdef status():
22598 ˇreturn 0
22599 "});
22600 // test relative indent is preserved when tab
22601 // for `try`, `except`, `else`, `finally`, `match` and `def`
22602 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22603 cx.assert_editor_state(indoc! {"
22604 def main():
22605 ˇtry:
22606 ˇfetch()
22607 ˇexcept ValueError:
22608 ˇhandle_error()
22609 ˇelse:
22610 ˇmatch value:
22611 ˇcase _:
22612 ˇfinally:
22613 ˇdef status():
22614 ˇreturn 0
22615 "});
22616}
22617
22618#[gpui::test]
22619async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
22620 init_test(cx, |_| {});
22621
22622 let mut cx = EditorTestContext::new(cx).await;
22623 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22624 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22625
22626 // test `else` auto outdents when typed inside `if` block
22627 cx.set_state(indoc! {"
22628 def main():
22629 if i == 2:
22630 return
22631 ˇ
22632 "});
22633 cx.update_editor(|editor, window, cx| {
22634 editor.handle_input("else:", window, cx);
22635 });
22636 cx.assert_editor_state(indoc! {"
22637 def main():
22638 if i == 2:
22639 return
22640 else:ˇ
22641 "});
22642
22643 // test `except` auto outdents when typed inside `try` block
22644 cx.set_state(indoc! {"
22645 def main():
22646 try:
22647 i = 2
22648 ˇ
22649 "});
22650 cx.update_editor(|editor, window, cx| {
22651 editor.handle_input("except:", window, cx);
22652 });
22653 cx.assert_editor_state(indoc! {"
22654 def main():
22655 try:
22656 i = 2
22657 except:ˇ
22658 "});
22659
22660 // test `else` auto outdents when typed inside `except` block
22661 cx.set_state(indoc! {"
22662 def main():
22663 try:
22664 i = 2
22665 except:
22666 j = 2
22667 ˇ
22668 "});
22669 cx.update_editor(|editor, window, cx| {
22670 editor.handle_input("else:", window, cx);
22671 });
22672 cx.assert_editor_state(indoc! {"
22673 def main():
22674 try:
22675 i = 2
22676 except:
22677 j = 2
22678 else:ˇ
22679 "});
22680
22681 // test `finally` auto outdents when typed inside `else` block
22682 cx.set_state(indoc! {"
22683 def main():
22684 try:
22685 i = 2
22686 except:
22687 j = 2
22688 else:
22689 k = 2
22690 ˇ
22691 "});
22692 cx.update_editor(|editor, window, cx| {
22693 editor.handle_input("finally:", window, cx);
22694 });
22695 cx.assert_editor_state(indoc! {"
22696 def main():
22697 try:
22698 i = 2
22699 except:
22700 j = 2
22701 else:
22702 k = 2
22703 finally:ˇ
22704 "});
22705
22706 // test `else` does not outdents when typed inside `except` block right after for block
22707 cx.set_state(indoc! {"
22708 def main():
22709 try:
22710 i = 2
22711 except:
22712 for i in range(n):
22713 pass
22714 ˇ
22715 "});
22716 cx.update_editor(|editor, window, cx| {
22717 editor.handle_input("else:", window, cx);
22718 });
22719 cx.assert_editor_state(indoc! {"
22720 def main():
22721 try:
22722 i = 2
22723 except:
22724 for i in range(n):
22725 pass
22726 else:ˇ
22727 "});
22728
22729 // test `finally` auto outdents when typed inside `else` block right after for block
22730 cx.set_state(indoc! {"
22731 def main():
22732 try:
22733 i = 2
22734 except:
22735 j = 2
22736 else:
22737 for i in range(n):
22738 pass
22739 ˇ
22740 "});
22741 cx.update_editor(|editor, window, cx| {
22742 editor.handle_input("finally:", window, cx);
22743 });
22744 cx.assert_editor_state(indoc! {"
22745 def main():
22746 try:
22747 i = 2
22748 except:
22749 j = 2
22750 else:
22751 for i in range(n):
22752 pass
22753 finally:ˇ
22754 "});
22755
22756 // test `except` outdents to inner "try" block
22757 cx.set_state(indoc! {"
22758 def main():
22759 try:
22760 i = 2
22761 if i == 2:
22762 try:
22763 i = 3
22764 ˇ
22765 "});
22766 cx.update_editor(|editor, window, cx| {
22767 editor.handle_input("except:", window, cx);
22768 });
22769 cx.assert_editor_state(indoc! {"
22770 def main():
22771 try:
22772 i = 2
22773 if i == 2:
22774 try:
22775 i = 3
22776 except:ˇ
22777 "});
22778
22779 // test `except` outdents to outer "try" block
22780 cx.set_state(indoc! {"
22781 def main():
22782 try:
22783 i = 2
22784 if i == 2:
22785 try:
22786 i = 3
22787 ˇ
22788 "});
22789 cx.update_editor(|editor, window, cx| {
22790 editor.handle_input("except:", window, cx);
22791 });
22792 cx.assert_editor_state(indoc! {"
22793 def main():
22794 try:
22795 i = 2
22796 if i == 2:
22797 try:
22798 i = 3
22799 except:ˇ
22800 "});
22801
22802 // test `else` stays at correct indent when typed after `for` block
22803 cx.set_state(indoc! {"
22804 def main():
22805 for i in range(10):
22806 if i == 3:
22807 break
22808 ˇ
22809 "});
22810 cx.update_editor(|editor, window, cx| {
22811 editor.handle_input("else:", window, cx);
22812 });
22813 cx.assert_editor_state(indoc! {"
22814 def main():
22815 for i in range(10):
22816 if i == 3:
22817 break
22818 else:ˇ
22819 "});
22820
22821 // test does not outdent on typing after line with square brackets
22822 cx.set_state(indoc! {"
22823 def f() -> list[str]:
22824 ˇ
22825 "});
22826 cx.update_editor(|editor, window, cx| {
22827 editor.handle_input("a", window, cx);
22828 });
22829 cx.assert_editor_state(indoc! {"
22830 def f() -> list[str]:
22831 aˇ
22832 "});
22833
22834 // test does not outdent on typing : after case keyword
22835 cx.set_state(indoc! {"
22836 match 1:
22837 caseˇ
22838 "});
22839 cx.update_editor(|editor, window, cx| {
22840 editor.handle_input(":", window, cx);
22841 });
22842 cx.assert_editor_state(indoc! {"
22843 match 1:
22844 case:ˇ
22845 "});
22846}
22847
22848#[gpui::test]
22849async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
22850 init_test(cx, |_| {});
22851 update_test_language_settings(cx, |settings| {
22852 settings.defaults.extend_comment_on_newline = Some(false);
22853 });
22854 let mut cx = EditorTestContext::new(cx).await;
22855 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22856 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22857
22858 // test correct indent after newline on comment
22859 cx.set_state(indoc! {"
22860 # COMMENT:ˇ
22861 "});
22862 cx.update_editor(|editor, window, cx| {
22863 editor.newline(&Newline, window, cx);
22864 });
22865 cx.assert_editor_state(indoc! {"
22866 # COMMENT:
22867 ˇ
22868 "});
22869
22870 // test correct indent after newline in brackets
22871 cx.set_state(indoc! {"
22872 {ˇ}
22873 "});
22874 cx.update_editor(|editor, window, cx| {
22875 editor.newline(&Newline, window, cx);
22876 });
22877 cx.run_until_parked();
22878 cx.assert_editor_state(indoc! {"
22879 {
22880 ˇ
22881 }
22882 "});
22883
22884 cx.set_state(indoc! {"
22885 (ˇ)
22886 "});
22887 cx.update_editor(|editor, window, cx| {
22888 editor.newline(&Newline, window, cx);
22889 });
22890 cx.run_until_parked();
22891 cx.assert_editor_state(indoc! {"
22892 (
22893 ˇ
22894 )
22895 "});
22896
22897 // do not indent after empty lists or dictionaries
22898 cx.set_state(indoc! {"
22899 a = []ˇ
22900 "});
22901 cx.update_editor(|editor, window, cx| {
22902 editor.newline(&Newline, window, cx);
22903 });
22904 cx.run_until_parked();
22905 cx.assert_editor_state(indoc! {"
22906 a = []
22907 ˇ
22908 "});
22909}
22910
22911#[gpui::test]
22912async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
22913 init_test(cx, |_| {});
22914
22915 let mut cx = EditorTestContext::new(cx).await;
22916 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
22917 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22918
22919 // test cursor move to start of each line on tab
22920 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
22921 cx.set_state(indoc! {"
22922 function main() {
22923 ˇ for item in $items; do
22924 ˇ while [ -n \"$item\" ]; do
22925 ˇ if [ \"$value\" -gt 10 ]; then
22926 ˇ continue
22927 ˇ elif [ \"$value\" -lt 0 ]; then
22928 ˇ break
22929 ˇ else
22930 ˇ echo \"$item\"
22931 ˇ fi
22932 ˇ done
22933 ˇ done
22934 ˇ}
22935 "});
22936 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22937 cx.assert_editor_state(indoc! {"
22938 function main() {
22939 ˇfor item in $items; do
22940 ˇwhile [ -n \"$item\" ]; do
22941 ˇif [ \"$value\" -gt 10 ]; then
22942 ˇcontinue
22943 ˇelif [ \"$value\" -lt 0 ]; then
22944 ˇbreak
22945 ˇelse
22946 ˇecho \"$item\"
22947 ˇfi
22948 ˇdone
22949 ˇdone
22950 ˇ}
22951 "});
22952 // test relative indent is preserved when tab
22953 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22954 cx.assert_editor_state(indoc! {"
22955 function main() {
22956 ˇfor item in $items; do
22957 ˇwhile [ -n \"$item\" ]; do
22958 ˇif [ \"$value\" -gt 10 ]; then
22959 ˇcontinue
22960 ˇelif [ \"$value\" -lt 0 ]; then
22961 ˇbreak
22962 ˇelse
22963 ˇecho \"$item\"
22964 ˇfi
22965 ˇdone
22966 ˇdone
22967 ˇ}
22968 "});
22969
22970 // test cursor move to start of each line on tab
22971 // for `case` statement with patterns
22972 cx.set_state(indoc! {"
22973 function handle() {
22974 ˇ case \"$1\" in
22975 ˇ start)
22976 ˇ echo \"a\"
22977 ˇ ;;
22978 ˇ stop)
22979 ˇ echo \"b\"
22980 ˇ ;;
22981 ˇ *)
22982 ˇ echo \"c\"
22983 ˇ ;;
22984 ˇ esac
22985 ˇ}
22986 "});
22987 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22988 cx.assert_editor_state(indoc! {"
22989 function handle() {
22990 ˇcase \"$1\" in
22991 ˇstart)
22992 ˇecho \"a\"
22993 ˇ;;
22994 ˇstop)
22995 ˇecho \"b\"
22996 ˇ;;
22997 ˇ*)
22998 ˇecho \"c\"
22999 ˇ;;
23000 ˇesac
23001 ˇ}
23002 "});
23003}
23004
23005#[gpui::test]
23006async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
23007 init_test(cx, |_| {});
23008
23009 let mut cx = EditorTestContext::new(cx).await;
23010 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
23011 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23012
23013 // test indents on comment insert
23014 cx.set_state(indoc! {"
23015 function main() {
23016 ˇ for item in $items; do
23017 ˇ while [ -n \"$item\" ]; do
23018 ˇ if [ \"$value\" -gt 10 ]; then
23019 ˇ continue
23020 ˇ elif [ \"$value\" -lt 0 ]; then
23021 ˇ break
23022 ˇ else
23023 ˇ echo \"$item\"
23024 ˇ fi
23025 ˇ done
23026 ˇ done
23027 ˇ}
23028 "});
23029 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
23030 cx.assert_editor_state(indoc! {"
23031 function main() {
23032 #ˇ for item in $items; do
23033 #ˇ while [ -n \"$item\" ]; do
23034 #ˇ if [ \"$value\" -gt 10 ]; then
23035 #ˇ continue
23036 #ˇ elif [ \"$value\" -lt 0 ]; then
23037 #ˇ break
23038 #ˇ else
23039 #ˇ echo \"$item\"
23040 #ˇ fi
23041 #ˇ done
23042 #ˇ done
23043 #ˇ}
23044 "});
23045}
23046
23047#[gpui::test]
23048async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
23049 init_test(cx, |_| {});
23050
23051 let mut cx = EditorTestContext::new(cx).await;
23052 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
23053 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23054
23055 // test `else` auto outdents when typed inside `if` block
23056 cx.set_state(indoc! {"
23057 if [ \"$1\" = \"test\" ]; then
23058 echo \"foo bar\"
23059 ˇ
23060 "});
23061 cx.update_editor(|editor, window, cx| {
23062 editor.handle_input("else", window, cx);
23063 });
23064 cx.assert_editor_state(indoc! {"
23065 if [ \"$1\" = \"test\" ]; then
23066 echo \"foo bar\"
23067 elseˇ
23068 "});
23069
23070 // test `elif` auto outdents when typed inside `if` block
23071 cx.set_state(indoc! {"
23072 if [ \"$1\" = \"test\" ]; then
23073 echo \"foo bar\"
23074 ˇ
23075 "});
23076 cx.update_editor(|editor, window, cx| {
23077 editor.handle_input("elif", window, cx);
23078 });
23079 cx.assert_editor_state(indoc! {"
23080 if [ \"$1\" = \"test\" ]; then
23081 echo \"foo bar\"
23082 elifˇ
23083 "});
23084
23085 // test `fi` auto outdents when typed inside `else` block
23086 cx.set_state(indoc! {"
23087 if [ \"$1\" = \"test\" ]; then
23088 echo \"foo bar\"
23089 else
23090 echo \"bar baz\"
23091 ˇ
23092 "});
23093 cx.update_editor(|editor, window, cx| {
23094 editor.handle_input("fi", window, cx);
23095 });
23096 cx.assert_editor_state(indoc! {"
23097 if [ \"$1\" = \"test\" ]; then
23098 echo \"foo bar\"
23099 else
23100 echo \"bar baz\"
23101 fiˇ
23102 "});
23103
23104 // test `done` auto outdents when typed inside `while` block
23105 cx.set_state(indoc! {"
23106 while read line; do
23107 echo \"$line\"
23108 ˇ
23109 "});
23110 cx.update_editor(|editor, window, cx| {
23111 editor.handle_input("done", window, cx);
23112 });
23113 cx.assert_editor_state(indoc! {"
23114 while read line; do
23115 echo \"$line\"
23116 doneˇ
23117 "});
23118
23119 // test `done` auto outdents when typed inside `for` block
23120 cx.set_state(indoc! {"
23121 for file in *.txt; do
23122 cat \"$file\"
23123 ˇ
23124 "});
23125 cx.update_editor(|editor, window, cx| {
23126 editor.handle_input("done", window, cx);
23127 });
23128 cx.assert_editor_state(indoc! {"
23129 for file in *.txt; do
23130 cat \"$file\"
23131 doneˇ
23132 "});
23133
23134 // test `esac` auto outdents when typed inside `case` block
23135 cx.set_state(indoc! {"
23136 case \"$1\" in
23137 start)
23138 echo \"foo bar\"
23139 ;;
23140 stop)
23141 echo \"bar baz\"
23142 ;;
23143 ˇ
23144 "});
23145 cx.update_editor(|editor, window, cx| {
23146 editor.handle_input("esac", window, cx);
23147 });
23148 cx.assert_editor_state(indoc! {"
23149 case \"$1\" in
23150 start)
23151 echo \"foo bar\"
23152 ;;
23153 stop)
23154 echo \"bar baz\"
23155 ;;
23156 esacˇ
23157 "});
23158
23159 // test `*)` auto outdents when typed inside `case` block
23160 cx.set_state(indoc! {"
23161 case \"$1\" in
23162 start)
23163 echo \"foo bar\"
23164 ;;
23165 ˇ
23166 "});
23167 cx.update_editor(|editor, window, cx| {
23168 editor.handle_input("*)", window, cx);
23169 });
23170 cx.assert_editor_state(indoc! {"
23171 case \"$1\" in
23172 start)
23173 echo \"foo bar\"
23174 ;;
23175 *)ˇ
23176 "});
23177
23178 // test `fi` outdents to correct level with nested if blocks
23179 cx.set_state(indoc! {"
23180 if [ \"$1\" = \"test\" ]; then
23181 echo \"outer if\"
23182 if [ \"$2\" = \"debug\" ]; then
23183 echo \"inner if\"
23184 ˇ
23185 "});
23186 cx.update_editor(|editor, window, cx| {
23187 editor.handle_input("fi", window, cx);
23188 });
23189 cx.assert_editor_state(indoc! {"
23190 if [ \"$1\" = \"test\" ]; then
23191 echo \"outer if\"
23192 if [ \"$2\" = \"debug\" ]; then
23193 echo \"inner if\"
23194 fiˇ
23195 "});
23196}
23197
23198#[gpui::test]
23199async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
23200 init_test(cx, |_| {});
23201 update_test_language_settings(cx, |settings| {
23202 settings.defaults.extend_comment_on_newline = Some(false);
23203 });
23204 let mut cx = EditorTestContext::new(cx).await;
23205 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
23206 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23207
23208 // test correct indent after newline on comment
23209 cx.set_state(indoc! {"
23210 # COMMENT:ˇ
23211 "});
23212 cx.update_editor(|editor, window, cx| {
23213 editor.newline(&Newline, window, cx);
23214 });
23215 cx.assert_editor_state(indoc! {"
23216 # COMMENT:
23217 ˇ
23218 "});
23219
23220 // test correct indent after newline after `then`
23221 cx.set_state(indoc! {"
23222
23223 if [ \"$1\" = \"test\" ]; thenˇ
23224 "});
23225 cx.update_editor(|editor, window, cx| {
23226 editor.newline(&Newline, window, cx);
23227 });
23228 cx.run_until_parked();
23229 cx.assert_editor_state(indoc! {"
23230
23231 if [ \"$1\" = \"test\" ]; then
23232 ˇ
23233 "});
23234
23235 // test correct indent after newline after `else`
23236 cx.set_state(indoc! {"
23237 if [ \"$1\" = \"test\" ]; then
23238 elseˇ
23239 "});
23240 cx.update_editor(|editor, window, cx| {
23241 editor.newline(&Newline, window, cx);
23242 });
23243 cx.run_until_parked();
23244 cx.assert_editor_state(indoc! {"
23245 if [ \"$1\" = \"test\" ]; then
23246 else
23247 ˇ
23248 "});
23249
23250 // test correct indent after newline after `elif`
23251 cx.set_state(indoc! {"
23252 if [ \"$1\" = \"test\" ]; then
23253 elifˇ
23254 "});
23255 cx.update_editor(|editor, window, cx| {
23256 editor.newline(&Newline, window, cx);
23257 });
23258 cx.run_until_parked();
23259 cx.assert_editor_state(indoc! {"
23260 if [ \"$1\" = \"test\" ]; then
23261 elif
23262 ˇ
23263 "});
23264
23265 // test correct indent after newline after `do`
23266 cx.set_state(indoc! {"
23267 for file in *.txt; doˇ
23268 "});
23269 cx.update_editor(|editor, window, cx| {
23270 editor.newline(&Newline, window, cx);
23271 });
23272 cx.run_until_parked();
23273 cx.assert_editor_state(indoc! {"
23274 for file in *.txt; do
23275 ˇ
23276 "});
23277
23278 // test correct indent after newline after case pattern
23279 cx.set_state(indoc! {"
23280 case \"$1\" in
23281 start)ˇ
23282 "});
23283 cx.update_editor(|editor, window, cx| {
23284 editor.newline(&Newline, window, cx);
23285 });
23286 cx.run_until_parked();
23287 cx.assert_editor_state(indoc! {"
23288 case \"$1\" in
23289 start)
23290 ˇ
23291 "});
23292
23293 // test correct indent after newline after case pattern
23294 cx.set_state(indoc! {"
23295 case \"$1\" in
23296 start)
23297 ;;
23298 *)ˇ
23299 "});
23300 cx.update_editor(|editor, window, cx| {
23301 editor.newline(&Newline, window, cx);
23302 });
23303 cx.run_until_parked();
23304 cx.assert_editor_state(indoc! {"
23305 case \"$1\" in
23306 start)
23307 ;;
23308 *)
23309 ˇ
23310 "});
23311
23312 // test correct indent after newline after function opening brace
23313 cx.set_state(indoc! {"
23314 function test() {ˇ}
23315 "});
23316 cx.update_editor(|editor, window, cx| {
23317 editor.newline(&Newline, window, cx);
23318 });
23319 cx.run_until_parked();
23320 cx.assert_editor_state(indoc! {"
23321 function test() {
23322 ˇ
23323 }
23324 "});
23325
23326 // test no extra indent after semicolon on same line
23327 cx.set_state(indoc! {"
23328 echo \"test\";ˇ
23329 "});
23330 cx.update_editor(|editor, window, cx| {
23331 editor.newline(&Newline, window, cx);
23332 });
23333 cx.run_until_parked();
23334 cx.assert_editor_state(indoc! {"
23335 echo \"test\";
23336 ˇ
23337 "});
23338}
23339
23340fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
23341 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
23342 point..point
23343}
23344
23345#[track_caller]
23346fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
23347 let (text, ranges) = marked_text_ranges(marked_text, true);
23348 assert_eq!(editor.text(cx), text);
23349 assert_eq!(
23350 editor.selections.ranges(cx),
23351 ranges,
23352 "Assert selections are {}",
23353 marked_text
23354 );
23355}
23356
23357pub fn handle_signature_help_request(
23358 cx: &mut EditorLspTestContext,
23359 mocked_response: lsp::SignatureHelp,
23360) -> impl Future<Output = ()> + use<> {
23361 let mut request =
23362 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
23363 let mocked_response = mocked_response.clone();
23364 async move { Ok(Some(mocked_response)) }
23365 });
23366
23367 async move {
23368 request.next().await;
23369 }
23370}
23371
23372#[track_caller]
23373pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
23374 cx.update_editor(|editor, _, _| {
23375 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
23376 let entries = menu.entries.borrow();
23377 let entries = entries
23378 .iter()
23379 .map(|entry| entry.string.as_str())
23380 .collect::<Vec<_>>();
23381 assert_eq!(entries, expected);
23382 } else {
23383 panic!("Expected completions menu");
23384 }
23385 });
23386}
23387
23388/// Handle completion request passing a marked string specifying where the completion
23389/// should be triggered from using '|' character, what range should be replaced, and what completions
23390/// should be returned using '<' and '>' to delimit the range.
23391///
23392/// Also see `handle_completion_request_with_insert_and_replace`.
23393#[track_caller]
23394pub fn handle_completion_request(
23395 marked_string: &str,
23396 completions: Vec<&'static str>,
23397 is_incomplete: bool,
23398 counter: Arc<AtomicUsize>,
23399 cx: &mut EditorLspTestContext,
23400) -> impl Future<Output = ()> {
23401 let complete_from_marker: TextRangeMarker = '|'.into();
23402 let replace_range_marker: TextRangeMarker = ('<', '>').into();
23403 let (_, mut marked_ranges) = marked_text_ranges_by(
23404 marked_string,
23405 vec![complete_from_marker.clone(), replace_range_marker.clone()],
23406 );
23407
23408 let complete_from_position =
23409 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
23410 let replace_range =
23411 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
23412
23413 let mut request =
23414 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
23415 let completions = completions.clone();
23416 counter.fetch_add(1, atomic::Ordering::Release);
23417 async move {
23418 assert_eq!(params.text_document_position.text_document.uri, url.clone());
23419 assert_eq!(
23420 params.text_document_position.position,
23421 complete_from_position
23422 );
23423 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
23424 is_incomplete: is_incomplete,
23425 item_defaults: None,
23426 items: completions
23427 .iter()
23428 .map(|completion_text| lsp::CompletionItem {
23429 label: completion_text.to_string(),
23430 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
23431 range: replace_range,
23432 new_text: completion_text.to_string(),
23433 })),
23434 ..Default::default()
23435 })
23436 .collect(),
23437 })))
23438 }
23439 });
23440
23441 async move {
23442 request.next().await;
23443 }
23444}
23445
23446/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
23447/// given instead, which also contains an `insert` range.
23448///
23449/// This function uses markers to define ranges:
23450/// - `|` marks the cursor position
23451/// - `<>` marks the replace range
23452/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
23453pub fn handle_completion_request_with_insert_and_replace(
23454 cx: &mut EditorLspTestContext,
23455 marked_string: &str,
23456 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
23457 counter: Arc<AtomicUsize>,
23458) -> impl Future<Output = ()> {
23459 let complete_from_marker: TextRangeMarker = '|'.into();
23460 let replace_range_marker: TextRangeMarker = ('<', '>').into();
23461 let insert_range_marker: TextRangeMarker = ('{', '}').into();
23462
23463 let (_, mut marked_ranges) = marked_text_ranges_by(
23464 marked_string,
23465 vec![
23466 complete_from_marker.clone(),
23467 replace_range_marker.clone(),
23468 insert_range_marker.clone(),
23469 ],
23470 );
23471
23472 let complete_from_position =
23473 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
23474 let replace_range =
23475 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
23476
23477 let insert_range = match marked_ranges.remove(&insert_range_marker) {
23478 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
23479 _ => lsp::Range {
23480 start: replace_range.start,
23481 end: complete_from_position,
23482 },
23483 };
23484
23485 let mut request =
23486 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
23487 let completions = completions.clone();
23488 counter.fetch_add(1, atomic::Ordering::Release);
23489 async move {
23490 assert_eq!(params.text_document_position.text_document.uri, url.clone());
23491 assert_eq!(
23492 params.text_document_position.position, complete_from_position,
23493 "marker `|` position doesn't match",
23494 );
23495 Ok(Some(lsp::CompletionResponse::Array(
23496 completions
23497 .iter()
23498 .map(|(label, new_text)| lsp::CompletionItem {
23499 label: label.to_string(),
23500 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
23501 lsp::InsertReplaceEdit {
23502 insert: insert_range,
23503 replace: replace_range,
23504 new_text: new_text.to_string(),
23505 },
23506 )),
23507 ..Default::default()
23508 })
23509 .collect(),
23510 )))
23511 }
23512 });
23513
23514 async move {
23515 request.next().await;
23516 }
23517}
23518
23519fn handle_resolve_completion_request(
23520 cx: &mut EditorLspTestContext,
23521 edits: Option<Vec<(&'static str, &'static str)>>,
23522) -> impl Future<Output = ()> {
23523 let edits = edits.map(|edits| {
23524 edits
23525 .iter()
23526 .map(|(marked_string, new_text)| {
23527 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
23528 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
23529 lsp::TextEdit::new(replace_range, new_text.to_string())
23530 })
23531 .collect::<Vec<_>>()
23532 });
23533
23534 let mut request =
23535 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
23536 let edits = edits.clone();
23537 async move {
23538 Ok(lsp::CompletionItem {
23539 additional_text_edits: edits,
23540 ..Default::default()
23541 })
23542 }
23543 });
23544
23545 async move {
23546 request.next().await;
23547 }
23548}
23549
23550pub(crate) fn update_test_language_settings(
23551 cx: &mut TestAppContext,
23552 f: impl Fn(&mut AllLanguageSettingsContent),
23553) {
23554 cx.update(|cx| {
23555 SettingsStore::update_global(cx, |store, cx| {
23556 store.update_user_settings::<AllLanguageSettings>(cx, f);
23557 });
23558 });
23559}
23560
23561pub(crate) fn update_test_project_settings(
23562 cx: &mut TestAppContext,
23563 f: impl Fn(&mut ProjectSettings),
23564) {
23565 cx.update(|cx| {
23566 SettingsStore::update_global(cx, |store, cx| {
23567 store.update_user_settings::<ProjectSettings>(cx, f);
23568 });
23569 });
23570}
23571
23572pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
23573 cx.update(|cx| {
23574 assets::Assets.load_test_fonts(cx);
23575 let store = SettingsStore::test(cx);
23576 cx.set_global(store);
23577 theme::init(theme::LoadThemes::JustBase, cx);
23578 release_channel::init(SemanticVersion::default(), cx);
23579 client::init_settings(cx);
23580 language::init(cx);
23581 Project::init_settings(cx);
23582 workspace::init_settings(cx);
23583 crate::init(cx);
23584 });
23585 zlog::init_test();
23586 update_test_language_settings(cx, f);
23587}
23588
23589#[track_caller]
23590fn assert_hunk_revert(
23591 not_reverted_text_with_selections: &str,
23592 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
23593 expected_reverted_text_with_selections: &str,
23594 base_text: &str,
23595 cx: &mut EditorLspTestContext,
23596) {
23597 cx.set_state(not_reverted_text_with_selections);
23598 cx.set_head_text(base_text);
23599 cx.executor().run_until_parked();
23600
23601 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
23602 let snapshot = editor.snapshot(window, cx);
23603 let reverted_hunk_statuses = snapshot
23604 .buffer_snapshot
23605 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
23606 .map(|hunk| hunk.status().kind)
23607 .collect::<Vec<_>>();
23608
23609 editor.git_restore(&Default::default(), window, cx);
23610 reverted_hunk_statuses
23611 });
23612 cx.executor().run_until_parked();
23613 cx.assert_editor_state(expected_reverted_text_with_selections);
23614 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
23615}
23616
23617#[gpui::test(iterations = 10)]
23618async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
23619 init_test(cx, |_| {});
23620
23621 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
23622 let counter = diagnostic_requests.clone();
23623
23624 let fs = FakeFs::new(cx.executor());
23625 fs.insert_tree(
23626 path!("/a"),
23627 json!({
23628 "first.rs": "fn main() { let a = 5; }",
23629 "second.rs": "// Test file",
23630 }),
23631 )
23632 .await;
23633
23634 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23635 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23636 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23637
23638 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23639 language_registry.add(rust_lang());
23640 let mut fake_servers = language_registry.register_fake_lsp(
23641 "Rust",
23642 FakeLspAdapter {
23643 capabilities: lsp::ServerCapabilities {
23644 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
23645 lsp::DiagnosticOptions {
23646 identifier: None,
23647 inter_file_dependencies: true,
23648 workspace_diagnostics: true,
23649 work_done_progress_options: Default::default(),
23650 },
23651 )),
23652 ..Default::default()
23653 },
23654 ..Default::default()
23655 },
23656 );
23657
23658 let editor = workspace
23659 .update(cx, |workspace, window, cx| {
23660 workspace.open_abs_path(
23661 PathBuf::from(path!("/a/first.rs")),
23662 OpenOptions::default(),
23663 window,
23664 cx,
23665 )
23666 })
23667 .unwrap()
23668 .await
23669 .unwrap()
23670 .downcast::<Editor>()
23671 .unwrap();
23672 let fake_server = fake_servers.next().await.unwrap();
23673 let server_id = fake_server.server.server_id();
23674 let mut first_request = fake_server
23675 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
23676 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
23677 let result_id = Some(new_result_id.to_string());
23678 assert_eq!(
23679 params.text_document.uri,
23680 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
23681 );
23682 async move {
23683 Ok(lsp::DocumentDiagnosticReportResult::Report(
23684 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
23685 related_documents: None,
23686 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
23687 items: Vec::new(),
23688 result_id,
23689 },
23690 }),
23691 ))
23692 }
23693 });
23694
23695 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
23696 project.update(cx, |project, cx| {
23697 let buffer_id = editor
23698 .read(cx)
23699 .buffer()
23700 .read(cx)
23701 .as_singleton()
23702 .expect("created a singleton buffer")
23703 .read(cx)
23704 .remote_id();
23705 let buffer_result_id = project
23706 .lsp_store()
23707 .read(cx)
23708 .result_id(server_id, buffer_id, cx);
23709 assert_eq!(expected, buffer_result_id);
23710 });
23711 };
23712
23713 ensure_result_id(None, cx);
23714 cx.executor().advance_clock(Duration::from_millis(60));
23715 cx.executor().run_until_parked();
23716 assert_eq!(
23717 diagnostic_requests.load(atomic::Ordering::Acquire),
23718 1,
23719 "Opening file should trigger diagnostic request"
23720 );
23721 first_request
23722 .next()
23723 .await
23724 .expect("should have sent the first diagnostics pull request");
23725 ensure_result_id(Some("1".to_string()), cx);
23726
23727 // Editing should trigger diagnostics
23728 editor.update_in(cx, |editor, window, cx| {
23729 editor.handle_input("2", window, cx)
23730 });
23731 cx.executor().advance_clock(Duration::from_millis(60));
23732 cx.executor().run_until_parked();
23733 assert_eq!(
23734 diagnostic_requests.load(atomic::Ordering::Acquire),
23735 2,
23736 "Editing should trigger diagnostic request"
23737 );
23738 ensure_result_id(Some("2".to_string()), cx);
23739
23740 // Moving cursor should not trigger diagnostic request
23741 editor.update_in(cx, |editor, window, cx| {
23742 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23743 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
23744 });
23745 });
23746 cx.executor().advance_clock(Duration::from_millis(60));
23747 cx.executor().run_until_parked();
23748 assert_eq!(
23749 diagnostic_requests.load(atomic::Ordering::Acquire),
23750 2,
23751 "Cursor movement should not trigger diagnostic request"
23752 );
23753 ensure_result_id(Some("2".to_string()), cx);
23754 // Multiple rapid edits should be debounced
23755 for _ in 0..5 {
23756 editor.update_in(cx, |editor, window, cx| {
23757 editor.handle_input("x", window, cx)
23758 });
23759 }
23760 cx.executor().advance_clock(Duration::from_millis(60));
23761 cx.executor().run_until_parked();
23762
23763 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
23764 assert!(
23765 final_requests <= 4,
23766 "Multiple rapid edits should be debounced (got {final_requests} requests)",
23767 );
23768 ensure_result_id(Some(final_requests.to_string()), cx);
23769}
23770
23771#[gpui::test]
23772async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
23773 // Regression test for issue #11671
23774 // Previously, adding a cursor after moving multiple cursors would reset
23775 // the cursor count instead of adding to the existing cursors.
23776 init_test(cx, |_| {});
23777 let mut cx = EditorTestContext::new(cx).await;
23778
23779 // Create a simple buffer with cursor at start
23780 cx.set_state(indoc! {"
23781 ˇaaaa
23782 bbbb
23783 cccc
23784 dddd
23785 eeee
23786 ffff
23787 gggg
23788 hhhh"});
23789
23790 // Add 2 cursors below (so we have 3 total)
23791 cx.update_editor(|editor, window, cx| {
23792 editor.add_selection_below(&Default::default(), window, cx);
23793 editor.add_selection_below(&Default::default(), window, cx);
23794 });
23795
23796 // Verify we have 3 cursors
23797 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
23798 assert_eq!(
23799 initial_count, 3,
23800 "Should have 3 cursors after adding 2 below"
23801 );
23802
23803 // Move down one line
23804 cx.update_editor(|editor, window, cx| {
23805 editor.move_down(&MoveDown, window, cx);
23806 });
23807
23808 // Add another cursor below
23809 cx.update_editor(|editor, window, cx| {
23810 editor.add_selection_below(&Default::default(), window, cx);
23811 });
23812
23813 // Should now have 4 cursors (3 original + 1 new)
23814 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
23815 assert_eq!(
23816 final_count, 4,
23817 "Should have 4 cursors after moving and adding another"
23818 );
23819}
23820
23821#[gpui::test(iterations = 10)]
23822async fn test_document_colors(cx: &mut TestAppContext) {
23823 let expected_color = Rgba {
23824 r: 0.33,
23825 g: 0.33,
23826 b: 0.33,
23827 a: 0.33,
23828 };
23829
23830 init_test(cx, |_| {});
23831
23832 let fs = FakeFs::new(cx.executor());
23833 fs.insert_tree(
23834 path!("/a"),
23835 json!({
23836 "first.rs": "fn main() { let a = 5; }",
23837 }),
23838 )
23839 .await;
23840
23841 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23842 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23843 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23844
23845 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23846 language_registry.add(rust_lang());
23847 let mut fake_servers = language_registry.register_fake_lsp(
23848 "Rust",
23849 FakeLspAdapter {
23850 capabilities: lsp::ServerCapabilities {
23851 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
23852 ..lsp::ServerCapabilities::default()
23853 },
23854 name: "rust-analyzer",
23855 ..FakeLspAdapter::default()
23856 },
23857 );
23858 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
23859 "Rust",
23860 FakeLspAdapter {
23861 capabilities: lsp::ServerCapabilities {
23862 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
23863 ..lsp::ServerCapabilities::default()
23864 },
23865 name: "not-rust-analyzer",
23866 ..FakeLspAdapter::default()
23867 },
23868 );
23869
23870 let editor = workspace
23871 .update(cx, |workspace, window, cx| {
23872 workspace.open_abs_path(
23873 PathBuf::from(path!("/a/first.rs")),
23874 OpenOptions::default(),
23875 window,
23876 cx,
23877 )
23878 })
23879 .unwrap()
23880 .await
23881 .unwrap()
23882 .downcast::<Editor>()
23883 .unwrap();
23884 let fake_language_server = fake_servers.next().await.unwrap();
23885 let fake_language_server_without_capabilities =
23886 fake_servers_without_capabilities.next().await.unwrap();
23887 let requests_made = Arc::new(AtomicUsize::new(0));
23888 let closure_requests_made = Arc::clone(&requests_made);
23889 let mut color_request_handle = fake_language_server
23890 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
23891 let requests_made = Arc::clone(&closure_requests_made);
23892 async move {
23893 assert_eq!(
23894 params.text_document.uri,
23895 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
23896 );
23897 requests_made.fetch_add(1, atomic::Ordering::Release);
23898 Ok(vec![
23899 lsp::ColorInformation {
23900 range: lsp::Range {
23901 start: lsp::Position {
23902 line: 0,
23903 character: 0,
23904 },
23905 end: lsp::Position {
23906 line: 0,
23907 character: 1,
23908 },
23909 },
23910 color: lsp::Color {
23911 red: 0.33,
23912 green: 0.33,
23913 blue: 0.33,
23914 alpha: 0.33,
23915 },
23916 },
23917 lsp::ColorInformation {
23918 range: lsp::Range {
23919 start: lsp::Position {
23920 line: 0,
23921 character: 0,
23922 },
23923 end: lsp::Position {
23924 line: 0,
23925 character: 1,
23926 },
23927 },
23928 color: lsp::Color {
23929 red: 0.33,
23930 green: 0.33,
23931 blue: 0.33,
23932 alpha: 0.33,
23933 },
23934 },
23935 ])
23936 }
23937 });
23938
23939 let _handle = fake_language_server_without_capabilities
23940 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
23941 panic!("Should not be called");
23942 });
23943 cx.executor().advance_clock(Duration::from_millis(100));
23944 color_request_handle.next().await.unwrap();
23945 cx.run_until_parked();
23946 assert_eq!(
23947 1,
23948 requests_made.load(atomic::Ordering::Acquire),
23949 "Should query for colors once per editor open"
23950 );
23951 editor.update_in(cx, |editor, _, cx| {
23952 assert_eq!(
23953 vec![expected_color],
23954 extract_color_inlays(editor, cx),
23955 "Should have an initial inlay"
23956 );
23957 });
23958
23959 // opening another file in a split should not influence the LSP query counter
23960 workspace
23961 .update(cx, |workspace, window, cx| {
23962 assert_eq!(
23963 workspace.panes().len(),
23964 1,
23965 "Should have one pane with one editor"
23966 );
23967 workspace.move_item_to_pane_in_direction(
23968 &MoveItemToPaneInDirection {
23969 direction: SplitDirection::Right,
23970 focus: false,
23971 clone: true,
23972 },
23973 window,
23974 cx,
23975 );
23976 })
23977 .unwrap();
23978 cx.run_until_parked();
23979 workspace
23980 .update(cx, |workspace, _, cx| {
23981 let panes = workspace.panes();
23982 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
23983 for pane in panes {
23984 let editor = pane
23985 .read(cx)
23986 .active_item()
23987 .and_then(|item| item.downcast::<Editor>())
23988 .expect("Should have opened an editor in each split");
23989 let editor_file = editor
23990 .read(cx)
23991 .buffer()
23992 .read(cx)
23993 .as_singleton()
23994 .expect("test deals with singleton buffers")
23995 .read(cx)
23996 .file()
23997 .expect("test buffese should have a file")
23998 .path();
23999 assert_eq!(
24000 editor_file.as_ref(),
24001 Path::new("first.rs"),
24002 "Both editors should be opened for the same file"
24003 )
24004 }
24005 })
24006 .unwrap();
24007
24008 cx.executor().advance_clock(Duration::from_millis(500));
24009 let save = editor.update_in(cx, |editor, window, cx| {
24010 editor.move_to_end(&MoveToEnd, window, cx);
24011 editor.handle_input("dirty", window, cx);
24012 editor.save(
24013 SaveOptions {
24014 format: true,
24015 autosave: true,
24016 },
24017 project.clone(),
24018 window,
24019 cx,
24020 )
24021 });
24022 save.await.unwrap();
24023
24024 color_request_handle.next().await.unwrap();
24025 cx.run_until_parked();
24026 assert_eq!(
24027 3,
24028 requests_made.load(atomic::Ordering::Acquire),
24029 "Should query for colors once per save and once per formatting after save"
24030 );
24031
24032 drop(editor);
24033 let close = workspace
24034 .update(cx, |workspace, window, cx| {
24035 workspace.active_pane().update(cx, |pane, cx| {
24036 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24037 })
24038 })
24039 .unwrap();
24040 close.await.unwrap();
24041 let close = workspace
24042 .update(cx, |workspace, window, cx| {
24043 workspace.active_pane().update(cx, |pane, cx| {
24044 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24045 })
24046 })
24047 .unwrap();
24048 close.await.unwrap();
24049 assert_eq!(
24050 3,
24051 requests_made.load(atomic::Ordering::Acquire),
24052 "After saving and closing all editors, no extra requests should be made"
24053 );
24054 workspace
24055 .update(cx, |workspace, _, cx| {
24056 assert!(
24057 workspace.active_item(cx).is_none(),
24058 "Should close all editors"
24059 )
24060 })
24061 .unwrap();
24062
24063 workspace
24064 .update(cx, |workspace, window, cx| {
24065 workspace.active_pane().update(cx, |pane, cx| {
24066 pane.navigate_backward(window, cx);
24067 })
24068 })
24069 .unwrap();
24070 cx.executor().advance_clock(Duration::from_millis(100));
24071 cx.run_until_parked();
24072 let editor = workspace
24073 .update(cx, |workspace, _, cx| {
24074 workspace
24075 .active_item(cx)
24076 .expect("Should have reopened the editor again after navigating back")
24077 .downcast::<Editor>()
24078 .expect("Should be an editor")
24079 })
24080 .unwrap();
24081 color_request_handle.next().await.unwrap();
24082 assert_eq!(
24083 3,
24084 requests_made.load(atomic::Ordering::Acquire),
24085 "Cache should be reused on buffer close and reopen"
24086 );
24087 editor.update(cx, |editor, cx| {
24088 assert_eq!(
24089 vec![expected_color],
24090 extract_color_inlays(editor, cx),
24091 "Should have an initial inlay"
24092 );
24093 });
24094}
24095
24096#[gpui::test]
24097async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
24098 init_test(cx, |_| {});
24099 let (editor, cx) = cx.add_window_view(Editor::single_line);
24100 editor.update_in(cx, |editor, window, cx| {
24101 editor.set_text("oops\n\nwow\n", window, cx)
24102 });
24103 cx.run_until_parked();
24104 editor.update(cx, |editor, cx| {
24105 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
24106 });
24107 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
24108 cx.run_until_parked();
24109 editor.update(cx, |editor, cx| {
24110 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
24111 });
24112}
24113
24114#[track_caller]
24115fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
24116 editor
24117 .all_inlays(cx)
24118 .into_iter()
24119 .filter_map(|inlay| inlay.get_color())
24120 .map(Rgba::from)
24121 .collect()
24122}