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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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,
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,
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,
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,
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_nodes(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(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
8033 cx.update_editor(|editor, window, cx| {
8034 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
8035 });
8036
8037 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
8038}
8039
8040#[gpui::test]
8041async fn test_fold_function_bodies(cx: &mut TestAppContext) {
8042 init_test(cx, |_| {});
8043
8044 let base_text = r#"
8045 impl A {
8046 // this is an uncommitted comment
8047
8048 fn b() {
8049 c();
8050 }
8051
8052 // this is another uncommitted comment
8053
8054 fn d() {
8055 // e
8056 // f
8057 }
8058 }
8059
8060 fn g() {
8061 // h
8062 }
8063 "#
8064 .unindent();
8065
8066 let text = r#"
8067 ˇimpl A {
8068
8069 fn b() {
8070 c();
8071 }
8072
8073 fn d() {
8074 // e
8075 // f
8076 }
8077 }
8078
8079 fn g() {
8080 // h
8081 }
8082 "#
8083 .unindent();
8084
8085 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8086 cx.set_state(&text);
8087 cx.set_head_text(&base_text);
8088 cx.update_editor(|editor, window, cx| {
8089 editor.expand_all_diff_hunks(&Default::default(), window, cx);
8090 });
8091
8092 cx.assert_state_with_diff(
8093 "
8094 ˇimpl A {
8095 - // this is an uncommitted comment
8096
8097 fn b() {
8098 c();
8099 }
8100
8101 - // this is another uncommitted comment
8102 -
8103 fn d() {
8104 // e
8105 // f
8106 }
8107 }
8108
8109 fn g() {
8110 // h
8111 }
8112 "
8113 .unindent(),
8114 );
8115
8116 let expected_display_text = "
8117 impl A {
8118 // this is an uncommitted comment
8119
8120 fn b() {
8121 ⋯
8122 }
8123
8124 // this is another uncommitted comment
8125
8126 fn d() {
8127 ⋯
8128 }
8129 }
8130
8131 fn g() {
8132 ⋯
8133 }
8134 "
8135 .unindent();
8136
8137 cx.update_editor(|editor, window, cx| {
8138 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
8139 assert_eq!(editor.display_text(cx), expected_display_text);
8140 });
8141}
8142
8143#[gpui::test]
8144async fn test_autoindent(cx: &mut TestAppContext) {
8145 init_test(cx, |_| {});
8146
8147 let language = Arc::new(
8148 Language::new(
8149 LanguageConfig {
8150 brackets: BracketPairConfig {
8151 pairs: vec![
8152 BracketPair {
8153 start: "{".to_string(),
8154 end: "}".to_string(),
8155 close: false,
8156 surround: false,
8157 newline: true,
8158 },
8159 BracketPair {
8160 start: "(".to_string(),
8161 end: ")".to_string(),
8162 close: false,
8163 surround: false,
8164 newline: true,
8165 },
8166 ],
8167 ..Default::default()
8168 },
8169 ..Default::default()
8170 },
8171 Some(tree_sitter_rust::LANGUAGE.into()),
8172 )
8173 .with_indents_query(
8174 r#"
8175 (_ "(" ")" @end) @indent
8176 (_ "{" "}" @end) @indent
8177 "#,
8178 )
8179 .unwrap(),
8180 );
8181
8182 let text = "fn a() {}";
8183
8184 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8185 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8186 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8187 editor
8188 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8189 .await;
8190
8191 editor.update_in(cx, |editor, window, cx| {
8192 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8193 s.select_ranges([5..5, 8..8, 9..9])
8194 });
8195 editor.newline(&Newline, window, cx);
8196 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
8197 assert_eq!(
8198 editor.selections.ranges(cx),
8199 &[
8200 Point::new(1, 4)..Point::new(1, 4),
8201 Point::new(3, 4)..Point::new(3, 4),
8202 Point::new(5, 0)..Point::new(5, 0)
8203 ]
8204 );
8205 });
8206}
8207
8208#[gpui::test]
8209async fn test_autoindent_disabled(cx: &mut TestAppContext) {
8210 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
8211
8212 let language = Arc::new(
8213 Language::new(
8214 LanguageConfig {
8215 brackets: BracketPairConfig {
8216 pairs: vec![
8217 BracketPair {
8218 start: "{".to_string(),
8219 end: "}".to_string(),
8220 close: false,
8221 surround: false,
8222 newline: true,
8223 },
8224 BracketPair {
8225 start: "(".to_string(),
8226 end: ")".to_string(),
8227 close: false,
8228 surround: false,
8229 newline: true,
8230 },
8231 ],
8232 ..Default::default()
8233 },
8234 ..Default::default()
8235 },
8236 Some(tree_sitter_rust::LANGUAGE.into()),
8237 )
8238 .with_indents_query(
8239 r#"
8240 (_ "(" ")" @end) @indent
8241 (_ "{" "}" @end) @indent
8242 "#,
8243 )
8244 .unwrap(),
8245 );
8246
8247 let text = "fn a() {}";
8248
8249 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8250 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8251 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8252 editor
8253 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8254 .await;
8255
8256 editor.update_in(cx, |editor, window, cx| {
8257 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8258 s.select_ranges([5..5, 8..8, 9..9])
8259 });
8260 editor.newline(&Newline, window, cx);
8261 assert_eq!(
8262 editor.text(cx),
8263 indoc!(
8264 "
8265 fn a(
8266
8267 ) {
8268
8269 }
8270 "
8271 )
8272 );
8273 assert_eq!(
8274 editor.selections.ranges(cx),
8275 &[
8276 Point::new(1, 0)..Point::new(1, 0),
8277 Point::new(3, 0)..Point::new(3, 0),
8278 Point::new(5, 0)..Point::new(5, 0)
8279 ]
8280 );
8281 });
8282}
8283
8284#[gpui::test]
8285async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
8286 init_test(cx, |settings| {
8287 settings.defaults.auto_indent = Some(true);
8288 settings.languages.0.insert(
8289 "python".into(),
8290 LanguageSettingsContent {
8291 auto_indent: Some(false),
8292 ..Default::default()
8293 },
8294 );
8295 });
8296
8297 let mut cx = EditorTestContext::new(cx).await;
8298
8299 let injected_language = Arc::new(
8300 Language::new(
8301 LanguageConfig {
8302 brackets: BracketPairConfig {
8303 pairs: vec![
8304 BracketPair {
8305 start: "{".to_string(),
8306 end: "}".to_string(),
8307 close: false,
8308 surround: false,
8309 newline: true,
8310 },
8311 BracketPair {
8312 start: "(".to_string(),
8313 end: ")".to_string(),
8314 close: true,
8315 surround: false,
8316 newline: true,
8317 },
8318 ],
8319 ..Default::default()
8320 },
8321 name: "python".into(),
8322 ..Default::default()
8323 },
8324 Some(tree_sitter_python::LANGUAGE.into()),
8325 )
8326 .with_indents_query(
8327 r#"
8328 (_ "(" ")" @end) @indent
8329 (_ "{" "}" @end) @indent
8330 "#,
8331 )
8332 .unwrap(),
8333 );
8334
8335 let language = Arc::new(
8336 Language::new(
8337 LanguageConfig {
8338 brackets: BracketPairConfig {
8339 pairs: vec![
8340 BracketPair {
8341 start: "{".to_string(),
8342 end: "}".to_string(),
8343 close: false,
8344 surround: false,
8345 newline: true,
8346 },
8347 BracketPair {
8348 start: "(".to_string(),
8349 end: ")".to_string(),
8350 close: true,
8351 surround: false,
8352 newline: true,
8353 },
8354 ],
8355 ..Default::default()
8356 },
8357 name: LanguageName::new("rust"),
8358 ..Default::default()
8359 },
8360 Some(tree_sitter_rust::LANGUAGE.into()),
8361 )
8362 .with_indents_query(
8363 r#"
8364 (_ "(" ")" @end) @indent
8365 (_ "{" "}" @end) @indent
8366 "#,
8367 )
8368 .unwrap()
8369 .with_injection_query(
8370 r#"
8371 (macro_invocation
8372 macro: (identifier) @_macro_name
8373 (token_tree) @injection.content
8374 (#set! injection.language "python"))
8375 "#,
8376 )
8377 .unwrap(),
8378 );
8379
8380 cx.language_registry().add(injected_language);
8381 cx.language_registry().add(language.clone());
8382
8383 cx.update_buffer(|buffer, cx| {
8384 buffer.set_language(Some(language), cx);
8385 });
8386
8387 cx.set_state(r#"struct A {ˇ}"#);
8388
8389 cx.update_editor(|editor, window, cx| {
8390 editor.newline(&Default::default(), window, cx);
8391 });
8392
8393 cx.assert_editor_state(indoc!(
8394 "struct A {
8395 ˇ
8396 }"
8397 ));
8398
8399 cx.set_state(r#"select_biased!(ˇ)"#);
8400
8401 cx.update_editor(|editor, window, cx| {
8402 editor.newline(&Default::default(), window, cx);
8403 editor.handle_input("def ", window, cx);
8404 editor.handle_input("(", window, cx);
8405 editor.newline(&Default::default(), window, cx);
8406 editor.handle_input("a", window, cx);
8407 });
8408
8409 cx.assert_editor_state(indoc!(
8410 "select_biased!(
8411 def (
8412 aˇ
8413 )
8414 )"
8415 ));
8416}
8417
8418#[gpui::test]
8419async fn test_autoindent_selections(cx: &mut TestAppContext) {
8420 init_test(cx, |_| {});
8421
8422 {
8423 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8424 cx.set_state(indoc! {"
8425 impl A {
8426
8427 fn b() {}
8428
8429 «fn c() {
8430
8431 }ˇ»
8432 }
8433 "});
8434
8435 cx.update_editor(|editor, window, cx| {
8436 editor.autoindent(&Default::default(), window, cx);
8437 });
8438
8439 cx.assert_editor_state(indoc! {"
8440 impl A {
8441
8442 fn b() {}
8443
8444 «fn c() {
8445
8446 }ˇ»
8447 }
8448 "});
8449 }
8450
8451 {
8452 let mut cx = EditorTestContext::new_multibuffer(
8453 cx,
8454 [indoc! { "
8455 impl A {
8456 «
8457 // a
8458 fn b(){}
8459 »
8460 «
8461 }
8462 fn c(){}
8463 »
8464 "}],
8465 );
8466
8467 let buffer = cx.update_editor(|editor, _, cx| {
8468 let buffer = editor.buffer().update(cx, |buffer, _| {
8469 buffer.all_buffers().iter().next().unwrap().clone()
8470 });
8471 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
8472 buffer
8473 });
8474
8475 cx.run_until_parked();
8476 cx.update_editor(|editor, window, cx| {
8477 editor.select_all(&Default::default(), window, cx);
8478 editor.autoindent(&Default::default(), window, cx)
8479 });
8480 cx.run_until_parked();
8481
8482 cx.update(|_, cx| {
8483 assert_eq!(
8484 buffer.read(cx).text(),
8485 indoc! { "
8486 impl A {
8487
8488 // a
8489 fn b(){}
8490
8491
8492 }
8493 fn c(){}
8494
8495 " }
8496 )
8497 });
8498 }
8499}
8500
8501#[gpui::test]
8502async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
8503 init_test(cx, |_| {});
8504
8505 let mut cx = EditorTestContext::new(cx).await;
8506
8507 let language = Arc::new(Language::new(
8508 LanguageConfig {
8509 brackets: BracketPairConfig {
8510 pairs: vec![
8511 BracketPair {
8512 start: "{".to_string(),
8513 end: "}".to_string(),
8514 close: true,
8515 surround: true,
8516 newline: true,
8517 },
8518 BracketPair {
8519 start: "(".to_string(),
8520 end: ")".to_string(),
8521 close: true,
8522 surround: true,
8523 newline: true,
8524 },
8525 BracketPair {
8526 start: "/*".to_string(),
8527 end: " */".to_string(),
8528 close: true,
8529 surround: true,
8530 newline: true,
8531 },
8532 BracketPair {
8533 start: "[".to_string(),
8534 end: "]".to_string(),
8535 close: false,
8536 surround: false,
8537 newline: true,
8538 },
8539 BracketPair {
8540 start: "\"".to_string(),
8541 end: "\"".to_string(),
8542 close: true,
8543 surround: true,
8544 newline: false,
8545 },
8546 BracketPair {
8547 start: "<".to_string(),
8548 end: ">".to_string(),
8549 close: false,
8550 surround: true,
8551 newline: true,
8552 },
8553 ],
8554 ..Default::default()
8555 },
8556 autoclose_before: "})]".to_string(),
8557 ..Default::default()
8558 },
8559 Some(tree_sitter_rust::LANGUAGE.into()),
8560 ));
8561
8562 cx.language_registry().add(language.clone());
8563 cx.update_buffer(|buffer, cx| {
8564 buffer.set_language(Some(language), cx);
8565 });
8566
8567 cx.set_state(
8568 &r#"
8569 🏀ˇ
8570 εˇ
8571 ❤️ˇ
8572 "#
8573 .unindent(),
8574 );
8575
8576 // autoclose multiple nested brackets at multiple cursors
8577 cx.update_editor(|editor, window, cx| {
8578 editor.handle_input("{", window, cx);
8579 editor.handle_input("{", window, cx);
8580 editor.handle_input("{", window, cx);
8581 });
8582 cx.assert_editor_state(
8583 &"
8584 🏀{{{ˇ}}}
8585 ε{{{ˇ}}}
8586 ❤️{{{ˇ}}}
8587 "
8588 .unindent(),
8589 );
8590
8591 // insert a different closing bracket
8592 cx.update_editor(|editor, window, cx| {
8593 editor.handle_input(")", window, cx);
8594 });
8595 cx.assert_editor_state(
8596 &"
8597 🏀{{{)ˇ}}}
8598 ε{{{)ˇ}}}
8599 ❤️{{{)ˇ}}}
8600 "
8601 .unindent(),
8602 );
8603
8604 // skip over the auto-closed brackets when typing a closing bracket
8605 cx.update_editor(|editor, window, cx| {
8606 editor.move_right(&MoveRight, window, cx);
8607 editor.handle_input("}", window, cx);
8608 editor.handle_input("}", window, cx);
8609 editor.handle_input("}", window, cx);
8610 });
8611 cx.assert_editor_state(
8612 &"
8613 🏀{{{)}}}}ˇ
8614 ε{{{)}}}}ˇ
8615 ❤️{{{)}}}}ˇ
8616 "
8617 .unindent(),
8618 );
8619
8620 // autoclose multi-character pairs
8621 cx.set_state(
8622 &"
8623 ˇ
8624 ˇ
8625 "
8626 .unindent(),
8627 );
8628 cx.update_editor(|editor, window, cx| {
8629 editor.handle_input("/", window, cx);
8630 editor.handle_input("*", window, cx);
8631 });
8632 cx.assert_editor_state(
8633 &"
8634 /*ˇ */
8635 /*ˇ */
8636 "
8637 .unindent(),
8638 );
8639
8640 // one cursor autocloses a multi-character pair, one cursor
8641 // does not autoclose.
8642 cx.set_state(
8643 &"
8644 /ˇ
8645 ˇ
8646 "
8647 .unindent(),
8648 );
8649 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
8650 cx.assert_editor_state(
8651 &"
8652 /*ˇ */
8653 *ˇ
8654 "
8655 .unindent(),
8656 );
8657
8658 // Don't autoclose if the next character isn't whitespace and isn't
8659 // listed in the language's "autoclose_before" section.
8660 cx.set_state("ˇa b");
8661 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8662 cx.assert_editor_state("{ˇa b");
8663
8664 // Don't autoclose if `close` is false for the bracket pair
8665 cx.set_state("ˇ");
8666 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
8667 cx.assert_editor_state("[ˇ");
8668
8669 // Surround with brackets if text is selected
8670 cx.set_state("«aˇ» b");
8671 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8672 cx.assert_editor_state("{«aˇ»} b");
8673
8674 // Autoclose when not immediately after a word character
8675 cx.set_state("a ˇ");
8676 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8677 cx.assert_editor_state("a \"ˇ\"");
8678
8679 // Autoclose pair where the start and end characters are the same
8680 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8681 cx.assert_editor_state("a \"\"ˇ");
8682
8683 // Don't autoclose when immediately after a word character
8684 cx.set_state("aˇ");
8685 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8686 cx.assert_editor_state("a\"ˇ");
8687
8688 // Do autoclose when after a non-word character
8689 cx.set_state("{ˇ");
8690 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8691 cx.assert_editor_state("{\"ˇ\"");
8692
8693 // Non identical pairs autoclose regardless of preceding character
8694 cx.set_state("aˇ");
8695 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8696 cx.assert_editor_state("a{ˇ}");
8697
8698 // Don't autoclose pair if autoclose is disabled
8699 cx.set_state("ˇ");
8700 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8701 cx.assert_editor_state("<ˇ");
8702
8703 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
8704 cx.set_state("«aˇ» b");
8705 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8706 cx.assert_editor_state("<«aˇ»> b");
8707}
8708
8709#[gpui::test]
8710async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
8711 init_test(cx, |settings| {
8712 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8713 });
8714
8715 let mut cx = EditorTestContext::new(cx).await;
8716
8717 let language = Arc::new(Language::new(
8718 LanguageConfig {
8719 brackets: BracketPairConfig {
8720 pairs: vec![
8721 BracketPair {
8722 start: "{".to_string(),
8723 end: "}".to_string(),
8724 close: true,
8725 surround: true,
8726 newline: true,
8727 },
8728 BracketPair {
8729 start: "(".to_string(),
8730 end: ")".to_string(),
8731 close: true,
8732 surround: true,
8733 newline: true,
8734 },
8735 BracketPair {
8736 start: "[".to_string(),
8737 end: "]".to_string(),
8738 close: false,
8739 surround: false,
8740 newline: true,
8741 },
8742 ],
8743 ..Default::default()
8744 },
8745 autoclose_before: "})]".to_string(),
8746 ..Default::default()
8747 },
8748 Some(tree_sitter_rust::LANGUAGE.into()),
8749 ));
8750
8751 cx.language_registry().add(language.clone());
8752 cx.update_buffer(|buffer, cx| {
8753 buffer.set_language(Some(language), cx);
8754 });
8755
8756 cx.set_state(
8757 &"
8758 ˇ
8759 ˇ
8760 ˇ
8761 "
8762 .unindent(),
8763 );
8764
8765 // ensure only matching closing brackets are skipped over
8766 cx.update_editor(|editor, window, cx| {
8767 editor.handle_input("}", window, cx);
8768 editor.move_left(&MoveLeft, window, cx);
8769 editor.handle_input(")", window, cx);
8770 editor.move_left(&MoveLeft, window, cx);
8771 });
8772 cx.assert_editor_state(
8773 &"
8774 ˇ)}
8775 ˇ)}
8776 ˇ)}
8777 "
8778 .unindent(),
8779 );
8780
8781 // skip-over closing brackets at multiple cursors
8782 cx.update_editor(|editor, window, cx| {
8783 editor.handle_input(")", window, cx);
8784 editor.handle_input("}", window, cx);
8785 });
8786 cx.assert_editor_state(
8787 &"
8788 )}ˇ
8789 )}ˇ
8790 )}ˇ
8791 "
8792 .unindent(),
8793 );
8794
8795 // ignore non-close brackets
8796 cx.update_editor(|editor, window, cx| {
8797 editor.handle_input("]", window, cx);
8798 editor.move_left(&MoveLeft, window, cx);
8799 editor.handle_input("]", window, cx);
8800 });
8801 cx.assert_editor_state(
8802 &"
8803 )}]ˇ]
8804 )}]ˇ]
8805 )}]ˇ]
8806 "
8807 .unindent(),
8808 );
8809}
8810
8811#[gpui::test]
8812async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8813 init_test(cx, |_| {});
8814
8815 let mut cx = EditorTestContext::new(cx).await;
8816
8817 let html_language = Arc::new(
8818 Language::new(
8819 LanguageConfig {
8820 name: "HTML".into(),
8821 brackets: BracketPairConfig {
8822 pairs: vec![
8823 BracketPair {
8824 start: "<".into(),
8825 end: ">".into(),
8826 close: true,
8827 ..Default::default()
8828 },
8829 BracketPair {
8830 start: "{".into(),
8831 end: "}".into(),
8832 close: true,
8833 ..Default::default()
8834 },
8835 BracketPair {
8836 start: "(".into(),
8837 end: ")".into(),
8838 close: true,
8839 ..Default::default()
8840 },
8841 ],
8842 ..Default::default()
8843 },
8844 autoclose_before: "})]>".into(),
8845 ..Default::default()
8846 },
8847 Some(tree_sitter_html::LANGUAGE.into()),
8848 )
8849 .with_injection_query(
8850 r#"
8851 (script_element
8852 (raw_text) @injection.content
8853 (#set! injection.language "javascript"))
8854 "#,
8855 )
8856 .unwrap(),
8857 );
8858
8859 let javascript_language = Arc::new(Language::new(
8860 LanguageConfig {
8861 name: "JavaScript".into(),
8862 brackets: BracketPairConfig {
8863 pairs: vec![
8864 BracketPair {
8865 start: "/*".into(),
8866 end: " */".into(),
8867 close: true,
8868 ..Default::default()
8869 },
8870 BracketPair {
8871 start: "{".into(),
8872 end: "}".into(),
8873 close: true,
8874 ..Default::default()
8875 },
8876 BracketPair {
8877 start: "(".into(),
8878 end: ")".into(),
8879 close: true,
8880 ..Default::default()
8881 },
8882 ],
8883 ..Default::default()
8884 },
8885 autoclose_before: "})]>".into(),
8886 ..Default::default()
8887 },
8888 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8889 ));
8890
8891 cx.language_registry().add(html_language.clone());
8892 cx.language_registry().add(javascript_language);
8893 cx.executor().run_until_parked();
8894
8895 cx.update_buffer(|buffer, cx| {
8896 buffer.set_language(Some(html_language), cx);
8897 });
8898
8899 cx.set_state(
8900 &r#"
8901 <body>ˇ
8902 <script>
8903 var x = 1;ˇ
8904 </script>
8905 </body>ˇ
8906 "#
8907 .unindent(),
8908 );
8909
8910 // Precondition: different languages are active at different locations.
8911 cx.update_editor(|editor, window, cx| {
8912 let snapshot = editor.snapshot(window, cx);
8913 let cursors = editor.selections.ranges::<usize>(cx);
8914 let languages = cursors
8915 .iter()
8916 .map(|c| snapshot.language_at(c.start).unwrap().name())
8917 .collect::<Vec<_>>();
8918 assert_eq!(
8919 languages,
8920 &["HTML".into(), "JavaScript".into(), "HTML".into()]
8921 );
8922 });
8923
8924 // Angle brackets autoclose in HTML, but not JavaScript.
8925 cx.update_editor(|editor, window, cx| {
8926 editor.handle_input("<", window, cx);
8927 editor.handle_input("a", window, cx);
8928 });
8929 cx.assert_editor_state(
8930 &r#"
8931 <body><aˇ>
8932 <script>
8933 var x = 1;<aˇ
8934 </script>
8935 </body><aˇ>
8936 "#
8937 .unindent(),
8938 );
8939
8940 // Curly braces and parens autoclose in both HTML and JavaScript.
8941 cx.update_editor(|editor, window, cx| {
8942 editor.handle_input(" b=", window, cx);
8943 editor.handle_input("{", window, cx);
8944 editor.handle_input("c", window, cx);
8945 editor.handle_input("(", window, cx);
8946 });
8947 cx.assert_editor_state(
8948 &r#"
8949 <body><a b={c(ˇ)}>
8950 <script>
8951 var x = 1;<a b={c(ˇ)}
8952 </script>
8953 </body><a b={c(ˇ)}>
8954 "#
8955 .unindent(),
8956 );
8957
8958 // Brackets that were already autoclosed are skipped.
8959 cx.update_editor(|editor, window, cx| {
8960 editor.handle_input(")", window, cx);
8961 editor.handle_input("d", window, cx);
8962 editor.handle_input("}", window, cx);
8963 });
8964 cx.assert_editor_state(
8965 &r#"
8966 <body><a b={c()d}ˇ>
8967 <script>
8968 var x = 1;<a b={c()d}ˇ
8969 </script>
8970 </body><a b={c()d}ˇ>
8971 "#
8972 .unindent(),
8973 );
8974 cx.update_editor(|editor, window, cx| {
8975 editor.handle_input(">", window, cx);
8976 });
8977 cx.assert_editor_state(
8978 &r#"
8979 <body><a b={c()d}>ˇ
8980 <script>
8981 var x = 1;<a b={c()d}>ˇ
8982 </script>
8983 </body><a b={c()d}>ˇ
8984 "#
8985 .unindent(),
8986 );
8987
8988 // Reset
8989 cx.set_state(
8990 &r#"
8991 <body>ˇ
8992 <script>
8993 var x = 1;ˇ
8994 </script>
8995 </body>ˇ
8996 "#
8997 .unindent(),
8998 );
8999
9000 cx.update_editor(|editor, window, cx| {
9001 editor.handle_input("<", window, cx);
9002 });
9003 cx.assert_editor_state(
9004 &r#"
9005 <body><ˇ>
9006 <script>
9007 var x = 1;<ˇ
9008 </script>
9009 </body><ˇ>
9010 "#
9011 .unindent(),
9012 );
9013
9014 // When backspacing, the closing angle brackets are removed.
9015 cx.update_editor(|editor, window, cx| {
9016 editor.backspace(&Backspace, window, cx);
9017 });
9018 cx.assert_editor_state(
9019 &r#"
9020 <body>ˇ
9021 <script>
9022 var x = 1;ˇ
9023 </script>
9024 </body>ˇ
9025 "#
9026 .unindent(),
9027 );
9028
9029 // Block comments autoclose in JavaScript, but not HTML.
9030 cx.update_editor(|editor, window, cx| {
9031 editor.handle_input("/", window, cx);
9032 editor.handle_input("*", window, cx);
9033 });
9034 cx.assert_editor_state(
9035 &r#"
9036 <body>/*ˇ
9037 <script>
9038 var x = 1;/*ˇ */
9039 </script>
9040 </body>/*ˇ
9041 "#
9042 .unindent(),
9043 );
9044}
9045
9046#[gpui::test]
9047async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
9048 init_test(cx, |_| {});
9049
9050 let mut cx = EditorTestContext::new(cx).await;
9051
9052 let rust_language = Arc::new(
9053 Language::new(
9054 LanguageConfig {
9055 name: "Rust".into(),
9056 brackets: serde_json::from_value(json!([
9057 { "start": "{", "end": "}", "close": true, "newline": true },
9058 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
9059 ]))
9060 .unwrap(),
9061 autoclose_before: "})]>".into(),
9062 ..Default::default()
9063 },
9064 Some(tree_sitter_rust::LANGUAGE.into()),
9065 )
9066 .with_override_query("(string_literal) @string")
9067 .unwrap(),
9068 );
9069
9070 cx.language_registry().add(rust_language.clone());
9071 cx.update_buffer(|buffer, cx| {
9072 buffer.set_language(Some(rust_language), cx);
9073 });
9074
9075 cx.set_state(
9076 &r#"
9077 let x = ˇ
9078 "#
9079 .unindent(),
9080 );
9081
9082 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
9083 cx.update_editor(|editor, window, cx| {
9084 editor.handle_input("\"", window, cx);
9085 });
9086 cx.assert_editor_state(
9087 &r#"
9088 let x = "ˇ"
9089 "#
9090 .unindent(),
9091 );
9092
9093 // Inserting another quotation mark. The cursor moves across the existing
9094 // automatically-inserted quotation mark.
9095 cx.update_editor(|editor, window, cx| {
9096 editor.handle_input("\"", window, cx);
9097 });
9098 cx.assert_editor_state(
9099 &r#"
9100 let x = ""ˇ
9101 "#
9102 .unindent(),
9103 );
9104
9105 // Reset
9106 cx.set_state(
9107 &r#"
9108 let x = ˇ
9109 "#
9110 .unindent(),
9111 );
9112
9113 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
9114 cx.update_editor(|editor, window, cx| {
9115 editor.handle_input("\"", window, cx);
9116 editor.handle_input(" ", window, cx);
9117 editor.move_left(&Default::default(), window, cx);
9118 editor.handle_input("\\", window, cx);
9119 editor.handle_input("\"", window, cx);
9120 });
9121 cx.assert_editor_state(
9122 &r#"
9123 let x = "\"ˇ "
9124 "#
9125 .unindent(),
9126 );
9127
9128 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
9129 // mark. Nothing is inserted.
9130 cx.update_editor(|editor, window, cx| {
9131 editor.move_right(&Default::default(), window, cx);
9132 editor.handle_input("\"", window, cx);
9133 });
9134 cx.assert_editor_state(
9135 &r#"
9136 let x = "\" "ˇ
9137 "#
9138 .unindent(),
9139 );
9140}
9141
9142#[gpui::test]
9143async fn test_surround_with_pair(cx: &mut TestAppContext) {
9144 init_test(cx, |_| {});
9145
9146 let language = Arc::new(Language::new(
9147 LanguageConfig {
9148 brackets: BracketPairConfig {
9149 pairs: vec![
9150 BracketPair {
9151 start: "{".to_string(),
9152 end: "}".to_string(),
9153 close: true,
9154 surround: true,
9155 newline: true,
9156 },
9157 BracketPair {
9158 start: "/* ".to_string(),
9159 end: "*/".to_string(),
9160 close: true,
9161 surround: true,
9162 ..Default::default()
9163 },
9164 ],
9165 ..Default::default()
9166 },
9167 ..Default::default()
9168 },
9169 Some(tree_sitter_rust::LANGUAGE.into()),
9170 ));
9171
9172 let text = r#"
9173 a
9174 b
9175 c
9176 "#
9177 .unindent();
9178
9179 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9180 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9181 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9182 editor
9183 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9184 .await;
9185
9186 editor.update_in(cx, |editor, window, cx| {
9187 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9188 s.select_display_ranges([
9189 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
9190 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
9191 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
9192 ])
9193 });
9194
9195 editor.handle_input("{", window, cx);
9196 editor.handle_input("{", window, cx);
9197 editor.handle_input("{", window, cx);
9198 assert_eq!(
9199 editor.text(cx),
9200 "
9201 {{{a}}}
9202 {{{b}}}
9203 {{{c}}}
9204 "
9205 .unindent()
9206 );
9207 assert_eq!(
9208 editor.selections.display_ranges(cx),
9209 [
9210 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
9211 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
9212 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
9213 ]
9214 );
9215
9216 editor.undo(&Undo, window, cx);
9217 editor.undo(&Undo, window, cx);
9218 editor.undo(&Undo, window, cx);
9219 assert_eq!(
9220 editor.text(cx),
9221 "
9222 a
9223 b
9224 c
9225 "
9226 .unindent()
9227 );
9228 assert_eq!(
9229 editor.selections.display_ranges(cx),
9230 [
9231 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
9232 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
9233 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
9234 ]
9235 );
9236
9237 // Ensure inserting the first character of a multi-byte bracket pair
9238 // doesn't surround the selections with the bracket.
9239 editor.handle_input("/", window, cx);
9240 assert_eq!(
9241 editor.text(cx),
9242 "
9243 /
9244 /
9245 /
9246 "
9247 .unindent()
9248 );
9249 assert_eq!(
9250 editor.selections.display_ranges(cx),
9251 [
9252 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
9253 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
9254 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
9255 ]
9256 );
9257
9258 editor.undo(&Undo, window, cx);
9259 assert_eq!(
9260 editor.text(cx),
9261 "
9262 a
9263 b
9264 c
9265 "
9266 .unindent()
9267 );
9268 assert_eq!(
9269 editor.selections.display_ranges(cx),
9270 [
9271 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
9272 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
9273 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
9274 ]
9275 );
9276
9277 // Ensure inserting the last character of a multi-byte bracket pair
9278 // doesn't surround the selections with the bracket.
9279 editor.handle_input("*", window, cx);
9280 assert_eq!(
9281 editor.text(cx),
9282 "
9283 *
9284 *
9285 *
9286 "
9287 .unindent()
9288 );
9289 assert_eq!(
9290 editor.selections.display_ranges(cx),
9291 [
9292 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
9293 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
9294 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
9295 ]
9296 );
9297 });
9298}
9299
9300#[gpui::test]
9301async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
9302 init_test(cx, |_| {});
9303
9304 let language = Arc::new(Language::new(
9305 LanguageConfig {
9306 brackets: BracketPairConfig {
9307 pairs: vec![BracketPair {
9308 start: "{".to_string(),
9309 end: "}".to_string(),
9310 close: true,
9311 surround: true,
9312 newline: true,
9313 }],
9314 ..Default::default()
9315 },
9316 autoclose_before: "}".to_string(),
9317 ..Default::default()
9318 },
9319 Some(tree_sitter_rust::LANGUAGE.into()),
9320 ));
9321
9322 let text = r#"
9323 a
9324 b
9325 c
9326 "#
9327 .unindent();
9328
9329 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9330 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9331 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9332 editor
9333 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9334 .await;
9335
9336 editor.update_in(cx, |editor, window, cx| {
9337 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9338 s.select_ranges([
9339 Point::new(0, 1)..Point::new(0, 1),
9340 Point::new(1, 1)..Point::new(1, 1),
9341 Point::new(2, 1)..Point::new(2, 1),
9342 ])
9343 });
9344
9345 editor.handle_input("{", window, cx);
9346 editor.handle_input("{", window, cx);
9347 editor.handle_input("_", window, cx);
9348 assert_eq!(
9349 editor.text(cx),
9350 "
9351 a{{_}}
9352 b{{_}}
9353 c{{_}}
9354 "
9355 .unindent()
9356 );
9357 assert_eq!(
9358 editor.selections.ranges::<Point>(cx),
9359 [
9360 Point::new(0, 4)..Point::new(0, 4),
9361 Point::new(1, 4)..Point::new(1, 4),
9362 Point::new(2, 4)..Point::new(2, 4)
9363 ]
9364 );
9365
9366 editor.backspace(&Default::default(), window, cx);
9367 editor.backspace(&Default::default(), window, cx);
9368 assert_eq!(
9369 editor.text(cx),
9370 "
9371 a{}
9372 b{}
9373 c{}
9374 "
9375 .unindent()
9376 );
9377 assert_eq!(
9378 editor.selections.ranges::<Point>(cx),
9379 [
9380 Point::new(0, 2)..Point::new(0, 2),
9381 Point::new(1, 2)..Point::new(1, 2),
9382 Point::new(2, 2)..Point::new(2, 2)
9383 ]
9384 );
9385
9386 editor.delete_to_previous_word_start(&Default::default(), window, cx);
9387 assert_eq!(
9388 editor.text(cx),
9389 "
9390 a
9391 b
9392 c
9393 "
9394 .unindent()
9395 );
9396 assert_eq!(
9397 editor.selections.ranges::<Point>(cx),
9398 [
9399 Point::new(0, 1)..Point::new(0, 1),
9400 Point::new(1, 1)..Point::new(1, 1),
9401 Point::new(2, 1)..Point::new(2, 1)
9402 ]
9403 );
9404 });
9405}
9406
9407#[gpui::test]
9408async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
9409 init_test(cx, |settings| {
9410 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9411 });
9412
9413 let mut cx = EditorTestContext::new(cx).await;
9414
9415 let language = Arc::new(Language::new(
9416 LanguageConfig {
9417 brackets: BracketPairConfig {
9418 pairs: vec![
9419 BracketPair {
9420 start: "{".to_string(),
9421 end: "}".to_string(),
9422 close: true,
9423 surround: true,
9424 newline: true,
9425 },
9426 BracketPair {
9427 start: "(".to_string(),
9428 end: ")".to_string(),
9429 close: true,
9430 surround: true,
9431 newline: true,
9432 },
9433 BracketPair {
9434 start: "[".to_string(),
9435 end: "]".to_string(),
9436 close: false,
9437 surround: true,
9438 newline: true,
9439 },
9440 ],
9441 ..Default::default()
9442 },
9443 autoclose_before: "})]".to_string(),
9444 ..Default::default()
9445 },
9446 Some(tree_sitter_rust::LANGUAGE.into()),
9447 ));
9448
9449 cx.language_registry().add(language.clone());
9450 cx.update_buffer(|buffer, cx| {
9451 buffer.set_language(Some(language), cx);
9452 });
9453
9454 cx.set_state(
9455 &"
9456 {(ˇ)}
9457 [[ˇ]]
9458 {(ˇ)}
9459 "
9460 .unindent(),
9461 );
9462
9463 cx.update_editor(|editor, window, cx| {
9464 editor.backspace(&Default::default(), window, cx);
9465 editor.backspace(&Default::default(), window, cx);
9466 });
9467
9468 cx.assert_editor_state(
9469 &"
9470 ˇ
9471 ˇ]]
9472 ˇ
9473 "
9474 .unindent(),
9475 );
9476
9477 cx.update_editor(|editor, window, cx| {
9478 editor.handle_input("{", window, cx);
9479 editor.handle_input("{", window, cx);
9480 editor.move_right(&MoveRight, window, cx);
9481 editor.move_right(&MoveRight, window, cx);
9482 editor.move_left(&MoveLeft, window, cx);
9483 editor.move_left(&MoveLeft, window, cx);
9484 editor.backspace(&Default::default(), window, cx);
9485 });
9486
9487 cx.assert_editor_state(
9488 &"
9489 {ˇ}
9490 {ˇ}]]
9491 {ˇ}
9492 "
9493 .unindent(),
9494 );
9495
9496 cx.update_editor(|editor, window, cx| {
9497 editor.backspace(&Default::default(), window, cx);
9498 });
9499
9500 cx.assert_editor_state(
9501 &"
9502 ˇ
9503 ˇ]]
9504 ˇ
9505 "
9506 .unindent(),
9507 );
9508}
9509
9510#[gpui::test]
9511async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
9512 init_test(cx, |_| {});
9513
9514 let language = Arc::new(Language::new(
9515 LanguageConfig::default(),
9516 Some(tree_sitter_rust::LANGUAGE.into()),
9517 ));
9518
9519 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
9520 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9521 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9522 editor
9523 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9524 .await;
9525
9526 editor.update_in(cx, |editor, window, cx| {
9527 editor.set_auto_replace_emoji_shortcode(true);
9528
9529 editor.handle_input("Hello ", window, cx);
9530 editor.handle_input(":wave", window, cx);
9531 assert_eq!(editor.text(cx), "Hello :wave".unindent());
9532
9533 editor.handle_input(":", window, cx);
9534 assert_eq!(editor.text(cx), "Hello 👋".unindent());
9535
9536 editor.handle_input(" :smile", window, cx);
9537 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
9538
9539 editor.handle_input(":", window, cx);
9540 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
9541
9542 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
9543 editor.handle_input(":wave", window, cx);
9544 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
9545
9546 editor.handle_input(":", window, cx);
9547 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
9548
9549 editor.handle_input(":1", window, cx);
9550 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
9551
9552 editor.handle_input(":", window, cx);
9553 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
9554
9555 // Ensure shortcode does not get replaced when it is part of a word
9556 editor.handle_input(" Test:wave", window, cx);
9557 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
9558
9559 editor.handle_input(":", window, cx);
9560 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
9561
9562 editor.set_auto_replace_emoji_shortcode(false);
9563
9564 // Ensure shortcode does not get replaced when auto replace is off
9565 editor.handle_input(" :wave", window, cx);
9566 assert_eq!(
9567 editor.text(cx),
9568 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
9569 );
9570
9571 editor.handle_input(":", window, cx);
9572 assert_eq!(
9573 editor.text(cx),
9574 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
9575 );
9576 });
9577}
9578
9579#[gpui::test]
9580async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
9581 init_test(cx, |_| {});
9582
9583 let (text, insertion_ranges) = marked_text_ranges(
9584 indoc! {"
9585 ˇ
9586 "},
9587 false,
9588 );
9589
9590 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
9591 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9592
9593 _ = editor.update_in(cx, |editor, window, cx| {
9594 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
9595
9596 editor
9597 .insert_snippet(&insertion_ranges, snippet, window, cx)
9598 .unwrap();
9599
9600 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
9601 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
9602 assert_eq!(editor.text(cx), expected_text);
9603 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
9604 }
9605
9606 assert(
9607 editor,
9608 cx,
9609 indoc! {"
9610 type «» =•
9611 "},
9612 );
9613
9614 assert!(editor.context_menu_visible(), "There should be a matches");
9615 });
9616}
9617
9618#[gpui::test]
9619async fn test_snippets(cx: &mut TestAppContext) {
9620 init_test(cx, |_| {});
9621
9622 let mut cx = EditorTestContext::new(cx).await;
9623
9624 cx.set_state(indoc! {"
9625 a.ˇ b
9626 a.ˇ b
9627 a.ˇ b
9628 "});
9629
9630 cx.update_editor(|editor, window, cx| {
9631 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
9632 let insertion_ranges = editor
9633 .selections
9634 .all(cx)
9635 .iter()
9636 .map(|s| s.range())
9637 .collect::<Vec<_>>();
9638 editor
9639 .insert_snippet(&insertion_ranges, snippet, window, cx)
9640 .unwrap();
9641 });
9642
9643 cx.assert_editor_state(indoc! {"
9644 a.f(«oneˇ», two, «threeˇ») b
9645 a.f(«oneˇ», two, «threeˇ») b
9646 a.f(«oneˇ», two, «threeˇ») b
9647 "});
9648
9649 // Can't move earlier than the first tab stop
9650 cx.update_editor(|editor, window, cx| {
9651 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9652 });
9653 cx.assert_editor_state(indoc! {"
9654 a.f(«oneˇ», two, «threeˇ») b
9655 a.f(«oneˇ», two, «threeˇ») b
9656 a.f(«oneˇ», two, «threeˇ») b
9657 "});
9658
9659 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9660 cx.assert_editor_state(indoc! {"
9661 a.f(one, «twoˇ», three) b
9662 a.f(one, «twoˇ», three) b
9663 a.f(one, «twoˇ», three) b
9664 "});
9665
9666 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
9667 cx.assert_editor_state(indoc! {"
9668 a.f(«oneˇ», two, «threeˇ») b
9669 a.f(«oneˇ», two, «threeˇ») b
9670 a.f(«oneˇ», two, «threeˇ») b
9671 "});
9672
9673 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9674 cx.assert_editor_state(indoc! {"
9675 a.f(one, «twoˇ», three) b
9676 a.f(one, «twoˇ», three) b
9677 a.f(one, «twoˇ», three) b
9678 "});
9679 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9680 cx.assert_editor_state(indoc! {"
9681 a.f(one, two, three)ˇ b
9682 a.f(one, two, three)ˇ b
9683 a.f(one, two, three)ˇ b
9684 "});
9685
9686 // As soon as the last tab stop is reached, snippet state is gone
9687 cx.update_editor(|editor, window, cx| {
9688 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9689 });
9690 cx.assert_editor_state(indoc! {"
9691 a.f(one, two, three)ˇ b
9692 a.f(one, two, three)ˇ b
9693 a.f(one, two, three)ˇ b
9694 "});
9695}
9696
9697#[gpui::test]
9698async fn test_snippet_indentation(cx: &mut TestAppContext) {
9699 init_test(cx, |_| {});
9700
9701 let mut cx = EditorTestContext::new(cx).await;
9702
9703 cx.update_editor(|editor, window, cx| {
9704 let snippet = Snippet::parse(indoc! {"
9705 /*
9706 * Multiline comment with leading indentation
9707 *
9708 * $1
9709 */
9710 $0"})
9711 .unwrap();
9712 let insertion_ranges = editor
9713 .selections
9714 .all(cx)
9715 .iter()
9716 .map(|s| s.range())
9717 .collect::<Vec<_>>();
9718 editor
9719 .insert_snippet(&insertion_ranges, snippet, window, cx)
9720 .unwrap();
9721 });
9722
9723 cx.assert_editor_state(indoc! {"
9724 /*
9725 * Multiline comment with leading indentation
9726 *
9727 * ˇ
9728 */
9729 "});
9730
9731 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9732 cx.assert_editor_state(indoc! {"
9733 /*
9734 * Multiline comment with leading indentation
9735 *
9736 *•
9737 */
9738 ˇ"});
9739}
9740
9741#[gpui::test]
9742async fn test_document_format_during_save(cx: &mut TestAppContext) {
9743 init_test(cx, |_| {});
9744
9745 let fs = FakeFs::new(cx.executor());
9746 fs.insert_file(path!("/file.rs"), Default::default()).await;
9747
9748 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
9749
9750 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9751 language_registry.add(rust_lang());
9752 let mut fake_servers = language_registry.register_fake_lsp(
9753 "Rust",
9754 FakeLspAdapter {
9755 capabilities: lsp::ServerCapabilities {
9756 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9757 ..Default::default()
9758 },
9759 ..Default::default()
9760 },
9761 );
9762
9763 let buffer = project
9764 .update(cx, |project, cx| {
9765 project.open_local_buffer(path!("/file.rs"), cx)
9766 })
9767 .await
9768 .unwrap();
9769
9770 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9771 let (editor, cx) = cx.add_window_view(|window, cx| {
9772 build_editor_with_project(project.clone(), buffer, window, cx)
9773 });
9774 editor.update_in(cx, |editor, window, cx| {
9775 editor.set_text("one\ntwo\nthree\n", window, cx)
9776 });
9777 assert!(cx.read(|cx| editor.is_dirty(cx)));
9778
9779 cx.executor().start_waiting();
9780 let fake_server = fake_servers.next().await.unwrap();
9781
9782 {
9783 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9784 move |params, _| async move {
9785 assert_eq!(
9786 params.text_document.uri,
9787 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9788 );
9789 assert_eq!(params.options.tab_size, 4);
9790 Ok(Some(vec![lsp::TextEdit::new(
9791 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9792 ", ".to_string(),
9793 )]))
9794 },
9795 );
9796 let save = editor
9797 .update_in(cx, |editor, window, cx| {
9798 editor.save(
9799 SaveOptions {
9800 format: true,
9801 autosave: false,
9802 },
9803 project.clone(),
9804 window,
9805 cx,
9806 )
9807 })
9808 .unwrap();
9809 cx.executor().start_waiting();
9810 save.await;
9811
9812 assert_eq!(
9813 editor.update(cx, |editor, cx| editor.text(cx)),
9814 "one, two\nthree\n"
9815 );
9816 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9817 }
9818
9819 {
9820 editor.update_in(cx, |editor, window, cx| {
9821 editor.set_text("one\ntwo\nthree\n", window, cx)
9822 });
9823 assert!(cx.read(|cx| editor.is_dirty(cx)));
9824
9825 // Ensure we can still save even if formatting hangs.
9826 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9827 move |params, _| async move {
9828 assert_eq!(
9829 params.text_document.uri,
9830 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9831 );
9832 futures::future::pending::<()>().await;
9833 unreachable!()
9834 },
9835 );
9836 let save = editor
9837 .update_in(cx, |editor, window, cx| {
9838 editor.save(
9839 SaveOptions {
9840 format: true,
9841 autosave: false,
9842 },
9843 project.clone(),
9844 window,
9845 cx,
9846 )
9847 })
9848 .unwrap();
9849 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9850 cx.executor().start_waiting();
9851 save.await;
9852 assert_eq!(
9853 editor.update(cx, |editor, cx| editor.text(cx)),
9854 "one\ntwo\nthree\n"
9855 );
9856 }
9857
9858 // Set rust language override and assert overridden tabsize is sent to language server
9859 update_test_language_settings(cx, |settings| {
9860 settings.languages.0.insert(
9861 "Rust".into(),
9862 LanguageSettingsContent {
9863 tab_size: NonZeroU32::new(8),
9864 ..Default::default()
9865 },
9866 );
9867 });
9868
9869 {
9870 editor.update_in(cx, |editor, window, cx| {
9871 editor.set_text("somehting_new\n", window, cx)
9872 });
9873 assert!(cx.read(|cx| editor.is_dirty(cx)));
9874 let _formatting_request_signal = fake_server
9875 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9876 assert_eq!(
9877 params.text_document.uri,
9878 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9879 );
9880 assert_eq!(params.options.tab_size, 8);
9881 Ok(Some(vec![]))
9882 });
9883 let save = editor
9884 .update_in(cx, |editor, window, cx| {
9885 editor.save(
9886 SaveOptions {
9887 format: true,
9888 autosave: false,
9889 },
9890 project.clone(),
9891 window,
9892 cx,
9893 )
9894 })
9895 .unwrap();
9896 cx.executor().start_waiting();
9897 save.await;
9898 }
9899}
9900
9901#[gpui::test]
9902async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
9903 init_test(cx, |settings| {
9904 settings.defaults.ensure_final_newline_on_save = Some(false);
9905 });
9906
9907 let fs = FakeFs::new(cx.executor());
9908 fs.insert_file(path!("/file.txt"), "foo".into()).await;
9909
9910 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
9911
9912 let buffer = project
9913 .update(cx, |project, cx| {
9914 project.open_local_buffer(path!("/file.txt"), cx)
9915 })
9916 .await
9917 .unwrap();
9918
9919 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9920 let (editor, cx) = cx.add_window_view(|window, cx| {
9921 build_editor_with_project(project.clone(), buffer, window, cx)
9922 });
9923 editor.update_in(cx, |editor, window, cx| {
9924 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9925 s.select_ranges([0..0])
9926 });
9927 });
9928 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9929
9930 editor.update_in(cx, |editor, window, cx| {
9931 editor.handle_input("\n", window, cx)
9932 });
9933 cx.run_until_parked();
9934 save(&editor, &project, cx).await;
9935 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9936
9937 editor.update_in(cx, |editor, window, cx| {
9938 editor.undo(&Default::default(), window, cx);
9939 });
9940 save(&editor, &project, cx).await;
9941 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9942
9943 editor.update_in(cx, |editor, window, cx| {
9944 editor.redo(&Default::default(), window, cx);
9945 });
9946 cx.run_until_parked();
9947 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9948
9949 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
9950 let save = editor
9951 .update_in(cx, |editor, window, cx| {
9952 editor.save(
9953 SaveOptions {
9954 format: true,
9955 autosave: false,
9956 },
9957 project.clone(),
9958 window,
9959 cx,
9960 )
9961 })
9962 .unwrap();
9963 cx.executor().start_waiting();
9964 save.await;
9965 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9966 }
9967}
9968
9969#[gpui::test]
9970async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9971 init_test(cx, |_| {});
9972
9973 let cols = 4;
9974 let rows = 10;
9975 let sample_text_1 = sample_text(rows, cols, 'a');
9976 assert_eq!(
9977 sample_text_1,
9978 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9979 );
9980 let sample_text_2 = sample_text(rows, cols, 'l');
9981 assert_eq!(
9982 sample_text_2,
9983 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9984 );
9985 let sample_text_3 = sample_text(rows, cols, 'v');
9986 assert_eq!(
9987 sample_text_3,
9988 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9989 );
9990
9991 let fs = FakeFs::new(cx.executor());
9992 fs.insert_tree(
9993 path!("/a"),
9994 json!({
9995 "main.rs": sample_text_1,
9996 "other.rs": sample_text_2,
9997 "lib.rs": sample_text_3,
9998 }),
9999 )
10000 .await;
10001
10002 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10003 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10004 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
10005
10006 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10007 language_registry.add(rust_lang());
10008 let mut fake_servers = language_registry.register_fake_lsp(
10009 "Rust",
10010 FakeLspAdapter {
10011 capabilities: lsp::ServerCapabilities {
10012 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10013 ..Default::default()
10014 },
10015 ..Default::default()
10016 },
10017 );
10018
10019 let worktree = project.update(cx, |project, cx| {
10020 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
10021 assert_eq!(worktrees.len(), 1);
10022 worktrees.pop().unwrap()
10023 });
10024 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
10025
10026 let buffer_1 = project
10027 .update(cx, |project, cx| {
10028 project.open_buffer((worktree_id, "main.rs"), cx)
10029 })
10030 .await
10031 .unwrap();
10032 let buffer_2 = project
10033 .update(cx, |project, cx| {
10034 project.open_buffer((worktree_id, "other.rs"), cx)
10035 })
10036 .await
10037 .unwrap();
10038 let buffer_3 = project
10039 .update(cx, |project, cx| {
10040 project.open_buffer((worktree_id, "lib.rs"), cx)
10041 })
10042 .await
10043 .unwrap();
10044
10045 let multi_buffer = cx.new(|cx| {
10046 let mut multi_buffer = MultiBuffer::new(ReadWrite);
10047 multi_buffer.push_excerpts(
10048 buffer_1.clone(),
10049 [
10050 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
10051 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
10052 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
10053 ],
10054 cx,
10055 );
10056 multi_buffer.push_excerpts(
10057 buffer_2.clone(),
10058 [
10059 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
10060 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
10061 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
10062 ],
10063 cx,
10064 );
10065 multi_buffer.push_excerpts(
10066 buffer_3.clone(),
10067 [
10068 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
10069 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
10070 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
10071 ],
10072 cx,
10073 );
10074 multi_buffer
10075 });
10076 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
10077 Editor::new(
10078 EditorMode::full(),
10079 multi_buffer,
10080 Some(project.clone()),
10081 window,
10082 cx,
10083 )
10084 });
10085
10086 multi_buffer_editor.update_in(cx, |editor, window, cx| {
10087 editor.change_selections(
10088 SelectionEffects::scroll(Autoscroll::Next),
10089 window,
10090 cx,
10091 |s| s.select_ranges(Some(1..2)),
10092 );
10093 editor.insert("|one|two|three|", window, cx);
10094 });
10095 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
10096 multi_buffer_editor.update_in(cx, |editor, window, cx| {
10097 editor.change_selections(
10098 SelectionEffects::scroll(Autoscroll::Next),
10099 window,
10100 cx,
10101 |s| s.select_ranges(Some(60..70)),
10102 );
10103 editor.insert("|four|five|six|", window, cx);
10104 });
10105 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
10106
10107 // First two buffers should be edited, but not the third one.
10108 assert_eq!(
10109 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
10110 "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}",
10111 );
10112 buffer_1.update(cx, |buffer, _| {
10113 assert!(buffer.is_dirty());
10114 assert_eq!(
10115 buffer.text(),
10116 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
10117 )
10118 });
10119 buffer_2.update(cx, |buffer, _| {
10120 assert!(buffer.is_dirty());
10121 assert_eq!(
10122 buffer.text(),
10123 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
10124 )
10125 });
10126 buffer_3.update(cx, |buffer, _| {
10127 assert!(!buffer.is_dirty());
10128 assert_eq!(buffer.text(), sample_text_3,)
10129 });
10130 cx.executor().run_until_parked();
10131
10132 cx.executor().start_waiting();
10133 let save = multi_buffer_editor
10134 .update_in(cx, |editor, window, cx| {
10135 editor.save(
10136 SaveOptions {
10137 format: true,
10138 autosave: false,
10139 },
10140 project.clone(),
10141 window,
10142 cx,
10143 )
10144 })
10145 .unwrap();
10146
10147 let fake_server = fake_servers.next().await.unwrap();
10148 fake_server
10149 .server
10150 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
10151 Ok(Some(vec![lsp::TextEdit::new(
10152 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10153 format!("[{} formatted]", params.text_document.uri),
10154 )]))
10155 })
10156 .detach();
10157 save.await;
10158
10159 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
10160 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
10161 assert_eq!(
10162 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
10163 uri!(
10164 "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}"
10165 ),
10166 );
10167 buffer_1.update(cx, |buffer, _| {
10168 assert!(!buffer.is_dirty());
10169 assert_eq!(
10170 buffer.text(),
10171 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
10172 )
10173 });
10174 buffer_2.update(cx, |buffer, _| {
10175 assert!(!buffer.is_dirty());
10176 assert_eq!(
10177 buffer.text(),
10178 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
10179 )
10180 });
10181 buffer_3.update(cx, |buffer, _| {
10182 assert!(!buffer.is_dirty());
10183 assert_eq!(buffer.text(), sample_text_3,)
10184 });
10185}
10186
10187#[gpui::test]
10188async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
10189 init_test(cx, |_| {});
10190
10191 let fs = FakeFs::new(cx.executor());
10192 fs.insert_tree(
10193 path!("/dir"),
10194 json!({
10195 "file1.rs": "fn main() { println!(\"hello\"); }",
10196 "file2.rs": "fn test() { println!(\"test\"); }",
10197 "file3.rs": "fn other() { println!(\"other\"); }\n",
10198 }),
10199 )
10200 .await;
10201
10202 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
10203 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10204 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
10205
10206 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10207 language_registry.add(rust_lang());
10208
10209 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
10210 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
10211
10212 // Open three buffers
10213 let buffer_1 = project
10214 .update(cx, |project, cx| {
10215 project.open_buffer((worktree_id, "file1.rs"), cx)
10216 })
10217 .await
10218 .unwrap();
10219 let buffer_2 = project
10220 .update(cx, |project, cx| {
10221 project.open_buffer((worktree_id, "file2.rs"), cx)
10222 })
10223 .await
10224 .unwrap();
10225 let buffer_3 = project
10226 .update(cx, |project, cx| {
10227 project.open_buffer((worktree_id, "file3.rs"), cx)
10228 })
10229 .await
10230 .unwrap();
10231
10232 // Create a multi-buffer with all three buffers
10233 let multi_buffer = cx.new(|cx| {
10234 let mut multi_buffer = MultiBuffer::new(ReadWrite);
10235 multi_buffer.push_excerpts(
10236 buffer_1.clone(),
10237 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
10238 cx,
10239 );
10240 multi_buffer.push_excerpts(
10241 buffer_2.clone(),
10242 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
10243 cx,
10244 );
10245 multi_buffer.push_excerpts(
10246 buffer_3.clone(),
10247 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
10248 cx,
10249 );
10250 multi_buffer
10251 });
10252
10253 let editor = cx.new_window_entity(|window, cx| {
10254 Editor::new(
10255 EditorMode::full(),
10256 multi_buffer,
10257 Some(project.clone()),
10258 window,
10259 cx,
10260 )
10261 });
10262
10263 // Edit only the first buffer
10264 editor.update_in(cx, |editor, window, cx| {
10265 editor.change_selections(
10266 SelectionEffects::scroll(Autoscroll::Next),
10267 window,
10268 cx,
10269 |s| s.select_ranges(Some(10..10)),
10270 );
10271 editor.insert("// edited", window, cx);
10272 });
10273
10274 // Verify that only buffer 1 is dirty
10275 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
10276 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10277 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10278
10279 // Get write counts after file creation (files were created with initial content)
10280 // We expect each file to have been written once during creation
10281 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
10282 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
10283 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
10284
10285 // Perform autosave
10286 let save_task = editor.update_in(cx, |editor, window, cx| {
10287 editor.save(
10288 SaveOptions {
10289 format: true,
10290 autosave: true,
10291 },
10292 project.clone(),
10293 window,
10294 cx,
10295 )
10296 });
10297 save_task.await.unwrap();
10298
10299 // Only the dirty buffer should have been saved
10300 assert_eq!(
10301 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
10302 1,
10303 "Buffer 1 was dirty, so it should have been written once during autosave"
10304 );
10305 assert_eq!(
10306 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
10307 0,
10308 "Buffer 2 was clean, so it should not have been written during autosave"
10309 );
10310 assert_eq!(
10311 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
10312 0,
10313 "Buffer 3 was clean, so it should not have been written during autosave"
10314 );
10315
10316 // Verify buffer states after autosave
10317 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10318 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10319 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10320
10321 // Now perform a manual save (format = true)
10322 let save_task = editor.update_in(cx, |editor, window, cx| {
10323 editor.save(
10324 SaveOptions {
10325 format: true,
10326 autosave: false,
10327 },
10328 project.clone(),
10329 window,
10330 cx,
10331 )
10332 });
10333 save_task.await.unwrap();
10334
10335 // During manual save, clean buffers don't get written to disk
10336 // They just get did_save called for language server notifications
10337 assert_eq!(
10338 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
10339 1,
10340 "Buffer 1 should only have been written once total (during autosave, not manual save)"
10341 );
10342 assert_eq!(
10343 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
10344 0,
10345 "Buffer 2 should not have been written at all"
10346 );
10347 assert_eq!(
10348 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
10349 0,
10350 "Buffer 3 should not have been written at all"
10351 );
10352}
10353
10354async fn setup_range_format_test(
10355 cx: &mut TestAppContext,
10356) -> (
10357 Entity<Project>,
10358 Entity<Editor>,
10359 &mut gpui::VisualTestContext,
10360 lsp::FakeLanguageServer,
10361) {
10362 init_test(cx, |_| {});
10363
10364 let fs = FakeFs::new(cx.executor());
10365 fs.insert_file(path!("/file.rs"), Default::default()).await;
10366
10367 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10368
10369 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10370 language_registry.add(rust_lang());
10371 let mut fake_servers = language_registry.register_fake_lsp(
10372 "Rust",
10373 FakeLspAdapter {
10374 capabilities: lsp::ServerCapabilities {
10375 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
10376 ..lsp::ServerCapabilities::default()
10377 },
10378 ..FakeLspAdapter::default()
10379 },
10380 );
10381
10382 let buffer = project
10383 .update(cx, |project, cx| {
10384 project.open_local_buffer(path!("/file.rs"), cx)
10385 })
10386 .await
10387 .unwrap();
10388
10389 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10390 let (editor, cx) = cx.add_window_view(|window, cx| {
10391 build_editor_with_project(project.clone(), buffer, window, cx)
10392 });
10393
10394 cx.executor().start_waiting();
10395 let fake_server = fake_servers.next().await.unwrap();
10396
10397 (project, editor, cx, fake_server)
10398}
10399
10400#[gpui::test]
10401async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
10402 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10403
10404 editor.update_in(cx, |editor, window, cx| {
10405 editor.set_text("one\ntwo\nthree\n", window, cx)
10406 });
10407 assert!(cx.read(|cx| editor.is_dirty(cx)));
10408
10409 let save = editor
10410 .update_in(cx, |editor, window, cx| {
10411 editor.save(
10412 SaveOptions {
10413 format: true,
10414 autosave: false,
10415 },
10416 project.clone(),
10417 window,
10418 cx,
10419 )
10420 })
10421 .unwrap();
10422 fake_server
10423 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10424 assert_eq!(
10425 params.text_document.uri,
10426 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10427 );
10428 assert_eq!(params.options.tab_size, 4);
10429 Ok(Some(vec![lsp::TextEdit::new(
10430 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10431 ", ".to_string(),
10432 )]))
10433 })
10434 .next()
10435 .await;
10436 cx.executor().start_waiting();
10437 save.await;
10438 assert_eq!(
10439 editor.update(cx, |editor, cx| editor.text(cx)),
10440 "one, two\nthree\n"
10441 );
10442 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10443}
10444
10445#[gpui::test]
10446async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
10447 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10448
10449 editor.update_in(cx, |editor, window, cx| {
10450 editor.set_text("one\ntwo\nthree\n", window, cx)
10451 });
10452 assert!(cx.read(|cx| editor.is_dirty(cx)));
10453
10454 // Test that save still works when formatting hangs
10455 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
10456 move |params, _| async move {
10457 assert_eq!(
10458 params.text_document.uri,
10459 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10460 );
10461 futures::future::pending::<()>().await;
10462 unreachable!()
10463 },
10464 );
10465 let save = editor
10466 .update_in(cx, |editor, window, cx| {
10467 editor.save(
10468 SaveOptions {
10469 format: true,
10470 autosave: false,
10471 },
10472 project.clone(),
10473 window,
10474 cx,
10475 )
10476 })
10477 .unwrap();
10478 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10479 cx.executor().start_waiting();
10480 save.await;
10481 assert_eq!(
10482 editor.update(cx, |editor, cx| editor.text(cx)),
10483 "one\ntwo\nthree\n"
10484 );
10485 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10486}
10487
10488#[gpui::test]
10489async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
10490 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10491
10492 // Buffer starts clean, no formatting should be requested
10493 let save = editor
10494 .update_in(cx, |editor, window, cx| {
10495 editor.save(
10496 SaveOptions {
10497 format: false,
10498 autosave: false,
10499 },
10500 project.clone(),
10501 window,
10502 cx,
10503 )
10504 })
10505 .unwrap();
10506 let _pending_format_request = fake_server
10507 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
10508 panic!("Should not be invoked");
10509 })
10510 .next();
10511 cx.executor().start_waiting();
10512 save.await;
10513 cx.run_until_parked();
10514}
10515
10516#[gpui::test]
10517async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
10518 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10519
10520 // Set Rust language override and assert overridden tabsize is sent to language server
10521 update_test_language_settings(cx, |settings| {
10522 settings.languages.0.insert(
10523 "Rust".into(),
10524 LanguageSettingsContent {
10525 tab_size: NonZeroU32::new(8),
10526 ..Default::default()
10527 },
10528 );
10529 });
10530
10531 editor.update_in(cx, |editor, window, cx| {
10532 editor.set_text("something_new\n", window, cx)
10533 });
10534 assert!(cx.read(|cx| editor.is_dirty(cx)));
10535 let save = editor
10536 .update_in(cx, |editor, window, cx| {
10537 editor.save(
10538 SaveOptions {
10539 format: true,
10540 autosave: false,
10541 },
10542 project.clone(),
10543 window,
10544 cx,
10545 )
10546 })
10547 .unwrap();
10548 fake_server
10549 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10550 assert_eq!(
10551 params.text_document.uri,
10552 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10553 );
10554 assert_eq!(params.options.tab_size, 8);
10555 Ok(Some(Vec::new()))
10556 })
10557 .next()
10558 .await;
10559 save.await;
10560}
10561
10562#[gpui::test]
10563async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
10564 init_test(cx, |settings| {
10565 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
10566 Formatter::LanguageServer { name: None },
10567 )))
10568 });
10569
10570 let fs = FakeFs::new(cx.executor());
10571 fs.insert_file(path!("/file.rs"), Default::default()).await;
10572
10573 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10574
10575 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10576 language_registry.add(Arc::new(Language::new(
10577 LanguageConfig {
10578 name: "Rust".into(),
10579 matcher: LanguageMatcher {
10580 path_suffixes: vec!["rs".to_string()],
10581 ..Default::default()
10582 },
10583 ..LanguageConfig::default()
10584 },
10585 Some(tree_sitter_rust::LANGUAGE.into()),
10586 )));
10587 update_test_language_settings(cx, |settings| {
10588 // Enable Prettier formatting for the same buffer, and ensure
10589 // LSP is called instead of Prettier.
10590 settings.defaults.prettier = Some(PrettierSettings {
10591 allowed: true,
10592 ..PrettierSettings::default()
10593 });
10594 });
10595 let mut fake_servers = language_registry.register_fake_lsp(
10596 "Rust",
10597 FakeLspAdapter {
10598 capabilities: lsp::ServerCapabilities {
10599 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10600 ..Default::default()
10601 },
10602 ..Default::default()
10603 },
10604 );
10605
10606 let buffer = project
10607 .update(cx, |project, cx| {
10608 project.open_local_buffer(path!("/file.rs"), cx)
10609 })
10610 .await
10611 .unwrap();
10612
10613 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10614 let (editor, cx) = cx.add_window_view(|window, cx| {
10615 build_editor_with_project(project.clone(), buffer, window, cx)
10616 });
10617 editor.update_in(cx, |editor, window, cx| {
10618 editor.set_text("one\ntwo\nthree\n", window, cx)
10619 });
10620
10621 cx.executor().start_waiting();
10622 let fake_server = fake_servers.next().await.unwrap();
10623
10624 let format = editor
10625 .update_in(cx, |editor, window, cx| {
10626 editor.perform_format(
10627 project.clone(),
10628 FormatTrigger::Manual,
10629 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10630 window,
10631 cx,
10632 )
10633 })
10634 .unwrap();
10635 fake_server
10636 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10637 assert_eq!(
10638 params.text_document.uri,
10639 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10640 );
10641 assert_eq!(params.options.tab_size, 4);
10642 Ok(Some(vec![lsp::TextEdit::new(
10643 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10644 ", ".to_string(),
10645 )]))
10646 })
10647 .next()
10648 .await;
10649 cx.executor().start_waiting();
10650 format.await;
10651 assert_eq!(
10652 editor.update(cx, |editor, cx| editor.text(cx)),
10653 "one, two\nthree\n"
10654 );
10655
10656 editor.update_in(cx, |editor, window, cx| {
10657 editor.set_text("one\ntwo\nthree\n", window, cx)
10658 });
10659 // Ensure we don't lock if formatting hangs.
10660 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10661 move |params, _| async move {
10662 assert_eq!(
10663 params.text_document.uri,
10664 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10665 );
10666 futures::future::pending::<()>().await;
10667 unreachable!()
10668 },
10669 );
10670 let format = editor
10671 .update_in(cx, |editor, window, cx| {
10672 editor.perform_format(
10673 project,
10674 FormatTrigger::Manual,
10675 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10676 window,
10677 cx,
10678 )
10679 })
10680 .unwrap();
10681 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10682 cx.executor().start_waiting();
10683 format.await;
10684 assert_eq!(
10685 editor.update(cx, |editor, cx| editor.text(cx)),
10686 "one\ntwo\nthree\n"
10687 );
10688}
10689
10690#[gpui::test]
10691async fn test_multiple_formatters(cx: &mut TestAppContext) {
10692 init_test(cx, |settings| {
10693 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
10694 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10695 Formatter::LanguageServer { name: None },
10696 Formatter::CodeActions(
10697 [
10698 ("code-action-1".into(), true),
10699 ("code-action-2".into(), true),
10700 ]
10701 .into_iter()
10702 .collect(),
10703 ),
10704 ])))
10705 });
10706
10707 let fs = FakeFs::new(cx.executor());
10708 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
10709 .await;
10710
10711 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10712 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10713 language_registry.add(rust_lang());
10714
10715 let mut fake_servers = language_registry.register_fake_lsp(
10716 "Rust",
10717 FakeLspAdapter {
10718 capabilities: lsp::ServerCapabilities {
10719 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10720 execute_command_provider: Some(lsp::ExecuteCommandOptions {
10721 commands: vec!["the-command-for-code-action-1".into()],
10722 ..Default::default()
10723 }),
10724 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10725 ..Default::default()
10726 },
10727 ..Default::default()
10728 },
10729 );
10730
10731 let buffer = project
10732 .update(cx, |project, cx| {
10733 project.open_local_buffer(path!("/file.rs"), cx)
10734 })
10735 .await
10736 .unwrap();
10737
10738 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10739 let (editor, cx) = cx.add_window_view(|window, cx| {
10740 build_editor_with_project(project.clone(), buffer, window, cx)
10741 });
10742
10743 cx.executor().start_waiting();
10744
10745 let fake_server = fake_servers.next().await.unwrap();
10746 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10747 move |_params, _| async move {
10748 Ok(Some(vec![lsp::TextEdit::new(
10749 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10750 "applied-formatting\n".to_string(),
10751 )]))
10752 },
10753 );
10754 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10755 move |params, _| async move {
10756 assert_eq!(
10757 params.context.only,
10758 Some(vec!["code-action-1".into(), "code-action-2".into()])
10759 );
10760 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
10761 Ok(Some(vec![
10762 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10763 kind: Some("code-action-1".into()),
10764 edit: Some(lsp::WorkspaceEdit::new(
10765 [(
10766 uri.clone(),
10767 vec![lsp::TextEdit::new(
10768 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10769 "applied-code-action-1-edit\n".to_string(),
10770 )],
10771 )]
10772 .into_iter()
10773 .collect(),
10774 )),
10775 command: Some(lsp::Command {
10776 command: "the-command-for-code-action-1".into(),
10777 ..Default::default()
10778 }),
10779 ..Default::default()
10780 }),
10781 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10782 kind: Some("code-action-2".into()),
10783 edit: Some(lsp::WorkspaceEdit::new(
10784 [(
10785 uri,
10786 vec![lsp::TextEdit::new(
10787 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10788 "applied-code-action-2-edit\n".to_string(),
10789 )],
10790 )]
10791 .into_iter()
10792 .collect(),
10793 )),
10794 ..Default::default()
10795 }),
10796 ]))
10797 },
10798 );
10799
10800 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
10801 move |params, _| async move { Ok(params) }
10802 });
10803
10804 let command_lock = Arc::new(futures::lock::Mutex::new(()));
10805 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
10806 let fake = fake_server.clone();
10807 let lock = command_lock.clone();
10808 move |params, _| {
10809 assert_eq!(params.command, "the-command-for-code-action-1");
10810 let fake = fake.clone();
10811 let lock = lock.clone();
10812 async move {
10813 lock.lock().await;
10814 fake.server
10815 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
10816 label: None,
10817 edit: lsp::WorkspaceEdit {
10818 changes: Some(
10819 [(
10820 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
10821 vec![lsp::TextEdit {
10822 range: lsp::Range::new(
10823 lsp::Position::new(0, 0),
10824 lsp::Position::new(0, 0),
10825 ),
10826 new_text: "applied-code-action-1-command\n".into(),
10827 }],
10828 )]
10829 .into_iter()
10830 .collect(),
10831 ),
10832 ..Default::default()
10833 },
10834 })
10835 .await
10836 .into_response()
10837 .unwrap();
10838 Ok(Some(json!(null)))
10839 }
10840 }
10841 });
10842
10843 cx.executor().start_waiting();
10844 editor
10845 .update_in(cx, |editor, window, cx| {
10846 editor.perform_format(
10847 project.clone(),
10848 FormatTrigger::Manual,
10849 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10850 window,
10851 cx,
10852 )
10853 })
10854 .unwrap()
10855 .await;
10856 editor.update(cx, |editor, cx| {
10857 assert_eq!(
10858 editor.text(cx),
10859 r#"
10860 applied-code-action-2-edit
10861 applied-code-action-1-command
10862 applied-code-action-1-edit
10863 applied-formatting
10864 one
10865 two
10866 three
10867 "#
10868 .unindent()
10869 );
10870 });
10871
10872 editor.update_in(cx, |editor, window, cx| {
10873 editor.undo(&Default::default(), window, cx);
10874 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10875 });
10876
10877 // Perform a manual edit while waiting for an LSP command
10878 // that's being run as part of a formatting code action.
10879 let lock_guard = command_lock.lock().await;
10880 let format = editor
10881 .update_in(cx, |editor, window, cx| {
10882 editor.perform_format(
10883 project.clone(),
10884 FormatTrigger::Manual,
10885 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10886 window,
10887 cx,
10888 )
10889 })
10890 .unwrap();
10891 cx.run_until_parked();
10892 editor.update(cx, |editor, cx| {
10893 assert_eq!(
10894 editor.text(cx),
10895 r#"
10896 applied-code-action-1-edit
10897 applied-formatting
10898 one
10899 two
10900 three
10901 "#
10902 .unindent()
10903 );
10904
10905 editor.buffer.update(cx, |buffer, cx| {
10906 let ix = buffer.len(cx);
10907 buffer.edit([(ix..ix, "edited\n")], None, cx);
10908 });
10909 });
10910
10911 // Allow the LSP command to proceed. Because the buffer was edited,
10912 // the second code action will not be run.
10913 drop(lock_guard);
10914 format.await;
10915 editor.update_in(cx, |editor, window, cx| {
10916 assert_eq!(
10917 editor.text(cx),
10918 r#"
10919 applied-code-action-1-command
10920 applied-code-action-1-edit
10921 applied-formatting
10922 one
10923 two
10924 three
10925 edited
10926 "#
10927 .unindent()
10928 );
10929
10930 // The manual edit is undone first, because it is the last thing the user did
10931 // (even though the command completed afterwards).
10932 editor.undo(&Default::default(), window, cx);
10933 assert_eq!(
10934 editor.text(cx),
10935 r#"
10936 applied-code-action-1-command
10937 applied-code-action-1-edit
10938 applied-formatting
10939 one
10940 two
10941 three
10942 "#
10943 .unindent()
10944 );
10945
10946 // All the formatting (including the command, which completed after the manual edit)
10947 // is undone together.
10948 editor.undo(&Default::default(), window, cx);
10949 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10950 });
10951}
10952
10953#[gpui::test]
10954async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
10955 init_test(cx, |settings| {
10956 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10957 Formatter::LanguageServer { name: None },
10958 ])))
10959 });
10960
10961 let fs = FakeFs::new(cx.executor());
10962 fs.insert_file(path!("/file.ts"), Default::default()).await;
10963
10964 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10965
10966 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10967 language_registry.add(Arc::new(Language::new(
10968 LanguageConfig {
10969 name: "TypeScript".into(),
10970 matcher: LanguageMatcher {
10971 path_suffixes: vec!["ts".to_string()],
10972 ..Default::default()
10973 },
10974 ..LanguageConfig::default()
10975 },
10976 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10977 )));
10978 update_test_language_settings(cx, |settings| {
10979 settings.defaults.prettier = Some(PrettierSettings {
10980 allowed: true,
10981 ..PrettierSettings::default()
10982 });
10983 });
10984 let mut fake_servers = language_registry.register_fake_lsp(
10985 "TypeScript",
10986 FakeLspAdapter {
10987 capabilities: lsp::ServerCapabilities {
10988 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10989 ..Default::default()
10990 },
10991 ..Default::default()
10992 },
10993 );
10994
10995 let buffer = project
10996 .update(cx, |project, cx| {
10997 project.open_local_buffer(path!("/file.ts"), cx)
10998 })
10999 .await
11000 .unwrap();
11001
11002 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11003 let (editor, cx) = cx.add_window_view(|window, cx| {
11004 build_editor_with_project(project.clone(), buffer, window, cx)
11005 });
11006 editor.update_in(cx, |editor, window, cx| {
11007 editor.set_text(
11008 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
11009 window,
11010 cx,
11011 )
11012 });
11013
11014 cx.executor().start_waiting();
11015 let fake_server = fake_servers.next().await.unwrap();
11016
11017 let format = editor
11018 .update_in(cx, |editor, window, cx| {
11019 editor.perform_code_action_kind(
11020 project.clone(),
11021 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
11022 window,
11023 cx,
11024 )
11025 })
11026 .unwrap();
11027 fake_server
11028 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
11029 assert_eq!(
11030 params.text_document.uri,
11031 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
11032 );
11033 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
11034 lsp::CodeAction {
11035 title: "Organize Imports".to_string(),
11036 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
11037 edit: Some(lsp::WorkspaceEdit {
11038 changes: Some(
11039 [(
11040 params.text_document.uri.clone(),
11041 vec![lsp::TextEdit::new(
11042 lsp::Range::new(
11043 lsp::Position::new(1, 0),
11044 lsp::Position::new(2, 0),
11045 ),
11046 "".to_string(),
11047 )],
11048 )]
11049 .into_iter()
11050 .collect(),
11051 ),
11052 ..Default::default()
11053 }),
11054 ..Default::default()
11055 },
11056 )]))
11057 })
11058 .next()
11059 .await;
11060 cx.executor().start_waiting();
11061 format.await;
11062 assert_eq!(
11063 editor.update(cx, |editor, cx| editor.text(cx)),
11064 "import { a } from 'module';\n\nconst x = a;\n"
11065 );
11066
11067 editor.update_in(cx, |editor, window, cx| {
11068 editor.set_text(
11069 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
11070 window,
11071 cx,
11072 )
11073 });
11074 // Ensure we don't lock if code action hangs.
11075 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
11076 move |params, _| async move {
11077 assert_eq!(
11078 params.text_document.uri,
11079 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
11080 );
11081 futures::future::pending::<()>().await;
11082 unreachable!()
11083 },
11084 );
11085 let format = editor
11086 .update_in(cx, |editor, window, cx| {
11087 editor.perform_code_action_kind(
11088 project,
11089 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
11090 window,
11091 cx,
11092 )
11093 })
11094 .unwrap();
11095 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
11096 cx.executor().start_waiting();
11097 format.await;
11098 assert_eq!(
11099 editor.update(cx, |editor, cx| editor.text(cx)),
11100 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
11101 );
11102}
11103
11104#[gpui::test]
11105async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
11106 init_test(cx, |_| {});
11107
11108 let mut cx = EditorLspTestContext::new_rust(
11109 lsp::ServerCapabilities {
11110 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11111 ..Default::default()
11112 },
11113 cx,
11114 )
11115 .await;
11116
11117 cx.set_state(indoc! {"
11118 one.twoˇ
11119 "});
11120
11121 // The format request takes a long time. When it completes, it inserts
11122 // a newline and an indent before the `.`
11123 cx.lsp
11124 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
11125 let executor = cx.background_executor().clone();
11126 async move {
11127 executor.timer(Duration::from_millis(100)).await;
11128 Ok(Some(vec![lsp::TextEdit {
11129 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
11130 new_text: "\n ".into(),
11131 }]))
11132 }
11133 });
11134
11135 // Submit a format request.
11136 let format_1 = cx
11137 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
11138 .unwrap();
11139 cx.executor().run_until_parked();
11140
11141 // Submit a second format request.
11142 let format_2 = cx
11143 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
11144 .unwrap();
11145 cx.executor().run_until_parked();
11146
11147 // Wait for both format requests to complete
11148 cx.executor().advance_clock(Duration::from_millis(200));
11149 cx.executor().start_waiting();
11150 format_1.await.unwrap();
11151 cx.executor().start_waiting();
11152 format_2.await.unwrap();
11153
11154 // The formatting edits only happens once.
11155 cx.assert_editor_state(indoc! {"
11156 one
11157 .twoˇ
11158 "});
11159}
11160
11161#[gpui::test]
11162async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
11163 init_test(cx, |settings| {
11164 settings.defaults.formatter = Some(SelectedFormatter::Auto)
11165 });
11166
11167 let mut cx = EditorLspTestContext::new_rust(
11168 lsp::ServerCapabilities {
11169 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11170 ..Default::default()
11171 },
11172 cx,
11173 )
11174 .await;
11175
11176 // Set up a buffer white some trailing whitespace and no trailing newline.
11177 cx.set_state(
11178 &[
11179 "one ", //
11180 "twoˇ", //
11181 "three ", //
11182 "four", //
11183 ]
11184 .join("\n"),
11185 );
11186
11187 // Submit a format request.
11188 let format = cx
11189 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
11190 .unwrap();
11191
11192 // Record which buffer changes have been sent to the language server
11193 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
11194 cx.lsp
11195 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
11196 let buffer_changes = buffer_changes.clone();
11197 move |params, _| {
11198 buffer_changes.lock().extend(
11199 params
11200 .content_changes
11201 .into_iter()
11202 .map(|e| (e.range.unwrap(), e.text)),
11203 );
11204 }
11205 });
11206
11207 // Handle formatting requests to the language server.
11208 cx.lsp
11209 .set_request_handler::<lsp::request::Formatting, _, _>({
11210 let buffer_changes = buffer_changes.clone();
11211 move |_, _| {
11212 // When formatting is requested, trailing whitespace has already been stripped,
11213 // and the trailing newline has already been added.
11214 assert_eq!(
11215 &buffer_changes.lock()[1..],
11216 &[
11217 (
11218 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
11219 "".into()
11220 ),
11221 (
11222 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
11223 "".into()
11224 ),
11225 (
11226 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
11227 "\n".into()
11228 ),
11229 ]
11230 );
11231
11232 // Insert blank lines between each line of the buffer.
11233 async move {
11234 Ok(Some(vec![
11235 lsp::TextEdit {
11236 range: lsp::Range::new(
11237 lsp::Position::new(1, 0),
11238 lsp::Position::new(1, 0),
11239 ),
11240 new_text: "\n".into(),
11241 },
11242 lsp::TextEdit {
11243 range: lsp::Range::new(
11244 lsp::Position::new(2, 0),
11245 lsp::Position::new(2, 0),
11246 ),
11247 new_text: "\n".into(),
11248 },
11249 ]))
11250 }
11251 }
11252 });
11253
11254 // After formatting the buffer, the trailing whitespace is stripped,
11255 // a newline is appended, and the edits provided by the language server
11256 // have been applied.
11257 format.await.unwrap();
11258 cx.assert_editor_state(
11259 &[
11260 "one", //
11261 "", //
11262 "twoˇ", //
11263 "", //
11264 "three", //
11265 "four", //
11266 "", //
11267 ]
11268 .join("\n"),
11269 );
11270
11271 // Undoing the formatting undoes the trailing whitespace removal, the
11272 // trailing newline, and the LSP edits.
11273 cx.update_buffer(|buffer, cx| buffer.undo(cx));
11274 cx.assert_editor_state(
11275 &[
11276 "one ", //
11277 "twoˇ", //
11278 "three ", //
11279 "four", //
11280 ]
11281 .join("\n"),
11282 );
11283}
11284
11285#[gpui::test]
11286async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
11287 cx: &mut TestAppContext,
11288) {
11289 init_test(cx, |_| {});
11290
11291 cx.update(|cx| {
11292 cx.update_global::<SettingsStore, _>(|settings, cx| {
11293 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11294 settings.auto_signature_help = Some(true);
11295 });
11296 });
11297 });
11298
11299 let mut cx = EditorLspTestContext::new_rust(
11300 lsp::ServerCapabilities {
11301 signature_help_provider: Some(lsp::SignatureHelpOptions {
11302 ..Default::default()
11303 }),
11304 ..Default::default()
11305 },
11306 cx,
11307 )
11308 .await;
11309
11310 let language = Language::new(
11311 LanguageConfig {
11312 name: "Rust".into(),
11313 brackets: BracketPairConfig {
11314 pairs: vec![
11315 BracketPair {
11316 start: "{".to_string(),
11317 end: "}".to_string(),
11318 close: true,
11319 surround: true,
11320 newline: true,
11321 },
11322 BracketPair {
11323 start: "(".to_string(),
11324 end: ")".to_string(),
11325 close: true,
11326 surround: true,
11327 newline: true,
11328 },
11329 BracketPair {
11330 start: "/*".to_string(),
11331 end: " */".to_string(),
11332 close: true,
11333 surround: true,
11334 newline: true,
11335 },
11336 BracketPair {
11337 start: "[".to_string(),
11338 end: "]".to_string(),
11339 close: false,
11340 surround: false,
11341 newline: true,
11342 },
11343 BracketPair {
11344 start: "\"".to_string(),
11345 end: "\"".to_string(),
11346 close: true,
11347 surround: true,
11348 newline: false,
11349 },
11350 BracketPair {
11351 start: "<".to_string(),
11352 end: ">".to_string(),
11353 close: false,
11354 surround: true,
11355 newline: true,
11356 },
11357 ],
11358 ..Default::default()
11359 },
11360 autoclose_before: "})]".to_string(),
11361 ..Default::default()
11362 },
11363 Some(tree_sitter_rust::LANGUAGE.into()),
11364 );
11365 let language = Arc::new(language);
11366
11367 cx.language_registry().add(language.clone());
11368 cx.update_buffer(|buffer, cx| {
11369 buffer.set_language(Some(language), cx);
11370 });
11371
11372 cx.set_state(
11373 &r#"
11374 fn main() {
11375 sampleˇ
11376 }
11377 "#
11378 .unindent(),
11379 );
11380
11381 cx.update_editor(|editor, window, cx| {
11382 editor.handle_input("(", window, cx);
11383 });
11384 cx.assert_editor_state(
11385 &"
11386 fn main() {
11387 sample(ˇ)
11388 }
11389 "
11390 .unindent(),
11391 );
11392
11393 let mocked_response = lsp::SignatureHelp {
11394 signatures: vec![lsp::SignatureInformation {
11395 label: "fn sample(param1: u8, param2: u8)".to_string(),
11396 documentation: None,
11397 parameters: Some(vec![
11398 lsp::ParameterInformation {
11399 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11400 documentation: None,
11401 },
11402 lsp::ParameterInformation {
11403 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11404 documentation: None,
11405 },
11406 ]),
11407 active_parameter: None,
11408 }],
11409 active_signature: Some(0),
11410 active_parameter: Some(0),
11411 };
11412 handle_signature_help_request(&mut cx, mocked_response).await;
11413
11414 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11415 .await;
11416
11417 cx.editor(|editor, _, _| {
11418 let signature_help_state = editor.signature_help_state.popover().cloned();
11419 let signature = signature_help_state.unwrap();
11420 assert_eq!(
11421 signature.signatures[signature.current_signature].label,
11422 "fn sample(param1: u8, param2: u8)"
11423 );
11424 });
11425}
11426
11427#[gpui::test]
11428async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
11429 init_test(cx, |_| {});
11430
11431 cx.update(|cx| {
11432 cx.update_global::<SettingsStore, _>(|settings, cx| {
11433 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11434 settings.auto_signature_help = Some(false);
11435 settings.show_signature_help_after_edits = Some(false);
11436 });
11437 });
11438 });
11439
11440 let mut cx = EditorLspTestContext::new_rust(
11441 lsp::ServerCapabilities {
11442 signature_help_provider: Some(lsp::SignatureHelpOptions {
11443 ..Default::default()
11444 }),
11445 ..Default::default()
11446 },
11447 cx,
11448 )
11449 .await;
11450
11451 let language = Language::new(
11452 LanguageConfig {
11453 name: "Rust".into(),
11454 brackets: BracketPairConfig {
11455 pairs: vec![
11456 BracketPair {
11457 start: "{".to_string(),
11458 end: "}".to_string(),
11459 close: true,
11460 surround: true,
11461 newline: true,
11462 },
11463 BracketPair {
11464 start: "(".to_string(),
11465 end: ")".to_string(),
11466 close: true,
11467 surround: true,
11468 newline: true,
11469 },
11470 BracketPair {
11471 start: "/*".to_string(),
11472 end: " */".to_string(),
11473 close: true,
11474 surround: true,
11475 newline: true,
11476 },
11477 BracketPair {
11478 start: "[".to_string(),
11479 end: "]".to_string(),
11480 close: false,
11481 surround: false,
11482 newline: true,
11483 },
11484 BracketPair {
11485 start: "\"".to_string(),
11486 end: "\"".to_string(),
11487 close: true,
11488 surround: true,
11489 newline: false,
11490 },
11491 BracketPair {
11492 start: "<".to_string(),
11493 end: ">".to_string(),
11494 close: false,
11495 surround: true,
11496 newline: true,
11497 },
11498 ],
11499 ..Default::default()
11500 },
11501 autoclose_before: "})]".to_string(),
11502 ..Default::default()
11503 },
11504 Some(tree_sitter_rust::LANGUAGE.into()),
11505 );
11506 let language = Arc::new(language);
11507
11508 cx.language_registry().add(language.clone());
11509 cx.update_buffer(|buffer, cx| {
11510 buffer.set_language(Some(language), cx);
11511 });
11512
11513 // Ensure that signature_help is not called when no signature help is enabled.
11514 cx.set_state(
11515 &r#"
11516 fn main() {
11517 sampleˇ
11518 }
11519 "#
11520 .unindent(),
11521 );
11522 cx.update_editor(|editor, window, cx| {
11523 editor.handle_input("(", window, cx);
11524 });
11525 cx.assert_editor_state(
11526 &"
11527 fn main() {
11528 sample(ˇ)
11529 }
11530 "
11531 .unindent(),
11532 );
11533 cx.editor(|editor, _, _| {
11534 assert!(editor.signature_help_state.task().is_none());
11535 });
11536
11537 let mocked_response = lsp::SignatureHelp {
11538 signatures: vec![lsp::SignatureInformation {
11539 label: "fn sample(param1: u8, param2: u8)".to_string(),
11540 documentation: None,
11541 parameters: Some(vec![
11542 lsp::ParameterInformation {
11543 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11544 documentation: None,
11545 },
11546 lsp::ParameterInformation {
11547 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11548 documentation: None,
11549 },
11550 ]),
11551 active_parameter: None,
11552 }],
11553 active_signature: Some(0),
11554 active_parameter: Some(0),
11555 };
11556
11557 // Ensure that signature_help is called when enabled afte edits
11558 cx.update(|_, cx| {
11559 cx.update_global::<SettingsStore, _>(|settings, cx| {
11560 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11561 settings.auto_signature_help = Some(false);
11562 settings.show_signature_help_after_edits = Some(true);
11563 });
11564 });
11565 });
11566 cx.set_state(
11567 &r#"
11568 fn main() {
11569 sampleˇ
11570 }
11571 "#
11572 .unindent(),
11573 );
11574 cx.update_editor(|editor, window, cx| {
11575 editor.handle_input("(", window, cx);
11576 });
11577 cx.assert_editor_state(
11578 &"
11579 fn main() {
11580 sample(ˇ)
11581 }
11582 "
11583 .unindent(),
11584 );
11585 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11586 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11587 .await;
11588 cx.update_editor(|editor, _, _| {
11589 let signature_help_state = editor.signature_help_state.popover().cloned();
11590 assert!(signature_help_state.is_some());
11591 let signature = signature_help_state.unwrap();
11592 assert_eq!(
11593 signature.signatures[signature.current_signature].label,
11594 "fn sample(param1: u8, param2: u8)"
11595 );
11596 editor.signature_help_state = SignatureHelpState::default();
11597 });
11598
11599 // Ensure that signature_help is called when auto signature help override is enabled
11600 cx.update(|_, cx| {
11601 cx.update_global::<SettingsStore, _>(|settings, cx| {
11602 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11603 settings.auto_signature_help = Some(true);
11604 settings.show_signature_help_after_edits = Some(false);
11605 });
11606 });
11607 });
11608 cx.set_state(
11609 &r#"
11610 fn main() {
11611 sampleˇ
11612 }
11613 "#
11614 .unindent(),
11615 );
11616 cx.update_editor(|editor, window, cx| {
11617 editor.handle_input("(", window, cx);
11618 });
11619 cx.assert_editor_state(
11620 &"
11621 fn main() {
11622 sample(ˇ)
11623 }
11624 "
11625 .unindent(),
11626 );
11627 handle_signature_help_request(&mut cx, mocked_response).await;
11628 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11629 .await;
11630 cx.editor(|editor, _, _| {
11631 let signature_help_state = editor.signature_help_state.popover().cloned();
11632 assert!(signature_help_state.is_some());
11633 let signature = signature_help_state.unwrap();
11634 assert_eq!(
11635 signature.signatures[signature.current_signature].label,
11636 "fn sample(param1: u8, param2: u8)"
11637 );
11638 });
11639}
11640
11641#[gpui::test]
11642async fn test_signature_help(cx: &mut TestAppContext) {
11643 init_test(cx, |_| {});
11644 cx.update(|cx| {
11645 cx.update_global::<SettingsStore, _>(|settings, cx| {
11646 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11647 settings.auto_signature_help = Some(true);
11648 });
11649 });
11650 });
11651
11652 let mut cx = EditorLspTestContext::new_rust(
11653 lsp::ServerCapabilities {
11654 signature_help_provider: Some(lsp::SignatureHelpOptions {
11655 ..Default::default()
11656 }),
11657 ..Default::default()
11658 },
11659 cx,
11660 )
11661 .await;
11662
11663 // A test that directly calls `show_signature_help`
11664 cx.update_editor(|editor, window, cx| {
11665 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11666 });
11667
11668 let mocked_response = lsp::SignatureHelp {
11669 signatures: vec![lsp::SignatureInformation {
11670 label: "fn sample(param1: u8, param2: u8)".to_string(),
11671 documentation: None,
11672 parameters: Some(vec![
11673 lsp::ParameterInformation {
11674 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11675 documentation: None,
11676 },
11677 lsp::ParameterInformation {
11678 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11679 documentation: None,
11680 },
11681 ]),
11682 active_parameter: None,
11683 }],
11684 active_signature: Some(0),
11685 active_parameter: Some(0),
11686 };
11687 handle_signature_help_request(&mut cx, mocked_response).await;
11688
11689 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11690 .await;
11691
11692 cx.editor(|editor, _, _| {
11693 let signature_help_state = editor.signature_help_state.popover().cloned();
11694 assert!(signature_help_state.is_some());
11695 let signature = signature_help_state.unwrap();
11696 assert_eq!(
11697 signature.signatures[signature.current_signature].label,
11698 "fn sample(param1: u8, param2: u8)"
11699 );
11700 });
11701
11702 // When exiting outside from inside the brackets, `signature_help` is closed.
11703 cx.set_state(indoc! {"
11704 fn main() {
11705 sample(ˇ);
11706 }
11707
11708 fn sample(param1: u8, param2: u8) {}
11709 "});
11710
11711 cx.update_editor(|editor, window, cx| {
11712 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11713 s.select_ranges([0..0])
11714 });
11715 });
11716
11717 let mocked_response = lsp::SignatureHelp {
11718 signatures: Vec::new(),
11719 active_signature: None,
11720 active_parameter: None,
11721 };
11722 handle_signature_help_request(&mut cx, mocked_response).await;
11723
11724 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11725 .await;
11726
11727 cx.editor(|editor, _, _| {
11728 assert!(!editor.signature_help_state.is_shown());
11729 });
11730
11731 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
11732 cx.set_state(indoc! {"
11733 fn main() {
11734 sample(ˇ);
11735 }
11736
11737 fn sample(param1: u8, param2: u8) {}
11738 "});
11739
11740 let mocked_response = lsp::SignatureHelp {
11741 signatures: vec![lsp::SignatureInformation {
11742 label: "fn sample(param1: u8, param2: u8)".to_string(),
11743 documentation: None,
11744 parameters: Some(vec![
11745 lsp::ParameterInformation {
11746 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11747 documentation: None,
11748 },
11749 lsp::ParameterInformation {
11750 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11751 documentation: None,
11752 },
11753 ]),
11754 active_parameter: None,
11755 }],
11756 active_signature: Some(0),
11757 active_parameter: Some(0),
11758 };
11759 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11760 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11761 .await;
11762 cx.editor(|editor, _, _| {
11763 assert!(editor.signature_help_state.is_shown());
11764 });
11765
11766 // Restore the popover with more parameter input
11767 cx.set_state(indoc! {"
11768 fn main() {
11769 sample(param1, param2ˇ);
11770 }
11771
11772 fn sample(param1: u8, param2: u8) {}
11773 "});
11774
11775 let mocked_response = lsp::SignatureHelp {
11776 signatures: vec![lsp::SignatureInformation {
11777 label: "fn sample(param1: u8, param2: u8)".to_string(),
11778 documentation: None,
11779 parameters: Some(vec![
11780 lsp::ParameterInformation {
11781 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11782 documentation: None,
11783 },
11784 lsp::ParameterInformation {
11785 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11786 documentation: None,
11787 },
11788 ]),
11789 active_parameter: None,
11790 }],
11791 active_signature: Some(0),
11792 active_parameter: Some(1),
11793 };
11794 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11795 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11796 .await;
11797
11798 // When selecting a range, the popover is gone.
11799 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
11800 cx.update_editor(|editor, window, cx| {
11801 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11802 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11803 })
11804 });
11805 cx.assert_editor_state(indoc! {"
11806 fn main() {
11807 sample(param1, «ˇparam2»);
11808 }
11809
11810 fn sample(param1: u8, param2: u8) {}
11811 "});
11812 cx.editor(|editor, _, _| {
11813 assert!(!editor.signature_help_state.is_shown());
11814 });
11815
11816 // When unselecting again, the popover is back if within the brackets.
11817 cx.update_editor(|editor, window, cx| {
11818 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11819 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11820 })
11821 });
11822 cx.assert_editor_state(indoc! {"
11823 fn main() {
11824 sample(param1, ˇparam2);
11825 }
11826
11827 fn sample(param1: u8, param2: u8) {}
11828 "});
11829 handle_signature_help_request(&mut cx, mocked_response).await;
11830 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11831 .await;
11832 cx.editor(|editor, _, _| {
11833 assert!(editor.signature_help_state.is_shown());
11834 });
11835
11836 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
11837 cx.update_editor(|editor, window, cx| {
11838 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11839 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
11840 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11841 })
11842 });
11843 cx.assert_editor_state(indoc! {"
11844 fn main() {
11845 sample(param1, ˇparam2);
11846 }
11847
11848 fn sample(param1: u8, param2: u8) {}
11849 "});
11850
11851 let mocked_response = lsp::SignatureHelp {
11852 signatures: vec![lsp::SignatureInformation {
11853 label: "fn sample(param1: u8, param2: u8)".to_string(),
11854 documentation: None,
11855 parameters: Some(vec![
11856 lsp::ParameterInformation {
11857 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11858 documentation: None,
11859 },
11860 lsp::ParameterInformation {
11861 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11862 documentation: None,
11863 },
11864 ]),
11865 active_parameter: None,
11866 }],
11867 active_signature: Some(0),
11868 active_parameter: Some(1),
11869 };
11870 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11871 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11872 .await;
11873 cx.update_editor(|editor, _, cx| {
11874 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
11875 });
11876 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11877 .await;
11878 cx.update_editor(|editor, window, cx| {
11879 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11880 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11881 })
11882 });
11883 cx.assert_editor_state(indoc! {"
11884 fn main() {
11885 sample(param1, «ˇparam2»);
11886 }
11887
11888 fn sample(param1: u8, param2: u8) {}
11889 "});
11890 cx.update_editor(|editor, window, cx| {
11891 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11892 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11893 })
11894 });
11895 cx.assert_editor_state(indoc! {"
11896 fn main() {
11897 sample(param1, ˇparam2);
11898 }
11899
11900 fn sample(param1: u8, param2: u8) {}
11901 "});
11902 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
11903 .await;
11904}
11905
11906#[gpui::test]
11907async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
11908 init_test(cx, |_| {});
11909
11910 let mut cx = EditorLspTestContext::new_rust(
11911 lsp::ServerCapabilities {
11912 signature_help_provider: Some(lsp::SignatureHelpOptions {
11913 ..Default::default()
11914 }),
11915 ..Default::default()
11916 },
11917 cx,
11918 )
11919 .await;
11920
11921 cx.set_state(indoc! {"
11922 fn main() {
11923 overloadedˇ
11924 }
11925 "});
11926
11927 cx.update_editor(|editor, window, cx| {
11928 editor.handle_input("(", window, cx);
11929 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11930 });
11931
11932 // Mock response with 3 signatures
11933 let mocked_response = lsp::SignatureHelp {
11934 signatures: vec![
11935 lsp::SignatureInformation {
11936 label: "fn overloaded(x: i32)".to_string(),
11937 documentation: None,
11938 parameters: Some(vec![lsp::ParameterInformation {
11939 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11940 documentation: None,
11941 }]),
11942 active_parameter: None,
11943 },
11944 lsp::SignatureInformation {
11945 label: "fn overloaded(x: i32, y: i32)".to_string(),
11946 documentation: None,
11947 parameters: Some(vec![
11948 lsp::ParameterInformation {
11949 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11950 documentation: None,
11951 },
11952 lsp::ParameterInformation {
11953 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11954 documentation: None,
11955 },
11956 ]),
11957 active_parameter: None,
11958 },
11959 lsp::SignatureInformation {
11960 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
11961 documentation: None,
11962 parameters: Some(vec![
11963 lsp::ParameterInformation {
11964 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11965 documentation: None,
11966 },
11967 lsp::ParameterInformation {
11968 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11969 documentation: None,
11970 },
11971 lsp::ParameterInformation {
11972 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
11973 documentation: None,
11974 },
11975 ]),
11976 active_parameter: None,
11977 },
11978 ],
11979 active_signature: Some(1),
11980 active_parameter: Some(0),
11981 };
11982 handle_signature_help_request(&mut cx, mocked_response).await;
11983
11984 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11985 .await;
11986
11987 // Verify we have multiple signatures and the right one is selected
11988 cx.editor(|editor, _, _| {
11989 let popover = editor.signature_help_state.popover().cloned().unwrap();
11990 assert_eq!(popover.signatures.len(), 3);
11991 // active_signature was 1, so that should be the current
11992 assert_eq!(popover.current_signature, 1);
11993 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
11994 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
11995 assert_eq!(
11996 popover.signatures[2].label,
11997 "fn overloaded(x: i32, y: i32, z: i32)"
11998 );
11999 });
12000
12001 // Test navigation functionality
12002 cx.update_editor(|editor, window, cx| {
12003 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
12004 });
12005
12006 cx.editor(|editor, _, _| {
12007 let popover = editor.signature_help_state.popover().cloned().unwrap();
12008 assert_eq!(popover.current_signature, 2);
12009 });
12010
12011 // Test wrap around
12012 cx.update_editor(|editor, window, cx| {
12013 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
12014 });
12015
12016 cx.editor(|editor, _, _| {
12017 let popover = editor.signature_help_state.popover().cloned().unwrap();
12018 assert_eq!(popover.current_signature, 0);
12019 });
12020
12021 // Test previous navigation
12022 cx.update_editor(|editor, window, cx| {
12023 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
12024 });
12025
12026 cx.editor(|editor, _, _| {
12027 let popover = editor.signature_help_state.popover().cloned().unwrap();
12028 assert_eq!(popover.current_signature, 2);
12029 });
12030}
12031
12032#[gpui::test]
12033async fn test_completion_mode(cx: &mut TestAppContext) {
12034 init_test(cx, |_| {});
12035 let mut cx = EditorLspTestContext::new_rust(
12036 lsp::ServerCapabilities {
12037 completion_provider: Some(lsp::CompletionOptions {
12038 resolve_provider: Some(true),
12039 ..Default::default()
12040 }),
12041 ..Default::default()
12042 },
12043 cx,
12044 )
12045 .await;
12046
12047 struct Run {
12048 run_description: &'static str,
12049 initial_state: String,
12050 buffer_marked_text: String,
12051 completion_label: &'static str,
12052 completion_text: &'static str,
12053 expected_with_insert_mode: String,
12054 expected_with_replace_mode: String,
12055 expected_with_replace_subsequence_mode: String,
12056 expected_with_replace_suffix_mode: String,
12057 }
12058
12059 let runs = [
12060 Run {
12061 run_description: "Start of word matches completion text",
12062 initial_state: "before ediˇ after".into(),
12063 buffer_marked_text: "before <edi|> after".into(),
12064 completion_label: "editor",
12065 completion_text: "editor",
12066 expected_with_insert_mode: "before editorˇ after".into(),
12067 expected_with_replace_mode: "before editorˇ after".into(),
12068 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12069 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12070 },
12071 Run {
12072 run_description: "Accept same text at the middle of the word",
12073 initial_state: "before ediˇtor after".into(),
12074 buffer_marked_text: "before <edi|tor> after".into(),
12075 completion_label: "editor",
12076 completion_text: "editor",
12077 expected_with_insert_mode: "before editorˇtor after".into(),
12078 expected_with_replace_mode: "before editorˇ after".into(),
12079 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12080 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12081 },
12082 Run {
12083 run_description: "End of word matches completion text -- cursor at end",
12084 initial_state: "before torˇ after".into(),
12085 buffer_marked_text: "before <tor|> after".into(),
12086 completion_label: "editor",
12087 completion_text: "editor",
12088 expected_with_insert_mode: "before editorˇ after".into(),
12089 expected_with_replace_mode: "before editorˇ after".into(),
12090 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12091 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12092 },
12093 Run {
12094 run_description: "End of word matches completion text -- cursor at start",
12095 initial_state: "before ˇtor after".into(),
12096 buffer_marked_text: "before <|tor> after".into(),
12097 completion_label: "editor",
12098 completion_text: "editor",
12099 expected_with_insert_mode: "before editorˇtor after".into(),
12100 expected_with_replace_mode: "before editorˇ after".into(),
12101 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12102 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12103 },
12104 Run {
12105 run_description: "Prepend text containing whitespace",
12106 initial_state: "pˇfield: bool".into(),
12107 buffer_marked_text: "<p|field>: bool".into(),
12108 completion_label: "pub ",
12109 completion_text: "pub ",
12110 expected_with_insert_mode: "pub ˇfield: bool".into(),
12111 expected_with_replace_mode: "pub ˇ: bool".into(),
12112 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
12113 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
12114 },
12115 Run {
12116 run_description: "Add element to start of list",
12117 initial_state: "[element_ˇelement_2]".into(),
12118 buffer_marked_text: "[<element_|element_2>]".into(),
12119 completion_label: "element_1",
12120 completion_text: "element_1",
12121 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
12122 expected_with_replace_mode: "[element_1ˇ]".into(),
12123 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
12124 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
12125 },
12126 Run {
12127 run_description: "Add element to start of list -- first and second elements are equal",
12128 initial_state: "[elˇelement]".into(),
12129 buffer_marked_text: "[<el|element>]".into(),
12130 completion_label: "element",
12131 completion_text: "element",
12132 expected_with_insert_mode: "[elementˇelement]".into(),
12133 expected_with_replace_mode: "[elementˇ]".into(),
12134 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
12135 expected_with_replace_suffix_mode: "[elementˇ]".into(),
12136 },
12137 Run {
12138 run_description: "Ends with matching suffix",
12139 initial_state: "SubˇError".into(),
12140 buffer_marked_text: "<Sub|Error>".into(),
12141 completion_label: "SubscriptionError",
12142 completion_text: "SubscriptionError",
12143 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
12144 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
12145 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
12146 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
12147 },
12148 Run {
12149 run_description: "Suffix is a subsequence -- contiguous",
12150 initial_state: "SubˇErr".into(),
12151 buffer_marked_text: "<Sub|Err>".into(),
12152 completion_label: "SubscriptionError",
12153 completion_text: "SubscriptionError",
12154 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
12155 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
12156 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
12157 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
12158 },
12159 Run {
12160 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
12161 initial_state: "Suˇscrirr".into(),
12162 buffer_marked_text: "<Su|scrirr>".into(),
12163 completion_label: "SubscriptionError",
12164 completion_text: "SubscriptionError",
12165 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
12166 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
12167 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
12168 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
12169 },
12170 Run {
12171 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
12172 initial_state: "foo(indˇix)".into(),
12173 buffer_marked_text: "foo(<ind|ix>)".into(),
12174 completion_label: "node_index",
12175 completion_text: "node_index",
12176 expected_with_insert_mode: "foo(node_indexˇix)".into(),
12177 expected_with_replace_mode: "foo(node_indexˇ)".into(),
12178 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
12179 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
12180 },
12181 Run {
12182 run_description: "Replace range ends before cursor - should extend to cursor",
12183 initial_state: "before editˇo after".into(),
12184 buffer_marked_text: "before <{ed}>it|o after".into(),
12185 completion_label: "editor",
12186 completion_text: "editor",
12187 expected_with_insert_mode: "before editorˇo after".into(),
12188 expected_with_replace_mode: "before editorˇo after".into(),
12189 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
12190 expected_with_replace_suffix_mode: "before editorˇo after".into(),
12191 },
12192 Run {
12193 run_description: "Uses label for suffix matching",
12194 initial_state: "before ediˇtor after".into(),
12195 buffer_marked_text: "before <edi|tor> after".into(),
12196 completion_label: "editor",
12197 completion_text: "editor()",
12198 expected_with_insert_mode: "before editor()ˇtor after".into(),
12199 expected_with_replace_mode: "before editor()ˇ after".into(),
12200 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
12201 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
12202 },
12203 Run {
12204 run_description: "Case insensitive subsequence and suffix matching",
12205 initial_state: "before EDiˇtoR after".into(),
12206 buffer_marked_text: "before <EDi|toR> after".into(),
12207 completion_label: "editor",
12208 completion_text: "editor",
12209 expected_with_insert_mode: "before editorˇtoR after".into(),
12210 expected_with_replace_mode: "before editorˇ after".into(),
12211 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12212 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12213 },
12214 ];
12215
12216 for run in runs {
12217 let run_variations = [
12218 (LspInsertMode::Insert, run.expected_with_insert_mode),
12219 (LspInsertMode::Replace, run.expected_with_replace_mode),
12220 (
12221 LspInsertMode::ReplaceSubsequence,
12222 run.expected_with_replace_subsequence_mode,
12223 ),
12224 (
12225 LspInsertMode::ReplaceSuffix,
12226 run.expected_with_replace_suffix_mode,
12227 ),
12228 ];
12229
12230 for (lsp_insert_mode, expected_text) in run_variations {
12231 eprintln!(
12232 "run = {:?}, mode = {lsp_insert_mode:.?}",
12233 run.run_description,
12234 );
12235
12236 update_test_language_settings(&mut cx, |settings| {
12237 settings.defaults.completions = Some(CompletionSettings {
12238 lsp_insert_mode,
12239 words: WordsCompletionMode::Disabled,
12240 words_min_length: 0,
12241 lsp: true,
12242 lsp_fetch_timeout_ms: 0,
12243 });
12244 });
12245
12246 cx.set_state(&run.initial_state);
12247 cx.update_editor(|editor, window, cx| {
12248 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12249 });
12250
12251 let counter = Arc::new(AtomicUsize::new(0));
12252 handle_completion_request_with_insert_and_replace(
12253 &mut cx,
12254 &run.buffer_marked_text,
12255 vec![(run.completion_label, run.completion_text)],
12256 counter.clone(),
12257 )
12258 .await;
12259 cx.condition(|editor, _| editor.context_menu_visible())
12260 .await;
12261 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12262
12263 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12264 editor
12265 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12266 .unwrap()
12267 });
12268 cx.assert_editor_state(&expected_text);
12269 handle_resolve_completion_request(&mut cx, None).await;
12270 apply_additional_edits.await.unwrap();
12271 }
12272 }
12273}
12274
12275#[gpui::test]
12276async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
12277 init_test(cx, |_| {});
12278 let mut cx = EditorLspTestContext::new_rust(
12279 lsp::ServerCapabilities {
12280 completion_provider: Some(lsp::CompletionOptions {
12281 resolve_provider: Some(true),
12282 ..Default::default()
12283 }),
12284 ..Default::default()
12285 },
12286 cx,
12287 )
12288 .await;
12289
12290 let initial_state = "SubˇError";
12291 let buffer_marked_text = "<Sub|Error>";
12292 let completion_text = "SubscriptionError";
12293 let expected_with_insert_mode = "SubscriptionErrorˇError";
12294 let expected_with_replace_mode = "SubscriptionErrorˇ";
12295
12296 update_test_language_settings(&mut cx, |settings| {
12297 settings.defaults.completions = Some(CompletionSettings {
12298 words: WordsCompletionMode::Disabled,
12299 words_min_length: 0,
12300 // set the opposite here to ensure that the action is overriding the default behavior
12301 lsp_insert_mode: LspInsertMode::Insert,
12302 lsp: true,
12303 lsp_fetch_timeout_ms: 0,
12304 });
12305 });
12306
12307 cx.set_state(initial_state);
12308 cx.update_editor(|editor, window, cx| {
12309 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12310 });
12311
12312 let counter = Arc::new(AtomicUsize::new(0));
12313 handle_completion_request_with_insert_and_replace(
12314 &mut cx,
12315 buffer_marked_text,
12316 vec![(completion_text, completion_text)],
12317 counter.clone(),
12318 )
12319 .await;
12320 cx.condition(|editor, _| editor.context_menu_visible())
12321 .await;
12322 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12323
12324 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12325 editor
12326 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12327 .unwrap()
12328 });
12329 cx.assert_editor_state(expected_with_replace_mode);
12330 handle_resolve_completion_request(&mut cx, None).await;
12331 apply_additional_edits.await.unwrap();
12332
12333 update_test_language_settings(&mut cx, |settings| {
12334 settings.defaults.completions = Some(CompletionSettings {
12335 words: WordsCompletionMode::Disabled,
12336 words_min_length: 0,
12337 // set the opposite here to ensure that the action is overriding the default behavior
12338 lsp_insert_mode: LspInsertMode::Replace,
12339 lsp: true,
12340 lsp_fetch_timeout_ms: 0,
12341 });
12342 });
12343
12344 cx.set_state(initial_state);
12345 cx.update_editor(|editor, window, cx| {
12346 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12347 });
12348 handle_completion_request_with_insert_and_replace(
12349 &mut cx,
12350 buffer_marked_text,
12351 vec![(completion_text, completion_text)],
12352 counter.clone(),
12353 )
12354 .await;
12355 cx.condition(|editor, _| editor.context_menu_visible())
12356 .await;
12357 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12358
12359 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12360 editor
12361 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
12362 .unwrap()
12363 });
12364 cx.assert_editor_state(expected_with_insert_mode);
12365 handle_resolve_completion_request(&mut cx, None).await;
12366 apply_additional_edits.await.unwrap();
12367}
12368
12369#[gpui::test]
12370async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
12371 init_test(cx, |_| {});
12372 let mut cx = EditorLspTestContext::new_rust(
12373 lsp::ServerCapabilities {
12374 completion_provider: Some(lsp::CompletionOptions {
12375 resolve_provider: Some(true),
12376 ..Default::default()
12377 }),
12378 ..Default::default()
12379 },
12380 cx,
12381 )
12382 .await;
12383
12384 // scenario: surrounding text matches completion text
12385 let completion_text = "to_offset";
12386 let initial_state = indoc! {"
12387 1. buf.to_offˇsuffix
12388 2. buf.to_offˇsuf
12389 3. buf.to_offˇfix
12390 4. buf.to_offˇ
12391 5. into_offˇensive
12392 6. ˇsuffix
12393 7. let ˇ //
12394 8. aaˇzz
12395 9. buf.to_off«zzzzzˇ»suffix
12396 10. buf.«ˇzzzzz»suffix
12397 11. to_off«ˇzzzzz»
12398
12399 buf.to_offˇsuffix // newest cursor
12400 "};
12401 let completion_marked_buffer = indoc! {"
12402 1. buf.to_offsuffix
12403 2. buf.to_offsuf
12404 3. buf.to_offfix
12405 4. buf.to_off
12406 5. into_offensive
12407 6. suffix
12408 7. let //
12409 8. aazz
12410 9. buf.to_offzzzzzsuffix
12411 10. buf.zzzzzsuffix
12412 11. to_offzzzzz
12413
12414 buf.<to_off|suffix> // newest cursor
12415 "};
12416 let expected = indoc! {"
12417 1. buf.to_offsetˇ
12418 2. buf.to_offsetˇsuf
12419 3. buf.to_offsetˇfix
12420 4. buf.to_offsetˇ
12421 5. into_offsetˇensive
12422 6. to_offsetˇsuffix
12423 7. let to_offsetˇ //
12424 8. aato_offsetˇzz
12425 9. buf.to_offsetˇ
12426 10. buf.to_offsetˇsuffix
12427 11. to_offsetˇ
12428
12429 buf.to_offsetˇ // newest cursor
12430 "};
12431 cx.set_state(initial_state);
12432 cx.update_editor(|editor, window, cx| {
12433 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12434 });
12435 handle_completion_request_with_insert_and_replace(
12436 &mut cx,
12437 completion_marked_buffer,
12438 vec![(completion_text, completion_text)],
12439 Arc::new(AtomicUsize::new(0)),
12440 )
12441 .await;
12442 cx.condition(|editor, _| editor.context_menu_visible())
12443 .await;
12444 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12445 editor
12446 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12447 .unwrap()
12448 });
12449 cx.assert_editor_state(expected);
12450 handle_resolve_completion_request(&mut cx, None).await;
12451 apply_additional_edits.await.unwrap();
12452
12453 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
12454 let completion_text = "foo_and_bar";
12455 let initial_state = indoc! {"
12456 1. ooanbˇ
12457 2. zooanbˇ
12458 3. ooanbˇz
12459 4. zooanbˇz
12460 5. ooanˇ
12461 6. oanbˇ
12462
12463 ooanbˇ
12464 "};
12465 let completion_marked_buffer = indoc! {"
12466 1. ooanb
12467 2. zooanb
12468 3. ooanbz
12469 4. zooanbz
12470 5. ooan
12471 6. oanb
12472
12473 <ooanb|>
12474 "};
12475 let expected = indoc! {"
12476 1. foo_and_barˇ
12477 2. zfoo_and_barˇ
12478 3. foo_and_barˇz
12479 4. zfoo_and_barˇz
12480 5. ooanfoo_and_barˇ
12481 6. oanbfoo_and_barˇ
12482
12483 foo_and_barˇ
12484 "};
12485 cx.set_state(initial_state);
12486 cx.update_editor(|editor, window, cx| {
12487 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12488 });
12489 handle_completion_request_with_insert_and_replace(
12490 &mut cx,
12491 completion_marked_buffer,
12492 vec![(completion_text, completion_text)],
12493 Arc::new(AtomicUsize::new(0)),
12494 )
12495 .await;
12496 cx.condition(|editor, _| editor.context_menu_visible())
12497 .await;
12498 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12499 editor
12500 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12501 .unwrap()
12502 });
12503 cx.assert_editor_state(expected);
12504 handle_resolve_completion_request(&mut cx, None).await;
12505 apply_additional_edits.await.unwrap();
12506
12507 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
12508 // (expects the same as if it was inserted at the end)
12509 let completion_text = "foo_and_bar";
12510 let initial_state = indoc! {"
12511 1. ooˇanb
12512 2. zooˇanb
12513 3. ooˇanbz
12514 4. zooˇanbz
12515
12516 ooˇanb
12517 "};
12518 let completion_marked_buffer = indoc! {"
12519 1. ooanb
12520 2. zooanb
12521 3. ooanbz
12522 4. zooanbz
12523
12524 <oo|anb>
12525 "};
12526 let expected = indoc! {"
12527 1. foo_and_barˇ
12528 2. zfoo_and_barˇ
12529 3. foo_and_barˇz
12530 4. zfoo_and_barˇz
12531
12532 foo_and_barˇ
12533 "};
12534 cx.set_state(initial_state);
12535 cx.update_editor(|editor, window, cx| {
12536 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12537 });
12538 handle_completion_request_with_insert_and_replace(
12539 &mut cx,
12540 completion_marked_buffer,
12541 vec![(completion_text, completion_text)],
12542 Arc::new(AtomicUsize::new(0)),
12543 )
12544 .await;
12545 cx.condition(|editor, _| editor.context_menu_visible())
12546 .await;
12547 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12548 editor
12549 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12550 .unwrap()
12551 });
12552 cx.assert_editor_state(expected);
12553 handle_resolve_completion_request(&mut cx, None).await;
12554 apply_additional_edits.await.unwrap();
12555}
12556
12557// This used to crash
12558#[gpui::test]
12559async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
12560 init_test(cx, |_| {});
12561
12562 let buffer_text = indoc! {"
12563 fn main() {
12564 10.satu;
12565
12566 //
12567 // separate cursors so they open in different excerpts (manually reproducible)
12568 //
12569
12570 10.satu20;
12571 }
12572 "};
12573 let multibuffer_text_with_selections = indoc! {"
12574 fn main() {
12575 10.satuˇ;
12576
12577 //
12578
12579 //
12580
12581 10.satuˇ20;
12582 }
12583 "};
12584 let expected_multibuffer = indoc! {"
12585 fn main() {
12586 10.saturating_sub()ˇ;
12587
12588 //
12589
12590 //
12591
12592 10.saturating_sub()ˇ;
12593 }
12594 "};
12595
12596 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
12597 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
12598
12599 let fs = FakeFs::new(cx.executor());
12600 fs.insert_tree(
12601 path!("/a"),
12602 json!({
12603 "main.rs": buffer_text,
12604 }),
12605 )
12606 .await;
12607
12608 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12609 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12610 language_registry.add(rust_lang());
12611 let mut fake_servers = language_registry.register_fake_lsp(
12612 "Rust",
12613 FakeLspAdapter {
12614 capabilities: lsp::ServerCapabilities {
12615 completion_provider: Some(lsp::CompletionOptions {
12616 resolve_provider: None,
12617 ..lsp::CompletionOptions::default()
12618 }),
12619 ..lsp::ServerCapabilities::default()
12620 },
12621 ..FakeLspAdapter::default()
12622 },
12623 );
12624 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12625 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12626 let buffer = project
12627 .update(cx, |project, cx| {
12628 project.open_local_buffer(path!("/a/main.rs"), cx)
12629 })
12630 .await
12631 .unwrap();
12632
12633 let multi_buffer = cx.new(|cx| {
12634 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
12635 multi_buffer.push_excerpts(
12636 buffer.clone(),
12637 [ExcerptRange::new(0..first_excerpt_end)],
12638 cx,
12639 );
12640 multi_buffer.push_excerpts(
12641 buffer.clone(),
12642 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
12643 cx,
12644 );
12645 multi_buffer
12646 });
12647
12648 let editor = workspace
12649 .update(cx, |_, window, cx| {
12650 cx.new(|cx| {
12651 Editor::new(
12652 EditorMode::Full {
12653 scale_ui_elements_with_buffer_font_size: false,
12654 show_active_line_background: false,
12655 sized_by_content: false,
12656 },
12657 multi_buffer.clone(),
12658 Some(project.clone()),
12659 window,
12660 cx,
12661 )
12662 })
12663 })
12664 .unwrap();
12665
12666 let pane = workspace
12667 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12668 .unwrap();
12669 pane.update_in(cx, |pane, window, cx| {
12670 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
12671 });
12672
12673 let fake_server = fake_servers.next().await.unwrap();
12674
12675 editor.update_in(cx, |editor, window, cx| {
12676 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12677 s.select_ranges([
12678 Point::new(1, 11)..Point::new(1, 11),
12679 Point::new(7, 11)..Point::new(7, 11),
12680 ])
12681 });
12682
12683 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
12684 });
12685
12686 editor.update_in(cx, |editor, window, cx| {
12687 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12688 });
12689
12690 fake_server
12691 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12692 let completion_item = lsp::CompletionItem {
12693 label: "saturating_sub()".into(),
12694 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
12695 lsp::InsertReplaceEdit {
12696 new_text: "saturating_sub()".to_owned(),
12697 insert: lsp::Range::new(
12698 lsp::Position::new(7, 7),
12699 lsp::Position::new(7, 11),
12700 ),
12701 replace: lsp::Range::new(
12702 lsp::Position::new(7, 7),
12703 lsp::Position::new(7, 13),
12704 ),
12705 },
12706 )),
12707 ..lsp::CompletionItem::default()
12708 };
12709
12710 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
12711 })
12712 .next()
12713 .await
12714 .unwrap();
12715
12716 cx.condition(&editor, |editor, _| editor.context_menu_visible())
12717 .await;
12718
12719 editor
12720 .update_in(cx, |editor, window, cx| {
12721 editor
12722 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12723 .unwrap()
12724 })
12725 .await
12726 .unwrap();
12727
12728 editor.update(cx, |editor, cx| {
12729 assert_text_with_selections(editor, expected_multibuffer, cx);
12730 })
12731}
12732
12733#[gpui::test]
12734async fn test_completion(cx: &mut TestAppContext) {
12735 init_test(cx, |_| {});
12736
12737 let mut cx = EditorLspTestContext::new_rust(
12738 lsp::ServerCapabilities {
12739 completion_provider: Some(lsp::CompletionOptions {
12740 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12741 resolve_provider: Some(true),
12742 ..Default::default()
12743 }),
12744 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12745 ..Default::default()
12746 },
12747 cx,
12748 )
12749 .await;
12750 let counter = Arc::new(AtomicUsize::new(0));
12751
12752 cx.set_state(indoc! {"
12753 oneˇ
12754 two
12755 three
12756 "});
12757 cx.simulate_keystroke(".");
12758 handle_completion_request(
12759 indoc! {"
12760 one.|<>
12761 two
12762 three
12763 "},
12764 vec!["first_completion", "second_completion"],
12765 true,
12766 counter.clone(),
12767 &mut cx,
12768 )
12769 .await;
12770 cx.condition(|editor, _| editor.context_menu_visible())
12771 .await;
12772 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12773
12774 let _handler = handle_signature_help_request(
12775 &mut cx,
12776 lsp::SignatureHelp {
12777 signatures: vec![lsp::SignatureInformation {
12778 label: "test signature".to_string(),
12779 documentation: None,
12780 parameters: Some(vec![lsp::ParameterInformation {
12781 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
12782 documentation: None,
12783 }]),
12784 active_parameter: None,
12785 }],
12786 active_signature: None,
12787 active_parameter: None,
12788 },
12789 );
12790 cx.update_editor(|editor, window, cx| {
12791 assert!(
12792 !editor.signature_help_state.is_shown(),
12793 "No signature help was called for"
12794 );
12795 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12796 });
12797 cx.run_until_parked();
12798 cx.update_editor(|editor, _, _| {
12799 assert!(
12800 !editor.signature_help_state.is_shown(),
12801 "No signature help should be shown when completions menu is open"
12802 );
12803 });
12804
12805 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12806 editor.context_menu_next(&Default::default(), window, cx);
12807 editor
12808 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12809 .unwrap()
12810 });
12811 cx.assert_editor_state(indoc! {"
12812 one.second_completionˇ
12813 two
12814 three
12815 "});
12816
12817 handle_resolve_completion_request(
12818 &mut cx,
12819 Some(vec![
12820 (
12821 //This overlaps with the primary completion edit which is
12822 //misbehavior from the LSP spec, test that we filter it out
12823 indoc! {"
12824 one.second_ˇcompletion
12825 two
12826 threeˇ
12827 "},
12828 "overlapping additional edit",
12829 ),
12830 (
12831 indoc! {"
12832 one.second_completion
12833 two
12834 threeˇ
12835 "},
12836 "\nadditional edit",
12837 ),
12838 ]),
12839 )
12840 .await;
12841 apply_additional_edits.await.unwrap();
12842 cx.assert_editor_state(indoc! {"
12843 one.second_completionˇ
12844 two
12845 three
12846 additional edit
12847 "});
12848
12849 cx.set_state(indoc! {"
12850 one.second_completion
12851 twoˇ
12852 threeˇ
12853 additional edit
12854 "});
12855 cx.simulate_keystroke(" ");
12856 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12857 cx.simulate_keystroke("s");
12858 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12859
12860 cx.assert_editor_state(indoc! {"
12861 one.second_completion
12862 two sˇ
12863 three sˇ
12864 additional edit
12865 "});
12866 handle_completion_request(
12867 indoc! {"
12868 one.second_completion
12869 two s
12870 three <s|>
12871 additional edit
12872 "},
12873 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12874 true,
12875 counter.clone(),
12876 &mut cx,
12877 )
12878 .await;
12879 cx.condition(|editor, _| editor.context_menu_visible())
12880 .await;
12881 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12882
12883 cx.simulate_keystroke("i");
12884
12885 handle_completion_request(
12886 indoc! {"
12887 one.second_completion
12888 two si
12889 three <si|>
12890 additional edit
12891 "},
12892 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12893 true,
12894 counter.clone(),
12895 &mut cx,
12896 )
12897 .await;
12898 cx.condition(|editor, _| editor.context_menu_visible())
12899 .await;
12900 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12901
12902 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12903 editor
12904 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12905 .unwrap()
12906 });
12907 cx.assert_editor_state(indoc! {"
12908 one.second_completion
12909 two sixth_completionˇ
12910 three sixth_completionˇ
12911 additional edit
12912 "});
12913
12914 apply_additional_edits.await.unwrap();
12915
12916 update_test_language_settings(&mut cx, |settings| {
12917 settings.defaults.show_completions_on_input = Some(false);
12918 });
12919 cx.set_state("editorˇ");
12920 cx.simulate_keystroke(".");
12921 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12922 cx.simulate_keystrokes("c l o");
12923 cx.assert_editor_state("editor.cloˇ");
12924 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12925 cx.update_editor(|editor, window, cx| {
12926 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12927 });
12928 handle_completion_request(
12929 "editor.<clo|>",
12930 vec!["close", "clobber"],
12931 true,
12932 counter.clone(),
12933 &mut cx,
12934 )
12935 .await;
12936 cx.condition(|editor, _| editor.context_menu_visible())
12937 .await;
12938 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
12939
12940 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12941 editor
12942 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12943 .unwrap()
12944 });
12945 cx.assert_editor_state("editor.clobberˇ");
12946 handle_resolve_completion_request(&mut cx, None).await;
12947 apply_additional_edits.await.unwrap();
12948}
12949
12950#[gpui::test]
12951async fn test_completion_reuse(cx: &mut TestAppContext) {
12952 init_test(cx, |_| {});
12953
12954 let mut cx = EditorLspTestContext::new_rust(
12955 lsp::ServerCapabilities {
12956 completion_provider: Some(lsp::CompletionOptions {
12957 trigger_characters: Some(vec![".".to_string()]),
12958 ..Default::default()
12959 }),
12960 ..Default::default()
12961 },
12962 cx,
12963 )
12964 .await;
12965
12966 let counter = Arc::new(AtomicUsize::new(0));
12967 cx.set_state("objˇ");
12968 cx.simulate_keystroke(".");
12969
12970 // Initial completion request returns complete results
12971 let is_incomplete = false;
12972 handle_completion_request(
12973 "obj.|<>",
12974 vec!["a", "ab", "abc"],
12975 is_incomplete,
12976 counter.clone(),
12977 &mut cx,
12978 )
12979 .await;
12980 cx.run_until_parked();
12981 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12982 cx.assert_editor_state("obj.ˇ");
12983 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12984
12985 // Type "a" - filters existing completions
12986 cx.simulate_keystroke("a");
12987 cx.run_until_parked();
12988 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12989 cx.assert_editor_state("obj.aˇ");
12990 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12991
12992 // Type "b" - filters existing completions
12993 cx.simulate_keystroke("b");
12994 cx.run_until_parked();
12995 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12996 cx.assert_editor_state("obj.abˇ");
12997 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12998
12999 // Type "c" - filters existing completions
13000 cx.simulate_keystroke("c");
13001 cx.run_until_parked();
13002 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13003 cx.assert_editor_state("obj.abcˇ");
13004 check_displayed_completions(vec!["abc"], &mut cx);
13005
13006 // Backspace to delete "c" - filters existing completions
13007 cx.update_editor(|editor, window, cx| {
13008 editor.backspace(&Backspace, window, cx);
13009 });
13010 cx.run_until_parked();
13011 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13012 cx.assert_editor_state("obj.abˇ");
13013 check_displayed_completions(vec!["ab", "abc"], &mut cx);
13014
13015 // Moving cursor to the left dismisses menu.
13016 cx.update_editor(|editor, window, cx| {
13017 editor.move_left(&MoveLeft, window, cx);
13018 });
13019 cx.run_until_parked();
13020 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13021 cx.assert_editor_state("obj.aˇb");
13022 cx.update_editor(|editor, _, _| {
13023 assert_eq!(editor.context_menu_visible(), false);
13024 });
13025
13026 // Type "b" - new request
13027 cx.simulate_keystroke("b");
13028 let is_incomplete = false;
13029 handle_completion_request(
13030 "obj.<ab|>a",
13031 vec!["ab", "abc"],
13032 is_incomplete,
13033 counter.clone(),
13034 &mut cx,
13035 )
13036 .await;
13037 cx.run_until_parked();
13038 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13039 cx.assert_editor_state("obj.abˇb");
13040 check_displayed_completions(vec!["ab", "abc"], &mut cx);
13041
13042 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
13043 cx.update_editor(|editor, window, cx| {
13044 editor.backspace(&Backspace, window, cx);
13045 });
13046 let is_incomplete = false;
13047 handle_completion_request(
13048 "obj.<a|>b",
13049 vec!["a", "ab", "abc"],
13050 is_incomplete,
13051 counter.clone(),
13052 &mut cx,
13053 )
13054 .await;
13055 cx.run_until_parked();
13056 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
13057 cx.assert_editor_state("obj.aˇb");
13058 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
13059
13060 // Backspace to delete "a" - dismisses menu.
13061 cx.update_editor(|editor, window, cx| {
13062 editor.backspace(&Backspace, window, cx);
13063 });
13064 cx.run_until_parked();
13065 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
13066 cx.assert_editor_state("obj.ˇb");
13067 cx.update_editor(|editor, _, _| {
13068 assert_eq!(editor.context_menu_visible(), false);
13069 });
13070}
13071
13072#[gpui::test]
13073async fn test_word_completion(cx: &mut TestAppContext) {
13074 let lsp_fetch_timeout_ms = 10;
13075 init_test(cx, |language_settings| {
13076 language_settings.defaults.completions = Some(CompletionSettings {
13077 words: WordsCompletionMode::Fallback,
13078 words_min_length: 0,
13079 lsp: true,
13080 lsp_fetch_timeout_ms: 10,
13081 lsp_insert_mode: LspInsertMode::Insert,
13082 });
13083 });
13084
13085 let mut cx = EditorLspTestContext::new_rust(
13086 lsp::ServerCapabilities {
13087 completion_provider: Some(lsp::CompletionOptions {
13088 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13089 ..lsp::CompletionOptions::default()
13090 }),
13091 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13092 ..lsp::ServerCapabilities::default()
13093 },
13094 cx,
13095 )
13096 .await;
13097
13098 let throttle_completions = Arc::new(AtomicBool::new(false));
13099
13100 let lsp_throttle_completions = throttle_completions.clone();
13101 let _completion_requests_handler =
13102 cx.lsp
13103 .server
13104 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
13105 let lsp_throttle_completions = lsp_throttle_completions.clone();
13106 let cx = cx.clone();
13107 async move {
13108 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
13109 cx.background_executor()
13110 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
13111 .await;
13112 }
13113 Ok(Some(lsp::CompletionResponse::Array(vec![
13114 lsp::CompletionItem {
13115 label: "first".into(),
13116 ..lsp::CompletionItem::default()
13117 },
13118 lsp::CompletionItem {
13119 label: "last".into(),
13120 ..lsp::CompletionItem::default()
13121 },
13122 ])))
13123 }
13124 });
13125
13126 cx.set_state(indoc! {"
13127 oneˇ
13128 two
13129 three
13130 "});
13131 cx.simulate_keystroke(".");
13132 cx.executor().run_until_parked();
13133 cx.condition(|editor, _| editor.context_menu_visible())
13134 .await;
13135 cx.update_editor(|editor, window, cx| {
13136 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13137 {
13138 assert_eq!(
13139 completion_menu_entries(menu),
13140 &["first", "last"],
13141 "When LSP server is fast to reply, no fallback word completions are used"
13142 );
13143 } else {
13144 panic!("expected completion menu to be open");
13145 }
13146 editor.cancel(&Cancel, window, cx);
13147 });
13148 cx.executor().run_until_parked();
13149 cx.condition(|editor, _| !editor.context_menu_visible())
13150 .await;
13151
13152 throttle_completions.store(true, atomic::Ordering::Release);
13153 cx.simulate_keystroke(".");
13154 cx.executor()
13155 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
13156 cx.executor().run_until_parked();
13157 cx.condition(|editor, _| editor.context_menu_visible())
13158 .await;
13159 cx.update_editor(|editor, _, _| {
13160 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13161 {
13162 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
13163 "When LSP server is slow, document words can be shown instead, if configured accordingly");
13164 } else {
13165 panic!("expected completion menu to be open");
13166 }
13167 });
13168}
13169
13170#[gpui::test]
13171async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
13172 init_test(cx, |language_settings| {
13173 language_settings.defaults.completions = Some(CompletionSettings {
13174 words: WordsCompletionMode::Enabled,
13175 words_min_length: 0,
13176 lsp: true,
13177 lsp_fetch_timeout_ms: 0,
13178 lsp_insert_mode: LspInsertMode::Insert,
13179 });
13180 });
13181
13182 let mut cx = EditorLspTestContext::new_rust(
13183 lsp::ServerCapabilities {
13184 completion_provider: Some(lsp::CompletionOptions {
13185 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13186 ..lsp::CompletionOptions::default()
13187 }),
13188 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13189 ..lsp::ServerCapabilities::default()
13190 },
13191 cx,
13192 )
13193 .await;
13194
13195 let _completion_requests_handler =
13196 cx.lsp
13197 .server
13198 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
13199 Ok(Some(lsp::CompletionResponse::Array(vec![
13200 lsp::CompletionItem {
13201 label: "first".into(),
13202 ..lsp::CompletionItem::default()
13203 },
13204 lsp::CompletionItem {
13205 label: "last".into(),
13206 ..lsp::CompletionItem::default()
13207 },
13208 ])))
13209 });
13210
13211 cx.set_state(indoc! {"ˇ
13212 first
13213 last
13214 second
13215 "});
13216 cx.simulate_keystroke(".");
13217 cx.executor().run_until_parked();
13218 cx.condition(|editor, _| editor.context_menu_visible())
13219 .await;
13220 cx.update_editor(|editor, _, _| {
13221 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13222 {
13223 assert_eq!(
13224 completion_menu_entries(menu),
13225 &["first", "last", "second"],
13226 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
13227 );
13228 } else {
13229 panic!("expected completion menu to be open");
13230 }
13231 });
13232}
13233
13234#[gpui::test]
13235async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
13236 init_test(cx, |language_settings| {
13237 language_settings.defaults.completions = Some(CompletionSettings {
13238 words: WordsCompletionMode::Disabled,
13239 words_min_length: 0,
13240 lsp: true,
13241 lsp_fetch_timeout_ms: 0,
13242 lsp_insert_mode: LspInsertMode::Insert,
13243 });
13244 });
13245
13246 let mut cx = EditorLspTestContext::new_rust(
13247 lsp::ServerCapabilities {
13248 completion_provider: Some(lsp::CompletionOptions {
13249 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13250 ..lsp::CompletionOptions::default()
13251 }),
13252 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13253 ..lsp::ServerCapabilities::default()
13254 },
13255 cx,
13256 )
13257 .await;
13258
13259 let _completion_requests_handler =
13260 cx.lsp
13261 .server
13262 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
13263 panic!("LSP completions should not be queried when dealing with word completions")
13264 });
13265
13266 cx.set_state(indoc! {"ˇ
13267 first
13268 last
13269 second
13270 "});
13271 cx.update_editor(|editor, window, cx| {
13272 editor.show_word_completions(&ShowWordCompletions, window, cx);
13273 });
13274 cx.executor().run_until_parked();
13275 cx.condition(|editor, _| editor.context_menu_visible())
13276 .await;
13277 cx.update_editor(|editor, _, _| {
13278 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13279 {
13280 assert_eq!(
13281 completion_menu_entries(menu),
13282 &["first", "last", "second"],
13283 "`ShowWordCompletions` action should show word completions"
13284 );
13285 } else {
13286 panic!("expected completion menu to be open");
13287 }
13288 });
13289
13290 cx.simulate_keystroke("l");
13291 cx.executor().run_until_parked();
13292 cx.condition(|editor, _| editor.context_menu_visible())
13293 .await;
13294 cx.update_editor(|editor, _, _| {
13295 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13296 {
13297 assert_eq!(
13298 completion_menu_entries(menu),
13299 &["last"],
13300 "After showing word completions, further editing should filter them and not query the LSP"
13301 );
13302 } else {
13303 panic!("expected completion menu to be open");
13304 }
13305 });
13306}
13307
13308#[gpui::test]
13309async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
13310 init_test(cx, |language_settings| {
13311 language_settings.defaults.completions = Some(CompletionSettings {
13312 words: WordsCompletionMode::Fallback,
13313 words_min_length: 0,
13314 lsp: false,
13315 lsp_fetch_timeout_ms: 0,
13316 lsp_insert_mode: LspInsertMode::Insert,
13317 });
13318 });
13319
13320 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13321
13322 cx.set_state(indoc! {"ˇ
13323 0_usize
13324 let
13325 33
13326 4.5f32
13327 "});
13328 cx.update_editor(|editor, window, cx| {
13329 editor.show_completions(&ShowCompletions::default(), window, cx);
13330 });
13331 cx.executor().run_until_parked();
13332 cx.condition(|editor, _| editor.context_menu_visible())
13333 .await;
13334 cx.update_editor(|editor, window, cx| {
13335 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13336 {
13337 assert_eq!(
13338 completion_menu_entries(menu),
13339 &["let"],
13340 "With no digits in the completion query, no digits should be in the word completions"
13341 );
13342 } else {
13343 panic!("expected completion menu to be open");
13344 }
13345 editor.cancel(&Cancel, window, cx);
13346 });
13347
13348 cx.set_state(indoc! {"3ˇ
13349 0_usize
13350 let
13351 3
13352 33.35f32
13353 "});
13354 cx.update_editor(|editor, window, cx| {
13355 editor.show_completions(&ShowCompletions::default(), window, cx);
13356 });
13357 cx.executor().run_until_parked();
13358 cx.condition(|editor, _| editor.context_menu_visible())
13359 .await;
13360 cx.update_editor(|editor, _, _| {
13361 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13362 {
13363 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
13364 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
13365 } else {
13366 panic!("expected completion menu to be open");
13367 }
13368 });
13369}
13370
13371#[gpui::test]
13372async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
13373 init_test(cx, |language_settings| {
13374 language_settings.defaults.completions = Some(CompletionSettings {
13375 words: WordsCompletionMode::Enabled,
13376 words_min_length: 3,
13377 lsp: true,
13378 lsp_fetch_timeout_ms: 0,
13379 lsp_insert_mode: LspInsertMode::Insert,
13380 });
13381 });
13382
13383 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13384 cx.set_state(indoc! {"ˇ
13385 wow
13386 wowen
13387 wowser
13388 "});
13389 cx.simulate_keystroke("w");
13390 cx.executor().run_until_parked();
13391 cx.update_editor(|editor, _, _| {
13392 if editor.context_menu.borrow_mut().is_some() {
13393 panic!(
13394 "expected completion menu to be hidden, as words completion threshold is not met"
13395 );
13396 }
13397 });
13398
13399 cx.simulate_keystroke("o");
13400 cx.executor().run_until_parked();
13401 cx.update_editor(|editor, _, _| {
13402 if editor.context_menu.borrow_mut().is_some() {
13403 panic!(
13404 "expected completion menu to be hidden, as words completion threshold is not met still"
13405 );
13406 }
13407 });
13408
13409 cx.simulate_keystroke("w");
13410 cx.executor().run_until_parked();
13411 cx.update_editor(|editor, _, _| {
13412 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13413 {
13414 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
13415 } else {
13416 panic!("expected completion menu to be open after the word completions threshold is met");
13417 }
13418 });
13419}
13420
13421fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
13422 let position = || lsp::Position {
13423 line: params.text_document_position.position.line,
13424 character: params.text_document_position.position.character,
13425 };
13426 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13427 range: lsp::Range {
13428 start: position(),
13429 end: position(),
13430 },
13431 new_text: text.to_string(),
13432 }))
13433}
13434
13435#[gpui::test]
13436async fn test_multiline_completion(cx: &mut TestAppContext) {
13437 init_test(cx, |_| {});
13438
13439 let fs = FakeFs::new(cx.executor());
13440 fs.insert_tree(
13441 path!("/a"),
13442 json!({
13443 "main.ts": "a",
13444 }),
13445 )
13446 .await;
13447
13448 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13449 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13450 let typescript_language = Arc::new(Language::new(
13451 LanguageConfig {
13452 name: "TypeScript".into(),
13453 matcher: LanguageMatcher {
13454 path_suffixes: vec!["ts".to_string()],
13455 ..LanguageMatcher::default()
13456 },
13457 line_comments: vec!["// ".into()],
13458 ..LanguageConfig::default()
13459 },
13460 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13461 ));
13462 language_registry.add(typescript_language.clone());
13463 let mut fake_servers = language_registry.register_fake_lsp(
13464 "TypeScript",
13465 FakeLspAdapter {
13466 capabilities: lsp::ServerCapabilities {
13467 completion_provider: Some(lsp::CompletionOptions {
13468 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13469 ..lsp::CompletionOptions::default()
13470 }),
13471 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13472 ..lsp::ServerCapabilities::default()
13473 },
13474 // Emulate vtsls label generation
13475 label_for_completion: Some(Box::new(|item, _| {
13476 let text = if let Some(description) = item
13477 .label_details
13478 .as_ref()
13479 .and_then(|label_details| label_details.description.as_ref())
13480 {
13481 format!("{} {}", item.label, description)
13482 } else if let Some(detail) = &item.detail {
13483 format!("{} {}", item.label, detail)
13484 } else {
13485 item.label.clone()
13486 };
13487 let len = text.len();
13488 Some(language::CodeLabel {
13489 text,
13490 runs: Vec::new(),
13491 filter_range: 0..len,
13492 })
13493 })),
13494 ..FakeLspAdapter::default()
13495 },
13496 );
13497 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13498 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13499 let worktree_id = workspace
13500 .update(cx, |workspace, _window, cx| {
13501 workspace.project().update(cx, |project, cx| {
13502 project.worktrees(cx).next().unwrap().read(cx).id()
13503 })
13504 })
13505 .unwrap();
13506 let _buffer = project
13507 .update(cx, |project, cx| {
13508 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
13509 })
13510 .await
13511 .unwrap();
13512 let editor = workspace
13513 .update(cx, |workspace, window, cx| {
13514 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
13515 })
13516 .unwrap()
13517 .await
13518 .unwrap()
13519 .downcast::<Editor>()
13520 .unwrap();
13521 let fake_server = fake_servers.next().await.unwrap();
13522
13523 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
13524 let multiline_label_2 = "a\nb\nc\n";
13525 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
13526 let multiline_description = "d\ne\nf\n";
13527 let multiline_detail_2 = "g\nh\ni\n";
13528
13529 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
13530 move |params, _| async move {
13531 Ok(Some(lsp::CompletionResponse::Array(vec![
13532 lsp::CompletionItem {
13533 label: multiline_label.to_string(),
13534 text_edit: gen_text_edit(¶ms, "new_text_1"),
13535 ..lsp::CompletionItem::default()
13536 },
13537 lsp::CompletionItem {
13538 label: "single line label 1".to_string(),
13539 detail: Some(multiline_detail.to_string()),
13540 text_edit: gen_text_edit(¶ms, "new_text_2"),
13541 ..lsp::CompletionItem::default()
13542 },
13543 lsp::CompletionItem {
13544 label: "single line label 2".to_string(),
13545 label_details: Some(lsp::CompletionItemLabelDetails {
13546 description: Some(multiline_description.to_string()),
13547 detail: None,
13548 }),
13549 text_edit: gen_text_edit(¶ms, "new_text_2"),
13550 ..lsp::CompletionItem::default()
13551 },
13552 lsp::CompletionItem {
13553 label: multiline_label_2.to_string(),
13554 detail: Some(multiline_detail_2.to_string()),
13555 text_edit: gen_text_edit(¶ms, "new_text_3"),
13556 ..lsp::CompletionItem::default()
13557 },
13558 lsp::CompletionItem {
13559 label: "Label with many spaces and \t but without newlines".to_string(),
13560 detail: Some(
13561 "Details with many spaces and \t but without newlines".to_string(),
13562 ),
13563 text_edit: gen_text_edit(¶ms, "new_text_4"),
13564 ..lsp::CompletionItem::default()
13565 },
13566 ])))
13567 },
13568 );
13569
13570 editor.update_in(cx, |editor, window, cx| {
13571 cx.focus_self(window);
13572 editor.move_to_end(&MoveToEnd, window, cx);
13573 editor.handle_input(".", window, cx);
13574 });
13575 cx.run_until_parked();
13576 completion_handle.next().await.unwrap();
13577
13578 editor.update(cx, |editor, _| {
13579 assert!(editor.context_menu_visible());
13580 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13581 {
13582 let completion_labels = menu
13583 .completions
13584 .borrow()
13585 .iter()
13586 .map(|c| c.label.text.clone())
13587 .collect::<Vec<_>>();
13588 assert_eq!(
13589 completion_labels,
13590 &[
13591 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
13592 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
13593 "single line label 2 d e f ",
13594 "a b c g h i ",
13595 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
13596 ],
13597 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
13598 );
13599
13600 for completion in menu
13601 .completions
13602 .borrow()
13603 .iter() {
13604 assert_eq!(
13605 completion.label.filter_range,
13606 0..completion.label.text.len(),
13607 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
13608 );
13609 }
13610 } else {
13611 panic!("expected completion menu to be open");
13612 }
13613 });
13614}
13615
13616#[gpui::test]
13617async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
13618 init_test(cx, |_| {});
13619 let mut cx = EditorLspTestContext::new_rust(
13620 lsp::ServerCapabilities {
13621 completion_provider: Some(lsp::CompletionOptions {
13622 trigger_characters: Some(vec![".".to_string()]),
13623 ..Default::default()
13624 }),
13625 ..Default::default()
13626 },
13627 cx,
13628 )
13629 .await;
13630 cx.lsp
13631 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13632 Ok(Some(lsp::CompletionResponse::Array(vec![
13633 lsp::CompletionItem {
13634 label: "first".into(),
13635 ..Default::default()
13636 },
13637 lsp::CompletionItem {
13638 label: "last".into(),
13639 ..Default::default()
13640 },
13641 ])))
13642 });
13643 cx.set_state("variableˇ");
13644 cx.simulate_keystroke(".");
13645 cx.executor().run_until_parked();
13646
13647 cx.update_editor(|editor, _, _| {
13648 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13649 {
13650 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
13651 } else {
13652 panic!("expected completion menu to be open");
13653 }
13654 });
13655
13656 cx.update_editor(|editor, window, cx| {
13657 editor.move_page_down(&MovePageDown::default(), window, cx);
13658 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13659 {
13660 assert!(
13661 menu.selected_item == 1,
13662 "expected PageDown to select the last item from the context menu"
13663 );
13664 } else {
13665 panic!("expected completion menu to stay open after PageDown");
13666 }
13667 });
13668
13669 cx.update_editor(|editor, window, cx| {
13670 editor.move_page_up(&MovePageUp::default(), window, cx);
13671 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13672 {
13673 assert!(
13674 menu.selected_item == 0,
13675 "expected PageUp to select the first item from the context menu"
13676 );
13677 } else {
13678 panic!("expected completion menu to stay open after PageUp");
13679 }
13680 });
13681}
13682
13683#[gpui::test]
13684async fn test_as_is_completions(cx: &mut TestAppContext) {
13685 init_test(cx, |_| {});
13686 let mut cx = EditorLspTestContext::new_rust(
13687 lsp::ServerCapabilities {
13688 completion_provider: Some(lsp::CompletionOptions {
13689 ..Default::default()
13690 }),
13691 ..Default::default()
13692 },
13693 cx,
13694 )
13695 .await;
13696 cx.lsp
13697 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13698 Ok(Some(lsp::CompletionResponse::Array(vec![
13699 lsp::CompletionItem {
13700 label: "unsafe".into(),
13701 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13702 range: lsp::Range {
13703 start: lsp::Position {
13704 line: 1,
13705 character: 2,
13706 },
13707 end: lsp::Position {
13708 line: 1,
13709 character: 3,
13710 },
13711 },
13712 new_text: "unsafe".to_string(),
13713 })),
13714 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
13715 ..Default::default()
13716 },
13717 ])))
13718 });
13719 cx.set_state("fn a() {}\n nˇ");
13720 cx.executor().run_until_parked();
13721 cx.update_editor(|editor, window, cx| {
13722 editor.show_completions(
13723 &ShowCompletions {
13724 trigger: Some("\n".into()),
13725 },
13726 window,
13727 cx,
13728 );
13729 });
13730 cx.executor().run_until_parked();
13731
13732 cx.update_editor(|editor, window, cx| {
13733 editor.confirm_completion(&Default::default(), window, cx)
13734 });
13735 cx.executor().run_until_parked();
13736 cx.assert_editor_state("fn a() {}\n unsafeˇ");
13737}
13738
13739#[gpui::test]
13740async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
13741 init_test(cx, |_| {});
13742 let language =
13743 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
13744 let mut cx = EditorLspTestContext::new(
13745 language,
13746 lsp::ServerCapabilities {
13747 completion_provider: Some(lsp::CompletionOptions {
13748 ..lsp::CompletionOptions::default()
13749 }),
13750 ..lsp::ServerCapabilities::default()
13751 },
13752 cx,
13753 )
13754 .await;
13755
13756 cx.set_state(
13757 "#ifndef BAR_H
13758#define BAR_H
13759
13760#include <stdbool.h>
13761
13762int fn_branch(bool do_branch1, bool do_branch2);
13763
13764#endif // BAR_H
13765ˇ",
13766 );
13767 cx.executor().run_until_parked();
13768 cx.update_editor(|editor, window, cx| {
13769 editor.handle_input("#", window, cx);
13770 });
13771 cx.executor().run_until_parked();
13772 cx.update_editor(|editor, window, cx| {
13773 editor.handle_input("i", window, cx);
13774 });
13775 cx.executor().run_until_parked();
13776 cx.update_editor(|editor, window, cx| {
13777 editor.handle_input("n", window, cx);
13778 });
13779 cx.executor().run_until_parked();
13780 cx.assert_editor_state(
13781 "#ifndef BAR_H
13782#define BAR_H
13783
13784#include <stdbool.h>
13785
13786int fn_branch(bool do_branch1, bool do_branch2);
13787
13788#endif // BAR_H
13789#inˇ",
13790 );
13791
13792 cx.lsp
13793 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13794 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13795 is_incomplete: false,
13796 item_defaults: None,
13797 items: vec![lsp::CompletionItem {
13798 kind: Some(lsp::CompletionItemKind::SNIPPET),
13799 label_details: Some(lsp::CompletionItemLabelDetails {
13800 detail: Some("header".to_string()),
13801 description: None,
13802 }),
13803 label: " include".to_string(),
13804 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13805 range: lsp::Range {
13806 start: lsp::Position {
13807 line: 8,
13808 character: 1,
13809 },
13810 end: lsp::Position {
13811 line: 8,
13812 character: 1,
13813 },
13814 },
13815 new_text: "include \"$0\"".to_string(),
13816 })),
13817 sort_text: Some("40b67681include".to_string()),
13818 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13819 filter_text: Some("include".to_string()),
13820 insert_text: Some("include \"$0\"".to_string()),
13821 ..lsp::CompletionItem::default()
13822 }],
13823 })))
13824 });
13825 cx.update_editor(|editor, window, cx| {
13826 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13827 });
13828 cx.executor().run_until_parked();
13829 cx.update_editor(|editor, window, cx| {
13830 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
13831 });
13832 cx.executor().run_until_parked();
13833 cx.assert_editor_state(
13834 "#ifndef BAR_H
13835#define BAR_H
13836
13837#include <stdbool.h>
13838
13839int fn_branch(bool do_branch1, bool do_branch2);
13840
13841#endif // BAR_H
13842#include \"ˇ\"",
13843 );
13844
13845 cx.lsp
13846 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13847 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13848 is_incomplete: true,
13849 item_defaults: None,
13850 items: vec![lsp::CompletionItem {
13851 kind: Some(lsp::CompletionItemKind::FILE),
13852 label: "AGL/".to_string(),
13853 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13854 range: lsp::Range {
13855 start: lsp::Position {
13856 line: 8,
13857 character: 10,
13858 },
13859 end: lsp::Position {
13860 line: 8,
13861 character: 11,
13862 },
13863 },
13864 new_text: "AGL/".to_string(),
13865 })),
13866 sort_text: Some("40b67681AGL/".to_string()),
13867 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
13868 filter_text: Some("AGL/".to_string()),
13869 insert_text: Some("AGL/".to_string()),
13870 ..lsp::CompletionItem::default()
13871 }],
13872 })))
13873 });
13874 cx.update_editor(|editor, window, cx| {
13875 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13876 });
13877 cx.executor().run_until_parked();
13878 cx.update_editor(|editor, window, cx| {
13879 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
13880 });
13881 cx.executor().run_until_parked();
13882 cx.assert_editor_state(
13883 r##"#ifndef BAR_H
13884#define BAR_H
13885
13886#include <stdbool.h>
13887
13888int fn_branch(bool do_branch1, bool do_branch2);
13889
13890#endif // BAR_H
13891#include "AGL/ˇ"##,
13892 );
13893
13894 cx.update_editor(|editor, window, cx| {
13895 editor.handle_input("\"", window, cx);
13896 });
13897 cx.executor().run_until_parked();
13898 cx.assert_editor_state(
13899 r##"#ifndef BAR_H
13900#define BAR_H
13901
13902#include <stdbool.h>
13903
13904int fn_branch(bool do_branch1, bool do_branch2);
13905
13906#endif // BAR_H
13907#include "AGL/"ˇ"##,
13908 );
13909}
13910
13911#[gpui::test]
13912async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
13913 init_test(cx, |_| {});
13914
13915 let mut cx = EditorLspTestContext::new_rust(
13916 lsp::ServerCapabilities {
13917 completion_provider: Some(lsp::CompletionOptions {
13918 trigger_characters: Some(vec![".".to_string()]),
13919 resolve_provider: Some(true),
13920 ..Default::default()
13921 }),
13922 ..Default::default()
13923 },
13924 cx,
13925 )
13926 .await;
13927
13928 cx.set_state("fn main() { let a = 2ˇ; }");
13929 cx.simulate_keystroke(".");
13930 let completion_item = lsp::CompletionItem {
13931 label: "Some".into(),
13932 kind: Some(lsp::CompletionItemKind::SNIPPET),
13933 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13934 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13935 kind: lsp::MarkupKind::Markdown,
13936 value: "```rust\nSome(2)\n```".to_string(),
13937 })),
13938 deprecated: Some(false),
13939 sort_text: Some("Some".to_string()),
13940 filter_text: Some("Some".to_string()),
13941 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13942 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13943 range: lsp::Range {
13944 start: lsp::Position {
13945 line: 0,
13946 character: 22,
13947 },
13948 end: lsp::Position {
13949 line: 0,
13950 character: 22,
13951 },
13952 },
13953 new_text: "Some(2)".to_string(),
13954 })),
13955 additional_text_edits: Some(vec![lsp::TextEdit {
13956 range: lsp::Range {
13957 start: lsp::Position {
13958 line: 0,
13959 character: 20,
13960 },
13961 end: lsp::Position {
13962 line: 0,
13963 character: 22,
13964 },
13965 },
13966 new_text: "".to_string(),
13967 }]),
13968 ..Default::default()
13969 };
13970
13971 let closure_completion_item = completion_item.clone();
13972 let counter = Arc::new(AtomicUsize::new(0));
13973 let counter_clone = counter.clone();
13974 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13975 let task_completion_item = closure_completion_item.clone();
13976 counter_clone.fetch_add(1, atomic::Ordering::Release);
13977 async move {
13978 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13979 is_incomplete: true,
13980 item_defaults: None,
13981 items: vec![task_completion_item],
13982 })))
13983 }
13984 });
13985
13986 cx.condition(|editor, _| editor.context_menu_visible())
13987 .await;
13988 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
13989 assert!(request.next().await.is_some());
13990 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13991
13992 cx.simulate_keystrokes("S o m");
13993 cx.condition(|editor, _| editor.context_menu_visible())
13994 .await;
13995 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
13996 assert!(request.next().await.is_some());
13997 assert!(request.next().await.is_some());
13998 assert!(request.next().await.is_some());
13999 request.close();
14000 assert!(request.next().await.is_none());
14001 assert_eq!(
14002 counter.load(atomic::Ordering::Acquire),
14003 4,
14004 "With the completions menu open, only one LSP request should happen per input"
14005 );
14006}
14007
14008#[gpui::test]
14009async fn test_toggle_comment(cx: &mut TestAppContext) {
14010 init_test(cx, |_| {});
14011 let mut cx = EditorTestContext::new(cx).await;
14012 let language = Arc::new(Language::new(
14013 LanguageConfig {
14014 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
14015 ..Default::default()
14016 },
14017 Some(tree_sitter_rust::LANGUAGE.into()),
14018 ));
14019 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
14020
14021 // If multiple selections intersect a line, the line is only toggled once.
14022 cx.set_state(indoc! {"
14023 fn a() {
14024 «//b();
14025 ˇ»// «c();
14026 //ˇ» d();
14027 }
14028 "});
14029
14030 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14031
14032 cx.assert_editor_state(indoc! {"
14033 fn a() {
14034 «b();
14035 c();
14036 ˇ» d();
14037 }
14038 "});
14039
14040 // The comment prefix is inserted at the same column for every line in a
14041 // selection.
14042 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14043
14044 cx.assert_editor_state(indoc! {"
14045 fn a() {
14046 // «b();
14047 // c();
14048 ˇ»// d();
14049 }
14050 "});
14051
14052 // If a selection ends at the beginning of a line, that line is not toggled.
14053 cx.set_selections_state(indoc! {"
14054 fn a() {
14055 // b();
14056 «// c();
14057 ˇ» // d();
14058 }
14059 "});
14060
14061 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14062
14063 cx.assert_editor_state(indoc! {"
14064 fn a() {
14065 // b();
14066 «c();
14067 ˇ» // d();
14068 }
14069 "});
14070
14071 // If a selection span a single line and is empty, the line is toggled.
14072 cx.set_state(indoc! {"
14073 fn a() {
14074 a();
14075 b();
14076 ˇ
14077 }
14078 "});
14079
14080 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14081
14082 cx.assert_editor_state(indoc! {"
14083 fn a() {
14084 a();
14085 b();
14086 //•ˇ
14087 }
14088 "});
14089
14090 // If a selection span multiple lines, empty lines are not toggled.
14091 cx.set_state(indoc! {"
14092 fn a() {
14093 «a();
14094
14095 c();ˇ»
14096 }
14097 "});
14098
14099 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14100
14101 cx.assert_editor_state(indoc! {"
14102 fn a() {
14103 // «a();
14104
14105 // c();ˇ»
14106 }
14107 "});
14108
14109 // If a selection includes multiple comment prefixes, all lines are uncommented.
14110 cx.set_state(indoc! {"
14111 fn a() {
14112 «// a();
14113 /// b();
14114 //! c();ˇ»
14115 }
14116 "});
14117
14118 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14119
14120 cx.assert_editor_state(indoc! {"
14121 fn a() {
14122 «a();
14123 b();
14124 c();ˇ»
14125 }
14126 "});
14127}
14128
14129#[gpui::test]
14130async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
14131 init_test(cx, |_| {});
14132 let mut cx = EditorTestContext::new(cx).await;
14133 let language = Arc::new(Language::new(
14134 LanguageConfig {
14135 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
14136 ..Default::default()
14137 },
14138 Some(tree_sitter_rust::LANGUAGE.into()),
14139 ));
14140 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
14141
14142 let toggle_comments = &ToggleComments {
14143 advance_downwards: false,
14144 ignore_indent: true,
14145 };
14146
14147 // If multiple selections intersect a line, the line is only toggled once.
14148 cx.set_state(indoc! {"
14149 fn a() {
14150 // «b();
14151 // c();
14152 // ˇ» d();
14153 }
14154 "});
14155
14156 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14157
14158 cx.assert_editor_state(indoc! {"
14159 fn a() {
14160 «b();
14161 c();
14162 ˇ» d();
14163 }
14164 "});
14165
14166 // The comment prefix is inserted at the beginning of each line
14167 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14168
14169 cx.assert_editor_state(indoc! {"
14170 fn a() {
14171 // «b();
14172 // c();
14173 // ˇ» d();
14174 }
14175 "});
14176
14177 // If a selection ends at the beginning of a line, that line is not toggled.
14178 cx.set_selections_state(indoc! {"
14179 fn a() {
14180 // b();
14181 // «c();
14182 ˇ»// d();
14183 }
14184 "});
14185
14186 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14187
14188 cx.assert_editor_state(indoc! {"
14189 fn a() {
14190 // b();
14191 «c();
14192 ˇ»// d();
14193 }
14194 "});
14195
14196 // If a selection span a single line and is empty, the line is toggled.
14197 cx.set_state(indoc! {"
14198 fn a() {
14199 a();
14200 b();
14201 ˇ
14202 }
14203 "});
14204
14205 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14206
14207 cx.assert_editor_state(indoc! {"
14208 fn a() {
14209 a();
14210 b();
14211 //ˇ
14212 }
14213 "});
14214
14215 // If a selection span multiple lines, empty lines are not toggled.
14216 cx.set_state(indoc! {"
14217 fn a() {
14218 «a();
14219
14220 c();ˇ»
14221 }
14222 "});
14223
14224 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14225
14226 cx.assert_editor_state(indoc! {"
14227 fn a() {
14228 // «a();
14229
14230 // c();ˇ»
14231 }
14232 "});
14233
14234 // If a selection includes multiple comment prefixes, all lines are uncommented.
14235 cx.set_state(indoc! {"
14236 fn a() {
14237 // «a();
14238 /// b();
14239 //! c();ˇ»
14240 }
14241 "});
14242
14243 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14244
14245 cx.assert_editor_state(indoc! {"
14246 fn a() {
14247 «a();
14248 b();
14249 c();ˇ»
14250 }
14251 "});
14252}
14253
14254#[gpui::test]
14255async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
14256 init_test(cx, |_| {});
14257
14258 let language = Arc::new(Language::new(
14259 LanguageConfig {
14260 line_comments: vec!["// ".into()],
14261 ..Default::default()
14262 },
14263 Some(tree_sitter_rust::LANGUAGE.into()),
14264 ));
14265
14266 let mut cx = EditorTestContext::new(cx).await;
14267
14268 cx.language_registry().add(language.clone());
14269 cx.update_buffer(|buffer, cx| {
14270 buffer.set_language(Some(language), cx);
14271 });
14272
14273 let toggle_comments = &ToggleComments {
14274 advance_downwards: true,
14275 ignore_indent: false,
14276 };
14277
14278 // Single cursor on one line -> advance
14279 // Cursor moves horizontally 3 characters as well on non-blank line
14280 cx.set_state(indoc!(
14281 "fn a() {
14282 ˇdog();
14283 cat();
14284 }"
14285 ));
14286 cx.update_editor(|editor, window, cx| {
14287 editor.toggle_comments(toggle_comments, window, cx);
14288 });
14289 cx.assert_editor_state(indoc!(
14290 "fn a() {
14291 // dog();
14292 catˇ();
14293 }"
14294 ));
14295
14296 // Single selection on one line -> don't advance
14297 cx.set_state(indoc!(
14298 "fn a() {
14299 «dog()ˇ»;
14300 cat();
14301 }"
14302 ));
14303 cx.update_editor(|editor, window, cx| {
14304 editor.toggle_comments(toggle_comments, window, cx);
14305 });
14306 cx.assert_editor_state(indoc!(
14307 "fn a() {
14308 // «dog()ˇ»;
14309 cat();
14310 }"
14311 ));
14312
14313 // Multiple cursors on one line -> advance
14314 cx.set_state(indoc!(
14315 "fn a() {
14316 ˇdˇog();
14317 cat();
14318 }"
14319 ));
14320 cx.update_editor(|editor, window, cx| {
14321 editor.toggle_comments(toggle_comments, window, cx);
14322 });
14323 cx.assert_editor_state(indoc!(
14324 "fn a() {
14325 // dog();
14326 catˇ(ˇ);
14327 }"
14328 ));
14329
14330 // Multiple cursors on one line, with selection -> don't advance
14331 cx.set_state(indoc!(
14332 "fn a() {
14333 ˇdˇog«()ˇ»;
14334 cat();
14335 }"
14336 ));
14337 cx.update_editor(|editor, window, cx| {
14338 editor.toggle_comments(toggle_comments, window, cx);
14339 });
14340 cx.assert_editor_state(indoc!(
14341 "fn a() {
14342 // ˇdˇog«()ˇ»;
14343 cat();
14344 }"
14345 ));
14346
14347 // Single cursor on one line -> advance
14348 // Cursor moves to column 0 on blank line
14349 cx.set_state(indoc!(
14350 "fn a() {
14351 ˇdog();
14352
14353 cat();
14354 }"
14355 ));
14356 cx.update_editor(|editor, window, cx| {
14357 editor.toggle_comments(toggle_comments, window, cx);
14358 });
14359 cx.assert_editor_state(indoc!(
14360 "fn a() {
14361 // dog();
14362 ˇ
14363 cat();
14364 }"
14365 ));
14366
14367 // Single cursor on one line -> advance
14368 // Cursor starts and ends at column 0
14369 cx.set_state(indoc!(
14370 "fn a() {
14371 ˇ dog();
14372 cat();
14373 }"
14374 ));
14375 cx.update_editor(|editor, window, cx| {
14376 editor.toggle_comments(toggle_comments, window, cx);
14377 });
14378 cx.assert_editor_state(indoc!(
14379 "fn a() {
14380 // dog();
14381 ˇ cat();
14382 }"
14383 ));
14384}
14385
14386#[gpui::test]
14387async fn test_toggle_block_comment(cx: &mut TestAppContext) {
14388 init_test(cx, |_| {});
14389
14390 let mut cx = EditorTestContext::new(cx).await;
14391
14392 let html_language = Arc::new(
14393 Language::new(
14394 LanguageConfig {
14395 name: "HTML".into(),
14396 block_comment: Some(BlockCommentConfig {
14397 start: "<!-- ".into(),
14398 prefix: "".into(),
14399 end: " -->".into(),
14400 tab_size: 0,
14401 }),
14402 ..Default::default()
14403 },
14404 Some(tree_sitter_html::LANGUAGE.into()),
14405 )
14406 .with_injection_query(
14407 r#"
14408 (script_element
14409 (raw_text) @injection.content
14410 (#set! injection.language "javascript"))
14411 "#,
14412 )
14413 .unwrap(),
14414 );
14415
14416 let javascript_language = Arc::new(Language::new(
14417 LanguageConfig {
14418 name: "JavaScript".into(),
14419 line_comments: vec!["// ".into()],
14420 ..Default::default()
14421 },
14422 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
14423 ));
14424
14425 cx.language_registry().add(html_language.clone());
14426 cx.language_registry().add(javascript_language);
14427 cx.update_buffer(|buffer, cx| {
14428 buffer.set_language(Some(html_language), cx);
14429 });
14430
14431 // Toggle comments for empty selections
14432 cx.set_state(
14433 &r#"
14434 <p>A</p>ˇ
14435 <p>B</p>ˇ
14436 <p>C</p>ˇ
14437 "#
14438 .unindent(),
14439 );
14440 cx.update_editor(|editor, window, cx| {
14441 editor.toggle_comments(&ToggleComments::default(), window, cx)
14442 });
14443 cx.assert_editor_state(
14444 &r#"
14445 <!-- <p>A</p>ˇ -->
14446 <!-- <p>B</p>ˇ -->
14447 <!-- <p>C</p>ˇ -->
14448 "#
14449 .unindent(),
14450 );
14451 cx.update_editor(|editor, window, cx| {
14452 editor.toggle_comments(&ToggleComments::default(), window, cx)
14453 });
14454 cx.assert_editor_state(
14455 &r#"
14456 <p>A</p>ˇ
14457 <p>B</p>ˇ
14458 <p>C</p>ˇ
14459 "#
14460 .unindent(),
14461 );
14462
14463 // Toggle comments for mixture of empty and non-empty selections, where
14464 // multiple selections occupy a given line.
14465 cx.set_state(
14466 &r#"
14467 <p>A«</p>
14468 <p>ˇ»B</p>ˇ
14469 <p>C«</p>
14470 <p>ˇ»D</p>ˇ
14471 "#
14472 .unindent(),
14473 );
14474
14475 cx.update_editor(|editor, window, cx| {
14476 editor.toggle_comments(&ToggleComments::default(), window, cx)
14477 });
14478 cx.assert_editor_state(
14479 &r#"
14480 <!-- <p>A«</p>
14481 <p>ˇ»B</p>ˇ -->
14482 <!-- <p>C«</p>
14483 <p>ˇ»D</p>ˇ -->
14484 "#
14485 .unindent(),
14486 );
14487 cx.update_editor(|editor, window, cx| {
14488 editor.toggle_comments(&ToggleComments::default(), window, cx)
14489 });
14490 cx.assert_editor_state(
14491 &r#"
14492 <p>A«</p>
14493 <p>ˇ»B</p>ˇ
14494 <p>C«</p>
14495 <p>ˇ»D</p>ˇ
14496 "#
14497 .unindent(),
14498 );
14499
14500 // Toggle comments when different languages are active for different
14501 // selections.
14502 cx.set_state(
14503 &r#"
14504 ˇ<script>
14505 ˇvar x = new Y();
14506 ˇ</script>
14507 "#
14508 .unindent(),
14509 );
14510 cx.executor().run_until_parked();
14511 cx.update_editor(|editor, window, cx| {
14512 editor.toggle_comments(&ToggleComments::default(), window, cx)
14513 });
14514 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
14515 // Uncommenting and commenting from this position brings in even more wrong artifacts.
14516 cx.assert_editor_state(
14517 &r#"
14518 <!-- ˇ<script> -->
14519 // ˇvar x = new Y();
14520 <!-- ˇ</script> -->
14521 "#
14522 .unindent(),
14523 );
14524}
14525
14526#[gpui::test]
14527fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
14528 init_test(cx, |_| {});
14529
14530 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14531 let multibuffer = cx.new(|cx| {
14532 let mut multibuffer = MultiBuffer::new(ReadWrite);
14533 multibuffer.push_excerpts(
14534 buffer.clone(),
14535 [
14536 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
14537 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
14538 ],
14539 cx,
14540 );
14541 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
14542 multibuffer
14543 });
14544
14545 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
14546 editor.update_in(cx, |editor, window, cx| {
14547 assert_eq!(editor.text(cx), "aaaa\nbbbb");
14548 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14549 s.select_ranges([
14550 Point::new(0, 0)..Point::new(0, 0),
14551 Point::new(1, 0)..Point::new(1, 0),
14552 ])
14553 });
14554
14555 editor.handle_input("X", window, cx);
14556 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
14557 assert_eq!(
14558 editor.selections.ranges(cx),
14559 [
14560 Point::new(0, 1)..Point::new(0, 1),
14561 Point::new(1, 1)..Point::new(1, 1),
14562 ]
14563 );
14564
14565 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
14566 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14567 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
14568 });
14569 editor.backspace(&Default::default(), window, cx);
14570 assert_eq!(editor.text(cx), "Xa\nbbb");
14571 assert_eq!(
14572 editor.selections.ranges(cx),
14573 [Point::new(1, 0)..Point::new(1, 0)]
14574 );
14575
14576 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14577 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
14578 });
14579 editor.backspace(&Default::default(), window, cx);
14580 assert_eq!(editor.text(cx), "X\nbb");
14581 assert_eq!(
14582 editor.selections.ranges(cx),
14583 [Point::new(0, 1)..Point::new(0, 1)]
14584 );
14585 });
14586}
14587
14588#[gpui::test]
14589fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
14590 init_test(cx, |_| {});
14591
14592 let markers = vec![('[', ']').into(), ('(', ')').into()];
14593 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
14594 indoc! {"
14595 [aaaa
14596 (bbbb]
14597 cccc)",
14598 },
14599 markers.clone(),
14600 );
14601 let excerpt_ranges = markers.into_iter().map(|marker| {
14602 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
14603 ExcerptRange::new(context)
14604 });
14605 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
14606 let multibuffer = cx.new(|cx| {
14607 let mut multibuffer = MultiBuffer::new(ReadWrite);
14608 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
14609 multibuffer
14610 });
14611
14612 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
14613 editor.update_in(cx, |editor, window, cx| {
14614 let (expected_text, selection_ranges) = marked_text_ranges(
14615 indoc! {"
14616 aaaa
14617 bˇbbb
14618 bˇbbˇb
14619 cccc"
14620 },
14621 true,
14622 );
14623 assert_eq!(editor.text(cx), expected_text);
14624 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14625 s.select_ranges(selection_ranges)
14626 });
14627
14628 editor.handle_input("X", window, cx);
14629
14630 let (expected_text, expected_selections) = marked_text_ranges(
14631 indoc! {"
14632 aaaa
14633 bXˇbbXb
14634 bXˇbbXˇb
14635 cccc"
14636 },
14637 false,
14638 );
14639 assert_eq!(editor.text(cx), expected_text);
14640 assert_eq!(editor.selections.ranges(cx), expected_selections);
14641
14642 editor.newline(&Newline, window, cx);
14643 let (expected_text, expected_selections) = marked_text_ranges(
14644 indoc! {"
14645 aaaa
14646 bX
14647 ˇbbX
14648 b
14649 bX
14650 ˇbbX
14651 ˇb
14652 cccc"
14653 },
14654 false,
14655 );
14656 assert_eq!(editor.text(cx), expected_text);
14657 assert_eq!(editor.selections.ranges(cx), expected_selections);
14658 });
14659}
14660
14661#[gpui::test]
14662fn test_refresh_selections(cx: &mut TestAppContext) {
14663 init_test(cx, |_| {});
14664
14665 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14666 let mut excerpt1_id = None;
14667 let multibuffer = cx.new(|cx| {
14668 let mut multibuffer = MultiBuffer::new(ReadWrite);
14669 excerpt1_id = multibuffer
14670 .push_excerpts(
14671 buffer.clone(),
14672 [
14673 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14674 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14675 ],
14676 cx,
14677 )
14678 .into_iter()
14679 .next();
14680 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14681 multibuffer
14682 });
14683
14684 let editor = cx.add_window(|window, cx| {
14685 let mut editor = build_editor(multibuffer.clone(), window, cx);
14686 let snapshot = editor.snapshot(window, cx);
14687 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14688 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
14689 });
14690 editor.begin_selection(
14691 Point::new(2, 1).to_display_point(&snapshot),
14692 true,
14693 1,
14694 window,
14695 cx,
14696 );
14697 assert_eq!(
14698 editor.selections.ranges(cx),
14699 [
14700 Point::new(1, 3)..Point::new(1, 3),
14701 Point::new(2, 1)..Point::new(2, 1),
14702 ]
14703 );
14704 editor
14705 });
14706
14707 // Refreshing selections is a no-op when excerpts haven't changed.
14708 _ = editor.update(cx, |editor, window, cx| {
14709 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14710 assert_eq!(
14711 editor.selections.ranges(cx),
14712 [
14713 Point::new(1, 3)..Point::new(1, 3),
14714 Point::new(2, 1)..Point::new(2, 1),
14715 ]
14716 );
14717 });
14718
14719 multibuffer.update(cx, |multibuffer, cx| {
14720 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14721 });
14722 _ = editor.update(cx, |editor, window, cx| {
14723 // Removing an excerpt causes the first selection to become degenerate.
14724 assert_eq!(
14725 editor.selections.ranges(cx),
14726 [
14727 Point::new(0, 0)..Point::new(0, 0),
14728 Point::new(0, 1)..Point::new(0, 1)
14729 ]
14730 );
14731
14732 // Refreshing selections will relocate the first selection to the original buffer
14733 // location.
14734 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14735 assert_eq!(
14736 editor.selections.ranges(cx),
14737 [
14738 Point::new(0, 1)..Point::new(0, 1),
14739 Point::new(0, 3)..Point::new(0, 3)
14740 ]
14741 );
14742 assert!(editor.selections.pending_anchor().is_some());
14743 });
14744}
14745
14746#[gpui::test]
14747fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
14748 init_test(cx, |_| {});
14749
14750 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14751 let mut excerpt1_id = None;
14752 let multibuffer = cx.new(|cx| {
14753 let mut multibuffer = MultiBuffer::new(ReadWrite);
14754 excerpt1_id = multibuffer
14755 .push_excerpts(
14756 buffer.clone(),
14757 [
14758 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14759 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14760 ],
14761 cx,
14762 )
14763 .into_iter()
14764 .next();
14765 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14766 multibuffer
14767 });
14768
14769 let editor = cx.add_window(|window, cx| {
14770 let mut editor = build_editor(multibuffer.clone(), window, cx);
14771 let snapshot = editor.snapshot(window, cx);
14772 editor.begin_selection(
14773 Point::new(1, 3).to_display_point(&snapshot),
14774 false,
14775 1,
14776 window,
14777 cx,
14778 );
14779 assert_eq!(
14780 editor.selections.ranges(cx),
14781 [Point::new(1, 3)..Point::new(1, 3)]
14782 );
14783 editor
14784 });
14785
14786 multibuffer.update(cx, |multibuffer, cx| {
14787 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14788 });
14789 _ = editor.update(cx, |editor, window, cx| {
14790 assert_eq!(
14791 editor.selections.ranges(cx),
14792 [Point::new(0, 0)..Point::new(0, 0)]
14793 );
14794
14795 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
14796 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14797 assert_eq!(
14798 editor.selections.ranges(cx),
14799 [Point::new(0, 3)..Point::new(0, 3)]
14800 );
14801 assert!(editor.selections.pending_anchor().is_some());
14802 });
14803}
14804
14805#[gpui::test]
14806async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
14807 init_test(cx, |_| {});
14808
14809 let language = Arc::new(
14810 Language::new(
14811 LanguageConfig {
14812 brackets: BracketPairConfig {
14813 pairs: vec![
14814 BracketPair {
14815 start: "{".to_string(),
14816 end: "}".to_string(),
14817 close: true,
14818 surround: true,
14819 newline: true,
14820 },
14821 BracketPair {
14822 start: "/* ".to_string(),
14823 end: " */".to_string(),
14824 close: true,
14825 surround: true,
14826 newline: true,
14827 },
14828 ],
14829 ..Default::default()
14830 },
14831 ..Default::default()
14832 },
14833 Some(tree_sitter_rust::LANGUAGE.into()),
14834 )
14835 .with_indents_query("")
14836 .unwrap(),
14837 );
14838
14839 let text = concat!(
14840 "{ }\n", //
14841 " x\n", //
14842 " /* */\n", //
14843 "x\n", //
14844 "{{} }\n", //
14845 );
14846
14847 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
14848 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14849 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14850 editor
14851 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
14852 .await;
14853
14854 editor.update_in(cx, |editor, window, cx| {
14855 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14856 s.select_display_ranges([
14857 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
14858 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
14859 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
14860 ])
14861 });
14862 editor.newline(&Newline, window, cx);
14863
14864 assert_eq!(
14865 editor.buffer().read(cx).read(cx).text(),
14866 concat!(
14867 "{ \n", // Suppress rustfmt
14868 "\n", //
14869 "}\n", //
14870 " x\n", //
14871 " /* \n", //
14872 " \n", //
14873 " */\n", //
14874 "x\n", //
14875 "{{} \n", //
14876 "}\n", //
14877 )
14878 );
14879 });
14880}
14881
14882#[gpui::test]
14883fn test_highlighted_ranges(cx: &mut TestAppContext) {
14884 init_test(cx, |_| {});
14885
14886 let editor = cx.add_window(|window, cx| {
14887 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
14888 build_editor(buffer, window, cx)
14889 });
14890
14891 _ = editor.update(cx, |editor, window, cx| {
14892 struct Type1;
14893 struct Type2;
14894
14895 let buffer = editor.buffer.read(cx).snapshot(cx);
14896
14897 let anchor_range =
14898 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
14899
14900 editor.highlight_background::<Type1>(
14901 &[
14902 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
14903 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
14904 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
14905 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
14906 ],
14907 |_| Hsla::red(),
14908 cx,
14909 );
14910 editor.highlight_background::<Type2>(
14911 &[
14912 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
14913 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
14914 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
14915 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
14916 ],
14917 |_| Hsla::green(),
14918 cx,
14919 );
14920
14921 let snapshot = editor.snapshot(window, cx);
14922 let mut highlighted_ranges = editor.background_highlights_in_range(
14923 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
14924 &snapshot,
14925 cx.theme(),
14926 );
14927 // Enforce a consistent ordering based on color without relying on the ordering of the
14928 // highlight's `TypeId` which is non-executor.
14929 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
14930 assert_eq!(
14931 highlighted_ranges,
14932 &[
14933 (
14934 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
14935 Hsla::red(),
14936 ),
14937 (
14938 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14939 Hsla::red(),
14940 ),
14941 (
14942 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
14943 Hsla::green(),
14944 ),
14945 (
14946 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
14947 Hsla::green(),
14948 ),
14949 ]
14950 );
14951 assert_eq!(
14952 editor.background_highlights_in_range(
14953 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
14954 &snapshot,
14955 cx.theme(),
14956 ),
14957 &[(
14958 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14959 Hsla::red(),
14960 )]
14961 );
14962 });
14963}
14964
14965#[gpui::test]
14966async fn test_following(cx: &mut TestAppContext) {
14967 init_test(cx, |_| {});
14968
14969 let fs = FakeFs::new(cx.executor());
14970 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14971
14972 let buffer = project.update(cx, |project, cx| {
14973 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
14974 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
14975 });
14976 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
14977 let follower = cx.update(|cx| {
14978 cx.open_window(
14979 WindowOptions {
14980 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
14981 gpui::Point::new(px(0.), px(0.)),
14982 gpui::Point::new(px(10.), px(80.)),
14983 ))),
14984 ..Default::default()
14985 },
14986 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
14987 )
14988 .unwrap()
14989 });
14990
14991 let is_still_following = Rc::new(RefCell::new(true));
14992 let follower_edit_event_count = Rc::new(RefCell::new(0));
14993 let pending_update = Rc::new(RefCell::new(None));
14994 let leader_entity = leader.root(cx).unwrap();
14995 let follower_entity = follower.root(cx).unwrap();
14996 _ = follower.update(cx, {
14997 let update = pending_update.clone();
14998 let is_still_following = is_still_following.clone();
14999 let follower_edit_event_count = follower_edit_event_count.clone();
15000 |_, window, cx| {
15001 cx.subscribe_in(
15002 &leader_entity,
15003 window,
15004 move |_, leader, event, window, cx| {
15005 leader.read(cx).add_event_to_update_proto(
15006 event,
15007 &mut update.borrow_mut(),
15008 window,
15009 cx,
15010 );
15011 },
15012 )
15013 .detach();
15014
15015 cx.subscribe_in(
15016 &follower_entity,
15017 window,
15018 move |_, _, event: &EditorEvent, _window, _cx| {
15019 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
15020 *is_still_following.borrow_mut() = false;
15021 }
15022
15023 if let EditorEvent::BufferEdited = event {
15024 *follower_edit_event_count.borrow_mut() += 1;
15025 }
15026 },
15027 )
15028 .detach();
15029 }
15030 });
15031
15032 // Update the selections only
15033 _ = leader.update(cx, |leader, window, cx| {
15034 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15035 s.select_ranges([1..1])
15036 });
15037 });
15038 follower
15039 .update(cx, |follower, window, cx| {
15040 follower.apply_update_proto(
15041 &project,
15042 pending_update.borrow_mut().take().unwrap(),
15043 window,
15044 cx,
15045 )
15046 })
15047 .unwrap()
15048 .await
15049 .unwrap();
15050 _ = follower.update(cx, |follower, _, cx| {
15051 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
15052 });
15053 assert!(*is_still_following.borrow());
15054 assert_eq!(*follower_edit_event_count.borrow(), 0);
15055
15056 // Update the scroll position only
15057 _ = leader.update(cx, |leader, window, cx| {
15058 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
15059 });
15060 follower
15061 .update(cx, |follower, window, cx| {
15062 follower.apply_update_proto(
15063 &project,
15064 pending_update.borrow_mut().take().unwrap(),
15065 window,
15066 cx,
15067 )
15068 })
15069 .unwrap()
15070 .await
15071 .unwrap();
15072 assert_eq!(
15073 follower
15074 .update(cx, |follower, _, cx| follower.scroll_position(cx))
15075 .unwrap(),
15076 gpui::Point::new(1.5, 3.5)
15077 );
15078 assert!(*is_still_following.borrow());
15079 assert_eq!(*follower_edit_event_count.borrow(), 0);
15080
15081 // Update the selections and scroll position. The follower's scroll position is updated
15082 // via autoscroll, not via the leader's exact scroll position.
15083 _ = leader.update(cx, |leader, window, cx| {
15084 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15085 s.select_ranges([0..0])
15086 });
15087 leader.request_autoscroll(Autoscroll::newest(), cx);
15088 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
15089 });
15090 follower
15091 .update(cx, |follower, window, cx| {
15092 follower.apply_update_proto(
15093 &project,
15094 pending_update.borrow_mut().take().unwrap(),
15095 window,
15096 cx,
15097 )
15098 })
15099 .unwrap()
15100 .await
15101 .unwrap();
15102 _ = follower.update(cx, |follower, _, cx| {
15103 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
15104 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
15105 });
15106 assert!(*is_still_following.borrow());
15107
15108 // Creating a pending selection that precedes another selection
15109 _ = leader.update(cx, |leader, window, cx| {
15110 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15111 s.select_ranges([1..1])
15112 });
15113 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
15114 });
15115 follower
15116 .update(cx, |follower, window, cx| {
15117 follower.apply_update_proto(
15118 &project,
15119 pending_update.borrow_mut().take().unwrap(),
15120 window,
15121 cx,
15122 )
15123 })
15124 .unwrap()
15125 .await
15126 .unwrap();
15127 _ = follower.update(cx, |follower, _, cx| {
15128 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
15129 });
15130 assert!(*is_still_following.borrow());
15131
15132 // Extend the pending selection so that it surrounds another selection
15133 _ = leader.update(cx, |leader, window, cx| {
15134 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
15135 });
15136 follower
15137 .update(cx, |follower, window, cx| {
15138 follower.apply_update_proto(
15139 &project,
15140 pending_update.borrow_mut().take().unwrap(),
15141 window,
15142 cx,
15143 )
15144 })
15145 .unwrap()
15146 .await
15147 .unwrap();
15148 _ = follower.update(cx, |follower, _, cx| {
15149 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
15150 });
15151
15152 // Scrolling locally breaks the follow
15153 _ = follower.update(cx, |follower, window, cx| {
15154 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
15155 follower.set_scroll_anchor(
15156 ScrollAnchor {
15157 anchor: top_anchor,
15158 offset: gpui::Point::new(0.0, 0.5),
15159 },
15160 window,
15161 cx,
15162 );
15163 });
15164 assert!(!(*is_still_following.borrow()));
15165}
15166
15167#[gpui::test]
15168async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
15169 init_test(cx, |_| {});
15170
15171 let fs = FakeFs::new(cx.executor());
15172 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
15173 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15174 let pane = workspace
15175 .update(cx, |workspace, _, _| workspace.active_pane().clone())
15176 .unwrap();
15177
15178 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15179
15180 let leader = pane.update_in(cx, |_, window, cx| {
15181 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
15182 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
15183 });
15184
15185 // Start following the editor when it has no excerpts.
15186 let mut state_message =
15187 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
15188 let workspace_entity = workspace.root(cx).unwrap();
15189 let follower_1 = cx
15190 .update_window(*workspace.deref(), |_, window, cx| {
15191 Editor::from_state_proto(
15192 workspace_entity,
15193 ViewId {
15194 creator: CollaboratorId::PeerId(PeerId::default()),
15195 id: 0,
15196 },
15197 &mut state_message,
15198 window,
15199 cx,
15200 )
15201 })
15202 .unwrap()
15203 .unwrap()
15204 .await
15205 .unwrap();
15206
15207 let update_message = Rc::new(RefCell::new(None));
15208 follower_1.update_in(cx, {
15209 let update = update_message.clone();
15210 |_, window, cx| {
15211 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
15212 leader.read(cx).add_event_to_update_proto(
15213 event,
15214 &mut update.borrow_mut(),
15215 window,
15216 cx,
15217 );
15218 })
15219 .detach();
15220 }
15221 });
15222
15223 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
15224 (
15225 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
15226 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
15227 )
15228 });
15229
15230 // Insert some excerpts.
15231 leader.update(cx, |leader, cx| {
15232 leader.buffer.update(cx, |multibuffer, cx| {
15233 multibuffer.set_excerpts_for_path(
15234 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
15235 buffer_1.clone(),
15236 vec![
15237 Point::row_range(0..3),
15238 Point::row_range(1..6),
15239 Point::row_range(12..15),
15240 ],
15241 0,
15242 cx,
15243 );
15244 multibuffer.set_excerpts_for_path(
15245 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
15246 buffer_2.clone(),
15247 vec![Point::row_range(0..6), Point::row_range(8..12)],
15248 0,
15249 cx,
15250 );
15251 });
15252 });
15253
15254 // Apply the update of adding the excerpts.
15255 follower_1
15256 .update_in(cx, |follower, window, cx| {
15257 follower.apply_update_proto(
15258 &project,
15259 update_message.borrow().clone().unwrap(),
15260 window,
15261 cx,
15262 )
15263 })
15264 .await
15265 .unwrap();
15266 assert_eq!(
15267 follower_1.update(cx, |editor, cx| editor.text(cx)),
15268 leader.update(cx, |editor, cx| editor.text(cx))
15269 );
15270 update_message.borrow_mut().take();
15271
15272 // Start following separately after it already has excerpts.
15273 let mut state_message =
15274 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
15275 let workspace_entity = workspace.root(cx).unwrap();
15276 let follower_2 = cx
15277 .update_window(*workspace.deref(), |_, window, cx| {
15278 Editor::from_state_proto(
15279 workspace_entity,
15280 ViewId {
15281 creator: CollaboratorId::PeerId(PeerId::default()),
15282 id: 0,
15283 },
15284 &mut state_message,
15285 window,
15286 cx,
15287 )
15288 })
15289 .unwrap()
15290 .unwrap()
15291 .await
15292 .unwrap();
15293 assert_eq!(
15294 follower_2.update(cx, |editor, cx| editor.text(cx)),
15295 leader.update(cx, |editor, cx| editor.text(cx))
15296 );
15297
15298 // Remove some excerpts.
15299 leader.update(cx, |leader, cx| {
15300 leader.buffer.update(cx, |multibuffer, cx| {
15301 let excerpt_ids = multibuffer.excerpt_ids();
15302 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
15303 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
15304 });
15305 });
15306
15307 // Apply the update of removing the excerpts.
15308 follower_1
15309 .update_in(cx, |follower, window, cx| {
15310 follower.apply_update_proto(
15311 &project,
15312 update_message.borrow().clone().unwrap(),
15313 window,
15314 cx,
15315 )
15316 })
15317 .await
15318 .unwrap();
15319 follower_2
15320 .update_in(cx, |follower, window, cx| {
15321 follower.apply_update_proto(
15322 &project,
15323 update_message.borrow().clone().unwrap(),
15324 window,
15325 cx,
15326 )
15327 })
15328 .await
15329 .unwrap();
15330 update_message.borrow_mut().take();
15331 assert_eq!(
15332 follower_1.update(cx, |editor, cx| editor.text(cx)),
15333 leader.update(cx, |editor, cx| editor.text(cx))
15334 );
15335}
15336
15337#[gpui::test]
15338async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15339 init_test(cx, |_| {});
15340
15341 let mut cx = EditorTestContext::new(cx).await;
15342 let lsp_store =
15343 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
15344
15345 cx.set_state(indoc! {"
15346 ˇfn func(abc def: i32) -> u32 {
15347 }
15348 "});
15349
15350 cx.update(|_, cx| {
15351 lsp_store.update(cx, |lsp_store, cx| {
15352 lsp_store
15353 .update_diagnostics(
15354 LanguageServerId(0),
15355 lsp::PublishDiagnosticsParams {
15356 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
15357 version: None,
15358 diagnostics: vec![
15359 lsp::Diagnostic {
15360 range: lsp::Range::new(
15361 lsp::Position::new(0, 11),
15362 lsp::Position::new(0, 12),
15363 ),
15364 severity: Some(lsp::DiagnosticSeverity::ERROR),
15365 ..Default::default()
15366 },
15367 lsp::Diagnostic {
15368 range: lsp::Range::new(
15369 lsp::Position::new(0, 12),
15370 lsp::Position::new(0, 15),
15371 ),
15372 severity: Some(lsp::DiagnosticSeverity::ERROR),
15373 ..Default::default()
15374 },
15375 lsp::Diagnostic {
15376 range: lsp::Range::new(
15377 lsp::Position::new(0, 25),
15378 lsp::Position::new(0, 28),
15379 ),
15380 severity: Some(lsp::DiagnosticSeverity::ERROR),
15381 ..Default::default()
15382 },
15383 ],
15384 },
15385 None,
15386 DiagnosticSourceKind::Pushed,
15387 &[],
15388 cx,
15389 )
15390 .unwrap()
15391 });
15392 });
15393
15394 executor.run_until_parked();
15395
15396 cx.update_editor(|editor, window, cx| {
15397 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15398 });
15399
15400 cx.assert_editor_state(indoc! {"
15401 fn func(abc def: i32) -> ˇu32 {
15402 }
15403 "});
15404
15405 cx.update_editor(|editor, window, cx| {
15406 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15407 });
15408
15409 cx.assert_editor_state(indoc! {"
15410 fn func(abc ˇdef: i32) -> u32 {
15411 }
15412 "});
15413
15414 cx.update_editor(|editor, window, cx| {
15415 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15416 });
15417
15418 cx.assert_editor_state(indoc! {"
15419 fn func(abcˇ def: i32) -> u32 {
15420 }
15421 "});
15422
15423 cx.update_editor(|editor, window, cx| {
15424 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15425 });
15426
15427 cx.assert_editor_state(indoc! {"
15428 fn func(abc def: i32) -> ˇu32 {
15429 }
15430 "});
15431}
15432
15433#[gpui::test]
15434async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15435 init_test(cx, |_| {});
15436
15437 let mut cx = EditorTestContext::new(cx).await;
15438
15439 let diff_base = r#"
15440 use some::mod;
15441
15442 const A: u32 = 42;
15443
15444 fn main() {
15445 println!("hello");
15446
15447 println!("world");
15448 }
15449 "#
15450 .unindent();
15451
15452 // Edits are modified, removed, modified, added
15453 cx.set_state(
15454 &r#"
15455 use some::modified;
15456
15457 ˇ
15458 fn main() {
15459 println!("hello there");
15460
15461 println!("around the");
15462 println!("world");
15463 }
15464 "#
15465 .unindent(),
15466 );
15467
15468 cx.set_head_text(&diff_base);
15469 executor.run_until_parked();
15470
15471 cx.update_editor(|editor, window, cx| {
15472 //Wrap around the bottom of the buffer
15473 for _ in 0..3 {
15474 editor.go_to_next_hunk(&GoToHunk, window, cx);
15475 }
15476 });
15477
15478 cx.assert_editor_state(
15479 &r#"
15480 ˇuse some::modified;
15481
15482
15483 fn main() {
15484 println!("hello there");
15485
15486 println!("around the");
15487 println!("world");
15488 }
15489 "#
15490 .unindent(),
15491 );
15492
15493 cx.update_editor(|editor, window, cx| {
15494 //Wrap around the top of the buffer
15495 for _ in 0..2 {
15496 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15497 }
15498 });
15499
15500 cx.assert_editor_state(
15501 &r#"
15502 use some::modified;
15503
15504
15505 fn main() {
15506 ˇ println!("hello there");
15507
15508 println!("around the");
15509 println!("world");
15510 }
15511 "#
15512 .unindent(),
15513 );
15514
15515 cx.update_editor(|editor, window, cx| {
15516 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15517 });
15518
15519 cx.assert_editor_state(
15520 &r#"
15521 use some::modified;
15522
15523 ˇ
15524 fn main() {
15525 println!("hello there");
15526
15527 println!("around the");
15528 println!("world");
15529 }
15530 "#
15531 .unindent(),
15532 );
15533
15534 cx.update_editor(|editor, window, cx| {
15535 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15536 });
15537
15538 cx.assert_editor_state(
15539 &r#"
15540 ˇuse some::modified;
15541
15542
15543 fn main() {
15544 println!("hello there");
15545
15546 println!("around the");
15547 println!("world");
15548 }
15549 "#
15550 .unindent(),
15551 );
15552
15553 cx.update_editor(|editor, window, cx| {
15554 for _ in 0..2 {
15555 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15556 }
15557 });
15558
15559 cx.assert_editor_state(
15560 &r#"
15561 use some::modified;
15562
15563
15564 fn main() {
15565 ˇ println!("hello there");
15566
15567 println!("around the");
15568 println!("world");
15569 }
15570 "#
15571 .unindent(),
15572 );
15573
15574 cx.update_editor(|editor, window, cx| {
15575 editor.fold(&Fold, window, cx);
15576 });
15577
15578 cx.update_editor(|editor, window, cx| {
15579 editor.go_to_next_hunk(&GoToHunk, window, cx);
15580 });
15581
15582 cx.assert_editor_state(
15583 &r#"
15584 ˇuse some::modified;
15585
15586
15587 fn main() {
15588 println!("hello there");
15589
15590 println!("around the");
15591 println!("world");
15592 }
15593 "#
15594 .unindent(),
15595 );
15596}
15597
15598#[test]
15599fn test_split_words() {
15600 fn split(text: &str) -> Vec<&str> {
15601 split_words(text).collect()
15602 }
15603
15604 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
15605 assert_eq!(split("hello_world"), &["hello_", "world"]);
15606 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
15607 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
15608 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
15609 assert_eq!(split("helloworld"), &["helloworld"]);
15610
15611 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
15612}
15613
15614#[gpui::test]
15615async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
15616 init_test(cx, |_| {});
15617
15618 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
15619 let mut assert = |before, after| {
15620 let _state_context = cx.set_state(before);
15621 cx.run_until_parked();
15622 cx.update_editor(|editor, window, cx| {
15623 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
15624 });
15625 cx.run_until_parked();
15626 cx.assert_editor_state(after);
15627 };
15628
15629 // Outside bracket jumps to outside of matching bracket
15630 assert("console.logˇ(var);", "console.log(var)ˇ;");
15631 assert("console.log(var)ˇ;", "console.logˇ(var);");
15632
15633 // Inside bracket jumps to inside of matching bracket
15634 assert("console.log(ˇvar);", "console.log(varˇ);");
15635 assert("console.log(varˇ);", "console.log(ˇvar);");
15636
15637 // When outside a bracket and inside, favor jumping to the inside bracket
15638 assert(
15639 "console.log('foo', [1, 2, 3]ˇ);",
15640 "console.log(ˇ'foo', [1, 2, 3]);",
15641 );
15642 assert(
15643 "console.log(ˇ'foo', [1, 2, 3]);",
15644 "console.log('foo', [1, 2, 3]ˇ);",
15645 );
15646
15647 // Bias forward if two options are equally likely
15648 assert(
15649 "let result = curried_fun()ˇ();",
15650 "let result = curried_fun()()ˇ;",
15651 );
15652
15653 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
15654 assert(
15655 indoc! {"
15656 function test() {
15657 console.log('test')ˇ
15658 }"},
15659 indoc! {"
15660 function test() {
15661 console.logˇ('test')
15662 }"},
15663 );
15664}
15665
15666#[gpui::test]
15667async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
15668 init_test(cx, |_| {});
15669
15670 let fs = FakeFs::new(cx.executor());
15671 fs.insert_tree(
15672 path!("/a"),
15673 json!({
15674 "main.rs": "fn main() { let a = 5; }",
15675 "other.rs": "// Test file",
15676 }),
15677 )
15678 .await;
15679 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15680
15681 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15682 language_registry.add(Arc::new(Language::new(
15683 LanguageConfig {
15684 name: "Rust".into(),
15685 matcher: LanguageMatcher {
15686 path_suffixes: vec!["rs".to_string()],
15687 ..Default::default()
15688 },
15689 brackets: BracketPairConfig {
15690 pairs: vec![BracketPair {
15691 start: "{".to_string(),
15692 end: "}".to_string(),
15693 close: true,
15694 surround: true,
15695 newline: true,
15696 }],
15697 disabled_scopes_by_bracket_ix: Vec::new(),
15698 },
15699 ..Default::default()
15700 },
15701 Some(tree_sitter_rust::LANGUAGE.into()),
15702 )));
15703 let mut fake_servers = language_registry.register_fake_lsp(
15704 "Rust",
15705 FakeLspAdapter {
15706 capabilities: lsp::ServerCapabilities {
15707 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15708 first_trigger_character: "{".to_string(),
15709 more_trigger_character: None,
15710 }),
15711 ..Default::default()
15712 },
15713 ..Default::default()
15714 },
15715 );
15716
15717 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15718
15719 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15720
15721 let worktree_id = workspace
15722 .update(cx, |workspace, _, cx| {
15723 workspace.project().update(cx, |project, cx| {
15724 project.worktrees(cx).next().unwrap().read(cx).id()
15725 })
15726 })
15727 .unwrap();
15728
15729 let buffer = project
15730 .update(cx, |project, cx| {
15731 project.open_local_buffer(path!("/a/main.rs"), cx)
15732 })
15733 .await
15734 .unwrap();
15735 let editor_handle = workspace
15736 .update(cx, |workspace, window, cx| {
15737 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
15738 })
15739 .unwrap()
15740 .await
15741 .unwrap()
15742 .downcast::<Editor>()
15743 .unwrap();
15744
15745 cx.executor().start_waiting();
15746 let fake_server = fake_servers.next().await.unwrap();
15747
15748 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
15749 |params, _| async move {
15750 assert_eq!(
15751 params.text_document_position.text_document.uri,
15752 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
15753 );
15754 assert_eq!(
15755 params.text_document_position.position,
15756 lsp::Position::new(0, 21),
15757 );
15758
15759 Ok(Some(vec![lsp::TextEdit {
15760 new_text: "]".to_string(),
15761 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15762 }]))
15763 },
15764 );
15765
15766 editor_handle.update_in(cx, |editor, window, cx| {
15767 window.focus(&editor.focus_handle(cx));
15768 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15769 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
15770 });
15771 editor.handle_input("{", window, cx);
15772 });
15773
15774 cx.executor().run_until_parked();
15775
15776 buffer.update(cx, |buffer, _| {
15777 assert_eq!(
15778 buffer.text(),
15779 "fn main() { let a = {5}; }",
15780 "No extra braces from on type formatting should appear in the buffer"
15781 )
15782 });
15783}
15784
15785#[gpui::test(iterations = 20, seeds(31))]
15786async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
15787 init_test(cx, |_| {});
15788
15789 let mut cx = EditorLspTestContext::new_rust(
15790 lsp::ServerCapabilities {
15791 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15792 first_trigger_character: ".".to_string(),
15793 more_trigger_character: None,
15794 }),
15795 ..Default::default()
15796 },
15797 cx,
15798 )
15799 .await;
15800
15801 cx.update_buffer(|buffer, _| {
15802 // This causes autoindent to be async.
15803 buffer.set_sync_parse_timeout(Duration::ZERO)
15804 });
15805
15806 cx.set_state("fn c() {\n d()ˇ\n}\n");
15807 cx.simulate_keystroke("\n");
15808 cx.run_until_parked();
15809
15810 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
15811 let mut request =
15812 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
15813 let buffer_cloned = buffer_cloned.clone();
15814 async move {
15815 buffer_cloned.update(&mut cx, |buffer, _| {
15816 assert_eq!(
15817 buffer.text(),
15818 "fn c() {\n d()\n .\n}\n",
15819 "OnTypeFormatting should triggered after autoindent applied"
15820 )
15821 })?;
15822
15823 Ok(Some(vec![]))
15824 }
15825 });
15826
15827 cx.simulate_keystroke(".");
15828 cx.run_until_parked();
15829
15830 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
15831 assert!(request.next().await.is_some());
15832 request.close();
15833 assert!(request.next().await.is_none());
15834}
15835
15836#[gpui::test]
15837async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
15838 init_test(cx, |_| {});
15839
15840 let fs = FakeFs::new(cx.executor());
15841 fs.insert_tree(
15842 path!("/a"),
15843 json!({
15844 "main.rs": "fn main() { let a = 5; }",
15845 "other.rs": "// Test file",
15846 }),
15847 )
15848 .await;
15849
15850 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15851
15852 let server_restarts = Arc::new(AtomicUsize::new(0));
15853 let closure_restarts = Arc::clone(&server_restarts);
15854 let language_server_name = "test language server";
15855 let language_name: LanguageName = "Rust".into();
15856
15857 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15858 language_registry.add(Arc::new(Language::new(
15859 LanguageConfig {
15860 name: language_name.clone(),
15861 matcher: LanguageMatcher {
15862 path_suffixes: vec!["rs".to_string()],
15863 ..Default::default()
15864 },
15865 ..Default::default()
15866 },
15867 Some(tree_sitter_rust::LANGUAGE.into()),
15868 )));
15869 let mut fake_servers = language_registry.register_fake_lsp(
15870 "Rust",
15871 FakeLspAdapter {
15872 name: language_server_name,
15873 initialization_options: Some(json!({
15874 "testOptionValue": true
15875 })),
15876 initializer: Some(Box::new(move |fake_server| {
15877 let task_restarts = Arc::clone(&closure_restarts);
15878 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
15879 task_restarts.fetch_add(1, atomic::Ordering::Release);
15880 futures::future::ready(Ok(()))
15881 });
15882 })),
15883 ..Default::default()
15884 },
15885 );
15886
15887 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15888 let _buffer = project
15889 .update(cx, |project, cx| {
15890 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
15891 })
15892 .await
15893 .unwrap();
15894 let _fake_server = fake_servers.next().await.unwrap();
15895 update_test_language_settings(cx, |language_settings| {
15896 language_settings.languages.0.insert(
15897 language_name.clone(),
15898 LanguageSettingsContent {
15899 tab_size: NonZeroU32::new(8),
15900 ..Default::default()
15901 },
15902 );
15903 });
15904 cx.executor().run_until_parked();
15905 assert_eq!(
15906 server_restarts.load(atomic::Ordering::Acquire),
15907 0,
15908 "Should not restart LSP server on an unrelated change"
15909 );
15910
15911 update_test_project_settings(cx, |project_settings| {
15912 project_settings.lsp.insert(
15913 "Some other server name".into(),
15914 LspSettings {
15915 binary: None,
15916 settings: None,
15917 initialization_options: Some(json!({
15918 "some other init value": false
15919 })),
15920 enable_lsp_tasks: false,
15921 },
15922 );
15923 });
15924 cx.executor().run_until_parked();
15925 assert_eq!(
15926 server_restarts.load(atomic::Ordering::Acquire),
15927 0,
15928 "Should not restart LSP server on an unrelated LSP settings change"
15929 );
15930
15931 update_test_project_settings(cx, |project_settings| {
15932 project_settings.lsp.insert(
15933 language_server_name.into(),
15934 LspSettings {
15935 binary: None,
15936 settings: None,
15937 initialization_options: Some(json!({
15938 "anotherInitValue": false
15939 })),
15940 enable_lsp_tasks: false,
15941 },
15942 );
15943 });
15944 cx.executor().run_until_parked();
15945 assert_eq!(
15946 server_restarts.load(atomic::Ordering::Acquire),
15947 1,
15948 "Should restart LSP server on a related LSP settings change"
15949 );
15950
15951 update_test_project_settings(cx, |project_settings| {
15952 project_settings.lsp.insert(
15953 language_server_name.into(),
15954 LspSettings {
15955 binary: None,
15956 settings: None,
15957 initialization_options: Some(json!({
15958 "anotherInitValue": false
15959 })),
15960 enable_lsp_tasks: false,
15961 },
15962 );
15963 });
15964 cx.executor().run_until_parked();
15965 assert_eq!(
15966 server_restarts.load(atomic::Ordering::Acquire),
15967 1,
15968 "Should not restart LSP server on a related LSP settings change that is the same"
15969 );
15970
15971 update_test_project_settings(cx, |project_settings| {
15972 project_settings.lsp.insert(
15973 language_server_name.into(),
15974 LspSettings {
15975 binary: None,
15976 settings: None,
15977 initialization_options: None,
15978 enable_lsp_tasks: false,
15979 },
15980 );
15981 });
15982 cx.executor().run_until_parked();
15983 assert_eq!(
15984 server_restarts.load(atomic::Ordering::Acquire),
15985 2,
15986 "Should restart LSP server on another related LSP settings change"
15987 );
15988}
15989
15990#[gpui::test]
15991async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
15992 init_test(cx, |_| {});
15993
15994 let mut cx = EditorLspTestContext::new_rust(
15995 lsp::ServerCapabilities {
15996 completion_provider: Some(lsp::CompletionOptions {
15997 trigger_characters: Some(vec![".".to_string()]),
15998 resolve_provider: Some(true),
15999 ..Default::default()
16000 }),
16001 ..Default::default()
16002 },
16003 cx,
16004 )
16005 .await;
16006
16007 cx.set_state("fn main() { let a = 2ˇ; }");
16008 cx.simulate_keystroke(".");
16009 let completion_item = lsp::CompletionItem {
16010 label: "some".into(),
16011 kind: Some(lsp::CompletionItemKind::SNIPPET),
16012 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
16013 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
16014 kind: lsp::MarkupKind::Markdown,
16015 value: "```rust\nSome(2)\n```".to_string(),
16016 })),
16017 deprecated: Some(false),
16018 sort_text: Some("fffffff2".to_string()),
16019 filter_text: Some("some".to_string()),
16020 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
16021 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16022 range: lsp::Range {
16023 start: lsp::Position {
16024 line: 0,
16025 character: 22,
16026 },
16027 end: lsp::Position {
16028 line: 0,
16029 character: 22,
16030 },
16031 },
16032 new_text: "Some(2)".to_string(),
16033 })),
16034 additional_text_edits: Some(vec![lsp::TextEdit {
16035 range: lsp::Range {
16036 start: lsp::Position {
16037 line: 0,
16038 character: 20,
16039 },
16040 end: lsp::Position {
16041 line: 0,
16042 character: 22,
16043 },
16044 },
16045 new_text: "".to_string(),
16046 }]),
16047 ..Default::default()
16048 };
16049
16050 let closure_completion_item = completion_item.clone();
16051 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16052 let task_completion_item = closure_completion_item.clone();
16053 async move {
16054 Ok(Some(lsp::CompletionResponse::Array(vec![
16055 task_completion_item,
16056 ])))
16057 }
16058 });
16059
16060 request.next().await;
16061
16062 cx.condition(|editor, _| editor.context_menu_visible())
16063 .await;
16064 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
16065 editor
16066 .confirm_completion(&ConfirmCompletion::default(), window, cx)
16067 .unwrap()
16068 });
16069 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
16070
16071 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
16072 let task_completion_item = completion_item.clone();
16073 async move { Ok(task_completion_item) }
16074 })
16075 .next()
16076 .await
16077 .unwrap();
16078 apply_additional_edits.await.unwrap();
16079 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
16080}
16081
16082#[gpui::test]
16083async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
16084 init_test(cx, |_| {});
16085
16086 let mut cx = EditorLspTestContext::new_rust(
16087 lsp::ServerCapabilities {
16088 completion_provider: Some(lsp::CompletionOptions {
16089 trigger_characters: Some(vec![".".to_string()]),
16090 resolve_provider: Some(true),
16091 ..Default::default()
16092 }),
16093 ..Default::default()
16094 },
16095 cx,
16096 )
16097 .await;
16098
16099 cx.set_state("fn main() { let a = 2ˇ; }");
16100 cx.simulate_keystroke(".");
16101
16102 let item1 = lsp::CompletionItem {
16103 label: "method id()".to_string(),
16104 filter_text: Some("id".to_string()),
16105 detail: None,
16106 documentation: None,
16107 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16108 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16109 new_text: ".id".to_string(),
16110 })),
16111 ..lsp::CompletionItem::default()
16112 };
16113
16114 let item2 = lsp::CompletionItem {
16115 label: "other".to_string(),
16116 filter_text: Some("other".to_string()),
16117 detail: None,
16118 documentation: None,
16119 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16120 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16121 new_text: ".other".to_string(),
16122 })),
16123 ..lsp::CompletionItem::default()
16124 };
16125
16126 let item1 = item1.clone();
16127 cx.set_request_handler::<lsp::request::Completion, _, _>({
16128 let item1 = item1.clone();
16129 move |_, _, _| {
16130 let item1 = item1.clone();
16131 let item2 = item2.clone();
16132 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
16133 }
16134 })
16135 .next()
16136 .await;
16137
16138 cx.condition(|editor, _| editor.context_menu_visible())
16139 .await;
16140 cx.update_editor(|editor, _, _| {
16141 let context_menu = editor.context_menu.borrow_mut();
16142 let context_menu = context_menu
16143 .as_ref()
16144 .expect("Should have the context menu deployed");
16145 match context_menu {
16146 CodeContextMenu::Completions(completions_menu) => {
16147 let completions = completions_menu.completions.borrow_mut();
16148 assert_eq!(
16149 completions
16150 .iter()
16151 .map(|completion| &completion.label.text)
16152 .collect::<Vec<_>>(),
16153 vec!["method id()", "other"]
16154 )
16155 }
16156 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
16157 }
16158 });
16159
16160 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
16161 let item1 = item1.clone();
16162 move |_, item_to_resolve, _| {
16163 let item1 = item1.clone();
16164 async move {
16165 if item1 == item_to_resolve {
16166 Ok(lsp::CompletionItem {
16167 label: "method id()".to_string(),
16168 filter_text: Some("id".to_string()),
16169 detail: Some("Now resolved!".to_string()),
16170 documentation: Some(lsp::Documentation::String("Docs".to_string())),
16171 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16172 range: lsp::Range::new(
16173 lsp::Position::new(0, 22),
16174 lsp::Position::new(0, 22),
16175 ),
16176 new_text: ".id".to_string(),
16177 })),
16178 ..lsp::CompletionItem::default()
16179 })
16180 } else {
16181 Ok(item_to_resolve)
16182 }
16183 }
16184 }
16185 })
16186 .next()
16187 .await
16188 .unwrap();
16189 cx.run_until_parked();
16190
16191 cx.update_editor(|editor, window, cx| {
16192 editor.context_menu_next(&Default::default(), window, cx);
16193 });
16194
16195 cx.update_editor(|editor, _, _| {
16196 let context_menu = editor.context_menu.borrow_mut();
16197 let context_menu = context_menu
16198 .as_ref()
16199 .expect("Should have the context menu deployed");
16200 match context_menu {
16201 CodeContextMenu::Completions(completions_menu) => {
16202 let completions = completions_menu.completions.borrow_mut();
16203 assert_eq!(
16204 completions
16205 .iter()
16206 .map(|completion| &completion.label.text)
16207 .collect::<Vec<_>>(),
16208 vec!["method id() Now resolved!", "other"],
16209 "Should update first completion label, but not second as the filter text did not match."
16210 );
16211 }
16212 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
16213 }
16214 });
16215}
16216
16217#[gpui::test]
16218async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
16219 init_test(cx, |_| {});
16220 let mut cx = EditorLspTestContext::new_rust(
16221 lsp::ServerCapabilities {
16222 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
16223 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
16224 completion_provider: Some(lsp::CompletionOptions {
16225 resolve_provider: Some(true),
16226 ..Default::default()
16227 }),
16228 ..Default::default()
16229 },
16230 cx,
16231 )
16232 .await;
16233 cx.set_state(indoc! {"
16234 struct TestStruct {
16235 field: i32
16236 }
16237
16238 fn mainˇ() {
16239 let unused_var = 42;
16240 let test_struct = TestStruct { field: 42 };
16241 }
16242 "});
16243 let symbol_range = cx.lsp_range(indoc! {"
16244 struct TestStruct {
16245 field: i32
16246 }
16247
16248 «fn main»() {
16249 let unused_var = 42;
16250 let test_struct = TestStruct { field: 42 };
16251 }
16252 "});
16253 let mut hover_requests =
16254 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
16255 Ok(Some(lsp::Hover {
16256 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
16257 kind: lsp::MarkupKind::Markdown,
16258 value: "Function documentation".to_string(),
16259 }),
16260 range: Some(symbol_range),
16261 }))
16262 });
16263
16264 // Case 1: Test that code action menu hide hover popover
16265 cx.dispatch_action(Hover);
16266 hover_requests.next().await;
16267 cx.condition(|editor, _| editor.hover_state.visible()).await;
16268 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
16269 move |_, _, _| async move {
16270 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
16271 lsp::CodeAction {
16272 title: "Remove unused variable".to_string(),
16273 kind: Some(CodeActionKind::QUICKFIX),
16274 edit: Some(lsp::WorkspaceEdit {
16275 changes: Some(
16276 [(
16277 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
16278 vec![lsp::TextEdit {
16279 range: lsp::Range::new(
16280 lsp::Position::new(5, 4),
16281 lsp::Position::new(5, 27),
16282 ),
16283 new_text: "".to_string(),
16284 }],
16285 )]
16286 .into_iter()
16287 .collect(),
16288 ),
16289 ..Default::default()
16290 }),
16291 ..Default::default()
16292 },
16293 )]))
16294 },
16295 );
16296 cx.update_editor(|editor, window, cx| {
16297 editor.toggle_code_actions(
16298 &ToggleCodeActions {
16299 deployed_from: None,
16300 quick_launch: false,
16301 },
16302 window,
16303 cx,
16304 );
16305 });
16306 code_action_requests.next().await;
16307 cx.run_until_parked();
16308 cx.condition(|editor, _| editor.context_menu_visible())
16309 .await;
16310 cx.update_editor(|editor, _, _| {
16311 assert!(
16312 !editor.hover_state.visible(),
16313 "Hover popover should be hidden when code action menu is shown"
16314 );
16315 // Hide code actions
16316 editor.context_menu.take();
16317 });
16318
16319 // Case 2: Test that code completions hide hover popover
16320 cx.dispatch_action(Hover);
16321 hover_requests.next().await;
16322 cx.condition(|editor, _| editor.hover_state.visible()).await;
16323 let counter = Arc::new(AtomicUsize::new(0));
16324 let mut completion_requests =
16325 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16326 let counter = counter.clone();
16327 async move {
16328 counter.fetch_add(1, atomic::Ordering::Release);
16329 Ok(Some(lsp::CompletionResponse::Array(vec![
16330 lsp::CompletionItem {
16331 label: "main".into(),
16332 kind: Some(lsp::CompletionItemKind::FUNCTION),
16333 detail: Some("() -> ()".to_string()),
16334 ..Default::default()
16335 },
16336 lsp::CompletionItem {
16337 label: "TestStruct".into(),
16338 kind: Some(lsp::CompletionItemKind::STRUCT),
16339 detail: Some("struct TestStruct".to_string()),
16340 ..Default::default()
16341 },
16342 ])))
16343 }
16344 });
16345 cx.update_editor(|editor, window, cx| {
16346 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
16347 });
16348 completion_requests.next().await;
16349 cx.condition(|editor, _| editor.context_menu_visible())
16350 .await;
16351 cx.update_editor(|editor, _, _| {
16352 assert!(
16353 !editor.hover_state.visible(),
16354 "Hover popover should be hidden when completion menu is shown"
16355 );
16356 });
16357}
16358
16359#[gpui::test]
16360async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
16361 init_test(cx, |_| {});
16362
16363 let mut cx = EditorLspTestContext::new_rust(
16364 lsp::ServerCapabilities {
16365 completion_provider: Some(lsp::CompletionOptions {
16366 trigger_characters: Some(vec![".".to_string()]),
16367 resolve_provider: Some(true),
16368 ..Default::default()
16369 }),
16370 ..Default::default()
16371 },
16372 cx,
16373 )
16374 .await;
16375
16376 cx.set_state("fn main() { let a = 2ˇ; }");
16377 cx.simulate_keystroke(".");
16378
16379 let unresolved_item_1 = lsp::CompletionItem {
16380 label: "id".to_string(),
16381 filter_text: Some("id".to_string()),
16382 detail: None,
16383 documentation: None,
16384 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16385 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16386 new_text: ".id".to_string(),
16387 })),
16388 ..lsp::CompletionItem::default()
16389 };
16390 let resolved_item_1 = lsp::CompletionItem {
16391 additional_text_edits: Some(vec![lsp::TextEdit {
16392 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
16393 new_text: "!!".to_string(),
16394 }]),
16395 ..unresolved_item_1.clone()
16396 };
16397 let unresolved_item_2 = lsp::CompletionItem {
16398 label: "other".to_string(),
16399 filter_text: Some("other".to_string()),
16400 detail: None,
16401 documentation: None,
16402 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16403 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16404 new_text: ".other".to_string(),
16405 })),
16406 ..lsp::CompletionItem::default()
16407 };
16408 let resolved_item_2 = lsp::CompletionItem {
16409 additional_text_edits: Some(vec![lsp::TextEdit {
16410 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
16411 new_text: "??".to_string(),
16412 }]),
16413 ..unresolved_item_2.clone()
16414 };
16415
16416 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
16417 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
16418 cx.lsp
16419 .server
16420 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
16421 let unresolved_item_1 = unresolved_item_1.clone();
16422 let resolved_item_1 = resolved_item_1.clone();
16423 let unresolved_item_2 = unresolved_item_2.clone();
16424 let resolved_item_2 = resolved_item_2.clone();
16425 let resolve_requests_1 = resolve_requests_1.clone();
16426 let resolve_requests_2 = resolve_requests_2.clone();
16427 move |unresolved_request, _| {
16428 let unresolved_item_1 = unresolved_item_1.clone();
16429 let resolved_item_1 = resolved_item_1.clone();
16430 let unresolved_item_2 = unresolved_item_2.clone();
16431 let resolved_item_2 = resolved_item_2.clone();
16432 let resolve_requests_1 = resolve_requests_1.clone();
16433 let resolve_requests_2 = resolve_requests_2.clone();
16434 async move {
16435 if unresolved_request == unresolved_item_1 {
16436 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
16437 Ok(resolved_item_1.clone())
16438 } else if unresolved_request == unresolved_item_2 {
16439 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
16440 Ok(resolved_item_2.clone())
16441 } else {
16442 panic!("Unexpected completion item {unresolved_request:?}")
16443 }
16444 }
16445 }
16446 })
16447 .detach();
16448
16449 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16450 let unresolved_item_1 = unresolved_item_1.clone();
16451 let unresolved_item_2 = unresolved_item_2.clone();
16452 async move {
16453 Ok(Some(lsp::CompletionResponse::Array(vec![
16454 unresolved_item_1,
16455 unresolved_item_2,
16456 ])))
16457 }
16458 })
16459 .next()
16460 .await;
16461
16462 cx.condition(|editor, _| editor.context_menu_visible())
16463 .await;
16464 cx.update_editor(|editor, _, _| {
16465 let context_menu = editor.context_menu.borrow_mut();
16466 let context_menu = context_menu
16467 .as_ref()
16468 .expect("Should have the context menu deployed");
16469 match context_menu {
16470 CodeContextMenu::Completions(completions_menu) => {
16471 let completions = completions_menu.completions.borrow_mut();
16472 assert_eq!(
16473 completions
16474 .iter()
16475 .map(|completion| &completion.label.text)
16476 .collect::<Vec<_>>(),
16477 vec!["id", "other"]
16478 )
16479 }
16480 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
16481 }
16482 });
16483 cx.run_until_parked();
16484
16485 cx.update_editor(|editor, window, cx| {
16486 editor.context_menu_next(&ContextMenuNext, window, cx);
16487 });
16488 cx.run_until_parked();
16489 cx.update_editor(|editor, window, cx| {
16490 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
16491 });
16492 cx.run_until_parked();
16493 cx.update_editor(|editor, window, cx| {
16494 editor.context_menu_next(&ContextMenuNext, window, cx);
16495 });
16496 cx.run_until_parked();
16497 cx.update_editor(|editor, window, cx| {
16498 editor
16499 .compose_completion(&ComposeCompletion::default(), window, cx)
16500 .expect("No task returned")
16501 })
16502 .await
16503 .expect("Completion failed");
16504 cx.run_until_parked();
16505
16506 cx.update_editor(|editor, _, cx| {
16507 assert_eq!(
16508 resolve_requests_1.load(atomic::Ordering::Acquire),
16509 1,
16510 "Should always resolve once despite multiple selections"
16511 );
16512 assert_eq!(
16513 resolve_requests_2.load(atomic::Ordering::Acquire),
16514 1,
16515 "Should always resolve once after multiple selections and applying the completion"
16516 );
16517 assert_eq!(
16518 editor.text(cx),
16519 "fn main() { let a = ??.other; }",
16520 "Should use resolved data when applying the completion"
16521 );
16522 });
16523}
16524
16525#[gpui::test]
16526async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
16527 init_test(cx, |_| {});
16528
16529 let item_0 = lsp::CompletionItem {
16530 label: "abs".into(),
16531 insert_text: Some("abs".into()),
16532 data: Some(json!({ "very": "special"})),
16533 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
16534 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
16535 lsp::InsertReplaceEdit {
16536 new_text: "abs".to_string(),
16537 insert: lsp::Range::default(),
16538 replace: lsp::Range::default(),
16539 },
16540 )),
16541 ..lsp::CompletionItem::default()
16542 };
16543 let items = iter::once(item_0.clone())
16544 .chain((11..51).map(|i| lsp::CompletionItem {
16545 label: format!("item_{}", i),
16546 insert_text: Some(format!("item_{}", i)),
16547 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
16548 ..lsp::CompletionItem::default()
16549 }))
16550 .collect::<Vec<_>>();
16551
16552 let default_commit_characters = vec!["?".to_string()];
16553 let default_data = json!({ "default": "data"});
16554 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
16555 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
16556 let default_edit_range = lsp::Range {
16557 start: lsp::Position {
16558 line: 0,
16559 character: 5,
16560 },
16561 end: lsp::Position {
16562 line: 0,
16563 character: 5,
16564 },
16565 };
16566
16567 let mut cx = EditorLspTestContext::new_rust(
16568 lsp::ServerCapabilities {
16569 completion_provider: Some(lsp::CompletionOptions {
16570 trigger_characters: Some(vec![".".to_string()]),
16571 resolve_provider: Some(true),
16572 ..Default::default()
16573 }),
16574 ..Default::default()
16575 },
16576 cx,
16577 )
16578 .await;
16579
16580 cx.set_state("fn main() { let a = 2ˇ; }");
16581 cx.simulate_keystroke(".");
16582
16583 let completion_data = default_data.clone();
16584 let completion_characters = default_commit_characters.clone();
16585 let completion_items = items.clone();
16586 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16587 let default_data = completion_data.clone();
16588 let default_commit_characters = completion_characters.clone();
16589 let items = completion_items.clone();
16590 async move {
16591 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16592 items,
16593 item_defaults: Some(lsp::CompletionListItemDefaults {
16594 data: Some(default_data.clone()),
16595 commit_characters: Some(default_commit_characters.clone()),
16596 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
16597 default_edit_range,
16598 )),
16599 insert_text_format: Some(default_insert_text_format),
16600 insert_text_mode: Some(default_insert_text_mode),
16601 }),
16602 ..lsp::CompletionList::default()
16603 })))
16604 }
16605 })
16606 .next()
16607 .await;
16608
16609 let resolved_items = Arc::new(Mutex::new(Vec::new()));
16610 cx.lsp
16611 .server
16612 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
16613 let closure_resolved_items = resolved_items.clone();
16614 move |item_to_resolve, _| {
16615 let closure_resolved_items = closure_resolved_items.clone();
16616 async move {
16617 closure_resolved_items.lock().push(item_to_resolve.clone());
16618 Ok(item_to_resolve)
16619 }
16620 }
16621 })
16622 .detach();
16623
16624 cx.condition(|editor, _| editor.context_menu_visible())
16625 .await;
16626 cx.run_until_parked();
16627 cx.update_editor(|editor, _, _| {
16628 let menu = editor.context_menu.borrow_mut();
16629 match menu.as_ref().expect("should have the completions menu") {
16630 CodeContextMenu::Completions(completions_menu) => {
16631 assert_eq!(
16632 completions_menu
16633 .entries
16634 .borrow()
16635 .iter()
16636 .map(|mat| mat.string.clone())
16637 .collect::<Vec<String>>(),
16638 items
16639 .iter()
16640 .map(|completion| completion.label.clone())
16641 .collect::<Vec<String>>()
16642 );
16643 }
16644 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
16645 }
16646 });
16647 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
16648 // with 4 from the end.
16649 assert_eq!(
16650 *resolved_items.lock(),
16651 [&items[0..16], &items[items.len() - 4..items.len()]]
16652 .concat()
16653 .iter()
16654 .cloned()
16655 .map(|mut item| {
16656 if item.data.is_none() {
16657 item.data = Some(default_data.clone());
16658 }
16659 item
16660 })
16661 .collect::<Vec<lsp::CompletionItem>>(),
16662 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
16663 );
16664 resolved_items.lock().clear();
16665
16666 cx.update_editor(|editor, window, cx| {
16667 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
16668 });
16669 cx.run_until_parked();
16670 // Completions that have already been resolved are skipped.
16671 assert_eq!(
16672 *resolved_items.lock(),
16673 items[items.len() - 17..items.len() - 4]
16674 .iter()
16675 .cloned()
16676 .map(|mut item| {
16677 if item.data.is_none() {
16678 item.data = Some(default_data.clone());
16679 }
16680 item
16681 })
16682 .collect::<Vec<lsp::CompletionItem>>()
16683 );
16684 resolved_items.lock().clear();
16685}
16686
16687#[gpui::test]
16688async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
16689 init_test(cx, |_| {});
16690
16691 let mut cx = EditorLspTestContext::new(
16692 Language::new(
16693 LanguageConfig {
16694 matcher: LanguageMatcher {
16695 path_suffixes: vec!["jsx".into()],
16696 ..Default::default()
16697 },
16698 overrides: [(
16699 "element".into(),
16700 LanguageConfigOverride {
16701 completion_query_characters: Override::Set(['-'].into_iter().collect()),
16702 ..Default::default()
16703 },
16704 )]
16705 .into_iter()
16706 .collect(),
16707 ..Default::default()
16708 },
16709 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16710 )
16711 .with_override_query("(jsx_self_closing_element) @element")
16712 .unwrap(),
16713 lsp::ServerCapabilities {
16714 completion_provider: Some(lsp::CompletionOptions {
16715 trigger_characters: Some(vec![":".to_string()]),
16716 ..Default::default()
16717 }),
16718 ..Default::default()
16719 },
16720 cx,
16721 )
16722 .await;
16723
16724 cx.lsp
16725 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16726 Ok(Some(lsp::CompletionResponse::Array(vec![
16727 lsp::CompletionItem {
16728 label: "bg-blue".into(),
16729 ..Default::default()
16730 },
16731 lsp::CompletionItem {
16732 label: "bg-red".into(),
16733 ..Default::default()
16734 },
16735 lsp::CompletionItem {
16736 label: "bg-yellow".into(),
16737 ..Default::default()
16738 },
16739 ])))
16740 });
16741
16742 cx.set_state(r#"<p class="bgˇ" />"#);
16743
16744 // Trigger completion when typing a dash, because the dash is an extra
16745 // word character in the 'element' scope, which contains the cursor.
16746 cx.simulate_keystroke("-");
16747 cx.executor().run_until_parked();
16748 cx.update_editor(|editor, _, _| {
16749 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16750 {
16751 assert_eq!(
16752 completion_menu_entries(menu),
16753 &["bg-blue", "bg-red", "bg-yellow"]
16754 );
16755 } else {
16756 panic!("expected completion menu to be open");
16757 }
16758 });
16759
16760 cx.simulate_keystroke("l");
16761 cx.executor().run_until_parked();
16762 cx.update_editor(|editor, _, _| {
16763 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16764 {
16765 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
16766 } else {
16767 panic!("expected completion menu to be open");
16768 }
16769 });
16770
16771 // When filtering completions, consider the character after the '-' to
16772 // be the start of a subword.
16773 cx.set_state(r#"<p class="yelˇ" />"#);
16774 cx.simulate_keystroke("l");
16775 cx.executor().run_until_parked();
16776 cx.update_editor(|editor, _, _| {
16777 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16778 {
16779 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
16780 } else {
16781 panic!("expected completion menu to be open");
16782 }
16783 });
16784}
16785
16786fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
16787 let entries = menu.entries.borrow();
16788 entries.iter().map(|mat| mat.string.clone()).collect()
16789}
16790
16791#[gpui::test]
16792async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
16793 init_test(cx, |settings| {
16794 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
16795 Formatter::Prettier,
16796 )))
16797 });
16798
16799 let fs = FakeFs::new(cx.executor());
16800 fs.insert_file(path!("/file.ts"), Default::default()).await;
16801
16802 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
16803 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16804
16805 language_registry.add(Arc::new(Language::new(
16806 LanguageConfig {
16807 name: "TypeScript".into(),
16808 matcher: LanguageMatcher {
16809 path_suffixes: vec!["ts".to_string()],
16810 ..Default::default()
16811 },
16812 ..Default::default()
16813 },
16814 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
16815 )));
16816 update_test_language_settings(cx, |settings| {
16817 settings.defaults.prettier = Some(PrettierSettings {
16818 allowed: true,
16819 ..PrettierSettings::default()
16820 });
16821 });
16822
16823 let test_plugin = "test_plugin";
16824 let _ = language_registry.register_fake_lsp(
16825 "TypeScript",
16826 FakeLspAdapter {
16827 prettier_plugins: vec![test_plugin],
16828 ..Default::default()
16829 },
16830 );
16831
16832 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
16833 let buffer = project
16834 .update(cx, |project, cx| {
16835 project.open_local_buffer(path!("/file.ts"), cx)
16836 })
16837 .await
16838 .unwrap();
16839
16840 let buffer_text = "one\ntwo\nthree\n";
16841 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16842 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16843 editor.update_in(cx, |editor, window, cx| {
16844 editor.set_text(buffer_text, window, cx)
16845 });
16846
16847 editor
16848 .update_in(cx, |editor, window, cx| {
16849 editor.perform_format(
16850 project.clone(),
16851 FormatTrigger::Manual,
16852 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16853 window,
16854 cx,
16855 )
16856 })
16857 .unwrap()
16858 .await;
16859 assert_eq!(
16860 editor.update(cx, |editor, cx| editor.text(cx)),
16861 buffer_text.to_string() + prettier_format_suffix,
16862 "Test prettier formatting was not applied to the original buffer text",
16863 );
16864
16865 update_test_language_settings(cx, |settings| {
16866 settings.defaults.formatter = Some(SelectedFormatter::Auto)
16867 });
16868 let format = editor.update_in(cx, |editor, window, cx| {
16869 editor.perform_format(
16870 project.clone(),
16871 FormatTrigger::Manual,
16872 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16873 window,
16874 cx,
16875 )
16876 });
16877 format.await.unwrap();
16878 assert_eq!(
16879 editor.update(cx, |editor, cx| editor.text(cx)),
16880 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
16881 "Autoformatting (via test prettier) was not applied to the original buffer text",
16882 );
16883}
16884
16885#[gpui::test]
16886async fn test_addition_reverts(cx: &mut TestAppContext) {
16887 init_test(cx, |_| {});
16888 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16889 let base_text = indoc! {r#"
16890 struct Row;
16891 struct Row1;
16892 struct Row2;
16893
16894 struct Row4;
16895 struct Row5;
16896 struct Row6;
16897
16898 struct Row8;
16899 struct Row9;
16900 struct Row10;"#};
16901
16902 // When addition hunks are not adjacent to carets, no hunk revert is performed
16903 assert_hunk_revert(
16904 indoc! {r#"struct Row;
16905 struct Row1;
16906 struct Row1.1;
16907 struct Row1.2;
16908 struct Row2;ˇ
16909
16910 struct Row4;
16911 struct Row5;
16912 struct Row6;
16913
16914 struct Row8;
16915 ˇstruct Row9;
16916 struct Row9.1;
16917 struct Row9.2;
16918 struct Row9.3;
16919 struct Row10;"#},
16920 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16921 indoc! {r#"struct Row;
16922 struct Row1;
16923 struct Row1.1;
16924 struct Row1.2;
16925 struct Row2;ˇ
16926
16927 struct Row4;
16928 struct Row5;
16929 struct Row6;
16930
16931 struct Row8;
16932 ˇstruct Row9;
16933 struct Row9.1;
16934 struct Row9.2;
16935 struct Row9.3;
16936 struct Row10;"#},
16937 base_text,
16938 &mut cx,
16939 );
16940 // Same for selections
16941 assert_hunk_revert(
16942 indoc! {r#"struct Row;
16943 struct Row1;
16944 struct Row2;
16945 struct Row2.1;
16946 struct Row2.2;
16947 «ˇ
16948 struct Row4;
16949 struct» Row5;
16950 «struct Row6;
16951 ˇ»
16952 struct Row9.1;
16953 struct Row9.2;
16954 struct Row9.3;
16955 struct Row8;
16956 struct Row9;
16957 struct Row10;"#},
16958 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16959 indoc! {r#"struct Row;
16960 struct Row1;
16961 struct Row2;
16962 struct Row2.1;
16963 struct Row2.2;
16964 «ˇ
16965 struct Row4;
16966 struct» Row5;
16967 «struct Row6;
16968 ˇ»
16969 struct Row9.1;
16970 struct Row9.2;
16971 struct Row9.3;
16972 struct Row8;
16973 struct Row9;
16974 struct Row10;"#},
16975 base_text,
16976 &mut cx,
16977 );
16978
16979 // When carets and selections intersect the addition hunks, those are reverted.
16980 // Adjacent carets got merged.
16981 assert_hunk_revert(
16982 indoc! {r#"struct Row;
16983 ˇ// something on the top
16984 struct Row1;
16985 struct Row2;
16986 struct Roˇw3.1;
16987 struct Row2.2;
16988 struct Row2.3;ˇ
16989
16990 struct Row4;
16991 struct ˇRow5.1;
16992 struct Row5.2;
16993 struct «Rowˇ»5.3;
16994 struct Row5;
16995 struct Row6;
16996 ˇ
16997 struct Row9.1;
16998 struct «Rowˇ»9.2;
16999 struct «ˇRow»9.3;
17000 struct Row8;
17001 struct Row9;
17002 «ˇ// something on bottom»
17003 struct Row10;"#},
17004 vec![
17005 DiffHunkStatusKind::Added,
17006 DiffHunkStatusKind::Added,
17007 DiffHunkStatusKind::Added,
17008 DiffHunkStatusKind::Added,
17009 DiffHunkStatusKind::Added,
17010 ],
17011 indoc! {r#"struct Row;
17012 ˇstruct Row1;
17013 struct Row2;
17014 ˇ
17015 struct Row4;
17016 ˇstruct Row5;
17017 struct Row6;
17018 ˇ
17019 ˇstruct Row8;
17020 struct Row9;
17021 ˇstruct Row10;"#},
17022 base_text,
17023 &mut cx,
17024 );
17025}
17026
17027#[gpui::test]
17028async fn test_modification_reverts(cx: &mut TestAppContext) {
17029 init_test(cx, |_| {});
17030 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17031 let base_text = indoc! {r#"
17032 struct Row;
17033 struct Row1;
17034 struct Row2;
17035
17036 struct Row4;
17037 struct Row5;
17038 struct Row6;
17039
17040 struct Row8;
17041 struct Row9;
17042 struct Row10;"#};
17043
17044 // Modification hunks behave the same as the addition ones.
17045 assert_hunk_revert(
17046 indoc! {r#"struct Row;
17047 struct Row1;
17048 struct Row33;
17049 ˇ
17050 struct Row4;
17051 struct Row5;
17052 struct Row6;
17053 ˇ
17054 struct Row99;
17055 struct Row9;
17056 struct Row10;"#},
17057 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
17058 indoc! {r#"struct Row;
17059 struct Row1;
17060 struct Row33;
17061 ˇ
17062 struct Row4;
17063 struct Row5;
17064 struct Row6;
17065 ˇ
17066 struct Row99;
17067 struct Row9;
17068 struct Row10;"#},
17069 base_text,
17070 &mut cx,
17071 );
17072 assert_hunk_revert(
17073 indoc! {r#"struct Row;
17074 struct Row1;
17075 struct Row33;
17076 «ˇ
17077 struct Row4;
17078 struct» Row5;
17079 «struct Row6;
17080 ˇ»
17081 struct Row99;
17082 struct Row9;
17083 struct Row10;"#},
17084 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
17085 indoc! {r#"struct Row;
17086 struct Row1;
17087 struct Row33;
17088 «ˇ
17089 struct Row4;
17090 struct» Row5;
17091 «struct Row6;
17092 ˇ»
17093 struct Row99;
17094 struct Row9;
17095 struct Row10;"#},
17096 base_text,
17097 &mut cx,
17098 );
17099
17100 assert_hunk_revert(
17101 indoc! {r#"ˇstruct Row1.1;
17102 struct Row1;
17103 «ˇstr»uct Row22;
17104
17105 struct ˇRow44;
17106 struct Row5;
17107 struct «Rˇ»ow66;ˇ
17108
17109 «struˇ»ct Row88;
17110 struct Row9;
17111 struct Row1011;ˇ"#},
17112 vec![
17113 DiffHunkStatusKind::Modified,
17114 DiffHunkStatusKind::Modified,
17115 DiffHunkStatusKind::Modified,
17116 DiffHunkStatusKind::Modified,
17117 DiffHunkStatusKind::Modified,
17118 DiffHunkStatusKind::Modified,
17119 ],
17120 indoc! {r#"struct Row;
17121 ˇstruct Row1;
17122 struct Row2;
17123 ˇ
17124 struct Row4;
17125 ˇstruct Row5;
17126 struct Row6;
17127 ˇ
17128 struct Row8;
17129 ˇstruct Row9;
17130 struct Row10;ˇ"#},
17131 base_text,
17132 &mut cx,
17133 );
17134}
17135
17136#[gpui::test]
17137async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
17138 init_test(cx, |_| {});
17139 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17140 let base_text = indoc! {r#"
17141 one
17142
17143 two
17144 three
17145 "#};
17146
17147 cx.set_head_text(base_text);
17148 cx.set_state("\nˇ\n");
17149 cx.executor().run_until_parked();
17150 cx.update_editor(|editor, _window, cx| {
17151 editor.expand_selected_diff_hunks(cx);
17152 });
17153 cx.executor().run_until_parked();
17154 cx.update_editor(|editor, window, cx| {
17155 editor.backspace(&Default::default(), window, cx);
17156 });
17157 cx.run_until_parked();
17158 cx.assert_state_with_diff(
17159 indoc! {r#"
17160
17161 - two
17162 - threeˇ
17163 +
17164 "#}
17165 .to_string(),
17166 );
17167}
17168
17169#[gpui::test]
17170async fn test_deletion_reverts(cx: &mut TestAppContext) {
17171 init_test(cx, |_| {});
17172 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17173 let base_text = indoc! {r#"struct Row;
17174struct Row1;
17175struct Row2;
17176
17177struct Row4;
17178struct Row5;
17179struct Row6;
17180
17181struct Row8;
17182struct Row9;
17183struct Row10;"#};
17184
17185 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
17186 assert_hunk_revert(
17187 indoc! {r#"struct Row;
17188 struct Row2;
17189
17190 ˇstruct Row4;
17191 struct Row5;
17192 struct Row6;
17193 ˇ
17194 struct Row8;
17195 struct Row10;"#},
17196 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
17197 indoc! {r#"struct Row;
17198 struct Row2;
17199
17200 ˇstruct Row4;
17201 struct Row5;
17202 struct Row6;
17203 ˇ
17204 struct Row8;
17205 struct Row10;"#},
17206 base_text,
17207 &mut cx,
17208 );
17209 assert_hunk_revert(
17210 indoc! {r#"struct Row;
17211 struct Row2;
17212
17213 «ˇstruct Row4;
17214 struct» Row5;
17215 «struct Row6;
17216 ˇ»
17217 struct Row8;
17218 struct Row10;"#},
17219 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
17220 indoc! {r#"struct Row;
17221 struct Row2;
17222
17223 «ˇstruct Row4;
17224 struct» Row5;
17225 «struct Row6;
17226 ˇ»
17227 struct Row8;
17228 struct Row10;"#},
17229 base_text,
17230 &mut cx,
17231 );
17232
17233 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
17234 assert_hunk_revert(
17235 indoc! {r#"struct Row;
17236 ˇstruct Row2;
17237
17238 struct Row4;
17239 struct Row5;
17240 struct Row6;
17241
17242 struct Row8;ˇ
17243 struct Row10;"#},
17244 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
17245 indoc! {r#"struct Row;
17246 struct Row1;
17247 ˇstruct Row2;
17248
17249 struct Row4;
17250 struct Row5;
17251 struct Row6;
17252
17253 struct Row8;ˇ
17254 struct Row9;
17255 struct Row10;"#},
17256 base_text,
17257 &mut cx,
17258 );
17259 assert_hunk_revert(
17260 indoc! {r#"struct Row;
17261 struct Row2«ˇ;
17262 struct Row4;
17263 struct» Row5;
17264 «struct Row6;
17265
17266 struct Row8;ˇ»
17267 struct Row10;"#},
17268 vec![
17269 DiffHunkStatusKind::Deleted,
17270 DiffHunkStatusKind::Deleted,
17271 DiffHunkStatusKind::Deleted,
17272 ],
17273 indoc! {r#"struct Row;
17274 struct Row1;
17275 struct Row2«ˇ;
17276
17277 struct Row4;
17278 struct» Row5;
17279 «struct Row6;
17280
17281 struct Row8;ˇ»
17282 struct Row9;
17283 struct Row10;"#},
17284 base_text,
17285 &mut cx,
17286 );
17287}
17288
17289#[gpui::test]
17290async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
17291 init_test(cx, |_| {});
17292
17293 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
17294 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
17295 let base_text_3 =
17296 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
17297
17298 let text_1 = edit_first_char_of_every_line(base_text_1);
17299 let text_2 = edit_first_char_of_every_line(base_text_2);
17300 let text_3 = edit_first_char_of_every_line(base_text_3);
17301
17302 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
17303 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
17304 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
17305
17306 let multibuffer = cx.new(|cx| {
17307 let mut multibuffer = MultiBuffer::new(ReadWrite);
17308 multibuffer.push_excerpts(
17309 buffer_1.clone(),
17310 [
17311 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17312 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17313 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17314 ],
17315 cx,
17316 );
17317 multibuffer.push_excerpts(
17318 buffer_2.clone(),
17319 [
17320 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17321 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17322 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17323 ],
17324 cx,
17325 );
17326 multibuffer.push_excerpts(
17327 buffer_3.clone(),
17328 [
17329 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17330 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17331 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17332 ],
17333 cx,
17334 );
17335 multibuffer
17336 });
17337
17338 let fs = FakeFs::new(cx.executor());
17339 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
17340 let (editor, cx) = cx
17341 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
17342 editor.update_in(cx, |editor, _window, cx| {
17343 for (buffer, diff_base) in [
17344 (buffer_1.clone(), base_text_1),
17345 (buffer_2.clone(), base_text_2),
17346 (buffer_3.clone(), base_text_3),
17347 ] {
17348 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
17349 editor
17350 .buffer
17351 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
17352 }
17353 });
17354 cx.executor().run_until_parked();
17355
17356 editor.update_in(cx, |editor, window, cx| {
17357 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}");
17358 editor.select_all(&SelectAll, window, cx);
17359 editor.git_restore(&Default::default(), window, cx);
17360 });
17361 cx.executor().run_until_parked();
17362
17363 // When all ranges are selected, all buffer hunks are reverted.
17364 editor.update(cx, |editor, cx| {
17365 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");
17366 });
17367 buffer_1.update(cx, |buffer, _| {
17368 assert_eq!(buffer.text(), base_text_1);
17369 });
17370 buffer_2.update(cx, |buffer, _| {
17371 assert_eq!(buffer.text(), base_text_2);
17372 });
17373 buffer_3.update(cx, |buffer, _| {
17374 assert_eq!(buffer.text(), base_text_3);
17375 });
17376
17377 editor.update_in(cx, |editor, window, cx| {
17378 editor.undo(&Default::default(), window, cx);
17379 });
17380
17381 editor.update_in(cx, |editor, window, cx| {
17382 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17383 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
17384 });
17385 editor.git_restore(&Default::default(), window, cx);
17386 });
17387
17388 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
17389 // but not affect buffer_2 and its related excerpts.
17390 editor.update(cx, |editor, cx| {
17391 assert_eq!(
17392 editor.text(cx),
17393 "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}"
17394 );
17395 });
17396 buffer_1.update(cx, |buffer, _| {
17397 assert_eq!(buffer.text(), base_text_1);
17398 });
17399 buffer_2.update(cx, |buffer, _| {
17400 assert_eq!(
17401 buffer.text(),
17402 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
17403 );
17404 });
17405 buffer_3.update(cx, |buffer, _| {
17406 assert_eq!(
17407 buffer.text(),
17408 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
17409 );
17410 });
17411
17412 fn edit_first_char_of_every_line(text: &str) -> String {
17413 text.split('\n')
17414 .map(|line| format!("X{}", &line[1..]))
17415 .collect::<Vec<_>>()
17416 .join("\n")
17417 }
17418}
17419
17420#[gpui::test]
17421async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
17422 init_test(cx, |_| {});
17423
17424 let cols = 4;
17425 let rows = 10;
17426 let sample_text_1 = sample_text(rows, cols, 'a');
17427 assert_eq!(
17428 sample_text_1,
17429 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
17430 );
17431 let sample_text_2 = sample_text(rows, cols, 'l');
17432 assert_eq!(
17433 sample_text_2,
17434 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
17435 );
17436 let sample_text_3 = sample_text(rows, cols, 'v');
17437 assert_eq!(
17438 sample_text_3,
17439 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
17440 );
17441
17442 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
17443 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
17444 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
17445
17446 let multi_buffer = cx.new(|cx| {
17447 let mut multibuffer = MultiBuffer::new(ReadWrite);
17448 multibuffer.push_excerpts(
17449 buffer_1.clone(),
17450 [
17451 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17452 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17453 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17454 ],
17455 cx,
17456 );
17457 multibuffer.push_excerpts(
17458 buffer_2.clone(),
17459 [
17460 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17461 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17462 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17463 ],
17464 cx,
17465 );
17466 multibuffer.push_excerpts(
17467 buffer_3.clone(),
17468 [
17469 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17470 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17471 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17472 ],
17473 cx,
17474 );
17475 multibuffer
17476 });
17477
17478 let fs = FakeFs::new(cx.executor());
17479 fs.insert_tree(
17480 "/a",
17481 json!({
17482 "main.rs": sample_text_1,
17483 "other.rs": sample_text_2,
17484 "lib.rs": sample_text_3,
17485 }),
17486 )
17487 .await;
17488 let project = Project::test(fs, ["/a".as_ref()], cx).await;
17489 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17490 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17491 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17492 Editor::new(
17493 EditorMode::full(),
17494 multi_buffer,
17495 Some(project.clone()),
17496 window,
17497 cx,
17498 )
17499 });
17500 let multibuffer_item_id = workspace
17501 .update(cx, |workspace, window, cx| {
17502 assert!(
17503 workspace.active_item(cx).is_none(),
17504 "active item should be None before the first item is added"
17505 );
17506 workspace.add_item_to_active_pane(
17507 Box::new(multi_buffer_editor.clone()),
17508 None,
17509 true,
17510 window,
17511 cx,
17512 );
17513 let active_item = workspace
17514 .active_item(cx)
17515 .expect("should have an active item after adding the multi buffer");
17516 assert!(
17517 !active_item.is_singleton(cx),
17518 "A multi buffer was expected to active after adding"
17519 );
17520 active_item.item_id()
17521 })
17522 .unwrap();
17523 cx.executor().run_until_parked();
17524
17525 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17526 editor.change_selections(
17527 SelectionEffects::scroll(Autoscroll::Next),
17528 window,
17529 cx,
17530 |s| s.select_ranges(Some(1..2)),
17531 );
17532 editor.open_excerpts(&OpenExcerpts, window, cx);
17533 });
17534 cx.executor().run_until_parked();
17535 let first_item_id = workspace
17536 .update(cx, |workspace, window, cx| {
17537 let active_item = workspace
17538 .active_item(cx)
17539 .expect("should have an active item after navigating into the 1st buffer");
17540 let first_item_id = active_item.item_id();
17541 assert_ne!(
17542 first_item_id, multibuffer_item_id,
17543 "Should navigate into the 1st buffer and activate it"
17544 );
17545 assert!(
17546 active_item.is_singleton(cx),
17547 "New active item should be a singleton buffer"
17548 );
17549 assert_eq!(
17550 active_item
17551 .act_as::<Editor>(cx)
17552 .expect("should have navigated into an editor for the 1st buffer")
17553 .read(cx)
17554 .text(cx),
17555 sample_text_1
17556 );
17557
17558 workspace
17559 .go_back(workspace.active_pane().downgrade(), window, cx)
17560 .detach_and_log_err(cx);
17561
17562 first_item_id
17563 })
17564 .unwrap();
17565 cx.executor().run_until_parked();
17566 workspace
17567 .update(cx, |workspace, _, cx| {
17568 let active_item = workspace
17569 .active_item(cx)
17570 .expect("should have an active item after navigating back");
17571 assert_eq!(
17572 active_item.item_id(),
17573 multibuffer_item_id,
17574 "Should navigate back to the multi buffer"
17575 );
17576 assert!(!active_item.is_singleton(cx));
17577 })
17578 .unwrap();
17579
17580 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17581 editor.change_selections(
17582 SelectionEffects::scroll(Autoscroll::Next),
17583 window,
17584 cx,
17585 |s| s.select_ranges(Some(39..40)),
17586 );
17587 editor.open_excerpts(&OpenExcerpts, window, cx);
17588 });
17589 cx.executor().run_until_parked();
17590 let second_item_id = workspace
17591 .update(cx, |workspace, window, cx| {
17592 let active_item = workspace
17593 .active_item(cx)
17594 .expect("should have an active item after navigating into the 2nd buffer");
17595 let second_item_id = active_item.item_id();
17596 assert_ne!(
17597 second_item_id, multibuffer_item_id,
17598 "Should navigate away from the multibuffer"
17599 );
17600 assert_ne!(
17601 second_item_id, first_item_id,
17602 "Should navigate into the 2nd buffer and activate it"
17603 );
17604 assert!(
17605 active_item.is_singleton(cx),
17606 "New active item should be a singleton buffer"
17607 );
17608 assert_eq!(
17609 active_item
17610 .act_as::<Editor>(cx)
17611 .expect("should have navigated into an editor")
17612 .read(cx)
17613 .text(cx),
17614 sample_text_2
17615 );
17616
17617 workspace
17618 .go_back(workspace.active_pane().downgrade(), window, cx)
17619 .detach_and_log_err(cx);
17620
17621 second_item_id
17622 })
17623 .unwrap();
17624 cx.executor().run_until_parked();
17625 workspace
17626 .update(cx, |workspace, _, cx| {
17627 let active_item = workspace
17628 .active_item(cx)
17629 .expect("should have an active item after navigating back from the 2nd buffer");
17630 assert_eq!(
17631 active_item.item_id(),
17632 multibuffer_item_id,
17633 "Should navigate back from the 2nd buffer to the multi buffer"
17634 );
17635 assert!(!active_item.is_singleton(cx));
17636 })
17637 .unwrap();
17638
17639 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17640 editor.change_selections(
17641 SelectionEffects::scroll(Autoscroll::Next),
17642 window,
17643 cx,
17644 |s| s.select_ranges(Some(70..70)),
17645 );
17646 editor.open_excerpts(&OpenExcerpts, window, cx);
17647 });
17648 cx.executor().run_until_parked();
17649 workspace
17650 .update(cx, |workspace, window, cx| {
17651 let active_item = workspace
17652 .active_item(cx)
17653 .expect("should have an active item after navigating into the 3rd buffer");
17654 let third_item_id = active_item.item_id();
17655 assert_ne!(
17656 third_item_id, multibuffer_item_id,
17657 "Should navigate into the 3rd buffer and activate it"
17658 );
17659 assert_ne!(third_item_id, first_item_id);
17660 assert_ne!(third_item_id, second_item_id);
17661 assert!(
17662 active_item.is_singleton(cx),
17663 "New active item should be a singleton buffer"
17664 );
17665 assert_eq!(
17666 active_item
17667 .act_as::<Editor>(cx)
17668 .expect("should have navigated into an editor")
17669 .read(cx)
17670 .text(cx),
17671 sample_text_3
17672 );
17673
17674 workspace
17675 .go_back(workspace.active_pane().downgrade(), window, cx)
17676 .detach_and_log_err(cx);
17677 })
17678 .unwrap();
17679 cx.executor().run_until_parked();
17680 workspace
17681 .update(cx, |workspace, _, cx| {
17682 let active_item = workspace
17683 .active_item(cx)
17684 .expect("should have an active item after navigating back from the 3rd buffer");
17685 assert_eq!(
17686 active_item.item_id(),
17687 multibuffer_item_id,
17688 "Should navigate back from the 3rd buffer to the multi buffer"
17689 );
17690 assert!(!active_item.is_singleton(cx));
17691 })
17692 .unwrap();
17693}
17694
17695#[gpui::test]
17696async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17697 init_test(cx, |_| {});
17698
17699 let mut cx = EditorTestContext::new(cx).await;
17700
17701 let diff_base = r#"
17702 use some::mod;
17703
17704 const A: u32 = 42;
17705
17706 fn main() {
17707 println!("hello");
17708
17709 println!("world");
17710 }
17711 "#
17712 .unindent();
17713
17714 cx.set_state(
17715 &r#"
17716 use some::modified;
17717
17718 ˇ
17719 fn main() {
17720 println!("hello there");
17721
17722 println!("around the");
17723 println!("world");
17724 }
17725 "#
17726 .unindent(),
17727 );
17728
17729 cx.set_head_text(&diff_base);
17730 executor.run_until_parked();
17731
17732 cx.update_editor(|editor, window, cx| {
17733 editor.go_to_next_hunk(&GoToHunk, window, cx);
17734 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17735 });
17736 executor.run_until_parked();
17737 cx.assert_state_with_diff(
17738 r#"
17739 use some::modified;
17740
17741
17742 fn main() {
17743 - println!("hello");
17744 + ˇ println!("hello there");
17745
17746 println!("around the");
17747 println!("world");
17748 }
17749 "#
17750 .unindent(),
17751 );
17752
17753 cx.update_editor(|editor, window, cx| {
17754 for _ in 0..2 {
17755 editor.go_to_next_hunk(&GoToHunk, window, cx);
17756 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17757 }
17758 });
17759 executor.run_until_parked();
17760 cx.assert_state_with_diff(
17761 r#"
17762 - use some::mod;
17763 + ˇuse some::modified;
17764
17765
17766 fn main() {
17767 - println!("hello");
17768 + println!("hello there");
17769
17770 + println!("around the");
17771 println!("world");
17772 }
17773 "#
17774 .unindent(),
17775 );
17776
17777 cx.update_editor(|editor, window, cx| {
17778 editor.go_to_next_hunk(&GoToHunk, window, cx);
17779 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17780 });
17781 executor.run_until_parked();
17782 cx.assert_state_with_diff(
17783 r#"
17784 - use some::mod;
17785 + use some::modified;
17786
17787 - const A: u32 = 42;
17788 ˇ
17789 fn main() {
17790 - println!("hello");
17791 + println!("hello there");
17792
17793 + println!("around the");
17794 println!("world");
17795 }
17796 "#
17797 .unindent(),
17798 );
17799
17800 cx.update_editor(|editor, window, cx| {
17801 editor.cancel(&Cancel, window, cx);
17802 });
17803
17804 cx.assert_state_with_diff(
17805 r#"
17806 use some::modified;
17807
17808 ˇ
17809 fn main() {
17810 println!("hello there");
17811
17812 println!("around the");
17813 println!("world");
17814 }
17815 "#
17816 .unindent(),
17817 );
17818}
17819
17820#[gpui::test]
17821async fn test_diff_base_change_with_expanded_diff_hunks(
17822 executor: BackgroundExecutor,
17823 cx: &mut TestAppContext,
17824) {
17825 init_test(cx, |_| {});
17826
17827 let mut cx = EditorTestContext::new(cx).await;
17828
17829 let diff_base = r#"
17830 use some::mod1;
17831 use some::mod2;
17832
17833 const A: u32 = 42;
17834 const B: u32 = 42;
17835 const C: u32 = 42;
17836
17837 fn main() {
17838 println!("hello");
17839
17840 println!("world");
17841 }
17842 "#
17843 .unindent();
17844
17845 cx.set_state(
17846 &r#"
17847 use some::mod2;
17848
17849 const A: u32 = 42;
17850 const C: u32 = 42;
17851
17852 fn main(ˇ) {
17853 //println!("hello");
17854
17855 println!("world");
17856 //
17857 //
17858 }
17859 "#
17860 .unindent(),
17861 );
17862
17863 cx.set_head_text(&diff_base);
17864 executor.run_until_parked();
17865
17866 cx.update_editor(|editor, window, cx| {
17867 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17868 });
17869 executor.run_until_parked();
17870 cx.assert_state_with_diff(
17871 r#"
17872 - use some::mod1;
17873 use some::mod2;
17874
17875 const A: u32 = 42;
17876 - const B: u32 = 42;
17877 const C: u32 = 42;
17878
17879 fn main(ˇ) {
17880 - println!("hello");
17881 + //println!("hello");
17882
17883 println!("world");
17884 + //
17885 + //
17886 }
17887 "#
17888 .unindent(),
17889 );
17890
17891 cx.set_head_text("new diff base!");
17892 executor.run_until_parked();
17893 cx.assert_state_with_diff(
17894 r#"
17895 - new diff base!
17896 + use some::mod2;
17897 +
17898 + const A: u32 = 42;
17899 + const C: u32 = 42;
17900 +
17901 + fn main(ˇ) {
17902 + //println!("hello");
17903 +
17904 + println!("world");
17905 + //
17906 + //
17907 + }
17908 "#
17909 .unindent(),
17910 );
17911}
17912
17913#[gpui::test]
17914async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
17915 init_test(cx, |_| {});
17916
17917 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17918 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17919 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17920 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17921 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
17922 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
17923
17924 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
17925 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
17926 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
17927
17928 let multi_buffer = cx.new(|cx| {
17929 let mut multibuffer = MultiBuffer::new(ReadWrite);
17930 multibuffer.push_excerpts(
17931 buffer_1.clone(),
17932 [
17933 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17934 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17935 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17936 ],
17937 cx,
17938 );
17939 multibuffer.push_excerpts(
17940 buffer_2.clone(),
17941 [
17942 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17943 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17944 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17945 ],
17946 cx,
17947 );
17948 multibuffer.push_excerpts(
17949 buffer_3.clone(),
17950 [
17951 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17952 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17953 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17954 ],
17955 cx,
17956 );
17957 multibuffer
17958 });
17959
17960 let editor =
17961 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17962 editor
17963 .update(cx, |editor, _window, cx| {
17964 for (buffer, diff_base) in [
17965 (buffer_1.clone(), file_1_old),
17966 (buffer_2.clone(), file_2_old),
17967 (buffer_3.clone(), file_3_old),
17968 ] {
17969 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
17970 editor
17971 .buffer
17972 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
17973 }
17974 })
17975 .unwrap();
17976
17977 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17978 cx.run_until_parked();
17979
17980 cx.assert_editor_state(
17981 &"
17982 ˇaaa
17983 ccc
17984 ddd
17985
17986 ggg
17987 hhh
17988
17989
17990 lll
17991 mmm
17992 NNN
17993
17994 qqq
17995 rrr
17996
17997 uuu
17998 111
17999 222
18000 333
18001
18002 666
18003 777
18004
18005 000
18006 !!!"
18007 .unindent(),
18008 );
18009
18010 cx.update_editor(|editor, window, cx| {
18011 editor.select_all(&SelectAll, window, cx);
18012 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18013 });
18014 cx.executor().run_until_parked();
18015
18016 cx.assert_state_with_diff(
18017 "
18018 «aaa
18019 - bbb
18020 ccc
18021 ddd
18022
18023 ggg
18024 hhh
18025
18026
18027 lll
18028 mmm
18029 - nnn
18030 + NNN
18031
18032 qqq
18033 rrr
18034
18035 uuu
18036 111
18037 222
18038 333
18039
18040 + 666
18041 777
18042
18043 000
18044 !!!ˇ»"
18045 .unindent(),
18046 );
18047}
18048
18049#[gpui::test]
18050async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
18051 init_test(cx, |_| {});
18052
18053 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
18054 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
18055
18056 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
18057 let multi_buffer = cx.new(|cx| {
18058 let mut multibuffer = MultiBuffer::new(ReadWrite);
18059 multibuffer.push_excerpts(
18060 buffer.clone(),
18061 [
18062 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
18063 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
18064 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
18065 ],
18066 cx,
18067 );
18068 multibuffer
18069 });
18070
18071 let editor =
18072 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
18073 editor
18074 .update(cx, |editor, _window, cx| {
18075 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
18076 editor
18077 .buffer
18078 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
18079 })
18080 .unwrap();
18081
18082 let mut cx = EditorTestContext::for_editor(editor, cx).await;
18083 cx.run_until_parked();
18084
18085 cx.update_editor(|editor, window, cx| {
18086 editor.expand_all_diff_hunks(&Default::default(), window, cx)
18087 });
18088 cx.executor().run_until_parked();
18089
18090 // When the start of a hunk coincides with the start of its excerpt,
18091 // the hunk is expanded. When the start of a a hunk is earlier than
18092 // the start of its excerpt, the hunk is not expanded.
18093 cx.assert_state_with_diff(
18094 "
18095 ˇaaa
18096 - bbb
18097 + BBB
18098
18099 - ddd
18100 - eee
18101 + DDD
18102 + EEE
18103 fff
18104
18105 iii
18106 "
18107 .unindent(),
18108 );
18109}
18110
18111#[gpui::test]
18112async fn test_edits_around_expanded_insertion_hunks(
18113 executor: BackgroundExecutor,
18114 cx: &mut TestAppContext,
18115) {
18116 init_test(cx, |_| {});
18117
18118 let mut cx = EditorTestContext::new(cx).await;
18119
18120 let diff_base = r#"
18121 use some::mod1;
18122 use some::mod2;
18123
18124 const A: u32 = 42;
18125
18126 fn main() {
18127 println!("hello");
18128
18129 println!("world");
18130 }
18131 "#
18132 .unindent();
18133 executor.run_until_parked();
18134 cx.set_state(
18135 &r#"
18136 use some::mod1;
18137 use some::mod2;
18138
18139 const A: u32 = 42;
18140 const B: u32 = 42;
18141 const C: u32 = 42;
18142 ˇ
18143
18144 fn main() {
18145 println!("hello");
18146
18147 println!("world");
18148 }
18149 "#
18150 .unindent(),
18151 );
18152
18153 cx.set_head_text(&diff_base);
18154 executor.run_until_parked();
18155
18156 cx.update_editor(|editor, window, cx| {
18157 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18158 });
18159 executor.run_until_parked();
18160
18161 cx.assert_state_with_diff(
18162 r#"
18163 use some::mod1;
18164 use some::mod2;
18165
18166 const A: u32 = 42;
18167 + const B: u32 = 42;
18168 + const C: u32 = 42;
18169 + ˇ
18170
18171 fn main() {
18172 println!("hello");
18173
18174 println!("world");
18175 }
18176 "#
18177 .unindent(),
18178 );
18179
18180 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
18181 executor.run_until_parked();
18182
18183 cx.assert_state_with_diff(
18184 r#"
18185 use some::mod1;
18186 use some::mod2;
18187
18188 const A: u32 = 42;
18189 + const B: u32 = 42;
18190 + const C: u32 = 42;
18191 + const D: u32 = 42;
18192 + ˇ
18193
18194 fn main() {
18195 println!("hello");
18196
18197 println!("world");
18198 }
18199 "#
18200 .unindent(),
18201 );
18202
18203 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
18204 executor.run_until_parked();
18205
18206 cx.assert_state_with_diff(
18207 r#"
18208 use some::mod1;
18209 use some::mod2;
18210
18211 const A: u32 = 42;
18212 + const B: u32 = 42;
18213 + const C: u32 = 42;
18214 + const D: u32 = 42;
18215 + const E: u32 = 42;
18216 + ˇ
18217
18218 fn main() {
18219 println!("hello");
18220
18221 println!("world");
18222 }
18223 "#
18224 .unindent(),
18225 );
18226
18227 cx.update_editor(|editor, window, cx| {
18228 editor.delete_line(&DeleteLine, window, cx);
18229 });
18230 executor.run_until_parked();
18231
18232 cx.assert_state_with_diff(
18233 r#"
18234 use some::mod1;
18235 use some::mod2;
18236
18237 const A: u32 = 42;
18238 + const B: u32 = 42;
18239 + const C: u32 = 42;
18240 + const D: u32 = 42;
18241 + const E: u32 = 42;
18242 ˇ
18243 fn main() {
18244 println!("hello");
18245
18246 println!("world");
18247 }
18248 "#
18249 .unindent(),
18250 );
18251
18252 cx.update_editor(|editor, window, cx| {
18253 editor.move_up(&MoveUp, window, cx);
18254 editor.delete_line(&DeleteLine, window, cx);
18255 editor.move_up(&MoveUp, window, cx);
18256 editor.delete_line(&DeleteLine, window, cx);
18257 editor.move_up(&MoveUp, window, cx);
18258 editor.delete_line(&DeleteLine, window, cx);
18259 });
18260 executor.run_until_parked();
18261 cx.assert_state_with_diff(
18262 r#"
18263 use some::mod1;
18264 use some::mod2;
18265
18266 const A: u32 = 42;
18267 + const B: u32 = 42;
18268 ˇ
18269 fn main() {
18270 println!("hello");
18271
18272 println!("world");
18273 }
18274 "#
18275 .unindent(),
18276 );
18277
18278 cx.update_editor(|editor, window, cx| {
18279 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
18280 editor.delete_line(&DeleteLine, window, cx);
18281 });
18282 executor.run_until_parked();
18283 cx.assert_state_with_diff(
18284 r#"
18285 ˇ
18286 fn main() {
18287 println!("hello");
18288
18289 println!("world");
18290 }
18291 "#
18292 .unindent(),
18293 );
18294}
18295
18296#[gpui::test]
18297async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
18298 init_test(cx, |_| {});
18299
18300 let mut cx = EditorTestContext::new(cx).await;
18301 cx.set_head_text(indoc! { "
18302 one
18303 two
18304 three
18305 four
18306 five
18307 "
18308 });
18309 cx.set_state(indoc! { "
18310 one
18311 ˇthree
18312 five
18313 "});
18314 cx.run_until_parked();
18315 cx.update_editor(|editor, window, cx| {
18316 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18317 });
18318 cx.assert_state_with_diff(
18319 indoc! { "
18320 one
18321 - two
18322 ˇthree
18323 - four
18324 five
18325 "}
18326 .to_string(),
18327 );
18328 cx.update_editor(|editor, window, cx| {
18329 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18330 });
18331
18332 cx.assert_state_with_diff(
18333 indoc! { "
18334 one
18335 ˇthree
18336 five
18337 "}
18338 .to_string(),
18339 );
18340
18341 cx.set_state(indoc! { "
18342 one
18343 ˇTWO
18344 three
18345 four
18346 five
18347 "});
18348 cx.run_until_parked();
18349 cx.update_editor(|editor, window, cx| {
18350 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18351 });
18352
18353 cx.assert_state_with_diff(
18354 indoc! { "
18355 one
18356 - two
18357 + ˇTWO
18358 three
18359 four
18360 five
18361 "}
18362 .to_string(),
18363 );
18364 cx.update_editor(|editor, window, cx| {
18365 editor.move_up(&Default::default(), window, cx);
18366 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18367 });
18368 cx.assert_state_with_diff(
18369 indoc! { "
18370 one
18371 ˇTWO
18372 three
18373 four
18374 five
18375 "}
18376 .to_string(),
18377 );
18378}
18379
18380#[gpui::test]
18381async fn test_edits_around_expanded_deletion_hunks(
18382 executor: BackgroundExecutor,
18383 cx: &mut TestAppContext,
18384) {
18385 init_test(cx, |_| {});
18386
18387 let mut cx = EditorTestContext::new(cx).await;
18388
18389 let diff_base = r#"
18390 use some::mod1;
18391 use some::mod2;
18392
18393 const A: u32 = 42;
18394 const B: u32 = 42;
18395 const C: u32 = 42;
18396
18397
18398 fn main() {
18399 println!("hello");
18400
18401 println!("world");
18402 }
18403 "#
18404 .unindent();
18405 executor.run_until_parked();
18406 cx.set_state(
18407 &r#"
18408 use some::mod1;
18409 use some::mod2;
18410
18411 ˇconst B: u32 = 42;
18412 const C: u32 = 42;
18413
18414
18415 fn main() {
18416 println!("hello");
18417
18418 println!("world");
18419 }
18420 "#
18421 .unindent(),
18422 );
18423
18424 cx.set_head_text(&diff_base);
18425 executor.run_until_parked();
18426
18427 cx.update_editor(|editor, window, cx| {
18428 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18429 });
18430 executor.run_until_parked();
18431
18432 cx.assert_state_with_diff(
18433 r#"
18434 use some::mod1;
18435 use some::mod2;
18436
18437 - const A: u32 = 42;
18438 ˇconst B: u32 = 42;
18439 const C: u32 = 42;
18440
18441
18442 fn main() {
18443 println!("hello");
18444
18445 println!("world");
18446 }
18447 "#
18448 .unindent(),
18449 );
18450
18451 cx.update_editor(|editor, window, cx| {
18452 editor.delete_line(&DeleteLine, window, cx);
18453 });
18454 executor.run_until_parked();
18455 cx.assert_state_with_diff(
18456 r#"
18457 use some::mod1;
18458 use some::mod2;
18459
18460 - const A: u32 = 42;
18461 - const B: u32 = 42;
18462 ˇconst C: u32 = 42;
18463
18464
18465 fn main() {
18466 println!("hello");
18467
18468 println!("world");
18469 }
18470 "#
18471 .unindent(),
18472 );
18473
18474 cx.update_editor(|editor, window, cx| {
18475 editor.delete_line(&DeleteLine, window, cx);
18476 });
18477 executor.run_until_parked();
18478 cx.assert_state_with_diff(
18479 r#"
18480 use some::mod1;
18481 use some::mod2;
18482
18483 - const A: u32 = 42;
18484 - const B: u32 = 42;
18485 - const C: u32 = 42;
18486 ˇ
18487
18488 fn main() {
18489 println!("hello");
18490
18491 println!("world");
18492 }
18493 "#
18494 .unindent(),
18495 );
18496
18497 cx.update_editor(|editor, window, cx| {
18498 editor.handle_input("replacement", window, cx);
18499 });
18500 executor.run_until_parked();
18501 cx.assert_state_with_diff(
18502 r#"
18503 use some::mod1;
18504 use some::mod2;
18505
18506 - const A: u32 = 42;
18507 - const B: u32 = 42;
18508 - const C: u32 = 42;
18509 -
18510 + replacementˇ
18511
18512 fn main() {
18513 println!("hello");
18514
18515 println!("world");
18516 }
18517 "#
18518 .unindent(),
18519 );
18520}
18521
18522#[gpui::test]
18523async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18524 init_test(cx, |_| {});
18525
18526 let mut cx = EditorTestContext::new(cx).await;
18527
18528 let base_text = r#"
18529 one
18530 two
18531 three
18532 four
18533 five
18534 "#
18535 .unindent();
18536 executor.run_until_parked();
18537 cx.set_state(
18538 &r#"
18539 one
18540 two
18541 fˇour
18542 five
18543 "#
18544 .unindent(),
18545 );
18546
18547 cx.set_head_text(&base_text);
18548 executor.run_until_parked();
18549
18550 cx.update_editor(|editor, window, cx| {
18551 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18552 });
18553 executor.run_until_parked();
18554
18555 cx.assert_state_with_diff(
18556 r#"
18557 one
18558 two
18559 - three
18560 fˇour
18561 five
18562 "#
18563 .unindent(),
18564 );
18565
18566 cx.update_editor(|editor, window, cx| {
18567 editor.backspace(&Backspace, window, cx);
18568 editor.backspace(&Backspace, window, cx);
18569 });
18570 executor.run_until_parked();
18571 cx.assert_state_with_diff(
18572 r#"
18573 one
18574 two
18575 - threeˇ
18576 - four
18577 + our
18578 five
18579 "#
18580 .unindent(),
18581 );
18582}
18583
18584#[gpui::test]
18585async fn test_edit_after_expanded_modification_hunk(
18586 executor: BackgroundExecutor,
18587 cx: &mut TestAppContext,
18588) {
18589 init_test(cx, |_| {});
18590
18591 let mut cx = EditorTestContext::new(cx).await;
18592
18593 let diff_base = r#"
18594 use some::mod1;
18595 use some::mod2;
18596
18597 const A: u32 = 42;
18598 const B: u32 = 42;
18599 const C: u32 = 42;
18600 const D: u32 = 42;
18601
18602
18603 fn main() {
18604 println!("hello");
18605
18606 println!("world");
18607 }"#
18608 .unindent();
18609
18610 cx.set_state(
18611 &r#"
18612 use some::mod1;
18613 use some::mod2;
18614
18615 const A: u32 = 42;
18616 const B: u32 = 42;
18617 const C: u32 = 43ˇ
18618 const D: u32 = 42;
18619
18620
18621 fn main() {
18622 println!("hello");
18623
18624 println!("world");
18625 }"#
18626 .unindent(),
18627 );
18628
18629 cx.set_head_text(&diff_base);
18630 executor.run_until_parked();
18631 cx.update_editor(|editor, window, cx| {
18632 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18633 });
18634 executor.run_until_parked();
18635
18636 cx.assert_state_with_diff(
18637 r#"
18638 use some::mod1;
18639 use some::mod2;
18640
18641 const A: u32 = 42;
18642 const B: u32 = 42;
18643 - const C: u32 = 42;
18644 + const C: u32 = 43ˇ
18645 const D: u32 = 42;
18646
18647
18648 fn main() {
18649 println!("hello");
18650
18651 println!("world");
18652 }"#
18653 .unindent(),
18654 );
18655
18656 cx.update_editor(|editor, window, cx| {
18657 editor.handle_input("\nnew_line\n", window, cx);
18658 });
18659 executor.run_until_parked();
18660
18661 cx.assert_state_with_diff(
18662 r#"
18663 use some::mod1;
18664 use some::mod2;
18665
18666 const A: u32 = 42;
18667 const B: u32 = 42;
18668 - const C: u32 = 42;
18669 + const C: u32 = 43
18670 + new_line
18671 + ˇ
18672 const D: u32 = 42;
18673
18674
18675 fn main() {
18676 println!("hello");
18677
18678 println!("world");
18679 }"#
18680 .unindent(),
18681 );
18682}
18683
18684#[gpui::test]
18685async fn test_stage_and_unstage_added_file_hunk(
18686 executor: BackgroundExecutor,
18687 cx: &mut TestAppContext,
18688) {
18689 init_test(cx, |_| {});
18690
18691 let mut cx = EditorTestContext::new(cx).await;
18692 cx.update_editor(|editor, _, cx| {
18693 editor.set_expand_all_diff_hunks(cx);
18694 });
18695
18696 let working_copy = r#"
18697 ˇfn main() {
18698 println!("hello, world!");
18699 }
18700 "#
18701 .unindent();
18702
18703 cx.set_state(&working_copy);
18704 executor.run_until_parked();
18705
18706 cx.assert_state_with_diff(
18707 r#"
18708 + ˇfn main() {
18709 + println!("hello, world!");
18710 + }
18711 "#
18712 .unindent(),
18713 );
18714 cx.assert_index_text(None);
18715
18716 cx.update_editor(|editor, window, cx| {
18717 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18718 });
18719 executor.run_until_parked();
18720 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
18721 cx.assert_state_with_diff(
18722 r#"
18723 + ˇfn main() {
18724 + println!("hello, world!");
18725 + }
18726 "#
18727 .unindent(),
18728 );
18729
18730 cx.update_editor(|editor, window, cx| {
18731 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18732 });
18733 executor.run_until_parked();
18734 cx.assert_index_text(None);
18735}
18736
18737async fn setup_indent_guides_editor(
18738 text: &str,
18739 cx: &mut TestAppContext,
18740) -> (BufferId, EditorTestContext) {
18741 init_test(cx, |_| {});
18742
18743 let mut cx = EditorTestContext::new(cx).await;
18744
18745 let buffer_id = cx.update_editor(|editor, window, cx| {
18746 editor.set_text(text, window, cx);
18747 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
18748
18749 buffer_ids[0]
18750 });
18751
18752 (buffer_id, cx)
18753}
18754
18755fn assert_indent_guides(
18756 range: Range<u32>,
18757 expected: Vec<IndentGuide>,
18758 active_indices: Option<Vec<usize>>,
18759 cx: &mut EditorTestContext,
18760) {
18761 let indent_guides = cx.update_editor(|editor, window, cx| {
18762 let snapshot = editor.snapshot(window, cx).display_snapshot;
18763 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
18764 editor,
18765 MultiBufferRow(range.start)..MultiBufferRow(range.end),
18766 true,
18767 &snapshot,
18768 cx,
18769 );
18770
18771 indent_guides.sort_by(|a, b| {
18772 a.depth.cmp(&b.depth).then(
18773 a.start_row
18774 .cmp(&b.start_row)
18775 .then(a.end_row.cmp(&b.end_row)),
18776 )
18777 });
18778 indent_guides
18779 });
18780
18781 if let Some(expected) = active_indices {
18782 let active_indices = cx.update_editor(|editor, window, cx| {
18783 let snapshot = editor.snapshot(window, cx).display_snapshot;
18784 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
18785 });
18786
18787 assert_eq!(
18788 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
18789 expected,
18790 "Active indent guide indices do not match"
18791 );
18792 }
18793
18794 assert_eq!(indent_guides, expected, "Indent guides do not match");
18795}
18796
18797fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
18798 IndentGuide {
18799 buffer_id,
18800 start_row: MultiBufferRow(start_row),
18801 end_row: MultiBufferRow(end_row),
18802 depth,
18803 tab_size: 4,
18804 settings: IndentGuideSettings {
18805 enabled: true,
18806 line_width: 1,
18807 active_line_width: 1,
18808 ..Default::default()
18809 },
18810 }
18811}
18812
18813#[gpui::test]
18814async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
18815 let (buffer_id, mut cx) = setup_indent_guides_editor(
18816 &"
18817 fn main() {
18818 let a = 1;
18819 }"
18820 .unindent(),
18821 cx,
18822 )
18823 .await;
18824
18825 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18826}
18827
18828#[gpui::test]
18829async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
18830 let (buffer_id, mut cx) = setup_indent_guides_editor(
18831 &"
18832 fn main() {
18833 let a = 1;
18834 let b = 2;
18835 }"
18836 .unindent(),
18837 cx,
18838 )
18839 .await;
18840
18841 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
18842}
18843
18844#[gpui::test]
18845async fn test_indent_guide_nested(cx: &mut TestAppContext) {
18846 let (buffer_id, mut cx) = setup_indent_guides_editor(
18847 &"
18848 fn main() {
18849 let a = 1;
18850 if a == 3 {
18851 let b = 2;
18852 } else {
18853 let c = 3;
18854 }
18855 }"
18856 .unindent(),
18857 cx,
18858 )
18859 .await;
18860
18861 assert_indent_guides(
18862 0..8,
18863 vec![
18864 indent_guide(buffer_id, 1, 6, 0),
18865 indent_guide(buffer_id, 3, 3, 1),
18866 indent_guide(buffer_id, 5, 5, 1),
18867 ],
18868 None,
18869 &mut cx,
18870 );
18871}
18872
18873#[gpui::test]
18874async fn test_indent_guide_tab(cx: &mut TestAppContext) {
18875 let (buffer_id, mut cx) = setup_indent_guides_editor(
18876 &"
18877 fn main() {
18878 let a = 1;
18879 let b = 2;
18880 let c = 3;
18881 }"
18882 .unindent(),
18883 cx,
18884 )
18885 .await;
18886
18887 assert_indent_guides(
18888 0..5,
18889 vec![
18890 indent_guide(buffer_id, 1, 3, 0),
18891 indent_guide(buffer_id, 2, 2, 1),
18892 ],
18893 None,
18894 &mut cx,
18895 );
18896}
18897
18898#[gpui::test]
18899async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
18900 let (buffer_id, mut cx) = setup_indent_guides_editor(
18901 &"
18902 fn main() {
18903 let a = 1;
18904
18905 let c = 3;
18906 }"
18907 .unindent(),
18908 cx,
18909 )
18910 .await;
18911
18912 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
18913}
18914
18915#[gpui::test]
18916async fn test_indent_guide_complex(cx: &mut TestAppContext) {
18917 let (buffer_id, mut cx) = setup_indent_guides_editor(
18918 &"
18919 fn main() {
18920 let a = 1;
18921
18922 let c = 3;
18923
18924 if a == 3 {
18925 let b = 2;
18926 } else {
18927 let c = 3;
18928 }
18929 }"
18930 .unindent(),
18931 cx,
18932 )
18933 .await;
18934
18935 assert_indent_guides(
18936 0..11,
18937 vec![
18938 indent_guide(buffer_id, 1, 9, 0),
18939 indent_guide(buffer_id, 6, 6, 1),
18940 indent_guide(buffer_id, 8, 8, 1),
18941 ],
18942 None,
18943 &mut cx,
18944 );
18945}
18946
18947#[gpui::test]
18948async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
18949 let (buffer_id, mut cx) = setup_indent_guides_editor(
18950 &"
18951 fn main() {
18952 let a = 1;
18953
18954 let c = 3;
18955
18956 if a == 3 {
18957 let b = 2;
18958 } else {
18959 let c = 3;
18960 }
18961 }"
18962 .unindent(),
18963 cx,
18964 )
18965 .await;
18966
18967 assert_indent_guides(
18968 1..11,
18969 vec![
18970 indent_guide(buffer_id, 1, 9, 0),
18971 indent_guide(buffer_id, 6, 6, 1),
18972 indent_guide(buffer_id, 8, 8, 1),
18973 ],
18974 None,
18975 &mut cx,
18976 );
18977}
18978
18979#[gpui::test]
18980async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
18981 let (buffer_id, mut cx) = setup_indent_guides_editor(
18982 &"
18983 fn main() {
18984 let a = 1;
18985
18986 let c = 3;
18987
18988 if a == 3 {
18989 let b = 2;
18990 } else {
18991 let c = 3;
18992 }
18993 }"
18994 .unindent(),
18995 cx,
18996 )
18997 .await;
18998
18999 assert_indent_guides(
19000 1..10,
19001 vec![
19002 indent_guide(buffer_id, 1, 9, 0),
19003 indent_guide(buffer_id, 6, 6, 1),
19004 indent_guide(buffer_id, 8, 8, 1),
19005 ],
19006 None,
19007 &mut cx,
19008 );
19009}
19010
19011#[gpui::test]
19012async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
19013 let (buffer_id, mut cx) = setup_indent_guides_editor(
19014 &"
19015 fn main() {
19016 if a {
19017 b(
19018 c,
19019 d,
19020 )
19021 } else {
19022 e(
19023 f
19024 )
19025 }
19026 }"
19027 .unindent(),
19028 cx,
19029 )
19030 .await;
19031
19032 assert_indent_guides(
19033 0..11,
19034 vec![
19035 indent_guide(buffer_id, 1, 10, 0),
19036 indent_guide(buffer_id, 2, 5, 1),
19037 indent_guide(buffer_id, 7, 9, 1),
19038 indent_guide(buffer_id, 3, 4, 2),
19039 indent_guide(buffer_id, 8, 8, 2),
19040 ],
19041 None,
19042 &mut cx,
19043 );
19044
19045 cx.update_editor(|editor, window, cx| {
19046 editor.fold_at(MultiBufferRow(2), window, cx);
19047 assert_eq!(
19048 editor.display_text(cx),
19049 "
19050 fn main() {
19051 if a {
19052 b(⋯
19053 )
19054 } else {
19055 e(
19056 f
19057 )
19058 }
19059 }"
19060 .unindent()
19061 );
19062 });
19063
19064 assert_indent_guides(
19065 0..11,
19066 vec![
19067 indent_guide(buffer_id, 1, 10, 0),
19068 indent_guide(buffer_id, 2, 5, 1),
19069 indent_guide(buffer_id, 7, 9, 1),
19070 indent_guide(buffer_id, 8, 8, 2),
19071 ],
19072 None,
19073 &mut cx,
19074 );
19075}
19076
19077#[gpui::test]
19078async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
19079 let (buffer_id, mut cx) = setup_indent_guides_editor(
19080 &"
19081 block1
19082 block2
19083 block3
19084 block4
19085 block2
19086 block1
19087 block1"
19088 .unindent(),
19089 cx,
19090 )
19091 .await;
19092
19093 assert_indent_guides(
19094 1..10,
19095 vec![
19096 indent_guide(buffer_id, 1, 4, 0),
19097 indent_guide(buffer_id, 2, 3, 1),
19098 indent_guide(buffer_id, 3, 3, 2),
19099 ],
19100 None,
19101 &mut cx,
19102 );
19103}
19104
19105#[gpui::test]
19106async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
19107 let (buffer_id, mut cx) = setup_indent_guides_editor(
19108 &"
19109 block1
19110 block2
19111 block3
19112
19113 block1
19114 block1"
19115 .unindent(),
19116 cx,
19117 )
19118 .await;
19119
19120 assert_indent_guides(
19121 0..6,
19122 vec![
19123 indent_guide(buffer_id, 1, 2, 0),
19124 indent_guide(buffer_id, 2, 2, 1),
19125 ],
19126 None,
19127 &mut cx,
19128 );
19129}
19130
19131#[gpui::test]
19132async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
19133 let (buffer_id, mut cx) = setup_indent_guides_editor(
19134 &"
19135 function component() {
19136 \treturn (
19137 \t\t\t
19138 \t\t<div>
19139 \t\t\t<abc></abc>
19140 \t\t</div>
19141 \t)
19142 }"
19143 .unindent(),
19144 cx,
19145 )
19146 .await;
19147
19148 assert_indent_guides(
19149 0..8,
19150 vec![
19151 indent_guide(buffer_id, 1, 6, 0),
19152 indent_guide(buffer_id, 2, 5, 1),
19153 indent_guide(buffer_id, 4, 4, 2),
19154 ],
19155 None,
19156 &mut cx,
19157 );
19158}
19159
19160#[gpui::test]
19161async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
19162 let (buffer_id, mut cx) = setup_indent_guides_editor(
19163 &"
19164 function component() {
19165 \treturn (
19166 \t
19167 \t\t<div>
19168 \t\t\t<abc></abc>
19169 \t\t</div>
19170 \t)
19171 }"
19172 .unindent(),
19173 cx,
19174 )
19175 .await;
19176
19177 assert_indent_guides(
19178 0..8,
19179 vec![
19180 indent_guide(buffer_id, 1, 6, 0),
19181 indent_guide(buffer_id, 2, 5, 1),
19182 indent_guide(buffer_id, 4, 4, 2),
19183 ],
19184 None,
19185 &mut cx,
19186 );
19187}
19188
19189#[gpui::test]
19190async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
19191 let (buffer_id, mut cx) = setup_indent_guides_editor(
19192 &"
19193 block1
19194
19195
19196
19197 block2
19198 "
19199 .unindent(),
19200 cx,
19201 )
19202 .await;
19203
19204 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
19205}
19206
19207#[gpui::test]
19208async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
19209 let (buffer_id, mut cx) = setup_indent_guides_editor(
19210 &"
19211 def a:
19212 \tb = 3
19213 \tif True:
19214 \t\tc = 4
19215 \t\td = 5
19216 \tprint(b)
19217 "
19218 .unindent(),
19219 cx,
19220 )
19221 .await;
19222
19223 assert_indent_guides(
19224 0..6,
19225 vec![
19226 indent_guide(buffer_id, 1, 5, 0),
19227 indent_guide(buffer_id, 3, 4, 1),
19228 ],
19229 None,
19230 &mut cx,
19231 );
19232}
19233
19234#[gpui::test]
19235async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
19236 let (buffer_id, mut cx) = setup_indent_guides_editor(
19237 &"
19238 fn main() {
19239 let a = 1;
19240 }"
19241 .unindent(),
19242 cx,
19243 )
19244 .await;
19245
19246 cx.update_editor(|editor, window, cx| {
19247 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19248 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
19249 });
19250 });
19251
19252 assert_indent_guides(
19253 0..3,
19254 vec![indent_guide(buffer_id, 1, 1, 0)],
19255 Some(vec![0]),
19256 &mut cx,
19257 );
19258}
19259
19260#[gpui::test]
19261async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
19262 let (buffer_id, mut cx) = setup_indent_guides_editor(
19263 &"
19264 fn main() {
19265 if 1 == 2 {
19266 let a = 1;
19267 }
19268 }"
19269 .unindent(),
19270 cx,
19271 )
19272 .await;
19273
19274 cx.update_editor(|editor, window, cx| {
19275 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19276 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
19277 });
19278 });
19279
19280 assert_indent_guides(
19281 0..4,
19282 vec![
19283 indent_guide(buffer_id, 1, 3, 0),
19284 indent_guide(buffer_id, 2, 2, 1),
19285 ],
19286 Some(vec![1]),
19287 &mut cx,
19288 );
19289
19290 cx.update_editor(|editor, window, cx| {
19291 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19292 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
19293 });
19294 });
19295
19296 assert_indent_guides(
19297 0..4,
19298 vec![
19299 indent_guide(buffer_id, 1, 3, 0),
19300 indent_guide(buffer_id, 2, 2, 1),
19301 ],
19302 Some(vec![1]),
19303 &mut cx,
19304 );
19305
19306 cx.update_editor(|editor, window, cx| {
19307 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19308 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
19309 });
19310 });
19311
19312 assert_indent_guides(
19313 0..4,
19314 vec![
19315 indent_guide(buffer_id, 1, 3, 0),
19316 indent_guide(buffer_id, 2, 2, 1),
19317 ],
19318 Some(vec![0]),
19319 &mut cx,
19320 );
19321}
19322
19323#[gpui::test]
19324async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
19325 let (buffer_id, mut cx) = setup_indent_guides_editor(
19326 &"
19327 fn main() {
19328 let a = 1;
19329
19330 let b = 2;
19331 }"
19332 .unindent(),
19333 cx,
19334 )
19335 .await;
19336
19337 cx.update_editor(|editor, window, cx| {
19338 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19339 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
19340 });
19341 });
19342
19343 assert_indent_guides(
19344 0..5,
19345 vec![indent_guide(buffer_id, 1, 3, 0)],
19346 Some(vec![0]),
19347 &mut cx,
19348 );
19349}
19350
19351#[gpui::test]
19352async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
19353 let (buffer_id, mut cx) = setup_indent_guides_editor(
19354 &"
19355 def m:
19356 a = 1
19357 pass"
19358 .unindent(),
19359 cx,
19360 )
19361 .await;
19362
19363 cx.update_editor(|editor, window, cx| {
19364 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19365 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
19366 });
19367 });
19368
19369 assert_indent_guides(
19370 0..3,
19371 vec![indent_guide(buffer_id, 1, 2, 0)],
19372 Some(vec![0]),
19373 &mut cx,
19374 );
19375}
19376
19377#[gpui::test]
19378async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
19379 init_test(cx, |_| {});
19380 let mut cx = EditorTestContext::new(cx).await;
19381 let text = indoc! {
19382 "
19383 impl A {
19384 fn b() {
19385 0;
19386 3;
19387 5;
19388 6;
19389 7;
19390 }
19391 }
19392 "
19393 };
19394 let base_text = indoc! {
19395 "
19396 impl A {
19397 fn b() {
19398 0;
19399 1;
19400 2;
19401 3;
19402 4;
19403 }
19404 fn c() {
19405 5;
19406 6;
19407 7;
19408 }
19409 }
19410 "
19411 };
19412
19413 cx.update_editor(|editor, window, cx| {
19414 editor.set_text(text, window, cx);
19415
19416 editor.buffer().update(cx, |multibuffer, cx| {
19417 let buffer = multibuffer.as_singleton().unwrap();
19418 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
19419
19420 multibuffer.set_all_diff_hunks_expanded(cx);
19421 multibuffer.add_diff(diff, cx);
19422
19423 buffer.read(cx).remote_id()
19424 })
19425 });
19426 cx.run_until_parked();
19427
19428 cx.assert_state_with_diff(
19429 indoc! { "
19430 impl A {
19431 fn b() {
19432 0;
19433 - 1;
19434 - 2;
19435 3;
19436 - 4;
19437 - }
19438 - fn c() {
19439 5;
19440 6;
19441 7;
19442 }
19443 }
19444 ˇ"
19445 }
19446 .to_string(),
19447 );
19448
19449 let mut actual_guides = cx.update_editor(|editor, window, cx| {
19450 editor
19451 .snapshot(window, cx)
19452 .buffer_snapshot
19453 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
19454 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
19455 .collect::<Vec<_>>()
19456 });
19457 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
19458 assert_eq!(
19459 actual_guides,
19460 vec![
19461 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
19462 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
19463 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
19464 ]
19465 );
19466}
19467
19468#[gpui::test]
19469async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19470 init_test(cx, |_| {});
19471 let mut cx = EditorTestContext::new(cx).await;
19472
19473 let diff_base = r#"
19474 a
19475 b
19476 c
19477 "#
19478 .unindent();
19479
19480 cx.set_state(
19481 &r#"
19482 ˇA
19483 b
19484 C
19485 "#
19486 .unindent(),
19487 );
19488 cx.set_head_text(&diff_base);
19489 cx.update_editor(|editor, window, cx| {
19490 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19491 });
19492 executor.run_until_parked();
19493
19494 let both_hunks_expanded = r#"
19495 - a
19496 + ˇA
19497 b
19498 - c
19499 + C
19500 "#
19501 .unindent();
19502
19503 cx.assert_state_with_diff(both_hunks_expanded.clone());
19504
19505 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19506 let snapshot = editor.snapshot(window, cx);
19507 let hunks = editor
19508 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19509 .collect::<Vec<_>>();
19510 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19511 let buffer_id = hunks[0].buffer_id;
19512 hunks
19513 .into_iter()
19514 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
19515 .collect::<Vec<_>>()
19516 });
19517 assert_eq!(hunk_ranges.len(), 2);
19518
19519 cx.update_editor(|editor, _, cx| {
19520 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19521 });
19522 executor.run_until_parked();
19523
19524 let second_hunk_expanded = r#"
19525 ˇA
19526 b
19527 - c
19528 + C
19529 "#
19530 .unindent();
19531
19532 cx.assert_state_with_diff(second_hunk_expanded);
19533
19534 cx.update_editor(|editor, _, cx| {
19535 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19536 });
19537 executor.run_until_parked();
19538
19539 cx.assert_state_with_diff(both_hunks_expanded.clone());
19540
19541 cx.update_editor(|editor, _, cx| {
19542 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19543 });
19544 executor.run_until_parked();
19545
19546 let first_hunk_expanded = r#"
19547 - a
19548 + ˇA
19549 b
19550 C
19551 "#
19552 .unindent();
19553
19554 cx.assert_state_with_diff(first_hunk_expanded);
19555
19556 cx.update_editor(|editor, _, cx| {
19557 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19558 });
19559 executor.run_until_parked();
19560
19561 cx.assert_state_with_diff(both_hunks_expanded);
19562
19563 cx.set_state(
19564 &r#"
19565 ˇA
19566 b
19567 "#
19568 .unindent(),
19569 );
19570 cx.run_until_parked();
19571
19572 // TODO this cursor position seems bad
19573 cx.assert_state_with_diff(
19574 r#"
19575 - ˇa
19576 + A
19577 b
19578 "#
19579 .unindent(),
19580 );
19581
19582 cx.update_editor(|editor, window, cx| {
19583 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19584 });
19585
19586 cx.assert_state_with_diff(
19587 r#"
19588 - ˇa
19589 + A
19590 b
19591 - c
19592 "#
19593 .unindent(),
19594 );
19595
19596 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19597 let snapshot = editor.snapshot(window, cx);
19598 let hunks = editor
19599 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19600 .collect::<Vec<_>>();
19601 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19602 let buffer_id = hunks[0].buffer_id;
19603 hunks
19604 .into_iter()
19605 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
19606 .collect::<Vec<_>>()
19607 });
19608 assert_eq!(hunk_ranges.len(), 2);
19609
19610 cx.update_editor(|editor, _, cx| {
19611 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19612 });
19613 executor.run_until_parked();
19614
19615 cx.assert_state_with_diff(
19616 r#"
19617 - ˇa
19618 + A
19619 b
19620 "#
19621 .unindent(),
19622 );
19623}
19624
19625#[gpui::test]
19626async fn test_toggle_deletion_hunk_at_start_of_file(
19627 executor: BackgroundExecutor,
19628 cx: &mut TestAppContext,
19629) {
19630 init_test(cx, |_| {});
19631 let mut cx = EditorTestContext::new(cx).await;
19632
19633 let diff_base = r#"
19634 a
19635 b
19636 c
19637 "#
19638 .unindent();
19639
19640 cx.set_state(
19641 &r#"
19642 ˇb
19643 c
19644 "#
19645 .unindent(),
19646 );
19647 cx.set_head_text(&diff_base);
19648 cx.update_editor(|editor, window, cx| {
19649 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19650 });
19651 executor.run_until_parked();
19652
19653 let hunk_expanded = r#"
19654 - a
19655 ˇb
19656 c
19657 "#
19658 .unindent();
19659
19660 cx.assert_state_with_diff(hunk_expanded.clone());
19661
19662 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19663 let snapshot = editor.snapshot(window, cx);
19664 let hunks = editor
19665 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19666 .collect::<Vec<_>>();
19667 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19668 let buffer_id = hunks[0].buffer_id;
19669 hunks
19670 .into_iter()
19671 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
19672 .collect::<Vec<_>>()
19673 });
19674 assert_eq!(hunk_ranges.len(), 1);
19675
19676 cx.update_editor(|editor, _, cx| {
19677 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19678 });
19679 executor.run_until_parked();
19680
19681 let hunk_collapsed = r#"
19682 ˇb
19683 c
19684 "#
19685 .unindent();
19686
19687 cx.assert_state_with_diff(hunk_collapsed);
19688
19689 cx.update_editor(|editor, _, cx| {
19690 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19691 });
19692 executor.run_until_parked();
19693
19694 cx.assert_state_with_diff(hunk_expanded);
19695}
19696
19697#[gpui::test]
19698async fn test_display_diff_hunks(cx: &mut TestAppContext) {
19699 init_test(cx, |_| {});
19700
19701 let fs = FakeFs::new(cx.executor());
19702 fs.insert_tree(
19703 path!("/test"),
19704 json!({
19705 ".git": {},
19706 "file-1": "ONE\n",
19707 "file-2": "TWO\n",
19708 "file-3": "THREE\n",
19709 }),
19710 )
19711 .await;
19712
19713 fs.set_head_for_repo(
19714 path!("/test/.git").as_ref(),
19715 &[
19716 ("file-1".into(), "one\n".into()),
19717 ("file-2".into(), "two\n".into()),
19718 ("file-3".into(), "three\n".into()),
19719 ],
19720 "deadbeef",
19721 );
19722
19723 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
19724 let mut buffers = vec![];
19725 for i in 1..=3 {
19726 let buffer = project
19727 .update(cx, |project, cx| {
19728 let path = format!(path!("/test/file-{}"), i);
19729 project.open_local_buffer(path, cx)
19730 })
19731 .await
19732 .unwrap();
19733 buffers.push(buffer);
19734 }
19735
19736 let multibuffer = cx.new(|cx| {
19737 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
19738 multibuffer.set_all_diff_hunks_expanded(cx);
19739 for buffer in &buffers {
19740 let snapshot = buffer.read(cx).snapshot();
19741 multibuffer.set_excerpts_for_path(
19742 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
19743 buffer.clone(),
19744 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
19745 DEFAULT_MULTIBUFFER_CONTEXT,
19746 cx,
19747 );
19748 }
19749 multibuffer
19750 });
19751
19752 let editor = cx.add_window(|window, cx| {
19753 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
19754 });
19755 cx.run_until_parked();
19756
19757 let snapshot = editor
19758 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19759 .unwrap();
19760 let hunks = snapshot
19761 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
19762 .map(|hunk| match hunk {
19763 DisplayDiffHunk::Unfolded {
19764 display_row_range, ..
19765 } => display_row_range,
19766 DisplayDiffHunk::Folded { .. } => unreachable!(),
19767 })
19768 .collect::<Vec<_>>();
19769 assert_eq!(
19770 hunks,
19771 [
19772 DisplayRow(2)..DisplayRow(4),
19773 DisplayRow(7)..DisplayRow(9),
19774 DisplayRow(12)..DisplayRow(14),
19775 ]
19776 );
19777}
19778
19779#[gpui::test]
19780async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
19781 init_test(cx, |_| {});
19782
19783 let mut cx = EditorTestContext::new(cx).await;
19784 cx.set_head_text(indoc! { "
19785 one
19786 two
19787 three
19788 four
19789 five
19790 "
19791 });
19792 cx.set_index_text(indoc! { "
19793 one
19794 two
19795 three
19796 four
19797 five
19798 "
19799 });
19800 cx.set_state(indoc! {"
19801 one
19802 TWO
19803 ˇTHREE
19804 FOUR
19805 five
19806 "});
19807 cx.run_until_parked();
19808 cx.update_editor(|editor, window, cx| {
19809 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19810 });
19811 cx.run_until_parked();
19812 cx.assert_index_text(Some(indoc! {"
19813 one
19814 TWO
19815 THREE
19816 FOUR
19817 five
19818 "}));
19819 cx.set_state(indoc! { "
19820 one
19821 TWO
19822 ˇTHREE-HUNDRED
19823 FOUR
19824 five
19825 "});
19826 cx.run_until_parked();
19827 cx.update_editor(|editor, window, cx| {
19828 let snapshot = editor.snapshot(window, cx);
19829 let hunks = editor
19830 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19831 .collect::<Vec<_>>();
19832 assert_eq!(hunks.len(), 1);
19833 assert_eq!(
19834 hunks[0].status(),
19835 DiffHunkStatus {
19836 kind: DiffHunkStatusKind::Modified,
19837 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
19838 }
19839 );
19840
19841 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19842 });
19843 cx.run_until_parked();
19844 cx.assert_index_text(Some(indoc! {"
19845 one
19846 TWO
19847 THREE-HUNDRED
19848 FOUR
19849 five
19850 "}));
19851}
19852
19853#[gpui::test]
19854fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
19855 init_test(cx, |_| {});
19856
19857 let editor = cx.add_window(|window, cx| {
19858 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
19859 build_editor(buffer, window, cx)
19860 });
19861
19862 let render_args = Arc::new(Mutex::new(None));
19863 let snapshot = editor
19864 .update(cx, |editor, window, cx| {
19865 let snapshot = editor.buffer().read(cx).snapshot(cx);
19866 let range =
19867 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
19868
19869 struct RenderArgs {
19870 row: MultiBufferRow,
19871 folded: bool,
19872 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
19873 }
19874
19875 let crease = Crease::inline(
19876 range,
19877 FoldPlaceholder::test(),
19878 {
19879 let toggle_callback = render_args.clone();
19880 move |row, folded, callback, _window, _cx| {
19881 *toggle_callback.lock() = Some(RenderArgs {
19882 row,
19883 folded,
19884 callback,
19885 });
19886 div()
19887 }
19888 },
19889 |_row, _folded, _window, _cx| div(),
19890 );
19891
19892 editor.insert_creases(Some(crease), cx);
19893 let snapshot = editor.snapshot(window, cx);
19894 let _div =
19895 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
19896 snapshot
19897 })
19898 .unwrap();
19899
19900 let render_args = render_args.lock().take().unwrap();
19901 assert_eq!(render_args.row, MultiBufferRow(1));
19902 assert!(!render_args.folded);
19903 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19904
19905 cx.update_window(*editor, |_, window, cx| {
19906 (render_args.callback)(true, window, cx)
19907 })
19908 .unwrap();
19909 let snapshot = editor
19910 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19911 .unwrap();
19912 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
19913
19914 cx.update_window(*editor, |_, window, cx| {
19915 (render_args.callback)(false, window, cx)
19916 })
19917 .unwrap();
19918 let snapshot = editor
19919 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19920 .unwrap();
19921 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19922}
19923
19924#[gpui::test]
19925async fn test_input_text(cx: &mut TestAppContext) {
19926 init_test(cx, |_| {});
19927 let mut cx = EditorTestContext::new(cx).await;
19928
19929 cx.set_state(
19930 &r#"ˇone
19931 two
19932
19933 three
19934 fourˇ
19935 five
19936
19937 siˇx"#
19938 .unindent(),
19939 );
19940
19941 cx.dispatch_action(HandleInput(String::new()));
19942 cx.assert_editor_state(
19943 &r#"ˇone
19944 two
19945
19946 three
19947 fourˇ
19948 five
19949
19950 siˇx"#
19951 .unindent(),
19952 );
19953
19954 cx.dispatch_action(HandleInput("AAAA".to_string()));
19955 cx.assert_editor_state(
19956 &r#"AAAAˇone
19957 two
19958
19959 three
19960 fourAAAAˇ
19961 five
19962
19963 siAAAAˇx"#
19964 .unindent(),
19965 );
19966}
19967
19968#[gpui::test]
19969async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
19970 init_test(cx, |_| {});
19971
19972 let mut cx = EditorTestContext::new(cx).await;
19973 cx.set_state(
19974 r#"let foo = 1;
19975let foo = 2;
19976let foo = 3;
19977let fooˇ = 4;
19978let foo = 5;
19979let foo = 6;
19980let foo = 7;
19981let foo = 8;
19982let foo = 9;
19983let foo = 10;
19984let foo = 11;
19985let foo = 12;
19986let foo = 13;
19987let foo = 14;
19988let foo = 15;"#,
19989 );
19990
19991 cx.update_editor(|e, window, cx| {
19992 assert_eq!(
19993 e.next_scroll_position,
19994 NextScrollCursorCenterTopBottom::Center,
19995 "Default next scroll direction is center",
19996 );
19997
19998 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19999 assert_eq!(
20000 e.next_scroll_position,
20001 NextScrollCursorCenterTopBottom::Top,
20002 "After center, next scroll direction should be top",
20003 );
20004
20005 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
20006 assert_eq!(
20007 e.next_scroll_position,
20008 NextScrollCursorCenterTopBottom::Bottom,
20009 "After top, next scroll direction should be bottom",
20010 );
20011
20012 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
20013 assert_eq!(
20014 e.next_scroll_position,
20015 NextScrollCursorCenterTopBottom::Center,
20016 "After bottom, scrolling should start over",
20017 );
20018
20019 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
20020 assert_eq!(
20021 e.next_scroll_position,
20022 NextScrollCursorCenterTopBottom::Top,
20023 "Scrolling continues if retriggered fast enough"
20024 );
20025 });
20026
20027 cx.executor()
20028 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
20029 cx.executor().run_until_parked();
20030 cx.update_editor(|e, _, _| {
20031 assert_eq!(
20032 e.next_scroll_position,
20033 NextScrollCursorCenterTopBottom::Center,
20034 "If scrolling is not triggered fast enough, it should reset"
20035 );
20036 });
20037}
20038
20039#[gpui::test]
20040async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
20041 init_test(cx, |_| {});
20042 let mut cx = EditorLspTestContext::new_rust(
20043 lsp::ServerCapabilities {
20044 definition_provider: Some(lsp::OneOf::Left(true)),
20045 references_provider: Some(lsp::OneOf::Left(true)),
20046 ..lsp::ServerCapabilities::default()
20047 },
20048 cx,
20049 )
20050 .await;
20051
20052 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
20053 let go_to_definition = cx
20054 .lsp
20055 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
20056 move |params, _| async move {
20057 if empty_go_to_definition {
20058 Ok(None)
20059 } else {
20060 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
20061 uri: params.text_document_position_params.text_document.uri,
20062 range: lsp::Range::new(
20063 lsp::Position::new(4, 3),
20064 lsp::Position::new(4, 6),
20065 ),
20066 })))
20067 }
20068 },
20069 );
20070 let references = cx
20071 .lsp
20072 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
20073 Ok(Some(vec![lsp::Location {
20074 uri: params.text_document_position.text_document.uri,
20075 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
20076 }]))
20077 });
20078 (go_to_definition, references)
20079 };
20080
20081 cx.set_state(
20082 &r#"fn one() {
20083 let mut a = ˇtwo();
20084 }
20085
20086 fn two() {}"#
20087 .unindent(),
20088 );
20089 set_up_lsp_handlers(false, &mut cx);
20090 let navigated = cx
20091 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
20092 .await
20093 .expect("Failed to navigate to definition");
20094 assert_eq!(
20095 navigated,
20096 Navigated::Yes,
20097 "Should have navigated to definition from the GetDefinition response"
20098 );
20099 cx.assert_editor_state(
20100 &r#"fn one() {
20101 let mut a = two();
20102 }
20103
20104 fn «twoˇ»() {}"#
20105 .unindent(),
20106 );
20107
20108 let editors = cx.update_workspace(|workspace, _, cx| {
20109 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
20110 });
20111 cx.update_editor(|_, _, test_editor_cx| {
20112 assert_eq!(
20113 editors.len(),
20114 1,
20115 "Initially, only one, test, editor should be open in the workspace"
20116 );
20117 assert_eq!(
20118 test_editor_cx.entity(),
20119 editors.last().expect("Asserted len is 1").clone()
20120 );
20121 });
20122
20123 set_up_lsp_handlers(true, &mut cx);
20124 let navigated = cx
20125 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
20126 .await
20127 .expect("Failed to navigate to lookup references");
20128 assert_eq!(
20129 navigated,
20130 Navigated::Yes,
20131 "Should have navigated to references as a fallback after empty GoToDefinition response"
20132 );
20133 // We should not change the selections in the existing file,
20134 // if opening another milti buffer with the references
20135 cx.assert_editor_state(
20136 &r#"fn one() {
20137 let mut a = two();
20138 }
20139
20140 fn «twoˇ»() {}"#
20141 .unindent(),
20142 );
20143 let editors = cx.update_workspace(|workspace, _, cx| {
20144 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
20145 });
20146 cx.update_editor(|_, _, test_editor_cx| {
20147 assert_eq!(
20148 editors.len(),
20149 2,
20150 "After falling back to references search, we open a new editor with the results"
20151 );
20152 let references_fallback_text = editors
20153 .into_iter()
20154 .find(|new_editor| *new_editor != test_editor_cx.entity())
20155 .expect("Should have one non-test editor now")
20156 .read(test_editor_cx)
20157 .text(test_editor_cx);
20158 assert_eq!(
20159 references_fallback_text, "fn one() {\n let mut a = two();\n}",
20160 "Should use the range from the references response and not the GoToDefinition one"
20161 );
20162 });
20163}
20164
20165#[gpui::test]
20166async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
20167 init_test(cx, |_| {});
20168 cx.update(|cx| {
20169 let mut editor_settings = EditorSettings::get_global(cx).clone();
20170 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
20171 EditorSettings::override_global(editor_settings, cx);
20172 });
20173 let mut cx = EditorLspTestContext::new_rust(
20174 lsp::ServerCapabilities {
20175 definition_provider: Some(lsp::OneOf::Left(true)),
20176 references_provider: Some(lsp::OneOf::Left(true)),
20177 ..lsp::ServerCapabilities::default()
20178 },
20179 cx,
20180 )
20181 .await;
20182 let original_state = r#"fn one() {
20183 let mut a = ˇtwo();
20184 }
20185
20186 fn two() {}"#
20187 .unindent();
20188 cx.set_state(&original_state);
20189
20190 let mut go_to_definition = cx
20191 .lsp
20192 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
20193 move |_, _| async move { Ok(None) },
20194 );
20195 let _references = cx
20196 .lsp
20197 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
20198 panic!("Should not call for references with no go to definition fallback")
20199 });
20200
20201 let navigated = cx
20202 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
20203 .await
20204 .expect("Failed to navigate to lookup references");
20205 go_to_definition
20206 .next()
20207 .await
20208 .expect("Should have called the go_to_definition handler");
20209
20210 assert_eq!(
20211 navigated,
20212 Navigated::No,
20213 "Should have navigated to references as a fallback after empty GoToDefinition response"
20214 );
20215 cx.assert_editor_state(&original_state);
20216 let editors = cx.update_workspace(|workspace, _, cx| {
20217 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
20218 });
20219 cx.update_editor(|_, _, _| {
20220 assert_eq!(
20221 editors.len(),
20222 1,
20223 "After unsuccessful fallback, no other editor should have been opened"
20224 );
20225 });
20226}
20227
20228#[gpui::test]
20229async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
20230 init_test(cx, |_| {});
20231
20232 let language = Arc::new(Language::new(
20233 LanguageConfig::default(),
20234 Some(tree_sitter_rust::LANGUAGE.into()),
20235 ));
20236
20237 let text = r#"
20238 #[cfg(test)]
20239 mod tests() {
20240 #[test]
20241 fn runnable_1() {
20242 let a = 1;
20243 }
20244
20245 #[test]
20246 fn runnable_2() {
20247 let a = 1;
20248 let b = 2;
20249 }
20250 }
20251 "#
20252 .unindent();
20253
20254 let fs = FakeFs::new(cx.executor());
20255 fs.insert_file("/file.rs", Default::default()).await;
20256
20257 let project = Project::test(fs, ["/a".as_ref()], cx).await;
20258 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20259 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20260 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
20261 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
20262
20263 let editor = cx.new_window_entity(|window, cx| {
20264 Editor::new(
20265 EditorMode::full(),
20266 multi_buffer,
20267 Some(project.clone()),
20268 window,
20269 cx,
20270 )
20271 });
20272
20273 editor.update_in(cx, |editor, window, cx| {
20274 let snapshot = editor.buffer().read(cx).snapshot(cx);
20275 editor.tasks.insert(
20276 (buffer.read(cx).remote_id(), 3),
20277 RunnableTasks {
20278 templates: vec![],
20279 offset: snapshot.anchor_before(43),
20280 column: 0,
20281 extra_variables: HashMap::default(),
20282 context_range: BufferOffset(43)..BufferOffset(85),
20283 },
20284 );
20285 editor.tasks.insert(
20286 (buffer.read(cx).remote_id(), 8),
20287 RunnableTasks {
20288 templates: vec![],
20289 offset: snapshot.anchor_before(86),
20290 column: 0,
20291 extra_variables: HashMap::default(),
20292 context_range: BufferOffset(86)..BufferOffset(191),
20293 },
20294 );
20295
20296 // Test finding task when cursor is inside function body
20297 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20298 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
20299 });
20300 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
20301 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
20302
20303 // Test finding task when cursor is on function name
20304 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20305 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
20306 });
20307 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
20308 assert_eq!(row, 8, "Should find task when cursor is on function name");
20309 });
20310}
20311
20312#[gpui::test]
20313async fn test_folding_buffers(cx: &mut TestAppContext) {
20314 init_test(cx, |_| {});
20315
20316 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
20317 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
20318 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
20319
20320 let fs = FakeFs::new(cx.executor());
20321 fs.insert_tree(
20322 path!("/a"),
20323 json!({
20324 "first.rs": sample_text_1,
20325 "second.rs": sample_text_2,
20326 "third.rs": sample_text_3,
20327 }),
20328 )
20329 .await;
20330 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20331 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20332 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20333 let worktree = project.update(cx, |project, cx| {
20334 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20335 assert_eq!(worktrees.len(), 1);
20336 worktrees.pop().unwrap()
20337 });
20338 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20339
20340 let buffer_1 = project
20341 .update(cx, |project, cx| {
20342 project.open_buffer((worktree_id, "first.rs"), cx)
20343 })
20344 .await
20345 .unwrap();
20346 let buffer_2 = project
20347 .update(cx, |project, cx| {
20348 project.open_buffer((worktree_id, "second.rs"), cx)
20349 })
20350 .await
20351 .unwrap();
20352 let buffer_3 = project
20353 .update(cx, |project, cx| {
20354 project.open_buffer((worktree_id, "third.rs"), cx)
20355 })
20356 .await
20357 .unwrap();
20358
20359 let multi_buffer = cx.new(|cx| {
20360 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20361 multi_buffer.push_excerpts(
20362 buffer_1.clone(),
20363 [
20364 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20365 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20366 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20367 ],
20368 cx,
20369 );
20370 multi_buffer.push_excerpts(
20371 buffer_2.clone(),
20372 [
20373 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20374 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20375 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20376 ],
20377 cx,
20378 );
20379 multi_buffer.push_excerpts(
20380 buffer_3.clone(),
20381 [
20382 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20383 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20384 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20385 ],
20386 cx,
20387 );
20388 multi_buffer
20389 });
20390 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20391 Editor::new(
20392 EditorMode::full(),
20393 multi_buffer.clone(),
20394 Some(project.clone()),
20395 window,
20396 cx,
20397 )
20398 });
20399
20400 assert_eq!(
20401 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20402 "\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",
20403 );
20404
20405 multi_buffer_editor.update(cx, |editor, cx| {
20406 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
20407 });
20408 assert_eq!(
20409 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20410 "\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",
20411 "After folding the first buffer, its text should not be displayed"
20412 );
20413
20414 multi_buffer_editor.update(cx, |editor, cx| {
20415 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
20416 });
20417 assert_eq!(
20418 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20419 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
20420 "After folding the second buffer, its text should not be displayed"
20421 );
20422
20423 multi_buffer_editor.update(cx, |editor, cx| {
20424 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
20425 });
20426 assert_eq!(
20427 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20428 "\n\n\n\n\n",
20429 "After folding the third buffer, its text should not be displayed"
20430 );
20431
20432 // Emulate selection inside the fold logic, that should work
20433 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20434 editor
20435 .snapshot(window, cx)
20436 .next_line_boundary(Point::new(0, 4));
20437 });
20438
20439 multi_buffer_editor.update(cx, |editor, cx| {
20440 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
20441 });
20442 assert_eq!(
20443 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20444 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
20445 "After unfolding the second buffer, its text should be displayed"
20446 );
20447
20448 // Typing inside of buffer 1 causes that buffer to be unfolded.
20449 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20450 assert_eq!(
20451 multi_buffer
20452 .read(cx)
20453 .snapshot(cx)
20454 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
20455 .collect::<String>(),
20456 "bbbb"
20457 );
20458 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
20459 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
20460 });
20461 editor.handle_input("B", window, cx);
20462 });
20463
20464 assert_eq!(
20465 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20466 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
20467 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
20468 );
20469
20470 multi_buffer_editor.update(cx, |editor, cx| {
20471 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
20472 });
20473 assert_eq!(
20474 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20475 "\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",
20476 "After unfolding the all buffers, all original text should be displayed"
20477 );
20478}
20479
20480#[gpui::test]
20481async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
20482 init_test(cx, |_| {});
20483
20484 let sample_text_1 = "1111\n2222\n3333".to_string();
20485 let sample_text_2 = "4444\n5555\n6666".to_string();
20486 let sample_text_3 = "7777\n8888\n9999".to_string();
20487
20488 let fs = FakeFs::new(cx.executor());
20489 fs.insert_tree(
20490 path!("/a"),
20491 json!({
20492 "first.rs": sample_text_1,
20493 "second.rs": sample_text_2,
20494 "third.rs": sample_text_3,
20495 }),
20496 )
20497 .await;
20498 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20499 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20500 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20501 let worktree = project.update(cx, |project, cx| {
20502 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20503 assert_eq!(worktrees.len(), 1);
20504 worktrees.pop().unwrap()
20505 });
20506 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20507
20508 let buffer_1 = project
20509 .update(cx, |project, cx| {
20510 project.open_buffer((worktree_id, "first.rs"), cx)
20511 })
20512 .await
20513 .unwrap();
20514 let buffer_2 = project
20515 .update(cx, |project, cx| {
20516 project.open_buffer((worktree_id, "second.rs"), cx)
20517 })
20518 .await
20519 .unwrap();
20520 let buffer_3 = project
20521 .update(cx, |project, cx| {
20522 project.open_buffer((worktree_id, "third.rs"), cx)
20523 })
20524 .await
20525 .unwrap();
20526
20527 let multi_buffer = cx.new(|cx| {
20528 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20529 multi_buffer.push_excerpts(
20530 buffer_1.clone(),
20531 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20532 cx,
20533 );
20534 multi_buffer.push_excerpts(
20535 buffer_2.clone(),
20536 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20537 cx,
20538 );
20539 multi_buffer.push_excerpts(
20540 buffer_3.clone(),
20541 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20542 cx,
20543 );
20544 multi_buffer
20545 });
20546
20547 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20548 Editor::new(
20549 EditorMode::full(),
20550 multi_buffer,
20551 Some(project.clone()),
20552 window,
20553 cx,
20554 )
20555 });
20556
20557 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
20558 assert_eq!(
20559 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20560 full_text,
20561 );
20562
20563 multi_buffer_editor.update(cx, |editor, cx| {
20564 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
20565 });
20566 assert_eq!(
20567 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20568 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
20569 "After folding the first buffer, its text should not be displayed"
20570 );
20571
20572 multi_buffer_editor.update(cx, |editor, cx| {
20573 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
20574 });
20575
20576 assert_eq!(
20577 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20578 "\n\n\n\n\n\n7777\n8888\n9999",
20579 "After folding the second buffer, its text should not be displayed"
20580 );
20581
20582 multi_buffer_editor.update(cx, |editor, cx| {
20583 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
20584 });
20585 assert_eq!(
20586 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20587 "\n\n\n\n\n",
20588 "After folding the third buffer, its text should not be displayed"
20589 );
20590
20591 multi_buffer_editor.update(cx, |editor, cx| {
20592 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
20593 });
20594 assert_eq!(
20595 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20596 "\n\n\n\n4444\n5555\n6666\n\n",
20597 "After unfolding the second buffer, its text should be displayed"
20598 );
20599
20600 multi_buffer_editor.update(cx, |editor, cx| {
20601 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
20602 });
20603 assert_eq!(
20604 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20605 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
20606 "After unfolding the first buffer, its text should be displayed"
20607 );
20608
20609 multi_buffer_editor.update(cx, |editor, cx| {
20610 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
20611 });
20612 assert_eq!(
20613 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20614 full_text,
20615 "After unfolding all buffers, all original text should be displayed"
20616 );
20617}
20618
20619#[gpui::test]
20620async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
20621 init_test(cx, |_| {});
20622
20623 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
20624
20625 let fs = FakeFs::new(cx.executor());
20626 fs.insert_tree(
20627 path!("/a"),
20628 json!({
20629 "main.rs": sample_text,
20630 }),
20631 )
20632 .await;
20633 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20634 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20635 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20636 let worktree = project.update(cx, |project, cx| {
20637 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20638 assert_eq!(worktrees.len(), 1);
20639 worktrees.pop().unwrap()
20640 });
20641 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20642
20643 let buffer_1 = project
20644 .update(cx, |project, cx| {
20645 project.open_buffer((worktree_id, "main.rs"), cx)
20646 })
20647 .await
20648 .unwrap();
20649
20650 let multi_buffer = cx.new(|cx| {
20651 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20652 multi_buffer.push_excerpts(
20653 buffer_1.clone(),
20654 [ExcerptRange::new(
20655 Point::new(0, 0)
20656 ..Point::new(
20657 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
20658 0,
20659 ),
20660 )],
20661 cx,
20662 );
20663 multi_buffer
20664 });
20665 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20666 Editor::new(
20667 EditorMode::full(),
20668 multi_buffer,
20669 Some(project.clone()),
20670 window,
20671 cx,
20672 )
20673 });
20674
20675 let selection_range = Point::new(1, 0)..Point::new(2, 0);
20676 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20677 enum TestHighlight {}
20678 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
20679 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
20680 editor.highlight_text::<TestHighlight>(
20681 vec![highlight_range.clone()],
20682 HighlightStyle::color(Hsla::green()),
20683 cx,
20684 );
20685 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20686 s.select_ranges(Some(highlight_range))
20687 });
20688 });
20689
20690 let full_text = format!("\n\n{sample_text}");
20691 assert_eq!(
20692 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20693 full_text,
20694 );
20695}
20696
20697#[gpui::test]
20698async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
20699 init_test(cx, |_| {});
20700 cx.update(|cx| {
20701 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
20702 "keymaps/default-linux.json",
20703 cx,
20704 )
20705 .unwrap();
20706 cx.bind_keys(default_key_bindings);
20707 });
20708
20709 let (editor, cx) = cx.add_window_view(|window, cx| {
20710 let multi_buffer = MultiBuffer::build_multi(
20711 [
20712 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
20713 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
20714 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
20715 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
20716 ],
20717 cx,
20718 );
20719 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
20720
20721 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
20722 // fold all but the second buffer, so that we test navigating between two
20723 // adjacent folded buffers, as well as folded buffers at the start and
20724 // end the multibuffer
20725 editor.fold_buffer(buffer_ids[0], cx);
20726 editor.fold_buffer(buffer_ids[2], cx);
20727 editor.fold_buffer(buffer_ids[3], cx);
20728
20729 editor
20730 });
20731 cx.simulate_resize(size(px(1000.), px(1000.)));
20732
20733 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
20734 cx.assert_excerpts_with_selections(indoc! {"
20735 [EXCERPT]
20736 ˇ[FOLDED]
20737 [EXCERPT]
20738 a1
20739 b1
20740 [EXCERPT]
20741 [FOLDED]
20742 [EXCERPT]
20743 [FOLDED]
20744 "
20745 });
20746 cx.simulate_keystroke("down");
20747 cx.assert_excerpts_with_selections(indoc! {"
20748 [EXCERPT]
20749 [FOLDED]
20750 [EXCERPT]
20751 ˇa1
20752 b1
20753 [EXCERPT]
20754 [FOLDED]
20755 [EXCERPT]
20756 [FOLDED]
20757 "
20758 });
20759 cx.simulate_keystroke("down");
20760 cx.assert_excerpts_with_selections(indoc! {"
20761 [EXCERPT]
20762 [FOLDED]
20763 [EXCERPT]
20764 a1
20765 ˇb1
20766 [EXCERPT]
20767 [FOLDED]
20768 [EXCERPT]
20769 [FOLDED]
20770 "
20771 });
20772 cx.simulate_keystroke("down");
20773 cx.assert_excerpts_with_selections(indoc! {"
20774 [EXCERPT]
20775 [FOLDED]
20776 [EXCERPT]
20777 a1
20778 b1
20779 ˇ[EXCERPT]
20780 [FOLDED]
20781 [EXCERPT]
20782 [FOLDED]
20783 "
20784 });
20785 cx.simulate_keystroke("down");
20786 cx.assert_excerpts_with_selections(indoc! {"
20787 [EXCERPT]
20788 [FOLDED]
20789 [EXCERPT]
20790 a1
20791 b1
20792 [EXCERPT]
20793 ˇ[FOLDED]
20794 [EXCERPT]
20795 [FOLDED]
20796 "
20797 });
20798 for _ in 0..5 {
20799 cx.simulate_keystroke("down");
20800 cx.assert_excerpts_with_selections(indoc! {"
20801 [EXCERPT]
20802 [FOLDED]
20803 [EXCERPT]
20804 a1
20805 b1
20806 [EXCERPT]
20807 [FOLDED]
20808 [EXCERPT]
20809 ˇ[FOLDED]
20810 "
20811 });
20812 }
20813
20814 cx.simulate_keystroke("up");
20815 cx.assert_excerpts_with_selections(indoc! {"
20816 [EXCERPT]
20817 [FOLDED]
20818 [EXCERPT]
20819 a1
20820 b1
20821 [EXCERPT]
20822 ˇ[FOLDED]
20823 [EXCERPT]
20824 [FOLDED]
20825 "
20826 });
20827 cx.simulate_keystroke("up");
20828 cx.assert_excerpts_with_selections(indoc! {"
20829 [EXCERPT]
20830 [FOLDED]
20831 [EXCERPT]
20832 a1
20833 b1
20834 ˇ[EXCERPT]
20835 [FOLDED]
20836 [EXCERPT]
20837 [FOLDED]
20838 "
20839 });
20840 cx.simulate_keystroke("up");
20841 cx.assert_excerpts_with_selections(indoc! {"
20842 [EXCERPT]
20843 [FOLDED]
20844 [EXCERPT]
20845 a1
20846 ˇb1
20847 [EXCERPT]
20848 [FOLDED]
20849 [EXCERPT]
20850 [FOLDED]
20851 "
20852 });
20853 cx.simulate_keystroke("up");
20854 cx.assert_excerpts_with_selections(indoc! {"
20855 [EXCERPT]
20856 [FOLDED]
20857 [EXCERPT]
20858 ˇa1
20859 b1
20860 [EXCERPT]
20861 [FOLDED]
20862 [EXCERPT]
20863 [FOLDED]
20864 "
20865 });
20866 for _ in 0..5 {
20867 cx.simulate_keystroke("up");
20868 cx.assert_excerpts_with_selections(indoc! {"
20869 [EXCERPT]
20870 ˇ[FOLDED]
20871 [EXCERPT]
20872 a1
20873 b1
20874 [EXCERPT]
20875 [FOLDED]
20876 [EXCERPT]
20877 [FOLDED]
20878 "
20879 });
20880 }
20881}
20882
20883#[gpui::test]
20884async fn test_edit_prediction_text(cx: &mut TestAppContext) {
20885 init_test(cx, |_| {});
20886
20887 // Simple insertion
20888 assert_highlighted_edits(
20889 "Hello, world!",
20890 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
20891 true,
20892 cx,
20893 |highlighted_edits, cx| {
20894 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
20895 assert_eq!(highlighted_edits.highlights.len(), 1);
20896 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
20897 assert_eq!(
20898 highlighted_edits.highlights[0].1.background_color,
20899 Some(cx.theme().status().created_background)
20900 );
20901 },
20902 )
20903 .await;
20904
20905 // Replacement
20906 assert_highlighted_edits(
20907 "This is a test.",
20908 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
20909 false,
20910 cx,
20911 |highlighted_edits, cx| {
20912 assert_eq!(highlighted_edits.text, "That is a test.");
20913 assert_eq!(highlighted_edits.highlights.len(), 1);
20914 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
20915 assert_eq!(
20916 highlighted_edits.highlights[0].1.background_color,
20917 Some(cx.theme().status().created_background)
20918 );
20919 },
20920 )
20921 .await;
20922
20923 // Multiple edits
20924 assert_highlighted_edits(
20925 "Hello, world!",
20926 vec![
20927 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
20928 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
20929 ],
20930 false,
20931 cx,
20932 |highlighted_edits, cx| {
20933 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
20934 assert_eq!(highlighted_edits.highlights.len(), 2);
20935 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
20936 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
20937 assert_eq!(
20938 highlighted_edits.highlights[0].1.background_color,
20939 Some(cx.theme().status().created_background)
20940 );
20941 assert_eq!(
20942 highlighted_edits.highlights[1].1.background_color,
20943 Some(cx.theme().status().created_background)
20944 );
20945 },
20946 )
20947 .await;
20948
20949 // Multiple lines with edits
20950 assert_highlighted_edits(
20951 "First line\nSecond line\nThird line\nFourth line",
20952 vec![
20953 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
20954 (
20955 Point::new(2, 0)..Point::new(2, 10),
20956 "New third line".to_string(),
20957 ),
20958 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
20959 ],
20960 false,
20961 cx,
20962 |highlighted_edits, cx| {
20963 assert_eq!(
20964 highlighted_edits.text,
20965 "Second modified\nNew third line\nFourth updated line"
20966 );
20967 assert_eq!(highlighted_edits.highlights.len(), 3);
20968 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
20969 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
20970 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
20971 for highlight in &highlighted_edits.highlights {
20972 assert_eq!(
20973 highlight.1.background_color,
20974 Some(cx.theme().status().created_background)
20975 );
20976 }
20977 },
20978 )
20979 .await;
20980}
20981
20982#[gpui::test]
20983async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
20984 init_test(cx, |_| {});
20985
20986 // Deletion
20987 assert_highlighted_edits(
20988 "Hello, world!",
20989 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
20990 true,
20991 cx,
20992 |highlighted_edits, cx| {
20993 assert_eq!(highlighted_edits.text, "Hello, world!");
20994 assert_eq!(highlighted_edits.highlights.len(), 1);
20995 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
20996 assert_eq!(
20997 highlighted_edits.highlights[0].1.background_color,
20998 Some(cx.theme().status().deleted_background)
20999 );
21000 },
21001 )
21002 .await;
21003
21004 // Insertion
21005 assert_highlighted_edits(
21006 "Hello, world!",
21007 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
21008 true,
21009 cx,
21010 |highlighted_edits, cx| {
21011 assert_eq!(highlighted_edits.highlights.len(), 1);
21012 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
21013 assert_eq!(
21014 highlighted_edits.highlights[0].1.background_color,
21015 Some(cx.theme().status().created_background)
21016 );
21017 },
21018 )
21019 .await;
21020}
21021
21022async fn assert_highlighted_edits(
21023 text: &str,
21024 edits: Vec<(Range<Point>, String)>,
21025 include_deletions: bool,
21026 cx: &mut TestAppContext,
21027 assertion_fn: impl Fn(HighlightedText, &App),
21028) {
21029 let window = cx.add_window(|window, cx| {
21030 let buffer = MultiBuffer::build_simple(text, cx);
21031 Editor::new(EditorMode::full(), buffer, None, window, cx)
21032 });
21033 let cx = &mut VisualTestContext::from_window(*window, cx);
21034
21035 let (buffer, snapshot) = window
21036 .update(cx, |editor, _window, cx| {
21037 (
21038 editor.buffer().clone(),
21039 editor.buffer().read(cx).snapshot(cx),
21040 )
21041 })
21042 .unwrap();
21043
21044 let edits = edits
21045 .into_iter()
21046 .map(|(range, edit)| {
21047 (
21048 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
21049 edit,
21050 )
21051 })
21052 .collect::<Vec<_>>();
21053
21054 let text_anchor_edits = edits
21055 .clone()
21056 .into_iter()
21057 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
21058 .collect::<Vec<_>>();
21059
21060 let edit_preview = window
21061 .update(cx, |_, _window, cx| {
21062 buffer
21063 .read(cx)
21064 .as_singleton()
21065 .unwrap()
21066 .read(cx)
21067 .preview_edits(text_anchor_edits.into(), cx)
21068 })
21069 .unwrap()
21070 .await;
21071
21072 cx.update(|_window, cx| {
21073 let highlighted_edits = edit_prediction_edit_text(
21074 snapshot.as_singleton().unwrap().2,
21075 &edits,
21076 &edit_preview,
21077 include_deletions,
21078 cx,
21079 );
21080 assertion_fn(highlighted_edits, cx)
21081 });
21082}
21083
21084#[track_caller]
21085fn assert_breakpoint(
21086 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
21087 path: &Arc<Path>,
21088 expected: Vec<(u32, Breakpoint)>,
21089) {
21090 if expected.is_empty() {
21091 assert!(!breakpoints.contains_key(path), "{}", path.display());
21092 } else {
21093 let mut breakpoint = breakpoints
21094 .get(path)
21095 .unwrap()
21096 .iter()
21097 .map(|breakpoint| {
21098 (
21099 breakpoint.row,
21100 Breakpoint {
21101 message: breakpoint.message.clone(),
21102 state: breakpoint.state,
21103 condition: breakpoint.condition.clone(),
21104 hit_condition: breakpoint.hit_condition.clone(),
21105 },
21106 )
21107 })
21108 .collect::<Vec<_>>();
21109
21110 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
21111
21112 assert_eq!(expected, breakpoint);
21113 }
21114}
21115
21116fn add_log_breakpoint_at_cursor(
21117 editor: &mut Editor,
21118 log_message: &str,
21119 window: &mut Window,
21120 cx: &mut Context<Editor>,
21121) {
21122 let (anchor, bp) = editor
21123 .breakpoints_at_cursors(window, cx)
21124 .first()
21125 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
21126 .unwrap_or_else(|| {
21127 let cursor_position: Point = editor.selections.newest(cx).head();
21128
21129 let breakpoint_position = editor
21130 .snapshot(window, cx)
21131 .display_snapshot
21132 .buffer_snapshot
21133 .anchor_before(Point::new(cursor_position.row, 0));
21134
21135 (breakpoint_position, Breakpoint::new_log(log_message))
21136 });
21137
21138 editor.edit_breakpoint_at_anchor(
21139 anchor,
21140 bp,
21141 BreakpointEditAction::EditLogMessage(log_message.into()),
21142 cx,
21143 );
21144}
21145
21146#[gpui::test]
21147async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
21148 init_test(cx, |_| {});
21149
21150 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
21151 let fs = FakeFs::new(cx.executor());
21152 fs.insert_tree(
21153 path!("/a"),
21154 json!({
21155 "main.rs": sample_text,
21156 }),
21157 )
21158 .await;
21159 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21160 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21161 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21162
21163 let fs = FakeFs::new(cx.executor());
21164 fs.insert_tree(
21165 path!("/a"),
21166 json!({
21167 "main.rs": sample_text,
21168 }),
21169 )
21170 .await;
21171 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21172 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21173 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21174 let worktree_id = workspace
21175 .update(cx, |workspace, _window, cx| {
21176 workspace.project().update(cx, |project, cx| {
21177 project.worktrees(cx).next().unwrap().read(cx).id()
21178 })
21179 })
21180 .unwrap();
21181
21182 let buffer = project
21183 .update(cx, |project, cx| {
21184 project.open_buffer((worktree_id, "main.rs"), cx)
21185 })
21186 .await
21187 .unwrap();
21188
21189 let (editor, cx) = cx.add_window_view(|window, cx| {
21190 Editor::new(
21191 EditorMode::full(),
21192 MultiBuffer::build_from_buffer(buffer, cx),
21193 Some(project.clone()),
21194 window,
21195 cx,
21196 )
21197 });
21198
21199 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
21200 let abs_path = project.read_with(cx, |project, cx| {
21201 project
21202 .absolute_path(&project_path, cx)
21203 .map(Arc::from)
21204 .unwrap()
21205 });
21206
21207 // assert we can add breakpoint on the first line
21208 editor.update_in(cx, |editor, window, cx| {
21209 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21210 editor.move_to_end(&MoveToEnd, window, cx);
21211 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21212 });
21213
21214 let breakpoints = editor.update(cx, |editor, cx| {
21215 editor
21216 .breakpoint_store()
21217 .as_ref()
21218 .unwrap()
21219 .read(cx)
21220 .all_source_breakpoints(cx)
21221 });
21222
21223 assert_eq!(1, breakpoints.len());
21224 assert_breakpoint(
21225 &breakpoints,
21226 &abs_path,
21227 vec![
21228 (0, Breakpoint::new_standard()),
21229 (3, Breakpoint::new_standard()),
21230 ],
21231 );
21232
21233 editor.update_in(cx, |editor, window, cx| {
21234 editor.move_to_beginning(&MoveToBeginning, window, cx);
21235 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21236 });
21237
21238 let breakpoints = editor.update(cx, |editor, cx| {
21239 editor
21240 .breakpoint_store()
21241 .as_ref()
21242 .unwrap()
21243 .read(cx)
21244 .all_source_breakpoints(cx)
21245 });
21246
21247 assert_eq!(1, breakpoints.len());
21248 assert_breakpoint(
21249 &breakpoints,
21250 &abs_path,
21251 vec![(3, Breakpoint::new_standard())],
21252 );
21253
21254 editor.update_in(cx, |editor, window, cx| {
21255 editor.move_to_end(&MoveToEnd, window, cx);
21256 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21257 });
21258
21259 let breakpoints = editor.update(cx, |editor, cx| {
21260 editor
21261 .breakpoint_store()
21262 .as_ref()
21263 .unwrap()
21264 .read(cx)
21265 .all_source_breakpoints(cx)
21266 });
21267
21268 assert_eq!(0, breakpoints.len());
21269 assert_breakpoint(&breakpoints, &abs_path, vec![]);
21270}
21271
21272#[gpui::test]
21273async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
21274 init_test(cx, |_| {});
21275
21276 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
21277
21278 let fs = FakeFs::new(cx.executor());
21279 fs.insert_tree(
21280 path!("/a"),
21281 json!({
21282 "main.rs": sample_text,
21283 }),
21284 )
21285 .await;
21286 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21287 let (workspace, cx) =
21288 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21289
21290 let worktree_id = workspace.update(cx, |workspace, cx| {
21291 workspace.project().update(cx, |project, cx| {
21292 project.worktrees(cx).next().unwrap().read(cx).id()
21293 })
21294 });
21295
21296 let buffer = project
21297 .update(cx, |project, cx| {
21298 project.open_buffer((worktree_id, "main.rs"), cx)
21299 })
21300 .await
21301 .unwrap();
21302
21303 let (editor, cx) = cx.add_window_view(|window, cx| {
21304 Editor::new(
21305 EditorMode::full(),
21306 MultiBuffer::build_from_buffer(buffer, cx),
21307 Some(project.clone()),
21308 window,
21309 cx,
21310 )
21311 });
21312
21313 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
21314 let abs_path = project.read_with(cx, |project, cx| {
21315 project
21316 .absolute_path(&project_path, cx)
21317 .map(Arc::from)
21318 .unwrap()
21319 });
21320
21321 editor.update_in(cx, |editor, window, cx| {
21322 add_log_breakpoint_at_cursor(editor, "hello world", 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 });
21333
21334 assert_breakpoint(
21335 &breakpoints,
21336 &abs_path,
21337 vec![(0, Breakpoint::new_log("hello world"))],
21338 );
21339
21340 // Removing a log message from a log breakpoint should remove it
21341 editor.update_in(cx, |editor, window, cx| {
21342 add_log_breakpoint_at_cursor(editor, "", window, cx);
21343 });
21344
21345 let breakpoints = editor.update(cx, |editor, cx| {
21346 editor
21347 .breakpoint_store()
21348 .as_ref()
21349 .unwrap()
21350 .read(cx)
21351 .all_source_breakpoints(cx)
21352 });
21353
21354 assert_breakpoint(&breakpoints, &abs_path, vec![]);
21355
21356 editor.update_in(cx, |editor, window, cx| {
21357 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21358 editor.move_to_end(&MoveToEnd, window, cx);
21359 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21360 // Not adding a log message to a standard breakpoint shouldn't remove it
21361 add_log_breakpoint_at_cursor(editor, "", window, cx);
21362 });
21363
21364 let breakpoints = editor.update(cx, |editor, cx| {
21365 editor
21366 .breakpoint_store()
21367 .as_ref()
21368 .unwrap()
21369 .read(cx)
21370 .all_source_breakpoints(cx)
21371 });
21372
21373 assert_breakpoint(
21374 &breakpoints,
21375 &abs_path,
21376 vec![
21377 (0, Breakpoint::new_standard()),
21378 (3, Breakpoint::new_standard()),
21379 ],
21380 );
21381
21382 editor.update_in(cx, |editor, window, cx| {
21383 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
21384 });
21385
21386 let breakpoints = editor.update(cx, |editor, cx| {
21387 editor
21388 .breakpoint_store()
21389 .as_ref()
21390 .unwrap()
21391 .read(cx)
21392 .all_source_breakpoints(cx)
21393 });
21394
21395 assert_breakpoint(
21396 &breakpoints,
21397 &abs_path,
21398 vec![
21399 (0, Breakpoint::new_standard()),
21400 (3, Breakpoint::new_log("hello world")),
21401 ],
21402 );
21403
21404 editor.update_in(cx, |editor, window, cx| {
21405 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
21406 });
21407
21408 let breakpoints = editor.update(cx, |editor, cx| {
21409 editor
21410 .breakpoint_store()
21411 .as_ref()
21412 .unwrap()
21413 .read(cx)
21414 .all_source_breakpoints(cx)
21415 });
21416
21417 assert_breakpoint(
21418 &breakpoints,
21419 &abs_path,
21420 vec![
21421 (0, Breakpoint::new_standard()),
21422 (3, Breakpoint::new_log("hello Earth!!")),
21423 ],
21424 );
21425}
21426
21427/// This also tests that Editor::breakpoint_at_cursor_head is working properly
21428/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
21429/// or when breakpoints were placed out of order. This tests for a regression too
21430#[gpui::test]
21431async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
21432 init_test(cx, |_| {});
21433
21434 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
21435 let fs = FakeFs::new(cx.executor());
21436 fs.insert_tree(
21437 path!("/a"),
21438 json!({
21439 "main.rs": sample_text,
21440 }),
21441 )
21442 .await;
21443 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21444 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21445 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21446
21447 let fs = FakeFs::new(cx.executor());
21448 fs.insert_tree(
21449 path!("/a"),
21450 json!({
21451 "main.rs": sample_text,
21452 }),
21453 )
21454 .await;
21455 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21456 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21457 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21458 let worktree_id = workspace
21459 .update(cx, |workspace, _window, cx| {
21460 workspace.project().update(cx, |project, cx| {
21461 project.worktrees(cx).next().unwrap().read(cx).id()
21462 })
21463 })
21464 .unwrap();
21465
21466 let buffer = project
21467 .update(cx, |project, cx| {
21468 project.open_buffer((worktree_id, "main.rs"), cx)
21469 })
21470 .await
21471 .unwrap();
21472
21473 let (editor, cx) = cx.add_window_view(|window, cx| {
21474 Editor::new(
21475 EditorMode::full(),
21476 MultiBuffer::build_from_buffer(buffer, cx),
21477 Some(project.clone()),
21478 window,
21479 cx,
21480 )
21481 });
21482
21483 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
21484 let abs_path = project.read_with(cx, |project, cx| {
21485 project
21486 .absolute_path(&project_path, cx)
21487 .map(Arc::from)
21488 .unwrap()
21489 });
21490
21491 // assert we can add breakpoint on the first line
21492 editor.update_in(cx, |editor, window, cx| {
21493 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21494 editor.move_to_end(&MoveToEnd, window, cx);
21495 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21496 editor.move_up(&MoveUp, window, cx);
21497 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21498 });
21499
21500 let breakpoints = editor.update(cx, |editor, cx| {
21501 editor
21502 .breakpoint_store()
21503 .as_ref()
21504 .unwrap()
21505 .read(cx)
21506 .all_source_breakpoints(cx)
21507 });
21508
21509 assert_eq!(1, breakpoints.len());
21510 assert_breakpoint(
21511 &breakpoints,
21512 &abs_path,
21513 vec![
21514 (0, Breakpoint::new_standard()),
21515 (2, Breakpoint::new_standard()),
21516 (3, Breakpoint::new_standard()),
21517 ],
21518 );
21519
21520 editor.update_in(cx, |editor, window, cx| {
21521 editor.move_to_beginning(&MoveToBeginning, window, cx);
21522 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21523 editor.move_to_end(&MoveToEnd, window, cx);
21524 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21525 // Disabling a breakpoint that doesn't exist should do nothing
21526 editor.move_up(&MoveUp, window, cx);
21527 editor.move_up(&MoveUp, window, cx);
21528 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21529 });
21530
21531 let breakpoints = editor.update(cx, |editor, cx| {
21532 editor
21533 .breakpoint_store()
21534 .as_ref()
21535 .unwrap()
21536 .read(cx)
21537 .all_source_breakpoints(cx)
21538 });
21539
21540 let disable_breakpoint = {
21541 let mut bp = Breakpoint::new_standard();
21542 bp.state = BreakpointState::Disabled;
21543 bp
21544 };
21545
21546 assert_eq!(1, breakpoints.len());
21547 assert_breakpoint(
21548 &breakpoints,
21549 &abs_path,
21550 vec![
21551 (0, disable_breakpoint.clone()),
21552 (2, Breakpoint::new_standard()),
21553 (3, disable_breakpoint.clone()),
21554 ],
21555 );
21556
21557 editor.update_in(cx, |editor, window, cx| {
21558 editor.move_to_beginning(&MoveToBeginning, window, cx);
21559 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
21560 editor.move_to_end(&MoveToEnd, window, cx);
21561 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
21562 editor.move_up(&MoveUp, window, cx);
21563 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21564 });
21565
21566 let breakpoints = editor.update(cx, |editor, cx| {
21567 editor
21568 .breakpoint_store()
21569 .as_ref()
21570 .unwrap()
21571 .read(cx)
21572 .all_source_breakpoints(cx)
21573 });
21574
21575 assert_eq!(1, breakpoints.len());
21576 assert_breakpoint(
21577 &breakpoints,
21578 &abs_path,
21579 vec![
21580 (0, Breakpoint::new_standard()),
21581 (2, disable_breakpoint),
21582 (3, Breakpoint::new_standard()),
21583 ],
21584 );
21585}
21586
21587#[gpui::test]
21588async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
21589 init_test(cx, |_| {});
21590 let capabilities = lsp::ServerCapabilities {
21591 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
21592 prepare_provider: Some(true),
21593 work_done_progress_options: Default::default(),
21594 })),
21595 ..Default::default()
21596 };
21597 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21598
21599 cx.set_state(indoc! {"
21600 struct Fˇoo {}
21601 "});
21602
21603 cx.update_editor(|editor, _, cx| {
21604 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21605 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21606 editor.highlight_background::<DocumentHighlightRead>(
21607 &[highlight_range],
21608 |theme| theme.colors().editor_document_highlight_read_background,
21609 cx,
21610 );
21611 });
21612
21613 let mut prepare_rename_handler = cx
21614 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
21615 move |_, _, _| async move {
21616 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
21617 start: lsp::Position {
21618 line: 0,
21619 character: 7,
21620 },
21621 end: lsp::Position {
21622 line: 0,
21623 character: 10,
21624 },
21625 })))
21626 },
21627 );
21628 let prepare_rename_task = cx
21629 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21630 .expect("Prepare rename was not started");
21631 prepare_rename_handler.next().await.unwrap();
21632 prepare_rename_task.await.expect("Prepare rename failed");
21633
21634 let mut rename_handler =
21635 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21636 let edit = lsp::TextEdit {
21637 range: lsp::Range {
21638 start: lsp::Position {
21639 line: 0,
21640 character: 7,
21641 },
21642 end: lsp::Position {
21643 line: 0,
21644 character: 10,
21645 },
21646 },
21647 new_text: "FooRenamed".to_string(),
21648 };
21649 Ok(Some(lsp::WorkspaceEdit::new(
21650 // Specify the same edit twice
21651 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
21652 )))
21653 });
21654 let rename_task = cx
21655 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21656 .expect("Confirm rename was not started");
21657 rename_handler.next().await.unwrap();
21658 rename_task.await.expect("Confirm rename failed");
21659 cx.run_until_parked();
21660
21661 // Despite two edits, only one is actually applied as those are identical
21662 cx.assert_editor_state(indoc! {"
21663 struct FooRenamedˇ {}
21664 "});
21665}
21666
21667#[gpui::test]
21668async fn test_rename_without_prepare(cx: &mut TestAppContext) {
21669 init_test(cx, |_| {});
21670 // These capabilities indicate that the server does not support prepare rename.
21671 let capabilities = lsp::ServerCapabilities {
21672 rename_provider: Some(lsp::OneOf::Left(true)),
21673 ..Default::default()
21674 };
21675 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21676
21677 cx.set_state(indoc! {"
21678 struct Fˇoo {}
21679 "});
21680
21681 cx.update_editor(|editor, _window, cx| {
21682 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21683 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21684 editor.highlight_background::<DocumentHighlightRead>(
21685 &[highlight_range],
21686 |theme| theme.colors().editor_document_highlight_read_background,
21687 cx,
21688 );
21689 });
21690
21691 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21692 .expect("Prepare rename was not started")
21693 .await
21694 .expect("Prepare rename failed");
21695
21696 let mut rename_handler =
21697 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21698 let edit = lsp::TextEdit {
21699 range: lsp::Range {
21700 start: lsp::Position {
21701 line: 0,
21702 character: 7,
21703 },
21704 end: lsp::Position {
21705 line: 0,
21706 character: 10,
21707 },
21708 },
21709 new_text: "FooRenamed".to_string(),
21710 };
21711 Ok(Some(lsp::WorkspaceEdit::new(
21712 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
21713 )))
21714 });
21715 let rename_task = cx
21716 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21717 .expect("Confirm rename was not started");
21718 rename_handler.next().await.unwrap();
21719 rename_task.await.expect("Confirm rename failed");
21720 cx.run_until_parked();
21721
21722 // Correct range is renamed, as `surrounding_word` is used to find it.
21723 cx.assert_editor_state(indoc! {"
21724 struct FooRenamedˇ {}
21725 "});
21726}
21727
21728#[gpui::test]
21729async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
21730 init_test(cx, |_| {});
21731 let mut cx = EditorTestContext::new(cx).await;
21732
21733 let language = Arc::new(
21734 Language::new(
21735 LanguageConfig::default(),
21736 Some(tree_sitter_html::LANGUAGE.into()),
21737 )
21738 .with_brackets_query(
21739 r#"
21740 ("<" @open "/>" @close)
21741 ("</" @open ">" @close)
21742 ("<" @open ">" @close)
21743 ("\"" @open "\"" @close)
21744 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
21745 "#,
21746 )
21747 .unwrap(),
21748 );
21749 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21750
21751 cx.set_state(indoc! {"
21752 <span>ˇ</span>
21753 "});
21754 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21755 cx.assert_editor_state(indoc! {"
21756 <span>
21757 ˇ
21758 </span>
21759 "});
21760
21761 cx.set_state(indoc! {"
21762 <span><span></span>ˇ</span>
21763 "});
21764 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21765 cx.assert_editor_state(indoc! {"
21766 <span><span></span>
21767 ˇ</span>
21768 "});
21769
21770 cx.set_state(indoc! {"
21771 <span>ˇ
21772 </span>
21773 "});
21774 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21775 cx.assert_editor_state(indoc! {"
21776 <span>
21777 ˇ
21778 </span>
21779 "});
21780}
21781
21782#[gpui::test(iterations = 10)]
21783async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
21784 init_test(cx, |_| {});
21785
21786 let fs = FakeFs::new(cx.executor());
21787 fs.insert_tree(
21788 path!("/dir"),
21789 json!({
21790 "a.ts": "a",
21791 }),
21792 )
21793 .await;
21794
21795 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
21796 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21797 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21798
21799 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21800 language_registry.add(Arc::new(Language::new(
21801 LanguageConfig {
21802 name: "TypeScript".into(),
21803 matcher: LanguageMatcher {
21804 path_suffixes: vec!["ts".to_string()],
21805 ..Default::default()
21806 },
21807 ..Default::default()
21808 },
21809 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
21810 )));
21811 let mut fake_language_servers = language_registry.register_fake_lsp(
21812 "TypeScript",
21813 FakeLspAdapter {
21814 capabilities: lsp::ServerCapabilities {
21815 code_lens_provider: Some(lsp::CodeLensOptions {
21816 resolve_provider: Some(true),
21817 }),
21818 execute_command_provider: Some(lsp::ExecuteCommandOptions {
21819 commands: vec!["_the/command".to_string()],
21820 ..lsp::ExecuteCommandOptions::default()
21821 }),
21822 ..lsp::ServerCapabilities::default()
21823 },
21824 ..FakeLspAdapter::default()
21825 },
21826 );
21827
21828 let editor = workspace
21829 .update(cx, |workspace, window, cx| {
21830 workspace.open_abs_path(
21831 PathBuf::from(path!("/dir/a.ts")),
21832 OpenOptions::default(),
21833 window,
21834 cx,
21835 )
21836 })
21837 .unwrap()
21838 .await
21839 .unwrap()
21840 .downcast::<Editor>()
21841 .unwrap();
21842 cx.executor().run_until_parked();
21843
21844 let fake_server = fake_language_servers.next().await.unwrap();
21845
21846 let buffer = editor.update(cx, |editor, cx| {
21847 editor
21848 .buffer()
21849 .read(cx)
21850 .as_singleton()
21851 .expect("have opened a single file by path")
21852 });
21853
21854 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
21855 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
21856 drop(buffer_snapshot);
21857 let actions = cx
21858 .update_window(*workspace, |_, window, cx| {
21859 project.code_actions(&buffer, anchor..anchor, window, cx)
21860 })
21861 .unwrap();
21862
21863 fake_server
21864 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21865 Ok(Some(vec![
21866 lsp::CodeLens {
21867 range: lsp::Range::default(),
21868 command: Some(lsp::Command {
21869 title: "Code lens command".to_owned(),
21870 command: "_the/command".to_owned(),
21871 arguments: None,
21872 }),
21873 data: None,
21874 },
21875 lsp::CodeLens {
21876 range: lsp::Range::default(),
21877 command: Some(lsp::Command {
21878 title: "Command not in capabilities".to_owned(),
21879 command: "not in capabilities".to_owned(),
21880 arguments: None,
21881 }),
21882 data: None,
21883 },
21884 lsp::CodeLens {
21885 range: lsp::Range {
21886 start: lsp::Position {
21887 line: 1,
21888 character: 1,
21889 },
21890 end: lsp::Position {
21891 line: 1,
21892 character: 1,
21893 },
21894 },
21895 command: Some(lsp::Command {
21896 title: "Command not in range".to_owned(),
21897 command: "_the/command".to_owned(),
21898 arguments: None,
21899 }),
21900 data: None,
21901 },
21902 ]))
21903 })
21904 .next()
21905 .await;
21906
21907 let actions = actions.await.unwrap();
21908 assert_eq!(
21909 actions.len(),
21910 1,
21911 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
21912 );
21913 let action = actions[0].clone();
21914 let apply = project.update(cx, |project, cx| {
21915 project.apply_code_action(buffer.clone(), action, true, cx)
21916 });
21917
21918 // Resolving the code action does not populate its edits. In absence of
21919 // edits, we must execute the given command.
21920 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
21921 |mut lens, _| async move {
21922 let lens_command = lens.command.as_mut().expect("should have a command");
21923 assert_eq!(lens_command.title, "Code lens command");
21924 lens_command.arguments = Some(vec![json!("the-argument")]);
21925 Ok(lens)
21926 },
21927 );
21928
21929 // While executing the command, the language server sends the editor
21930 // a `workspaceEdit` request.
21931 fake_server
21932 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
21933 let fake = fake_server.clone();
21934 move |params, _| {
21935 assert_eq!(params.command, "_the/command");
21936 let fake = fake.clone();
21937 async move {
21938 fake.server
21939 .request::<lsp::request::ApplyWorkspaceEdit>(
21940 lsp::ApplyWorkspaceEditParams {
21941 label: None,
21942 edit: lsp::WorkspaceEdit {
21943 changes: Some(
21944 [(
21945 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
21946 vec![lsp::TextEdit {
21947 range: lsp::Range::new(
21948 lsp::Position::new(0, 0),
21949 lsp::Position::new(0, 0),
21950 ),
21951 new_text: "X".into(),
21952 }],
21953 )]
21954 .into_iter()
21955 .collect(),
21956 ),
21957 ..lsp::WorkspaceEdit::default()
21958 },
21959 },
21960 )
21961 .await
21962 .into_response()
21963 .unwrap();
21964 Ok(Some(json!(null)))
21965 }
21966 }
21967 })
21968 .next()
21969 .await;
21970
21971 // Applying the code lens command returns a project transaction containing the edits
21972 // sent by the language server in its `workspaceEdit` request.
21973 let transaction = apply.await.unwrap();
21974 assert!(transaction.0.contains_key(&buffer));
21975 buffer.update(cx, |buffer, cx| {
21976 assert_eq!(buffer.text(), "Xa");
21977 buffer.undo(cx);
21978 assert_eq!(buffer.text(), "a");
21979 });
21980
21981 let actions_after_edits = cx
21982 .update_window(*workspace, |_, window, cx| {
21983 project.code_actions(&buffer, anchor..anchor, window, cx)
21984 })
21985 .unwrap()
21986 .await
21987 .unwrap();
21988 assert_eq!(
21989 actions, actions_after_edits,
21990 "For the same selection, same code lens actions should be returned"
21991 );
21992
21993 let _responses =
21994 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21995 panic!("No more code lens requests are expected");
21996 });
21997 editor.update_in(cx, |editor, window, cx| {
21998 editor.select_all(&SelectAll, window, cx);
21999 });
22000 cx.executor().run_until_parked();
22001 let new_actions = cx
22002 .update_window(*workspace, |_, window, cx| {
22003 project.code_actions(&buffer, anchor..anchor, window, cx)
22004 })
22005 .unwrap()
22006 .await
22007 .unwrap();
22008 assert_eq!(
22009 actions, new_actions,
22010 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
22011 );
22012}
22013
22014#[gpui::test]
22015async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
22016 init_test(cx, |_| {});
22017
22018 let fs = FakeFs::new(cx.executor());
22019 let main_text = r#"fn main() {
22020println!("1");
22021println!("2");
22022println!("3");
22023println!("4");
22024println!("5");
22025}"#;
22026 let lib_text = "mod foo {}";
22027 fs.insert_tree(
22028 path!("/a"),
22029 json!({
22030 "lib.rs": lib_text,
22031 "main.rs": main_text,
22032 }),
22033 )
22034 .await;
22035
22036 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22037 let (workspace, cx) =
22038 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22039 let worktree_id = workspace.update(cx, |workspace, cx| {
22040 workspace.project().update(cx, |project, cx| {
22041 project.worktrees(cx).next().unwrap().read(cx).id()
22042 })
22043 });
22044
22045 let expected_ranges = vec![
22046 Point::new(0, 0)..Point::new(0, 0),
22047 Point::new(1, 0)..Point::new(1, 1),
22048 Point::new(2, 0)..Point::new(2, 2),
22049 Point::new(3, 0)..Point::new(3, 3),
22050 ];
22051
22052 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22053 let editor_1 = workspace
22054 .update_in(cx, |workspace, window, cx| {
22055 workspace.open_path(
22056 (worktree_id, "main.rs"),
22057 Some(pane_1.downgrade()),
22058 true,
22059 window,
22060 cx,
22061 )
22062 })
22063 .unwrap()
22064 .await
22065 .downcast::<Editor>()
22066 .unwrap();
22067 pane_1.update(cx, |pane, cx| {
22068 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22069 open_editor.update(cx, |editor, cx| {
22070 assert_eq!(
22071 editor.display_text(cx),
22072 main_text,
22073 "Original main.rs text on initial open",
22074 );
22075 assert_eq!(
22076 editor
22077 .selections
22078 .all::<Point>(cx)
22079 .into_iter()
22080 .map(|s| s.range())
22081 .collect::<Vec<_>>(),
22082 vec![Point::zero()..Point::zero()],
22083 "Default selections on initial open",
22084 );
22085 })
22086 });
22087 editor_1.update_in(cx, |editor, window, cx| {
22088 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22089 s.select_ranges(expected_ranges.clone());
22090 });
22091 });
22092
22093 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
22094 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
22095 });
22096 let editor_2 = workspace
22097 .update_in(cx, |workspace, window, cx| {
22098 workspace.open_path(
22099 (worktree_id, "main.rs"),
22100 Some(pane_2.downgrade()),
22101 true,
22102 window,
22103 cx,
22104 )
22105 })
22106 .unwrap()
22107 .await
22108 .downcast::<Editor>()
22109 .unwrap();
22110 pane_2.update(cx, |pane, cx| {
22111 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22112 open_editor.update(cx, |editor, cx| {
22113 assert_eq!(
22114 editor.display_text(cx),
22115 main_text,
22116 "Original main.rs text on initial open in another panel",
22117 );
22118 assert_eq!(
22119 editor
22120 .selections
22121 .all::<Point>(cx)
22122 .into_iter()
22123 .map(|s| s.range())
22124 .collect::<Vec<_>>(),
22125 vec![Point::zero()..Point::zero()],
22126 "Default selections on initial open in another panel",
22127 );
22128 })
22129 });
22130
22131 editor_2.update_in(cx, |editor, window, cx| {
22132 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
22133 });
22134
22135 let _other_editor_1 = workspace
22136 .update_in(cx, |workspace, window, cx| {
22137 workspace.open_path(
22138 (worktree_id, "lib.rs"),
22139 Some(pane_1.downgrade()),
22140 true,
22141 window,
22142 cx,
22143 )
22144 })
22145 .unwrap()
22146 .await
22147 .downcast::<Editor>()
22148 .unwrap();
22149 pane_1
22150 .update_in(cx, |pane, window, cx| {
22151 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
22152 })
22153 .await
22154 .unwrap();
22155 drop(editor_1);
22156 pane_1.update(cx, |pane, cx| {
22157 pane.active_item()
22158 .unwrap()
22159 .downcast::<Editor>()
22160 .unwrap()
22161 .update(cx, |editor, cx| {
22162 assert_eq!(
22163 editor.display_text(cx),
22164 lib_text,
22165 "Other file should be open and active",
22166 );
22167 });
22168 assert_eq!(pane.items().count(), 1, "No other editors should be open");
22169 });
22170
22171 let _other_editor_2 = workspace
22172 .update_in(cx, |workspace, window, cx| {
22173 workspace.open_path(
22174 (worktree_id, "lib.rs"),
22175 Some(pane_2.downgrade()),
22176 true,
22177 window,
22178 cx,
22179 )
22180 })
22181 .unwrap()
22182 .await
22183 .downcast::<Editor>()
22184 .unwrap();
22185 pane_2
22186 .update_in(cx, |pane, window, cx| {
22187 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
22188 })
22189 .await
22190 .unwrap();
22191 drop(editor_2);
22192 pane_2.update(cx, |pane, cx| {
22193 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22194 open_editor.update(cx, |editor, cx| {
22195 assert_eq!(
22196 editor.display_text(cx),
22197 lib_text,
22198 "Other file should be open and active in another panel too",
22199 );
22200 });
22201 assert_eq!(
22202 pane.items().count(),
22203 1,
22204 "No other editors should be open in another pane",
22205 );
22206 });
22207
22208 let _editor_1_reopened = workspace
22209 .update_in(cx, |workspace, window, cx| {
22210 workspace.open_path(
22211 (worktree_id, "main.rs"),
22212 Some(pane_1.downgrade()),
22213 true,
22214 window,
22215 cx,
22216 )
22217 })
22218 .unwrap()
22219 .await
22220 .downcast::<Editor>()
22221 .unwrap();
22222 let _editor_2_reopened = workspace
22223 .update_in(cx, |workspace, window, cx| {
22224 workspace.open_path(
22225 (worktree_id, "main.rs"),
22226 Some(pane_2.downgrade()),
22227 true,
22228 window,
22229 cx,
22230 )
22231 })
22232 .unwrap()
22233 .await
22234 .downcast::<Editor>()
22235 .unwrap();
22236 pane_1.update(cx, |pane, cx| {
22237 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22238 open_editor.update(cx, |editor, cx| {
22239 assert_eq!(
22240 editor.display_text(cx),
22241 main_text,
22242 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
22243 );
22244 assert_eq!(
22245 editor
22246 .selections
22247 .all::<Point>(cx)
22248 .into_iter()
22249 .map(|s| s.range())
22250 .collect::<Vec<_>>(),
22251 expected_ranges,
22252 "Previous editor in the 1st panel had selections and should get them restored on reopen",
22253 );
22254 })
22255 });
22256 pane_2.update(cx, |pane, cx| {
22257 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22258 open_editor.update(cx, |editor, cx| {
22259 assert_eq!(
22260 editor.display_text(cx),
22261 r#"fn main() {
22262⋯rintln!("1");
22263⋯intln!("2");
22264⋯ntln!("3");
22265println!("4");
22266println!("5");
22267}"#,
22268 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
22269 );
22270 assert_eq!(
22271 editor
22272 .selections
22273 .all::<Point>(cx)
22274 .into_iter()
22275 .map(|s| s.range())
22276 .collect::<Vec<_>>(),
22277 vec![Point::zero()..Point::zero()],
22278 "Previous editor in the 2nd pane had no selections changed hence should restore none",
22279 );
22280 })
22281 });
22282}
22283
22284#[gpui::test]
22285async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
22286 init_test(cx, |_| {});
22287
22288 let fs = FakeFs::new(cx.executor());
22289 let main_text = r#"fn main() {
22290println!("1");
22291println!("2");
22292println!("3");
22293println!("4");
22294println!("5");
22295}"#;
22296 let lib_text = "mod foo {}";
22297 fs.insert_tree(
22298 path!("/a"),
22299 json!({
22300 "lib.rs": lib_text,
22301 "main.rs": main_text,
22302 }),
22303 )
22304 .await;
22305
22306 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22307 let (workspace, cx) =
22308 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22309 let worktree_id = workspace.update(cx, |workspace, cx| {
22310 workspace.project().update(cx, |project, cx| {
22311 project.worktrees(cx).next().unwrap().read(cx).id()
22312 })
22313 });
22314
22315 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22316 let editor = workspace
22317 .update_in(cx, |workspace, window, cx| {
22318 workspace.open_path(
22319 (worktree_id, "main.rs"),
22320 Some(pane.downgrade()),
22321 true,
22322 window,
22323 cx,
22324 )
22325 })
22326 .unwrap()
22327 .await
22328 .downcast::<Editor>()
22329 .unwrap();
22330 pane.update(cx, |pane, cx| {
22331 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22332 open_editor.update(cx, |editor, cx| {
22333 assert_eq!(
22334 editor.display_text(cx),
22335 main_text,
22336 "Original main.rs text on initial open",
22337 );
22338 })
22339 });
22340 editor.update_in(cx, |editor, window, cx| {
22341 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
22342 });
22343
22344 cx.update_global(|store: &mut SettingsStore, cx| {
22345 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
22346 s.restore_on_file_reopen = Some(false);
22347 });
22348 });
22349 editor.update_in(cx, |editor, window, cx| {
22350 editor.fold_ranges(
22351 vec![
22352 Point::new(1, 0)..Point::new(1, 1),
22353 Point::new(2, 0)..Point::new(2, 2),
22354 Point::new(3, 0)..Point::new(3, 3),
22355 ],
22356 false,
22357 window,
22358 cx,
22359 );
22360 });
22361 pane.update_in(cx, |pane, window, cx| {
22362 pane.close_all_items(&CloseAllItems::default(), window, cx)
22363 })
22364 .await
22365 .unwrap();
22366 pane.update(cx, |pane, _| {
22367 assert!(pane.active_item().is_none());
22368 });
22369 cx.update_global(|store: &mut SettingsStore, cx| {
22370 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
22371 s.restore_on_file_reopen = Some(true);
22372 });
22373 });
22374
22375 let _editor_reopened = workspace
22376 .update_in(cx, |workspace, window, cx| {
22377 workspace.open_path(
22378 (worktree_id, "main.rs"),
22379 Some(pane.downgrade()),
22380 true,
22381 window,
22382 cx,
22383 )
22384 })
22385 .unwrap()
22386 .await
22387 .downcast::<Editor>()
22388 .unwrap();
22389 pane.update(cx, |pane, cx| {
22390 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22391 open_editor.update(cx, |editor, cx| {
22392 assert_eq!(
22393 editor.display_text(cx),
22394 main_text,
22395 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
22396 );
22397 })
22398 });
22399}
22400
22401#[gpui::test]
22402async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
22403 struct EmptyModalView {
22404 focus_handle: gpui::FocusHandle,
22405 }
22406 impl EventEmitter<DismissEvent> for EmptyModalView {}
22407 impl Render for EmptyModalView {
22408 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
22409 div()
22410 }
22411 }
22412 impl Focusable for EmptyModalView {
22413 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
22414 self.focus_handle.clone()
22415 }
22416 }
22417 impl workspace::ModalView for EmptyModalView {}
22418 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
22419 EmptyModalView {
22420 focus_handle: cx.focus_handle(),
22421 }
22422 }
22423
22424 init_test(cx, |_| {});
22425
22426 let fs = FakeFs::new(cx.executor());
22427 let project = Project::test(fs, [], cx).await;
22428 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22429 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
22430 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22431 let editor = cx.new_window_entity(|window, cx| {
22432 Editor::new(
22433 EditorMode::full(),
22434 buffer,
22435 Some(project.clone()),
22436 window,
22437 cx,
22438 )
22439 });
22440 workspace
22441 .update(cx, |workspace, window, cx| {
22442 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
22443 })
22444 .unwrap();
22445 editor.update_in(cx, |editor, window, cx| {
22446 editor.open_context_menu(&OpenContextMenu, window, cx);
22447 assert!(editor.mouse_context_menu.is_some());
22448 });
22449 workspace
22450 .update(cx, |workspace, window, cx| {
22451 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
22452 })
22453 .unwrap();
22454 cx.read(|cx| {
22455 assert!(editor.read(cx).mouse_context_menu.is_none());
22456 });
22457}
22458
22459#[gpui::test]
22460async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
22461 init_test(cx, |_| {});
22462
22463 let fs = FakeFs::new(cx.executor());
22464 fs.insert_file(path!("/file.html"), Default::default())
22465 .await;
22466
22467 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
22468
22469 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22470 let html_language = Arc::new(Language::new(
22471 LanguageConfig {
22472 name: "HTML".into(),
22473 matcher: LanguageMatcher {
22474 path_suffixes: vec!["html".to_string()],
22475 ..LanguageMatcher::default()
22476 },
22477 brackets: BracketPairConfig {
22478 pairs: vec![BracketPair {
22479 start: "<".into(),
22480 end: ">".into(),
22481 close: true,
22482 ..Default::default()
22483 }],
22484 ..Default::default()
22485 },
22486 ..Default::default()
22487 },
22488 Some(tree_sitter_html::LANGUAGE.into()),
22489 ));
22490 language_registry.add(html_language);
22491 let mut fake_servers = language_registry.register_fake_lsp(
22492 "HTML",
22493 FakeLspAdapter {
22494 capabilities: lsp::ServerCapabilities {
22495 completion_provider: Some(lsp::CompletionOptions {
22496 resolve_provider: Some(true),
22497 ..Default::default()
22498 }),
22499 ..Default::default()
22500 },
22501 ..Default::default()
22502 },
22503 );
22504
22505 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22506 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22507
22508 let worktree_id = workspace
22509 .update(cx, |workspace, _window, cx| {
22510 workspace.project().update(cx, |project, cx| {
22511 project.worktrees(cx).next().unwrap().read(cx).id()
22512 })
22513 })
22514 .unwrap();
22515 project
22516 .update(cx, |project, cx| {
22517 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
22518 })
22519 .await
22520 .unwrap();
22521 let editor = workspace
22522 .update(cx, |workspace, window, cx| {
22523 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
22524 })
22525 .unwrap()
22526 .await
22527 .unwrap()
22528 .downcast::<Editor>()
22529 .unwrap();
22530
22531 let fake_server = fake_servers.next().await.unwrap();
22532 editor.update_in(cx, |editor, window, cx| {
22533 editor.set_text("<ad></ad>", window, cx);
22534 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22535 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
22536 });
22537 let Some((buffer, _)) = editor
22538 .buffer
22539 .read(cx)
22540 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
22541 else {
22542 panic!("Failed to get buffer for selection position");
22543 };
22544 let buffer = buffer.read(cx);
22545 let buffer_id = buffer.remote_id();
22546 let opening_range =
22547 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
22548 let closing_range =
22549 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
22550 let mut linked_ranges = HashMap::default();
22551 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
22552 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
22553 });
22554 let mut completion_handle =
22555 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
22556 Ok(Some(lsp::CompletionResponse::Array(vec![
22557 lsp::CompletionItem {
22558 label: "head".to_string(),
22559 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22560 lsp::InsertReplaceEdit {
22561 new_text: "head".to_string(),
22562 insert: lsp::Range::new(
22563 lsp::Position::new(0, 1),
22564 lsp::Position::new(0, 3),
22565 ),
22566 replace: lsp::Range::new(
22567 lsp::Position::new(0, 1),
22568 lsp::Position::new(0, 3),
22569 ),
22570 },
22571 )),
22572 ..Default::default()
22573 },
22574 ])))
22575 });
22576 editor.update_in(cx, |editor, window, cx| {
22577 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
22578 });
22579 cx.run_until_parked();
22580 completion_handle.next().await.unwrap();
22581 editor.update(cx, |editor, _| {
22582 assert!(
22583 editor.context_menu_visible(),
22584 "Completion menu should be visible"
22585 );
22586 });
22587 editor.update_in(cx, |editor, window, cx| {
22588 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
22589 });
22590 cx.executor().run_until_parked();
22591 editor.update(cx, |editor, cx| {
22592 assert_eq!(editor.text(cx), "<head></head>");
22593 });
22594}
22595
22596#[gpui::test]
22597async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
22598 init_test(cx, |_| {});
22599
22600 let fs = FakeFs::new(cx.executor());
22601 fs.insert_tree(
22602 path!("/root"),
22603 json!({
22604 "a": {
22605 "main.rs": "fn main() {}",
22606 },
22607 "foo": {
22608 "bar": {
22609 "external_file.rs": "pub mod external {}",
22610 }
22611 }
22612 }),
22613 )
22614 .await;
22615
22616 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
22617 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22618 language_registry.add(rust_lang());
22619 let _fake_servers = language_registry.register_fake_lsp(
22620 "Rust",
22621 FakeLspAdapter {
22622 ..FakeLspAdapter::default()
22623 },
22624 );
22625 let (workspace, cx) =
22626 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22627 let worktree_id = workspace.update(cx, |workspace, cx| {
22628 workspace.project().update(cx, |project, cx| {
22629 project.worktrees(cx).next().unwrap().read(cx).id()
22630 })
22631 });
22632
22633 let assert_language_servers_count =
22634 |expected: usize, context: &str, cx: &mut VisualTestContext| {
22635 project.update(cx, |project, cx| {
22636 let current = project
22637 .lsp_store()
22638 .read(cx)
22639 .as_local()
22640 .unwrap()
22641 .language_servers
22642 .len();
22643 assert_eq!(expected, current, "{context}");
22644 });
22645 };
22646
22647 assert_language_servers_count(
22648 0,
22649 "No servers should be running before any file is open",
22650 cx,
22651 );
22652 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22653 let main_editor = workspace
22654 .update_in(cx, |workspace, window, cx| {
22655 workspace.open_path(
22656 (worktree_id, "main.rs"),
22657 Some(pane.downgrade()),
22658 true,
22659 window,
22660 cx,
22661 )
22662 })
22663 .unwrap()
22664 .await
22665 .downcast::<Editor>()
22666 .unwrap();
22667 pane.update(cx, |pane, cx| {
22668 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22669 open_editor.update(cx, |editor, cx| {
22670 assert_eq!(
22671 editor.display_text(cx),
22672 "fn main() {}",
22673 "Original main.rs text on initial open",
22674 );
22675 });
22676 assert_eq!(open_editor, main_editor);
22677 });
22678 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
22679
22680 let external_editor = workspace
22681 .update_in(cx, |workspace, window, cx| {
22682 workspace.open_abs_path(
22683 PathBuf::from("/root/foo/bar/external_file.rs"),
22684 OpenOptions::default(),
22685 window,
22686 cx,
22687 )
22688 })
22689 .await
22690 .expect("opening external file")
22691 .downcast::<Editor>()
22692 .expect("downcasted external file's open element to editor");
22693 pane.update(cx, |pane, cx| {
22694 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22695 open_editor.update(cx, |editor, cx| {
22696 assert_eq!(
22697 editor.display_text(cx),
22698 "pub mod external {}",
22699 "External file is open now",
22700 );
22701 });
22702 assert_eq!(open_editor, external_editor);
22703 });
22704 assert_language_servers_count(
22705 1,
22706 "Second, external, *.rs file should join the existing server",
22707 cx,
22708 );
22709
22710 pane.update_in(cx, |pane, window, cx| {
22711 pane.close_active_item(&CloseActiveItem::default(), window, cx)
22712 })
22713 .await
22714 .unwrap();
22715 pane.update_in(cx, |pane, window, cx| {
22716 pane.navigate_backward(window, cx);
22717 });
22718 cx.run_until_parked();
22719 pane.update(cx, |pane, cx| {
22720 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22721 open_editor.update(cx, |editor, cx| {
22722 assert_eq!(
22723 editor.display_text(cx),
22724 "pub mod external {}",
22725 "External file is open now",
22726 );
22727 });
22728 });
22729 assert_language_servers_count(
22730 1,
22731 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
22732 cx,
22733 );
22734
22735 cx.update(|_, cx| {
22736 workspace::reload(cx);
22737 });
22738 assert_language_servers_count(
22739 1,
22740 "After reloading the worktree with local and external files opened, only one project should be started",
22741 cx,
22742 );
22743}
22744
22745#[gpui::test]
22746async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
22747 init_test(cx, |_| {});
22748
22749 let mut cx = EditorTestContext::new(cx).await;
22750 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22751 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22752
22753 // test cursor move to start of each line on tab
22754 // for `if`, `elif`, `else`, `while`, `with` and `for`
22755 cx.set_state(indoc! {"
22756 def main():
22757 ˇ for item in items:
22758 ˇ while item.active:
22759 ˇ if item.value > 10:
22760 ˇ continue
22761 ˇ elif item.value < 0:
22762 ˇ break
22763 ˇ else:
22764 ˇ with item.context() as ctx:
22765 ˇ yield count
22766 ˇ else:
22767 ˇ log('while else')
22768 ˇ else:
22769 ˇ log('for else')
22770 "});
22771 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22772 cx.assert_editor_state(indoc! {"
22773 def main():
22774 ˇfor item in items:
22775 ˇwhile item.active:
22776 ˇif item.value > 10:
22777 ˇcontinue
22778 ˇelif item.value < 0:
22779 ˇbreak
22780 ˇelse:
22781 ˇwith item.context() as ctx:
22782 ˇyield count
22783 ˇelse:
22784 ˇlog('while else')
22785 ˇelse:
22786 ˇlog('for else')
22787 "});
22788 // test relative indent is preserved when tab
22789 // for `if`, `elif`, `else`, `while`, `with` and `for`
22790 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22791 cx.assert_editor_state(indoc! {"
22792 def main():
22793 ˇfor item in items:
22794 ˇwhile item.active:
22795 ˇif item.value > 10:
22796 ˇcontinue
22797 ˇelif item.value < 0:
22798 ˇbreak
22799 ˇelse:
22800 ˇwith item.context() as ctx:
22801 ˇyield count
22802 ˇelse:
22803 ˇlog('while else')
22804 ˇelse:
22805 ˇlog('for else')
22806 "});
22807
22808 // test cursor move to start of each line on tab
22809 // for `try`, `except`, `else`, `finally`, `match` and `def`
22810 cx.set_state(indoc! {"
22811 def main():
22812 ˇ try:
22813 ˇ fetch()
22814 ˇ except ValueError:
22815 ˇ handle_error()
22816 ˇ else:
22817 ˇ match value:
22818 ˇ case _:
22819 ˇ finally:
22820 ˇ def status():
22821 ˇ return 0
22822 "});
22823 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22824 cx.assert_editor_state(indoc! {"
22825 def main():
22826 ˇtry:
22827 ˇfetch()
22828 ˇexcept ValueError:
22829 ˇhandle_error()
22830 ˇelse:
22831 ˇmatch value:
22832 ˇcase _:
22833 ˇfinally:
22834 ˇdef status():
22835 ˇreturn 0
22836 "});
22837 // test relative indent is preserved when tab
22838 // for `try`, `except`, `else`, `finally`, `match` and `def`
22839 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22840 cx.assert_editor_state(indoc! {"
22841 def main():
22842 ˇtry:
22843 ˇfetch()
22844 ˇexcept ValueError:
22845 ˇhandle_error()
22846 ˇelse:
22847 ˇmatch value:
22848 ˇcase _:
22849 ˇfinally:
22850 ˇdef status():
22851 ˇreturn 0
22852 "});
22853}
22854
22855#[gpui::test]
22856async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
22857 init_test(cx, |_| {});
22858
22859 let mut cx = EditorTestContext::new(cx).await;
22860 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22861 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22862
22863 // test `else` auto outdents when typed inside `if` block
22864 cx.set_state(indoc! {"
22865 def main():
22866 if i == 2:
22867 return
22868 ˇ
22869 "});
22870 cx.update_editor(|editor, window, cx| {
22871 editor.handle_input("else:", window, cx);
22872 });
22873 cx.assert_editor_state(indoc! {"
22874 def main():
22875 if i == 2:
22876 return
22877 else:ˇ
22878 "});
22879
22880 // test `except` auto outdents when typed inside `try` block
22881 cx.set_state(indoc! {"
22882 def main():
22883 try:
22884 i = 2
22885 ˇ
22886 "});
22887 cx.update_editor(|editor, window, cx| {
22888 editor.handle_input("except:", window, cx);
22889 });
22890 cx.assert_editor_state(indoc! {"
22891 def main():
22892 try:
22893 i = 2
22894 except:ˇ
22895 "});
22896
22897 // test `else` auto outdents when typed inside `except` block
22898 cx.set_state(indoc! {"
22899 def main():
22900 try:
22901 i = 2
22902 except:
22903 j = 2
22904 ˇ
22905 "});
22906 cx.update_editor(|editor, window, cx| {
22907 editor.handle_input("else:", window, cx);
22908 });
22909 cx.assert_editor_state(indoc! {"
22910 def main():
22911 try:
22912 i = 2
22913 except:
22914 j = 2
22915 else:ˇ
22916 "});
22917
22918 // test `finally` auto outdents when typed inside `else` block
22919 cx.set_state(indoc! {"
22920 def main():
22921 try:
22922 i = 2
22923 except:
22924 j = 2
22925 else:
22926 k = 2
22927 ˇ
22928 "});
22929 cx.update_editor(|editor, window, cx| {
22930 editor.handle_input("finally:", window, cx);
22931 });
22932 cx.assert_editor_state(indoc! {"
22933 def main():
22934 try:
22935 i = 2
22936 except:
22937 j = 2
22938 else:
22939 k = 2
22940 finally:ˇ
22941 "});
22942
22943 // test `else` does not outdents when typed inside `except` block right after for block
22944 cx.set_state(indoc! {"
22945 def main():
22946 try:
22947 i = 2
22948 except:
22949 for i in range(n):
22950 pass
22951 ˇ
22952 "});
22953 cx.update_editor(|editor, window, cx| {
22954 editor.handle_input("else:", window, cx);
22955 });
22956 cx.assert_editor_state(indoc! {"
22957 def main():
22958 try:
22959 i = 2
22960 except:
22961 for i in range(n):
22962 pass
22963 else:ˇ
22964 "});
22965
22966 // test `finally` auto outdents when typed inside `else` block right after for block
22967 cx.set_state(indoc! {"
22968 def main():
22969 try:
22970 i = 2
22971 except:
22972 j = 2
22973 else:
22974 for i in range(n):
22975 pass
22976 ˇ
22977 "});
22978 cx.update_editor(|editor, window, cx| {
22979 editor.handle_input("finally:", window, cx);
22980 });
22981 cx.assert_editor_state(indoc! {"
22982 def main():
22983 try:
22984 i = 2
22985 except:
22986 j = 2
22987 else:
22988 for i in range(n):
22989 pass
22990 finally:ˇ
22991 "});
22992
22993 // test `except` outdents to inner "try" block
22994 cx.set_state(indoc! {"
22995 def main():
22996 try:
22997 i = 2
22998 if i == 2:
22999 try:
23000 i = 3
23001 ˇ
23002 "});
23003 cx.update_editor(|editor, window, cx| {
23004 editor.handle_input("except:", window, cx);
23005 });
23006 cx.assert_editor_state(indoc! {"
23007 def main():
23008 try:
23009 i = 2
23010 if i == 2:
23011 try:
23012 i = 3
23013 except:ˇ
23014 "});
23015
23016 // test `except` outdents to outer "try" block
23017 cx.set_state(indoc! {"
23018 def main():
23019 try:
23020 i = 2
23021 if i == 2:
23022 try:
23023 i = 3
23024 ˇ
23025 "});
23026 cx.update_editor(|editor, window, cx| {
23027 editor.handle_input("except:", window, cx);
23028 });
23029 cx.assert_editor_state(indoc! {"
23030 def main():
23031 try:
23032 i = 2
23033 if i == 2:
23034 try:
23035 i = 3
23036 except:ˇ
23037 "});
23038
23039 // test `else` stays at correct indent when typed after `for` block
23040 cx.set_state(indoc! {"
23041 def main():
23042 for i in range(10):
23043 if i == 3:
23044 break
23045 ˇ
23046 "});
23047 cx.update_editor(|editor, window, cx| {
23048 editor.handle_input("else:", window, cx);
23049 });
23050 cx.assert_editor_state(indoc! {"
23051 def main():
23052 for i in range(10):
23053 if i == 3:
23054 break
23055 else:ˇ
23056 "});
23057
23058 // test does not outdent on typing after line with square brackets
23059 cx.set_state(indoc! {"
23060 def f() -> list[str]:
23061 ˇ
23062 "});
23063 cx.update_editor(|editor, window, cx| {
23064 editor.handle_input("a", window, cx);
23065 });
23066 cx.assert_editor_state(indoc! {"
23067 def f() -> list[str]:
23068 aˇ
23069 "});
23070
23071 // test does not outdent on typing : after case keyword
23072 cx.set_state(indoc! {"
23073 match 1:
23074 caseˇ
23075 "});
23076 cx.update_editor(|editor, window, cx| {
23077 editor.handle_input(":", window, cx);
23078 });
23079 cx.assert_editor_state(indoc! {"
23080 match 1:
23081 case:ˇ
23082 "});
23083}
23084
23085#[gpui::test]
23086async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
23087 init_test(cx, |_| {});
23088 update_test_language_settings(cx, |settings| {
23089 settings.defaults.extend_comment_on_newline = Some(false);
23090 });
23091 let mut cx = EditorTestContext::new(cx).await;
23092 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
23093 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23094
23095 // test correct indent after newline on comment
23096 cx.set_state(indoc! {"
23097 # COMMENT:ˇ
23098 "});
23099 cx.update_editor(|editor, window, cx| {
23100 editor.newline(&Newline, window, cx);
23101 });
23102 cx.assert_editor_state(indoc! {"
23103 # COMMENT:
23104 ˇ
23105 "});
23106
23107 // test correct indent after newline in brackets
23108 cx.set_state(indoc! {"
23109 {ˇ}
23110 "});
23111 cx.update_editor(|editor, window, cx| {
23112 editor.newline(&Newline, window, cx);
23113 });
23114 cx.run_until_parked();
23115 cx.assert_editor_state(indoc! {"
23116 {
23117 ˇ
23118 }
23119 "});
23120
23121 cx.set_state(indoc! {"
23122 (ˇ)
23123 "});
23124 cx.update_editor(|editor, window, cx| {
23125 editor.newline(&Newline, window, cx);
23126 });
23127 cx.run_until_parked();
23128 cx.assert_editor_state(indoc! {"
23129 (
23130 ˇ
23131 )
23132 "});
23133
23134 // do not indent after empty lists or dictionaries
23135 cx.set_state(indoc! {"
23136 a = []ˇ
23137 "});
23138 cx.update_editor(|editor, window, cx| {
23139 editor.newline(&Newline, window, cx);
23140 });
23141 cx.run_until_parked();
23142 cx.assert_editor_state(indoc! {"
23143 a = []
23144 ˇ
23145 "});
23146}
23147
23148#[gpui::test]
23149async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
23150 init_test(cx, |_| {});
23151
23152 let mut cx = EditorTestContext::new(cx).await;
23153 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
23154 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23155
23156 // test cursor move to start of each line on tab
23157 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
23158 cx.set_state(indoc! {"
23159 function main() {
23160 ˇ for item in $items; do
23161 ˇ while [ -n \"$item\" ]; do
23162 ˇ if [ \"$value\" -gt 10 ]; then
23163 ˇ continue
23164 ˇ elif [ \"$value\" -lt 0 ]; then
23165 ˇ break
23166 ˇ else
23167 ˇ echo \"$item\"
23168 ˇ fi
23169 ˇ done
23170 ˇ done
23171 ˇ}
23172 "});
23173 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23174 cx.assert_editor_state(indoc! {"
23175 function main() {
23176 ˇfor item in $items; do
23177 ˇwhile [ -n \"$item\" ]; do
23178 ˇif [ \"$value\" -gt 10 ]; then
23179 ˇcontinue
23180 ˇelif [ \"$value\" -lt 0 ]; then
23181 ˇbreak
23182 ˇelse
23183 ˇecho \"$item\"
23184 ˇfi
23185 ˇdone
23186 ˇdone
23187 ˇ}
23188 "});
23189 // test relative indent is preserved when tab
23190 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23191 cx.assert_editor_state(indoc! {"
23192 function main() {
23193 ˇfor item in $items; do
23194 ˇwhile [ -n \"$item\" ]; do
23195 ˇif [ \"$value\" -gt 10 ]; then
23196 ˇcontinue
23197 ˇelif [ \"$value\" -lt 0 ]; then
23198 ˇbreak
23199 ˇelse
23200 ˇecho \"$item\"
23201 ˇfi
23202 ˇdone
23203 ˇdone
23204 ˇ}
23205 "});
23206
23207 // test cursor move to start of each line on tab
23208 // for `case` statement with patterns
23209 cx.set_state(indoc! {"
23210 function handle() {
23211 ˇ case \"$1\" in
23212 ˇ start)
23213 ˇ echo \"a\"
23214 ˇ ;;
23215 ˇ stop)
23216 ˇ echo \"b\"
23217 ˇ ;;
23218 ˇ *)
23219 ˇ echo \"c\"
23220 ˇ ;;
23221 ˇ esac
23222 ˇ}
23223 "});
23224 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23225 cx.assert_editor_state(indoc! {"
23226 function handle() {
23227 ˇcase \"$1\" in
23228 ˇstart)
23229 ˇecho \"a\"
23230 ˇ;;
23231 ˇstop)
23232 ˇecho \"b\"
23233 ˇ;;
23234 ˇ*)
23235 ˇecho \"c\"
23236 ˇ;;
23237 ˇesac
23238 ˇ}
23239 "});
23240}
23241
23242#[gpui::test]
23243async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
23244 init_test(cx, |_| {});
23245
23246 let mut cx = EditorTestContext::new(cx).await;
23247 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
23248 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23249
23250 // test indents on comment insert
23251 cx.set_state(indoc! {"
23252 function main() {
23253 ˇ for item in $items; do
23254 ˇ while [ -n \"$item\" ]; do
23255 ˇ if [ \"$value\" -gt 10 ]; then
23256 ˇ continue
23257 ˇ elif [ \"$value\" -lt 0 ]; then
23258 ˇ break
23259 ˇ else
23260 ˇ echo \"$item\"
23261 ˇ fi
23262 ˇ done
23263 ˇ done
23264 ˇ}
23265 "});
23266 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
23267 cx.assert_editor_state(indoc! {"
23268 function main() {
23269 #ˇ for item in $items; do
23270 #ˇ while [ -n \"$item\" ]; do
23271 #ˇ if [ \"$value\" -gt 10 ]; then
23272 #ˇ continue
23273 #ˇ elif [ \"$value\" -lt 0 ]; then
23274 #ˇ break
23275 #ˇ else
23276 #ˇ echo \"$item\"
23277 #ˇ fi
23278 #ˇ done
23279 #ˇ done
23280 #ˇ}
23281 "});
23282}
23283
23284#[gpui::test]
23285async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
23286 init_test(cx, |_| {});
23287
23288 let mut cx = EditorTestContext::new(cx).await;
23289 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
23290 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23291
23292 // test `else` auto outdents when typed inside `if` block
23293 cx.set_state(indoc! {"
23294 if [ \"$1\" = \"test\" ]; then
23295 echo \"foo bar\"
23296 ˇ
23297 "});
23298 cx.update_editor(|editor, window, cx| {
23299 editor.handle_input("else", window, cx);
23300 });
23301 cx.assert_editor_state(indoc! {"
23302 if [ \"$1\" = \"test\" ]; then
23303 echo \"foo bar\"
23304 elseˇ
23305 "});
23306
23307 // test `elif` auto outdents when typed inside `if` block
23308 cx.set_state(indoc! {"
23309 if [ \"$1\" = \"test\" ]; then
23310 echo \"foo bar\"
23311 ˇ
23312 "});
23313 cx.update_editor(|editor, window, cx| {
23314 editor.handle_input("elif", window, cx);
23315 });
23316 cx.assert_editor_state(indoc! {"
23317 if [ \"$1\" = \"test\" ]; then
23318 echo \"foo bar\"
23319 elifˇ
23320 "});
23321
23322 // test `fi` auto outdents when typed inside `else` block
23323 cx.set_state(indoc! {"
23324 if [ \"$1\" = \"test\" ]; then
23325 echo \"foo bar\"
23326 else
23327 echo \"bar baz\"
23328 ˇ
23329 "});
23330 cx.update_editor(|editor, window, cx| {
23331 editor.handle_input("fi", window, cx);
23332 });
23333 cx.assert_editor_state(indoc! {"
23334 if [ \"$1\" = \"test\" ]; then
23335 echo \"foo bar\"
23336 else
23337 echo \"bar baz\"
23338 fiˇ
23339 "});
23340
23341 // test `done` auto outdents when typed inside `while` block
23342 cx.set_state(indoc! {"
23343 while read line; do
23344 echo \"$line\"
23345 ˇ
23346 "});
23347 cx.update_editor(|editor, window, cx| {
23348 editor.handle_input("done", window, cx);
23349 });
23350 cx.assert_editor_state(indoc! {"
23351 while read line; do
23352 echo \"$line\"
23353 doneˇ
23354 "});
23355
23356 // test `done` auto outdents when typed inside `for` block
23357 cx.set_state(indoc! {"
23358 for file in *.txt; do
23359 cat \"$file\"
23360 ˇ
23361 "});
23362 cx.update_editor(|editor, window, cx| {
23363 editor.handle_input("done", window, cx);
23364 });
23365 cx.assert_editor_state(indoc! {"
23366 for file in *.txt; do
23367 cat \"$file\"
23368 doneˇ
23369 "});
23370
23371 // test `esac` auto outdents when typed inside `case` block
23372 cx.set_state(indoc! {"
23373 case \"$1\" in
23374 start)
23375 echo \"foo bar\"
23376 ;;
23377 stop)
23378 echo \"bar baz\"
23379 ;;
23380 ˇ
23381 "});
23382 cx.update_editor(|editor, window, cx| {
23383 editor.handle_input("esac", window, cx);
23384 });
23385 cx.assert_editor_state(indoc! {"
23386 case \"$1\" in
23387 start)
23388 echo \"foo bar\"
23389 ;;
23390 stop)
23391 echo \"bar baz\"
23392 ;;
23393 esacˇ
23394 "});
23395
23396 // test `*)` auto outdents when typed inside `case` block
23397 cx.set_state(indoc! {"
23398 case \"$1\" in
23399 start)
23400 echo \"foo bar\"
23401 ;;
23402 ˇ
23403 "});
23404 cx.update_editor(|editor, window, cx| {
23405 editor.handle_input("*)", window, cx);
23406 });
23407 cx.assert_editor_state(indoc! {"
23408 case \"$1\" in
23409 start)
23410 echo \"foo bar\"
23411 ;;
23412 *)ˇ
23413 "});
23414
23415 // test `fi` outdents to correct level with nested if blocks
23416 cx.set_state(indoc! {"
23417 if [ \"$1\" = \"test\" ]; then
23418 echo \"outer if\"
23419 if [ \"$2\" = \"debug\" ]; then
23420 echo \"inner if\"
23421 ˇ
23422 "});
23423 cx.update_editor(|editor, window, cx| {
23424 editor.handle_input("fi", window, cx);
23425 });
23426 cx.assert_editor_state(indoc! {"
23427 if [ \"$1\" = \"test\" ]; then
23428 echo \"outer if\"
23429 if [ \"$2\" = \"debug\" ]; then
23430 echo \"inner if\"
23431 fiˇ
23432 "});
23433}
23434
23435#[gpui::test]
23436async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
23437 init_test(cx, |_| {});
23438 update_test_language_settings(cx, |settings| {
23439 settings.defaults.extend_comment_on_newline = Some(false);
23440 });
23441 let mut cx = EditorTestContext::new(cx).await;
23442 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
23443 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23444
23445 // test correct indent after newline on comment
23446 cx.set_state(indoc! {"
23447 # COMMENT:ˇ
23448 "});
23449 cx.update_editor(|editor, window, cx| {
23450 editor.newline(&Newline, window, cx);
23451 });
23452 cx.assert_editor_state(indoc! {"
23453 # COMMENT:
23454 ˇ
23455 "});
23456
23457 // test correct indent after newline after `then`
23458 cx.set_state(indoc! {"
23459
23460 if [ \"$1\" = \"test\" ]; thenˇ
23461 "});
23462 cx.update_editor(|editor, window, cx| {
23463 editor.newline(&Newline, window, cx);
23464 });
23465 cx.run_until_parked();
23466 cx.assert_editor_state(indoc! {"
23467
23468 if [ \"$1\" = \"test\" ]; then
23469 ˇ
23470 "});
23471
23472 // test correct indent after newline after `else`
23473 cx.set_state(indoc! {"
23474 if [ \"$1\" = \"test\" ]; then
23475 elseˇ
23476 "});
23477 cx.update_editor(|editor, window, cx| {
23478 editor.newline(&Newline, window, cx);
23479 });
23480 cx.run_until_parked();
23481 cx.assert_editor_state(indoc! {"
23482 if [ \"$1\" = \"test\" ]; then
23483 else
23484 ˇ
23485 "});
23486
23487 // test correct indent after newline after `elif`
23488 cx.set_state(indoc! {"
23489 if [ \"$1\" = \"test\" ]; then
23490 elifˇ
23491 "});
23492 cx.update_editor(|editor, window, cx| {
23493 editor.newline(&Newline, window, cx);
23494 });
23495 cx.run_until_parked();
23496 cx.assert_editor_state(indoc! {"
23497 if [ \"$1\" = \"test\" ]; then
23498 elif
23499 ˇ
23500 "});
23501
23502 // test correct indent after newline after `do`
23503 cx.set_state(indoc! {"
23504 for file in *.txt; doˇ
23505 "});
23506 cx.update_editor(|editor, window, cx| {
23507 editor.newline(&Newline, window, cx);
23508 });
23509 cx.run_until_parked();
23510 cx.assert_editor_state(indoc! {"
23511 for file in *.txt; do
23512 ˇ
23513 "});
23514
23515 // test correct indent after newline after case pattern
23516 cx.set_state(indoc! {"
23517 case \"$1\" in
23518 start)ˇ
23519 "});
23520 cx.update_editor(|editor, window, cx| {
23521 editor.newline(&Newline, window, cx);
23522 });
23523 cx.run_until_parked();
23524 cx.assert_editor_state(indoc! {"
23525 case \"$1\" in
23526 start)
23527 ˇ
23528 "});
23529
23530 // test correct indent after newline after case pattern
23531 cx.set_state(indoc! {"
23532 case \"$1\" in
23533 start)
23534 ;;
23535 *)ˇ
23536 "});
23537 cx.update_editor(|editor, window, cx| {
23538 editor.newline(&Newline, window, cx);
23539 });
23540 cx.run_until_parked();
23541 cx.assert_editor_state(indoc! {"
23542 case \"$1\" in
23543 start)
23544 ;;
23545 *)
23546 ˇ
23547 "});
23548
23549 // test correct indent after newline after function opening brace
23550 cx.set_state(indoc! {"
23551 function test() {ˇ}
23552 "});
23553 cx.update_editor(|editor, window, cx| {
23554 editor.newline(&Newline, window, cx);
23555 });
23556 cx.run_until_parked();
23557 cx.assert_editor_state(indoc! {"
23558 function test() {
23559 ˇ
23560 }
23561 "});
23562
23563 // test no extra indent after semicolon on same line
23564 cx.set_state(indoc! {"
23565 echo \"test\";ˇ
23566 "});
23567 cx.update_editor(|editor, window, cx| {
23568 editor.newline(&Newline, window, cx);
23569 });
23570 cx.run_until_parked();
23571 cx.assert_editor_state(indoc! {"
23572 echo \"test\";
23573 ˇ
23574 "});
23575}
23576
23577fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
23578 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
23579 point..point
23580}
23581
23582#[track_caller]
23583fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
23584 let (text, ranges) = marked_text_ranges(marked_text, true);
23585 assert_eq!(editor.text(cx), text);
23586 assert_eq!(
23587 editor.selections.ranges(cx),
23588 ranges,
23589 "Assert selections are {}",
23590 marked_text
23591 );
23592}
23593
23594pub fn handle_signature_help_request(
23595 cx: &mut EditorLspTestContext,
23596 mocked_response: lsp::SignatureHelp,
23597) -> impl Future<Output = ()> + use<> {
23598 let mut request =
23599 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
23600 let mocked_response = mocked_response.clone();
23601 async move { Ok(Some(mocked_response)) }
23602 });
23603
23604 async move {
23605 request.next().await;
23606 }
23607}
23608
23609#[track_caller]
23610pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
23611 cx.update_editor(|editor, _, _| {
23612 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
23613 let entries = menu.entries.borrow();
23614 let entries = entries
23615 .iter()
23616 .map(|entry| entry.string.as_str())
23617 .collect::<Vec<_>>();
23618 assert_eq!(entries, expected);
23619 } else {
23620 panic!("Expected completions menu");
23621 }
23622 });
23623}
23624
23625/// Handle completion request passing a marked string specifying where the completion
23626/// should be triggered from using '|' character, what range should be replaced, and what completions
23627/// should be returned using '<' and '>' to delimit the range.
23628///
23629/// Also see `handle_completion_request_with_insert_and_replace`.
23630#[track_caller]
23631pub fn handle_completion_request(
23632 marked_string: &str,
23633 completions: Vec<&'static str>,
23634 is_incomplete: bool,
23635 counter: Arc<AtomicUsize>,
23636 cx: &mut EditorLspTestContext,
23637) -> impl Future<Output = ()> {
23638 let complete_from_marker: TextRangeMarker = '|'.into();
23639 let replace_range_marker: TextRangeMarker = ('<', '>').into();
23640 let (_, mut marked_ranges) = marked_text_ranges_by(
23641 marked_string,
23642 vec![complete_from_marker.clone(), replace_range_marker.clone()],
23643 );
23644
23645 let complete_from_position =
23646 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
23647 let replace_range =
23648 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
23649
23650 let mut request =
23651 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
23652 let completions = completions.clone();
23653 counter.fetch_add(1, atomic::Ordering::Release);
23654 async move {
23655 assert_eq!(params.text_document_position.text_document.uri, url.clone());
23656 assert_eq!(
23657 params.text_document_position.position,
23658 complete_from_position
23659 );
23660 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
23661 is_incomplete,
23662 item_defaults: None,
23663 items: completions
23664 .iter()
23665 .map(|completion_text| lsp::CompletionItem {
23666 label: completion_text.to_string(),
23667 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
23668 range: replace_range,
23669 new_text: completion_text.to_string(),
23670 })),
23671 ..Default::default()
23672 })
23673 .collect(),
23674 })))
23675 }
23676 });
23677
23678 async move {
23679 request.next().await;
23680 }
23681}
23682
23683/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
23684/// given instead, which also contains an `insert` range.
23685///
23686/// This function uses markers to define ranges:
23687/// - `|` marks the cursor position
23688/// - `<>` marks the replace range
23689/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
23690pub fn handle_completion_request_with_insert_and_replace(
23691 cx: &mut EditorLspTestContext,
23692 marked_string: &str,
23693 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
23694 counter: Arc<AtomicUsize>,
23695) -> impl Future<Output = ()> {
23696 let complete_from_marker: TextRangeMarker = '|'.into();
23697 let replace_range_marker: TextRangeMarker = ('<', '>').into();
23698 let insert_range_marker: TextRangeMarker = ('{', '}').into();
23699
23700 let (_, mut marked_ranges) = marked_text_ranges_by(
23701 marked_string,
23702 vec![
23703 complete_from_marker.clone(),
23704 replace_range_marker.clone(),
23705 insert_range_marker.clone(),
23706 ],
23707 );
23708
23709 let complete_from_position =
23710 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
23711 let replace_range =
23712 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
23713
23714 let insert_range = match marked_ranges.remove(&insert_range_marker) {
23715 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
23716 _ => lsp::Range {
23717 start: replace_range.start,
23718 end: complete_from_position,
23719 },
23720 };
23721
23722 let mut request =
23723 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
23724 let completions = completions.clone();
23725 counter.fetch_add(1, atomic::Ordering::Release);
23726 async move {
23727 assert_eq!(params.text_document_position.text_document.uri, url.clone());
23728 assert_eq!(
23729 params.text_document_position.position, complete_from_position,
23730 "marker `|` position doesn't match",
23731 );
23732 Ok(Some(lsp::CompletionResponse::Array(
23733 completions
23734 .iter()
23735 .map(|(label, new_text)| lsp::CompletionItem {
23736 label: label.to_string(),
23737 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
23738 lsp::InsertReplaceEdit {
23739 insert: insert_range,
23740 replace: replace_range,
23741 new_text: new_text.to_string(),
23742 },
23743 )),
23744 ..Default::default()
23745 })
23746 .collect(),
23747 )))
23748 }
23749 });
23750
23751 async move {
23752 request.next().await;
23753 }
23754}
23755
23756fn handle_resolve_completion_request(
23757 cx: &mut EditorLspTestContext,
23758 edits: Option<Vec<(&'static str, &'static str)>>,
23759) -> impl Future<Output = ()> {
23760 let edits = edits.map(|edits| {
23761 edits
23762 .iter()
23763 .map(|(marked_string, new_text)| {
23764 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
23765 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
23766 lsp::TextEdit::new(replace_range, new_text.to_string())
23767 })
23768 .collect::<Vec<_>>()
23769 });
23770
23771 let mut request =
23772 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
23773 let edits = edits.clone();
23774 async move {
23775 Ok(lsp::CompletionItem {
23776 additional_text_edits: edits,
23777 ..Default::default()
23778 })
23779 }
23780 });
23781
23782 async move {
23783 request.next().await;
23784 }
23785}
23786
23787pub(crate) fn update_test_language_settings(
23788 cx: &mut TestAppContext,
23789 f: impl Fn(&mut AllLanguageSettingsContent),
23790) {
23791 cx.update(|cx| {
23792 SettingsStore::update_global(cx, |store, cx| {
23793 store.update_user_settings::<AllLanguageSettings>(cx, f);
23794 });
23795 });
23796}
23797
23798pub(crate) fn update_test_project_settings(
23799 cx: &mut TestAppContext,
23800 f: impl Fn(&mut ProjectSettings),
23801) {
23802 cx.update(|cx| {
23803 SettingsStore::update_global(cx, |store, cx| {
23804 store.update_user_settings::<ProjectSettings>(cx, f);
23805 });
23806 });
23807}
23808
23809pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
23810 cx.update(|cx| {
23811 assets::Assets.load_test_fonts(cx);
23812 let store = SettingsStore::test(cx);
23813 cx.set_global(store);
23814 theme::init(theme::LoadThemes::JustBase, cx);
23815 release_channel::init(SemanticVersion::default(), cx);
23816 client::init_settings(cx);
23817 language::init(cx);
23818 Project::init_settings(cx);
23819 workspace::init_settings(cx);
23820 crate::init(cx);
23821 });
23822 zlog::init_test();
23823 update_test_language_settings(cx, f);
23824}
23825
23826#[track_caller]
23827fn assert_hunk_revert(
23828 not_reverted_text_with_selections: &str,
23829 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
23830 expected_reverted_text_with_selections: &str,
23831 base_text: &str,
23832 cx: &mut EditorLspTestContext,
23833) {
23834 cx.set_state(not_reverted_text_with_selections);
23835 cx.set_head_text(base_text);
23836 cx.executor().run_until_parked();
23837
23838 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
23839 let snapshot = editor.snapshot(window, cx);
23840 let reverted_hunk_statuses = snapshot
23841 .buffer_snapshot
23842 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
23843 .map(|hunk| hunk.status().kind)
23844 .collect::<Vec<_>>();
23845
23846 editor.git_restore(&Default::default(), window, cx);
23847 reverted_hunk_statuses
23848 });
23849 cx.executor().run_until_parked();
23850 cx.assert_editor_state(expected_reverted_text_with_selections);
23851 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
23852}
23853
23854#[gpui::test(iterations = 10)]
23855async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
23856 init_test(cx, |_| {});
23857
23858 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
23859 let counter = diagnostic_requests.clone();
23860
23861 let fs = FakeFs::new(cx.executor());
23862 fs.insert_tree(
23863 path!("/a"),
23864 json!({
23865 "first.rs": "fn main() { let a = 5; }",
23866 "second.rs": "// Test file",
23867 }),
23868 )
23869 .await;
23870
23871 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23872 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23873 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23874
23875 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23876 language_registry.add(rust_lang());
23877 let mut fake_servers = language_registry.register_fake_lsp(
23878 "Rust",
23879 FakeLspAdapter {
23880 capabilities: lsp::ServerCapabilities {
23881 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
23882 lsp::DiagnosticOptions {
23883 identifier: None,
23884 inter_file_dependencies: true,
23885 workspace_diagnostics: true,
23886 work_done_progress_options: Default::default(),
23887 },
23888 )),
23889 ..Default::default()
23890 },
23891 ..Default::default()
23892 },
23893 );
23894
23895 let editor = workspace
23896 .update(cx, |workspace, window, cx| {
23897 workspace.open_abs_path(
23898 PathBuf::from(path!("/a/first.rs")),
23899 OpenOptions::default(),
23900 window,
23901 cx,
23902 )
23903 })
23904 .unwrap()
23905 .await
23906 .unwrap()
23907 .downcast::<Editor>()
23908 .unwrap();
23909 let fake_server = fake_servers.next().await.unwrap();
23910 let server_id = fake_server.server.server_id();
23911 let mut first_request = fake_server
23912 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
23913 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
23914 let result_id = Some(new_result_id.to_string());
23915 assert_eq!(
23916 params.text_document.uri,
23917 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
23918 );
23919 async move {
23920 Ok(lsp::DocumentDiagnosticReportResult::Report(
23921 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
23922 related_documents: None,
23923 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
23924 items: Vec::new(),
23925 result_id,
23926 },
23927 }),
23928 ))
23929 }
23930 });
23931
23932 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
23933 project.update(cx, |project, cx| {
23934 let buffer_id = editor
23935 .read(cx)
23936 .buffer()
23937 .read(cx)
23938 .as_singleton()
23939 .expect("created a singleton buffer")
23940 .read(cx)
23941 .remote_id();
23942 let buffer_result_id = project
23943 .lsp_store()
23944 .read(cx)
23945 .result_id(server_id, buffer_id, cx);
23946 assert_eq!(expected, buffer_result_id);
23947 });
23948 };
23949
23950 ensure_result_id(None, cx);
23951 cx.executor().advance_clock(Duration::from_millis(60));
23952 cx.executor().run_until_parked();
23953 assert_eq!(
23954 diagnostic_requests.load(atomic::Ordering::Acquire),
23955 1,
23956 "Opening file should trigger diagnostic request"
23957 );
23958 first_request
23959 .next()
23960 .await
23961 .expect("should have sent the first diagnostics pull request");
23962 ensure_result_id(Some("1".to_string()), cx);
23963
23964 // Editing should trigger diagnostics
23965 editor.update_in(cx, |editor, window, cx| {
23966 editor.handle_input("2", window, cx)
23967 });
23968 cx.executor().advance_clock(Duration::from_millis(60));
23969 cx.executor().run_until_parked();
23970 assert_eq!(
23971 diagnostic_requests.load(atomic::Ordering::Acquire),
23972 2,
23973 "Editing should trigger diagnostic request"
23974 );
23975 ensure_result_id(Some("2".to_string()), cx);
23976
23977 // Moving cursor should not trigger diagnostic request
23978 editor.update_in(cx, |editor, window, cx| {
23979 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23980 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
23981 });
23982 });
23983 cx.executor().advance_clock(Duration::from_millis(60));
23984 cx.executor().run_until_parked();
23985 assert_eq!(
23986 diagnostic_requests.load(atomic::Ordering::Acquire),
23987 2,
23988 "Cursor movement should not trigger diagnostic request"
23989 );
23990 ensure_result_id(Some("2".to_string()), cx);
23991 // Multiple rapid edits should be debounced
23992 for _ in 0..5 {
23993 editor.update_in(cx, |editor, window, cx| {
23994 editor.handle_input("x", window, cx)
23995 });
23996 }
23997 cx.executor().advance_clock(Duration::from_millis(60));
23998 cx.executor().run_until_parked();
23999
24000 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
24001 assert!(
24002 final_requests <= 4,
24003 "Multiple rapid edits should be debounced (got {final_requests} requests)",
24004 );
24005 ensure_result_id(Some(final_requests.to_string()), cx);
24006}
24007
24008#[gpui::test]
24009async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
24010 // Regression test for issue #11671
24011 // Previously, adding a cursor after moving multiple cursors would reset
24012 // the cursor count instead of adding to the existing cursors.
24013 init_test(cx, |_| {});
24014 let mut cx = EditorTestContext::new(cx).await;
24015
24016 // Create a simple buffer with cursor at start
24017 cx.set_state(indoc! {"
24018 ˇaaaa
24019 bbbb
24020 cccc
24021 dddd
24022 eeee
24023 ffff
24024 gggg
24025 hhhh"});
24026
24027 // Add 2 cursors below (so we have 3 total)
24028 cx.update_editor(|editor, window, cx| {
24029 editor.add_selection_below(&Default::default(), window, cx);
24030 editor.add_selection_below(&Default::default(), window, cx);
24031 });
24032
24033 // Verify we have 3 cursors
24034 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
24035 assert_eq!(
24036 initial_count, 3,
24037 "Should have 3 cursors after adding 2 below"
24038 );
24039
24040 // Move down one line
24041 cx.update_editor(|editor, window, cx| {
24042 editor.move_down(&MoveDown, window, cx);
24043 });
24044
24045 // Add another cursor below
24046 cx.update_editor(|editor, window, cx| {
24047 editor.add_selection_below(&Default::default(), window, cx);
24048 });
24049
24050 // Should now have 4 cursors (3 original + 1 new)
24051 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
24052 assert_eq!(
24053 final_count, 4,
24054 "Should have 4 cursors after moving and adding another"
24055 );
24056}
24057
24058#[gpui::test(iterations = 10)]
24059async fn test_document_colors(cx: &mut TestAppContext) {
24060 let expected_color = Rgba {
24061 r: 0.33,
24062 g: 0.33,
24063 b: 0.33,
24064 a: 0.33,
24065 };
24066
24067 init_test(cx, |_| {});
24068
24069 let fs = FakeFs::new(cx.executor());
24070 fs.insert_tree(
24071 path!("/a"),
24072 json!({
24073 "first.rs": "fn main() { let a = 5; }",
24074 }),
24075 )
24076 .await;
24077
24078 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24079 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24080 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24081
24082 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24083 language_registry.add(rust_lang());
24084 let mut fake_servers = language_registry.register_fake_lsp(
24085 "Rust",
24086 FakeLspAdapter {
24087 capabilities: lsp::ServerCapabilities {
24088 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
24089 ..lsp::ServerCapabilities::default()
24090 },
24091 name: "rust-analyzer",
24092 ..FakeLspAdapter::default()
24093 },
24094 );
24095 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
24096 "Rust",
24097 FakeLspAdapter {
24098 capabilities: lsp::ServerCapabilities {
24099 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
24100 ..lsp::ServerCapabilities::default()
24101 },
24102 name: "not-rust-analyzer",
24103 ..FakeLspAdapter::default()
24104 },
24105 );
24106
24107 let editor = workspace
24108 .update(cx, |workspace, window, cx| {
24109 workspace.open_abs_path(
24110 PathBuf::from(path!("/a/first.rs")),
24111 OpenOptions::default(),
24112 window,
24113 cx,
24114 )
24115 })
24116 .unwrap()
24117 .await
24118 .unwrap()
24119 .downcast::<Editor>()
24120 .unwrap();
24121 let fake_language_server = fake_servers.next().await.unwrap();
24122 let fake_language_server_without_capabilities =
24123 fake_servers_without_capabilities.next().await.unwrap();
24124 let requests_made = Arc::new(AtomicUsize::new(0));
24125 let closure_requests_made = Arc::clone(&requests_made);
24126 let mut color_request_handle = fake_language_server
24127 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
24128 let requests_made = Arc::clone(&closure_requests_made);
24129 async move {
24130 assert_eq!(
24131 params.text_document.uri,
24132 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
24133 );
24134 requests_made.fetch_add(1, atomic::Ordering::Release);
24135 Ok(vec![
24136 lsp::ColorInformation {
24137 range: lsp::Range {
24138 start: lsp::Position {
24139 line: 0,
24140 character: 0,
24141 },
24142 end: lsp::Position {
24143 line: 0,
24144 character: 1,
24145 },
24146 },
24147 color: lsp::Color {
24148 red: 0.33,
24149 green: 0.33,
24150 blue: 0.33,
24151 alpha: 0.33,
24152 },
24153 },
24154 lsp::ColorInformation {
24155 range: lsp::Range {
24156 start: lsp::Position {
24157 line: 0,
24158 character: 0,
24159 },
24160 end: lsp::Position {
24161 line: 0,
24162 character: 1,
24163 },
24164 },
24165 color: lsp::Color {
24166 red: 0.33,
24167 green: 0.33,
24168 blue: 0.33,
24169 alpha: 0.33,
24170 },
24171 },
24172 ])
24173 }
24174 });
24175
24176 let _handle = fake_language_server_without_capabilities
24177 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
24178 panic!("Should not be called");
24179 });
24180 cx.executor().advance_clock(Duration::from_millis(100));
24181 color_request_handle.next().await.unwrap();
24182 cx.run_until_parked();
24183 assert_eq!(
24184 1,
24185 requests_made.load(atomic::Ordering::Acquire),
24186 "Should query for colors once per editor open"
24187 );
24188 editor.update_in(cx, |editor, _, cx| {
24189 assert_eq!(
24190 vec![expected_color],
24191 extract_color_inlays(editor, cx),
24192 "Should have an initial inlay"
24193 );
24194 });
24195
24196 // opening another file in a split should not influence the LSP query counter
24197 workspace
24198 .update(cx, |workspace, window, cx| {
24199 assert_eq!(
24200 workspace.panes().len(),
24201 1,
24202 "Should have one pane with one editor"
24203 );
24204 workspace.move_item_to_pane_in_direction(
24205 &MoveItemToPaneInDirection {
24206 direction: SplitDirection::Right,
24207 focus: false,
24208 clone: true,
24209 },
24210 window,
24211 cx,
24212 );
24213 })
24214 .unwrap();
24215 cx.run_until_parked();
24216 workspace
24217 .update(cx, |workspace, _, cx| {
24218 let panes = workspace.panes();
24219 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
24220 for pane in panes {
24221 let editor = pane
24222 .read(cx)
24223 .active_item()
24224 .and_then(|item| item.downcast::<Editor>())
24225 .expect("Should have opened an editor in each split");
24226 let editor_file = editor
24227 .read(cx)
24228 .buffer()
24229 .read(cx)
24230 .as_singleton()
24231 .expect("test deals with singleton buffers")
24232 .read(cx)
24233 .file()
24234 .expect("test buffese should have a file")
24235 .path();
24236 assert_eq!(
24237 editor_file.as_ref(),
24238 Path::new("first.rs"),
24239 "Both editors should be opened for the same file"
24240 )
24241 }
24242 })
24243 .unwrap();
24244
24245 cx.executor().advance_clock(Duration::from_millis(500));
24246 let save = editor.update_in(cx, |editor, window, cx| {
24247 editor.move_to_end(&MoveToEnd, window, cx);
24248 editor.handle_input("dirty", window, cx);
24249 editor.save(
24250 SaveOptions {
24251 format: true,
24252 autosave: true,
24253 },
24254 project.clone(),
24255 window,
24256 cx,
24257 )
24258 });
24259 save.await.unwrap();
24260
24261 color_request_handle.next().await.unwrap();
24262 cx.run_until_parked();
24263 assert_eq!(
24264 3,
24265 requests_made.load(atomic::Ordering::Acquire),
24266 "Should query for colors once per save and once per formatting after save"
24267 );
24268
24269 drop(editor);
24270 let close = workspace
24271 .update(cx, |workspace, window, cx| {
24272 workspace.active_pane().update(cx, |pane, cx| {
24273 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24274 })
24275 })
24276 .unwrap();
24277 close.await.unwrap();
24278 let close = workspace
24279 .update(cx, |workspace, window, cx| {
24280 workspace.active_pane().update(cx, |pane, cx| {
24281 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24282 })
24283 })
24284 .unwrap();
24285 close.await.unwrap();
24286 assert_eq!(
24287 3,
24288 requests_made.load(atomic::Ordering::Acquire),
24289 "After saving and closing all editors, no extra requests should be made"
24290 );
24291 workspace
24292 .update(cx, |workspace, _, cx| {
24293 assert!(
24294 workspace.active_item(cx).is_none(),
24295 "Should close all editors"
24296 )
24297 })
24298 .unwrap();
24299
24300 workspace
24301 .update(cx, |workspace, window, cx| {
24302 workspace.active_pane().update(cx, |pane, cx| {
24303 pane.navigate_backward(window, cx);
24304 })
24305 })
24306 .unwrap();
24307 cx.executor().advance_clock(Duration::from_millis(100));
24308 cx.run_until_parked();
24309 let editor = workspace
24310 .update(cx, |workspace, _, cx| {
24311 workspace
24312 .active_item(cx)
24313 .expect("Should have reopened the editor again after navigating back")
24314 .downcast::<Editor>()
24315 .expect("Should be an editor")
24316 })
24317 .unwrap();
24318 color_request_handle.next().await.unwrap();
24319 assert_eq!(
24320 3,
24321 requests_made.load(atomic::Ordering::Acquire),
24322 "Cache should be reused on buffer close and reopen"
24323 );
24324 editor.update(cx, |editor, cx| {
24325 assert_eq!(
24326 vec![expected_color],
24327 extract_color_inlays(editor, cx),
24328 "Should have an initial inlay"
24329 );
24330 });
24331}
24332
24333#[gpui::test]
24334async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
24335 init_test(cx, |_| {});
24336 let (editor, cx) = cx.add_window_view(Editor::single_line);
24337 editor.update_in(cx, |editor, window, cx| {
24338 editor.set_text("oops\n\nwow\n", window, cx)
24339 });
24340 cx.run_until_parked();
24341 editor.update(cx, |editor, cx| {
24342 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
24343 });
24344 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
24345 cx.run_until_parked();
24346 editor.update(cx, |editor, cx| {
24347 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
24348 });
24349}
24350
24351#[track_caller]
24352fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
24353 editor
24354 .all_inlays(cx)
24355 .into_iter()
24356 .filter_map(|inlay| inlay.get_color())
24357 .map(Rgba::from)
24358 .collect()
24359}