1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 inline_completion_tests::FakeInlineCompletionProvider,
6 linked_editing_ranges::LinkedEditingRanges,
7 scroll::scroll_amount::ScrollAmount,
8 test::{
9 assert_text_with_selections, build_editor,
10 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
11 editor_test_context::EditorTestContext,
12 select_ranges,
13 },
14};
15use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
16use futures::StreamExt;
17use gpui::{
18 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
19 VisualTestContext, WindowBounds, WindowOptions, div,
20};
21use indoc::indoc;
22use language::{
23 BracketPairConfig,
24 Capability::ReadWrite,
25 DiagnosticSourceKind, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher,
26 LanguageName, Override, Point,
27 language_settings::{
28 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings, 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, CloseInactiveItems, MoveItemToPaneInDirection, NavigationEntry,
59 OpenOptions, ViewId,
60 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
61};
62
63#[gpui::test]
64fn test_edit_events(cx: &mut TestAppContext) {
65 init_test(cx, |_| {});
66
67 let buffer = cx.new(|cx| {
68 let mut buffer = language::Buffer::local("123456", cx);
69 buffer.set_group_interval(Duration::from_secs(1));
70 buffer
71 });
72
73 let events = Rc::new(RefCell::new(Vec::new()));
74 let editor1 = cx.add_window({
75 let events = events.clone();
76 |window, cx| {
77 let entity = cx.entity().clone();
78 cx.subscribe_in(
79 &entity,
80 window,
81 move |_, _, event: &EditorEvent, _, _| match event {
82 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
83 EditorEvent::BufferEdited => {
84 events.borrow_mut().push(("editor1", "buffer edited"))
85 }
86 _ => {}
87 },
88 )
89 .detach();
90 Editor::for_buffer(buffer.clone(), None, window, cx)
91 }
92 });
93
94 let editor2 = cx.add_window({
95 let events = events.clone();
96 |window, cx| {
97 cx.subscribe_in(
98 &cx.entity().clone(),
99 window,
100 move |_, _, event: &EditorEvent, _, _| match event {
101 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
102 EditorEvent::BufferEdited => {
103 events.borrow_mut().push(("editor2", "buffer edited"))
104 }
105 _ => {}
106 },
107 )
108 .detach();
109 Editor::for_buffer(buffer.clone(), None, window, cx)
110 }
111 });
112
113 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
114
115 // Mutating editor 1 will emit an `Edited` event only for that editor.
116 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
117 assert_eq!(
118 mem::take(&mut *events.borrow_mut()),
119 [
120 ("editor1", "edited"),
121 ("editor1", "buffer edited"),
122 ("editor2", "buffer edited"),
123 ]
124 );
125
126 // Mutating editor 2 will emit an `Edited` event only for that editor.
127 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
128 assert_eq!(
129 mem::take(&mut *events.borrow_mut()),
130 [
131 ("editor2", "edited"),
132 ("editor1", "buffer edited"),
133 ("editor2", "buffer edited"),
134 ]
135 );
136
137 // Undoing on editor 1 will emit an `Edited` event only for that editor.
138 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
139 assert_eq!(
140 mem::take(&mut *events.borrow_mut()),
141 [
142 ("editor1", "edited"),
143 ("editor1", "buffer edited"),
144 ("editor2", "buffer edited"),
145 ]
146 );
147
148 // Redoing on editor 1 will emit an `Edited` event only for that editor.
149 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
150 assert_eq!(
151 mem::take(&mut *events.borrow_mut()),
152 [
153 ("editor1", "edited"),
154 ("editor1", "buffer edited"),
155 ("editor2", "buffer edited"),
156 ]
157 );
158
159 // Undoing on editor 2 will emit an `Edited` event only for that editor.
160 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
161 assert_eq!(
162 mem::take(&mut *events.borrow_mut()),
163 [
164 ("editor2", "edited"),
165 ("editor1", "buffer edited"),
166 ("editor2", "buffer edited"),
167 ]
168 );
169
170 // Redoing on editor 2 will emit an `Edited` event only for that editor.
171 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
172 assert_eq!(
173 mem::take(&mut *events.borrow_mut()),
174 [
175 ("editor2", "edited"),
176 ("editor1", "buffer edited"),
177 ("editor2", "buffer edited"),
178 ]
179 );
180
181 // No event is emitted when the mutation is a no-op.
182 _ = editor2.update(cx, |editor, window, cx| {
183 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
184 s.select_ranges([0..0])
185 });
186
187 editor.backspace(&Backspace, window, cx);
188 });
189 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
190}
191
192#[gpui::test]
193fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
194 init_test(cx, |_| {});
195
196 let mut now = Instant::now();
197 let group_interval = Duration::from_millis(1);
198 let buffer = cx.new(|cx| {
199 let mut buf = language::Buffer::local("123456", cx);
200 buf.set_group_interval(group_interval);
201 buf
202 });
203 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
204 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
205
206 _ = editor.update(cx, |editor, window, cx| {
207 editor.start_transaction_at(now, window, cx);
208 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
209 s.select_ranges([2..4])
210 });
211
212 editor.insert("cd", window, cx);
213 editor.end_transaction_at(now, cx);
214 assert_eq!(editor.text(cx), "12cd56");
215 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
216
217 editor.start_transaction_at(now, window, cx);
218 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
219 s.select_ranges([4..5])
220 });
221 editor.insert("e", window, cx);
222 editor.end_transaction_at(now, cx);
223 assert_eq!(editor.text(cx), "12cde6");
224 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
225
226 now += group_interval + Duration::from_millis(1);
227 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
228 s.select_ranges([2..2])
229 });
230
231 // Simulate an edit in another editor
232 buffer.update(cx, |buffer, cx| {
233 buffer.start_transaction_at(now, cx);
234 buffer.edit([(0..1, "a")], None, cx);
235 buffer.edit([(1..1, "b")], None, cx);
236 buffer.end_transaction_at(now, cx);
237 });
238
239 assert_eq!(editor.text(cx), "ab2cde6");
240 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
241
242 // Last transaction happened past the group interval in a different editor.
243 // Undo it individually and don't restore selections.
244 editor.undo(&Undo, window, cx);
245 assert_eq!(editor.text(cx), "12cde6");
246 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
247
248 // First two transactions happened within the group interval in this editor.
249 // Undo them together and restore selections.
250 editor.undo(&Undo, window, cx);
251 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
252 assert_eq!(editor.text(cx), "123456");
253 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
254
255 // Redo the first two transactions together.
256 editor.redo(&Redo, window, cx);
257 assert_eq!(editor.text(cx), "12cde6");
258 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
259
260 // Redo the last transaction on its own.
261 editor.redo(&Redo, window, cx);
262 assert_eq!(editor.text(cx), "ab2cde6");
263 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
264
265 // Test empty transactions.
266 editor.start_transaction_at(now, window, cx);
267 editor.end_transaction_at(now, cx);
268 editor.undo(&Undo, window, cx);
269 assert_eq!(editor.text(cx), "12cde6");
270 });
271}
272
273#[gpui::test]
274fn test_ime_composition(cx: &mut TestAppContext) {
275 init_test(cx, |_| {});
276
277 let buffer = cx.new(|cx| {
278 let mut buffer = language::Buffer::local("abcde", cx);
279 // Ensure automatic grouping doesn't occur.
280 buffer.set_group_interval(Duration::ZERO);
281 buffer
282 });
283
284 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
285 cx.add_window(|window, cx| {
286 let mut editor = build_editor(buffer.clone(), window, cx);
287
288 // Start a new IME composition.
289 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
290 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
291 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
292 assert_eq!(editor.text(cx), "äbcde");
293 assert_eq!(
294 editor.marked_text_ranges(cx),
295 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
296 );
297
298 // Finalize IME composition.
299 editor.replace_text_in_range(None, "ā", window, cx);
300 assert_eq!(editor.text(cx), "ābcde");
301 assert_eq!(editor.marked_text_ranges(cx), None);
302
303 // IME composition edits are grouped and are undone/redone at once.
304 editor.undo(&Default::default(), window, cx);
305 assert_eq!(editor.text(cx), "abcde");
306 assert_eq!(editor.marked_text_ranges(cx), None);
307 editor.redo(&Default::default(), window, cx);
308 assert_eq!(editor.text(cx), "ābcde");
309 assert_eq!(editor.marked_text_ranges(cx), None);
310
311 // Start a new IME composition.
312 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
313 assert_eq!(
314 editor.marked_text_ranges(cx),
315 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
316 );
317
318 // Undoing during an IME composition cancels it.
319 editor.undo(&Default::default(), window, cx);
320 assert_eq!(editor.text(cx), "ābcde");
321 assert_eq!(editor.marked_text_ranges(cx), None);
322
323 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
324 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
325 assert_eq!(editor.text(cx), "ābcdè");
326 assert_eq!(
327 editor.marked_text_ranges(cx),
328 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
329 );
330
331 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
332 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
333 assert_eq!(editor.text(cx), "ābcdę");
334 assert_eq!(editor.marked_text_ranges(cx), None);
335
336 // Start a new IME composition with multiple cursors.
337 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
338 s.select_ranges([
339 OffsetUtf16(1)..OffsetUtf16(1),
340 OffsetUtf16(3)..OffsetUtf16(3),
341 OffsetUtf16(5)..OffsetUtf16(5),
342 ])
343 });
344 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
345 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
346 assert_eq!(
347 editor.marked_text_ranges(cx),
348 Some(vec![
349 OffsetUtf16(0)..OffsetUtf16(3),
350 OffsetUtf16(4)..OffsetUtf16(7),
351 OffsetUtf16(8)..OffsetUtf16(11)
352 ])
353 );
354
355 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
356 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
357 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
358 assert_eq!(
359 editor.marked_text_ranges(cx),
360 Some(vec![
361 OffsetUtf16(1)..OffsetUtf16(2),
362 OffsetUtf16(5)..OffsetUtf16(6),
363 OffsetUtf16(9)..OffsetUtf16(10)
364 ])
365 );
366
367 // Finalize IME composition with multiple cursors.
368 editor.replace_text_in_range(Some(9..10), "2", window, cx);
369 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
370 assert_eq!(editor.marked_text_ranges(cx), None);
371
372 editor
373 });
374}
375
376#[gpui::test]
377fn test_selection_with_mouse(cx: &mut TestAppContext) {
378 init_test(cx, |_| {});
379
380 let editor = cx.add_window(|window, cx| {
381 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
382 build_editor(buffer, window, cx)
383 });
384
385 _ = editor.update(cx, |editor, window, cx| {
386 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
387 });
388 assert_eq!(
389 editor
390 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
391 .unwrap(),
392 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
393 );
394
395 _ = editor.update(cx, |editor, window, cx| {
396 editor.update_selection(
397 DisplayPoint::new(DisplayRow(3), 3),
398 0,
399 gpui::Point::<f32>::default(),
400 window,
401 cx,
402 );
403 });
404
405 assert_eq!(
406 editor
407 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
408 .unwrap(),
409 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
410 );
411
412 _ = editor.update(cx, |editor, window, cx| {
413 editor.update_selection(
414 DisplayPoint::new(DisplayRow(1), 1),
415 0,
416 gpui::Point::<f32>::default(),
417 window,
418 cx,
419 );
420 });
421
422 assert_eq!(
423 editor
424 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
425 .unwrap(),
426 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
427 );
428
429 _ = editor.update(cx, |editor, window, cx| {
430 editor.end_selection(window, cx);
431 editor.update_selection(
432 DisplayPoint::new(DisplayRow(3), 3),
433 0,
434 gpui::Point::<f32>::default(),
435 window,
436 cx,
437 );
438 });
439
440 assert_eq!(
441 editor
442 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
443 .unwrap(),
444 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
445 );
446
447 _ = editor.update(cx, |editor, window, cx| {
448 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
449 editor.update_selection(
450 DisplayPoint::new(DisplayRow(0), 0),
451 0,
452 gpui::Point::<f32>::default(),
453 window,
454 cx,
455 );
456 });
457
458 assert_eq!(
459 editor
460 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
461 .unwrap(),
462 [
463 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
464 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
465 ]
466 );
467
468 _ = editor.update(cx, |editor, window, cx| {
469 editor.end_selection(window, cx);
470 });
471
472 assert_eq!(
473 editor
474 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
475 .unwrap(),
476 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
477 );
478}
479
480#[gpui::test]
481fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
482 init_test(cx, |_| {});
483
484 let editor = cx.add_window(|window, cx| {
485 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
486 build_editor(buffer, window, cx)
487 });
488
489 _ = editor.update(cx, |editor, window, cx| {
490 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
491 });
492
493 _ = editor.update(cx, |editor, window, cx| {
494 editor.end_selection(window, cx);
495 });
496
497 _ = editor.update(cx, |editor, window, cx| {
498 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
499 });
500
501 _ = editor.update(cx, |editor, window, cx| {
502 editor.end_selection(window, cx);
503 });
504
505 assert_eq!(
506 editor
507 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
508 .unwrap(),
509 [
510 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
511 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
512 ]
513 );
514
515 _ = editor.update(cx, |editor, window, cx| {
516 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
517 });
518
519 _ = editor.update(cx, |editor, window, cx| {
520 editor.end_selection(window, cx);
521 });
522
523 assert_eq!(
524 editor
525 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
526 .unwrap(),
527 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
528 );
529}
530
531#[gpui::test]
532fn test_canceling_pending_selection(cx: &mut TestAppContext) {
533 init_test(cx, |_| {});
534
535 let editor = cx.add_window(|window, cx| {
536 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
537 build_editor(buffer, window, cx)
538 });
539
540 _ = editor.update(cx, |editor, window, cx| {
541 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
542 assert_eq!(
543 editor.selections.display_ranges(cx),
544 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
545 );
546 });
547
548 _ = editor.update(cx, |editor, window, cx| {
549 editor.update_selection(
550 DisplayPoint::new(DisplayRow(3), 3),
551 0,
552 gpui::Point::<f32>::default(),
553 window,
554 cx,
555 );
556 assert_eq!(
557 editor.selections.display_ranges(cx),
558 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
559 );
560 });
561
562 _ = editor.update(cx, |editor, window, cx| {
563 editor.cancel(&Cancel, window, cx);
564 editor.update_selection(
565 DisplayPoint::new(DisplayRow(1), 1),
566 0,
567 gpui::Point::<f32>::default(),
568 window,
569 cx,
570 );
571 assert_eq!(
572 editor.selections.display_ranges(cx),
573 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
574 );
575 });
576}
577
578#[gpui::test]
579fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
580 init_test(cx, |_| {});
581
582 let editor = cx.add_window(|window, cx| {
583 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
584 build_editor(buffer, window, cx)
585 });
586
587 _ = editor.update(cx, |editor, window, cx| {
588 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
589 assert_eq!(
590 editor.selections.display_ranges(cx),
591 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
592 );
593
594 editor.move_down(&Default::default(), window, cx);
595 assert_eq!(
596 editor.selections.display_ranges(cx),
597 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
598 );
599
600 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
601 assert_eq!(
602 editor.selections.display_ranges(cx),
603 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
604 );
605
606 editor.move_up(&Default::default(), window, cx);
607 assert_eq!(
608 editor.selections.display_ranges(cx),
609 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
610 );
611 });
612}
613
614#[gpui::test]
615fn test_clone(cx: &mut TestAppContext) {
616 init_test(cx, |_| {});
617
618 let (text, selection_ranges) = marked_text_ranges(
619 indoc! {"
620 one
621 two
622 threeˇ
623 four
624 fiveˇ
625 "},
626 true,
627 );
628
629 let editor = cx.add_window(|window, cx| {
630 let buffer = MultiBuffer::build_simple(&text, cx);
631 build_editor(buffer, window, cx)
632 });
633
634 _ = editor.update(cx, |editor, window, cx| {
635 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
636 s.select_ranges(selection_ranges.clone())
637 });
638 editor.fold_creases(
639 vec![
640 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
641 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
642 ],
643 true,
644 window,
645 cx,
646 );
647 });
648
649 let cloned_editor = editor
650 .update(cx, |editor, _, cx| {
651 cx.open_window(Default::default(), |window, cx| {
652 cx.new(|cx| editor.clone(window, cx))
653 })
654 })
655 .unwrap()
656 .unwrap();
657
658 let snapshot = editor
659 .update(cx, |e, window, cx| e.snapshot(window, cx))
660 .unwrap();
661 let cloned_snapshot = cloned_editor
662 .update(cx, |e, window, cx| e.snapshot(window, cx))
663 .unwrap();
664
665 assert_eq!(
666 cloned_editor
667 .update(cx, |e, _, cx| e.display_text(cx))
668 .unwrap(),
669 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
670 );
671 assert_eq!(
672 cloned_snapshot
673 .folds_in_range(0..text.len())
674 .collect::<Vec<_>>(),
675 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
676 );
677 assert_set_eq!(
678 cloned_editor
679 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
680 .unwrap(),
681 editor
682 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
683 .unwrap()
684 );
685 assert_set_eq!(
686 cloned_editor
687 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
688 .unwrap(),
689 editor
690 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
691 .unwrap()
692 );
693}
694
695#[gpui::test]
696async fn test_navigation_history(cx: &mut TestAppContext) {
697 init_test(cx, |_| {});
698
699 use workspace::item::Item;
700
701 let fs = FakeFs::new(cx.executor());
702 let project = Project::test(fs, [], cx).await;
703 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
704 let pane = workspace
705 .update(cx, |workspace, _, _| workspace.active_pane().clone())
706 .unwrap();
707
708 _ = workspace.update(cx, |_v, window, cx| {
709 cx.new(|cx| {
710 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
711 let mut editor = build_editor(buffer.clone(), window, cx);
712 let handle = cx.entity();
713 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
714
715 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
716 editor.nav_history.as_mut().unwrap().pop_backward(cx)
717 }
718
719 // Move the cursor a small distance.
720 // Nothing is added to the navigation history.
721 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
722 s.select_display_ranges([
723 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
724 ])
725 });
726 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
727 s.select_display_ranges([
728 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
729 ])
730 });
731 assert!(pop_history(&mut editor, cx).is_none());
732
733 // Move the cursor a large distance.
734 // The history can jump back to the previous position.
735 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
736 s.select_display_ranges([
737 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
738 ])
739 });
740 let nav_entry = pop_history(&mut editor, cx).unwrap();
741 editor.navigate(nav_entry.data.unwrap(), window, cx);
742 assert_eq!(nav_entry.item.id(), cx.entity_id());
743 assert_eq!(
744 editor.selections.display_ranges(cx),
745 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
746 );
747 assert!(pop_history(&mut editor, cx).is_none());
748
749 // Move the cursor a small distance via the mouse.
750 // Nothing is added to the navigation history.
751 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
752 editor.end_selection(window, cx);
753 assert_eq!(
754 editor.selections.display_ranges(cx),
755 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
756 );
757 assert!(pop_history(&mut editor, cx).is_none());
758
759 // Move the cursor a large distance via the mouse.
760 // The history can jump back to the previous position.
761 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
762 editor.end_selection(window, cx);
763 assert_eq!(
764 editor.selections.display_ranges(cx),
765 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
766 );
767 let nav_entry = pop_history(&mut editor, cx).unwrap();
768 editor.navigate(nav_entry.data.unwrap(), window, cx);
769 assert_eq!(nav_entry.item.id(), cx.entity_id());
770 assert_eq!(
771 editor.selections.display_ranges(cx),
772 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
773 );
774 assert!(pop_history(&mut editor, cx).is_none());
775
776 // Set scroll position to check later
777 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
778 let original_scroll_position = editor.scroll_manager.anchor();
779
780 // Jump to the end of the document and adjust scroll
781 editor.move_to_end(&MoveToEnd, window, cx);
782 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
783 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
784
785 let nav_entry = pop_history(&mut editor, cx).unwrap();
786 editor.navigate(nav_entry.data.unwrap(), window, cx);
787 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
788
789 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
790 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
791 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
792 let invalid_point = Point::new(9999, 0);
793 editor.navigate(
794 Box::new(NavigationData {
795 cursor_anchor: invalid_anchor,
796 cursor_position: invalid_point,
797 scroll_anchor: ScrollAnchor {
798 anchor: invalid_anchor,
799 offset: Default::default(),
800 },
801 scroll_top_row: invalid_point.row,
802 }),
803 window,
804 cx,
805 );
806 assert_eq!(
807 editor.selections.display_ranges(cx),
808 &[editor.max_point(cx)..editor.max_point(cx)]
809 );
810 assert_eq!(
811 editor.scroll_position(cx),
812 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
813 );
814
815 editor
816 })
817 });
818}
819
820#[gpui::test]
821fn test_cancel(cx: &mut TestAppContext) {
822 init_test(cx, |_| {});
823
824 let editor = cx.add_window(|window, cx| {
825 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
826 build_editor(buffer, window, cx)
827 });
828
829 _ = editor.update(cx, |editor, window, cx| {
830 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
831 editor.update_selection(
832 DisplayPoint::new(DisplayRow(1), 1),
833 0,
834 gpui::Point::<f32>::default(),
835 window,
836 cx,
837 );
838 editor.end_selection(window, cx);
839
840 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
841 editor.update_selection(
842 DisplayPoint::new(DisplayRow(0), 3),
843 0,
844 gpui::Point::<f32>::default(),
845 window,
846 cx,
847 );
848 editor.end_selection(window, cx);
849 assert_eq!(
850 editor.selections.display_ranges(cx),
851 [
852 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
853 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
854 ]
855 );
856 });
857
858 _ = editor.update(cx, |editor, window, cx| {
859 editor.cancel(&Cancel, window, cx);
860 assert_eq!(
861 editor.selections.display_ranges(cx),
862 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
863 );
864 });
865
866 _ = editor.update(cx, |editor, window, cx| {
867 editor.cancel(&Cancel, window, cx);
868 assert_eq!(
869 editor.selections.display_ranges(cx),
870 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
871 );
872 });
873}
874
875#[gpui::test]
876fn test_fold_action(cx: &mut TestAppContext) {
877 init_test(cx, |_| {});
878
879 let editor = cx.add_window(|window, cx| {
880 let buffer = MultiBuffer::build_simple(
881 &"
882 impl Foo {
883 // Hello!
884
885 fn a() {
886 1
887 }
888
889 fn b() {
890 2
891 }
892
893 fn c() {
894 3
895 }
896 }
897 "
898 .unindent(),
899 cx,
900 );
901 build_editor(buffer.clone(), window, cx)
902 });
903
904 _ = editor.update(cx, |editor, window, cx| {
905 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
906 s.select_display_ranges([
907 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
908 ]);
909 });
910 editor.fold(&Fold, window, cx);
911 assert_eq!(
912 editor.display_text(cx),
913 "
914 impl Foo {
915 // Hello!
916
917 fn a() {
918 1
919 }
920
921 fn b() {⋯
922 }
923
924 fn c() {⋯
925 }
926 }
927 "
928 .unindent(),
929 );
930
931 editor.fold(&Fold, window, cx);
932 assert_eq!(
933 editor.display_text(cx),
934 "
935 impl Foo {⋯
936 }
937 "
938 .unindent(),
939 );
940
941 editor.unfold_lines(&UnfoldLines, window, cx);
942 assert_eq!(
943 editor.display_text(cx),
944 "
945 impl Foo {
946 // Hello!
947
948 fn a() {
949 1
950 }
951
952 fn b() {⋯
953 }
954
955 fn c() {⋯
956 }
957 }
958 "
959 .unindent(),
960 );
961
962 editor.unfold_lines(&UnfoldLines, window, cx);
963 assert_eq!(
964 editor.display_text(cx),
965 editor.buffer.read(cx).read(cx).text()
966 );
967 });
968}
969
970#[gpui::test]
971fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
972 init_test(cx, |_| {});
973
974 let editor = cx.add_window(|window, cx| {
975 let buffer = MultiBuffer::build_simple(
976 &"
977 class Foo:
978 # Hello!
979
980 def a():
981 print(1)
982
983 def b():
984 print(2)
985
986 def c():
987 print(3)
988 "
989 .unindent(),
990 cx,
991 );
992 build_editor(buffer.clone(), window, cx)
993 });
994
995 _ = editor.update(cx, |editor, window, cx| {
996 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
997 s.select_display_ranges([
998 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
999 ]);
1000 });
1001 editor.fold(&Fold, window, cx);
1002 assert_eq!(
1003 editor.display_text(cx),
1004 "
1005 class Foo:
1006 # Hello!
1007
1008 def a():
1009 print(1)
1010
1011 def b():⋯
1012
1013 def c():⋯
1014 "
1015 .unindent(),
1016 );
1017
1018 editor.fold(&Fold, window, cx);
1019 assert_eq!(
1020 editor.display_text(cx),
1021 "
1022 class Foo:⋯
1023 "
1024 .unindent(),
1025 );
1026
1027 editor.unfold_lines(&UnfoldLines, window, cx);
1028 assert_eq!(
1029 editor.display_text(cx),
1030 "
1031 class Foo:
1032 # Hello!
1033
1034 def a():
1035 print(1)
1036
1037 def b():⋯
1038
1039 def c():⋯
1040 "
1041 .unindent(),
1042 );
1043
1044 editor.unfold_lines(&UnfoldLines, window, cx);
1045 assert_eq!(
1046 editor.display_text(cx),
1047 editor.buffer.read(cx).read(cx).text()
1048 );
1049 });
1050}
1051
1052#[gpui::test]
1053fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1054 init_test(cx, |_| {});
1055
1056 let editor = cx.add_window(|window, cx| {
1057 let buffer = MultiBuffer::build_simple(
1058 &"
1059 class Foo:
1060 # Hello!
1061
1062 def a():
1063 print(1)
1064
1065 def b():
1066 print(2)
1067
1068
1069 def c():
1070 print(3)
1071
1072
1073 "
1074 .unindent(),
1075 cx,
1076 );
1077 build_editor(buffer.clone(), window, cx)
1078 });
1079
1080 _ = editor.update(cx, |editor, window, cx| {
1081 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1082 s.select_display_ranges([
1083 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1084 ]);
1085 });
1086 editor.fold(&Fold, window, cx);
1087 assert_eq!(
1088 editor.display_text(cx),
1089 "
1090 class Foo:
1091 # Hello!
1092
1093 def a():
1094 print(1)
1095
1096 def b():⋯
1097
1098
1099 def c():⋯
1100
1101
1102 "
1103 .unindent(),
1104 );
1105
1106 editor.fold(&Fold, window, cx);
1107 assert_eq!(
1108 editor.display_text(cx),
1109 "
1110 class Foo:⋯
1111
1112
1113 "
1114 .unindent(),
1115 );
1116
1117 editor.unfold_lines(&UnfoldLines, window, cx);
1118 assert_eq!(
1119 editor.display_text(cx),
1120 "
1121 class Foo:
1122 # Hello!
1123
1124 def a():
1125 print(1)
1126
1127 def b():⋯
1128
1129
1130 def c():⋯
1131
1132
1133 "
1134 .unindent(),
1135 );
1136
1137 editor.unfold_lines(&UnfoldLines, window, cx);
1138 assert_eq!(
1139 editor.display_text(cx),
1140 editor.buffer.read(cx).read(cx).text()
1141 );
1142 });
1143}
1144
1145#[gpui::test]
1146fn test_fold_at_level(cx: &mut TestAppContext) {
1147 init_test(cx, |_| {});
1148
1149 let editor = cx.add_window(|window, cx| {
1150 let buffer = MultiBuffer::build_simple(
1151 &"
1152 class Foo:
1153 # Hello!
1154
1155 def a():
1156 print(1)
1157
1158 def b():
1159 print(2)
1160
1161
1162 class Bar:
1163 # World!
1164
1165 def a():
1166 print(1)
1167
1168 def b():
1169 print(2)
1170
1171
1172 "
1173 .unindent(),
1174 cx,
1175 );
1176 build_editor(buffer.clone(), window, cx)
1177 });
1178
1179 _ = editor.update(cx, |editor, window, cx| {
1180 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1181 assert_eq!(
1182 editor.display_text(cx),
1183 "
1184 class Foo:
1185 # Hello!
1186
1187 def a():⋯
1188
1189 def b():⋯
1190
1191
1192 class Bar:
1193 # World!
1194
1195 def a():⋯
1196
1197 def b():⋯
1198
1199
1200 "
1201 .unindent(),
1202 );
1203
1204 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1205 assert_eq!(
1206 editor.display_text(cx),
1207 "
1208 class Foo:⋯
1209
1210
1211 class Bar:⋯
1212
1213
1214 "
1215 .unindent(),
1216 );
1217
1218 editor.unfold_all(&UnfoldAll, window, cx);
1219 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1220 assert_eq!(
1221 editor.display_text(cx),
1222 "
1223 class Foo:
1224 # Hello!
1225
1226 def a():
1227 print(1)
1228
1229 def b():
1230 print(2)
1231
1232
1233 class Bar:
1234 # World!
1235
1236 def a():
1237 print(1)
1238
1239 def b():
1240 print(2)
1241
1242
1243 "
1244 .unindent(),
1245 );
1246
1247 assert_eq!(
1248 editor.display_text(cx),
1249 editor.buffer.read(cx).read(cx).text()
1250 );
1251 });
1252}
1253
1254#[gpui::test]
1255fn test_move_cursor(cx: &mut TestAppContext) {
1256 init_test(cx, |_| {});
1257
1258 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1259 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1260
1261 buffer.update(cx, |buffer, cx| {
1262 buffer.edit(
1263 vec![
1264 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1265 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1266 ],
1267 None,
1268 cx,
1269 );
1270 });
1271 _ = editor.update(cx, |editor, window, cx| {
1272 assert_eq!(
1273 editor.selections.display_ranges(cx),
1274 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1275 );
1276
1277 editor.move_down(&MoveDown, window, cx);
1278 assert_eq!(
1279 editor.selections.display_ranges(cx),
1280 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1281 );
1282
1283 editor.move_right(&MoveRight, window, cx);
1284 assert_eq!(
1285 editor.selections.display_ranges(cx),
1286 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1287 );
1288
1289 editor.move_left(&MoveLeft, window, cx);
1290 assert_eq!(
1291 editor.selections.display_ranges(cx),
1292 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1293 );
1294
1295 editor.move_up(&MoveUp, window, cx);
1296 assert_eq!(
1297 editor.selections.display_ranges(cx),
1298 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1299 );
1300
1301 editor.move_to_end(&MoveToEnd, window, cx);
1302 assert_eq!(
1303 editor.selections.display_ranges(cx),
1304 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1305 );
1306
1307 editor.move_to_beginning(&MoveToBeginning, window, cx);
1308 assert_eq!(
1309 editor.selections.display_ranges(cx),
1310 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1311 );
1312
1313 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1314 s.select_display_ranges([
1315 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1316 ]);
1317 });
1318 editor.select_to_beginning(&SelectToBeginning, window, cx);
1319 assert_eq!(
1320 editor.selections.display_ranges(cx),
1321 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1322 );
1323
1324 editor.select_to_end(&SelectToEnd, window, cx);
1325 assert_eq!(
1326 editor.selections.display_ranges(cx),
1327 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1328 );
1329 });
1330}
1331
1332#[gpui::test]
1333fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1334 init_test(cx, |_| {});
1335
1336 let editor = cx.add_window(|window, cx| {
1337 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1338 build_editor(buffer.clone(), window, cx)
1339 });
1340
1341 assert_eq!('🟥'.len_utf8(), 4);
1342 assert_eq!('α'.len_utf8(), 2);
1343
1344 _ = editor.update(cx, |editor, window, cx| {
1345 editor.fold_creases(
1346 vec![
1347 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1348 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1349 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1350 ],
1351 true,
1352 window,
1353 cx,
1354 );
1355 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1356
1357 editor.move_right(&MoveRight, window, cx);
1358 assert_eq!(
1359 editor.selections.display_ranges(cx),
1360 &[empty_range(0, "🟥".len())]
1361 );
1362 editor.move_right(&MoveRight, window, cx);
1363 assert_eq!(
1364 editor.selections.display_ranges(cx),
1365 &[empty_range(0, "🟥🟧".len())]
1366 );
1367 editor.move_right(&MoveRight, window, cx);
1368 assert_eq!(
1369 editor.selections.display_ranges(cx),
1370 &[empty_range(0, "🟥🟧⋯".len())]
1371 );
1372
1373 editor.move_down(&MoveDown, window, cx);
1374 assert_eq!(
1375 editor.selections.display_ranges(cx),
1376 &[empty_range(1, "ab⋯e".len())]
1377 );
1378 editor.move_left(&MoveLeft, window, cx);
1379 assert_eq!(
1380 editor.selections.display_ranges(cx),
1381 &[empty_range(1, "ab⋯".len())]
1382 );
1383 editor.move_left(&MoveLeft, window, cx);
1384 assert_eq!(
1385 editor.selections.display_ranges(cx),
1386 &[empty_range(1, "ab".len())]
1387 );
1388 editor.move_left(&MoveLeft, window, cx);
1389 assert_eq!(
1390 editor.selections.display_ranges(cx),
1391 &[empty_range(1, "a".len())]
1392 );
1393
1394 editor.move_down(&MoveDown, window, cx);
1395 assert_eq!(
1396 editor.selections.display_ranges(cx),
1397 &[empty_range(2, "α".len())]
1398 );
1399 editor.move_right(&MoveRight, window, cx);
1400 assert_eq!(
1401 editor.selections.display_ranges(cx),
1402 &[empty_range(2, "αβ".len())]
1403 );
1404 editor.move_right(&MoveRight, window, cx);
1405 assert_eq!(
1406 editor.selections.display_ranges(cx),
1407 &[empty_range(2, "αβ⋯".len())]
1408 );
1409 editor.move_right(&MoveRight, window, cx);
1410 assert_eq!(
1411 editor.selections.display_ranges(cx),
1412 &[empty_range(2, "αβ⋯ε".len())]
1413 );
1414
1415 editor.move_up(&MoveUp, window, cx);
1416 assert_eq!(
1417 editor.selections.display_ranges(cx),
1418 &[empty_range(1, "ab⋯e".len())]
1419 );
1420 editor.move_down(&MoveDown, window, cx);
1421 assert_eq!(
1422 editor.selections.display_ranges(cx),
1423 &[empty_range(2, "αβ⋯ε".len())]
1424 );
1425 editor.move_up(&MoveUp, window, cx);
1426 assert_eq!(
1427 editor.selections.display_ranges(cx),
1428 &[empty_range(1, "ab⋯e".len())]
1429 );
1430
1431 editor.move_up(&MoveUp, window, cx);
1432 assert_eq!(
1433 editor.selections.display_ranges(cx),
1434 &[empty_range(0, "🟥🟧".len())]
1435 );
1436 editor.move_left(&MoveLeft, window, cx);
1437 assert_eq!(
1438 editor.selections.display_ranges(cx),
1439 &[empty_range(0, "🟥".len())]
1440 );
1441 editor.move_left(&MoveLeft, window, cx);
1442 assert_eq!(
1443 editor.selections.display_ranges(cx),
1444 &[empty_range(0, "".len())]
1445 );
1446 });
1447}
1448
1449#[gpui::test]
1450fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1451 init_test(cx, |_| {});
1452
1453 let editor = cx.add_window(|window, cx| {
1454 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1455 build_editor(buffer.clone(), window, cx)
1456 });
1457 _ = editor.update(cx, |editor, window, cx| {
1458 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1459 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1460 });
1461
1462 // moving above start of document should move selection to start of document,
1463 // but the next move down should still be at the original goal_x
1464 editor.move_up(&MoveUp, window, cx);
1465 assert_eq!(
1466 editor.selections.display_ranges(cx),
1467 &[empty_range(0, "".len())]
1468 );
1469
1470 editor.move_down(&MoveDown, window, cx);
1471 assert_eq!(
1472 editor.selections.display_ranges(cx),
1473 &[empty_range(1, "abcd".len())]
1474 );
1475
1476 editor.move_down(&MoveDown, window, cx);
1477 assert_eq!(
1478 editor.selections.display_ranges(cx),
1479 &[empty_range(2, "αβγ".len())]
1480 );
1481
1482 editor.move_down(&MoveDown, window, cx);
1483 assert_eq!(
1484 editor.selections.display_ranges(cx),
1485 &[empty_range(3, "abcd".len())]
1486 );
1487
1488 editor.move_down(&MoveDown, window, cx);
1489 assert_eq!(
1490 editor.selections.display_ranges(cx),
1491 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1492 );
1493
1494 // moving past end of document should not change goal_x
1495 editor.move_down(&MoveDown, window, cx);
1496 assert_eq!(
1497 editor.selections.display_ranges(cx),
1498 &[empty_range(5, "".len())]
1499 );
1500
1501 editor.move_down(&MoveDown, window, cx);
1502 assert_eq!(
1503 editor.selections.display_ranges(cx),
1504 &[empty_range(5, "".len())]
1505 );
1506
1507 editor.move_up(&MoveUp, window, cx);
1508 assert_eq!(
1509 editor.selections.display_ranges(cx),
1510 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1511 );
1512
1513 editor.move_up(&MoveUp, window, cx);
1514 assert_eq!(
1515 editor.selections.display_ranges(cx),
1516 &[empty_range(3, "abcd".len())]
1517 );
1518
1519 editor.move_up(&MoveUp, window, cx);
1520 assert_eq!(
1521 editor.selections.display_ranges(cx),
1522 &[empty_range(2, "αβγ".len())]
1523 );
1524 });
1525}
1526
1527#[gpui::test]
1528fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1529 init_test(cx, |_| {});
1530 let move_to_beg = MoveToBeginningOfLine {
1531 stop_at_soft_wraps: true,
1532 stop_at_indent: true,
1533 };
1534
1535 let delete_to_beg = DeleteToBeginningOfLine {
1536 stop_at_indent: false,
1537 };
1538
1539 let move_to_end = MoveToEndOfLine {
1540 stop_at_soft_wraps: true,
1541 };
1542
1543 let editor = cx.add_window(|window, cx| {
1544 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1545 build_editor(buffer, window, cx)
1546 });
1547 _ = editor.update(cx, |editor, window, cx| {
1548 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1549 s.select_display_ranges([
1550 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1551 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1552 ]);
1553 });
1554 });
1555
1556 _ = editor.update(cx, |editor, window, cx| {
1557 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1558 assert_eq!(
1559 editor.selections.display_ranges(cx),
1560 &[
1561 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1562 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1563 ]
1564 );
1565 });
1566
1567 _ = editor.update(cx, |editor, window, cx| {
1568 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1569 assert_eq!(
1570 editor.selections.display_ranges(cx),
1571 &[
1572 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1573 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1574 ]
1575 );
1576 });
1577
1578 _ = editor.update(cx, |editor, window, cx| {
1579 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1580 assert_eq!(
1581 editor.selections.display_ranges(cx),
1582 &[
1583 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1584 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1585 ]
1586 );
1587 });
1588
1589 _ = editor.update(cx, |editor, window, cx| {
1590 editor.move_to_end_of_line(&move_to_end, window, cx);
1591 assert_eq!(
1592 editor.selections.display_ranges(cx),
1593 &[
1594 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1595 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1596 ]
1597 );
1598 });
1599
1600 // Moving to the end of line again is a no-op.
1601 _ = editor.update(cx, |editor, window, cx| {
1602 editor.move_to_end_of_line(&move_to_end, window, cx);
1603 assert_eq!(
1604 editor.selections.display_ranges(cx),
1605 &[
1606 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1607 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1608 ]
1609 );
1610 });
1611
1612 _ = editor.update(cx, |editor, window, cx| {
1613 editor.move_left(&MoveLeft, window, cx);
1614 editor.select_to_beginning_of_line(
1615 &SelectToBeginningOfLine {
1616 stop_at_soft_wraps: true,
1617 stop_at_indent: true,
1618 },
1619 window,
1620 cx,
1621 );
1622 assert_eq!(
1623 editor.selections.display_ranges(cx),
1624 &[
1625 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1626 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1627 ]
1628 );
1629 });
1630
1631 _ = editor.update(cx, |editor, window, cx| {
1632 editor.select_to_beginning_of_line(
1633 &SelectToBeginningOfLine {
1634 stop_at_soft_wraps: true,
1635 stop_at_indent: true,
1636 },
1637 window,
1638 cx,
1639 );
1640 assert_eq!(
1641 editor.selections.display_ranges(cx),
1642 &[
1643 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1644 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1645 ]
1646 );
1647 });
1648
1649 _ = editor.update(cx, |editor, window, cx| {
1650 editor.select_to_beginning_of_line(
1651 &SelectToBeginningOfLine {
1652 stop_at_soft_wraps: true,
1653 stop_at_indent: true,
1654 },
1655 window,
1656 cx,
1657 );
1658 assert_eq!(
1659 editor.selections.display_ranges(cx),
1660 &[
1661 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1662 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1663 ]
1664 );
1665 });
1666
1667 _ = editor.update(cx, |editor, window, cx| {
1668 editor.select_to_end_of_line(
1669 &SelectToEndOfLine {
1670 stop_at_soft_wraps: true,
1671 },
1672 window,
1673 cx,
1674 );
1675 assert_eq!(
1676 editor.selections.display_ranges(cx),
1677 &[
1678 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1679 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1680 ]
1681 );
1682 });
1683
1684 _ = editor.update(cx, |editor, window, cx| {
1685 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1686 assert_eq!(editor.display_text(cx), "ab\n de");
1687 assert_eq!(
1688 editor.selections.display_ranges(cx),
1689 &[
1690 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1691 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1692 ]
1693 );
1694 });
1695
1696 _ = editor.update(cx, |editor, window, cx| {
1697 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1698 assert_eq!(editor.display_text(cx), "\n");
1699 assert_eq!(
1700 editor.selections.display_ranges(cx),
1701 &[
1702 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1703 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1704 ]
1705 );
1706 });
1707}
1708
1709#[gpui::test]
1710fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1711 init_test(cx, |_| {});
1712 let move_to_beg = MoveToBeginningOfLine {
1713 stop_at_soft_wraps: false,
1714 stop_at_indent: false,
1715 };
1716
1717 let move_to_end = MoveToEndOfLine {
1718 stop_at_soft_wraps: false,
1719 };
1720
1721 let editor = cx.add_window(|window, cx| {
1722 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1723 build_editor(buffer, window, cx)
1724 });
1725
1726 _ = editor.update(cx, |editor, window, cx| {
1727 editor.set_wrap_width(Some(140.0.into()), cx);
1728
1729 // We expect the following lines after wrapping
1730 // ```
1731 // thequickbrownfox
1732 // jumpedoverthelazydo
1733 // gs
1734 // ```
1735 // The final `gs` was soft-wrapped onto a new line.
1736 assert_eq!(
1737 "thequickbrownfox\njumpedoverthelaz\nydogs",
1738 editor.display_text(cx),
1739 );
1740
1741 // First, let's assert behavior on the first line, that was not soft-wrapped.
1742 // Start the cursor at the `k` on the first line
1743 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1744 s.select_display_ranges([
1745 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1746 ]);
1747 });
1748
1749 // Moving to the beginning of the line should put us at the beginning of the line.
1750 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1751 assert_eq!(
1752 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1753 editor.selections.display_ranges(cx)
1754 );
1755
1756 // Moving to the end of the line should put us at the end of the line.
1757 editor.move_to_end_of_line(&move_to_end, window, cx);
1758 assert_eq!(
1759 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1760 editor.selections.display_ranges(cx)
1761 );
1762
1763 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1764 // Start the cursor at the last line (`y` that was wrapped to a new line)
1765 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1766 s.select_display_ranges([
1767 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1768 ]);
1769 });
1770
1771 // Moving to the beginning of the line should put us at the start of the second line of
1772 // display text, i.e., the `j`.
1773 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1774 assert_eq!(
1775 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1776 editor.selections.display_ranges(cx)
1777 );
1778
1779 // Moving to the beginning of the line again should be a no-op.
1780 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1781 assert_eq!(
1782 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1783 editor.selections.display_ranges(cx)
1784 );
1785
1786 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1787 // next display line.
1788 editor.move_to_end_of_line(&move_to_end, window, cx);
1789 assert_eq!(
1790 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1791 editor.selections.display_ranges(cx)
1792 );
1793
1794 // Moving to the end of the line again should be a no-op.
1795 editor.move_to_end_of_line(&move_to_end, window, cx);
1796 assert_eq!(
1797 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1798 editor.selections.display_ranges(cx)
1799 );
1800 });
1801}
1802
1803#[gpui::test]
1804fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1805 init_test(cx, |_| {});
1806
1807 let move_to_beg = MoveToBeginningOfLine {
1808 stop_at_soft_wraps: true,
1809 stop_at_indent: true,
1810 };
1811
1812 let select_to_beg = SelectToBeginningOfLine {
1813 stop_at_soft_wraps: true,
1814 stop_at_indent: true,
1815 };
1816
1817 let delete_to_beg = DeleteToBeginningOfLine {
1818 stop_at_indent: true,
1819 };
1820
1821 let move_to_end = MoveToEndOfLine {
1822 stop_at_soft_wraps: false,
1823 };
1824
1825 let editor = cx.add_window(|window, cx| {
1826 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1827 build_editor(buffer, window, cx)
1828 });
1829
1830 _ = editor.update(cx, |editor, window, cx| {
1831 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1832 s.select_display_ranges([
1833 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1834 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1835 ]);
1836 });
1837
1838 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1839 // and the second cursor at the first non-whitespace character in the line.
1840 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1841 assert_eq!(
1842 editor.selections.display_ranges(cx),
1843 &[
1844 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1845 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1846 ]
1847 );
1848
1849 // Moving to the beginning of the line again should be a no-op for the first cursor,
1850 // and should move the second cursor to the beginning of the line.
1851 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1852 assert_eq!(
1853 editor.selections.display_ranges(cx),
1854 &[
1855 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1856 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1857 ]
1858 );
1859
1860 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1861 // and should move the second cursor back to the first non-whitespace character in the line.
1862 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1863 assert_eq!(
1864 editor.selections.display_ranges(cx),
1865 &[
1866 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1867 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1868 ]
1869 );
1870
1871 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1872 // and to the first non-whitespace character in the line for the second cursor.
1873 editor.move_to_end_of_line(&move_to_end, window, cx);
1874 editor.move_left(&MoveLeft, window, cx);
1875 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1876 assert_eq!(
1877 editor.selections.display_ranges(cx),
1878 &[
1879 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1880 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1881 ]
1882 );
1883
1884 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1885 // and should select to the beginning of the line for the second cursor.
1886 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1887 assert_eq!(
1888 editor.selections.display_ranges(cx),
1889 &[
1890 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1891 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1892 ]
1893 );
1894
1895 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1896 // and should delete to the first non-whitespace character in the line for the second cursor.
1897 editor.move_to_end_of_line(&move_to_end, window, cx);
1898 editor.move_left(&MoveLeft, window, cx);
1899 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1900 assert_eq!(editor.text(cx), "c\n f");
1901 });
1902}
1903
1904#[gpui::test]
1905fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1906 init_test(cx, |_| {});
1907
1908 let editor = cx.add_window(|window, cx| {
1909 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1910 build_editor(buffer, window, cx)
1911 });
1912 _ = editor.update(cx, |editor, window, cx| {
1913 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1914 s.select_display_ranges([
1915 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1916 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1917 ])
1918 });
1919 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1920 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1921
1922 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1923 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1924
1925 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1926 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1927
1928 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1929 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1930
1931 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1932 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
1933
1934 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1935 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1936
1937 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1938 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1939
1940 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1941 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1942
1943 editor.move_right(&MoveRight, window, cx);
1944 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1945 assert_selection_ranges(
1946 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
1947 editor,
1948 cx,
1949 );
1950
1951 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1952 assert_selection_ranges(
1953 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
1954 editor,
1955 cx,
1956 );
1957
1958 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1959 assert_selection_ranges(
1960 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
1961 editor,
1962 cx,
1963 );
1964 });
1965}
1966
1967#[gpui::test]
1968fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1969 init_test(cx, |_| {});
1970
1971 let editor = cx.add_window(|window, cx| {
1972 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1973 build_editor(buffer, window, cx)
1974 });
1975
1976 _ = editor.update(cx, |editor, window, cx| {
1977 editor.set_wrap_width(Some(140.0.into()), cx);
1978 assert_eq!(
1979 editor.display_text(cx),
1980 "use one::{\n two::three::\n four::five\n};"
1981 );
1982
1983 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1984 s.select_display_ranges([
1985 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1986 ]);
1987 });
1988
1989 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1990 assert_eq!(
1991 editor.selections.display_ranges(cx),
1992 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1993 );
1994
1995 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1996 assert_eq!(
1997 editor.selections.display_ranges(cx),
1998 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1999 );
2000
2001 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2002 assert_eq!(
2003 editor.selections.display_ranges(cx),
2004 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2005 );
2006
2007 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2008 assert_eq!(
2009 editor.selections.display_ranges(cx),
2010 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2011 );
2012
2013 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2014 assert_eq!(
2015 editor.selections.display_ranges(cx),
2016 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2017 );
2018
2019 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2020 assert_eq!(
2021 editor.selections.display_ranges(cx),
2022 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2023 );
2024 });
2025}
2026
2027#[gpui::test]
2028async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2029 init_test(cx, |_| {});
2030 let mut cx = EditorTestContext::new(cx).await;
2031
2032 let line_height = cx.editor(|editor, window, _| {
2033 editor
2034 .style()
2035 .unwrap()
2036 .text
2037 .line_height_in_pixels(window.rem_size())
2038 });
2039 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2040
2041 cx.set_state(
2042 &r#"ˇone
2043 two
2044
2045 three
2046 fourˇ
2047 five
2048
2049 six"#
2050 .unindent(),
2051 );
2052
2053 cx.update_editor(|editor, window, cx| {
2054 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2055 });
2056 cx.assert_editor_state(
2057 &r#"one
2058 two
2059 ˇ
2060 three
2061 four
2062 five
2063 ˇ
2064 six"#
2065 .unindent(),
2066 );
2067
2068 cx.update_editor(|editor, window, cx| {
2069 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2070 });
2071 cx.assert_editor_state(
2072 &r#"one
2073 two
2074
2075 three
2076 four
2077 five
2078 ˇ
2079 sixˇ"#
2080 .unindent(),
2081 );
2082
2083 cx.update_editor(|editor, window, cx| {
2084 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2085 });
2086 cx.assert_editor_state(
2087 &r#"one
2088 two
2089
2090 three
2091 four
2092 five
2093
2094 sixˇ"#
2095 .unindent(),
2096 );
2097
2098 cx.update_editor(|editor, window, cx| {
2099 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2100 });
2101 cx.assert_editor_state(
2102 &r#"one
2103 two
2104
2105 three
2106 four
2107 five
2108 ˇ
2109 six"#
2110 .unindent(),
2111 );
2112
2113 cx.update_editor(|editor, window, cx| {
2114 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2115 });
2116 cx.assert_editor_state(
2117 &r#"one
2118 two
2119 ˇ
2120 three
2121 four
2122 five
2123
2124 six"#
2125 .unindent(),
2126 );
2127
2128 cx.update_editor(|editor, window, cx| {
2129 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2130 });
2131 cx.assert_editor_state(
2132 &r#"ˇone
2133 two
2134
2135 three
2136 four
2137 five
2138
2139 six"#
2140 .unindent(),
2141 );
2142}
2143
2144#[gpui::test]
2145async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2146 init_test(cx, |_| {});
2147 let mut cx = EditorTestContext::new(cx).await;
2148 let line_height = cx.editor(|editor, window, _| {
2149 editor
2150 .style()
2151 .unwrap()
2152 .text
2153 .line_height_in_pixels(window.rem_size())
2154 });
2155 let window = cx.window;
2156 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2157
2158 cx.set_state(
2159 r#"ˇone
2160 two
2161 three
2162 four
2163 five
2164 six
2165 seven
2166 eight
2167 nine
2168 ten
2169 "#,
2170 );
2171
2172 cx.update_editor(|editor, window, cx| {
2173 assert_eq!(
2174 editor.snapshot(window, cx).scroll_position(),
2175 gpui::Point::new(0., 0.)
2176 );
2177 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2178 assert_eq!(
2179 editor.snapshot(window, cx).scroll_position(),
2180 gpui::Point::new(0., 3.)
2181 );
2182 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2183 assert_eq!(
2184 editor.snapshot(window, cx).scroll_position(),
2185 gpui::Point::new(0., 6.)
2186 );
2187 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2188 assert_eq!(
2189 editor.snapshot(window, cx).scroll_position(),
2190 gpui::Point::new(0., 3.)
2191 );
2192
2193 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2194 assert_eq!(
2195 editor.snapshot(window, cx).scroll_position(),
2196 gpui::Point::new(0., 1.)
2197 );
2198 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2199 assert_eq!(
2200 editor.snapshot(window, cx).scroll_position(),
2201 gpui::Point::new(0., 3.)
2202 );
2203 });
2204}
2205
2206#[gpui::test]
2207async fn test_autoscroll(cx: &mut TestAppContext) {
2208 init_test(cx, |_| {});
2209 let mut cx = EditorTestContext::new(cx).await;
2210
2211 let line_height = cx.update_editor(|editor, window, cx| {
2212 editor.set_vertical_scroll_margin(2, cx);
2213 editor
2214 .style()
2215 .unwrap()
2216 .text
2217 .line_height_in_pixels(window.rem_size())
2218 });
2219 let window = cx.window;
2220 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2221
2222 cx.set_state(
2223 r#"ˇone
2224 two
2225 three
2226 four
2227 five
2228 six
2229 seven
2230 eight
2231 nine
2232 ten
2233 "#,
2234 );
2235 cx.update_editor(|editor, window, cx| {
2236 assert_eq!(
2237 editor.snapshot(window, cx).scroll_position(),
2238 gpui::Point::new(0., 0.0)
2239 );
2240 });
2241
2242 // Add a cursor below the visible area. Since both cursors cannot fit
2243 // on screen, the editor autoscrolls to reveal the newest cursor, and
2244 // allows the vertical scroll margin below that cursor.
2245 cx.update_editor(|editor, window, cx| {
2246 editor.change_selections(Default::default(), window, cx, |selections| {
2247 selections.select_ranges([
2248 Point::new(0, 0)..Point::new(0, 0),
2249 Point::new(6, 0)..Point::new(6, 0),
2250 ]);
2251 })
2252 });
2253 cx.update_editor(|editor, window, cx| {
2254 assert_eq!(
2255 editor.snapshot(window, cx).scroll_position(),
2256 gpui::Point::new(0., 3.0)
2257 );
2258 });
2259
2260 // Move down. The editor cursor scrolls down to track the newest cursor.
2261 cx.update_editor(|editor, window, cx| {
2262 editor.move_down(&Default::default(), window, cx);
2263 });
2264 cx.update_editor(|editor, window, cx| {
2265 assert_eq!(
2266 editor.snapshot(window, cx).scroll_position(),
2267 gpui::Point::new(0., 4.0)
2268 );
2269 });
2270
2271 // Add a cursor above the visible area. Since both cursors fit on screen,
2272 // the editor scrolls to show both.
2273 cx.update_editor(|editor, window, cx| {
2274 editor.change_selections(Default::default(), window, cx, |selections| {
2275 selections.select_ranges([
2276 Point::new(1, 0)..Point::new(1, 0),
2277 Point::new(6, 0)..Point::new(6, 0),
2278 ]);
2279 })
2280 });
2281 cx.update_editor(|editor, window, cx| {
2282 assert_eq!(
2283 editor.snapshot(window, cx).scroll_position(),
2284 gpui::Point::new(0., 1.0)
2285 );
2286 });
2287}
2288
2289#[gpui::test]
2290async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2291 init_test(cx, |_| {});
2292 let mut cx = EditorTestContext::new(cx).await;
2293
2294 let line_height = cx.editor(|editor, window, _cx| {
2295 editor
2296 .style()
2297 .unwrap()
2298 .text
2299 .line_height_in_pixels(window.rem_size())
2300 });
2301 let window = cx.window;
2302 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2303 cx.set_state(
2304 &r#"
2305 ˇone
2306 two
2307 threeˇ
2308 four
2309 five
2310 six
2311 seven
2312 eight
2313 nine
2314 ten
2315 "#
2316 .unindent(),
2317 );
2318
2319 cx.update_editor(|editor, window, cx| {
2320 editor.move_page_down(&MovePageDown::default(), window, cx)
2321 });
2322 cx.assert_editor_state(
2323 &r#"
2324 one
2325 two
2326 three
2327 ˇfour
2328 five
2329 sixˇ
2330 seven
2331 eight
2332 nine
2333 ten
2334 "#
2335 .unindent(),
2336 );
2337
2338 cx.update_editor(|editor, window, cx| {
2339 editor.move_page_down(&MovePageDown::default(), window, cx)
2340 });
2341 cx.assert_editor_state(
2342 &r#"
2343 one
2344 two
2345 three
2346 four
2347 five
2348 six
2349 ˇseven
2350 eight
2351 nineˇ
2352 ten
2353 "#
2354 .unindent(),
2355 );
2356
2357 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2358 cx.assert_editor_state(
2359 &r#"
2360 one
2361 two
2362 three
2363 ˇfour
2364 five
2365 sixˇ
2366 seven
2367 eight
2368 nine
2369 ten
2370 "#
2371 .unindent(),
2372 );
2373
2374 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2375 cx.assert_editor_state(
2376 &r#"
2377 ˇone
2378 two
2379 threeˇ
2380 four
2381 five
2382 six
2383 seven
2384 eight
2385 nine
2386 ten
2387 "#
2388 .unindent(),
2389 );
2390
2391 // Test select collapsing
2392 cx.update_editor(|editor, window, cx| {
2393 editor.move_page_down(&MovePageDown::default(), window, cx);
2394 editor.move_page_down(&MovePageDown::default(), window, cx);
2395 editor.move_page_down(&MovePageDown::default(), window, cx);
2396 });
2397 cx.assert_editor_state(
2398 &r#"
2399 one
2400 two
2401 three
2402 four
2403 five
2404 six
2405 seven
2406 eight
2407 nine
2408 ˇten
2409 ˇ"#
2410 .unindent(),
2411 );
2412}
2413
2414#[gpui::test]
2415async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2416 init_test(cx, |_| {});
2417 let mut cx = EditorTestContext::new(cx).await;
2418 cx.set_state("one «two threeˇ» four");
2419 cx.update_editor(|editor, window, cx| {
2420 editor.delete_to_beginning_of_line(
2421 &DeleteToBeginningOfLine {
2422 stop_at_indent: false,
2423 },
2424 window,
2425 cx,
2426 );
2427 assert_eq!(editor.text(cx), " four");
2428 });
2429}
2430
2431#[gpui::test]
2432fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2433 init_test(cx, |_| {});
2434
2435 let editor = cx.add_window(|window, cx| {
2436 let buffer = MultiBuffer::build_simple("one two three four", cx);
2437 build_editor(buffer.clone(), window, cx)
2438 });
2439
2440 _ = editor.update(cx, |editor, window, cx| {
2441 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2442 s.select_display_ranges([
2443 // an empty selection - the preceding word fragment is deleted
2444 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2445 // characters selected - they are deleted
2446 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2447 ])
2448 });
2449 editor.delete_to_previous_word_start(
2450 &DeleteToPreviousWordStart {
2451 ignore_newlines: false,
2452 },
2453 window,
2454 cx,
2455 );
2456 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2457 });
2458
2459 _ = editor.update(cx, |editor, window, cx| {
2460 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2461 s.select_display_ranges([
2462 // an empty selection - the following word fragment is deleted
2463 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2464 // characters selected - they are deleted
2465 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2466 ])
2467 });
2468 editor.delete_to_next_word_end(
2469 &DeleteToNextWordEnd {
2470 ignore_newlines: false,
2471 },
2472 window,
2473 cx,
2474 );
2475 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2476 });
2477}
2478
2479#[gpui::test]
2480fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2481 init_test(cx, |_| {});
2482
2483 let editor = cx.add_window(|window, cx| {
2484 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2485 build_editor(buffer.clone(), window, cx)
2486 });
2487 let del_to_prev_word_start = DeleteToPreviousWordStart {
2488 ignore_newlines: false,
2489 };
2490 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2491 ignore_newlines: true,
2492 };
2493
2494 _ = editor.update(cx, |editor, window, cx| {
2495 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2496 s.select_display_ranges([
2497 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2498 ])
2499 });
2500 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2501 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2502 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2503 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2504 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2505 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2506 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2507 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2508 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2509 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2510 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2511 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2512 });
2513}
2514
2515#[gpui::test]
2516fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2517 init_test(cx, |_| {});
2518
2519 let editor = cx.add_window(|window, cx| {
2520 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2521 build_editor(buffer.clone(), window, cx)
2522 });
2523 let del_to_next_word_end = DeleteToNextWordEnd {
2524 ignore_newlines: false,
2525 };
2526 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2527 ignore_newlines: true,
2528 };
2529
2530 _ = editor.update(cx, |editor, window, cx| {
2531 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2532 s.select_display_ranges([
2533 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2534 ])
2535 });
2536 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2537 assert_eq!(
2538 editor.buffer.read(cx).read(cx).text(),
2539 "one\n two\nthree\n four"
2540 );
2541 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2542 assert_eq!(
2543 editor.buffer.read(cx).read(cx).text(),
2544 "\n two\nthree\n four"
2545 );
2546 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2547 assert_eq!(
2548 editor.buffer.read(cx).read(cx).text(),
2549 "two\nthree\n four"
2550 );
2551 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2552 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2553 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2554 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2555 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2556 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2557 });
2558}
2559
2560#[gpui::test]
2561fn test_newline(cx: &mut TestAppContext) {
2562 init_test(cx, |_| {});
2563
2564 let editor = cx.add_window(|window, cx| {
2565 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2566 build_editor(buffer.clone(), window, cx)
2567 });
2568
2569 _ = editor.update(cx, |editor, window, cx| {
2570 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2571 s.select_display_ranges([
2572 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2573 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2574 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2575 ])
2576 });
2577
2578 editor.newline(&Newline, window, cx);
2579 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2580 });
2581}
2582
2583#[gpui::test]
2584fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2585 init_test(cx, |_| {});
2586
2587 let editor = cx.add_window(|window, cx| {
2588 let buffer = MultiBuffer::build_simple(
2589 "
2590 a
2591 b(
2592 X
2593 )
2594 c(
2595 X
2596 )
2597 "
2598 .unindent()
2599 .as_str(),
2600 cx,
2601 );
2602 let mut editor = build_editor(buffer.clone(), window, cx);
2603 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2604 s.select_ranges([
2605 Point::new(2, 4)..Point::new(2, 5),
2606 Point::new(5, 4)..Point::new(5, 5),
2607 ])
2608 });
2609 editor
2610 });
2611
2612 _ = editor.update(cx, |editor, window, cx| {
2613 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2614 editor.buffer.update(cx, |buffer, cx| {
2615 buffer.edit(
2616 [
2617 (Point::new(1, 2)..Point::new(3, 0), ""),
2618 (Point::new(4, 2)..Point::new(6, 0), ""),
2619 ],
2620 None,
2621 cx,
2622 );
2623 assert_eq!(
2624 buffer.read(cx).text(),
2625 "
2626 a
2627 b()
2628 c()
2629 "
2630 .unindent()
2631 );
2632 });
2633 assert_eq!(
2634 editor.selections.ranges(cx),
2635 &[
2636 Point::new(1, 2)..Point::new(1, 2),
2637 Point::new(2, 2)..Point::new(2, 2),
2638 ],
2639 );
2640
2641 editor.newline(&Newline, window, cx);
2642 assert_eq!(
2643 editor.text(cx),
2644 "
2645 a
2646 b(
2647 )
2648 c(
2649 )
2650 "
2651 .unindent()
2652 );
2653
2654 // The selections are moved after the inserted newlines
2655 assert_eq!(
2656 editor.selections.ranges(cx),
2657 &[
2658 Point::new(2, 0)..Point::new(2, 0),
2659 Point::new(4, 0)..Point::new(4, 0),
2660 ],
2661 );
2662 });
2663}
2664
2665#[gpui::test]
2666async fn test_newline_above(cx: &mut TestAppContext) {
2667 init_test(cx, |settings| {
2668 settings.defaults.tab_size = NonZeroU32::new(4)
2669 });
2670
2671 let language = Arc::new(
2672 Language::new(
2673 LanguageConfig::default(),
2674 Some(tree_sitter_rust::LANGUAGE.into()),
2675 )
2676 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2677 .unwrap(),
2678 );
2679
2680 let mut cx = EditorTestContext::new(cx).await;
2681 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2682 cx.set_state(indoc! {"
2683 const a: ˇA = (
2684 (ˇ
2685 «const_functionˇ»(ˇ),
2686 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2687 )ˇ
2688 ˇ);ˇ
2689 "});
2690
2691 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2692 cx.assert_editor_state(indoc! {"
2693 ˇ
2694 const a: A = (
2695 ˇ
2696 (
2697 ˇ
2698 ˇ
2699 const_function(),
2700 ˇ
2701 ˇ
2702 ˇ
2703 ˇ
2704 something_else,
2705 ˇ
2706 )
2707 ˇ
2708 ˇ
2709 );
2710 "});
2711}
2712
2713#[gpui::test]
2714async fn test_newline_below(cx: &mut TestAppContext) {
2715 init_test(cx, |settings| {
2716 settings.defaults.tab_size = NonZeroU32::new(4)
2717 });
2718
2719 let language = Arc::new(
2720 Language::new(
2721 LanguageConfig::default(),
2722 Some(tree_sitter_rust::LANGUAGE.into()),
2723 )
2724 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2725 .unwrap(),
2726 );
2727
2728 let mut cx = EditorTestContext::new(cx).await;
2729 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2730 cx.set_state(indoc! {"
2731 const a: ˇA = (
2732 (ˇ
2733 «const_functionˇ»(ˇ),
2734 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2735 )ˇ
2736 ˇ);ˇ
2737 "});
2738
2739 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2740 cx.assert_editor_state(indoc! {"
2741 const a: A = (
2742 ˇ
2743 (
2744 ˇ
2745 const_function(),
2746 ˇ
2747 ˇ
2748 something_else,
2749 ˇ
2750 ˇ
2751 ˇ
2752 ˇ
2753 )
2754 ˇ
2755 );
2756 ˇ
2757 ˇ
2758 "});
2759}
2760
2761#[gpui::test]
2762async fn test_newline_comments(cx: &mut TestAppContext) {
2763 init_test(cx, |settings| {
2764 settings.defaults.tab_size = NonZeroU32::new(4)
2765 });
2766
2767 let language = Arc::new(Language::new(
2768 LanguageConfig {
2769 line_comments: vec!["// ".into()],
2770 ..LanguageConfig::default()
2771 },
2772 None,
2773 ));
2774 {
2775 let mut cx = EditorTestContext::new(cx).await;
2776 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2777 cx.set_state(indoc! {"
2778 // Fooˇ
2779 "});
2780
2781 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2782 cx.assert_editor_state(indoc! {"
2783 // Foo
2784 // ˇ
2785 "});
2786 // Ensure that we add comment prefix when existing line contains space
2787 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2788 cx.assert_editor_state(
2789 indoc! {"
2790 // Foo
2791 //s
2792 // ˇ
2793 "}
2794 .replace("s", " ") // s is used as space placeholder to prevent format on save
2795 .as_str(),
2796 );
2797 // Ensure that we add comment prefix when existing line does not contain space
2798 cx.set_state(indoc! {"
2799 // Foo
2800 //ˇ
2801 "});
2802 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2803 cx.assert_editor_state(indoc! {"
2804 // Foo
2805 //
2806 // ˇ
2807 "});
2808 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2809 cx.set_state(indoc! {"
2810 ˇ// Foo
2811 "});
2812 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2813 cx.assert_editor_state(indoc! {"
2814
2815 ˇ// Foo
2816 "});
2817 }
2818 // Ensure that comment continuations can be disabled.
2819 update_test_language_settings(cx, |settings| {
2820 settings.defaults.extend_comment_on_newline = Some(false);
2821 });
2822 let mut cx = EditorTestContext::new(cx).await;
2823 cx.set_state(indoc! {"
2824 // Fooˇ
2825 "});
2826 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2827 cx.assert_editor_state(indoc! {"
2828 // Foo
2829 ˇ
2830 "});
2831}
2832
2833#[gpui::test]
2834async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
2835 init_test(cx, |settings| {
2836 settings.defaults.tab_size = NonZeroU32::new(4)
2837 });
2838
2839 let language = Arc::new(Language::new(
2840 LanguageConfig {
2841 line_comments: vec!["// ".into(), "/// ".into()],
2842 ..LanguageConfig::default()
2843 },
2844 None,
2845 ));
2846 {
2847 let mut cx = EditorTestContext::new(cx).await;
2848 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2849 cx.set_state(indoc! {"
2850 //ˇ
2851 "});
2852 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2853 cx.assert_editor_state(indoc! {"
2854 //
2855 // ˇ
2856 "});
2857
2858 cx.set_state(indoc! {"
2859 ///ˇ
2860 "});
2861 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2862 cx.assert_editor_state(indoc! {"
2863 ///
2864 /// ˇ
2865 "});
2866 }
2867}
2868
2869#[gpui::test]
2870async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
2871 init_test(cx, |settings| {
2872 settings.defaults.tab_size = NonZeroU32::new(4)
2873 });
2874
2875 let language = Arc::new(
2876 Language::new(
2877 LanguageConfig {
2878 documentation: Some(language::DocumentationConfig {
2879 start: "/**".into(),
2880 end: "*/".into(),
2881 prefix: "* ".into(),
2882 tab_size: NonZeroU32::new(1).unwrap(),
2883 }),
2884
2885 ..LanguageConfig::default()
2886 },
2887 Some(tree_sitter_rust::LANGUAGE.into()),
2888 )
2889 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
2890 .unwrap(),
2891 );
2892
2893 {
2894 let mut cx = EditorTestContext::new(cx).await;
2895 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2896 cx.set_state(indoc! {"
2897 /**ˇ
2898 "});
2899
2900 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2901 cx.assert_editor_state(indoc! {"
2902 /**
2903 * ˇ
2904 "});
2905 // Ensure that if cursor is before the comment start,
2906 // we do not actually insert a comment prefix.
2907 cx.set_state(indoc! {"
2908 ˇ/**
2909 "});
2910 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2911 cx.assert_editor_state(indoc! {"
2912
2913 ˇ/**
2914 "});
2915 // Ensure that if cursor is between it doesn't add comment prefix.
2916 cx.set_state(indoc! {"
2917 /*ˇ*
2918 "});
2919 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2920 cx.assert_editor_state(indoc! {"
2921 /*
2922 ˇ*
2923 "});
2924 // Ensure that if suffix exists on same line after cursor it adds new line.
2925 cx.set_state(indoc! {"
2926 /**ˇ*/
2927 "});
2928 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2929 cx.assert_editor_state(indoc! {"
2930 /**
2931 * ˇ
2932 */
2933 "});
2934 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2935 cx.set_state(indoc! {"
2936 /**ˇ */
2937 "});
2938 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2939 cx.assert_editor_state(indoc! {"
2940 /**
2941 * ˇ
2942 */
2943 "});
2944 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2945 cx.set_state(indoc! {"
2946 /** ˇ*/
2947 "});
2948 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2949 cx.assert_editor_state(
2950 indoc! {"
2951 /**s
2952 * ˇ
2953 */
2954 "}
2955 .replace("s", " ") // s is used as space placeholder to prevent format on save
2956 .as_str(),
2957 );
2958 // Ensure that delimiter space is preserved when newline on already
2959 // spaced delimiter.
2960 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2961 cx.assert_editor_state(
2962 indoc! {"
2963 /**s
2964 *s
2965 * ˇ
2966 */
2967 "}
2968 .replace("s", " ") // s is used as space placeholder to prevent format on save
2969 .as_str(),
2970 );
2971 // Ensure that delimiter space is preserved when space is not
2972 // on existing delimiter.
2973 cx.set_state(indoc! {"
2974 /**
2975 *ˇ
2976 */
2977 "});
2978 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2979 cx.assert_editor_state(indoc! {"
2980 /**
2981 *
2982 * ˇ
2983 */
2984 "});
2985 // Ensure that if suffix exists on same line after cursor it
2986 // doesn't add extra new line if prefix is not on same line.
2987 cx.set_state(indoc! {"
2988 /**
2989 ˇ*/
2990 "});
2991 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2992 cx.assert_editor_state(indoc! {"
2993 /**
2994
2995 ˇ*/
2996 "});
2997 // Ensure that it detects suffix after existing prefix.
2998 cx.set_state(indoc! {"
2999 /**ˇ/
3000 "});
3001 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3002 cx.assert_editor_state(indoc! {"
3003 /**
3004 ˇ/
3005 "});
3006 // Ensure that if suffix exists on same line before
3007 // cursor it does not add comment prefix.
3008 cx.set_state(indoc! {"
3009 /** */ˇ
3010 "});
3011 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3012 cx.assert_editor_state(indoc! {"
3013 /** */
3014 ˇ
3015 "});
3016 // Ensure that if suffix exists on same line before
3017 // cursor it does not add comment prefix.
3018 cx.set_state(indoc! {"
3019 /**
3020 *
3021 */ˇ
3022 "});
3023 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3024 cx.assert_editor_state(indoc! {"
3025 /**
3026 *
3027 */
3028 ˇ
3029 "});
3030
3031 // Ensure that inline comment followed by code
3032 // doesn't add comment prefix on newline
3033 cx.set_state(indoc! {"
3034 /** */ textˇ
3035 "});
3036 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3037 cx.assert_editor_state(indoc! {"
3038 /** */ text
3039 ˇ
3040 "});
3041
3042 // Ensure that text after comment end tag
3043 // doesn't add comment prefix on newline
3044 cx.set_state(indoc! {"
3045 /**
3046 *
3047 */ˇtext
3048 "});
3049 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3050 cx.assert_editor_state(indoc! {"
3051 /**
3052 *
3053 */
3054 ˇtext
3055 "});
3056
3057 // Ensure if not comment block it doesn't
3058 // add comment prefix on newline
3059 cx.set_state(indoc! {"
3060 * textˇ
3061 "});
3062 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3063 cx.assert_editor_state(indoc! {"
3064 * text
3065 ˇ
3066 "});
3067 }
3068 // Ensure that comment continuations can be disabled.
3069 update_test_language_settings(cx, |settings| {
3070 settings.defaults.extend_comment_on_newline = Some(false);
3071 });
3072 let mut cx = EditorTestContext::new(cx).await;
3073 cx.set_state(indoc! {"
3074 /**ˇ
3075 "});
3076 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3077 cx.assert_editor_state(indoc! {"
3078 /**
3079 ˇ
3080 "});
3081}
3082
3083#[gpui::test]
3084fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3085 init_test(cx, |_| {});
3086
3087 let editor = cx.add_window(|window, cx| {
3088 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3089 let mut editor = build_editor(buffer.clone(), window, cx);
3090 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3091 s.select_ranges([3..4, 11..12, 19..20])
3092 });
3093 editor
3094 });
3095
3096 _ = editor.update(cx, |editor, window, cx| {
3097 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3098 editor.buffer.update(cx, |buffer, cx| {
3099 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3100 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3101 });
3102 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3103
3104 editor.insert("Z", window, cx);
3105 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3106
3107 // The selections are moved after the inserted characters
3108 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3109 });
3110}
3111
3112#[gpui::test]
3113async fn test_tab(cx: &mut TestAppContext) {
3114 init_test(cx, |settings| {
3115 settings.defaults.tab_size = NonZeroU32::new(3)
3116 });
3117
3118 let mut cx = EditorTestContext::new(cx).await;
3119 cx.set_state(indoc! {"
3120 ˇabˇc
3121 ˇ🏀ˇ🏀ˇefg
3122 dˇ
3123 "});
3124 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3125 cx.assert_editor_state(indoc! {"
3126 ˇab ˇc
3127 ˇ🏀 ˇ🏀 ˇefg
3128 d ˇ
3129 "});
3130
3131 cx.set_state(indoc! {"
3132 a
3133 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3134 "});
3135 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3136 cx.assert_editor_state(indoc! {"
3137 a
3138 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3139 "});
3140}
3141
3142#[gpui::test]
3143async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3144 init_test(cx, |_| {});
3145
3146 let mut cx = EditorTestContext::new(cx).await;
3147 let language = Arc::new(
3148 Language::new(
3149 LanguageConfig::default(),
3150 Some(tree_sitter_rust::LANGUAGE.into()),
3151 )
3152 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3153 .unwrap(),
3154 );
3155 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3156
3157 // test when all cursors are not at suggested indent
3158 // then simply move to their suggested indent location
3159 cx.set_state(indoc! {"
3160 const a: B = (
3161 c(
3162 ˇ
3163 ˇ )
3164 );
3165 "});
3166 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3167 cx.assert_editor_state(indoc! {"
3168 const a: B = (
3169 c(
3170 ˇ
3171 ˇ)
3172 );
3173 "});
3174
3175 // test cursor already at suggested indent not moving when
3176 // other cursors are yet to reach their suggested indents
3177 cx.set_state(indoc! {"
3178 ˇ
3179 const a: B = (
3180 c(
3181 d(
3182 ˇ
3183 )
3184 ˇ
3185 ˇ )
3186 );
3187 "});
3188 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3189 cx.assert_editor_state(indoc! {"
3190 ˇ
3191 const a: B = (
3192 c(
3193 d(
3194 ˇ
3195 )
3196 ˇ
3197 ˇ)
3198 );
3199 "});
3200 // test when all cursors are at suggested indent then tab is inserted
3201 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3202 cx.assert_editor_state(indoc! {"
3203 ˇ
3204 const a: B = (
3205 c(
3206 d(
3207 ˇ
3208 )
3209 ˇ
3210 ˇ)
3211 );
3212 "});
3213
3214 // test when current indent is less than suggested indent,
3215 // we adjust line to match suggested indent and move cursor to it
3216 //
3217 // when no other cursor is at word boundary, all of them should move
3218 cx.set_state(indoc! {"
3219 const a: B = (
3220 c(
3221 d(
3222 ˇ
3223 ˇ )
3224 ˇ )
3225 );
3226 "});
3227 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3228 cx.assert_editor_state(indoc! {"
3229 const a: B = (
3230 c(
3231 d(
3232 ˇ
3233 ˇ)
3234 ˇ)
3235 );
3236 "});
3237
3238 // test when current indent is less than suggested indent,
3239 // we adjust line to match suggested indent and move cursor to it
3240 //
3241 // when some other cursor is at word boundary, it should not move
3242 cx.set_state(indoc! {"
3243 const a: B = (
3244 c(
3245 d(
3246 ˇ
3247 ˇ )
3248 ˇ)
3249 );
3250 "});
3251 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3252 cx.assert_editor_state(indoc! {"
3253 const a: B = (
3254 c(
3255 d(
3256 ˇ
3257 ˇ)
3258 ˇ)
3259 );
3260 "});
3261
3262 // test when current indent is more than suggested indent,
3263 // we just move cursor to current indent instead of suggested indent
3264 //
3265 // when no other cursor is at word boundary, all of them should move
3266 cx.set_state(indoc! {"
3267 const a: B = (
3268 c(
3269 d(
3270 ˇ
3271 ˇ )
3272 ˇ )
3273 );
3274 "});
3275 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3276 cx.assert_editor_state(indoc! {"
3277 const a: B = (
3278 c(
3279 d(
3280 ˇ
3281 ˇ)
3282 ˇ)
3283 );
3284 "});
3285 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3286 cx.assert_editor_state(indoc! {"
3287 const a: B = (
3288 c(
3289 d(
3290 ˇ
3291 ˇ)
3292 ˇ)
3293 );
3294 "});
3295
3296 // test when current indent is more than suggested indent,
3297 // we just move cursor to current indent instead of suggested indent
3298 //
3299 // when some other cursor is at word boundary, it doesn't move
3300 cx.set_state(indoc! {"
3301 const a: B = (
3302 c(
3303 d(
3304 ˇ
3305 ˇ )
3306 ˇ)
3307 );
3308 "});
3309 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3310 cx.assert_editor_state(indoc! {"
3311 const a: B = (
3312 c(
3313 d(
3314 ˇ
3315 ˇ)
3316 ˇ)
3317 );
3318 "});
3319
3320 // handle auto-indent when there are multiple cursors on the same line
3321 cx.set_state(indoc! {"
3322 const a: B = (
3323 c(
3324 ˇ ˇ
3325 ˇ )
3326 );
3327 "});
3328 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3329 cx.assert_editor_state(indoc! {"
3330 const a: B = (
3331 c(
3332 ˇ
3333 ˇ)
3334 );
3335 "});
3336}
3337
3338#[gpui::test]
3339async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3340 init_test(cx, |settings| {
3341 settings.defaults.tab_size = NonZeroU32::new(3)
3342 });
3343
3344 let mut cx = EditorTestContext::new(cx).await;
3345 cx.set_state(indoc! {"
3346 ˇ
3347 \t ˇ
3348 \t ˇ
3349 \t ˇ
3350 \t \t\t \t \t\t \t\t \t \t ˇ
3351 "});
3352
3353 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3354 cx.assert_editor_state(indoc! {"
3355 ˇ
3356 \t ˇ
3357 \t ˇ
3358 \t ˇ
3359 \t \t\t \t \t\t \t\t \t \t ˇ
3360 "});
3361}
3362
3363#[gpui::test]
3364async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3365 init_test(cx, |settings| {
3366 settings.defaults.tab_size = NonZeroU32::new(4)
3367 });
3368
3369 let language = Arc::new(
3370 Language::new(
3371 LanguageConfig::default(),
3372 Some(tree_sitter_rust::LANGUAGE.into()),
3373 )
3374 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3375 .unwrap(),
3376 );
3377
3378 let mut cx = EditorTestContext::new(cx).await;
3379 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3380 cx.set_state(indoc! {"
3381 fn a() {
3382 if b {
3383 \t ˇc
3384 }
3385 }
3386 "});
3387
3388 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3389 cx.assert_editor_state(indoc! {"
3390 fn a() {
3391 if b {
3392 ˇc
3393 }
3394 }
3395 "});
3396}
3397
3398#[gpui::test]
3399async fn test_indent_outdent(cx: &mut TestAppContext) {
3400 init_test(cx, |settings| {
3401 settings.defaults.tab_size = NonZeroU32::new(4);
3402 });
3403
3404 let mut cx = EditorTestContext::new(cx).await;
3405
3406 cx.set_state(indoc! {"
3407 «oneˇ» «twoˇ»
3408 three
3409 four
3410 "});
3411 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3412 cx.assert_editor_state(indoc! {"
3413 «oneˇ» «twoˇ»
3414 three
3415 four
3416 "});
3417
3418 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3419 cx.assert_editor_state(indoc! {"
3420 «oneˇ» «twoˇ»
3421 three
3422 four
3423 "});
3424
3425 // select across line ending
3426 cx.set_state(indoc! {"
3427 one two
3428 t«hree
3429 ˇ» four
3430 "});
3431 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3432 cx.assert_editor_state(indoc! {"
3433 one two
3434 t«hree
3435 ˇ» four
3436 "});
3437
3438 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3439 cx.assert_editor_state(indoc! {"
3440 one two
3441 t«hree
3442 ˇ» four
3443 "});
3444
3445 // Ensure that indenting/outdenting works when the cursor is at column 0.
3446 cx.set_state(indoc! {"
3447 one two
3448 ˇthree
3449 four
3450 "});
3451 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3452 cx.assert_editor_state(indoc! {"
3453 one two
3454 ˇthree
3455 four
3456 "});
3457
3458 cx.set_state(indoc! {"
3459 one two
3460 ˇ three
3461 four
3462 "});
3463 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3464 cx.assert_editor_state(indoc! {"
3465 one two
3466 ˇthree
3467 four
3468 "});
3469}
3470
3471#[gpui::test]
3472async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3473 init_test(cx, |settings| {
3474 settings.defaults.hard_tabs = Some(true);
3475 });
3476
3477 let mut cx = EditorTestContext::new(cx).await;
3478
3479 // select two ranges on one line
3480 cx.set_state(indoc! {"
3481 «oneˇ» «twoˇ»
3482 three
3483 four
3484 "});
3485 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3486 cx.assert_editor_state(indoc! {"
3487 \t«oneˇ» «twoˇ»
3488 three
3489 four
3490 "});
3491 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3492 cx.assert_editor_state(indoc! {"
3493 \t\t«oneˇ» «twoˇ»
3494 three
3495 four
3496 "});
3497 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3498 cx.assert_editor_state(indoc! {"
3499 \t«oneˇ» «twoˇ»
3500 three
3501 four
3502 "});
3503 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3504 cx.assert_editor_state(indoc! {"
3505 «oneˇ» «twoˇ»
3506 three
3507 four
3508 "});
3509
3510 // select across a line ending
3511 cx.set_state(indoc! {"
3512 one two
3513 t«hree
3514 ˇ»four
3515 "});
3516 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3517 cx.assert_editor_state(indoc! {"
3518 one two
3519 \tt«hree
3520 ˇ»four
3521 "});
3522 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3523 cx.assert_editor_state(indoc! {"
3524 one two
3525 \t\tt«hree
3526 ˇ»four
3527 "});
3528 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3529 cx.assert_editor_state(indoc! {"
3530 one two
3531 \tt«hree
3532 ˇ»four
3533 "});
3534 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3535 cx.assert_editor_state(indoc! {"
3536 one two
3537 t«hree
3538 ˇ»four
3539 "});
3540
3541 // Ensure that indenting/outdenting works when the cursor is at column 0.
3542 cx.set_state(indoc! {"
3543 one two
3544 ˇthree
3545 four
3546 "});
3547 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3548 cx.assert_editor_state(indoc! {"
3549 one two
3550 ˇthree
3551 four
3552 "});
3553 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3554 cx.assert_editor_state(indoc! {"
3555 one two
3556 \tˇthree
3557 four
3558 "});
3559 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3560 cx.assert_editor_state(indoc! {"
3561 one two
3562 ˇthree
3563 four
3564 "});
3565}
3566
3567#[gpui::test]
3568fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3569 init_test(cx, |settings| {
3570 settings.languages.0.extend([
3571 (
3572 "TOML".into(),
3573 LanguageSettingsContent {
3574 tab_size: NonZeroU32::new(2),
3575 ..Default::default()
3576 },
3577 ),
3578 (
3579 "Rust".into(),
3580 LanguageSettingsContent {
3581 tab_size: NonZeroU32::new(4),
3582 ..Default::default()
3583 },
3584 ),
3585 ]);
3586 });
3587
3588 let toml_language = Arc::new(Language::new(
3589 LanguageConfig {
3590 name: "TOML".into(),
3591 ..Default::default()
3592 },
3593 None,
3594 ));
3595 let rust_language = Arc::new(Language::new(
3596 LanguageConfig {
3597 name: "Rust".into(),
3598 ..Default::default()
3599 },
3600 None,
3601 ));
3602
3603 let toml_buffer =
3604 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3605 let rust_buffer =
3606 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3607 let multibuffer = cx.new(|cx| {
3608 let mut multibuffer = MultiBuffer::new(ReadWrite);
3609 multibuffer.push_excerpts(
3610 toml_buffer.clone(),
3611 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3612 cx,
3613 );
3614 multibuffer.push_excerpts(
3615 rust_buffer.clone(),
3616 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3617 cx,
3618 );
3619 multibuffer
3620 });
3621
3622 cx.add_window(|window, cx| {
3623 let mut editor = build_editor(multibuffer, window, cx);
3624
3625 assert_eq!(
3626 editor.text(cx),
3627 indoc! {"
3628 a = 1
3629 b = 2
3630
3631 const c: usize = 3;
3632 "}
3633 );
3634
3635 select_ranges(
3636 &mut editor,
3637 indoc! {"
3638 «aˇ» = 1
3639 b = 2
3640
3641 «const c:ˇ» usize = 3;
3642 "},
3643 window,
3644 cx,
3645 );
3646
3647 editor.tab(&Tab, window, cx);
3648 assert_text_with_selections(
3649 &mut editor,
3650 indoc! {"
3651 «aˇ» = 1
3652 b = 2
3653
3654 «const c:ˇ» usize = 3;
3655 "},
3656 cx,
3657 );
3658 editor.backtab(&Backtab, window, cx);
3659 assert_text_with_selections(
3660 &mut editor,
3661 indoc! {"
3662 «aˇ» = 1
3663 b = 2
3664
3665 «const c:ˇ» usize = 3;
3666 "},
3667 cx,
3668 );
3669
3670 editor
3671 });
3672}
3673
3674#[gpui::test]
3675async fn test_backspace(cx: &mut TestAppContext) {
3676 init_test(cx, |_| {});
3677
3678 let mut cx = EditorTestContext::new(cx).await;
3679
3680 // Basic backspace
3681 cx.set_state(indoc! {"
3682 onˇe two three
3683 fou«rˇ» five six
3684 seven «ˇeight nine
3685 »ten
3686 "});
3687 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3688 cx.assert_editor_state(indoc! {"
3689 oˇe two three
3690 fouˇ five six
3691 seven ˇten
3692 "});
3693
3694 // Test backspace inside and around indents
3695 cx.set_state(indoc! {"
3696 zero
3697 ˇone
3698 ˇtwo
3699 ˇ ˇ ˇ three
3700 ˇ ˇ four
3701 "});
3702 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3703 cx.assert_editor_state(indoc! {"
3704 zero
3705 ˇone
3706 ˇtwo
3707 ˇ threeˇ four
3708 "});
3709}
3710
3711#[gpui::test]
3712async fn test_delete(cx: &mut TestAppContext) {
3713 init_test(cx, |_| {});
3714
3715 let mut cx = EditorTestContext::new(cx).await;
3716 cx.set_state(indoc! {"
3717 onˇe two three
3718 fou«rˇ» five six
3719 seven «ˇeight nine
3720 »ten
3721 "});
3722 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3723 cx.assert_editor_state(indoc! {"
3724 onˇ two three
3725 fouˇ five six
3726 seven ˇten
3727 "});
3728}
3729
3730#[gpui::test]
3731fn test_delete_line(cx: &mut TestAppContext) {
3732 init_test(cx, |_| {});
3733
3734 let editor = cx.add_window(|window, cx| {
3735 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3736 build_editor(buffer, window, cx)
3737 });
3738 _ = editor.update(cx, |editor, window, cx| {
3739 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3740 s.select_display_ranges([
3741 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3742 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3743 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3744 ])
3745 });
3746 editor.delete_line(&DeleteLine, window, cx);
3747 assert_eq!(editor.display_text(cx), "ghi");
3748 assert_eq!(
3749 editor.selections.display_ranges(cx),
3750 vec![
3751 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3752 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3753 ]
3754 );
3755 });
3756
3757 let editor = cx.add_window(|window, cx| {
3758 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3759 build_editor(buffer, window, cx)
3760 });
3761 _ = editor.update(cx, |editor, window, cx| {
3762 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3763 s.select_display_ranges([
3764 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3765 ])
3766 });
3767 editor.delete_line(&DeleteLine, window, cx);
3768 assert_eq!(editor.display_text(cx), "ghi\n");
3769 assert_eq!(
3770 editor.selections.display_ranges(cx),
3771 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3772 );
3773 });
3774}
3775
3776#[gpui::test]
3777fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3778 init_test(cx, |_| {});
3779
3780 cx.add_window(|window, cx| {
3781 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3782 let mut editor = build_editor(buffer.clone(), window, cx);
3783 let buffer = buffer.read(cx).as_singleton().unwrap();
3784
3785 assert_eq!(
3786 editor.selections.ranges::<Point>(cx),
3787 &[Point::new(0, 0)..Point::new(0, 0)]
3788 );
3789
3790 // When on single line, replace newline at end by space
3791 editor.join_lines(&JoinLines, window, cx);
3792 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3793 assert_eq!(
3794 editor.selections.ranges::<Point>(cx),
3795 &[Point::new(0, 3)..Point::new(0, 3)]
3796 );
3797
3798 // When multiple lines are selected, remove newlines that are spanned by the selection
3799 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3800 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3801 });
3802 editor.join_lines(&JoinLines, window, cx);
3803 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3804 assert_eq!(
3805 editor.selections.ranges::<Point>(cx),
3806 &[Point::new(0, 11)..Point::new(0, 11)]
3807 );
3808
3809 // Undo should be transactional
3810 editor.undo(&Undo, window, cx);
3811 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3812 assert_eq!(
3813 editor.selections.ranges::<Point>(cx),
3814 &[Point::new(0, 5)..Point::new(2, 2)]
3815 );
3816
3817 // When joining an empty line don't insert a space
3818 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3819 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3820 });
3821 editor.join_lines(&JoinLines, window, cx);
3822 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3823 assert_eq!(
3824 editor.selections.ranges::<Point>(cx),
3825 [Point::new(2, 3)..Point::new(2, 3)]
3826 );
3827
3828 // We can remove trailing newlines
3829 editor.join_lines(&JoinLines, window, cx);
3830 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3831 assert_eq!(
3832 editor.selections.ranges::<Point>(cx),
3833 [Point::new(2, 3)..Point::new(2, 3)]
3834 );
3835
3836 // We don't blow up on the last line
3837 editor.join_lines(&JoinLines, window, cx);
3838 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3839 assert_eq!(
3840 editor.selections.ranges::<Point>(cx),
3841 [Point::new(2, 3)..Point::new(2, 3)]
3842 );
3843
3844 // reset to test indentation
3845 editor.buffer.update(cx, |buffer, cx| {
3846 buffer.edit(
3847 [
3848 (Point::new(1, 0)..Point::new(1, 2), " "),
3849 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3850 ],
3851 None,
3852 cx,
3853 )
3854 });
3855
3856 // We remove any leading spaces
3857 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3858 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3859 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3860 });
3861 editor.join_lines(&JoinLines, window, cx);
3862 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3863
3864 // We don't insert a space for a line containing only spaces
3865 editor.join_lines(&JoinLines, window, cx);
3866 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3867
3868 // We ignore any leading tabs
3869 editor.join_lines(&JoinLines, window, cx);
3870 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3871
3872 editor
3873 });
3874}
3875
3876#[gpui::test]
3877fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3878 init_test(cx, |_| {});
3879
3880 cx.add_window(|window, cx| {
3881 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3882 let mut editor = build_editor(buffer.clone(), window, cx);
3883 let buffer = buffer.read(cx).as_singleton().unwrap();
3884
3885 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3886 s.select_ranges([
3887 Point::new(0, 2)..Point::new(1, 1),
3888 Point::new(1, 2)..Point::new(1, 2),
3889 Point::new(3, 1)..Point::new(3, 2),
3890 ])
3891 });
3892
3893 editor.join_lines(&JoinLines, window, cx);
3894 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3895
3896 assert_eq!(
3897 editor.selections.ranges::<Point>(cx),
3898 [
3899 Point::new(0, 7)..Point::new(0, 7),
3900 Point::new(1, 3)..Point::new(1, 3)
3901 ]
3902 );
3903 editor
3904 });
3905}
3906
3907#[gpui::test]
3908async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3909 init_test(cx, |_| {});
3910
3911 let mut cx = EditorTestContext::new(cx).await;
3912
3913 let diff_base = r#"
3914 Line 0
3915 Line 1
3916 Line 2
3917 Line 3
3918 "#
3919 .unindent();
3920
3921 cx.set_state(
3922 &r#"
3923 ˇLine 0
3924 Line 1
3925 Line 2
3926 Line 3
3927 "#
3928 .unindent(),
3929 );
3930
3931 cx.set_head_text(&diff_base);
3932 executor.run_until_parked();
3933
3934 // Join lines
3935 cx.update_editor(|editor, window, cx| {
3936 editor.join_lines(&JoinLines, window, cx);
3937 });
3938 executor.run_until_parked();
3939
3940 cx.assert_editor_state(
3941 &r#"
3942 Line 0ˇ Line 1
3943 Line 2
3944 Line 3
3945 "#
3946 .unindent(),
3947 );
3948 // Join again
3949 cx.update_editor(|editor, window, cx| {
3950 editor.join_lines(&JoinLines, window, cx);
3951 });
3952 executor.run_until_parked();
3953
3954 cx.assert_editor_state(
3955 &r#"
3956 Line 0 Line 1ˇ Line 2
3957 Line 3
3958 "#
3959 .unindent(),
3960 );
3961}
3962
3963#[gpui::test]
3964async fn test_custom_newlines_cause_no_false_positive_diffs(
3965 executor: BackgroundExecutor,
3966 cx: &mut TestAppContext,
3967) {
3968 init_test(cx, |_| {});
3969 let mut cx = EditorTestContext::new(cx).await;
3970 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3971 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3972 executor.run_until_parked();
3973
3974 cx.update_editor(|editor, window, cx| {
3975 let snapshot = editor.snapshot(window, cx);
3976 assert_eq!(
3977 snapshot
3978 .buffer_snapshot
3979 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3980 .collect::<Vec<_>>(),
3981 Vec::new(),
3982 "Should not have any diffs for files with custom newlines"
3983 );
3984 });
3985}
3986
3987#[gpui::test]
3988async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
3989 init_test(cx, |_| {});
3990
3991 let mut cx = EditorTestContext::new(cx).await;
3992
3993 // Test sort_lines_case_insensitive()
3994 cx.set_state(indoc! {"
3995 «z
3996 y
3997 x
3998 Z
3999 Y
4000 Xˇ»
4001 "});
4002 cx.update_editor(|e, window, cx| {
4003 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4004 });
4005 cx.assert_editor_state(indoc! {"
4006 «x
4007 X
4008 y
4009 Y
4010 z
4011 Zˇ»
4012 "});
4013
4014 // Test reverse_lines()
4015 cx.set_state(indoc! {"
4016 «5
4017 4
4018 3
4019 2
4020 1ˇ»
4021 "});
4022 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4023 cx.assert_editor_state(indoc! {"
4024 «1
4025 2
4026 3
4027 4
4028 5ˇ»
4029 "});
4030
4031 // Skip testing shuffle_line()
4032
4033 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4034 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4035
4036 // Don't manipulate when cursor is on single line, but expand the selection
4037 cx.set_state(indoc! {"
4038 ddˇdd
4039 ccc
4040 bb
4041 a
4042 "});
4043 cx.update_editor(|e, window, cx| {
4044 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4045 });
4046 cx.assert_editor_state(indoc! {"
4047 «ddddˇ»
4048 ccc
4049 bb
4050 a
4051 "});
4052
4053 // Basic manipulate case
4054 // Start selection moves to column 0
4055 // End of selection shrinks to fit shorter line
4056 cx.set_state(indoc! {"
4057 dd«d
4058 ccc
4059 bb
4060 aaaaaˇ»
4061 "});
4062 cx.update_editor(|e, window, cx| {
4063 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4064 });
4065 cx.assert_editor_state(indoc! {"
4066 «aaaaa
4067 bb
4068 ccc
4069 dddˇ»
4070 "});
4071
4072 // Manipulate case with newlines
4073 cx.set_state(indoc! {"
4074 dd«d
4075 ccc
4076
4077 bb
4078 aaaaa
4079
4080 ˇ»
4081 "});
4082 cx.update_editor(|e, window, cx| {
4083 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4084 });
4085 cx.assert_editor_state(indoc! {"
4086 «
4087
4088 aaaaa
4089 bb
4090 ccc
4091 dddˇ»
4092
4093 "});
4094
4095 // Adding new line
4096 cx.set_state(indoc! {"
4097 aa«a
4098 bbˇ»b
4099 "});
4100 cx.update_editor(|e, window, cx| {
4101 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4102 });
4103 cx.assert_editor_state(indoc! {"
4104 «aaa
4105 bbb
4106 added_lineˇ»
4107 "});
4108
4109 // Removing line
4110 cx.set_state(indoc! {"
4111 aa«a
4112 bbbˇ»
4113 "});
4114 cx.update_editor(|e, window, cx| {
4115 e.manipulate_immutable_lines(window, cx, |lines| {
4116 lines.pop();
4117 })
4118 });
4119 cx.assert_editor_state(indoc! {"
4120 «aaaˇ»
4121 "});
4122
4123 // Removing all lines
4124 cx.set_state(indoc! {"
4125 aa«a
4126 bbbˇ»
4127 "});
4128 cx.update_editor(|e, window, cx| {
4129 e.manipulate_immutable_lines(window, cx, |lines| {
4130 lines.drain(..);
4131 })
4132 });
4133 cx.assert_editor_state(indoc! {"
4134 ˇ
4135 "});
4136}
4137
4138#[gpui::test]
4139async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4140 init_test(cx, |_| {});
4141
4142 let mut cx = EditorTestContext::new(cx).await;
4143
4144 // Consider continuous selection as single selection
4145 cx.set_state(indoc! {"
4146 Aaa«aa
4147 cˇ»c«c
4148 bb
4149 aaaˇ»aa
4150 "});
4151 cx.update_editor(|e, window, cx| {
4152 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4153 });
4154 cx.assert_editor_state(indoc! {"
4155 «Aaaaa
4156 ccc
4157 bb
4158 aaaaaˇ»
4159 "});
4160
4161 cx.set_state(indoc! {"
4162 Aaa«aa
4163 cˇ»c«c
4164 bb
4165 aaaˇ»aa
4166 "});
4167 cx.update_editor(|e, window, cx| {
4168 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4169 });
4170 cx.assert_editor_state(indoc! {"
4171 «Aaaaa
4172 ccc
4173 bbˇ»
4174 "});
4175
4176 // Consider non continuous selection as distinct dedup operations
4177 cx.set_state(indoc! {"
4178 «aaaaa
4179 bb
4180 aaaaa
4181 aaaaaˇ»
4182
4183 aaa«aaˇ»
4184 "});
4185 cx.update_editor(|e, window, cx| {
4186 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4187 });
4188 cx.assert_editor_state(indoc! {"
4189 «aaaaa
4190 bbˇ»
4191
4192 «aaaaaˇ»
4193 "});
4194}
4195
4196#[gpui::test]
4197async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4198 init_test(cx, |_| {});
4199
4200 let mut cx = EditorTestContext::new(cx).await;
4201
4202 cx.set_state(indoc! {"
4203 «Aaa
4204 aAa
4205 Aaaˇ»
4206 "});
4207 cx.update_editor(|e, window, cx| {
4208 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4209 });
4210 cx.assert_editor_state(indoc! {"
4211 «Aaa
4212 aAaˇ»
4213 "});
4214
4215 cx.set_state(indoc! {"
4216 «Aaa
4217 aAa
4218 aaAˇ»
4219 "});
4220 cx.update_editor(|e, window, cx| {
4221 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4222 });
4223 cx.assert_editor_state(indoc! {"
4224 «Aaaˇ»
4225 "});
4226}
4227
4228#[gpui::test]
4229async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4230 init_test(cx, |_| {});
4231
4232 let mut cx = EditorTestContext::new(cx).await;
4233
4234 // Manipulate with multiple selections on a single line
4235 cx.set_state(indoc! {"
4236 dd«dd
4237 cˇ»c«c
4238 bb
4239 aaaˇ»aa
4240 "});
4241 cx.update_editor(|e, window, cx| {
4242 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4243 });
4244 cx.assert_editor_state(indoc! {"
4245 «aaaaa
4246 bb
4247 ccc
4248 ddddˇ»
4249 "});
4250
4251 // Manipulate with multiple disjoin selections
4252 cx.set_state(indoc! {"
4253 5«
4254 4
4255 3
4256 2
4257 1ˇ»
4258
4259 dd«dd
4260 ccc
4261 bb
4262 aaaˇ»aa
4263 "});
4264 cx.update_editor(|e, window, cx| {
4265 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4266 });
4267 cx.assert_editor_state(indoc! {"
4268 «1
4269 2
4270 3
4271 4
4272 5ˇ»
4273
4274 «aaaaa
4275 bb
4276 ccc
4277 ddddˇ»
4278 "});
4279
4280 // Adding lines on each selection
4281 cx.set_state(indoc! {"
4282 2«
4283 1ˇ»
4284
4285 bb«bb
4286 aaaˇ»aa
4287 "});
4288 cx.update_editor(|e, window, cx| {
4289 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4290 });
4291 cx.assert_editor_state(indoc! {"
4292 «2
4293 1
4294 added lineˇ»
4295
4296 «bbbb
4297 aaaaa
4298 added lineˇ»
4299 "});
4300
4301 // Removing lines on each selection
4302 cx.set_state(indoc! {"
4303 2«
4304 1ˇ»
4305
4306 bb«bb
4307 aaaˇ»aa
4308 "});
4309 cx.update_editor(|e, window, cx| {
4310 e.manipulate_immutable_lines(window, cx, |lines| {
4311 lines.pop();
4312 })
4313 });
4314 cx.assert_editor_state(indoc! {"
4315 «2ˇ»
4316
4317 «bbbbˇ»
4318 "});
4319}
4320
4321#[gpui::test]
4322async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4323 init_test(cx, |settings| {
4324 settings.defaults.tab_size = NonZeroU32::new(3)
4325 });
4326
4327 let mut cx = EditorTestContext::new(cx).await;
4328
4329 // MULTI SELECTION
4330 // Ln.1 "«" tests empty lines
4331 // Ln.9 tests just leading whitespace
4332 cx.set_state(indoc! {"
4333 «
4334 abc // No indentationˇ»
4335 «\tabc // 1 tabˇ»
4336 \t\tabc « ˇ» // 2 tabs
4337 \t ab«c // Tab followed by space
4338 \tabc // Space followed by tab (3 spaces should be the result)
4339 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4340 abˇ»ˇc ˇ ˇ // Already space indented«
4341 \t
4342 \tabc\tdef // Only the leading tab is manipulatedˇ»
4343 "});
4344 cx.update_editor(|e, window, cx| {
4345 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4346 });
4347 cx.assert_editor_state(
4348 indoc! {"
4349 «
4350 abc // No indentation
4351 abc // 1 tab
4352 abc // 2 tabs
4353 abc // Tab followed by space
4354 abc // Space followed by tab (3 spaces should be the result)
4355 abc // Mixed indentation (tab conversion depends on the column)
4356 abc // Already space indented
4357 ·
4358 abc\tdef // Only the leading tab is manipulatedˇ»
4359 "}
4360 .replace("·", "")
4361 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4362 );
4363
4364 // Test on just a few lines, the others should remain unchanged
4365 // Only lines (3, 5, 10, 11) should change
4366 cx.set_state(
4367 indoc! {"
4368 ·
4369 abc // No indentation
4370 \tabcˇ // 1 tab
4371 \t\tabc // 2 tabs
4372 \t abcˇ // Tab followed by space
4373 \tabc // Space followed by tab (3 spaces should be the result)
4374 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4375 abc // Already space indented
4376 «\t
4377 \tabc\tdef // Only the leading tab is manipulatedˇ»
4378 "}
4379 .replace("·", "")
4380 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4381 );
4382 cx.update_editor(|e, window, cx| {
4383 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4384 });
4385 cx.assert_editor_state(
4386 indoc! {"
4387 ·
4388 abc // No indentation
4389 « abc // 1 tabˇ»
4390 \t\tabc // 2 tabs
4391 « abc // Tab followed by spaceˇ»
4392 \tabc // Space followed by tab (3 spaces should be the result)
4393 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4394 abc // Already space indented
4395 « ·
4396 abc\tdef // Only the leading tab is manipulatedˇ»
4397 "}
4398 .replace("·", "")
4399 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4400 );
4401
4402 // SINGLE SELECTION
4403 // Ln.1 "«" tests empty lines
4404 // Ln.9 tests just leading whitespace
4405 cx.set_state(indoc! {"
4406 «
4407 abc // No indentation
4408 \tabc // 1 tab
4409 \t\tabc // 2 tabs
4410 \t abc // Tab followed by space
4411 \tabc // Space followed by tab (3 spaces should be the result)
4412 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4413 abc // Already space indented
4414 \t
4415 \tabc\tdef // Only the leading tab is manipulatedˇ»
4416 "});
4417 cx.update_editor(|e, window, cx| {
4418 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4419 });
4420 cx.assert_editor_state(
4421 indoc! {"
4422 «
4423 abc // No indentation
4424 abc // 1 tab
4425 abc // 2 tabs
4426 abc // Tab followed by space
4427 abc // Space followed by tab (3 spaces should be the result)
4428 abc // Mixed indentation (tab conversion depends on the column)
4429 abc // Already space indented
4430 ·
4431 abc\tdef // Only the leading tab is manipulatedˇ»
4432 "}
4433 .replace("·", "")
4434 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4435 );
4436}
4437
4438#[gpui::test]
4439async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
4440 init_test(cx, |settings| {
4441 settings.defaults.tab_size = NonZeroU32::new(3)
4442 });
4443
4444 let mut cx = EditorTestContext::new(cx).await;
4445
4446 // MULTI SELECTION
4447 // Ln.1 "«" tests empty lines
4448 // Ln.11 tests just leading whitespace
4449 cx.set_state(indoc! {"
4450 «
4451 abˇ»ˇc // No indentation
4452 abc ˇ ˇ // 1 space (< 3 so dont convert)
4453 abc « // 2 spaces (< 3 so dont convert)
4454 abc // 3 spaces (convert)
4455 abc ˇ» // 5 spaces (1 tab + 2 spaces)
4456 «\tˇ»\t«\tˇ»abc // Already tab indented
4457 «\t abc // Tab followed by space
4458 \tabc // Space followed by tab (should be consumed due to tab)
4459 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4460 \tˇ» «\t
4461 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
4462 "});
4463 cx.update_editor(|e, window, cx| {
4464 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4465 });
4466 cx.assert_editor_state(indoc! {"
4467 «
4468 abc // No indentation
4469 abc // 1 space (< 3 so dont convert)
4470 abc // 2 spaces (< 3 so dont convert)
4471 \tabc // 3 spaces (convert)
4472 \t abc // 5 spaces (1 tab + 2 spaces)
4473 \t\t\tabc // Already tab indented
4474 \t abc // Tab followed by space
4475 \tabc // Space followed by tab (should be consumed due to tab)
4476 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4477 \t\t\t
4478 \tabc \t // Only the leading spaces should be convertedˇ»
4479 "});
4480
4481 // Test on just a few lines, the other should remain unchanged
4482 // Only lines (4, 8, 11, 12) should change
4483 cx.set_state(
4484 indoc! {"
4485 ·
4486 abc // No indentation
4487 abc // 1 space (< 3 so dont convert)
4488 abc // 2 spaces (< 3 so dont convert)
4489 « abc // 3 spaces (convert)ˇ»
4490 abc // 5 spaces (1 tab + 2 spaces)
4491 \t\t\tabc // Already tab indented
4492 \t abc // Tab followed by space
4493 \tabc ˇ // Space followed by tab (should be consumed due to tab)
4494 \t\t \tabc // Mixed indentation
4495 \t \t \t \tabc // Mixed indentation
4496 \t \tˇ
4497 « abc \t // Only the leading spaces should be convertedˇ»
4498 "}
4499 .replace("·", "")
4500 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4501 );
4502 cx.update_editor(|e, window, cx| {
4503 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4504 });
4505 cx.assert_editor_state(
4506 indoc! {"
4507 ·
4508 abc // No indentation
4509 abc // 1 space (< 3 so dont convert)
4510 abc // 2 spaces (< 3 so dont convert)
4511 «\tabc // 3 spaces (convert)ˇ»
4512 abc // 5 spaces (1 tab + 2 spaces)
4513 \t\t\tabc // Already tab indented
4514 \t abc // Tab followed by space
4515 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
4516 \t\t \tabc // Mixed indentation
4517 \t \t \t \tabc // Mixed indentation
4518 «\t\t\t
4519 \tabc \t // Only the leading spaces should be convertedˇ»
4520 "}
4521 .replace("·", "")
4522 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4523 );
4524
4525 // SINGLE SELECTION
4526 // Ln.1 "«" tests empty lines
4527 // Ln.11 tests just leading whitespace
4528 cx.set_state(indoc! {"
4529 «
4530 abc // No indentation
4531 abc // 1 space (< 3 so dont convert)
4532 abc // 2 spaces (< 3 so dont convert)
4533 abc // 3 spaces (convert)
4534 abc // 5 spaces (1 tab + 2 spaces)
4535 \t\t\tabc // Already tab indented
4536 \t abc // Tab followed by space
4537 \tabc // Space followed by tab (should be consumed due to tab)
4538 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4539 \t \t
4540 abc \t // Only the leading spaces should be convertedˇ»
4541 "});
4542 cx.update_editor(|e, window, cx| {
4543 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4544 });
4545 cx.assert_editor_state(indoc! {"
4546 «
4547 abc // No indentation
4548 abc // 1 space (< 3 so dont convert)
4549 abc // 2 spaces (< 3 so dont convert)
4550 \tabc // 3 spaces (convert)
4551 \t abc // 5 spaces (1 tab + 2 spaces)
4552 \t\t\tabc // Already tab indented
4553 \t abc // Tab followed by space
4554 \tabc // Space followed by tab (should be consumed due to tab)
4555 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4556 \t\t\t
4557 \tabc \t // Only the leading spaces should be convertedˇ»
4558 "});
4559}
4560
4561#[gpui::test]
4562async fn test_toggle_case(cx: &mut TestAppContext) {
4563 init_test(cx, |_| {});
4564
4565 let mut cx = EditorTestContext::new(cx).await;
4566
4567 // If all lower case -> upper case
4568 cx.set_state(indoc! {"
4569 «hello worldˇ»
4570 "});
4571 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4572 cx.assert_editor_state(indoc! {"
4573 «HELLO WORLDˇ»
4574 "});
4575
4576 // If all upper case -> lower case
4577 cx.set_state(indoc! {"
4578 «HELLO WORLDˇ»
4579 "});
4580 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4581 cx.assert_editor_state(indoc! {"
4582 «hello worldˇ»
4583 "});
4584
4585 // If any upper case characters are identified -> lower case
4586 // This matches JetBrains IDEs
4587 cx.set_state(indoc! {"
4588 «hEllo worldˇ»
4589 "});
4590 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4591 cx.assert_editor_state(indoc! {"
4592 «hello worldˇ»
4593 "});
4594}
4595
4596#[gpui::test]
4597async fn test_manipulate_text(cx: &mut TestAppContext) {
4598 init_test(cx, |_| {});
4599
4600 let mut cx = EditorTestContext::new(cx).await;
4601
4602 // Test convert_to_upper_case()
4603 cx.set_state(indoc! {"
4604 «hello worldˇ»
4605 "});
4606 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4607 cx.assert_editor_state(indoc! {"
4608 «HELLO WORLDˇ»
4609 "});
4610
4611 // Test convert_to_lower_case()
4612 cx.set_state(indoc! {"
4613 «HELLO WORLDˇ»
4614 "});
4615 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4616 cx.assert_editor_state(indoc! {"
4617 «hello worldˇ»
4618 "});
4619
4620 // Test multiple line, single selection case
4621 cx.set_state(indoc! {"
4622 «The quick brown
4623 fox jumps over
4624 the lazy dogˇ»
4625 "});
4626 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4627 cx.assert_editor_state(indoc! {"
4628 «The Quick Brown
4629 Fox Jumps Over
4630 The Lazy Dogˇ»
4631 "});
4632
4633 // Test multiple line, single selection case
4634 cx.set_state(indoc! {"
4635 «The quick brown
4636 fox jumps over
4637 the lazy dogˇ»
4638 "});
4639 cx.update_editor(|e, window, cx| {
4640 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4641 });
4642 cx.assert_editor_state(indoc! {"
4643 «TheQuickBrown
4644 FoxJumpsOver
4645 TheLazyDogˇ»
4646 "});
4647
4648 // From here on out, test more complex cases of manipulate_text()
4649
4650 // Test no selection case - should affect words cursors are in
4651 // Cursor at beginning, middle, and end of word
4652 cx.set_state(indoc! {"
4653 ˇhello big beauˇtiful worldˇ
4654 "});
4655 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4656 cx.assert_editor_state(indoc! {"
4657 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4658 "});
4659
4660 // Test multiple selections on a single line and across multiple lines
4661 cx.set_state(indoc! {"
4662 «Theˇ» quick «brown
4663 foxˇ» jumps «overˇ»
4664 the «lazyˇ» dog
4665 "});
4666 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4667 cx.assert_editor_state(indoc! {"
4668 «THEˇ» quick «BROWN
4669 FOXˇ» jumps «OVERˇ»
4670 the «LAZYˇ» dog
4671 "});
4672
4673 // Test case where text length grows
4674 cx.set_state(indoc! {"
4675 «tschüߡ»
4676 "});
4677 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4678 cx.assert_editor_state(indoc! {"
4679 «TSCHÜSSˇ»
4680 "});
4681
4682 // Test to make sure we don't crash when text shrinks
4683 cx.set_state(indoc! {"
4684 aaa_bbbˇ
4685 "});
4686 cx.update_editor(|e, window, cx| {
4687 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4688 });
4689 cx.assert_editor_state(indoc! {"
4690 «aaaBbbˇ»
4691 "});
4692
4693 // Test to make sure we all aware of the fact that each word can grow and shrink
4694 // Final selections should be aware of this fact
4695 cx.set_state(indoc! {"
4696 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4697 "});
4698 cx.update_editor(|e, window, cx| {
4699 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4700 });
4701 cx.assert_editor_state(indoc! {"
4702 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4703 "});
4704
4705 cx.set_state(indoc! {"
4706 «hElLo, WoRld!ˇ»
4707 "});
4708 cx.update_editor(|e, window, cx| {
4709 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4710 });
4711 cx.assert_editor_state(indoc! {"
4712 «HeLlO, wOrLD!ˇ»
4713 "});
4714}
4715
4716#[gpui::test]
4717fn test_duplicate_line(cx: &mut TestAppContext) {
4718 init_test(cx, |_| {});
4719
4720 let editor = cx.add_window(|window, cx| {
4721 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4722 build_editor(buffer, window, cx)
4723 });
4724 _ = editor.update(cx, |editor, window, cx| {
4725 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4726 s.select_display_ranges([
4727 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4728 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4729 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4730 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4731 ])
4732 });
4733 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4734 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4735 assert_eq!(
4736 editor.selections.display_ranges(cx),
4737 vec![
4738 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4739 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4740 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4741 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4742 ]
4743 );
4744 });
4745
4746 let editor = cx.add_window(|window, cx| {
4747 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4748 build_editor(buffer, window, cx)
4749 });
4750 _ = editor.update(cx, |editor, window, cx| {
4751 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4752 s.select_display_ranges([
4753 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4754 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4755 ])
4756 });
4757 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4758 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4759 assert_eq!(
4760 editor.selections.display_ranges(cx),
4761 vec![
4762 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4763 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4764 ]
4765 );
4766 });
4767
4768 // With `move_upwards` the selections stay in place, except for
4769 // the lines inserted above them
4770 let editor = cx.add_window(|window, cx| {
4771 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4772 build_editor(buffer, window, cx)
4773 });
4774 _ = editor.update(cx, |editor, window, cx| {
4775 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4776 s.select_display_ranges([
4777 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4778 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4779 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4780 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4781 ])
4782 });
4783 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4784 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4785 assert_eq!(
4786 editor.selections.display_ranges(cx),
4787 vec![
4788 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4789 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4790 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4791 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4792 ]
4793 );
4794 });
4795
4796 let editor = cx.add_window(|window, cx| {
4797 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4798 build_editor(buffer, window, cx)
4799 });
4800 _ = editor.update(cx, |editor, window, cx| {
4801 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4802 s.select_display_ranges([
4803 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4804 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4805 ])
4806 });
4807 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4808 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4809 assert_eq!(
4810 editor.selections.display_ranges(cx),
4811 vec![
4812 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4813 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4814 ]
4815 );
4816 });
4817
4818 let editor = cx.add_window(|window, cx| {
4819 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4820 build_editor(buffer, window, cx)
4821 });
4822 _ = editor.update(cx, |editor, window, cx| {
4823 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4824 s.select_display_ranges([
4825 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4826 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4827 ])
4828 });
4829 editor.duplicate_selection(&DuplicateSelection, window, cx);
4830 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4831 assert_eq!(
4832 editor.selections.display_ranges(cx),
4833 vec![
4834 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4835 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4836 ]
4837 );
4838 });
4839}
4840
4841#[gpui::test]
4842fn test_move_line_up_down(cx: &mut TestAppContext) {
4843 init_test(cx, |_| {});
4844
4845 let editor = cx.add_window(|window, cx| {
4846 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4847 build_editor(buffer, window, cx)
4848 });
4849 _ = editor.update(cx, |editor, window, cx| {
4850 editor.fold_creases(
4851 vec![
4852 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4853 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4854 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4855 ],
4856 true,
4857 window,
4858 cx,
4859 );
4860 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4861 s.select_display_ranges([
4862 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4863 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4864 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4865 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4866 ])
4867 });
4868 assert_eq!(
4869 editor.display_text(cx),
4870 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4871 );
4872
4873 editor.move_line_up(&MoveLineUp, window, cx);
4874 assert_eq!(
4875 editor.display_text(cx),
4876 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4877 );
4878 assert_eq!(
4879 editor.selections.display_ranges(cx),
4880 vec![
4881 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4882 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4883 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4884 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4885 ]
4886 );
4887 });
4888
4889 _ = editor.update(cx, |editor, window, cx| {
4890 editor.move_line_down(&MoveLineDown, window, cx);
4891 assert_eq!(
4892 editor.display_text(cx),
4893 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4894 );
4895 assert_eq!(
4896 editor.selections.display_ranges(cx),
4897 vec![
4898 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4899 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4900 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4901 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4902 ]
4903 );
4904 });
4905
4906 _ = editor.update(cx, |editor, window, cx| {
4907 editor.move_line_down(&MoveLineDown, window, cx);
4908 assert_eq!(
4909 editor.display_text(cx),
4910 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4911 );
4912 assert_eq!(
4913 editor.selections.display_ranges(cx),
4914 vec![
4915 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4916 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4917 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4918 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4919 ]
4920 );
4921 });
4922
4923 _ = editor.update(cx, |editor, window, cx| {
4924 editor.move_line_up(&MoveLineUp, window, cx);
4925 assert_eq!(
4926 editor.display_text(cx),
4927 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4928 );
4929 assert_eq!(
4930 editor.selections.display_ranges(cx),
4931 vec![
4932 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4933 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4934 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4935 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4936 ]
4937 );
4938 });
4939}
4940
4941#[gpui::test]
4942fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4943 init_test(cx, |_| {});
4944
4945 let editor = cx.add_window(|window, cx| {
4946 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4947 build_editor(buffer, window, cx)
4948 });
4949 _ = editor.update(cx, |editor, window, cx| {
4950 let snapshot = editor.buffer.read(cx).snapshot(cx);
4951 editor.insert_blocks(
4952 [BlockProperties {
4953 style: BlockStyle::Fixed,
4954 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4955 height: Some(1),
4956 render: Arc::new(|_| div().into_any()),
4957 priority: 0,
4958 render_in_minimap: true,
4959 }],
4960 Some(Autoscroll::fit()),
4961 cx,
4962 );
4963 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4964 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4965 });
4966 editor.move_line_down(&MoveLineDown, window, cx);
4967 });
4968}
4969
4970#[gpui::test]
4971async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4972 init_test(cx, |_| {});
4973
4974 let mut cx = EditorTestContext::new(cx).await;
4975 cx.set_state(
4976 &"
4977 ˇzero
4978 one
4979 two
4980 three
4981 four
4982 five
4983 "
4984 .unindent(),
4985 );
4986
4987 // Create a four-line block that replaces three lines of text.
4988 cx.update_editor(|editor, window, cx| {
4989 let snapshot = editor.snapshot(window, cx);
4990 let snapshot = &snapshot.buffer_snapshot;
4991 let placement = BlockPlacement::Replace(
4992 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4993 );
4994 editor.insert_blocks(
4995 [BlockProperties {
4996 placement,
4997 height: Some(4),
4998 style: BlockStyle::Sticky,
4999 render: Arc::new(|_| gpui::div().into_any_element()),
5000 priority: 0,
5001 render_in_minimap: true,
5002 }],
5003 None,
5004 cx,
5005 );
5006 });
5007
5008 // Move down so that the cursor touches the block.
5009 cx.update_editor(|editor, window, cx| {
5010 editor.move_down(&Default::default(), window, cx);
5011 });
5012 cx.assert_editor_state(
5013 &"
5014 zero
5015 «one
5016 two
5017 threeˇ»
5018 four
5019 five
5020 "
5021 .unindent(),
5022 );
5023
5024 // Move down past the block.
5025 cx.update_editor(|editor, window, cx| {
5026 editor.move_down(&Default::default(), window, cx);
5027 });
5028 cx.assert_editor_state(
5029 &"
5030 zero
5031 one
5032 two
5033 three
5034 ˇfour
5035 five
5036 "
5037 .unindent(),
5038 );
5039}
5040
5041#[gpui::test]
5042fn test_transpose(cx: &mut TestAppContext) {
5043 init_test(cx, |_| {});
5044
5045 _ = cx.add_window(|window, cx| {
5046 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5047 editor.set_style(EditorStyle::default(), window, cx);
5048 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5049 s.select_ranges([1..1])
5050 });
5051 editor.transpose(&Default::default(), window, cx);
5052 assert_eq!(editor.text(cx), "bac");
5053 assert_eq!(editor.selections.ranges(cx), [2..2]);
5054
5055 editor.transpose(&Default::default(), window, cx);
5056 assert_eq!(editor.text(cx), "bca");
5057 assert_eq!(editor.selections.ranges(cx), [3..3]);
5058
5059 editor.transpose(&Default::default(), window, cx);
5060 assert_eq!(editor.text(cx), "bac");
5061 assert_eq!(editor.selections.ranges(cx), [3..3]);
5062
5063 editor
5064 });
5065
5066 _ = cx.add_window(|window, cx| {
5067 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5068 editor.set_style(EditorStyle::default(), window, cx);
5069 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5070 s.select_ranges([3..3])
5071 });
5072 editor.transpose(&Default::default(), window, cx);
5073 assert_eq!(editor.text(cx), "acb\nde");
5074 assert_eq!(editor.selections.ranges(cx), [3..3]);
5075
5076 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5077 s.select_ranges([4..4])
5078 });
5079 editor.transpose(&Default::default(), window, cx);
5080 assert_eq!(editor.text(cx), "acbd\ne");
5081 assert_eq!(editor.selections.ranges(cx), [5..5]);
5082
5083 editor.transpose(&Default::default(), window, cx);
5084 assert_eq!(editor.text(cx), "acbde\n");
5085 assert_eq!(editor.selections.ranges(cx), [6..6]);
5086
5087 editor.transpose(&Default::default(), window, cx);
5088 assert_eq!(editor.text(cx), "acbd\ne");
5089 assert_eq!(editor.selections.ranges(cx), [6..6]);
5090
5091 editor
5092 });
5093
5094 _ = cx.add_window(|window, cx| {
5095 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5096 editor.set_style(EditorStyle::default(), window, cx);
5097 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5098 s.select_ranges([1..1, 2..2, 4..4])
5099 });
5100 editor.transpose(&Default::default(), window, cx);
5101 assert_eq!(editor.text(cx), "bacd\ne");
5102 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5103
5104 editor.transpose(&Default::default(), window, cx);
5105 assert_eq!(editor.text(cx), "bcade\n");
5106 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5107
5108 editor.transpose(&Default::default(), window, cx);
5109 assert_eq!(editor.text(cx), "bcda\ne");
5110 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5111
5112 editor.transpose(&Default::default(), window, cx);
5113 assert_eq!(editor.text(cx), "bcade\n");
5114 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5115
5116 editor.transpose(&Default::default(), window, cx);
5117 assert_eq!(editor.text(cx), "bcaed\n");
5118 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5119
5120 editor
5121 });
5122
5123 _ = cx.add_window(|window, cx| {
5124 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5125 editor.set_style(EditorStyle::default(), window, cx);
5126 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5127 s.select_ranges([4..4])
5128 });
5129 editor.transpose(&Default::default(), window, cx);
5130 assert_eq!(editor.text(cx), "🏀🍐✋");
5131 assert_eq!(editor.selections.ranges(cx), [8..8]);
5132
5133 editor.transpose(&Default::default(), window, cx);
5134 assert_eq!(editor.text(cx), "🏀✋🍐");
5135 assert_eq!(editor.selections.ranges(cx), [11..11]);
5136
5137 editor.transpose(&Default::default(), window, cx);
5138 assert_eq!(editor.text(cx), "🏀🍐✋");
5139 assert_eq!(editor.selections.ranges(cx), [11..11]);
5140
5141 editor
5142 });
5143}
5144
5145#[gpui::test]
5146async fn test_rewrap(cx: &mut TestAppContext) {
5147 init_test(cx, |settings| {
5148 settings.languages.0.extend([
5149 (
5150 "Markdown".into(),
5151 LanguageSettingsContent {
5152 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5153 preferred_line_length: Some(40),
5154 ..Default::default()
5155 },
5156 ),
5157 (
5158 "Plain Text".into(),
5159 LanguageSettingsContent {
5160 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5161 preferred_line_length: Some(40),
5162 ..Default::default()
5163 },
5164 ),
5165 (
5166 "C++".into(),
5167 LanguageSettingsContent {
5168 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5169 preferred_line_length: Some(40),
5170 ..Default::default()
5171 },
5172 ),
5173 (
5174 "Python".into(),
5175 LanguageSettingsContent {
5176 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5177 preferred_line_length: Some(40),
5178 ..Default::default()
5179 },
5180 ),
5181 (
5182 "Rust".into(),
5183 LanguageSettingsContent {
5184 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5185 preferred_line_length: Some(40),
5186 ..Default::default()
5187 },
5188 ),
5189 ])
5190 });
5191
5192 let mut cx = EditorTestContext::new(cx).await;
5193
5194 let cpp_language = Arc::new(Language::new(
5195 LanguageConfig {
5196 name: "C++".into(),
5197 line_comments: vec!["// ".into()],
5198 ..LanguageConfig::default()
5199 },
5200 None,
5201 ));
5202 let python_language = Arc::new(Language::new(
5203 LanguageConfig {
5204 name: "Python".into(),
5205 line_comments: vec!["# ".into()],
5206 ..LanguageConfig::default()
5207 },
5208 None,
5209 ));
5210 let markdown_language = Arc::new(Language::new(
5211 LanguageConfig {
5212 name: "Markdown".into(),
5213 rewrap_prefixes: vec![
5214 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5215 regex::Regex::new("[-*+]\\s+").unwrap(),
5216 ],
5217 ..LanguageConfig::default()
5218 },
5219 None,
5220 ));
5221 let rust_language = Arc::new(Language::new(
5222 LanguageConfig {
5223 name: "Rust".into(),
5224 line_comments: vec!["// ".into(), "/// ".into()],
5225 ..LanguageConfig::default()
5226 },
5227 Some(tree_sitter_rust::LANGUAGE.into()),
5228 ));
5229
5230 let plaintext_language = Arc::new(Language::new(
5231 LanguageConfig {
5232 name: "Plain Text".into(),
5233 ..LanguageConfig::default()
5234 },
5235 None,
5236 ));
5237
5238 // Test basic rewrapping of a long line with a cursor
5239 assert_rewrap(
5240 indoc! {"
5241 // ˇThis is a long comment that needs to be wrapped.
5242 "},
5243 indoc! {"
5244 // ˇThis is a long comment that needs to
5245 // be wrapped.
5246 "},
5247 cpp_language.clone(),
5248 &mut cx,
5249 );
5250
5251 // Test rewrapping a full selection
5252 assert_rewrap(
5253 indoc! {"
5254 «// This selected long comment needs to be wrapped.ˇ»"
5255 },
5256 indoc! {"
5257 «// This selected long comment needs to
5258 // be wrapped.ˇ»"
5259 },
5260 cpp_language.clone(),
5261 &mut cx,
5262 );
5263
5264 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5265 assert_rewrap(
5266 indoc! {"
5267 // ˇThis is the first line.
5268 // Thisˇ is the second line.
5269 // This is the thirdˇ line, all part of one paragraph.
5270 "},
5271 indoc! {"
5272 // ˇThis is the first line. Thisˇ is the
5273 // second line. This is the thirdˇ line,
5274 // all part of one paragraph.
5275 "},
5276 cpp_language.clone(),
5277 &mut cx,
5278 );
5279
5280 // Test multiple cursors in different paragraphs trigger separate rewraps
5281 assert_rewrap(
5282 indoc! {"
5283 // ˇThis is the first paragraph, first line.
5284 // ˇThis is the first paragraph, second line.
5285
5286 // ˇThis is the second paragraph, first line.
5287 // ˇThis is the second paragraph, second line.
5288 "},
5289 indoc! {"
5290 // ˇThis is the first paragraph, first
5291 // line. ˇThis is the first paragraph,
5292 // second line.
5293
5294 // ˇThis is the second paragraph, first
5295 // line. ˇThis is the second paragraph,
5296 // second line.
5297 "},
5298 cpp_language.clone(),
5299 &mut cx,
5300 );
5301
5302 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
5303 assert_rewrap(
5304 indoc! {"
5305 «// A regular long long comment to be wrapped.
5306 /// A documentation long comment to be wrapped.ˇ»
5307 "},
5308 indoc! {"
5309 «// A regular long long comment to be
5310 // wrapped.
5311 /// A documentation long comment to be
5312 /// wrapped.ˇ»
5313 "},
5314 rust_language.clone(),
5315 &mut cx,
5316 );
5317
5318 // Test that change in indentation level trigger seperate rewraps
5319 assert_rewrap(
5320 indoc! {"
5321 fn foo() {
5322 «// This is a long comment at the base indent.
5323 // This is a long comment at the next indent.ˇ»
5324 }
5325 "},
5326 indoc! {"
5327 fn foo() {
5328 «// This is a long comment at the
5329 // base indent.
5330 // This is a long comment at the
5331 // next indent.ˇ»
5332 }
5333 "},
5334 rust_language.clone(),
5335 &mut cx,
5336 );
5337
5338 // Test that different comment prefix characters (e.g., '#') are handled correctly
5339 assert_rewrap(
5340 indoc! {"
5341 # ˇThis is a long comment using a pound sign.
5342 "},
5343 indoc! {"
5344 # ˇThis is a long comment using a pound
5345 # sign.
5346 "},
5347 python_language.clone(),
5348 &mut cx,
5349 );
5350
5351 // Test rewrapping only affects comments, not code even when selected
5352 assert_rewrap(
5353 indoc! {"
5354 «/// This doc comment is long and should be wrapped.
5355 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5356 "},
5357 indoc! {"
5358 «/// This doc comment is long and should
5359 /// be wrapped.
5360 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5361 "},
5362 rust_language.clone(),
5363 &mut cx,
5364 );
5365
5366 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
5367 assert_rewrap(
5368 indoc! {"
5369 # Header
5370
5371 A long long long line of markdown text to wrap.ˇ
5372 "},
5373 indoc! {"
5374 # Header
5375
5376 A long long long line of markdown text
5377 to wrap.ˇ
5378 "},
5379 markdown_language.clone(),
5380 &mut cx,
5381 );
5382
5383 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
5384 assert_rewrap(
5385 indoc! {"
5386 «1. This is a numbered list item that is very long and needs to be wrapped properly.
5387 2. This is a numbered list item that is very long and needs to be wrapped properly.
5388 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
5389 "},
5390 indoc! {"
5391 «1. This is a numbered list item that is
5392 very long and needs to be wrapped
5393 properly.
5394 2. This is a numbered list item that is
5395 very long and needs to be wrapped
5396 properly.
5397 - This is an unordered list item that is
5398 also very long and should not merge
5399 with the numbered item.ˇ»
5400 "},
5401 markdown_language.clone(),
5402 &mut cx,
5403 );
5404
5405 // Test that rewrapping add indents for rewrapping boundary if not exists already.
5406 assert_rewrap(
5407 indoc! {"
5408 «1. This is a numbered list item that is
5409 very long and needs to be wrapped
5410 properly.
5411 2. This is a numbered list item that is
5412 very long and needs to be wrapped
5413 properly.
5414 - This is an unordered list item that is
5415 also very long and should not merge with
5416 the numbered item.ˇ»
5417 "},
5418 indoc! {"
5419 «1. This is a numbered list item that is
5420 very long and needs to be wrapped
5421 properly.
5422 2. This is a numbered list item that is
5423 very long and needs to be wrapped
5424 properly.
5425 - This is an unordered list item that is
5426 also very long and should not merge
5427 with the numbered item.ˇ»
5428 "},
5429 markdown_language.clone(),
5430 &mut cx,
5431 );
5432
5433 // Test that rewrapping maintain indents even when they already exists.
5434 assert_rewrap(
5435 indoc! {"
5436 «1. This is a numbered list
5437 item that is very long and needs to be wrapped properly.
5438 2. This is a numbered list
5439 item that is very long and needs to be wrapped properly.
5440 - This is an unordered list item that is also very long and
5441 should not merge with the numbered item.ˇ»
5442 "},
5443 indoc! {"
5444 «1. This is a numbered list item that is
5445 very long and needs to be wrapped
5446 properly.
5447 2. This is a numbered list item that is
5448 very long and needs to be wrapped
5449 properly.
5450 - This is an unordered list item that is
5451 also very long and should not merge
5452 with the numbered item.ˇ»
5453 "},
5454 markdown_language.clone(),
5455 &mut cx,
5456 );
5457
5458 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
5459 assert_rewrap(
5460 indoc! {"
5461 ˇThis is a very long line of plain text that will be wrapped.
5462 "},
5463 indoc! {"
5464 ˇThis is a very long line of plain text
5465 that will be wrapped.
5466 "},
5467 plaintext_language.clone(),
5468 &mut cx,
5469 );
5470
5471 // Test that non-commented code acts as a paragraph boundary within a selection
5472 assert_rewrap(
5473 indoc! {"
5474 «// This is the first long comment block to be wrapped.
5475 fn my_func(a: u32);
5476 // This is the second long comment block to be wrapped.ˇ»
5477 "},
5478 indoc! {"
5479 «// This is the first long comment block
5480 // to be wrapped.
5481 fn my_func(a: u32);
5482 // This is the second long comment block
5483 // to be wrapped.ˇ»
5484 "},
5485 rust_language.clone(),
5486 &mut cx,
5487 );
5488
5489 // Test rewrapping multiple selections, including ones with blank lines or tabs
5490 assert_rewrap(
5491 indoc! {"
5492 «ˇThis is a very long line that will be wrapped.
5493
5494 This is another paragraph in the same selection.»
5495
5496 «\tThis is a very long indented line that will be wrapped.ˇ»
5497 "},
5498 indoc! {"
5499 «ˇThis is a very long line that will be
5500 wrapped.
5501
5502 This is another paragraph in the same
5503 selection.»
5504
5505 «\tThis is a very long indented line
5506 \tthat will be wrapped.ˇ»
5507 "},
5508 plaintext_language.clone(),
5509 &mut cx,
5510 );
5511
5512 // Test that an empty comment line acts as a paragraph boundary
5513 assert_rewrap(
5514 indoc! {"
5515 // ˇThis is a long comment that will be wrapped.
5516 //
5517 // And this is another long comment that will also be wrapped.ˇ
5518 "},
5519 indoc! {"
5520 // ˇThis is a long comment that will be
5521 // wrapped.
5522 //
5523 // And this is another long comment that
5524 // will also be wrapped.ˇ
5525 "},
5526 cpp_language,
5527 &mut cx,
5528 );
5529
5530 #[track_caller]
5531 fn assert_rewrap(
5532 unwrapped_text: &str,
5533 wrapped_text: &str,
5534 language: Arc<Language>,
5535 cx: &mut EditorTestContext,
5536 ) {
5537 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5538 cx.set_state(unwrapped_text);
5539 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5540 cx.assert_editor_state(wrapped_text);
5541 }
5542}
5543
5544#[gpui::test]
5545async fn test_hard_wrap(cx: &mut TestAppContext) {
5546 init_test(cx, |_| {});
5547 let mut cx = EditorTestContext::new(cx).await;
5548
5549 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5550 cx.update_editor(|editor, _, cx| {
5551 editor.set_hard_wrap(Some(14), cx);
5552 });
5553
5554 cx.set_state(indoc!(
5555 "
5556 one two three ˇ
5557 "
5558 ));
5559 cx.simulate_input("four");
5560 cx.run_until_parked();
5561
5562 cx.assert_editor_state(indoc!(
5563 "
5564 one two three
5565 fourˇ
5566 "
5567 ));
5568
5569 cx.update_editor(|editor, window, cx| {
5570 editor.newline(&Default::default(), window, cx);
5571 });
5572 cx.run_until_parked();
5573 cx.assert_editor_state(indoc!(
5574 "
5575 one two three
5576 four
5577 ˇ
5578 "
5579 ));
5580
5581 cx.simulate_input("five");
5582 cx.run_until_parked();
5583 cx.assert_editor_state(indoc!(
5584 "
5585 one two three
5586 four
5587 fiveˇ
5588 "
5589 ));
5590
5591 cx.update_editor(|editor, window, cx| {
5592 editor.newline(&Default::default(), window, cx);
5593 });
5594 cx.run_until_parked();
5595 cx.simulate_input("# ");
5596 cx.run_until_parked();
5597 cx.assert_editor_state(indoc!(
5598 "
5599 one two three
5600 four
5601 five
5602 # ˇ
5603 "
5604 ));
5605
5606 cx.update_editor(|editor, window, cx| {
5607 editor.newline(&Default::default(), window, cx);
5608 });
5609 cx.run_until_parked();
5610 cx.assert_editor_state(indoc!(
5611 "
5612 one two three
5613 four
5614 five
5615 #\x20
5616 #ˇ
5617 "
5618 ));
5619
5620 cx.simulate_input(" 6");
5621 cx.run_until_parked();
5622 cx.assert_editor_state(indoc!(
5623 "
5624 one two three
5625 four
5626 five
5627 #
5628 # 6ˇ
5629 "
5630 ));
5631}
5632
5633#[gpui::test]
5634async fn test_clipboard(cx: &mut TestAppContext) {
5635 init_test(cx, |_| {});
5636
5637 let mut cx = EditorTestContext::new(cx).await;
5638
5639 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5640 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5641 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5642
5643 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5644 cx.set_state("two ˇfour ˇsix ˇ");
5645 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5646 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5647
5648 // Paste again but with only two cursors. Since the number of cursors doesn't
5649 // match the number of slices in the clipboard, the entire clipboard text
5650 // is pasted at each cursor.
5651 cx.set_state("ˇtwo one✅ four three six five ˇ");
5652 cx.update_editor(|e, window, cx| {
5653 e.handle_input("( ", window, cx);
5654 e.paste(&Paste, window, cx);
5655 e.handle_input(") ", window, cx);
5656 });
5657 cx.assert_editor_state(
5658 &([
5659 "( one✅ ",
5660 "three ",
5661 "five ) ˇtwo one✅ four three six five ( one✅ ",
5662 "three ",
5663 "five ) ˇ",
5664 ]
5665 .join("\n")),
5666 );
5667
5668 // Cut with three selections, one of which is full-line.
5669 cx.set_state(indoc! {"
5670 1«2ˇ»3
5671 4ˇ567
5672 «8ˇ»9"});
5673 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5674 cx.assert_editor_state(indoc! {"
5675 1ˇ3
5676 ˇ9"});
5677
5678 // Paste with three selections, noticing how the copied selection that was full-line
5679 // gets inserted before the second cursor.
5680 cx.set_state(indoc! {"
5681 1ˇ3
5682 9ˇ
5683 «oˇ»ne"});
5684 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5685 cx.assert_editor_state(indoc! {"
5686 12ˇ3
5687 4567
5688 9ˇ
5689 8ˇne"});
5690
5691 // Copy with a single cursor only, which writes the whole line into the clipboard.
5692 cx.set_state(indoc! {"
5693 The quick brown
5694 fox juˇmps over
5695 the lazy dog"});
5696 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5697 assert_eq!(
5698 cx.read_from_clipboard()
5699 .and_then(|item| item.text().as_deref().map(str::to_string)),
5700 Some("fox jumps over\n".to_string())
5701 );
5702
5703 // Paste with three selections, noticing how the copied full-line selection is inserted
5704 // before the empty selections but replaces the selection that is non-empty.
5705 cx.set_state(indoc! {"
5706 Tˇhe quick brown
5707 «foˇ»x jumps over
5708 tˇhe lazy dog"});
5709 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5710 cx.assert_editor_state(indoc! {"
5711 fox jumps over
5712 Tˇhe quick brown
5713 fox jumps over
5714 ˇx jumps over
5715 fox jumps over
5716 tˇhe lazy dog"});
5717}
5718
5719#[gpui::test]
5720async fn test_copy_trim(cx: &mut TestAppContext) {
5721 init_test(cx, |_| {});
5722
5723 let mut cx = EditorTestContext::new(cx).await;
5724 cx.set_state(
5725 r#" «for selection in selections.iter() {
5726 let mut start = selection.start;
5727 let mut end = selection.end;
5728 let is_entire_line = selection.is_empty();
5729 if is_entire_line {
5730 start = Point::new(start.row, 0);ˇ»
5731 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5732 }
5733 "#,
5734 );
5735 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5736 assert_eq!(
5737 cx.read_from_clipboard()
5738 .and_then(|item| item.text().as_deref().map(str::to_string)),
5739 Some(
5740 "for selection in selections.iter() {
5741 let mut start = selection.start;
5742 let mut end = selection.end;
5743 let is_entire_line = selection.is_empty();
5744 if is_entire_line {
5745 start = Point::new(start.row, 0);"
5746 .to_string()
5747 ),
5748 "Regular copying preserves all indentation selected",
5749 );
5750 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5751 assert_eq!(
5752 cx.read_from_clipboard()
5753 .and_then(|item| item.text().as_deref().map(str::to_string)),
5754 Some(
5755 "for selection in selections.iter() {
5756let mut start = selection.start;
5757let mut end = selection.end;
5758let is_entire_line = selection.is_empty();
5759if is_entire_line {
5760 start = Point::new(start.row, 0);"
5761 .to_string()
5762 ),
5763 "Copying with stripping should strip all leading whitespaces"
5764 );
5765
5766 cx.set_state(
5767 r#" « for selection in selections.iter() {
5768 let mut start = selection.start;
5769 let mut end = selection.end;
5770 let is_entire_line = selection.is_empty();
5771 if is_entire_line {
5772 start = Point::new(start.row, 0);ˇ»
5773 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5774 }
5775 "#,
5776 );
5777 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5778 assert_eq!(
5779 cx.read_from_clipboard()
5780 .and_then(|item| item.text().as_deref().map(str::to_string)),
5781 Some(
5782 " for selection in selections.iter() {
5783 let mut start = selection.start;
5784 let mut end = selection.end;
5785 let is_entire_line = selection.is_empty();
5786 if is_entire_line {
5787 start = Point::new(start.row, 0);"
5788 .to_string()
5789 ),
5790 "Regular copying preserves all indentation selected",
5791 );
5792 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5793 assert_eq!(
5794 cx.read_from_clipboard()
5795 .and_then(|item| item.text().as_deref().map(str::to_string)),
5796 Some(
5797 "for selection in selections.iter() {
5798let mut start = selection.start;
5799let mut end = selection.end;
5800let is_entire_line = selection.is_empty();
5801if is_entire_line {
5802 start = Point::new(start.row, 0);"
5803 .to_string()
5804 ),
5805 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5806 );
5807
5808 cx.set_state(
5809 r#" «ˇ for selection in selections.iter() {
5810 let mut start = selection.start;
5811 let mut end = selection.end;
5812 let is_entire_line = selection.is_empty();
5813 if is_entire_line {
5814 start = Point::new(start.row, 0);»
5815 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5816 }
5817 "#,
5818 );
5819 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5820 assert_eq!(
5821 cx.read_from_clipboard()
5822 .and_then(|item| item.text().as_deref().map(str::to_string)),
5823 Some(
5824 " for selection in selections.iter() {
5825 let mut start = selection.start;
5826 let mut end = selection.end;
5827 let is_entire_line = selection.is_empty();
5828 if is_entire_line {
5829 start = Point::new(start.row, 0);"
5830 .to_string()
5831 ),
5832 "Regular copying for reverse selection works the same",
5833 );
5834 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5835 assert_eq!(
5836 cx.read_from_clipboard()
5837 .and_then(|item| item.text().as_deref().map(str::to_string)),
5838 Some(
5839 "for selection in selections.iter() {
5840let mut start = selection.start;
5841let mut end = selection.end;
5842let is_entire_line = selection.is_empty();
5843if is_entire_line {
5844 start = Point::new(start.row, 0);"
5845 .to_string()
5846 ),
5847 "Copying with stripping for reverse selection works the same"
5848 );
5849
5850 cx.set_state(
5851 r#" for selection «in selections.iter() {
5852 let mut start = selection.start;
5853 let mut end = selection.end;
5854 let is_entire_line = selection.is_empty();
5855 if is_entire_line {
5856 start = Point::new(start.row, 0);ˇ»
5857 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5858 }
5859 "#,
5860 );
5861 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5862 assert_eq!(
5863 cx.read_from_clipboard()
5864 .and_then(|item| item.text().as_deref().map(str::to_string)),
5865 Some(
5866 "in selections.iter() {
5867 let mut start = selection.start;
5868 let mut end = selection.end;
5869 let is_entire_line = selection.is_empty();
5870 if is_entire_line {
5871 start = Point::new(start.row, 0);"
5872 .to_string()
5873 ),
5874 "When selecting past the indent, the copying works as usual",
5875 );
5876 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5877 assert_eq!(
5878 cx.read_from_clipboard()
5879 .and_then(|item| item.text().as_deref().map(str::to_string)),
5880 Some(
5881 "in selections.iter() {
5882 let mut start = selection.start;
5883 let mut end = selection.end;
5884 let is_entire_line = selection.is_empty();
5885 if is_entire_line {
5886 start = Point::new(start.row, 0);"
5887 .to_string()
5888 ),
5889 "When selecting past the indent, nothing is trimmed"
5890 );
5891
5892 cx.set_state(
5893 r#" «for selection in selections.iter() {
5894 let mut start = selection.start;
5895
5896 let mut end = selection.end;
5897 let is_entire_line = selection.is_empty();
5898 if is_entire_line {
5899 start = Point::new(start.row, 0);
5900ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5901 }
5902 "#,
5903 );
5904 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5905 assert_eq!(
5906 cx.read_from_clipboard()
5907 .and_then(|item| item.text().as_deref().map(str::to_string)),
5908 Some(
5909 "for selection in selections.iter() {
5910let mut start = selection.start;
5911
5912let mut end = selection.end;
5913let is_entire_line = selection.is_empty();
5914if is_entire_line {
5915 start = Point::new(start.row, 0);
5916"
5917 .to_string()
5918 ),
5919 "Copying with stripping should ignore empty lines"
5920 );
5921}
5922
5923#[gpui::test]
5924async fn test_paste_multiline(cx: &mut TestAppContext) {
5925 init_test(cx, |_| {});
5926
5927 let mut cx = EditorTestContext::new(cx).await;
5928 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5929
5930 // Cut an indented block, without the leading whitespace.
5931 cx.set_state(indoc! {"
5932 const a: B = (
5933 c(),
5934 «d(
5935 e,
5936 f
5937 )ˇ»
5938 );
5939 "});
5940 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5941 cx.assert_editor_state(indoc! {"
5942 const a: B = (
5943 c(),
5944 ˇ
5945 );
5946 "});
5947
5948 // Paste it at the same position.
5949 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5950 cx.assert_editor_state(indoc! {"
5951 const a: B = (
5952 c(),
5953 d(
5954 e,
5955 f
5956 )ˇ
5957 );
5958 "});
5959
5960 // Paste it at a line with a lower indent level.
5961 cx.set_state(indoc! {"
5962 ˇ
5963 const a: B = (
5964 c(),
5965 );
5966 "});
5967 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5968 cx.assert_editor_state(indoc! {"
5969 d(
5970 e,
5971 f
5972 )ˇ
5973 const a: B = (
5974 c(),
5975 );
5976 "});
5977
5978 // Cut an indented block, with the leading whitespace.
5979 cx.set_state(indoc! {"
5980 const a: B = (
5981 c(),
5982 « d(
5983 e,
5984 f
5985 )
5986 ˇ»);
5987 "});
5988 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5989 cx.assert_editor_state(indoc! {"
5990 const a: B = (
5991 c(),
5992 ˇ);
5993 "});
5994
5995 // Paste it at the same position.
5996 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5997 cx.assert_editor_state(indoc! {"
5998 const a: B = (
5999 c(),
6000 d(
6001 e,
6002 f
6003 )
6004 ˇ);
6005 "});
6006
6007 // Paste it at a line with a higher indent level.
6008 cx.set_state(indoc! {"
6009 const a: B = (
6010 c(),
6011 d(
6012 e,
6013 fˇ
6014 )
6015 );
6016 "});
6017 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6018 cx.assert_editor_state(indoc! {"
6019 const a: B = (
6020 c(),
6021 d(
6022 e,
6023 f d(
6024 e,
6025 f
6026 )
6027 ˇ
6028 )
6029 );
6030 "});
6031
6032 // Copy an indented block, starting mid-line
6033 cx.set_state(indoc! {"
6034 const a: B = (
6035 c(),
6036 somethin«g(
6037 e,
6038 f
6039 )ˇ»
6040 );
6041 "});
6042 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6043
6044 // Paste it on a line with a lower indent level
6045 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
6046 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6047 cx.assert_editor_state(indoc! {"
6048 const a: B = (
6049 c(),
6050 something(
6051 e,
6052 f
6053 )
6054 );
6055 g(
6056 e,
6057 f
6058 )ˇ"});
6059}
6060
6061#[gpui::test]
6062async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
6063 init_test(cx, |_| {});
6064
6065 cx.write_to_clipboard(ClipboardItem::new_string(
6066 " d(\n e\n );\n".into(),
6067 ));
6068
6069 let mut cx = EditorTestContext::new(cx).await;
6070 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6071
6072 cx.set_state(indoc! {"
6073 fn a() {
6074 b();
6075 if c() {
6076 ˇ
6077 }
6078 }
6079 "});
6080
6081 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6082 cx.assert_editor_state(indoc! {"
6083 fn a() {
6084 b();
6085 if c() {
6086 d(
6087 e
6088 );
6089 ˇ
6090 }
6091 }
6092 "});
6093
6094 cx.set_state(indoc! {"
6095 fn a() {
6096 b();
6097 ˇ
6098 }
6099 "});
6100
6101 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6102 cx.assert_editor_state(indoc! {"
6103 fn a() {
6104 b();
6105 d(
6106 e
6107 );
6108 ˇ
6109 }
6110 "});
6111}
6112
6113#[gpui::test]
6114fn test_select_all(cx: &mut TestAppContext) {
6115 init_test(cx, |_| {});
6116
6117 let editor = cx.add_window(|window, cx| {
6118 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
6119 build_editor(buffer, window, cx)
6120 });
6121 _ = editor.update(cx, |editor, window, cx| {
6122 editor.select_all(&SelectAll, window, cx);
6123 assert_eq!(
6124 editor.selections.display_ranges(cx),
6125 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
6126 );
6127 });
6128}
6129
6130#[gpui::test]
6131fn test_select_line(cx: &mut TestAppContext) {
6132 init_test(cx, |_| {});
6133
6134 let editor = cx.add_window(|window, cx| {
6135 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
6136 build_editor(buffer, window, cx)
6137 });
6138 _ = editor.update(cx, |editor, window, cx| {
6139 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6140 s.select_display_ranges([
6141 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6142 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6143 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6144 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
6145 ])
6146 });
6147 editor.select_line(&SelectLine, window, cx);
6148 assert_eq!(
6149 editor.selections.display_ranges(cx),
6150 vec![
6151 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
6152 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
6153 ]
6154 );
6155 });
6156
6157 _ = editor.update(cx, |editor, window, cx| {
6158 editor.select_line(&SelectLine, window, cx);
6159 assert_eq!(
6160 editor.selections.display_ranges(cx),
6161 vec![
6162 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
6163 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
6164 ]
6165 );
6166 });
6167
6168 _ = editor.update(cx, |editor, window, cx| {
6169 editor.select_line(&SelectLine, window, cx);
6170 assert_eq!(
6171 editor.selections.display_ranges(cx),
6172 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
6173 );
6174 });
6175}
6176
6177#[gpui::test]
6178async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
6179 init_test(cx, |_| {});
6180 let mut cx = EditorTestContext::new(cx).await;
6181
6182 #[track_caller]
6183 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
6184 cx.set_state(initial_state);
6185 cx.update_editor(|e, window, cx| {
6186 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
6187 });
6188 cx.assert_editor_state(expected_state);
6189 }
6190
6191 // Selection starts and ends at the middle of lines, left-to-right
6192 test(
6193 &mut cx,
6194 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
6195 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6196 );
6197 // Same thing, right-to-left
6198 test(
6199 &mut cx,
6200 "aa\nb«b\ncc\ndd\neˇ»e\nff",
6201 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6202 );
6203
6204 // Whole buffer, left-to-right, last line *doesn't* end with newline
6205 test(
6206 &mut cx,
6207 "«ˇaa\nbb\ncc\ndd\nee\nff»",
6208 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6209 );
6210 // Same thing, right-to-left
6211 test(
6212 &mut cx,
6213 "«aa\nbb\ncc\ndd\nee\nffˇ»",
6214 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6215 );
6216
6217 // Whole buffer, left-to-right, last line ends with newline
6218 test(
6219 &mut cx,
6220 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
6221 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6222 );
6223 // Same thing, right-to-left
6224 test(
6225 &mut cx,
6226 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
6227 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6228 );
6229
6230 // Starts at the end of a line, ends at the start of another
6231 test(
6232 &mut cx,
6233 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
6234 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
6235 );
6236}
6237
6238#[gpui::test]
6239async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
6240 init_test(cx, |_| {});
6241
6242 let editor = cx.add_window(|window, cx| {
6243 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
6244 build_editor(buffer, window, cx)
6245 });
6246
6247 // setup
6248 _ = editor.update(cx, |editor, window, cx| {
6249 editor.fold_creases(
6250 vec![
6251 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
6252 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
6253 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
6254 ],
6255 true,
6256 window,
6257 cx,
6258 );
6259 assert_eq!(
6260 editor.display_text(cx),
6261 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6262 );
6263 });
6264
6265 _ = editor.update(cx, |editor, window, cx| {
6266 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6267 s.select_display_ranges([
6268 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6269 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6270 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6271 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
6272 ])
6273 });
6274 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6275 assert_eq!(
6276 editor.display_text(cx),
6277 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6278 );
6279 });
6280 EditorTestContext::for_editor(editor, cx)
6281 .await
6282 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
6283
6284 _ = editor.update(cx, |editor, window, cx| {
6285 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6286 s.select_display_ranges([
6287 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
6288 ])
6289 });
6290 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6291 assert_eq!(
6292 editor.display_text(cx),
6293 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
6294 );
6295 assert_eq!(
6296 editor.selections.display_ranges(cx),
6297 [
6298 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
6299 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
6300 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
6301 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
6302 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
6303 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
6304 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
6305 ]
6306 );
6307 });
6308 EditorTestContext::for_editor(editor, cx)
6309 .await
6310 .assert_editor_state(
6311 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
6312 );
6313}
6314
6315#[gpui::test]
6316async fn test_add_selection_above_below(cx: &mut TestAppContext) {
6317 init_test(cx, |_| {});
6318
6319 let mut cx = EditorTestContext::new(cx).await;
6320
6321 cx.set_state(indoc!(
6322 r#"abc
6323 defˇghi
6324
6325 jk
6326 nlmo
6327 "#
6328 ));
6329
6330 cx.update_editor(|editor, window, cx| {
6331 editor.add_selection_above(&Default::default(), window, cx);
6332 });
6333
6334 cx.assert_editor_state(indoc!(
6335 r#"abcˇ
6336 defˇghi
6337
6338 jk
6339 nlmo
6340 "#
6341 ));
6342
6343 cx.update_editor(|editor, window, cx| {
6344 editor.add_selection_above(&Default::default(), window, cx);
6345 });
6346
6347 cx.assert_editor_state(indoc!(
6348 r#"abcˇ
6349 defˇghi
6350
6351 jk
6352 nlmo
6353 "#
6354 ));
6355
6356 cx.update_editor(|editor, window, cx| {
6357 editor.add_selection_below(&Default::default(), window, cx);
6358 });
6359
6360 cx.assert_editor_state(indoc!(
6361 r#"abc
6362 defˇghi
6363
6364 jk
6365 nlmo
6366 "#
6367 ));
6368
6369 cx.update_editor(|editor, window, cx| {
6370 editor.undo_selection(&Default::default(), window, cx);
6371 });
6372
6373 cx.assert_editor_state(indoc!(
6374 r#"abcˇ
6375 defˇghi
6376
6377 jk
6378 nlmo
6379 "#
6380 ));
6381
6382 cx.update_editor(|editor, window, cx| {
6383 editor.redo_selection(&Default::default(), window, cx);
6384 });
6385
6386 cx.assert_editor_state(indoc!(
6387 r#"abc
6388 defˇghi
6389
6390 jk
6391 nlmo
6392 "#
6393 ));
6394
6395 cx.update_editor(|editor, window, cx| {
6396 editor.add_selection_below(&Default::default(), window, cx);
6397 });
6398
6399 cx.assert_editor_state(indoc!(
6400 r#"abc
6401 defˇghi
6402 ˇ
6403 jk
6404 nlmo
6405 "#
6406 ));
6407
6408 cx.update_editor(|editor, window, cx| {
6409 editor.add_selection_below(&Default::default(), window, cx);
6410 });
6411
6412 cx.assert_editor_state(indoc!(
6413 r#"abc
6414 defˇghi
6415 ˇ
6416 jkˇ
6417 nlmo
6418 "#
6419 ));
6420
6421 cx.update_editor(|editor, window, cx| {
6422 editor.add_selection_below(&Default::default(), window, cx);
6423 });
6424
6425 cx.assert_editor_state(indoc!(
6426 r#"abc
6427 defˇghi
6428 ˇ
6429 jkˇ
6430 nlmˇo
6431 "#
6432 ));
6433
6434 cx.update_editor(|editor, window, cx| {
6435 editor.add_selection_below(&Default::default(), window, cx);
6436 });
6437
6438 cx.assert_editor_state(indoc!(
6439 r#"abc
6440 defˇghi
6441 ˇ
6442 jkˇ
6443 nlmˇo
6444 ˇ"#
6445 ));
6446
6447 // change selections
6448 cx.set_state(indoc!(
6449 r#"abc
6450 def«ˇg»hi
6451
6452 jk
6453 nlmo
6454 "#
6455 ));
6456
6457 cx.update_editor(|editor, window, cx| {
6458 editor.add_selection_below(&Default::default(), window, cx);
6459 });
6460
6461 cx.assert_editor_state(indoc!(
6462 r#"abc
6463 def«ˇg»hi
6464
6465 jk
6466 nlm«ˇo»
6467 "#
6468 ));
6469
6470 cx.update_editor(|editor, window, cx| {
6471 editor.add_selection_below(&Default::default(), window, cx);
6472 });
6473
6474 cx.assert_editor_state(indoc!(
6475 r#"abc
6476 def«ˇg»hi
6477
6478 jk
6479 nlm«ˇo»
6480 "#
6481 ));
6482
6483 cx.update_editor(|editor, window, cx| {
6484 editor.add_selection_above(&Default::default(), window, cx);
6485 });
6486
6487 cx.assert_editor_state(indoc!(
6488 r#"abc
6489 def«ˇg»hi
6490
6491 jk
6492 nlmo
6493 "#
6494 ));
6495
6496 cx.update_editor(|editor, window, cx| {
6497 editor.add_selection_above(&Default::default(), window, cx);
6498 });
6499
6500 cx.assert_editor_state(indoc!(
6501 r#"abc
6502 def«ˇg»hi
6503
6504 jk
6505 nlmo
6506 "#
6507 ));
6508
6509 // Change selections again
6510 cx.set_state(indoc!(
6511 r#"a«bc
6512 defgˇ»hi
6513
6514 jk
6515 nlmo
6516 "#
6517 ));
6518
6519 cx.update_editor(|editor, window, cx| {
6520 editor.add_selection_below(&Default::default(), window, cx);
6521 });
6522
6523 cx.assert_editor_state(indoc!(
6524 r#"a«bcˇ»
6525 d«efgˇ»hi
6526
6527 j«kˇ»
6528 nlmo
6529 "#
6530 ));
6531
6532 cx.update_editor(|editor, window, cx| {
6533 editor.add_selection_below(&Default::default(), window, cx);
6534 });
6535 cx.assert_editor_state(indoc!(
6536 r#"a«bcˇ»
6537 d«efgˇ»hi
6538
6539 j«kˇ»
6540 n«lmoˇ»
6541 "#
6542 ));
6543 cx.update_editor(|editor, window, cx| {
6544 editor.add_selection_above(&Default::default(), window, cx);
6545 });
6546
6547 cx.assert_editor_state(indoc!(
6548 r#"a«bcˇ»
6549 d«efgˇ»hi
6550
6551 j«kˇ»
6552 nlmo
6553 "#
6554 ));
6555
6556 // Change selections again
6557 cx.set_state(indoc!(
6558 r#"abc
6559 d«ˇefghi
6560
6561 jk
6562 nlm»o
6563 "#
6564 ));
6565
6566 cx.update_editor(|editor, window, cx| {
6567 editor.add_selection_above(&Default::default(), window, cx);
6568 });
6569
6570 cx.assert_editor_state(indoc!(
6571 r#"a«ˇbc»
6572 d«ˇef»ghi
6573
6574 j«ˇk»
6575 n«ˇlm»o
6576 "#
6577 ));
6578
6579 cx.update_editor(|editor, window, cx| {
6580 editor.add_selection_below(&Default::default(), window, cx);
6581 });
6582
6583 cx.assert_editor_state(indoc!(
6584 r#"abc
6585 d«ˇef»ghi
6586
6587 j«ˇk»
6588 n«ˇlm»o
6589 "#
6590 ));
6591}
6592
6593#[gpui::test]
6594async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
6595 init_test(cx, |_| {});
6596 let mut cx = EditorTestContext::new(cx).await;
6597
6598 cx.set_state(indoc!(
6599 r#"line onˇe
6600 liˇne two
6601 line three
6602 line four"#
6603 ));
6604
6605 cx.update_editor(|editor, window, cx| {
6606 editor.add_selection_below(&Default::default(), window, cx);
6607 });
6608
6609 // test multiple cursors expand in the same direction
6610 cx.assert_editor_state(indoc!(
6611 r#"line onˇe
6612 liˇne twˇo
6613 liˇne three
6614 line four"#
6615 ));
6616
6617 cx.update_editor(|editor, window, cx| {
6618 editor.add_selection_below(&Default::default(), window, cx);
6619 });
6620
6621 cx.update_editor(|editor, window, cx| {
6622 editor.add_selection_below(&Default::default(), window, cx);
6623 });
6624
6625 // test multiple cursors expand below overflow
6626 cx.assert_editor_state(indoc!(
6627 r#"line onˇe
6628 liˇne twˇo
6629 liˇne thˇree
6630 liˇne foˇur"#
6631 ));
6632
6633 cx.update_editor(|editor, window, cx| {
6634 editor.add_selection_above(&Default::default(), window, cx);
6635 });
6636
6637 // test multiple cursors retrieves back correctly
6638 cx.assert_editor_state(indoc!(
6639 r#"line onˇe
6640 liˇne twˇo
6641 liˇne thˇree
6642 line four"#
6643 ));
6644
6645 cx.update_editor(|editor, window, cx| {
6646 editor.add_selection_above(&Default::default(), window, cx);
6647 });
6648
6649 cx.update_editor(|editor, window, cx| {
6650 editor.add_selection_above(&Default::default(), window, cx);
6651 });
6652
6653 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
6654 cx.assert_editor_state(indoc!(
6655 r#"liˇne onˇe
6656 liˇne two
6657 line three
6658 line four"#
6659 ));
6660
6661 cx.update_editor(|editor, window, cx| {
6662 editor.undo_selection(&Default::default(), window, cx);
6663 });
6664
6665 // test undo
6666 cx.assert_editor_state(indoc!(
6667 r#"line onˇe
6668 liˇne twˇo
6669 line three
6670 line four"#
6671 ));
6672
6673 cx.update_editor(|editor, window, cx| {
6674 editor.redo_selection(&Default::default(), window, cx);
6675 });
6676
6677 // test redo
6678 cx.assert_editor_state(indoc!(
6679 r#"liˇne onˇe
6680 liˇne two
6681 line three
6682 line four"#
6683 ));
6684
6685 cx.set_state(indoc!(
6686 r#"abcd
6687 ef«ghˇ»
6688 ijkl
6689 «mˇ»nop"#
6690 ));
6691
6692 cx.update_editor(|editor, window, cx| {
6693 editor.add_selection_above(&Default::default(), window, cx);
6694 });
6695
6696 // test multiple selections expand in the same direction
6697 cx.assert_editor_state(indoc!(
6698 r#"ab«cdˇ»
6699 ef«ghˇ»
6700 «iˇ»jkl
6701 «mˇ»nop"#
6702 ));
6703
6704 cx.update_editor(|editor, window, cx| {
6705 editor.add_selection_above(&Default::default(), window, cx);
6706 });
6707
6708 // test multiple selection upward overflow
6709 cx.assert_editor_state(indoc!(
6710 r#"ab«cdˇ»
6711 «eˇ»f«ghˇ»
6712 «iˇ»jkl
6713 «mˇ»nop"#
6714 ));
6715
6716 cx.update_editor(|editor, window, cx| {
6717 editor.add_selection_below(&Default::default(), window, cx);
6718 });
6719
6720 // test multiple selection retrieves back correctly
6721 cx.assert_editor_state(indoc!(
6722 r#"abcd
6723 ef«ghˇ»
6724 «iˇ»jkl
6725 «mˇ»nop"#
6726 ));
6727
6728 cx.update_editor(|editor, window, cx| {
6729 editor.add_selection_below(&Default::default(), window, cx);
6730 });
6731
6732 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
6733 cx.assert_editor_state(indoc!(
6734 r#"abcd
6735 ef«ghˇ»
6736 ij«klˇ»
6737 «mˇ»nop"#
6738 ));
6739
6740 cx.update_editor(|editor, window, cx| {
6741 editor.undo_selection(&Default::default(), window, cx);
6742 });
6743
6744 // test undo
6745 cx.assert_editor_state(indoc!(
6746 r#"abcd
6747 ef«ghˇ»
6748 «iˇ»jkl
6749 «mˇ»nop"#
6750 ));
6751
6752 cx.update_editor(|editor, window, cx| {
6753 editor.redo_selection(&Default::default(), window, cx);
6754 });
6755
6756 // test redo
6757 cx.assert_editor_state(indoc!(
6758 r#"abcd
6759 ef«ghˇ»
6760 ij«klˇ»
6761 «mˇ»nop"#
6762 ));
6763}
6764
6765#[gpui::test]
6766async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
6767 init_test(cx, |_| {});
6768 let mut cx = EditorTestContext::new(cx).await;
6769
6770 cx.set_state(indoc!(
6771 r#"line onˇe
6772 liˇne two
6773 line three
6774 line four"#
6775 ));
6776
6777 cx.update_editor(|editor, window, cx| {
6778 editor.add_selection_below(&Default::default(), window, cx);
6779 editor.add_selection_below(&Default::default(), window, cx);
6780 editor.add_selection_below(&Default::default(), window, cx);
6781 });
6782
6783 // initial state with two multi cursor groups
6784 cx.assert_editor_state(indoc!(
6785 r#"line onˇe
6786 liˇne twˇo
6787 liˇne thˇree
6788 liˇne foˇur"#
6789 ));
6790
6791 // add single cursor in middle - simulate opt click
6792 cx.update_editor(|editor, window, cx| {
6793 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
6794 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6795 editor.end_selection(window, cx);
6796 });
6797
6798 cx.assert_editor_state(indoc!(
6799 r#"line onˇe
6800 liˇne twˇo
6801 liˇneˇ thˇree
6802 liˇne foˇur"#
6803 ));
6804
6805 cx.update_editor(|editor, window, cx| {
6806 editor.add_selection_above(&Default::default(), window, cx);
6807 });
6808
6809 // test new added selection expands above and existing selection shrinks
6810 cx.assert_editor_state(indoc!(
6811 r#"line onˇe
6812 liˇneˇ twˇo
6813 liˇneˇ thˇree
6814 line four"#
6815 ));
6816
6817 cx.update_editor(|editor, window, cx| {
6818 editor.add_selection_above(&Default::default(), window, cx);
6819 });
6820
6821 // test new added selection expands above and existing selection shrinks
6822 cx.assert_editor_state(indoc!(
6823 r#"lineˇ onˇe
6824 liˇneˇ twˇo
6825 lineˇ three
6826 line four"#
6827 ));
6828
6829 // intial state with two selection groups
6830 cx.set_state(indoc!(
6831 r#"abcd
6832 ef«ghˇ»
6833 ijkl
6834 «mˇ»nop"#
6835 ));
6836
6837 cx.update_editor(|editor, window, cx| {
6838 editor.add_selection_above(&Default::default(), window, cx);
6839 editor.add_selection_above(&Default::default(), window, cx);
6840 });
6841
6842 cx.assert_editor_state(indoc!(
6843 r#"ab«cdˇ»
6844 «eˇ»f«ghˇ»
6845 «iˇ»jkl
6846 «mˇ»nop"#
6847 ));
6848
6849 // add single selection in middle - simulate opt drag
6850 cx.update_editor(|editor, window, cx| {
6851 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
6852 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6853 editor.update_selection(
6854 DisplayPoint::new(DisplayRow(2), 4),
6855 0,
6856 gpui::Point::<f32>::default(),
6857 window,
6858 cx,
6859 );
6860 editor.end_selection(window, cx);
6861 });
6862
6863 cx.assert_editor_state(indoc!(
6864 r#"ab«cdˇ»
6865 «eˇ»f«ghˇ»
6866 «iˇ»jk«lˇ»
6867 «mˇ»nop"#
6868 ));
6869
6870 cx.update_editor(|editor, window, cx| {
6871 editor.add_selection_below(&Default::default(), window, cx);
6872 });
6873
6874 // test new added selection expands below, others shrinks from above
6875 cx.assert_editor_state(indoc!(
6876 r#"abcd
6877 ef«ghˇ»
6878 «iˇ»jk«lˇ»
6879 «mˇ»no«pˇ»"#
6880 ));
6881}
6882
6883#[gpui::test]
6884async fn test_select_next(cx: &mut TestAppContext) {
6885 init_test(cx, |_| {});
6886
6887 let mut cx = EditorTestContext::new(cx).await;
6888 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6889
6890 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6891 .unwrap();
6892 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6893
6894 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6895 .unwrap();
6896 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6897
6898 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6899 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6900
6901 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6902 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6903
6904 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6905 .unwrap();
6906 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6907
6908 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6909 .unwrap();
6910 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6911
6912 // Test selection direction should be preserved
6913 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6914
6915 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6916 .unwrap();
6917 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
6918}
6919
6920#[gpui::test]
6921async fn test_select_all_matches(cx: &mut TestAppContext) {
6922 init_test(cx, |_| {});
6923
6924 let mut cx = EditorTestContext::new(cx).await;
6925
6926 // Test caret-only selections
6927 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6928 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6929 .unwrap();
6930 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6931
6932 // Test left-to-right selections
6933 cx.set_state("abc\n«abcˇ»\nabc");
6934 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6935 .unwrap();
6936 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
6937
6938 // Test right-to-left selections
6939 cx.set_state("abc\n«ˇabc»\nabc");
6940 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6941 .unwrap();
6942 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
6943
6944 // Test selecting whitespace with caret selection
6945 cx.set_state("abc\nˇ abc\nabc");
6946 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6947 .unwrap();
6948 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6949
6950 // Test selecting whitespace with left-to-right selection
6951 cx.set_state("abc\n«ˇ »abc\nabc");
6952 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6953 .unwrap();
6954 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
6955
6956 // Test no matches with right-to-left selection
6957 cx.set_state("abc\n« ˇ»abc\nabc");
6958 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6959 .unwrap();
6960 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6961
6962 // Test with a single word and clip_at_line_ends=true (#29823)
6963 cx.set_state("aˇbc");
6964 cx.update_editor(|e, window, cx| {
6965 e.set_clip_at_line_ends(true, cx);
6966 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
6967 e.set_clip_at_line_ends(false, cx);
6968 });
6969 cx.assert_editor_state("«abcˇ»");
6970}
6971
6972#[gpui::test]
6973async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
6974 init_test(cx, |_| {});
6975
6976 let mut cx = EditorTestContext::new(cx).await;
6977
6978 let large_body_1 = "\nd".repeat(200);
6979 let large_body_2 = "\ne".repeat(200);
6980
6981 cx.set_state(&format!(
6982 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
6983 ));
6984 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
6985 let scroll_position = editor.scroll_position(cx);
6986 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
6987 scroll_position
6988 });
6989
6990 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6991 .unwrap();
6992 cx.assert_editor_state(&format!(
6993 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
6994 ));
6995 let scroll_position_after_selection =
6996 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
6997 assert_eq!(
6998 initial_scroll_position, scroll_position_after_selection,
6999 "Scroll position should not change after selecting all matches"
7000 );
7001}
7002
7003#[gpui::test]
7004async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
7005 init_test(cx, |_| {});
7006
7007 let mut cx = EditorLspTestContext::new_rust(
7008 lsp::ServerCapabilities {
7009 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7010 ..Default::default()
7011 },
7012 cx,
7013 )
7014 .await;
7015
7016 cx.set_state(indoc! {"
7017 line 1
7018 line 2
7019 linˇe 3
7020 line 4
7021 line 5
7022 "});
7023
7024 // Make an edit
7025 cx.update_editor(|editor, window, cx| {
7026 editor.handle_input("X", window, cx);
7027 });
7028
7029 // Move cursor to a different position
7030 cx.update_editor(|editor, window, cx| {
7031 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7032 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
7033 });
7034 });
7035
7036 cx.assert_editor_state(indoc! {"
7037 line 1
7038 line 2
7039 linXe 3
7040 line 4
7041 liˇne 5
7042 "});
7043
7044 cx.lsp
7045 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
7046 Ok(Some(vec![lsp::TextEdit::new(
7047 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
7048 "PREFIX ".to_string(),
7049 )]))
7050 });
7051
7052 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
7053 .unwrap()
7054 .await
7055 .unwrap();
7056
7057 cx.assert_editor_state(indoc! {"
7058 PREFIX line 1
7059 line 2
7060 linXe 3
7061 line 4
7062 liˇne 5
7063 "});
7064
7065 // Undo formatting
7066 cx.update_editor(|editor, window, cx| {
7067 editor.undo(&Default::default(), window, cx);
7068 });
7069
7070 // Verify cursor moved back to position after edit
7071 cx.assert_editor_state(indoc! {"
7072 line 1
7073 line 2
7074 linXˇe 3
7075 line 4
7076 line 5
7077 "});
7078}
7079
7080#[gpui::test]
7081async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) {
7082 init_test(cx, |_| {});
7083
7084 let mut cx = EditorTestContext::new(cx).await;
7085
7086 let provider = cx.new(|_| FakeInlineCompletionProvider::default());
7087 cx.update_editor(|editor, window, cx| {
7088 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
7089 });
7090
7091 cx.set_state(indoc! {"
7092 line 1
7093 line 2
7094 linˇe 3
7095 line 4
7096 line 5
7097 line 6
7098 line 7
7099 line 8
7100 line 9
7101 line 10
7102 "});
7103
7104 let snapshot = cx.buffer_snapshot();
7105 let edit_position = snapshot.anchor_after(Point::new(2, 4));
7106
7107 cx.update(|_, cx| {
7108 provider.update(cx, |provider, _| {
7109 provider.set_inline_completion(Some(inline_completion::InlineCompletion {
7110 id: None,
7111 edits: vec![(edit_position..edit_position, "X".into())],
7112 edit_preview: None,
7113 }))
7114 })
7115 });
7116
7117 cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
7118 cx.update_editor(|editor, window, cx| {
7119 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
7120 });
7121
7122 cx.assert_editor_state(indoc! {"
7123 line 1
7124 line 2
7125 lineXˇ 3
7126 line 4
7127 line 5
7128 line 6
7129 line 7
7130 line 8
7131 line 9
7132 line 10
7133 "});
7134
7135 cx.update_editor(|editor, window, cx| {
7136 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7137 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
7138 });
7139 });
7140
7141 cx.assert_editor_state(indoc! {"
7142 line 1
7143 line 2
7144 lineX 3
7145 line 4
7146 line 5
7147 line 6
7148 line 7
7149 line 8
7150 line 9
7151 liˇne 10
7152 "});
7153
7154 cx.update_editor(|editor, window, cx| {
7155 editor.undo(&Default::default(), window, cx);
7156 });
7157
7158 cx.assert_editor_state(indoc! {"
7159 line 1
7160 line 2
7161 lineˇ 3
7162 line 4
7163 line 5
7164 line 6
7165 line 7
7166 line 8
7167 line 9
7168 line 10
7169 "});
7170}
7171
7172#[gpui::test]
7173async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
7174 init_test(cx, |_| {});
7175
7176 let mut cx = EditorTestContext::new(cx).await;
7177 cx.set_state(
7178 r#"let foo = 2;
7179lˇet foo = 2;
7180let fooˇ = 2;
7181let foo = 2;
7182let foo = ˇ2;"#,
7183 );
7184
7185 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7186 .unwrap();
7187 cx.assert_editor_state(
7188 r#"let foo = 2;
7189«letˇ» foo = 2;
7190let «fooˇ» = 2;
7191let foo = 2;
7192let foo = «2ˇ»;"#,
7193 );
7194
7195 // noop for multiple selections with different contents
7196 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7197 .unwrap();
7198 cx.assert_editor_state(
7199 r#"let foo = 2;
7200«letˇ» foo = 2;
7201let «fooˇ» = 2;
7202let foo = 2;
7203let foo = «2ˇ»;"#,
7204 );
7205
7206 // Test last selection direction should be preserved
7207 cx.set_state(
7208 r#"let foo = 2;
7209let foo = 2;
7210let «fooˇ» = 2;
7211let «ˇfoo» = 2;
7212let foo = 2;"#,
7213 );
7214
7215 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7216 .unwrap();
7217 cx.assert_editor_state(
7218 r#"let foo = 2;
7219let foo = 2;
7220let «fooˇ» = 2;
7221let «ˇfoo» = 2;
7222let «ˇfoo» = 2;"#,
7223 );
7224}
7225
7226#[gpui::test]
7227async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
7228 init_test(cx, |_| {});
7229
7230 let mut cx =
7231 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
7232
7233 cx.assert_editor_state(indoc! {"
7234 ˇbbb
7235 ccc
7236
7237 bbb
7238 ccc
7239 "});
7240 cx.dispatch_action(SelectPrevious::default());
7241 cx.assert_editor_state(indoc! {"
7242 «bbbˇ»
7243 ccc
7244
7245 bbb
7246 ccc
7247 "});
7248 cx.dispatch_action(SelectPrevious::default());
7249 cx.assert_editor_state(indoc! {"
7250 «bbbˇ»
7251 ccc
7252
7253 «bbbˇ»
7254 ccc
7255 "});
7256}
7257
7258#[gpui::test]
7259async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
7260 init_test(cx, |_| {});
7261
7262 let mut cx = EditorTestContext::new(cx).await;
7263 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7264
7265 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7266 .unwrap();
7267 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7268
7269 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7270 .unwrap();
7271 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7272
7273 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7274 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7275
7276 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7277 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7278
7279 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7280 .unwrap();
7281 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
7282
7283 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7284 .unwrap();
7285 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7286}
7287
7288#[gpui::test]
7289async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
7290 init_test(cx, |_| {});
7291
7292 let mut cx = EditorTestContext::new(cx).await;
7293 cx.set_state("aˇ");
7294
7295 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7296 .unwrap();
7297 cx.assert_editor_state("«aˇ»");
7298 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7299 .unwrap();
7300 cx.assert_editor_state("«aˇ»");
7301}
7302
7303#[gpui::test]
7304async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
7305 init_test(cx, |_| {});
7306
7307 let mut cx = EditorTestContext::new(cx).await;
7308 cx.set_state(
7309 r#"let foo = 2;
7310lˇet foo = 2;
7311let fooˇ = 2;
7312let foo = 2;
7313let foo = ˇ2;"#,
7314 );
7315
7316 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7317 .unwrap();
7318 cx.assert_editor_state(
7319 r#"let foo = 2;
7320«letˇ» foo = 2;
7321let «fooˇ» = 2;
7322let foo = 2;
7323let foo = «2ˇ»;"#,
7324 );
7325
7326 // noop for multiple selections with different contents
7327 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7328 .unwrap();
7329 cx.assert_editor_state(
7330 r#"let foo = 2;
7331«letˇ» foo = 2;
7332let «fooˇ» = 2;
7333let foo = 2;
7334let foo = «2ˇ»;"#,
7335 );
7336}
7337
7338#[gpui::test]
7339async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
7340 init_test(cx, |_| {});
7341
7342 let mut cx = EditorTestContext::new(cx).await;
7343 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7344
7345 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7346 .unwrap();
7347 // selection direction is preserved
7348 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7349
7350 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7351 .unwrap();
7352 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7353
7354 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7355 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7356
7357 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7358 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7359
7360 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7361 .unwrap();
7362 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
7363
7364 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7365 .unwrap();
7366 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
7367}
7368
7369#[gpui::test]
7370async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
7371 init_test(cx, |_| {});
7372
7373 let language = Arc::new(Language::new(
7374 LanguageConfig::default(),
7375 Some(tree_sitter_rust::LANGUAGE.into()),
7376 ));
7377
7378 let text = r#"
7379 use mod1::mod2::{mod3, mod4};
7380
7381 fn fn_1(param1: bool, param2: &str) {
7382 let var1 = "text";
7383 }
7384 "#
7385 .unindent();
7386
7387 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7388 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7389 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7390
7391 editor
7392 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7393 .await;
7394
7395 editor.update_in(cx, |editor, window, cx| {
7396 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7397 s.select_display_ranges([
7398 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
7399 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
7400 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
7401 ]);
7402 });
7403 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7404 });
7405 editor.update(cx, |editor, cx| {
7406 assert_text_with_selections(
7407 editor,
7408 indoc! {r#"
7409 use mod1::mod2::{mod3, «mod4ˇ»};
7410
7411 fn fn_1«ˇ(param1: bool, param2: &str)» {
7412 let var1 = "«ˇtext»";
7413 }
7414 "#},
7415 cx,
7416 );
7417 });
7418
7419 editor.update_in(cx, |editor, window, cx| {
7420 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7421 });
7422 editor.update(cx, |editor, cx| {
7423 assert_text_with_selections(
7424 editor,
7425 indoc! {r#"
7426 use mod1::mod2::«{mod3, mod4}ˇ»;
7427
7428 «ˇfn fn_1(param1: bool, param2: &str) {
7429 let var1 = "text";
7430 }»
7431 "#},
7432 cx,
7433 );
7434 });
7435
7436 editor.update_in(cx, |editor, window, cx| {
7437 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7438 });
7439 assert_eq!(
7440 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7441 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7442 );
7443
7444 // Trying to expand the selected syntax node one more time has no effect.
7445 editor.update_in(cx, |editor, window, cx| {
7446 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7447 });
7448 assert_eq!(
7449 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7450 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7451 );
7452
7453 editor.update_in(cx, |editor, window, cx| {
7454 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7455 });
7456 editor.update(cx, |editor, cx| {
7457 assert_text_with_selections(
7458 editor,
7459 indoc! {r#"
7460 use mod1::mod2::«{mod3, mod4}ˇ»;
7461
7462 «ˇfn fn_1(param1: bool, param2: &str) {
7463 let var1 = "text";
7464 }»
7465 "#},
7466 cx,
7467 );
7468 });
7469
7470 editor.update_in(cx, |editor, window, cx| {
7471 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7472 });
7473 editor.update(cx, |editor, cx| {
7474 assert_text_with_selections(
7475 editor,
7476 indoc! {r#"
7477 use mod1::mod2::{mod3, «mod4ˇ»};
7478
7479 fn fn_1«ˇ(param1: bool, param2: &str)» {
7480 let var1 = "«ˇtext»";
7481 }
7482 "#},
7483 cx,
7484 );
7485 });
7486
7487 editor.update_in(cx, |editor, window, cx| {
7488 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7489 });
7490 editor.update(cx, |editor, cx| {
7491 assert_text_with_selections(
7492 editor,
7493 indoc! {r#"
7494 use mod1::mod2::{mod3, mo«ˇ»d4};
7495
7496 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7497 let var1 = "te«ˇ»xt";
7498 }
7499 "#},
7500 cx,
7501 );
7502 });
7503
7504 // Trying to shrink the selected syntax node one more time has no effect.
7505 editor.update_in(cx, |editor, window, cx| {
7506 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7507 });
7508 editor.update_in(cx, |editor, _, cx| {
7509 assert_text_with_selections(
7510 editor,
7511 indoc! {r#"
7512 use mod1::mod2::{mod3, mo«ˇ»d4};
7513
7514 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7515 let var1 = "te«ˇ»xt";
7516 }
7517 "#},
7518 cx,
7519 );
7520 });
7521
7522 // Ensure that we keep expanding the selection if the larger selection starts or ends within
7523 // a fold.
7524 editor.update_in(cx, |editor, window, cx| {
7525 editor.fold_creases(
7526 vec![
7527 Crease::simple(
7528 Point::new(0, 21)..Point::new(0, 24),
7529 FoldPlaceholder::test(),
7530 ),
7531 Crease::simple(
7532 Point::new(3, 20)..Point::new(3, 22),
7533 FoldPlaceholder::test(),
7534 ),
7535 ],
7536 true,
7537 window,
7538 cx,
7539 );
7540 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7541 });
7542 editor.update(cx, |editor, cx| {
7543 assert_text_with_selections(
7544 editor,
7545 indoc! {r#"
7546 use mod1::mod2::«{mod3, mod4}ˇ»;
7547
7548 fn fn_1«ˇ(param1: bool, param2: &str)» {
7549 let var1 = "«ˇtext»";
7550 }
7551 "#},
7552 cx,
7553 );
7554 });
7555}
7556
7557#[gpui::test]
7558async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
7559 init_test(cx, |_| {});
7560
7561 let language = Arc::new(Language::new(
7562 LanguageConfig::default(),
7563 Some(tree_sitter_rust::LANGUAGE.into()),
7564 ));
7565
7566 let text = "let a = 2;";
7567
7568 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7569 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7570 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7571
7572 editor
7573 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7574 .await;
7575
7576 // Test case 1: Cursor at end of word
7577 editor.update_in(cx, |editor, window, cx| {
7578 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7579 s.select_display_ranges([
7580 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
7581 ]);
7582 });
7583 });
7584 editor.update(cx, |editor, cx| {
7585 assert_text_with_selections(editor, "let aˇ = 2;", cx);
7586 });
7587 editor.update_in(cx, |editor, window, cx| {
7588 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7589 });
7590 editor.update(cx, |editor, cx| {
7591 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
7592 });
7593 editor.update_in(cx, |editor, window, cx| {
7594 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7595 });
7596 editor.update(cx, |editor, cx| {
7597 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7598 });
7599
7600 // Test case 2: Cursor at end of statement
7601 editor.update_in(cx, |editor, window, cx| {
7602 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7603 s.select_display_ranges([
7604 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
7605 ]);
7606 });
7607 });
7608 editor.update(cx, |editor, cx| {
7609 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
7610 });
7611 editor.update_in(cx, |editor, window, cx| {
7612 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7613 });
7614 editor.update(cx, |editor, cx| {
7615 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7616 });
7617}
7618
7619#[gpui::test]
7620async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
7621 init_test(cx, |_| {});
7622
7623 let language = Arc::new(Language::new(
7624 LanguageConfig::default(),
7625 Some(tree_sitter_rust::LANGUAGE.into()),
7626 ));
7627
7628 let text = r#"
7629 use mod1::mod2::{mod3, mod4};
7630
7631 fn fn_1(param1: bool, param2: &str) {
7632 let var1 = "hello world";
7633 }
7634 "#
7635 .unindent();
7636
7637 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7638 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7639 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7640
7641 editor
7642 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7643 .await;
7644
7645 // Test 1: Cursor on a letter of a string word
7646 editor.update_in(cx, |editor, window, cx| {
7647 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7648 s.select_display_ranges([
7649 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
7650 ]);
7651 });
7652 });
7653 editor.update_in(cx, |editor, window, cx| {
7654 assert_text_with_selections(
7655 editor,
7656 indoc! {r#"
7657 use mod1::mod2::{mod3, mod4};
7658
7659 fn fn_1(param1: bool, param2: &str) {
7660 let var1 = "hˇello world";
7661 }
7662 "#},
7663 cx,
7664 );
7665 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7666 assert_text_with_selections(
7667 editor,
7668 indoc! {r#"
7669 use mod1::mod2::{mod3, mod4};
7670
7671 fn fn_1(param1: bool, param2: &str) {
7672 let var1 = "«ˇhello» world";
7673 }
7674 "#},
7675 cx,
7676 );
7677 });
7678
7679 // Test 2: Partial selection within a word
7680 editor.update_in(cx, |editor, window, cx| {
7681 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7682 s.select_display_ranges([
7683 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7684 ]);
7685 });
7686 });
7687 editor.update_in(cx, |editor, window, cx| {
7688 assert_text_with_selections(
7689 editor,
7690 indoc! {r#"
7691 use mod1::mod2::{mod3, mod4};
7692
7693 fn fn_1(param1: bool, param2: &str) {
7694 let var1 = "h«elˇ»lo world";
7695 }
7696 "#},
7697 cx,
7698 );
7699 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7700 assert_text_with_selections(
7701 editor,
7702 indoc! {r#"
7703 use mod1::mod2::{mod3, mod4};
7704
7705 fn fn_1(param1: bool, param2: &str) {
7706 let var1 = "«ˇhello» world";
7707 }
7708 "#},
7709 cx,
7710 );
7711 });
7712
7713 // Test 3: Complete word already selected
7714 editor.update_in(cx, |editor, window, cx| {
7715 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7716 s.select_display_ranges([
7717 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7718 ]);
7719 });
7720 });
7721 editor.update_in(cx, |editor, window, cx| {
7722 assert_text_with_selections(
7723 editor,
7724 indoc! {r#"
7725 use mod1::mod2::{mod3, mod4};
7726
7727 fn fn_1(param1: bool, param2: &str) {
7728 let var1 = "«helloˇ» world";
7729 }
7730 "#},
7731 cx,
7732 );
7733 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7734 assert_text_with_selections(
7735 editor,
7736 indoc! {r#"
7737 use mod1::mod2::{mod3, mod4};
7738
7739 fn fn_1(param1: bool, param2: &str) {
7740 let var1 = "«hello worldˇ»";
7741 }
7742 "#},
7743 cx,
7744 );
7745 });
7746
7747 // Test 4: Selection spanning across words
7748 editor.update_in(cx, |editor, window, cx| {
7749 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7750 s.select_display_ranges([
7751 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7752 ]);
7753 });
7754 });
7755 editor.update_in(cx, |editor, window, cx| {
7756 assert_text_with_selections(
7757 editor,
7758 indoc! {r#"
7759 use mod1::mod2::{mod3, mod4};
7760
7761 fn fn_1(param1: bool, param2: &str) {
7762 let var1 = "hel«lo woˇ»rld";
7763 }
7764 "#},
7765 cx,
7766 );
7767 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7768 assert_text_with_selections(
7769 editor,
7770 indoc! {r#"
7771 use mod1::mod2::{mod3, mod4};
7772
7773 fn fn_1(param1: bool, param2: &str) {
7774 let var1 = "«ˇhello world»";
7775 }
7776 "#},
7777 cx,
7778 );
7779 });
7780
7781 // Test 5: Expansion beyond string
7782 editor.update_in(cx, |editor, window, cx| {
7783 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7784 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7785 assert_text_with_selections(
7786 editor,
7787 indoc! {r#"
7788 use mod1::mod2::{mod3, mod4};
7789
7790 fn fn_1(param1: bool, param2: &str) {
7791 «ˇlet var1 = "hello world";»
7792 }
7793 "#},
7794 cx,
7795 );
7796 });
7797}
7798
7799#[gpui::test]
7800async fn test_fold_function_bodies(cx: &mut TestAppContext) {
7801 init_test(cx, |_| {});
7802
7803 let base_text = r#"
7804 impl A {
7805 // this is an uncommitted comment
7806
7807 fn b() {
7808 c();
7809 }
7810
7811 // this is another uncommitted comment
7812
7813 fn d() {
7814 // e
7815 // f
7816 }
7817 }
7818
7819 fn g() {
7820 // h
7821 }
7822 "#
7823 .unindent();
7824
7825 let text = r#"
7826 ˇimpl A {
7827
7828 fn b() {
7829 c();
7830 }
7831
7832 fn d() {
7833 // e
7834 // f
7835 }
7836 }
7837
7838 fn g() {
7839 // h
7840 }
7841 "#
7842 .unindent();
7843
7844 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7845 cx.set_state(&text);
7846 cx.set_head_text(&base_text);
7847 cx.update_editor(|editor, window, cx| {
7848 editor.expand_all_diff_hunks(&Default::default(), window, cx);
7849 });
7850
7851 cx.assert_state_with_diff(
7852 "
7853 ˇimpl A {
7854 - // this is an uncommitted comment
7855
7856 fn b() {
7857 c();
7858 }
7859
7860 - // this is another uncommitted comment
7861 -
7862 fn d() {
7863 // e
7864 // f
7865 }
7866 }
7867
7868 fn g() {
7869 // h
7870 }
7871 "
7872 .unindent(),
7873 );
7874
7875 let expected_display_text = "
7876 impl A {
7877 // this is an uncommitted comment
7878
7879 fn b() {
7880 ⋯
7881 }
7882
7883 // this is another uncommitted comment
7884
7885 fn d() {
7886 ⋯
7887 }
7888 }
7889
7890 fn g() {
7891 ⋯
7892 }
7893 "
7894 .unindent();
7895
7896 cx.update_editor(|editor, window, cx| {
7897 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
7898 assert_eq!(editor.display_text(cx), expected_display_text);
7899 });
7900}
7901
7902#[gpui::test]
7903async fn test_autoindent(cx: &mut TestAppContext) {
7904 init_test(cx, |_| {});
7905
7906 let language = Arc::new(
7907 Language::new(
7908 LanguageConfig {
7909 brackets: BracketPairConfig {
7910 pairs: vec![
7911 BracketPair {
7912 start: "{".to_string(),
7913 end: "}".to_string(),
7914 close: false,
7915 surround: false,
7916 newline: true,
7917 },
7918 BracketPair {
7919 start: "(".to_string(),
7920 end: ")".to_string(),
7921 close: false,
7922 surround: false,
7923 newline: true,
7924 },
7925 ],
7926 ..Default::default()
7927 },
7928 ..Default::default()
7929 },
7930 Some(tree_sitter_rust::LANGUAGE.into()),
7931 )
7932 .with_indents_query(
7933 r#"
7934 (_ "(" ")" @end) @indent
7935 (_ "{" "}" @end) @indent
7936 "#,
7937 )
7938 .unwrap(),
7939 );
7940
7941 let text = "fn a() {}";
7942
7943 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7944 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7945 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7946 editor
7947 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7948 .await;
7949
7950 editor.update_in(cx, |editor, window, cx| {
7951 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7952 s.select_ranges([5..5, 8..8, 9..9])
7953 });
7954 editor.newline(&Newline, window, cx);
7955 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
7956 assert_eq!(
7957 editor.selections.ranges(cx),
7958 &[
7959 Point::new(1, 4)..Point::new(1, 4),
7960 Point::new(3, 4)..Point::new(3, 4),
7961 Point::new(5, 0)..Point::new(5, 0)
7962 ]
7963 );
7964 });
7965}
7966
7967#[gpui::test]
7968async fn test_autoindent_selections(cx: &mut TestAppContext) {
7969 init_test(cx, |_| {});
7970
7971 {
7972 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7973 cx.set_state(indoc! {"
7974 impl A {
7975
7976 fn b() {}
7977
7978 «fn c() {
7979
7980 }ˇ»
7981 }
7982 "});
7983
7984 cx.update_editor(|editor, window, cx| {
7985 editor.autoindent(&Default::default(), window, cx);
7986 });
7987
7988 cx.assert_editor_state(indoc! {"
7989 impl A {
7990
7991 fn b() {}
7992
7993 «fn c() {
7994
7995 }ˇ»
7996 }
7997 "});
7998 }
7999
8000 {
8001 let mut cx = EditorTestContext::new_multibuffer(
8002 cx,
8003 [indoc! { "
8004 impl A {
8005 «
8006 // a
8007 fn b(){}
8008 »
8009 «
8010 }
8011 fn c(){}
8012 »
8013 "}],
8014 );
8015
8016 let buffer = cx.update_editor(|editor, _, cx| {
8017 let buffer = editor.buffer().update(cx, |buffer, _| {
8018 buffer.all_buffers().iter().next().unwrap().clone()
8019 });
8020 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
8021 buffer
8022 });
8023
8024 cx.run_until_parked();
8025 cx.update_editor(|editor, window, cx| {
8026 editor.select_all(&Default::default(), window, cx);
8027 editor.autoindent(&Default::default(), window, cx)
8028 });
8029 cx.run_until_parked();
8030
8031 cx.update(|_, cx| {
8032 assert_eq!(
8033 buffer.read(cx).text(),
8034 indoc! { "
8035 impl A {
8036
8037 // a
8038 fn b(){}
8039
8040
8041 }
8042 fn c(){}
8043
8044 " }
8045 )
8046 });
8047 }
8048}
8049
8050#[gpui::test]
8051async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
8052 init_test(cx, |_| {});
8053
8054 let mut cx = EditorTestContext::new(cx).await;
8055
8056 let language = Arc::new(Language::new(
8057 LanguageConfig {
8058 brackets: BracketPairConfig {
8059 pairs: vec![
8060 BracketPair {
8061 start: "{".to_string(),
8062 end: "}".to_string(),
8063 close: true,
8064 surround: true,
8065 newline: true,
8066 },
8067 BracketPair {
8068 start: "(".to_string(),
8069 end: ")".to_string(),
8070 close: true,
8071 surround: true,
8072 newline: true,
8073 },
8074 BracketPair {
8075 start: "/*".to_string(),
8076 end: " */".to_string(),
8077 close: true,
8078 surround: true,
8079 newline: true,
8080 },
8081 BracketPair {
8082 start: "[".to_string(),
8083 end: "]".to_string(),
8084 close: false,
8085 surround: false,
8086 newline: true,
8087 },
8088 BracketPair {
8089 start: "\"".to_string(),
8090 end: "\"".to_string(),
8091 close: true,
8092 surround: true,
8093 newline: false,
8094 },
8095 BracketPair {
8096 start: "<".to_string(),
8097 end: ">".to_string(),
8098 close: false,
8099 surround: true,
8100 newline: true,
8101 },
8102 ],
8103 ..Default::default()
8104 },
8105 autoclose_before: "})]".to_string(),
8106 ..Default::default()
8107 },
8108 Some(tree_sitter_rust::LANGUAGE.into()),
8109 ));
8110
8111 cx.language_registry().add(language.clone());
8112 cx.update_buffer(|buffer, cx| {
8113 buffer.set_language(Some(language), cx);
8114 });
8115
8116 cx.set_state(
8117 &r#"
8118 🏀ˇ
8119 εˇ
8120 ❤️ˇ
8121 "#
8122 .unindent(),
8123 );
8124
8125 // autoclose multiple nested brackets at multiple cursors
8126 cx.update_editor(|editor, window, cx| {
8127 editor.handle_input("{", window, cx);
8128 editor.handle_input("{", window, cx);
8129 editor.handle_input("{", window, cx);
8130 });
8131 cx.assert_editor_state(
8132 &"
8133 🏀{{{ˇ}}}
8134 ε{{{ˇ}}}
8135 ❤️{{{ˇ}}}
8136 "
8137 .unindent(),
8138 );
8139
8140 // insert a different closing bracket
8141 cx.update_editor(|editor, window, cx| {
8142 editor.handle_input(")", window, cx);
8143 });
8144 cx.assert_editor_state(
8145 &"
8146 🏀{{{)ˇ}}}
8147 ε{{{)ˇ}}}
8148 ❤️{{{)ˇ}}}
8149 "
8150 .unindent(),
8151 );
8152
8153 // skip over the auto-closed brackets when typing a closing bracket
8154 cx.update_editor(|editor, window, cx| {
8155 editor.move_right(&MoveRight, window, cx);
8156 editor.handle_input("}", window, cx);
8157 editor.handle_input("}", window, cx);
8158 editor.handle_input("}", window, cx);
8159 });
8160 cx.assert_editor_state(
8161 &"
8162 🏀{{{)}}}}ˇ
8163 ε{{{)}}}}ˇ
8164 ❤️{{{)}}}}ˇ
8165 "
8166 .unindent(),
8167 );
8168
8169 // autoclose multi-character pairs
8170 cx.set_state(
8171 &"
8172 ˇ
8173 ˇ
8174 "
8175 .unindent(),
8176 );
8177 cx.update_editor(|editor, window, cx| {
8178 editor.handle_input("/", window, cx);
8179 editor.handle_input("*", window, cx);
8180 });
8181 cx.assert_editor_state(
8182 &"
8183 /*ˇ */
8184 /*ˇ */
8185 "
8186 .unindent(),
8187 );
8188
8189 // one cursor autocloses a multi-character pair, one cursor
8190 // does not autoclose.
8191 cx.set_state(
8192 &"
8193 /ˇ
8194 ˇ
8195 "
8196 .unindent(),
8197 );
8198 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
8199 cx.assert_editor_state(
8200 &"
8201 /*ˇ */
8202 *ˇ
8203 "
8204 .unindent(),
8205 );
8206
8207 // Don't autoclose if the next character isn't whitespace and isn't
8208 // listed in the language's "autoclose_before" section.
8209 cx.set_state("ˇa b");
8210 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8211 cx.assert_editor_state("{ˇa b");
8212
8213 // Don't autoclose if `close` is false for the bracket pair
8214 cx.set_state("ˇ");
8215 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
8216 cx.assert_editor_state("[ˇ");
8217
8218 // Surround with brackets if text is selected
8219 cx.set_state("«aˇ» b");
8220 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8221 cx.assert_editor_state("{«aˇ»} b");
8222
8223 // Autoclose when not immediately after a word character
8224 cx.set_state("a ˇ");
8225 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8226 cx.assert_editor_state("a \"ˇ\"");
8227
8228 // Autoclose pair where the start and end characters are the same
8229 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8230 cx.assert_editor_state("a \"\"ˇ");
8231
8232 // Don't autoclose when immediately after a word character
8233 cx.set_state("aˇ");
8234 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8235 cx.assert_editor_state("a\"ˇ");
8236
8237 // Do autoclose when after a non-word character
8238 cx.set_state("{ˇ");
8239 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8240 cx.assert_editor_state("{\"ˇ\"");
8241
8242 // Non identical pairs autoclose regardless of preceding character
8243 cx.set_state("aˇ");
8244 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8245 cx.assert_editor_state("a{ˇ}");
8246
8247 // Don't autoclose pair if autoclose is disabled
8248 cx.set_state("ˇ");
8249 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8250 cx.assert_editor_state("<ˇ");
8251
8252 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
8253 cx.set_state("«aˇ» b");
8254 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8255 cx.assert_editor_state("<«aˇ»> b");
8256}
8257
8258#[gpui::test]
8259async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
8260 init_test(cx, |settings| {
8261 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8262 });
8263
8264 let mut cx = EditorTestContext::new(cx).await;
8265
8266 let language = Arc::new(Language::new(
8267 LanguageConfig {
8268 brackets: BracketPairConfig {
8269 pairs: vec![
8270 BracketPair {
8271 start: "{".to_string(),
8272 end: "}".to_string(),
8273 close: true,
8274 surround: true,
8275 newline: true,
8276 },
8277 BracketPair {
8278 start: "(".to_string(),
8279 end: ")".to_string(),
8280 close: true,
8281 surround: true,
8282 newline: true,
8283 },
8284 BracketPair {
8285 start: "[".to_string(),
8286 end: "]".to_string(),
8287 close: false,
8288 surround: false,
8289 newline: true,
8290 },
8291 ],
8292 ..Default::default()
8293 },
8294 autoclose_before: "})]".to_string(),
8295 ..Default::default()
8296 },
8297 Some(tree_sitter_rust::LANGUAGE.into()),
8298 ));
8299
8300 cx.language_registry().add(language.clone());
8301 cx.update_buffer(|buffer, cx| {
8302 buffer.set_language(Some(language), cx);
8303 });
8304
8305 cx.set_state(
8306 &"
8307 ˇ
8308 ˇ
8309 ˇ
8310 "
8311 .unindent(),
8312 );
8313
8314 // ensure only matching closing brackets are skipped over
8315 cx.update_editor(|editor, window, cx| {
8316 editor.handle_input("}", window, cx);
8317 editor.move_left(&MoveLeft, window, cx);
8318 editor.handle_input(")", window, cx);
8319 editor.move_left(&MoveLeft, window, cx);
8320 });
8321 cx.assert_editor_state(
8322 &"
8323 ˇ)}
8324 ˇ)}
8325 ˇ)}
8326 "
8327 .unindent(),
8328 );
8329
8330 // skip-over closing brackets at multiple cursors
8331 cx.update_editor(|editor, window, cx| {
8332 editor.handle_input(")", window, cx);
8333 editor.handle_input("}", window, cx);
8334 });
8335 cx.assert_editor_state(
8336 &"
8337 )}ˇ
8338 )}ˇ
8339 )}ˇ
8340 "
8341 .unindent(),
8342 );
8343
8344 // ignore non-close brackets
8345 cx.update_editor(|editor, window, cx| {
8346 editor.handle_input("]", window, cx);
8347 editor.move_left(&MoveLeft, window, cx);
8348 editor.handle_input("]", window, cx);
8349 });
8350 cx.assert_editor_state(
8351 &"
8352 )}]ˇ]
8353 )}]ˇ]
8354 )}]ˇ]
8355 "
8356 .unindent(),
8357 );
8358}
8359
8360#[gpui::test]
8361async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8362 init_test(cx, |_| {});
8363
8364 let mut cx = EditorTestContext::new(cx).await;
8365
8366 let html_language = Arc::new(
8367 Language::new(
8368 LanguageConfig {
8369 name: "HTML".into(),
8370 brackets: BracketPairConfig {
8371 pairs: vec![
8372 BracketPair {
8373 start: "<".into(),
8374 end: ">".into(),
8375 close: true,
8376 ..Default::default()
8377 },
8378 BracketPair {
8379 start: "{".into(),
8380 end: "}".into(),
8381 close: true,
8382 ..Default::default()
8383 },
8384 BracketPair {
8385 start: "(".into(),
8386 end: ")".into(),
8387 close: true,
8388 ..Default::default()
8389 },
8390 ],
8391 ..Default::default()
8392 },
8393 autoclose_before: "})]>".into(),
8394 ..Default::default()
8395 },
8396 Some(tree_sitter_html::LANGUAGE.into()),
8397 )
8398 .with_injection_query(
8399 r#"
8400 (script_element
8401 (raw_text) @injection.content
8402 (#set! injection.language "javascript"))
8403 "#,
8404 )
8405 .unwrap(),
8406 );
8407
8408 let javascript_language = Arc::new(Language::new(
8409 LanguageConfig {
8410 name: "JavaScript".into(),
8411 brackets: BracketPairConfig {
8412 pairs: vec![
8413 BracketPair {
8414 start: "/*".into(),
8415 end: " */".into(),
8416 close: true,
8417 ..Default::default()
8418 },
8419 BracketPair {
8420 start: "{".into(),
8421 end: "}".into(),
8422 close: true,
8423 ..Default::default()
8424 },
8425 BracketPair {
8426 start: "(".into(),
8427 end: ")".into(),
8428 close: true,
8429 ..Default::default()
8430 },
8431 ],
8432 ..Default::default()
8433 },
8434 autoclose_before: "})]>".into(),
8435 ..Default::default()
8436 },
8437 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8438 ));
8439
8440 cx.language_registry().add(html_language.clone());
8441 cx.language_registry().add(javascript_language.clone());
8442
8443 cx.update_buffer(|buffer, cx| {
8444 buffer.set_language(Some(html_language), cx);
8445 });
8446
8447 cx.set_state(
8448 &r#"
8449 <body>ˇ
8450 <script>
8451 var x = 1;ˇ
8452 </script>
8453 </body>ˇ
8454 "#
8455 .unindent(),
8456 );
8457
8458 // Precondition: different languages are active at different locations.
8459 cx.update_editor(|editor, window, cx| {
8460 let snapshot = editor.snapshot(window, cx);
8461 let cursors = editor.selections.ranges::<usize>(cx);
8462 let languages = cursors
8463 .iter()
8464 .map(|c| snapshot.language_at(c.start).unwrap().name())
8465 .collect::<Vec<_>>();
8466 assert_eq!(
8467 languages,
8468 &["HTML".into(), "JavaScript".into(), "HTML".into()]
8469 );
8470 });
8471
8472 // Angle brackets autoclose in HTML, but not JavaScript.
8473 cx.update_editor(|editor, window, cx| {
8474 editor.handle_input("<", window, cx);
8475 editor.handle_input("a", window, cx);
8476 });
8477 cx.assert_editor_state(
8478 &r#"
8479 <body><aˇ>
8480 <script>
8481 var x = 1;<aˇ
8482 </script>
8483 </body><aˇ>
8484 "#
8485 .unindent(),
8486 );
8487
8488 // Curly braces and parens autoclose in both HTML and JavaScript.
8489 cx.update_editor(|editor, window, cx| {
8490 editor.handle_input(" b=", window, cx);
8491 editor.handle_input("{", window, cx);
8492 editor.handle_input("c", window, cx);
8493 editor.handle_input("(", window, cx);
8494 });
8495 cx.assert_editor_state(
8496 &r#"
8497 <body><a b={c(ˇ)}>
8498 <script>
8499 var x = 1;<a b={c(ˇ)}
8500 </script>
8501 </body><a b={c(ˇ)}>
8502 "#
8503 .unindent(),
8504 );
8505
8506 // Brackets that were already autoclosed are skipped.
8507 cx.update_editor(|editor, window, cx| {
8508 editor.handle_input(")", window, cx);
8509 editor.handle_input("d", window, cx);
8510 editor.handle_input("}", window, cx);
8511 });
8512 cx.assert_editor_state(
8513 &r#"
8514 <body><a b={c()d}ˇ>
8515 <script>
8516 var x = 1;<a b={c()d}ˇ
8517 </script>
8518 </body><a b={c()d}ˇ>
8519 "#
8520 .unindent(),
8521 );
8522 cx.update_editor(|editor, window, cx| {
8523 editor.handle_input(">", window, cx);
8524 });
8525 cx.assert_editor_state(
8526 &r#"
8527 <body><a b={c()d}>ˇ
8528 <script>
8529 var x = 1;<a b={c()d}>ˇ
8530 </script>
8531 </body><a b={c()d}>ˇ
8532 "#
8533 .unindent(),
8534 );
8535
8536 // Reset
8537 cx.set_state(
8538 &r#"
8539 <body>ˇ
8540 <script>
8541 var x = 1;ˇ
8542 </script>
8543 </body>ˇ
8544 "#
8545 .unindent(),
8546 );
8547
8548 cx.update_editor(|editor, window, cx| {
8549 editor.handle_input("<", window, cx);
8550 });
8551 cx.assert_editor_state(
8552 &r#"
8553 <body><ˇ>
8554 <script>
8555 var x = 1;<ˇ
8556 </script>
8557 </body><ˇ>
8558 "#
8559 .unindent(),
8560 );
8561
8562 // When backspacing, the closing angle brackets are removed.
8563 cx.update_editor(|editor, window, cx| {
8564 editor.backspace(&Backspace, window, cx);
8565 });
8566 cx.assert_editor_state(
8567 &r#"
8568 <body>ˇ
8569 <script>
8570 var x = 1;ˇ
8571 </script>
8572 </body>ˇ
8573 "#
8574 .unindent(),
8575 );
8576
8577 // Block comments autoclose in JavaScript, but not HTML.
8578 cx.update_editor(|editor, window, cx| {
8579 editor.handle_input("/", window, cx);
8580 editor.handle_input("*", window, cx);
8581 });
8582 cx.assert_editor_state(
8583 &r#"
8584 <body>/*ˇ
8585 <script>
8586 var x = 1;/*ˇ */
8587 </script>
8588 </body>/*ˇ
8589 "#
8590 .unindent(),
8591 );
8592}
8593
8594#[gpui::test]
8595async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
8596 init_test(cx, |_| {});
8597
8598 let mut cx = EditorTestContext::new(cx).await;
8599
8600 let rust_language = Arc::new(
8601 Language::new(
8602 LanguageConfig {
8603 name: "Rust".into(),
8604 brackets: serde_json::from_value(json!([
8605 { "start": "{", "end": "}", "close": true, "newline": true },
8606 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
8607 ]))
8608 .unwrap(),
8609 autoclose_before: "})]>".into(),
8610 ..Default::default()
8611 },
8612 Some(tree_sitter_rust::LANGUAGE.into()),
8613 )
8614 .with_override_query("(string_literal) @string")
8615 .unwrap(),
8616 );
8617
8618 cx.language_registry().add(rust_language.clone());
8619 cx.update_buffer(|buffer, cx| {
8620 buffer.set_language(Some(rust_language), cx);
8621 });
8622
8623 cx.set_state(
8624 &r#"
8625 let x = ˇ
8626 "#
8627 .unindent(),
8628 );
8629
8630 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
8631 cx.update_editor(|editor, window, cx| {
8632 editor.handle_input("\"", window, cx);
8633 });
8634 cx.assert_editor_state(
8635 &r#"
8636 let x = "ˇ"
8637 "#
8638 .unindent(),
8639 );
8640
8641 // Inserting another quotation mark. The cursor moves across the existing
8642 // automatically-inserted quotation mark.
8643 cx.update_editor(|editor, window, cx| {
8644 editor.handle_input("\"", window, cx);
8645 });
8646 cx.assert_editor_state(
8647 &r#"
8648 let x = ""ˇ
8649 "#
8650 .unindent(),
8651 );
8652
8653 // Reset
8654 cx.set_state(
8655 &r#"
8656 let x = ˇ
8657 "#
8658 .unindent(),
8659 );
8660
8661 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
8662 cx.update_editor(|editor, window, cx| {
8663 editor.handle_input("\"", window, cx);
8664 editor.handle_input(" ", window, cx);
8665 editor.move_left(&Default::default(), window, cx);
8666 editor.handle_input("\\", window, cx);
8667 editor.handle_input("\"", window, cx);
8668 });
8669 cx.assert_editor_state(
8670 &r#"
8671 let x = "\"ˇ "
8672 "#
8673 .unindent(),
8674 );
8675
8676 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8677 // mark. Nothing is inserted.
8678 cx.update_editor(|editor, window, cx| {
8679 editor.move_right(&Default::default(), window, cx);
8680 editor.handle_input("\"", window, cx);
8681 });
8682 cx.assert_editor_state(
8683 &r#"
8684 let x = "\" "ˇ
8685 "#
8686 .unindent(),
8687 );
8688}
8689
8690#[gpui::test]
8691async fn test_surround_with_pair(cx: &mut TestAppContext) {
8692 init_test(cx, |_| {});
8693
8694 let language = Arc::new(Language::new(
8695 LanguageConfig {
8696 brackets: BracketPairConfig {
8697 pairs: vec![
8698 BracketPair {
8699 start: "{".to_string(),
8700 end: "}".to_string(),
8701 close: true,
8702 surround: true,
8703 newline: true,
8704 },
8705 BracketPair {
8706 start: "/* ".to_string(),
8707 end: "*/".to_string(),
8708 close: true,
8709 surround: true,
8710 ..Default::default()
8711 },
8712 ],
8713 ..Default::default()
8714 },
8715 ..Default::default()
8716 },
8717 Some(tree_sitter_rust::LANGUAGE.into()),
8718 ));
8719
8720 let text = r#"
8721 a
8722 b
8723 c
8724 "#
8725 .unindent();
8726
8727 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8728 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8729 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8730 editor
8731 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8732 .await;
8733
8734 editor.update_in(cx, |editor, window, cx| {
8735 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8736 s.select_display_ranges([
8737 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8738 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8739 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8740 ])
8741 });
8742
8743 editor.handle_input("{", window, cx);
8744 editor.handle_input("{", window, cx);
8745 editor.handle_input("{", window, cx);
8746 assert_eq!(
8747 editor.text(cx),
8748 "
8749 {{{a}}}
8750 {{{b}}}
8751 {{{c}}}
8752 "
8753 .unindent()
8754 );
8755 assert_eq!(
8756 editor.selections.display_ranges(cx),
8757 [
8758 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
8759 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
8760 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
8761 ]
8762 );
8763
8764 editor.undo(&Undo, window, cx);
8765 editor.undo(&Undo, window, cx);
8766 editor.undo(&Undo, window, cx);
8767 assert_eq!(
8768 editor.text(cx),
8769 "
8770 a
8771 b
8772 c
8773 "
8774 .unindent()
8775 );
8776 assert_eq!(
8777 editor.selections.display_ranges(cx),
8778 [
8779 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8780 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8781 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8782 ]
8783 );
8784
8785 // Ensure inserting the first character of a multi-byte bracket pair
8786 // doesn't surround the selections with the bracket.
8787 editor.handle_input("/", window, cx);
8788 assert_eq!(
8789 editor.text(cx),
8790 "
8791 /
8792 /
8793 /
8794 "
8795 .unindent()
8796 );
8797 assert_eq!(
8798 editor.selections.display_ranges(cx),
8799 [
8800 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8801 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8802 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8803 ]
8804 );
8805
8806 editor.undo(&Undo, window, cx);
8807 assert_eq!(
8808 editor.text(cx),
8809 "
8810 a
8811 b
8812 c
8813 "
8814 .unindent()
8815 );
8816 assert_eq!(
8817 editor.selections.display_ranges(cx),
8818 [
8819 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8820 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8821 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8822 ]
8823 );
8824
8825 // Ensure inserting the last character of a multi-byte bracket pair
8826 // doesn't surround the selections with the bracket.
8827 editor.handle_input("*", window, cx);
8828 assert_eq!(
8829 editor.text(cx),
8830 "
8831 *
8832 *
8833 *
8834 "
8835 .unindent()
8836 );
8837 assert_eq!(
8838 editor.selections.display_ranges(cx),
8839 [
8840 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8841 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8842 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8843 ]
8844 );
8845 });
8846}
8847
8848#[gpui::test]
8849async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
8850 init_test(cx, |_| {});
8851
8852 let language = Arc::new(Language::new(
8853 LanguageConfig {
8854 brackets: BracketPairConfig {
8855 pairs: vec![BracketPair {
8856 start: "{".to_string(),
8857 end: "}".to_string(),
8858 close: true,
8859 surround: true,
8860 newline: true,
8861 }],
8862 ..Default::default()
8863 },
8864 autoclose_before: "}".to_string(),
8865 ..Default::default()
8866 },
8867 Some(tree_sitter_rust::LANGUAGE.into()),
8868 ));
8869
8870 let text = r#"
8871 a
8872 b
8873 c
8874 "#
8875 .unindent();
8876
8877 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8878 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8879 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8880 editor
8881 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8882 .await;
8883
8884 editor.update_in(cx, |editor, window, cx| {
8885 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8886 s.select_ranges([
8887 Point::new(0, 1)..Point::new(0, 1),
8888 Point::new(1, 1)..Point::new(1, 1),
8889 Point::new(2, 1)..Point::new(2, 1),
8890 ])
8891 });
8892
8893 editor.handle_input("{", window, cx);
8894 editor.handle_input("{", window, cx);
8895 editor.handle_input("_", window, cx);
8896 assert_eq!(
8897 editor.text(cx),
8898 "
8899 a{{_}}
8900 b{{_}}
8901 c{{_}}
8902 "
8903 .unindent()
8904 );
8905 assert_eq!(
8906 editor.selections.ranges::<Point>(cx),
8907 [
8908 Point::new(0, 4)..Point::new(0, 4),
8909 Point::new(1, 4)..Point::new(1, 4),
8910 Point::new(2, 4)..Point::new(2, 4)
8911 ]
8912 );
8913
8914 editor.backspace(&Default::default(), window, cx);
8915 editor.backspace(&Default::default(), window, cx);
8916 assert_eq!(
8917 editor.text(cx),
8918 "
8919 a{}
8920 b{}
8921 c{}
8922 "
8923 .unindent()
8924 );
8925 assert_eq!(
8926 editor.selections.ranges::<Point>(cx),
8927 [
8928 Point::new(0, 2)..Point::new(0, 2),
8929 Point::new(1, 2)..Point::new(1, 2),
8930 Point::new(2, 2)..Point::new(2, 2)
8931 ]
8932 );
8933
8934 editor.delete_to_previous_word_start(&Default::default(), window, cx);
8935 assert_eq!(
8936 editor.text(cx),
8937 "
8938 a
8939 b
8940 c
8941 "
8942 .unindent()
8943 );
8944 assert_eq!(
8945 editor.selections.ranges::<Point>(cx),
8946 [
8947 Point::new(0, 1)..Point::new(0, 1),
8948 Point::new(1, 1)..Point::new(1, 1),
8949 Point::new(2, 1)..Point::new(2, 1)
8950 ]
8951 );
8952 });
8953}
8954
8955#[gpui::test]
8956async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
8957 init_test(cx, |settings| {
8958 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8959 });
8960
8961 let mut cx = EditorTestContext::new(cx).await;
8962
8963 let language = Arc::new(Language::new(
8964 LanguageConfig {
8965 brackets: BracketPairConfig {
8966 pairs: vec![
8967 BracketPair {
8968 start: "{".to_string(),
8969 end: "}".to_string(),
8970 close: true,
8971 surround: true,
8972 newline: true,
8973 },
8974 BracketPair {
8975 start: "(".to_string(),
8976 end: ")".to_string(),
8977 close: true,
8978 surround: true,
8979 newline: true,
8980 },
8981 BracketPair {
8982 start: "[".to_string(),
8983 end: "]".to_string(),
8984 close: false,
8985 surround: true,
8986 newline: true,
8987 },
8988 ],
8989 ..Default::default()
8990 },
8991 autoclose_before: "})]".to_string(),
8992 ..Default::default()
8993 },
8994 Some(tree_sitter_rust::LANGUAGE.into()),
8995 ));
8996
8997 cx.language_registry().add(language.clone());
8998 cx.update_buffer(|buffer, cx| {
8999 buffer.set_language(Some(language), cx);
9000 });
9001
9002 cx.set_state(
9003 &"
9004 {(ˇ)}
9005 [[ˇ]]
9006 {(ˇ)}
9007 "
9008 .unindent(),
9009 );
9010
9011 cx.update_editor(|editor, window, cx| {
9012 editor.backspace(&Default::default(), window, cx);
9013 editor.backspace(&Default::default(), window, cx);
9014 });
9015
9016 cx.assert_editor_state(
9017 &"
9018 ˇ
9019 ˇ]]
9020 ˇ
9021 "
9022 .unindent(),
9023 );
9024
9025 cx.update_editor(|editor, window, cx| {
9026 editor.handle_input("{", window, cx);
9027 editor.handle_input("{", window, cx);
9028 editor.move_right(&MoveRight, window, cx);
9029 editor.move_right(&MoveRight, window, cx);
9030 editor.move_left(&MoveLeft, window, cx);
9031 editor.move_left(&MoveLeft, window, cx);
9032 editor.backspace(&Default::default(), window, cx);
9033 });
9034
9035 cx.assert_editor_state(
9036 &"
9037 {ˇ}
9038 {ˇ}]]
9039 {ˇ}
9040 "
9041 .unindent(),
9042 );
9043
9044 cx.update_editor(|editor, window, cx| {
9045 editor.backspace(&Default::default(), window, cx);
9046 });
9047
9048 cx.assert_editor_state(
9049 &"
9050 ˇ
9051 ˇ]]
9052 ˇ
9053 "
9054 .unindent(),
9055 );
9056}
9057
9058#[gpui::test]
9059async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
9060 init_test(cx, |_| {});
9061
9062 let language = Arc::new(Language::new(
9063 LanguageConfig::default(),
9064 Some(tree_sitter_rust::LANGUAGE.into()),
9065 ));
9066
9067 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
9068 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9069 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9070 editor
9071 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9072 .await;
9073
9074 editor.update_in(cx, |editor, window, cx| {
9075 editor.set_auto_replace_emoji_shortcode(true);
9076
9077 editor.handle_input("Hello ", window, cx);
9078 editor.handle_input(":wave", window, cx);
9079 assert_eq!(editor.text(cx), "Hello :wave".unindent());
9080
9081 editor.handle_input(":", window, cx);
9082 assert_eq!(editor.text(cx), "Hello 👋".unindent());
9083
9084 editor.handle_input(" :smile", window, cx);
9085 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
9086
9087 editor.handle_input(":", window, cx);
9088 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
9089
9090 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
9091 editor.handle_input(":wave", window, cx);
9092 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
9093
9094 editor.handle_input(":", window, cx);
9095 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
9096
9097 editor.handle_input(":1", window, cx);
9098 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
9099
9100 editor.handle_input(":", window, cx);
9101 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
9102
9103 // Ensure shortcode does not get replaced when it is part of a word
9104 editor.handle_input(" Test:wave", window, cx);
9105 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
9106
9107 editor.handle_input(":", window, cx);
9108 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
9109
9110 editor.set_auto_replace_emoji_shortcode(false);
9111
9112 // Ensure shortcode does not get replaced when auto replace is off
9113 editor.handle_input(" :wave", window, cx);
9114 assert_eq!(
9115 editor.text(cx),
9116 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
9117 );
9118
9119 editor.handle_input(":", window, cx);
9120 assert_eq!(
9121 editor.text(cx),
9122 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
9123 );
9124 });
9125}
9126
9127#[gpui::test]
9128async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
9129 init_test(cx, |_| {});
9130
9131 let (text, insertion_ranges) = marked_text_ranges(
9132 indoc! {"
9133 ˇ
9134 "},
9135 false,
9136 );
9137
9138 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
9139 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9140
9141 _ = editor.update_in(cx, |editor, window, cx| {
9142 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
9143
9144 editor
9145 .insert_snippet(&insertion_ranges, snippet, window, cx)
9146 .unwrap();
9147
9148 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
9149 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
9150 assert_eq!(editor.text(cx), expected_text);
9151 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
9152 }
9153
9154 assert(
9155 editor,
9156 cx,
9157 indoc! {"
9158 type «» =•
9159 "},
9160 );
9161
9162 assert!(editor.context_menu_visible(), "There should be a matches");
9163 });
9164}
9165
9166#[gpui::test]
9167async fn test_snippets(cx: &mut TestAppContext) {
9168 init_test(cx, |_| {});
9169
9170 let mut cx = EditorTestContext::new(cx).await;
9171
9172 cx.set_state(indoc! {"
9173 a.ˇ b
9174 a.ˇ b
9175 a.ˇ b
9176 "});
9177
9178 cx.update_editor(|editor, window, cx| {
9179 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
9180 let insertion_ranges = editor
9181 .selections
9182 .all(cx)
9183 .iter()
9184 .map(|s| s.range().clone())
9185 .collect::<Vec<_>>();
9186 editor
9187 .insert_snippet(&insertion_ranges, snippet, window, cx)
9188 .unwrap();
9189 });
9190
9191 cx.assert_editor_state(indoc! {"
9192 a.f(«oneˇ», two, «threeˇ») b
9193 a.f(«oneˇ», two, «threeˇ») b
9194 a.f(«oneˇ», two, «threeˇ») b
9195 "});
9196
9197 // Can't move earlier than the first tab stop
9198 cx.update_editor(|editor, window, cx| {
9199 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9200 });
9201 cx.assert_editor_state(indoc! {"
9202 a.f(«oneˇ», two, «threeˇ») b
9203 a.f(«oneˇ», two, «threeˇ») b
9204 a.f(«oneˇ», two, «threeˇ») b
9205 "});
9206
9207 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9208 cx.assert_editor_state(indoc! {"
9209 a.f(one, «twoˇ», three) b
9210 a.f(one, «twoˇ», three) b
9211 a.f(one, «twoˇ», three) b
9212 "});
9213
9214 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
9215 cx.assert_editor_state(indoc! {"
9216 a.f(«oneˇ», two, «threeˇ») b
9217 a.f(«oneˇ», two, «threeˇ») b
9218 a.f(«oneˇ», two, «threeˇ») b
9219 "});
9220
9221 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9222 cx.assert_editor_state(indoc! {"
9223 a.f(one, «twoˇ», three) b
9224 a.f(one, «twoˇ», three) b
9225 a.f(one, «twoˇ», three) b
9226 "});
9227 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9228 cx.assert_editor_state(indoc! {"
9229 a.f(one, two, three)ˇ b
9230 a.f(one, two, three)ˇ b
9231 a.f(one, two, three)ˇ b
9232 "});
9233
9234 // As soon as the last tab stop is reached, snippet state is gone
9235 cx.update_editor(|editor, window, cx| {
9236 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9237 });
9238 cx.assert_editor_state(indoc! {"
9239 a.f(one, two, three)ˇ b
9240 a.f(one, two, three)ˇ b
9241 a.f(one, two, three)ˇ b
9242 "});
9243}
9244
9245#[gpui::test]
9246async fn test_snippet_indentation(cx: &mut TestAppContext) {
9247 init_test(cx, |_| {});
9248
9249 let mut cx = EditorTestContext::new(cx).await;
9250
9251 cx.update_editor(|editor, window, cx| {
9252 let snippet = Snippet::parse(indoc! {"
9253 /*
9254 * Multiline comment with leading indentation
9255 *
9256 * $1
9257 */
9258 $0"})
9259 .unwrap();
9260 let insertion_ranges = editor
9261 .selections
9262 .all(cx)
9263 .iter()
9264 .map(|s| s.range().clone())
9265 .collect::<Vec<_>>();
9266 editor
9267 .insert_snippet(&insertion_ranges, snippet, window, cx)
9268 .unwrap();
9269 });
9270
9271 cx.assert_editor_state(indoc! {"
9272 /*
9273 * Multiline comment with leading indentation
9274 *
9275 * ˇ
9276 */
9277 "});
9278
9279 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9280 cx.assert_editor_state(indoc! {"
9281 /*
9282 * Multiline comment with leading indentation
9283 *
9284 *•
9285 */
9286 ˇ"});
9287}
9288
9289#[gpui::test]
9290async fn test_document_format_during_save(cx: &mut TestAppContext) {
9291 init_test(cx, |_| {});
9292
9293 let fs = FakeFs::new(cx.executor());
9294 fs.insert_file(path!("/file.rs"), Default::default()).await;
9295
9296 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
9297
9298 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9299 language_registry.add(rust_lang());
9300 let mut fake_servers = language_registry.register_fake_lsp(
9301 "Rust",
9302 FakeLspAdapter {
9303 capabilities: lsp::ServerCapabilities {
9304 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9305 ..Default::default()
9306 },
9307 ..Default::default()
9308 },
9309 );
9310
9311 let buffer = project
9312 .update(cx, |project, cx| {
9313 project.open_local_buffer(path!("/file.rs"), cx)
9314 })
9315 .await
9316 .unwrap();
9317
9318 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9319 let (editor, cx) = cx.add_window_view(|window, cx| {
9320 build_editor_with_project(project.clone(), buffer, window, cx)
9321 });
9322 editor.update_in(cx, |editor, window, cx| {
9323 editor.set_text("one\ntwo\nthree\n", window, cx)
9324 });
9325 assert!(cx.read(|cx| editor.is_dirty(cx)));
9326
9327 cx.executor().start_waiting();
9328 let fake_server = fake_servers.next().await.unwrap();
9329
9330 {
9331 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9332 move |params, _| async move {
9333 assert_eq!(
9334 params.text_document.uri,
9335 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9336 );
9337 assert_eq!(params.options.tab_size, 4);
9338 Ok(Some(vec![lsp::TextEdit::new(
9339 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9340 ", ".to_string(),
9341 )]))
9342 },
9343 );
9344 let save = editor
9345 .update_in(cx, |editor, window, cx| {
9346 editor.save(
9347 SaveOptions {
9348 format: true,
9349 autosave: false,
9350 },
9351 project.clone(),
9352 window,
9353 cx,
9354 )
9355 })
9356 .unwrap();
9357 cx.executor().start_waiting();
9358 save.await;
9359
9360 assert_eq!(
9361 editor.update(cx, |editor, cx| editor.text(cx)),
9362 "one, two\nthree\n"
9363 );
9364 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9365 }
9366
9367 {
9368 editor.update_in(cx, |editor, window, cx| {
9369 editor.set_text("one\ntwo\nthree\n", window, cx)
9370 });
9371 assert!(cx.read(|cx| editor.is_dirty(cx)));
9372
9373 // Ensure we can still save even if formatting hangs.
9374 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9375 move |params, _| async move {
9376 assert_eq!(
9377 params.text_document.uri,
9378 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9379 );
9380 futures::future::pending::<()>().await;
9381 unreachable!()
9382 },
9383 );
9384 let save = editor
9385 .update_in(cx, |editor, window, cx| {
9386 editor.save(
9387 SaveOptions {
9388 format: true,
9389 autosave: false,
9390 },
9391 project.clone(),
9392 window,
9393 cx,
9394 )
9395 })
9396 .unwrap();
9397 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9398 cx.executor().start_waiting();
9399 save.await;
9400 assert_eq!(
9401 editor.update(cx, |editor, cx| editor.text(cx)),
9402 "one\ntwo\nthree\n"
9403 );
9404 }
9405
9406 // Set rust language override and assert overridden tabsize is sent to language server
9407 update_test_language_settings(cx, |settings| {
9408 settings.languages.0.insert(
9409 "Rust".into(),
9410 LanguageSettingsContent {
9411 tab_size: NonZeroU32::new(8),
9412 ..Default::default()
9413 },
9414 );
9415 });
9416
9417 {
9418 editor.update_in(cx, |editor, window, cx| {
9419 editor.set_text("somehting_new\n", window, cx)
9420 });
9421 assert!(cx.read(|cx| editor.is_dirty(cx)));
9422 let _formatting_request_signal = fake_server
9423 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9424 assert_eq!(
9425 params.text_document.uri,
9426 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9427 );
9428 assert_eq!(params.options.tab_size, 8);
9429 Ok(Some(vec![]))
9430 });
9431 let save = editor
9432 .update_in(cx, |editor, window, cx| {
9433 editor.save(
9434 SaveOptions {
9435 format: true,
9436 autosave: false,
9437 },
9438 project.clone(),
9439 window,
9440 cx,
9441 )
9442 })
9443 .unwrap();
9444 cx.executor().start_waiting();
9445 save.await;
9446 }
9447}
9448
9449#[gpui::test]
9450async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9451 init_test(cx, |_| {});
9452
9453 let cols = 4;
9454 let rows = 10;
9455 let sample_text_1 = sample_text(rows, cols, 'a');
9456 assert_eq!(
9457 sample_text_1,
9458 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9459 );
9460 let sample_text_2 = sample_text(rows, cols, 'l');
9461 assert_eq!(
9462 sample_text_2,
9463 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9464 );
9465 let sample_text_3 = sample_text(rows, cols, 'v');
9466 assert_eq!(
9467 sample_text_3,
9468 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9469 );
9470
9471 let fs = FakeFs::new(cx.executor());
9472 fs.insert_tree(
9473 path!("/a"),
9474 json!({
9475 "main.rs": sample_text_1,
9476 "other.rs": sample_text_2,
9477 "lib.rs": sample_text_3,
9478 }),
9479 )
9480 .await;
9481
9482 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9483 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9484 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9485
9486 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9487 language_registry.add(rust_lang());
9488 let mut fake_servers = language_registry.register_fake_lsp(
9489 "Rust",
9490 FakeLspAdapter {
9491 capabilities: lsp::ServerCapabilities {
9492 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9493 ..Default::default()
9494 },
9495 ..Default::default()
9496 },
9497 );
9498
9499 let worktree = project.update(cx, |project, cx| {
9500 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
9501 assert_eq!(worktrees.len(), 1);
9502 worktrees.pop().unwrap()
9503 });
9504 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9505
9506 let buffer_1 = project
9507 .update(cx, |project, cx| {
9508 project.open_buffer((worktree_id, "main.rs"), cx)
9509 })
9510 .await
9511 .unwrap();
9512 let buffer_2 = project
9513 .update(cx, |project, cx| {
9514 project.open_buffer((worktree_id, "other.rs"), cx)
9515 })
9516 .await
9517 .unwrap();
9518 let buffer_3 = project
9519 .update(cx, |project, cx| {
9520 project.open_buffer((worktree_id, "lib.rs"), cx)
9521 })
9522 .await
9523 .unwrap();
9524
9525 let multi_buffer = cx.new(|cx| {
9526 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9527 multi_buffer.push_excerpts(
9528 buffer_1.clone(),
9529 [
9530 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9531 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9532 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9533 ],
9534 cx,
9535 );
9536 multi_buffer.push_excerpts(
9537 buffer_2.clone(),
9538 [
9539 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9540 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9541 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9542 ],
9543 cx,
9544 );
9545 multi_buffer.push_excerpts(
9546 buffer_3.clone(),
9547 [
9548 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9549 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9550 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9551 ],
9552 cx,
9553 );
9554 multi_buffer
9555 });
9556 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
9557 Editor::new(
9558 EditorMode::full(),
9559 multi_buffer,
9560 Some(project.clone()),
9561 window,
9562 cx,
9563 )
9564 });
9565
9566 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9567 editor.change_selections(
9568 SelectionEffects::scroll(Autoscroll::Next),
9569 window,
9570 cx,
9571 |s| s.select_ranges(Some(1..2)),
9572 );
9573 editor.insert("|one|two|three|", window, cx);
9574 });
9575 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9576 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9577 editor.change_selections(
9578 SelectionEffects::scroll(Autoscroll::Next),
9579 window,
9580 cx,
9581 |s| s.select_ranges(Some(60..70)),
9582 );
9583 editor.insert("|four|five|six|", window, cx);
9584 });
9585 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9586
9587 // First two buffers should be edited, but not the third one.
9588 assert_eq!(
9589 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9590 "a|one|two|three|aa\nbbbb\ncccc\n\nffff\ngggg\n\njjjj\nllll\nmmmm\nnnnn|four|five|six|\nr\n\nuuuu\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}",
9591 );
9592 buffer_1.update(cx, |buffer, _| {
9593 assert!(buffer.is_dirty());
9594 assert_eq!(
9595 buffer.text(),
9596 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
9597 )
9598 });
9599 buffer_2.update(cx, |buffer, _| {
9600 assert!(buffer.is_dirty());
9601 assert_eq!(
9602 buffer.text(),
9603 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
9604 )
9605 });
9606 buffer_3.update(cx, |buffer, _| {
9607 assert!(!buffer.is_dirty());
9608 assert_eq!(buffer.text(), sample_text_3,)
9609 });
9610 cx.executor().run_until_parked();
9611
9612 cx.executor().start_waiting();
9613 let save = multi_buffer_editor
9614 .update_in(cx, |editor, window, cx| {
9615 editor.save(
9616 SaveOptions {
9617 format: true,
9618 autosave: false,
9619 },
9620 project.clone(),
9621 window,
9622 cx,
9623 )
9624 })
9625 .unwrap();
9626
9627 let fake_server = fake_servers.next().await.unwrap();
9628 fake_server
9629 .server
9630 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
9631 Ok(Some(vec![lsp::TextEdit::new(
9632 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9633 format!("[{} formatted]", params.text_document.uri),
9634 )]))
9635 })
9636 .detach();
9637 save.await;
9638
9639 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
9640 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
9641 assert_eq!(
9642 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9643 uri!(
9644 "a|o[file:///a/main.rs formatted]bbbb\ncccc\n\nffff\ngggg\n\njjjj\n\nlll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|\nr\n\nuuuu\n\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}"
9645 ),
9646 );
9647 buffer_1.update(cx, |buffer, _| {
9648 assert!(!buffer.is_dirty());
9649 assert_eq!(
9650 buffer.text(),
9651 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
9652 )
9653 });
9654 buffer_2.update(cx, |buffer, _| {
9655 assert!(!buffer.is_dirty());
9656 assert_eq!(
9657 buffer.text(),
9658 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
9659 )
9660 });
9661 buffer_3.update(cx, |buffer, _| {
9662 assert!(!buffer.is_dirty());
9663 assert_eq!(buffer.text(), sample_text_3,)
9664 });
9665}
9666
9667#[gpui::test]
9668async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
9669 init_test(cx, |_| {});
9670
9671 let fs = FakeFs::new(cx.executor());
9672 fs.insert_tree(
9673 path!("/dir"),
9674 json!({
9675 "file1.rs": "fn main() { println!(\"hello\"); }",
9676 "file2.rs": "fn test() { println!(\"test\"); }",
9677 "file3.rs": "fn other() { println!(\"other\"); }\n",
9678 }),
9679 )
9680 .await;
9681
9682 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
9683 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9684 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9685
9686 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9687 language_registry.add(rust_lang());
9688
9689 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
9690 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9691
9692 // Open three buffers
9693 let buffer_1 = project
9694 .update(cx, |project, cx| {
9695 project.open_buffer((worktree_id, "file1.rs"), cx)
9696 })
9697 .await
9698 .unwrap();
9699 let buffer_2 = project
9700 .update(cx, |project, cx| {
9701 project.open_buffer((worktree_id, "file2.rs"), cx)
9702 })
9703 .await
9704 .unwrap();
9705 let buffer_3 = project
9706 .update(cx, |project, cx| {
9707 project.open_buffer((worktree_id, "file3.rs"), cx)
9708 })
9709 .await
9710 .unwrap();
9711
9712 // Create a multi-buffer with all three buffers
9713 let multi_buffer = cx.new(|cx| {
9714 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9715 multi_buffer.push_excerpts(
9716 buffer_1.clone(),
9717 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9718 cx,
9719 );
9720 multi_buffer.push_excerpts(
9721 buffer_2.clone(),
9722 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9723 cx,
9724 );
9725 multi_buffer.push_excerpts(
9726 buffer_3.clone(),
9727 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9728 cx,
9729 );
9730 multi_buffer
9731 });
9732
9733 let editor = cx.new_window_entity(|window, cx| {
9734 Editor::new(
9735 EditorMode::full(),
9736 multi_buffer,
9737 Some(project.clone()),
9738 window,
9739 cx,
9740 )
9741 });
9742
9743 // Edit only the first buffer
9744 editor.update_in(cx, |editor, window, cx| {
9745 editor.change_selections(
9746 SelectionEffects::scroll(Autoscroll::Next),
9747 window,
9748 cx,
9749 |s| s.select_ranges(Some(10..10)),
9750 );
9751 editor.insert("// edited", window, cx);
9752 });
9753
9754 // Verify that only buffer 1 is dirty
9755 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
9756 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9757 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9758
9759 // Get write counts after file creation (files were created with initial content)
9760 // We expect each file to have been written once during creation
9761 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
9762 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
9763 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
9764
9765 // Perform autosave
9766 let save_task = editor.update_in(cx, |editor, window, cx| {
9767 editor.save(
9768 SaveOptions {
9769 format: true,
9770 autosave: true,
9771 },
9772 project.clone(),
9773 window,
9774 cx,
9775 )
9776 });
9777 save_task.await.unwrap();
9778
9779 // Only the dirty buffer should have been saved
9780 assert_eq!(
9781 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9782 1,
9783 "Buffer 1 was dirty, so it should have been written once during autosave"
9784 );
9785 assert_eq!(
9786 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9787 0,
9788 "Buffer 2 was clean, so it should not have been written during autosave"
9789 );
9790 assert_eq!(
9791 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9792 0,
9793 "Buffer 3 was clean, so it should not have been written during autosave"
9794 );
9795
9796 // Verify buffer states after autosave
9797 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9798 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9799 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9800
9801 // Now perform a manual save (format = true)
9802 let save_task = editor.update_in(cx, |editor, window, cx| {
9803 editor.save(
9804 SaveOptions {
9805 format: true,
9806 autosave: false,
9807 },
9808 project.clone(),
9809 window,
9810 cx,
9811 )
9812 });
9813 save_task.await.unwrap();
9814
9815 // During manual save, clean buffers don't get written to disk
9816 // They just get did_save called for language server notifications
9817 assert_eq!(
9818 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9819 1,
9820 "Buffer 1 should only have been written once total (during autosave, not manual save)"
9821 );
9822 assert_eq!(
9823 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9824 0,
9825 "Buffer 2 should not have been written at all"
9826 );
9827 assert_eq!(
9828 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9829 0,
9830 "Buffer 3 should not have been written at all"
9831 );
9832}
9833
9834#[gpui::test]
9835async fn test_range_format_during_save(cx: &mut TestAppContext) {
9836 init_test(cx, |_| {});
9837
9838 let fs = FakeFs::new(cx.executor());
9839 fs.insert_file(path!("/file.rs"), Default::default()).await;
9840
9841 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9842
9843 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9844 language_registry.add(rust_lang());
9845 let mut fake_servers = language_registry.register_fake_lsp(
9846 "Rust",
9847 FakeLspAdapter {
9848 capabilities: lsp::ServerCapabilities {
9849 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
9850 ..Default::default()
9851 },
9852 ..Default::default()
9853 },
9854 );
9855
9856 let buffer = project
9857 .update(cx, |project, cx| {
9858 project.open_local_buffer(path!("/file.rs"), cx)
9859 })
9860 .await
9861 .unwrap();
9862
9863 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9864 let (editor, cx) = cx.add_window_view(|window, cx| {
9865 build_editor_with_project(project.clone(), buffer, window, cx)
9866 });
9867 editor.update_in(cx, |editor, window, cx| {
9868 editor.set_text("one\ntwo\nthree\n", window, cx)
9869 });
9870 assert!(cx.read(|cx| editor.is_dirty(cx)));
9871
9872 cx.executor().start_waiting();
9873 let fake_server = fake_servers.next().await.unwrap();
9874
9875 let save = editor
9876 .update_in(cx, |editor, window, cx| {
9877 editor.save(
9878 SaveOptions {
9879 format: true,
9880 autosave: false,
9881 },
9882 project.clone(),
9883 window,
9884 cx,
9885 )
9886 })
9887 .unwrap();
9888 fake_server
9889 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9890 assert_eq!(
9891 params.text_document.uri,
9892 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9893 );
9894 assert_eq!(params.options.tab_size, 4);
9895 Ok(Some(vec![lsp::TextEdit::new(
9896 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9897 ", ".to_string(),
9898 )]))
9899 })
9900 .next()
9901 .await;
9902 cx.executor().start_waiting();
9903 save.await;
9904 assert_eq!(
9905 editor.update(cx, |editor, cx| editor.text(cx)),
9906 "one, two\nthree\n"
9907 );
9908 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9909
9910 editor.update_in(cx, |editor, window, cx| {
9911 editor.set_text("one\ntwo\nthree\n", window, cx)
9912 });
9913 assert!(cx.read(|cx| editor.is_dirty(cx)));
9914
9915 // Ensure we can still save even if formatting hangs.
9916 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
9917 move |params, _| async move {
9918 assert_eq!(
9919 params.text_document.uri,
9920 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9921 );
9922 futures::future::pending::<()>().await;
9923 unreachable!()
9924 },
9925 );
9926 let save = editor
9927 .update_in(cx, |editor, window, cx| {
9928 editor.save(
9929 SaveOptions {
9930 format: true,
9931 autosave: false,
9932 },
9933 project.clone(),
9934 window,
9935 cx,
9936 )
9937 })
9938 .unwrap();
9939 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9940 cx.executor().start_waiting();
9941 save.await;
9942 assert_eq!(
9943 editor.update(cx, |editor, cx| editor.text(cx)),
9944 "one\ntwo\nthree\n"
9945 );
9946 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9947
9948 // For non-dirty buffer, no formatting request should be sent
9949 let save = editor
9950 .update_in(cx, |editor, window, cx| {
9951 editor.save(
9952 SaveOptions {
9953 format: false,
9954 autosave: false,
9955 },
9956 project.clone(),
9957 window,
9958 cx,
9959 )
9960 })
9961 .unwrap();
9962 let _pending_format_request = fake_server
9963 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
9964 panic!("Should not be invoked");
9965 })
9966 .next();
9967 cx.executor().start_waiting();
9968 save.await;
9969
9970 // Set Rust language override and assert overridden tabsize is sent to language server
9971 update_test_language_settings(cx, |settings| {
9972 settings.languages.0.insert(
9973 "Rust".into(),
9974 LanguageSettingsContent {
9975 tab_size: NonZeroU32::new(8),
9976 ..Default::default()
9977 },
9978 );
9979 });
9980
9981 editor.update_in(cx, |editor, window, cx| {
9982 editor.set_text("somehting_new\n", window, cx)
9983 });
9984 assert!(cx.read(|cx| editor.is_dirty(cx)));
9985 let save = editor
9986 .update_in(cx, |editor, window, cx| {
9987 editor.save(
9988 SaveOptions {
9989 format: true,
9990 autosave: false,
9991 },
9992 project.clone(),
9993 window,
9994 cx,
9995 )
9996 })
9997 .unwrap();
9998 fake_server
9999 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10000 assert_eq!(
10001 params.text_document.uri,
10002 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10003 );
10004 assert_eq!(params.options.tab_size, 8);
10005 Ok(Some(Vec::new()))
10006 })
10007 .next()
10008 .await;
10009 save.await;
10010}
10011
10012#[gpui::test]
10013async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
10014 init_test(cx, |settings| {
10015 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
10016 Formatter::LanguageServer { name: None },
10017 )))
10018 });
10019
10020 let fs = FakeFs::new(cx.executor());
10021 fs.insert_file(path!("/file.rs"), Default::default()).await;
10022
10023 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10024
10025 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10026 language_registry.add(Arc::new(Language::new(
10027 LanguageConfig {
10028 name: "Rust".into(),
10029 matcher: LanguageMatcher {
10030 path_suffixes: vec!["rs".to_string()],
10031 ..Default::default()
10032 },
10033 ..LanguageConfig::default()
10034 },
10035 Some(tree_sitter_rust::LANGUAGE.into()),
10036 )));
10037 update_test_language_settings(cx, |settings| {
10038 // Enable Prettier formatting for the same buffer, and ensure
10039 // LSP is called instead of Prettier.
10040 settings.defaults.prettier = Some(PrettierSettings {
10041 allowed: true,
10042 ..PrettierSettings::default()
10043 });
10044 });
10045 let mut fake_servers = language_registry.register_fake_lsp(
10046 "Rust",
10047 FakeLspAdapter {
10048 capabilities: lsp::ServerCapabilities {
10049 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10050 ..Default::default()
10051 },
10052 ..Default::default()
10053 },
10054 );
10055
10056 let buffer = project
10057 .update(cx, |project, cx| {
10058 project.open_local_buffer(path!("/file.rs"), cx)
10059 })
10060 .await
10061 .unwrap();
10062
10063 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10064 let (editor, cx) = cx.add_window_view(|window, cx| {
10065 build_editor_with_project(project.clone(), buffer, window, cx)
10066 });
10067 editor.update_in(cx, |editor, window, cx| {
10068 editor.set_text("one\ntwo\nthree\n", window, cx)
10069 });
10070
10071 cx.executor().start_waiting();
10072 let fake_server = fake_servers.next().await.unwrap();
10073
10074 let format = editor
10075 .update_in(cx, |editor, window, cx| {
10076 editor.perform_format(
10077 project.clone(),
10078 FormatTrigger::Manual,
10079 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10080 window,
10081 cx,
10082 )
10083 })
10084 .unwrap();
10085 fake_server
10086 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10087 assert_eq!(
10088 params.text_document.uri,
10089 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10090 );
10091 assert_eq!(params.options.tab_size, 4);
10092 Ok(Some(vec![lsp::TextEdit::new(
10093 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10094 ", ".to_string(),
10095 )]))
10096 })
10097 .next()
10098 .await;
10099 cx.executor().start_waiting();
10100 format.await;
10101 assert_eq!(
10102 editor.update(cx, |editor, cx| editor.text(cx)),
10103 "one, two\nthree\n"
10104 );
10105
10106 editor.update_in(cx, |editor, window, cx| {
10107 editor.set_text("one\ntwo\nthree\n", window, cx)
10108 });
10109 // Ensure we don't lock if formatting hangs.
10110 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10111 move |params, _| async move {
10112 assert_eq!(
10113 params.text_document.uri,
10114 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10115 );
10116 futures::future::pending::<()>().await;
10117 unreachable!()
10118 },
10119 );
10120 let format = editor
10121 .update_in(cx, |editor, window, cx| {
10122 editor.perform_format(
10123 project,
10124 FormatTrigger::Manual,
10125 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10126 window,
10127 cx,
10128 )
10129 })
10130 .unwrap();
10131 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10132 cx.executor().start_waiting();
10133 format.await;
10134 assert_eq!(
10135 editor.update(cx, |editor, cx| editor.text(cx)),
10136 "one\ntwo\nthree\n"
10137 );
10138}
10139
10140#[gpui::test]
10141async fn test_multiple_formatters(cx: &mut TestAppContext) {
10142 init_test(cx, |settings| {
10143 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
10144 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10145 Formatter::LanguageServer { name: None },
10146 Formatter::CodeActions(
10147 [
10148 ("code-action-1".into(), true),
10149 ("code-action-2".into(), true),
10150 ]
10151 .into_iter()
10152 .collect(),
10153 ),
10154 ])))
10155 });
10156
10157 let fs = FakeFs::new(cx.executor());
10158 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
10159 .await;
10160
10161 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10162 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10163 language_registry.add(rust_lang());
10164
10165 let mut fake_servers = language_registry.register_fake_lsp(
10166 "Rust",
10167 FakeLspAdapter {
10168 capabilities: lsp::ServerCapabilities {
10169 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10170 execute_command_provider: Some(lsp::ExecuteCommandOptions {
10171 commands: vec!["the-command-for-code-action-1".into()],
10172 ..Default::default()
10173 }),
10174 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10175 ..Default::default()
10176 },
10177 ..Default::default()
10178 },
10179 );
10180
10181 let buffer = project
10182 .update(cx, |project, cx| {
10183 project.open_local_buffer(path!("/file.rs"), cx)
10184 })
10185 .await
10186 .unwrap();
10187
10188 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10189 let (editor, cx) = cx.add_window_view(|window, cx| {
10190 build_editor_with_project(project.clone(), buffer, window, cx)
10191 });
10192
10193 cx.executor().start_waiting();
10194
10195 let fake_server = fake_servers.next().await.unwrap();
10196 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10197 move |_params, _| async move {
10198 Ok(Some(vec![lsp::TextEdit::new(
10199 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10200 "applied-formatting\n".to_string(),
10201 )]))
10202 },
10203 );
10204 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10205 move |params, _| async move {
10206 assert_eq!(
10207 params.context.only,
10208 Some(vec!["code-action-1".into(), "code-action-2".into()])
10209 );
10210 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
10211 Ok(Some(vec![
10212 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10213 kind: Some("code-action-1".into()),
10214 edit: Some(lsp::WorkspaceEdit::new(
10215 [(
10216 uri.clone(),
10217 vec![lsp::TextEdit::new(
10218 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10219 "applied-code-action-1-edit\n".to_string(),
10220 )],
10221 )]
10222 .into_iter()
10223 .collect(),
10224 )),
10225 command: Some(lsp::Command {
10226 command: "the-command-for-code-action-1".into(),
10227 ..Default::default()
10228 }),
10229 ..Default::default()
10230 }),
10231 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10232 kind: Some("code-action-2".into()),
10233 edit: Some(lsp::WorkspaceEdit::new(
10234 [(
10235 uri.clone(),
10236 vec![lsp::TextEdit::new(
10237 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10238 "applied-code-action-2-edit\n".to_string(),
10239 )],
10240 )]
10241 .into_iter()
10242 .collect(),
10243 )),
10244 ..Default::default()
10245 }),
10246 ]))
10247 },
10248 );
10249
10250 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
10251 move |params, _| async move { Ok(params) }
10252 });
10253
10254 let command_lock = Arc::new(futures::lock::Mutex::new(()));
10255 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
10256 let fake = fake_server.clone();
10257 let lock = command_lock.clone();
10258 move |params, _| {
10259 assert_eq!(params.command, "the-command-for-code-action-1");
10260 let fake = fake.clone();
10261 let lock = lock.clone();
10262 async move {
10263 lock.lock().await;
10264 fake.server
10265 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
10266 label: None,
10267 edit: lsp::WorkspaceEdit {
10268 changes: Some(
10269 [(
10270 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
10271 vec![lsp::TextEdit {
10272 range: lsp::Range::new(
10273 lsp::Position::new(0, 0),
10274 lsp::Position::new(0, 0),
10275 ),
10276 new_text: "applied-code-action-1-command\n".into(),
10277 }],
10278 )]
10279 .into_iter()
10280 .collect(),
10281 ),
10282 ..Default::default()
10283 },
10284 })
10285 .await
10286 .into_response()
10287 .unwrap();
10288 Ok(Some(json!(null)))
10289 }
10290 }
10291 });
10292
10293 cx.executor().start_waiting();
10294 editor
10295 .update_in(cx, |editor, window, cx| {
10296 editor.perform_format(
10297 project.clone(),
10298 FormatTrigger::Manual,
10299 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10300 window,
10301 cx,
10302 )
10303 })
10304 .unwrap()
10305 .await;
10306 editor.update(cx, |editor, cx| {
10307 assert_eq!(
10308 editor.text(cx),
10309 r#"
10310 applied-code-action-2-edit
10311 applied-code-action-1-command
10312 applied-code-action-1-edit
10313 applied-formatting
10314 one
10315 two
10316 three
10317 "#
10318 .unindent()
10319 );
10320 });
10321
10322 editor.update_in(cx, |editor, window, cx| {
10323 editor.undo(&Default::default(), window, cx);
10324 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10325 });
10326
10327 // Perform a manual edit while waiting for an LSP command
10328 // that's being run as part of a formatting code action.
10329 let lock_guard = command_lock.lock().await;
10330 let format = editor
10331 .update_in(cx, |editor, window, cx| {
10332 editor.perform_format(
10333 project.clone(),
10334 FormatTrigger::Manual,
10335 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10336 window,
10337 cx,
10338 )
10339 })
10340 .unwrap();
10341 cx.run_until_parked();
10342 editor.update(cx, |editor, cx| {
10343 assert_eq!(
10344 editor.text(cx),
10345 r#"
10346 applied-code-action-1-edit
10347 applied-formatting
10348 one
10349 two
10350 three
10351 "#
10352 .unindent()
10353 );
10354
10355 editor.buffer.update(cx, |buffer, cx| {
10356 let ix = buffer.len(cx);
10357 buffer.edit([(ix..ix, "edited\n")], None, cx);
10358 });
10359 });
10360
10361 // Allow the LSP command to proceed. Because the buffer was edited,
10362 // the second code action will not be run.
10363 drop(lock_guard);
10364 format.await;
10365 editor.update_in(cx, |editor, window, cx| {
10366 assert_eq!(
10367 editor.text(cx),
10368 r#"
10369 applied-code-action-1-command
10370 applied-code-action-1-edit
10371 applied-formatting
10372 one
10373 two
10374 three
10375 edited
10376 "#
10377 .unindent()
10378 );
10379
10380 // The manual edit is undone first, because it is the last thing the user did
10381 // (even though the command completed afterwards).
10382 editor.undo(&Default::default(), window, cx);
10383 assert_eq!(
10384 editor.text(cx),
10385 r#"
10386 applied-code-action-1-command
10387 applied-code-action-1-edit
10388 applied-formatting
10389 one
10390 two
10391 three
10392 "#
10393 .unindent()
10394 );
10395
10396 // All the formatting (including the command, which completed after the manual edit)
10397 // is undone together.
10398 editor.undo(&Default::default(), window, cx);
10399 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10400 });
10401}
10402
10403#[gpui::test]
10404async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
10405 init_test(cx, |settings| {
10406 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10407 Formatter::LanguageServer { name: None },
10408 ])))
10409 });
10410
10411 let fs = FakeFs::new(cx.executor());
10412 fs.insert_file(path!("/file.ts"), Default::default()).await;
10413
10414 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10415
10416 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10417 language_registry.add(Arc::new(Language::new(
10418 LanguageConfig {
10419 name: "TypeScript".into(),
10420 matcher: LanguageMatcher {
10421 path_suffixes: vec!["ts".to_string()],
10422 ..Default::default()
10423 },
10424 ..LanguageConfig::default()
10425 },
10426 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10427 )));
10428 update_test_language_settings(cx, |settings| {
10429 settings.defaults.prettier = Some(PrettierSettings {
10430 allowed: true,
10431 ..PrettierSettings::default()
10432 });
10433 });
10434 let mut fake_servers = language_registry.register_fake_lsp(
10435 "TypeScript",
10436 FakeLspAdapter {
10437 capabilities: lsp::ServerCapabilities {
10438 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10439 ..Default::default()
10440 },
10441 ..Default::default()
10442 },
10443 );
10444
10445 let buffer = project
10446 .update(cx, |project, cx| {
10447 project.open_local_buffer(path!("/file.ts"), cx)
10448 })
10449 .await
10450 .unwrap();
10451
10452 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10453 let (editor, cx) = cx.add_window_view(|window, cx| {
10454 build_editor_with_project(project.clone(), buffer, window, cx)
10455 });
10456 editor.update_in(cx, |editor, window, cx| {
10457 editor.set_text(
10458 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10459 window,
10460 cx,
10461 )
10462 });
10463
10464 cx.executor().start_waiting();
10465 let fake_server = fake_servers.next().await.unwrap();
10466
10467 let format = editor
10468 .update_in(cx, |editor, window, cx| {
10469 editor.perform_code_action_kind(
10470 project.clone(),
10471 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10472 window,
10473 cx,
10474 )
10475 })
10476 .unwrap();
10477 fake_server
10478 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
10479 assert_eq!(
10480 params.text_document.uri,
10481 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10482 );
10483 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
10484 lsp::CodeAction {
10485 title: "Organize Imports".to_string(),
10486 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
10487 edit: Some(lsp::WorkspaceEdit {
10488 changes: Some(
10489 [(
10490 params.text_document.uri.clone(),
10491 vec![lsp::TextEdit::new(
10492 lsp::Range::new(
10493 lsp::Position::new(1, 0),
10494 lsp::Position::new(2, 0),
10495 ),
10496 "".to_string(),
10497 )],
10498 )]
10499 .into_iter()
10500 .collect(),
10501 ),
10502 ..Default::default()
10503 }),
10504 ..Default::default()
10505 },
10506 )]))
10507 })
10508 .next()
10509 .await;
10510 cx.executor().start_waiting();
10511 format.await;
10512 assert_eq!(
10513 editor.update(cx, |editor, cx| editor.text(cx)),
10514 "import { a } from 'module';\n\nconst x = a;\n"
10515 );
10516
10517 editor.update_in(cx, |editor, window, cx| {
10518 editor.set_text(
10519 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10520 window,
10521 cx,
10522 )
10523 });
10524 // Ensure we don't lock if code action hangs.
10525 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10526 move |params, _| async move {
10527 assert_eq!(
10528 params.text_document.uri,
10529 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10530 );
10531 futures::future::pending::<()>().await;
10532 unreachable!()
10533 },
10534 );
10535 let format = editor
10536 .update_in(cx, |editor, window, cx| {
10537 editor.perform_code_action_kind(
10538 project,
10539 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10540 window,
10541 cx,
10542 )
10543 })
10544 .unwrap();
10545 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
10546 cx.executor().start_waiting();
10547 format.await;
10548 assert_eq!(
10549 editor.update(cx, |editor, cx| editor.text(cx)),
10550 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
10551 );
10552}
10553
10554#[gpui::test]
10555async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
10556 init_test(cx, |_| {});
10557
10558 let mut cx = EditorLspTestContext::new_rust(
10559 lsp::ServerCapabilities {
10560 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10561 ..Default::default()
10562 },
10563 cx,
10564 )
10565 .await;
10566
10567 cx.set_state(indoc! {"
10568 one.twoˇ
10569 "});
10570
10571 // The format request takes a long time. When it completes, it inserts
10572 // a newline and an indent before the `.`
10573 cx.lsp
10574 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
10575 let executor = cx.background_executor().clone();
10576 async move {
10577 executor.timer(Duration::from_millis(100)).await;
10578 Ok(Some(vec![lsp::TextEdit {
10579 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
10580 new_text: "\n ".into(),
10581 }]))
10582 }
10583 });
10584
10585 // Submit a format request.
10586 let format_1 = cx
10587 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10588 .unwrap();
10589 cx.executor().run_until_parked();
10590
10591 // Submit a second format request.
10592 let format_2 = cx
10593 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10594 .unwrap();
10595 cx.executor().run_until_parked();
10596
10597 // Wait for both format requests to complete
10598 cx.executor().advance_clock(Duration::from_millis(200));
10599 cx.executor().start_waiting();
10600 format_1.await.unwrap();
10601 cx.executor().start_waiting();
10602 format_2.await.unwrap();
10603
10604 // The formatting edits only happens once.
10605 cx.assert_editor_state(indoc! {"
10606 one
10607 .twoˇ
10608 "});
10609}
10610
10611#[gpui::test]
10612async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
10613 init_test(cx, |settings| {
10614 settings.defaults.formatter = Some(SelectedFormatter::Auto)
10615 });
10616
10617 let mut cx = EditorLspTestContext::new_rust(
10618 lsp::ServerCapabilities {
10619 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10620 ..Default::default()
10621 },
10622 cx,
10623 )
10624 .await;
10625
10626 // Set up a buffer white some trailing whitespace and no trailing newline.
10627 cx.set_state(
10628 &[
10629 "one ", //
10630 "twoˇ", //
10631 "three ", //
10632 "four", //
10633 ]
10634 .join("\n"),
10635 );
10636
10637 // Submit a format request.
10638 let format = cx
10639 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10640 .unwrap();
10641
10642 // Record which buffer changes have been sent to the language server
10643 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
10644 cx.lsp
10645 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
10646 let buffer_changes = buffer_changes.clone();
10647 move |params, _| {
10648 buffer_changes.lock().extend(
10649 params
10650 .content_changes
10651 .into_iter()
10652 .map(|e| (e.range.unwrap(), e.text)),
10653 );
10654 }
10655 });
10656
10657 // Handle formatting requests to the language server.
10658 cx.lsp
10659 .set_request_handler::<lsp::request::Formatting, _, _>({
10660 let buffer_changes = buffer_changes.clone();
10661 move |_, _| {
10662 // When formatting is requested, trailing whitespace has already been stripped,
10663 // and the trailing newline has already been added.
10664 assert_eq!(
10665 &buffer_changes.lock()[1..],
10666 &[
10667 (
10668 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
10669 "".into()
10670 ),
10671 (
10672 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
10673 "".into()
10674 ),
10675 (
10676 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
10677 "\n".into()
10678 ),
10679 ]
10680 );
10681
10682 // Insert blank lines between each line of the buffer.
10683 async move {
10684 Ok(Some(vec![
10685 lsp::TextEdit {
10686 range: lsp::Range::new(
10687 lsp::Position::new(1, 0),
10688 lsp::Position::new(1, 0),
10689 ),
10690 new_text: "\n".into(),
10691 },
10692 lsp::TextEdit {
10693 range: lsp::Range::new(
10694 lsp::Position::new(2, 0),
10695 lsp::Position::new(2, 0),
10696 ),
10697 new_text: "\n".into(),
10698 },
10699 ]))
10700 }
10701 }
10702 });
10703
10704 // After formatting the buffer, the trailing whitespace is stripped,
10705 // a newline is appended, and the edits provided by the language server
10706 // have been applied.
10707 format.await.unwrap();
10708 cx.assert_editor_state(
10709 &[
10710 "one", //
10711 "", //
10712 "twoˇ", //
10713 "", //
10714 "three", //
10715 "four", //
10716 "", //
10717 ]
10718 .join("\n"),
10719 );
10720
10721 // Undoing the formatting undoes the trailing whitespace removal, the
10722 // trailing newline, and the LSP edits.
10723 cx.update_buffer(|buffer, cx| buffer.undo(cx));
10724 cx.assert_editor_state(
10725 &[
10726 "one ", //
10727 "twoˇ", //
10728 "three ", //
10729 "four", //
10730 ]
10731 .join("\n"),
10732 );
10733}
10734
10735#[gpui::test]
10736async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
10737 cx: &mut TestAppContext,
10738) {
10739 init_test(cx, |_| {});
10740
10741 cx.update(|cx| {
10742 cx.update_global::<SettingsStore, _>(|settings, cx| {
10743 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10744 settings.auto_signature_help = Some(true);
10745 });
10746 });
10747 });
10748
10749 let mut cx = EditorLspTestContext::new_rust(
10750 lsp::ServerCapabilities {
10751 signature_help_provider: Some(lsp::SignatureHelpOptions {
10752 ..Default::default()
10753 }),
10754 ..Default::default()
10755 },
10756 cx,
10757 )
10758 .await;
10759
10760 let language = Language::new(
10761 LanguageConfig {
10762 name: "Rust".into(),
10763 brackets: BracketPairConfig {
10764 pairs: vec![
10765 BracketPair {
10766 start: "{".to_string(),
10767 end: "}".to_string(),
10768 close: true,
10769 surround: true,
10770 newline: true,
10771 },
10772 BracketPair {
10773 start: "(".to_string(),
10774 end: ")".to_string(),
10775 close: true,
10776 surround: true,
10777 newline: true,
10778 },
10779 BracketPair {
10780 start: "/*".to_string(),
10781 end: " */".to_string(),
10782 close: true,
10783 surround: true,
10784 newline: true,
10785 },
10786 BracketPair {
10787 start: "[".to_string(),
10788 end: "]".to_string(),
10789 close: false,
10790 surround: false,
10791 newline: true,
10792 },
10793 BracketPair {
10794 start: "\"".to_string(),
10795 end: "\"".to_string(),
10796 close: true,
10797 surround: true,
10798 newline: false,
10799 },
10800 BracketPair {
10801 start: "<".to_string(),
10802 end: ">".to_string(),
10803 close: false,
10804 surround: true,
10805 newline: true,
10806 },
10807 ],
10808 ..Default::default()
10809 },
10810 autoclose_before: "})]".to_string(),
10811 ..Default::default()
10812 },
10813 Some(tree_sitter_rust::LANGUAGE.into()),
10814 );
10815 let language = Arc::new(language);
10816
10817 cx.language_registry().add(language.clone());
10818 cx.update_buffer(|buffer, cx| {
10819 buffer.set_language(Some(language), cx);
10820 });
10821
10822 cx.set_state(
10823 &r#"
10824 fn main() {
10825 sampleˇ
10826 }
10827 "#
10828 .unindent(),
10829 );
10830
10831 cx.update_editor(|editor, window, cx| {
10832 editor.handle_input("(", window, cx);
10833 });
10834 cx.assert_editor_state(
10835 &"
10836 fn main() {
10837 sample(ˇ)
10838 }
10839 "
10840 .unindent(),
10841 );
10842
10843 let mocked_response = lsp::SignatureHelp {
10844 signatures: vec![lsp::SignatureInformation {
10845 label: "fn sample(param1: u8, param2: u8)".to_string(),
10846 documentation: None,
10847 parameters: Some(vec![
10848 lsp::ParameterInformation {
10849 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10850 documentation: None,
10851 },
10852 lsp::ParameterInformation {
10853 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10854 documentation: None,
10855 },
10856 ]),
10857 active_parameter: None,
10858 }],
10859 active_signature: Some(0),
10860 active_parameter: Some(0),
10861 };
10862 handle_signature_help_request(&mut cx, mocked_response).await;
10863
10864 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10865 .await;
10866
10867 cx.editor(|editor, _, _| {
10868 let signature_help_state = editor.signature_help_state.popover().cloned();
10869 let signature = signature_help_state.unwrap();
10870 assert_eq!(
10871 signature.signatures[signature.current_signature].label,
10872 "fn sample(param1: u8, param2: u8)"
10873 );
10874 });
10875}
10876
10877#[gpui::test]
10878async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
10879 init_test(cx, |_| {});
10880
10881 cx.update(|cx| {
10882 cx.update_global::<SettingsStore, _>(|settings, cx| {
10883 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10884 settings.auto_signature_help = Some(false);
10885 settings.show_signature_help_after_edits = Some(false);
10886 });
10887 });
10888 });
10889
10890 let mut cx = EditorLspTestContext::new_rust(
10891 lsp::ServerCapabilities {
10892 signature_help_provider: Some(lsp::SignatureHelpOptions {
10893 ..Default::default()
10894 }),
10895 ..Default::default()
10896 },
10897 cx,
10898 )
10899 .await;
10900
10901 let language = Language::new(
10902 LanguageConfig {
10903 name: "Rust".into(),
10904 brackets: BracketPairConfig {
10905 pairs: vec![
10906 BracketPair {
10907 start: "{".to_string(),
10908 end: "}".to_string(),
10909 close: true,
10910 surround: true,
10911 newline: true,
10912 },
10913 BracketPair {
10914 start: "(".to_string(),
10915 end: ")".to_string(),
10916 close: true,
10917 surround: true,
10918 newline: true,
10919 },
10920 BracketPair {
10921 start: "/*".to_string(),
10922 end: " */".to_string(),
10923 close: true,
10924 surround: true,
10925 newline: true,
10926 },
10927 BracketPair {
10928 start: "[".to_string(),
10929 end: "]".to_string(),
10930 close: false,
10931 surround: false,
10932 newline: true,
10933 },
10934 BracketPair {
10935 start: "\"".to_string(),
10936 end: "\"".to_string(),
10937 close: true,
10938 surround: true,
10939 newline: false,
10940 },
10941 BracketPair {
10942 start: "<".to_string(),
10943 end: ">".to_string(),
10944 close: false,
10945 surround: true,
10946 newline: true,
10947 },
10948 ],
10949 ..Default::default()
10950 },
10951 autoclose_before: "})]".to_string(),
10952 ..Default::default()
10953 },
10954 Some(tree_sitter_rust::LANGUAGE.into()),
10955 );
10956 let language = Arc::new(language);
10957
10958 cx.language_registry().add(language.clone());
10959 cx.update_buffer(|buffer, cx| {
10960 buffer.set_language(Some(language), cx);
10961 });
10962
10963 // Ensure that signature_help is not called when no signature help is enabled.
10964 cx.set_state(
10965 &r#"
10966 fn main() {
10967 sampleˇ
10968 }
10969 "#
10970 .unindent(),
10971 );
10972 cx.update_editor(|editor, window, cx| {
10973 editor.handle_input("(", window, cx);
10974 });
10975 cx.assert_editor_state(
10976 &"
10977 fn main() {
10978 sample(ˇ)
10979 }
10980 "
10981 .unindent(),
10982 );
10983 cx.editor(|editor, _, _| {
10984 assert!(editor.signature_help_state.task().is_none());
10985 });
10986
10987 let mocked_response = lsp::SignatureHelp {
10988 signatures: vec![lsp::SignatureInformation {
10989 label: "fn sample(param1: u8, param2: u8)".to_string(),
10990 documentation: None,
10991 parameters: Some(vec![
10992 lsp::ParameterInformation {
10993 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10994 documentation: None,
10995 },
10996 lsp::ParameterInformation {
10997 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10998 documentation: None,
10999 },
11000 ]),
11001 active_parameter: None,
11002 }],
11003 active_signature: Some(0),
11004 active_parameter: Some(0),
11005 };
11006
11007 // Ensure that signature_help is called when enabled afte edits
11008 cx.update(|_, cx| {
11009 cx.update_global::<SettingsStore, _>(|settings, cx| {
11010 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11011 settings.auto_signature_help = Some(false);
11012 settings.show_signature_help_after_edits = Some(true);
11013 });
11014 });
11015 });
11016 cx.set_state(
11017 &r#"
11018 fn main() {
11019 sampleˇ
11020 }
11021 "#
11022 .unindent(),
11023 );
11024 cx.update_editor(|editor, window, cx| {
11025 editor.handle_input("(", window, cx);
11026 });
11027 cx.assert_editor_state(
11028 &"
11029 fn main() {
11030 sample(ˇ)
11031 }
11032 "
11033 .unindent(),
11034 );
11035 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11036 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11037 .await;
11038 cx.update_editor(|editor, _, _| {
11039 let signature_help_state = editor.signature_help_state.popover().cloned();
11040 assert!(signature_help_state.is_some());
11041 let signature = signature_help_state.unwrap();
11042 assert_eq!(
11043 signature.signatures[signature.current_signature].label,
11044 "fn sample(param1: u8, param2: u8)"
11045 );
11046 editor.signature_help_state = SignatureHelpState::default();
11047 });
11048
11049 // Ensure that signature_help is called when auto signature help override is enabled
11050 cx.update(|_, cx| {
11051 cx.update_global::<SettingsStore, _>(|settings, cx| {
11052 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11053 settings.auto_signature_help = Some(true);
11054 settings.show_signature_help_after_edits = Some(false);
11055 });
11056 });
11057 });
11058 cx.set_state(
11059 &r#"
11060 fn main() {
11061 sampleˇ
11062 }
11063 "#
11064 .unindent(),
11065 );
11066 cx.update_editor(|editor, window, cx| {
11067 editor.handle_input("(", window, cx);
11068 });
11069 cx.assert_editor_state(
11070 &"
11071 fn main() {
11072 sample(ˇ)
11073 }
11074 "
11075 .unindent(),
11076 );
11077 handle_signature_help_request(&mut cx, mocked_response).await;
11078 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11079 .await;
11080 cx.editor(|editor, _, _| {
11081 let signature_help_state = editor.signature_help_state.popover().cloned();
11082 assert!(signature_help_state.is_some());
11083 let signature = signature_help_state.unwrap();
11084 assert_eq!(
11085 signature.signatures[signature.current_signature].label,
11086 "fn sample(param1: u8, param2: u8)"
11087 );
11088 });
11089}
11090
11091#[gpui::test]
11092async fn test_signature_help(cx: &mut TestAppContext) {
11093 init_test(cx, |_| {});
11094 cx.update(|cx| {
11095 cx.update_global::<SettingsStore, _>(|settings, cx| {
11096 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11097 settings.auto_signature_help = Some(true);
11098 });
11099 });
11100 });
11101
11102 let mut cx = EditorLspTestContext::new_rust(
11103 lsp::ServerCapabilities {
11104 signature_help_provider: Some(lsp::SignatureHelpOptions {
11105 ..Default::default()
11106 }),
11107 ..Default::default()
11108 },
11109 cx,
11110 )
11111 .await;
11112
11113 // A test that directly calls `show_signature_help`
11114 cx.update_editor(|editor, window, cx| {
11115 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11116 });
11117
11118 let mocked_response = lsp::SignatureHelp {
11119 signatures: vec![lsp::SignatureInformation {
11120 label: "fn sample(param1: u8, param2: u8)".to_string(),
11121 documentation: None,
11122 parameters: Some(vec![
11123 lsp::ParameterInformation {
11124 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11125 documentation: None,
11126 },
11127 lsp::ParameterInformation {
11128 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11129 documentation: None,
11130 },
11131 ]),
11132 active_parameter: None,
11133 }],
11134 active_signature: Some(0),
11135 active_parameter: Some(0),
11136 };
11137 handle_signature_help_request(&mut cx, mocked_response).await;
11138
11139 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11140 .await;
11141
11142 cx.editor(|editor, _, _| {
11143 let signature_help_state = editor.signature_help_state.popover().cloned();
11144 assert!(signature_help_state.is_some());
11145 let signature = signature_help_state.unwrap();
11146 assert_eq!(
11147 signature.signatures[signature.current_signature].label,
11148 "fn sample(param1: u8, param2: u8)"
11149 );
11150 });
11151
11152 // When exiting outside from inside the brackets, `signature_help` is closed.
11153 cx.set_state(indoc! {"
11154 fn main() {
11155 sample(ˇ);
11156 }
11157
11158 fn sample(param1: u8, param2: u8) {}
11159 "});
11160
11161 cx.update_editor(|editor, window, cx| {
11162 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11163 s.select_ranges([0..0])
11164 });
11165 });
11166
11167 let mocked_response = lsp::SignatureHelp {
11168 signatures: Vec::new(),
11169 active_signature: None,
11170 active_parameter: None,
11171 };
11172 handle_signature_help_request(&mut cx, mocked_response).await;
11173
11174 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11175 .await;
11176
11177 cx.editor(|editor, _, _| {
11178 assert!(!editor.signature_help_state.is_shown());
11179 });
11180
11181 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
11182 cx.set_state(indoc! {"
11183 fn main() {
11184 sample(ˇ);
11185 }
11186
11187 fn sample(param1: u8, param2: u8) {}
11188 "});
11189
11190 let mocked_response = lsp::SignatureHelp {
11191 signatures: vec![lsp::SignatureInformation {
11192 label: "fn sample(param1: u8, param2: u8)".to_string(),
11193 documentation: None,
11194 parameters: Some(vec![
11195 lsp::ParameterInformation {
11196 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11197 documentation: None,
11198 },
11199 lsp::ParameterInformation {
11200 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11201 documentation: None,
11202 },
11203 ]),
11204 active_parameter: None,
11205 }],
11206 active_signature: Some(0),
11207 active_parameter: Some(0),
11208 };
11209 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11210 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11211 .await;
11212 cx.editor(|editor, _, _| {
11213 assert!(editor.signature_help_state.is_shown());
11214 });
11215
11216 // Restore the popover with more parameter input
11217 cx.set_state(indoc! {"
11218 fn main() {
11219 sample(param1, param2ˇ);
11220 }
11221
11222 fn sample(param1: u8, param2: u8) {}
11223 "});
11224
11225 let mocked_response = lsp::SignatureHelp {
11226 signatures: vec![lsp::SignatureInformation {
11227 label: "fn sample(param1: u8, param2: u8)".to_string(),
11228 documentation: None,
11229 parameters: Some(vec![
11230 lsp::ParameterInformation {
11231 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11232 documentation: None,
11233 },
11234 lsp::ParameterInformation {
11235 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11236 documentation: None,
11237 },
11238 ]),
11239 active_parameter: None,
11240 }],
11241 active_signature: Some(0),
11242 active_parameter: Some(1),
11243 };
11244 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11245 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11246 .await;
11247
11248 // When selecting a range, the popover is gone.
11249 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
11250 cx.update_editor(|editor, window, cx| {
11251 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11252 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11253 })
11254 });
11255 cx.assert_editor_state(indoc! {"
11256 fn main() {
11257 sample(param1, «ˇparam2»);
11258 }
11259
11260 fn sample(param1: u8, param2: u8) {}
11261 "});
11262 cx.editor(|editor, _, _| {
11263 assert!(!editor.signature_help_state.is_shown());
11264 });
11265
11266 // When unselecting again, the popover is back if within the brackets.
11267 cx.update_editor(|editor, window, cx| {
11268 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11269 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11270 })
11271 });
11272 cx.assert_editor_state(indoc! {"
11273 fn main() {
11274 sample(param1, ˇparam2);
11275 }
11276
11277 fn sample(param1: u8, param2: u8) {}
11278 "});
11279 handle_signature_help_request(&mut cx, mocked_response).await;
11280 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11281 .await;
11282 cx.editor(|editor, _, _| {
11283 assert!(editor.signature_help_state.is_shown());
11284 });
11285
11286 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
11287 cx.update_editor(|editor, window, cx| {
11288 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11289 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
11290 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11291 })
11292 });
11293 cx.assert_editor_state(indoc! {"
11294 fn main() {
11295 sample(param1, ˇparam2);
11296 }
11297
11298 fn sample(param1: u8, param2: u8) {}
11299 "});
11300
11301 let mocked_response = lsp::SignatureHelp {
11302 signatures: vec![lsp::SignatureInformation {
11303 label: "fn sample(param1: u8, param2: u8)".to_string(),
11304 documentation: None,
11305 parameters: Some(vec![
11306 lsp::ParameterInformation {
11307 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11308 documentation: None,
11309 },
11310 lsp::ParameterInformation {
11311 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11312 documentation: None,
11313 },
11314 ]),
11315 active_parameter: None,
11316 }],
11317 active_signature: Some(0),
11318 active_parameter: Some(1),
11319 };
11320 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11321 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11322 .await;
11323 cx.update_editor(|editor, _, cx| {
11324 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
11325 });
11326 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11327 .await;
11328 cx.update_editor(|editor, window, cx| {
11329 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11330 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11331 })
11332 });
11333 cx.assert_editor_state(indoc! {"
11334 fn main() {
11335 sample(param1, «ˇparam2»);
11336 }
11337
11338 fn sample(param1: u8, param2: u8) {}
11339 "});
11340 cx.update_editor(|editor, window, cx| {
11341 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11342 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11343 })
11344 });
11345 cx.assert_editor_state(indoc! {"
11346 fn main() {
11347 sample(param1, ˇparam2);
11348 }
11349
11350 fn sample(param1: u8, param2: u8) {}
11351 "});
11352 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
11353 .await;
11354}
11355
11356#[gpui::test]
11357async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
11358 init_test(cx, |_| {});
11359
11360 let mut cx = EditorLspTestContext::new_rust(
11361 lsp::ServerCapabilities {
11362 signature_help_provider: Some(lsp::SignatureHelpOptions {
11363 ..Default::default()
11364 }),
11365 ..Default::default()
11366 },
11367 cx,
11368 )
11369 .await;
11370
11371 cx.set_state(indoc! {"
11372 fn main() {
11373 overloadedˇ
11374 }
11375 "});
11376
11377 cx.update_editor(|editor, window, cx| {
11378 editor.handle_input("(", window, cx);
11379 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11380 });
11381
11382 // Mock response with 3 signatures
11383 let mocked_response = lsp::SignatureHelp {
11384 signatures: vec![
11385 lsp::SignatureInformation {
11386 label: "fn overloaded(x: i32)".to_string(),
11387 documentation: None,
11388 parameters: Some(vec![lsp::ParameterInformation {
11389 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11390 documentation: None,
11391 }]),
11392 active_parameter: None,
11393 },
11394 lsp::SignatureInformation {
11395 label: "fn overloaded(x: i32, y: i32)".to_string(),
11396 documentation: None,
11397 parameters: Some(vec![
11398 lsp::ParameterInformation {
11399 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11400 documentation: None,
11401 },
11402 lsp::ParameterInformation {
11403 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11404 documentation: None,
11405 },
11406 ]),
11407 active_parameter: None,
11408 },
11409 lsp::SignatureInformation {
11410 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
11411 documentation: None,
11412 parameters: Some(vec![
11413 lsp::ParameterInformation {
11414 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11415 documentation: None,
11416 },
11417 lsp::ParameterInformation {
11418 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11419 documentation: None,
11420 },
11421 lsp::ParameterInformation {
11422 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
11423 documentation: None,
11424 },
11425 ]),
11426 active_parameter: None,
11427 },
11428 ],
11429 active_signature: Some(1),
11430 active_parameter: Some(0),
11431 };
11432 handle_signature_help_request(&mut cx, mocked_response).await;
11433
11434 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11435 .await;
11436
11437 // Verify we have multiple signatures and the right one is selected
11438 cx.editor(|editor, _, _| {
11439 let popover = editor.signature_help_state.popover().cloned().unwrap();
11440 assert_eq!(popover.signatures.len(), 3);
11441 // active_signature was 1, so that should be the current
11442 assert_eq!(popover.current_signature, 1);
11443 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
11444 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
11445 assert_eq!(
11446 popover.signatures[2].label,
11447 "fn overloaded(x: i32, y: i32, z: i32)"
11448 );
11449 });
11450
11451 // Test navigation functionality
11452 cx.update_editor(|editor, window, cx| {
11453 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11454 });
11455
11456 cx.editor(|editor, _, _| {
11457 let popover = editor.signature_help_state.popover().cloned().unwrap();
11458 assert_eq!(popover.current_signature, 2);
11459 });
11460
11461 // Test wrap around
11462 cx.update_editor(|editor, window, cx| {
11463 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11464 });
11465
11466 cx.editor(|editor, _, _| {
11467 let popover = editor.signature_help_state.popover().cloned().unwrap();
11468 assert_eq!(popover.current_signature, 0);
11469 });
11470
11471 // Test previous navigation
11472 cx.update_editor(|editor, window, cx| {
11473 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
11474 });
11475
11476 cx.editor(|editor, _, _| {
11477 let popover = editor.signature_help_state.popover().cloned().unwrap();
11478 assert_eq!(popover.current_signature, 2);
11479 });
11480}
11481
11482#[gpui::test]
11483async fn test_completion_mode(cx: &mut TestAppContext) {
11484 init_test(cx, |_| {});
11485 let mut cx = EditorLspTestContext::new_rust(
11486 lsp::ServerCapabilities {
11487 completion_provider: Some(lsp::CompletionOptions {
11488 resolve_provider: Some(true),
11489 ..Default::default()
11490 }),
11491 ..Default::default()
11492 },
11493 cx,
11494 )
11495 .await;
11496
11497 struct Run {
11498 run_description: &'static str,
11499 initial_state: String,
11500 buffer_marked_text: String,
11501 completion_label: &'static str,
11502 completion_text: &'static str,
11503 expected_with_insert_mode: String,
11504 expected_with_replace_mode: String,
11505 expected_with_replace_subsequence_mode: String,
11506 expected_with_replace_suffix_mode: String,
11507 }
11508
11509 let runs = [
11510 Run {
11511 run_description: "Start of word matches completion text",
11512 initial_state: "before ediˇ after".into(),
11513 buffer_marked_text: "before <edi|> after".into(),
11514 completion_label: "editor",
11515 completion_text: "editor",
11516 expected_with_insert_mode: "before editorˇ after".into(),
11517 expected_with_replace_mode: "before editorˇ after".into(),
11518 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11519 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11520 },
11521 Run {
11522 run_description: "Accept same text at the middle of the word",
11523 initial_state: "before ediˇtor after".into(),
11524 buffer_marked_text: "before <edi|tor> after".into(),
11525 completion_label: "editor",
11526 completion_text: "editor",
11527 expected_with_insert_mode: "before editorˇtor after".into(),
11528 expected_with_replace_mode: "before editorˇ after".into(),
11529 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11530 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11531 },
11532 Run {
11533 run_description: "End of word matches completion text -- cursor at end",
11534 initial_state: "before torˇ after".into(),
11535 buffer_marked_text: "before <tor|> after".into(),
11536 completion_label: "editor",
11537 completion_text: "editor",
11538 expected_with_insert_mode: "before editorˇ after".into(),
11539 expected_with_replace_mode: "before editorˇ after".into(),
11540 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11541 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11542 },
11543 Run {
11544 run_description: "End of word matches completion text -- cursor at start",
11545 initial_state: "before ˇtor after".into(),
11546 buffer_marked_text: "before <|tor> after".into(),
11547 completion_label: "editor",
11548 completion_text: "editor",
11549 expected_with_insert_mode: "before editorˇtor after".into(),
11550 expected_with_replace_mode: "before editorˇ after".into(),
11551 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11552 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11553 },
11554 Run {
11555 run_description: "Prepend text containing whitespace",
11556 initial_state: "pˇfield: bool".into(),
11557 buffer_marked_text: "<p|field>: bool".into(),
11558 completion_label: "pub ",
11559 completion_text: "pub ",
11560 expected_with_insert_mode: "pub ˇfield: bool".into(),
11561 expected_with_replace_mode: "pub ˇ: bool".into(),
11562 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
11563 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
11564 },
11565 Run {
11566 run_description: "Add element to start of list",
11567 initial_state: "[element_ˇelement_2]".into(),
11568 buffer_marked_text: "[<element_|element_2>]".into(),
11569 completion_label: "element_1",
11570 completion_text: "element_1",
11571 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
11572 expected_with_replace_mode: "[element_1ˇ]".into(),
11573 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
11574 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
11575 },
11576 Run {
11577 run_description: "Add element to start of list -- first and second elements are equal",
11578 initial_state: "[elˇelement]".into(),
11579 buffer_marked_text: "[<el|element>]".into(),
11580 completion_label: "element",
11581 completion_text: "element",
11582 expected_with_insert_mode: "[elementˇelement]".into(),
11583 expected_with_replace_mode: "[elementˇ]".into(),
11584 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
11585 expected_with_replace_suffix_mode: "[elementˇ]".into(),
11586 },
11587 Run {
11588 run_description: "Ends with matching suffix",
11589 initial_state: "SubˇError".into(),
11590 buffer_marked_text: "<Sub|Error>".into(),
11591 completion_label: "SubscriptionError",
11592 completion_text: "SubscriptionError",
11593 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
11594 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11595 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11596 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
11597 },
11598 Run {
11599 run_description: "Suffix is a subsequence -- contiguous",
11600 initial_state: "SubˇErr".into(),
11601 buffer_marked_text: "<Sub|Err>".into(),
11602 completion_label: "SubscriptionError",
11603 completion_text: "SubscriptionError",
11604 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
11605 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11606 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11607 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
11608 },
11609 Run {
11610 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
11611 initial_state: "Suˇscrirr".into(),
11612 buffer_marked_text: "<Su|scrirr>".into(),
11613 completion_label: "SubscriptionError",
11614 completion_text: "SubscriptionError",
11615 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
11616 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11617 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11618 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
11619 },
11620 Run {
11621 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
11622 initial_state: "foo(indˇix)".into(),
11623 buffer_marked_text: "foo(<ind|ix>)".into(),
11624 completion_label: "node_index",
11625 completion_text: "node_index",
11626 expected_with_insert_mode: "foo(node_indexˇix)".into(),
11627 expected_with_replace_mode: "foo(node_indexˇ)".into(),
11628 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
11629 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
11630 },
11631 Run {
11632 run_description: "Replace range ends before cursor - should extend to cursor",
11633 initial_state: "before editˇo after".into(),
11634 buffer_marked_text: "before <{ed}>it|o after".into(),
11635 completion_label: "editor",
11636 completion_text: "editor",
11637 expected_with_insert_mode: "before editorˇo after".into(),
11638 expected_with_replace_mode: "before editorˇo after".into(),
11639 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
11640 expected_with_replace_suffix_mode: "before editorˇo after".into(),
11641 },
11642 Run {
11643 run_description: "Uses label for suffix matching",
11644 initial_state: "before ediˇtor after".into(),
11645 buffer_marked_text: "before <edi|tor> after".into(),
11646 completion_label: "editor",
11647 completion_text: "editor()",
11648 expected_with_insert_mode: "before editor()ˇtor after".into(),
11649 expected_with_replace_mode: "before editor()ˇ after".into(),
11650 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
11651 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
11652 },
11653 Run {
11654 run_description: "Case insensitive subsequence and suffix matching",
11655 initial_state: "before EDiˇtoR after".into(),
11656 buffer_marked_text: "before <EDi|toR> after".into(),
11657 completion_label: "editor",
11658 completion_text: "editor",
11659 expected_with_insert_mode: "before editorˇtoR after".into(),
11660 expected_with_replace_mode: "before editorˇ after".into(),
11661 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11662 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11663 },
11664 ];
11665
11666 for run in runs {
11667 let run_variations = [
11668 (LspInsertMode::Insert, run.expected_with_insert_mode),
11669 (LspInsertMode::Replace, run.expected_with_replace_mode),
11670 (
11671 LspInsertMode::ReplaceSubsequence,
11672 run.expected_with_replace_subsequence_mode,
11673 ),
11674 (
11675 LspInsertMode::ReplaceSuffix,
11676 run.expected_with_replace_suffix_mode,
11677 ),
11678 ];
11679
11680 for (lsp_insert_mode, expected_text) in run_variations {
11681 eprintln!(
11682 "run = {:?}, mode = {lsp_insert_mode:.?}",
11683 run.run_description,
11684 );
11685
11686 update_test_language_settings(&mut cx, |settings| {
11687 settings.defaults.completions = Some(CompletionSettings {
11688 lsp_insert_mode,
11689 words: WordsCompletionMode::Disabled,
11690 lsp: true,
11691 lsp_fetch_timeout_ms: 0,
11692 });
11693 });
11694
11695 cx.set_state(&run.initial_state);
11696 cx.update_editor(|editor, window, cx| {
11697 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11698 });
11699
11700 let counter = Arc::new(AtomicUsize::new(0));
11701 handle_completion_request_with_insert_and_replace(
11702 &mut cx,
11703 &run.buffer_marked_text,
11704 vec![(run.completion_label, run.completion_text)],
11705 counter.clone(),
11706 )
11707 .await;
11708 cx.condition(|editor, _| editor.context_menu_visible())
11709 .await;
11710 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11711
11712 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11713 editor
11714 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11715 .unwrap()
11716 });
11717 cx.assert_editor_state(&expected_text);
11718 handle_resolve_completion_request(&mut cx, None).await;
11719 apply_additional_edits.await.unwrap();
11720 }
11721 }
11722}
11723
11724#[gpui::test]
11725async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
11726 init_test(cx, |_| {});
11727 let mut cx = EditorLspTestContext::new_rust(
11728 lsp::ServerCapabilities {
11729 completion_provider: Some(lsp::CompletionOptions {
11730 resolve_provider: Some(true),
11731 ..Default::default()
11732 }),
11733 ..Default::default()
11734 },
11735 cx,
11736 )
11737 .await;
11738
11739 let initial_state = "SubˇError";
11740 let buffer_marked_text = "<Sub|Error>";
11741 let completion_text = "SubscriptionError";
11742 let expected_with_insert_mode = "SubscriptionErrorˇError";
11743 let expected_with_replace_mode = "SubscriptionErrorˇ";
11744
11745 update_test_language_settings(&mut cx, |settings| {
11746 settings.defaults.completions = Some(CompletionSettings {
11747 words: WordsCompletionMode::Disabled,
11748 // set the opposite here to ensure that the action is overriding the default behavior
11749 lsp_insert_mode: LspInsertMode::Insert,
11750 lsp: true,
11751 lsp_fetch_timeout_ms: 0,
11752 });
11753 });
11754
11755 cx.set_state(initial_state);
11756 cx.update_editor(|editor, window, cx| {
11757 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11758 });
11759
11760 let counter = Arc::new(AtomicUsize::new(0));
11761 handle_completion_request_with_insert_and_replace(
11762 &mut cx,
11763 &buffer_marked_text,
11764 vec![(completion_text, completion_text)],
11765 counter.clone(),
11766 )
11767 .await;
11768 cx.condition(|editor, _| editor.context_menu_visible())
11769 .await;
11770 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11771
11772 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11773 editor
11774 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11775 .unwrap()
11776 });
11777 cx.assert_editor_state(&expected_with_replace_mode);
11778 handle_resolve_completion_request(&mut cx, None).await;
11779 apply_additional_edits.await.unwrap();
11780
11781 update_test_language_settings(&mut cx, |settings| {
11782 settings.defaults.completions = Some(CompletionSettings {
11783 words: WordsCompletionMode::Disabled,
11784 // set the opposite here to ensure that the action is overriding the default behavior
11785 lsp_insert_mode: LspInsertMode::Replace,
11786 lsp: true,
11787 lsp_fetch_timeout_ms: 0,
11788 });
11789 });
11790
11791 cx.set_state(initial_state);
11792 cx.update_editor(|editor, window, cx| {
11793 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11794 });
11795 handle_completion_request_with_insert_and_replace(
11796 &mut cx,
11797 &buffer_marked_text,
11798 vec![(completion_text, completion_text)],
11799 counter.clone(),
11800 )
11801 .await;
11802 cx.condition(|editor, _| editor.context_menu_visible())
11803 .await;
11804 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11805
11806 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11807 editor
11808 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
11809 .unwrap()
11810 });
11811 cx.assert_editor_state(&expected_with_insert_mode);
11812 handle_resolve_completion_request(&mut cx, None).await;
11813 apply_additional_edits.await.unwrap();
11814}
11815
11816#[gpui::test]
11817async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
11818 init_test(cx, |_| {});
11819 let mut cx = EditorLspTestContext::new_rust(
11820 lsp::ServerCapabilities {
11821 completion_provider: Some(lsp::CompletionOptions {
11822 resolve_provider: Some(true),
11823 ..Default::default()
11824 }),
11825 ..Default::default()
11826 },
11827 cx,
11828 )
11829 .await;
11830
11831 // scenario: surrounding text matches completion text
11832 let completion_text = "to_offset";
11833 let initial_state = indoc! {"
11834 1. buf.to_offˇsuffix
11835 2. buf.to_offˇsuf
11836 3. buf.to_offˇfix
11837 4. buf.to_offˇ
11838 5. into_offˇensive
11839 6. ˇsuffix
11840 7. let ˇ //
11841 8. aaˇzz
11842 9. buf.to_off«zzzzzˇ»suffix
11843 10. buf.«ˇzzzzz»suffix
11844 11. to_off«ˇzzzzz»
11845
11846 buf.to_offˇsuffix // newest cursor
11847 "};
11848 let completion_marked_buffer = indoc! {"
11849 1. buf.to_offsuffix
11850 2. buf.to_offsuf
11851 3. buf.to_offfix
11852 4. buf.to_off
11853 5. into_offensive
11854 6. suffix
11855 7. let //
11856 8. aazz
11857 9. buf.to_offzzzzzsuffix
11858 10. buf.zzzzzsuffix
11859 11. to_offzzzzz
11860
11861 buf.<to_off|suffix> // newest cursor
11862 "};
11863 let expected = indoc! {"
11864 1. buf.to_offsetˇ
11865 2. buf.to_offsetˇsuf
11866 3. buf.to_offsetˇfix
11867 4. buf.to_offsetˇ
11868 5. into_offsetˇensive
11869 6. to_offsetˇsuffix
11870 7. let to_offsetˇ //
11871 8. aato_offsetˇzz
11872 9. buf.to_offsetˇ
11873 10. buf.to_offsetˇsuffix
11874 11. to_offsetˇ
11875
11876 buf.to_offsetˇ // newest cursor
11877 "};
11878 cx.set_state(initial_state);
11879 cx.update_editor(|editor, window, cx| {
11880 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11881 });
11882 handle_completion_request_with_insert_and_replace(
11883 &mut cx,
11884 completion_marked_buffer,
11885 vec![(completion_text, completion_text)],
11886 Arc::new(AtomicUsize::new(0)),
11887 )
11888 .await;
11889 cx.condition(|editor, _| editor.context_menu_visible())
11890 .await;
11891 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11892 editor
11893 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11894 .unwrap()
11895 });
11896 cx.assert_editor_state(expected);
11897 handle_resolve_completion_request(&mut cx, None).await;
11898 apply_additional_edits.await.unwrap();
11899
11900 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
11901 let completion_text = "foo_and_bar";
11902 let initial_state = indoc! {"
11903 1. ooanbˇ
11904 2. zooanbˇ
11905 3. ooanbˇz
11906 4. zooanbˇz
11907 5. ooanˇ
11908 6. oanbˇ
11909
11910 ooanbˇ
11911 "};
11912 let completion_marked_buffer = indoc! {"
11913 1. ooanb
11914 2. zooanb
11915 3. ooanbz
11916 4. zooanbz
11917 5. ooan
11918 6. oanb
11919
11920 <ooanb|>
11921 "};
11922 let expected = indoc! {"
11923 1. foo_and_barˇ
11924 2. zfoo_and_barˇ
11925 3. foo_and_barˇz
11926 4. zfoo_and_barˇz
11927 5. ooanfoo_and_barˇ
11928 6. oanbfoo_and_barˇ
11929
11930 foo_and_barˇ
11931 "};
11932 cx.set_state(initial_state);
11933 cx.update_editor(|editor, window, cx| {
11934 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11935 });
11936 handle_completion_request_with_insert_and_replace(
11937 &mut cx,
11938 completion_marked_buffer,
11939 vec![(completion_text, completion_text)],
11940 Arc::new(AtomicUsize::new(0)),
11941 )
11942 .await;
11943 cx.condition(|editor, _| editor.context_menu_visible())
11944 .await;
11945 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11946 editor
11947 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11948 .unwrap()
11949 });
11950 cx.assert_editor_state(expected);
11951 handle_resolve_completion_request(&mut cx, None).await;
11952 apply_additional_edits.await.unwrap();
11953
11954 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
11955 // (expects the same as if it was inserted at the end)
11956 let completion_text = "foo_and_bar";
11957 let initial_state = indoc! {"
11958 1. ooˇanb
11959 2. zooˇanb
11960 3. ooˇanbz
11961 4. zooˇanbz
11962
11963 ooˇanb
11964 "};
11965 let completion_marked_buffer = indoc! {"
11966 1. ooanb
11967 2. zooanb
11968 3. ooanbz
11969 4. zooanbz
11970
11971 <oo|anb>
11972 "};
11973 let expected = indoc! {"
11974 1. foo_and_barˇ
11975 2. zfoo_and_barˇ
11976 3. foo_and_barˇz
11977 4. zfoo_and_barˇz
11978
11979 foo_and_barˇ
11980 "};
11981 cx.set_state(initial_state);
11982 cx.update_editor(|editor, window, cx| {
11983 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11984 });
11985 handle_completion_request_with_insert_and_replace(
11986 &mut cx,
11987 completion_marked_buffer,
11988 vec![(completion_text, completion_text)],
11989 Arc::new(AtomicUsize::new(0)),
11990 )
11991 .await;
11992 cx.condition(|editor, _| editor.context_menu_visible())
11993 .await;
11994 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11995 editor
11996 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11997 .unwrap()
11998 });
11999 cx.assert_editor_state(expected);
12000 handle_resolve_completion_request(&mut cx, None).await;
12001 apply_additional_edits.await.unwrap();
12002}
12003
12004// This used to crash
12005#[gpui::test]
12006async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
12007 init_test(cx, |_| {});
12008
12009 let buffer_text = indoc! {"
12010 fn main() {
12011 10.satu;
12012
12013 //
12014 // separate cursors so they open in different excerpts (manually reproducible)
12015 //
12016
12017 10.satu20;
12018 }
12019 "};
12020 let multibuffer_text_with_selections = indoc! {"
12021 fn main() {
12022 10.satuˇ;
12023
12024 //
12025
12026 //
12027
12028 10.satuˇ20;
12029 }
12030 "};
12031 let expected_multibuffer = indoc! {"
12032 fn main() {
12033 10.saturating_sub()ˇ;
12034
12035 //
12036
12037 //
12038
12039 10.saturating_sub()ˇ;
12040 }
12041 "};
12042
12043 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
12044 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
12045
12046 let fs = FakeFs::new(cx.executor());
12047 fs.insert_tree(
12048 path!("/a"),
12049 json!({
12050 "main.rs": buffer_text,
12051 }),
12052 )
12053 .await;
12054
12055 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12056 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12057 language_registry.add(rust_lang());
12058 let mut fake_servers = language_registry.register_fake_lsp(
12059 "Rust",
12060 FakeLspAdapter {
12061 capabilities: lsp::ServerCapabilities {
12062 completion_provider: Some(lsp::CompletionOptions {
12063 resolve_provider: None,
12064 ..lsp::CompletionOptions::default()
12065 }),
12066 ..lsp::ServerCapabilities::default()
12067 },
12068 ..FakeLspAdapter::default()
12069 },
12070 );
12071 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12072 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12073 let buffer = project
12074 .update(cx, |project, cx| {
12075 project.open_local_buffer(path!("/a/main.rs"), cx)
12076 })
12077 .await
12078 .unwrap();
12079
12080 let multi_buffer = cx.new(|cx| {
12081 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
12082 multi_buffer.push_excerpts(
12083 buffer.clone(),
12084 [ExcerptRange::new(0..first_excerpt_end)],
12085 cx,
12086 );
12087 multi_buffer.push_excerpts(
12088 buffer.clone(),
12089 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
12090 cx,
12091 );
12092 multi_buffer
12093 });
12094
12095 let editor = workspace
12096 .update(cx, |_, window, cx| {
12097 cx.new(|cx| {
12098 Editor::new(
12099 EditorMode::Full {
12100 scale_ui_elements_with_buffer_font_size: false,
12101 show_active_line_background: false,
12102 sized_by_content: false,
12103 },
12104 multi_buffer.clone(),
12105 Some(project.clone()),
12106 window,
12107 cx,
12108 )
12109 })
12110 })
12111 .unwrap();
12112
12113 let pane = workspace
12114 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12115 .unwrap();
12116 pane.update_in(cx, |pane, window, cx| {
12117 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
12118 });
12119
12120 let fake_server = fake_servers.next().await.unwrap();
12121
12122 editor.update_in(cx, |editor, window, cx| {
12123 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12124 s.select_ranges([
12125 Point::new(1, 11)..Point::new(1, 11),
12126 Point::new(7, 11)..Point::new(7, 11),
12127 ])
12128 });
12129
12130 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
12131 });
12132
12133 editor.update_in(cx, |editor, window, cx| {
12134 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12135 });
12136
12137 fake_server
12138 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12139 let completion_item = lsp::CompletionItem {
12140 label: "saturating_sub()".into(),
12141 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
12142 lsp::InsertReplaceEdit {
12143 new_text: "saturating_sub()".to_owned(),
12144 insert: lsp::Range::new(
12145 lsp::Position::new(7, 7),
12146 lsp::Position::new(7, 11),
12147 ),
12148 replace: lsp::Range::new(
12149 lsp::Position::new(7, 7),
12150 lsp::Position::new(7, 13),
12151 ),
12152 },
12153 )),
12154 ..lsp::CompletionItem::default()
12155 };
12156
12157 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
12158 })
12159 .next()
12160 .await
12161 .unwrap();
12162
12163 cx.condition(&editor, |editor, _| editor.context_menu_visible())
12164 .await;
12165
12166 editor
12167 .update_in(cx, |editor, window, cx| {
12168 editor
12169 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12170 .unwrap()
12171 })
12172 .await
12173 .unwrap();
12174
12175 editor.update(cx, |editor, cx| {
12176 assert_text_with_selections(editor, expected_multibuffer, cx);
12177 })
12178}
12179
12180#[gpui::test]
12181async fn test_completion(cx: &mut TestAppContext) {
12182 init_test(cx, |_| {});
12183
12184 let mut cx = EditorLspTestContext::new_rust(
12185 lsp::ServerCapabilities {
12186 completion_provider: Some(lsp::CompletionOptions {
12187 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12188 resolve_provider: Some(true),
12189 ..Default::default()
12190 }),
12191 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12192 ..Default::default()
12193 },
12194 cx,
12195 )
12196 .await;
12197 let counter = Arc::new(AtomicUsize::new(0));
12198
12199 cx.set_state(indoc! {"
12200 oneˇ
12201 two
12202 three
12203 "});
12204 cx.simulate_keystroke(".");
12205 handle_completion_request(
12206 indoc! {"
12207 one.|<>
12208 two
12209 three
12210 "},
12211 vec!["first_completion", "second_completion"],
12212 true,
12213 counter.clone(),
12214 &mut cx,
12215 )
12216 .await;
12217 cx.condition(|editor, _| editor.context_menu_visible())
12218 .await;
12219 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12220
12221 let _handler = handle_signature_help_request(
12222 &mut cx,
12223 lsp::SignatureHelp {
12224 signatures: vec![lsp::SignatureInformation {
12225 label: "test signature".to_string(),
12226 documentation: None,
12227 parameters: Some(vec![lsp::ParameterInformation {
12228 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
12229 documentation: None,
12230 }]),
12231 active_parameter: None,
12232 }],
12233 active_signature: None,
12234 active_parameter: None,
12235 },
12236 );
12237 cx.update_editor(|editor, window, cx| {
12238 assert!(
12239 !editor.signature_help_state.is_shown(),
12240 "No signature help was called for"
12241 );
12242 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12243 });
12244 cx.run_until_parked();
12245 cx.update_editor(|editor, _, _| {
12246 assert!(
12247 !editor.signature_help_state.is_shown(),
12248 "No signature help should be shown when completions menu is open"
12249 );
12250 });
12251
12252 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12253 editor.context_menu_next(&Default::default(), window, cx);
12254 editor
12255 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12256 .unwrap()
12257 });
12258 cx.assert_editor_state(indoc! {"
12259 one.second_completionˇ
12260 two
12261 three
12262 "});
12263
12264 handle_resolve_completion_request(
12265 &mut cx,
12266 Some(vec![
12267 (
12268 //This overlaps with the primary completion edit which is
12269 //misbehavior from the LSP spec, test that we filter it out
12270 indoc! {"
12271 one.second_ˇcompletion
12272 two
12273 threeˇ
12274 "},
12275 "overlapping additional edit",
12276 ),
12277 (
12278 indoc! {"
12279 one.second_completion
12280 two
12281 threeˇ
12282 "},
12283 "\nadditional edit",
12284 ),
12285 ]),
12286 )
12287 .await;
12288 apply_additional_edits.await.unwrap();
12289 cx.assert_editor_state(indoc! {"
12290 one.second_completionˇ
12291 two
12292 three
12293 additional edit
12294 "});
12295
12296 cx.set_state(indoc! {"
12297 one.second_completion
12298 twoˇ
12299 threeˇ
12300 additional edit
12301 "});
12302 cx.simulate_keystroke(" ");
12303 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12304 cx.simulate_keystroke("s");
12305 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12306
12307 cx.assert_editor_state(indoc! {"
12308 one.second_completion
12309 two sˇ
12310 three sˇ
12311 additional edit
12312 "});
12313 handle_completion_request(
12314 indoc! {"
12315 one.second_completion
12316 two s
12317 three <s|>
12318 additional edit
12319 "},
12320 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12321 true,
12322 counter.clone(),
12323 &mut cx,
12324 )
12325 .await;
12326 cx.condition(|editor, _| editor.context_menu_visible())
12327 .await;
12328 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12329
12330 cx.simulate_keystroke("i");
12331
12332 handle_completion_request(
12333 indoc! {"
12334 one.second_completion
12335 two si
12336 three <si|>
12337 additional edit
12338 "},
12339 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12340 true,
12341 counter.clone(),
12342 &mut cx,
12343 )
12344 .await;
12345 cx.condition(|editor, _| editor.context_menu_visible())
12346 .await;
12347 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12348
12349 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12350 editor
12351 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12352 .unwrap()
12353 });
12354 cx.assert_editor_state(indoc! {"
12355 one.second_completion
12356 two sixth_completionˇ
12357 three sixth_completionˇ
12358 additional edit
12359 "});
12360
12361 apply_additional_edits.await.unwrap();
12362
12363 update_test_language_settings(&mut cx, |settings| {
12364 settings.defaults.show_completions_on_input = Some(false);
12365 });
12366 cx.set_state("editorˇ");
12367 cx.simulate_keystroke(".");
12368 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12369 cx.simulate_keystrokes("c l o");
12370 cx.assert_editor_state("editor.cloˇ");
12371 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12372 cx.update_editor(|editor, window, cx| {
12373 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12374 });
12375 handle_completion_request(
12376 "editor.<clo|>",
12377 vec!["close", "clobber"],
12378 true,
12379 counter.clone(),
12380 &mut cx,
12381 )
12382 .await;
12383 cx.condition(|editor, _| editor.context_menu_visible())
12384 .await;
12385 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
12386
12387 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12388 editor
12389 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12390 .unwrap()
12391 });
12392 cx.assert_editor_state("editor.clobberˇ");
12393 handle_resolve_completion_request(&mut cx, None).await;
12394 apply_additional_edits.await.unwrap();
12395}
12396
12397#[gpui::test]
12398async fn test_completion_reuse(cx: &mut TestAppContext) {
12399 init_test(cx, |_| {});
12400
12401 let mut cx = EditorLspTestContext::new_rust(
12402 lsp::ServerCapabilities {
12403 completion_provider: Some(lsp::CompletionOptions {
12404 trigger_characters: Some(vec![".".to_string()]),
12405 ..Default::default()
12406 }),
12407 ..Default::default()
12408 },
12409 cx,
12410 )
12411 .await;
12412
12413 let counter = Arc::new(AtomicUsize::new(0));
12414 cx.set_state("objˇ");
12415 cx.simulate_keystroke(".");
12416
12417 // Initial completion request returns complete results
12418 let is_incomplete = false;
12419 handle_completion_request(
12420 "obj.|<>",
12421 vec!["a", "ab", "abc"],
12422 is_incomplete,
12423 counter.clone(),
12424 &mut cx,
12425 )
12426 .await;
12427 cx.run_until_parked();
12428 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12429 cx.assert_editor_state("obj.ˇ");
12430 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12431
12432 // Type "a" - filters existing completions
12433 cx.simulate_keystroke("a");
12434 cx.run_until_parked();
12435 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12436 cx.assert_editor_state("obj.aˇ");
12437 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12438
12439 // Type "b" - filters existing completions
12440 cx.simulate_keystroke("b");
12441 cx.run_until_parked();
12442 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12443 cx.assert_editor_state("obj.abˇ");
12444 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12445
12446 // Type "c" - filters existing completions
12447 cx.simulate_keystroke("c");
12448 cx.run_until_parked();
12449 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12450 cx.assert_editor_state("obj.abcˇ");
12451 check_displayed_completions(vec!["abc"], &mut cx);
12452
12453 // Backspace to delete "c" - filters existing completions
12454 cx.update_editor(|editor, window, cx| {
12455 editor.backspace(&Backspace, window, cx);
12456 });
12457 cx.run_until_parked();
12458 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12459 cx.assert_editor_state("obj.abˇ");
12460 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12461
12462 // Moving cursor to the left dismisses menu.
12463 cx.update_editor(|editor, window, cx| {
12464 editor.move_left(&MoveLeft, window, cx);
12465 });
12466 cx.run_until_parked();
12467 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12468 cx.assert_editor_state("obj.aˇb");
12469 cx.update_editor(|editor, _, _| {
12470 assert_eq!(editor.context_menu_visible(), false);
12471 });
12472
12473 // Type "b" - new request
12474 cx.simulate_keystroke("b");
12475 let is_incomplete = false;
12476 handle_completion_request(
12477 "obj.<ab|>a",
12478 vec!["ab", "abc"],
12479 is_incomplete,
12480 counter.clone(),
12481 &mut cx,
12482 )
12483 .await;
12484 cx.run_until_parked();
12485 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12486 cx.assert_editor_state("obj.abˇb");
12487 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12488
12489 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
12490 cx.update_editor(|editor, window, cx| {
12491 editor.backspace(&Backspace, window, cx);
12492 });
12493 let is_incomplete = false;
12494 handle_completion_request(
12495 "obj.<a|>b",
12496 vec!["a", "ab", "abc"],
12497 is_incomplete,
12498 counter.clone(),
12499 &mut cx,
12500 )
12501 .await;
12502 cx.run_until_parked();
12503 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12504 cx.assert_editor_state("obj.aˇb");
12505 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12506
12507 // Backspace to delete "a" - dismisses menu.
12508 cx.update_editor(|editor, window, cx| {
12509 editor.backspace(&Backspace, window, cx);
12510 });
12511 cx.run_until_parked();
12512 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12513 cx.assert_editor_state("obj.ˇb");
12514 cx.update_editor(|editor, _, _| {
12515 assert_eq!(editor.context_menu_visible(), false);
12516 });
12517}
12518
12519#[gpui::test]
12520async fn test_word_completion(cx: &mut TestAppContext) {
12521 let lsp_fetch_timeout_ms = 10;
12522 init_test(cx, |language_settings| {
12523 language_settings.defaults.completions = Some(CompletionSettings {
12524 words: WordsCompletionMode::Fallback,
12525 lsp: true,
12526 lsp_fetch_timeout_ms: 10,
12527 lsp_insert_mode: LspInsertMode::Insert,
12528 });
12529 });
12530
12531 let mut cx = EditorLspTestContext::new_rust(
12532 lsp::ServerCapabilities {
12533 completion_provider: Some(lsp::CompletionOptions {
12534 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12535 ..lsp::CompletionOptions::default()
12536 }),
12537 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12538 ..lsp::ServerCapabilities::default()
12539 },
12540 cx,
12541 )
12542 .await;
12543
12544 let throttle_completions = Arc::new(AtomicBool::new(false));
12545
12546 let lsp_throttle_completions = throttle_completions.clone();
12547 let _completion_requests_handler =
12548 cx.lsp
12549 .server
12550 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
12551 let lsp_throttle_completions = lsp_throttle_completions.clone();
12552 let cx = cx.clone();
12553 async move {
12554 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
12555 cx.background_executor()
12556 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
12557 .await;
12558 }
12559 Ok(Some(lsp::CompletionResponse::Array(vec![
12560 lsp::CompletionItem {
12561 label: "first".into(),
12562 ..lsp::CompletionItem::default()
12563 },
12564 lsp::CompletionItem {
12565 label: "last".into(),
12566 ..lsp::CompletionItem::default()
12567 },
12568 ])))
12569 }
12570 });
12571
12572 cx.set_state(indoc! {"
12573 oneˇ
12574 two
12575 three
12576 "});
12577 cx.simulate_keystroke(".");
12578 cx.executor().run_until_parked();
12579 cx.condition(|editor, _| editor.context_menu_visible())
12580 .await;
12581 cx.update_editor(|editor, window, cx| {
12582 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12583 {
12584 assert_eq!(
12585 completion_menu_entries(&menu),
12586 &["first", "last"],
12587 "When LSP server is fast to reply, no fallback word completions are used"
12588 );
12589 } else {
12590 panic!("expected completion menu to be open");
12591 }
12592 editor.cancel(&Cancel, window, cx);
12593 });
12594 cx.executor().run_until_parked();
12595 cx.condition(|editor, _| !editor.context_menu_visible())
12596 .await;
12597
12598 throttle_completions.store(true, atomic::Ordering::Release);
12599 cx.simulate_keystroke(".");
12600 cx.executor()
12601 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
12602 cx.executor().run_until_parked();
12603 cx.condition(|editor, _| editor.context_menu_visible())
12604 .await;
12605 cx.update_editor(|editor, _, _| {
12606 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12607 {
12608 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
12609 "When LSP server is slow, document words can be shown instead, if configured accordingly");
12610 } else {
12611 panic!("expected completion menu to be open");
12612 }
12613 });
12614}
12615
12616#[gpui::test]
12617async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
12618 init_test(cx, |language_settings| {
12619 language_settings.defaults.completions = Some(CompletionSettings {
12620 words: WordsCompletionMode::Enabled,
12621 lsp: true,
12622 lsp_fetch_timeout_ms: 0,
12623 lsp_insert_mode: LspInsertMode::Insert,
12624 });
12625 });
12626
12627 let mut cx = EditorLspTestContext::new_rust(
12628 lsp::ServerCapabilities {
12629 completion_provider: Some(lsp::CompletionOptions {
12630 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12631 ..lsp::CompletionOptions::default()
12632 }),
12633 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12634 ..lsp::ServerCapabilities::default()
12635 },
12636 cx,
12637 )
12638 .await;
12639
12640 let _completion_requests_handler =
12641 cx.lsp
12642 .server
12643 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12644 Ok(Some(lsp::CompletionResponse::Array(vec![
12645 lsp::CompletionItem {
12646 label: "first".into(),
12647 ..lsp::CompletionItem::default()
12648 },
12649 lsp::CompletionItem {
12650 label: "last".into(),
12651 ..lsp::CompletionItem::default()
12652 },
12653 ])))
12654 });
12655
12656 cx.set_state(indoc! {"ˇ
12657 first
12658 last
12659 second
12660 "});
12661 cx.simulate_keystroke(".");
12662 cx.executor().run_until_parked();
12663 cx.condition(|editor, _| editor.context_menu_visible())
12664 .await;
12665 cx.update_editor(|editor, _, _| {
12666 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12667 {
12668 assert_eq!(
12669 completion_menu_entries(&menu),
12670 &["first", "last", "second"],
12671 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
12672 );
12673 } else {
12674 panic!("expected completion menu to be open");
12675 }
12676 });
12677}
12678
12679#[gpui::test]
12680async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
12681 init_test(cx, |language_settings| {
12682 language_settings.defaults.completions = Some(CompletionSettings {
12683 words: WordsCompletionMode::Disabled,
12684 lsp: true,
12685 lsp_fetch_timeout_ms: 0,
12686 lsp_insert_mode: LspInsertMode::Insert,
12687 });
12688 });
12689
12690 let mut cx = EditorLspTestContext::new_rust(
12691 lsp::ServerCapabilities {
12692 completion_provider: Some(lsp::CompletionOptions {
12693 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12694 ..lsp::CompletionOptions::default()
12695 }),
12696 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12697 ..lsp::ServerCapabilities::default()
12698 },
12699 cx,
12700 )
12701 .await;
12702
12703 let _completion_requests_handler =
12704 cx.lsp
12705 .server
12706 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12707 panic!("LSP completions should not be queried when dealing with word completions")
12708 });
12709
12710 cx.set_state(indoc! {"ˇ
12711 first
12712 last
12713 second
12714 "});
12715 cx.update_editor(|editor, window, cx| {
12716 editor.show_word_completions(&ShowWordCompletions, window, cx);
12717 });
12718 cx.executor().run_until_parked();
12719 cx.condition(|editor, _| editor.context_menu_visible())
12720 .await;
12721 cx.update_editor(|editor, _, _| {
12722 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12723 {
12724 assert_eq!(
12725 completion_menu_entries(&menu),
12726 &["first", "last", "second"],
12727 "`ShowWordCompletions` action should show word completions"
12728 );
12729 } else {
12730 panic!("expected completion menu to be open");
12731 }
12732 });
12733
12734 cx.simulate_keystroke("l");
12735 cx.executor().run_until_parked();
12736 cx.condition(|editor, _| editor.context_menu_visible())
12737 .await;
12738 cx.update_editor(|editor, _, _| {
12739 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12740 {
12741 assert_eq!(
12742 completion_menu_entries(&menu),
12743 &["last"],
12744 "After showing word completions, further editing should filter them and not query the LSP"
12745 );
12746 } else {
12747 panic!("expected completion menu to be open");
12748 }
12749 });
12750}
12751
12752#[gpui::test]
12753async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
12754 init_test(cx, |language_settings| {
12755 language_settings.defaults.completions = Some(CompletionSettings {
12756 words: WordsCompletionMode::Fallback,
12757 lsp: false,
12758 lsp_fetch_timeout_ms: 0,
12759 lsp_insert_mode: LspInsertMode::Insert,
12760 });
12761 });
12762
12763 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12764
12765 cx.set_state(indoc! {"ˇ
12766 0_usize
12767 let
12768 33
12769 4.5f32
12770 "});
12771 cx.update_editor(|editor, window, cx| {
12772 editor.show_completions(&ShowCompletions::default(), window, cx);
12773 });
12774 cx.executor().run_until_parked();
12775 cx.condition(|editor, _| editor.context_menu_visible())
12776 .await;
12777 cx.update_editor(|editor, window, cx| {
12778 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12779 {
12780 assert_eq!(
12781 completion_menu_entries(&menu),
12782 &["let"],
12783 "With no digits in the completion query, no digits should be in the word completions"
12784 );
12785 } else {
12786 panic!("expected completion menu to be open");
12787 }
12788 editor.cancel(&Cancel, window, cx);
12789 });
12790
12791 cx.set_state(indoc! {"3ˇ
12792 0_usize
12793 let
12794 3
12795 33.35f32
12796 "});
12797 cx.update_editor(|editor, window, cx| {
12798 editor.show_completions(&ShowCompletions::default(), window, cx);
12799 });
12800 cx.executor().run_until_parked();
12801 cx.condition(|editor, _| editor.context_menu_visible())
12802 .await;
12803 cx.update_editor(|editor, _, _| {
12804 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12805 {
12806 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
12807 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
12808 } else {
12809 panic!("expected completion menu to be open");
12810 }
12811 });
12812}
12813
12814fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
12815 let position = || lsp::Position {
12816 line: params.text_document_position.position.line,
12817 character: params.text_document_position.position.character,
12818 };
12819 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12820 range: lsp::Range {
12821 start: position(),
12822 end: position(),
12823 },
12824 new_text: text.to_string(),
12825 }))
12826}
12827
12828#[gpui::test]
12829async fn test_multiline_completion(cx: &mut TestAppContext) {
12830 init_test(cx, |_| {});
12831
12832 let fs = FakeFs::new(cx.executor());
12833 fs.insert_tree(
12834 path!("/a"),
12835 json!({
12836 "main.ts": "a",
12837 }),
12838 )
12839 .await;
12840
12841 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12842 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12843 let typescript_language = Arc::new(Language::new(
12844 LanguageConfig {
12845 name: "TypeScript".into(),
12846 matcher: LanguageMatcher {
12847 path_suffixes: vec!["ts".to_string()],
12848 ..LanguageMatcher::default()
12849 },
12850 line_comments: vec!["// ".into()],
12851 ..LanguageConfig::default()
12852 },
12853 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12854 ));
12855 language_registry.add(typescript_language.clone());
12856 let mut fake_servers = language_registry.register_fake_lsp(
12857 "TypeScript",
12858 FakeLspAdapter {
12859 capabilities: lsp::ServerCapabilities {
12860 completion_provider: Some(lsp::CompletionOptions {
12861 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12862 ..lsp::CompletionOptions::default()
12863 }),
12864 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12865 ..lsp::ServerCapabilities::default()
12866 },
12867 // Emulate vtsls label generation
12868 label_for_completion: Some(Box::new(|item, _| {
12869 let text = if let Some(description) = item
12870 .label_details
12871 .as_ref()
12872 .and_then(|label_details| label_details.description.as_ref())
12873 {
12874 format!("{} {}", item.label, description)
12875 } else if let Some(detail) = &item.detail {
12876 format!("{} {}", item.label, detail)
12877 } else {
12878 item.label.clone()
12879 };
12880 let len = text.len();
12881 Some(language::CodeLabel {
12882 text,
12883 runs: Vec::new(),
12884 filter_range: 0..len,
12885 })
12886 })),
12887 ..FakeLspAdapter::default()
12888 },
12889 );
12890 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12891 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12892 let worktree_id = workspace
12893 .update(cx, |workspace, _window, cx| {
12894 workspace.project().update(cx, |project, cx| {
12895 project.worktrees(cx).next().unwrap().read(cx).id()
12896 })
12897 })
12898 .unwrap();
12899 let _buffer = project
12900 .update(cx, |project, cx| {
12901 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
12902 })
12903 .await
12904 .unwrap();
12905 let editor = workspace
12906 .update(cx, |workspace, window, cx| {
12907 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
12908 })
12909 .unwrap()
12910 .await
12911 .unwrap()
12912 .downcast::<Editor>()
12913 .unwrap();
12914 let fake_server = fake_servers.next().await.unwrap();
12915
12916 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
12917 let multiline_label_2 = "a\nb\nc\n";
12918 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
12919 let multiline_description = "d\ne\nf\n";
12920 let multiline_detail_2 = "g\nh\ni\n";
12921
12922 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
12923 move |params, _| async move {
12924 Ok(Some(lsp::CompletionResponse::Array(vec![
12925 lsp::CompletionItem {
12926 label: multiline_label.to_string(),
12927 text_edit: gen_text_edit(¶ms, "new_text_1"),
12928 ..lsp::CompletionItem::default()
12929 },
12930 lsp::CompletionItem {
12931 label: "single line label 1".to_string(),
12932 detail: Some(multiline_detail.to_string()),
12933 text_edit: gen_text_edit(¶ms, "new_text_2"),
12934 ..lsp::CompletionItem::default()
12935 },
12936 lsp::CompletionItem {
12937 label: "single line label 2".to_string(),
12938 label_details: Some(lsp::CompletionItemLabelDetails {
12939 description: Some(multiline_description.to_string()),
12940 detail: None,
12941 }),
12942 text_edit: gen_text_edit(¶ms, "new_text_2"),
12943 ..lsp::CompletionItem::default()
12944 },
12945 lsp::CompletionItem {
12946 label: multiline_label_2.to_string(),
12947 detail: Some(multiline_detail_2.to_string()),
12948 text_edit: gen_text_edit(¶ms, "new_text_3"),
12949 ..lsp::CompletionItem::default()
12950 },
12951 lsp::CompletionItem {
12952 label: "Label with many spaces and \t but without newlines".to_string(),
12953 detail: Some(
12954 "Details with many spaces and \t but without newlines".to_string(),
12955 ),
12956 text_edit: gen_text_edit(¶ms, "new_text_4"),
12957 ..lsp::CompletionItem::default()
12958 },
12959 ])))
12960 },
12961 );
12962
12963 editor.update_in(cx, |editor, window, cx| {
12964 cx.focus_self(window);
12965 editor.move_to_end(&MoveToEnd, window, cx);
12966 editor.handle_input(".", window, cx);
12967 });
12968 cx.run_until_parked();
12969 completion_handle.next().await.unwrap();
12970
12971 editor.update(cx, |editor, _| {
12972 assert!(editor.context_menu_visible());
12973 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12974 {
12975 let completion_labels = menu
12976 .completions
12977 .borrow()
12978 .iter()
12979 .map(|c| c.label.text.clone())
12980 .collect::<Vec<_>>();
12981 assert_eq!(
12982 completion_labels,
12983 &[
12984 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
12985 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
12986 "single line label 2 d e f ",
12987 "a b c g h i ",
12988 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
12989 ],
12990 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
12991 );
12992
12993 for completion in menu
12994 .completions
12995 .borrow()
12996 .iter() {
12997 assert_eq!(
12998 completion.label.filter_range,
12999 0..completion.label.text.len(),
13000 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
13001 );
13002 }
13003 } else {
13004 panic!("expected completion menu to be open");
13005 }
13006 });
13007}
13008
13009#[gpui::test]
13010async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
13011 init_test(cx, |_| {});
13012 let mut cx = EditorLspTestContext::new_rust(
13013 lsp::ServerCapabilities {
13014 completion_provider: Some(lsp::CompletionOptions {
13015 trigger_characters: Some(vec![".".to_string()]),
13016 ..Default::default()
13017 }),
13018 ..Default::default()
13019 },
13020 cx,
13021 )
13022 .await;
13023 cx.lsp
13024 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13025 Ok(Some(lsp::CompletionResponse::Array(vec![
13026 lsp::CompletionItem {
13027 label: "first".into(),
13028 ..Default::default()
13029 },
13030 lsp::CompletionItem {
13031 label: "last".into(),
13032 ..Default::default()
13033 },
13034 ])))
13035 });
13036 cx.set_state("variableˇ");
13037 cx.simulate_keystroke(".");
13038 cx.executor().run_until_parked();
13039
13040 cx.update_editor(|editor, _, _| {
13041 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13042 {
13043 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
13044 } else {
13045 panic!("expected completion menu to be open");
13046 }
13047 });
13048
13049 cx.update_editor(|editor, window, cx| {
13050 editor.move_page_down(&MovePageDown::default(), window, cx);
13051 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13052 {
13053 assert!(
13054 menu.selected_item == 1,
13055 "expected PageDown to select the last item from the context menu"
13056 );
13057 } else {
13058 panic!("expected completion menu to stay open after PageDown");
13059 }
13060 });
13061
13062 cx.update_editor(|editor, window, cx| {
13063 editor.move_page_up(&MovePageUp::default(), window, cx);
13064 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13065 {
13066 assert!(
13067 menu.selected_item == 0,
13068 "expected PageUp to select the first item from the context menu"
13069 );
13070 } else {
13071 panic!("expected completion menu to stay open after PageUp");
13072 }
13073 });
13074}
13075
13076#[gpui::test]
13077async fn test_as_is_completions(cx: &mut TestAppContext) {
13078 init_test(cx, |_| {});
13079 let mut cx = EditorLspTestContext::new_rust(
13080 lsp::ServerCapabilities {
13081 completion_provider: Some(lsp::CompletionOptions {
13082 ..Default::default()
13083 }),
13084 ..Default::default()
13085 },
13086 cx,
13087 )
13088 .await;
13089 cx.lsp
13090 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13091 Ok(Some(lsp::CompletionResponse::Array(vec![
13092 lsp::CompletionItem {
13093 label: "unsafe".into(),
13094 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13095 range: lsp::Range {
13096 start: lsp::Position {
13097 line: 1,
13098 character: 2,
13099 },
13100 end: lsp::Position {
13101 line: 1,
13102 character: 3,
13103 },
13104 },
13105 new_text: "unsafe".to_string(),
13106 })),
13107 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
13108 ..Default::default()
13109 },
13110 ])))
13111 });
13112 cx.set_state("fn a() {}\n nˇ");
13113 cx.executor().run_until_parked();
13114 cx.update_editor(|editor, window, cx| {
13115 editor.show_completions(
13116 &ShowCompletions {
13117 trigger: Some("\n".into()),
13118 },
13119 window,
13120 cx,
13121 );
13122 });
13123 cx.executor().run_until_parked();
13124
13125 cx.update_editor(|editor, window, cx| {
13126 editor.confirm_completion(&Default::default(), window, cx)
13127 });
13128 cx.executor().run_until_parked();
13129 cx.assert_editor_state("fn a() {}\n unsafeˇ");
13130}
13131
13132#[gpui::test]
13133async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
13134 init_test(cx, |_| {});
13135
13136 let mut cx = EditorLspTestContext::new_rust(
13137 lsp::ServerCapabilities {
13138 completion_provider: Some(lsp::CompletionOptions {
13139 trigger_characters: Some(vec![".".to_string()]),
13140 resolve_provider: Some(true),
13141 ..Default::default()
13142 }),
13143 ..Default::default()
13144 },
13145 cx,
13146 )
13147 .await;
13148
13149 cx.set_state("fn main() { let a = 2ˇ; }");
13150 cx.simulate_keystroke(".");
13151 let completion_item = lsp::CompletionItem {
13152 label: "Some".into(),
13153 kind: Some(lsp::CompletionItemKind::SNIPPET),
13154 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13155 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13156 kind: lsp::MarkupKind::Markdown,
13157 value: "```rust\nSome(2)\n```".to_string(),
13158 })),
13159 deprecated: Some(false),
13160 sort_text: Some("Some".to_string()),
13161 filter_text: Some("Some".to_string()),
13162 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13163 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13164 range: lsp::Range {
13165 start: lsp::Position {
13166 line: 0,
13167 character: 22,
13168 },
13169 end: lsp::Position {
13170 line: 0,
13171 character: 22,
13172 },
13173 },
13174 new_text: "Some(2)".to_string(),
13175 })),
13176 additional_text_edits: Some(vec![lsp::TextEdit {
13177 range: lsp::Range {
13178 start: lsp::Position {
13179 line: 0,
13180 character: 20,
13181 },
13182 end: lsp::Position {
13183 line: 0,
13184 character: 22,
13185 },
13186 },
13187 new_text: "".to_string(),
13188 }]),
13189 ..Default::default()
13190 };
13191
13192 let closure_completion_item = completion_item.clone();
13193 let counter = Arc::new(AtomicUsize::new(0));
13194 let counter_clone = counter.clone();
13195 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13196 let task_completion_item = closure_completion_item.clone();
13197 counter_clone.fetch_add(1, atomic::Ordering::Release);
13198 async move {
13199 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13200 is_incomplete: true,
13201 item_defaults: None,
13202 items: vec![task_completion_item],
13203 })))
13204 }
13205 });
13206
13207 cx.condition(|editor, _| editor.context_menu_visible())
13208 .await;
13209 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
13210 assert!(request.next().await.is_some());
13211 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13212
13213 cx.simulate_keystrokes("S o m");
13214 cx.condition(|editor, _| editor.context_menu_visible())
13215 .await;
13216 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
13217 assert!(request.next().await.is_some());
13218 assert!(request.next().await.is_some());
13219 assert!(request.next().await.is_some());
13220 request.close();
13221 assert!(request.next().await.is_none());
13222 assert_eq!(
13223 counter.load(atomic::Ordering::Acquire),
13224 4,
13225 "With the completions menu open, only one LSP request should happen per input"
13226 );
13227}
13228
13229#[gpui::test]
13230async fn test_toggle_comment(cx: &mut TestAppContext) {
13231 init_test(cx, |_| {});
13232 let mut cx = EditorTestContext::new(cx).await;
13233 let language = Arc::new(Language::new(
13234 LanguageConfig {
13235 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13236 ..Default::default()
13237 },
13238 Some(tree_sitter_rust::LANGUAGE.into()),
13239 ));
13240 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13241
13242 // If multiple selections intersect a line, the line is only toggled once.
13243 cx.set_state(indoc! {"
13244 fn a() {
13245 «//b();
13246 ˇ»// «c();
13247 //ˇ» d();
13248 }
13249 "});
13250
13251 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13252
13253 cx.assert_editor_state(indoc! {"
13254 fn a() {
13255 «b();
13256 c();
13257 ˇ» d();
13258 }
13259 "});
13260
13261 // The comment prefix is inserted at the same column for every line in a
13262 // selection.
13263 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13264
13265 cx.assert_editor_state(indoc! {"
13266 fn a() {
13267 // «b();
13268 // c();
13269 ˇ»// d();
13270 }
13271 "});
13272
13273 // If a selection ends at the beginning of a line, that line is not toggled.
13274 cx.set_selections_state(indoc! {"
13275 fn a() {
13276 // b();
13277 «// c();
13278 ˇ» // d();
13279 }
13280 "});
13281
13282 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13283
13284 cx.assert_editor_state(indoc! {"
13285 fn a() {
13286 // b();
13287 «c();
13288 ˇ» // d();
13289 }
13290 "});
13291
13292 // If a selection span a single line and is empty, the line is toggled.
13293 cx.set_state(indoc! {"
13294 fn a() {
13295 a();
13296 b();
13297 ˇ
13298 }
13299 "});
13300
13301 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13302
13303 cx.assert_editor_state(indoc! {"
13304 fn a() {
13305 a();
13306 b();
13307 //•ˇ
13308 }
13309 "});
13310
13311 // If a selection span multiple lines, empty lines are not toggled.
13312 cx.set_state(indoc! {"
13313 fn a() {
13314 «a();
13315
13316 c();ˇ»
13317 }
13318 "});
13319
13320 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13321
13322 cx.assert_editor_state(indoc! {"
13323 fn a() {
13324 // «a();
13325
13326 // c();ˇ»
13327 }
13328 "});
13329
13330 // If a selection includes multiple comment prefixes, all lines are uncommented.
13331 cx.set_state(indoc! {"
13332 fn a() {
13333 «// a();
13334 /// b();
13335 //! c();ˇ»
13336 }
13337 "});
13338
13339 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13340
13341 cx.assert_editor_state(indoc! {"
13342 fn a() {
13343 «a();
13344 b();
13345 c();ˇ»
13346 }
13347 "});
13348}
13349
13350#[gpui::test]
13351async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
13352 init_test(cx, |_| {});
13353 let mut cx = EditorTestContext::new(cx).await;
13354 let language = Arc::new(Language::new(
13355 LanguageConfig {
13356 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13357 ..Default::default()
13358 },
13359 Some(tree_sitter_rust::LANGUAGE.into()),
13360 ));
13361 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13362
13363 let toggle_comments = &ToggleComments {
13364 advance_downwards: false,
13365 ignore_indent: true,
13366 };
13367
13368 // If multiple selections intersect a line, the line is only toggled once.
13369 cx.set_state(indoc! {"
13370 fn a() {
13371 // «b();
13372 // c();
13373 // ˇ» d();
13374 }
13375 "});
13376
13377 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13378
13379 cx.assert_editor_state(indoc! {"
13380 fn a() {
13381 «b();
13382 c();
13383 ˇ» d();
13384 }
13385 "});
13386
13387 // The comment prefix is inserted at the beginning of each line
13388 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13389
13390 cx.assert_editor_state(indoc! {"
13391 fn a() {
13392 // «b();
13393 // c();
13394 // ˇ» d();
13395 }
13396 "});
13397
13398 // If a selection ends at the beginning of a line, that line is not toggled.
13399 cx.set_selections_state(indoc! {"
13400 fn a() {
13401 // b();
13402 // «c();
13403 ˇ»// d();
13404 }
13405 "});
13406
13407 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13408
13409 cx.assert_editor_state(indoc! {"
13410 fn a() {
13411 // b();
13412 «c();
13413 ˇ»// d();
13414 }
13415 "});
13416
13417 // If a selection span a single line and is empty, the line is toggled.
13418 cx.set_state(indoc! {"
13419 fn a() {
13420 a();
13421 b();
13422 ˇ
13423 }
13424 "});
13425
13426 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13427
13428 cx.assert_editor_state(indoc! {"
13429 fn a() {
13430 a();
13431 b();
13432 //ˇ
13433 }
13434 "});
13435
13436 // If a selection span multiple lines, empty lines are not toggled.
13437 cx.set_state(indoc! {"
13438 fn a() {
13439 «a();
13440
13441 c();ˇ»
13442 }
13443 "});
13444
13445 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13446
13447 cx.assert_editor_state(indoc! {"
13448 fn a() {
13449 // «a();
13450
13451 // c();ˇ»
13452 }
13453 "});
13454
13455 // If a selection includes multiple comment prefixes, all lines are uncommented.
13456 cx.set_state(indoc! {"
13457 fn a() {
13458 // «a();
13459 /// b();
13460 //! c();ˇ»
13461 }
13462 "});
13463
13464 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13465
13466 cx.assert_editor_state(indoc! {"
13467 fn a() {
13468 «a();
13469 b();
13470 c();ˇ»
13471 }
13472 "});
13473}
13474
13475#[gpui::test]
13476async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
13477 init_test(cx, |_| {});
13478
13479 let language = Arc::new(Language::new(
13480 LanguageConfig {
13481 line_comments: vec!["// ".into()],
13482 ..Default::default()
13483 },
13484 Some(tree_sitter_rust::LANGUAGE.into()),
13485 ));
13486
13487 let mut cx = EditorTestContext::new(cx).await;
13488
13489 cx.language_registry().add(language.clone());
13490 cx.update_buffer(|buffer, cx| {
13491 buffer.set_language(Some(language), cx);
13492 });
13493
13494 let toggle_comments = &ToggleComments {
13495 advance_downwards: true,
13496 ignore_indent: false,
13497 };
13498
13499 // Single cursor on one line -> advance
13500 // Cursor moves horizontally 3 characters as well on non-blank line
13501 cx.set_state(indoc!(
13502 "fn a() {
13503 ˇdog();
13504 cat();
13505 }"
13506 ));
13507 cx.update_editor(|editor, window, cx| {
13508 editor.toggle_comments(toggle_comments, window, cx);
13509 });
13510 cx.assert_editor_state(indoc!(
13511 "fn a() {
13512 // dog();
13513 catˇ();
13514 }"
13515 ));
13516
13517 // Single selection on one line -> don't advance
13518 cx.set_state(indoc!(
13519 "fn a() {
13520 «dog()ˇ»;
13521 cat();
13522 }"
13523 ));
13524 cx.update_editor(|editor, window, cx| {
13525 editor.toggle_comments(toggle_comments, window, cx);
13526 });
13527 cx.assert_editor_state(indoc!(
13528 "fn a() {
13529 // «dog()ˇ»;
13530 cat();
13531 }"
13532 ));
13533
13534 // Multiple cursors on one line -> advance
13535 cx.set_state(indoc!(
13536 "fn a() {
13537 ˇdˇog();
13538 cat();
13539 }"
13540 ));
13541 cx.update_editor(|editor, window, cx| {
13542 editor.toggle_comments(toggle_comments, window, cx);
13543 });
13544 cx.assert_editor_state(indoc!(
13545 "fn a() {
13546 // dog();
13547 catˇ(ˇ);
13548 }"
13549 ));
13550
13551 // Multiple cursors on one line, with selection -> don't advance
13552 cx.set_state(indoc!(
13553 "fn a() {
13554 ˇdˇog«()ˇ»;
13555 cat();
13556 }"
13557 ));
13558 cx.update_editor(|editor, window, cx| {
13559 editor.toggle_comments(toggle_comments, window, cx);
13560 });
13561 cx.assert_editor_state(indoc!(
13562 "fn a() {
13563 // ˇdˇog«()ˇ»;
13564 cat();
13565 }"
13566 ));
13567
13568 // Single cursor on one line -> advance
13569 // Cursor moves to column 0 on blank line
13570 cx.set_state(indoc!(
13571 "fn a() {
13572 ˇdog();
13573
13574 cat();
13575 }"
13576 ));
13577 cx.update_editor(|editor, window, cx| {
13578 editor.toggle_comments(toggle_comments, window, cx);
13579 });
13580 cx.assert_editor_state(indoc!(
13581 "fn a() {
13582 // dog();
13583 ˇ
13584 cat();
13585 }"
13586 ));
13587
13588 // Single cursor on one line -> advance
13589 // Cursor starts and ends at column 0
13590 cx.set_state(indoc!(
13591 "fn a() {
13592 ˇ dog();
13593 cat();
13594 }"
13595 ));
13596 cx.update_editor(|editor, window, cx| {
13597 editor.toggle_comments(toggle_comments, window, cx);
13598 });
13599 cx.assert_editor_state(indoc!(
13600 "fn a() {
13601 // dog();
13602 ˇ cat();
13603 }"
13604 ));
13605}
13606
13607#[gpui::test]
13608async fn test_toggle_block_comment(cx: &mut TestAppContext) {
13609 init_test(cx, |_| {});
13610
13611 let mut cx = EditorTestContext::new(cx).await;
13612
13613 let html_language = Arc::new(
13614 Language::new(
13615 LanguageConfig {
13616 name: "HTML".into(),
13617 block_comment: Some(("<!-- ".into(), " -->".into())),
13618 ..Default::default()
13619 },
13620 Some(tree_sitter_html::LANGUAGE.into()),
13621 )
13622 .with_injection_query(
13623 r#"
13624 (script_element
13625 (raw_text) @injection.content
13626 (#set! injection.language "javascript"))
13627 "#,
13628 )
13629 .unwrap(),
13630 );
13631
13632 let javascript_language = Arc::new(Language::new(
13633 LanguageConfig {
13634 name: "JavaScript".into(),
13635 line_comments: vec!["// ".into()],
13636 ..Default::default()
13637 },
13638 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13639 ));
13640
13641 cx.language_registry().add(html_language.clone());
13642 cx.language_registry().add(javascript_language.clone());
13643 cx.update_buffer(|buffer, cx| {
13644 buffer.set_language(Some(html_language), cx);
13645 });
13646
13647 // Toggle comments for empty selections
13648 cx.set_state(
13649 &r#"
13650 <p>A</p>ˇ
13651 <p>B</p>ˇ
13652 <p>C</p>ˇ
13653 "#
13654 .unindent(),
13655 );
13656 cx.update_editor(|editor, window, cx| {
13657 editor.toggle_comments(&ToggleComments::default(), window, cx)
13658 });
13659 cx.assert_editor_state(
13660 &r#"
13661 <!-- <p>A</p>ˇ -->
13662 <!-- <p>B</p>ˇ -->
13663 <!-- <p>C</p>ˇ -->
13664 "#
13665 .unindent(),
13666 );
13667 cx.update_editor(|editor, window, cx| {
13668 editor.toggle_comments(&ToggleComments::default(), window, cx)
13669 });
13670 cx.assert_editor_state(
13671 &r#"
13672 <p>A</p>ˇ
13673 <p>B</p>ˇ
13674 <p>C</p>ˇ
13675 "#
13676 .unindent(),
13677 );
13678
13679 // Toggle comments for mixture of empty and non-empty selections, where
13680 // multiple selections occupy a given line.
13681 cx.set_state(
13682 &r#"
13683 <p>A«</p>
13684 <p>ˇ»B</p>ˇ
13685 <p>C«</p>
13686 <p>ˇ»D</p>ˇ
13687 "#
13688 .unindent(),
13689 );
13690
13691 cx.update_editor(|editor, window, cx| {
13692 editor.toggle_comments(&ToggleComments::default(), window, cx)
13693 });
13694 cx.assert_editor_state(
13695 &r#"
13696 <!-- <p>A«</p>
13697 <p>ˇ»B</p>ˇ -->
13698 <!-- <p>C«</p>
13699 <p>ˇ»D</p>ˇ -->
13700 "#
13701 .unindent(),
13702 );
13703 cx.update_editor(|editor, window, cx| {
13704 editor.toggle_comments(&ToggleComments::default(), window, cx)
13705 });
13706 cx.assert_editor_state(
13707 &r#"
13708 <p>A«</p>
13709 <p>ˇ»B</p>ˇ
13710 <p>C«</p>
13711 <p>ˇ»D</p>ˇ
13712 "#
13713 .unindent(),
13714 );
13715
13716 // Toggle comments when different languages are active for different
13717 // selections.
13718 cx.set_state(
13719 &r#"
13720 ˇ<script>
13721 ˇvar x = new Y();
13722 ˇ</script>
13723 "#
13724 .unindent(),
13725 );
13726 cx.executor().run_until_parked();
13727 cx.update_editor(|editor, window, cx| {
13728 editor.toggle_comments(&ToggleComments::default(), window, cx)
13729 });
13730 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
13731 // Uncommenting and commenting from this position brings in even more wrong artifacts.
13732 cx.assert_editor_state(
13733 &r#"
13734 <!-- ˇ<script> -->
13735 // ˇvar x = new Y();
13736 <!-- ˇ</script> -->
13737 "#
13738 .unindent(),
13739 );
13740}
13741
13742#[gpui::test]
13743fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
13744 init_test(cx, |_| {});
13745
13746 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13747 let multibuffer = cx.new(|cx| {
13748 let mut multibuffer = MultiBuffer::new(ReadWrite);
13749 multibuffer.push_excerpts(
13750 buffer.clone(),
13751 [
13752 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
13753 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
13754 ],
13755 cx,
13756 );
13757 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
13758 multibuffer
13759 });
13760
13761 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13762 editor.update_in(cx, |editor, window, cx| {
13763 assert_eq!(editor.text(cx), "aaaa\nbbbb");
13764 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13765 s.select_ranges([
13766 Point::new(0, 0)..Point::new(0, 0),
13767 Point::new(1, 0)..Point::new(1, 0),
13768 ])
13769 });
13770
13771 editor.handle_input("X", window, cx);
13772 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
13773 assert_eq!(
13774 editor.selections.ranges(cx),
13775 [
13776 Point::new(0, 1)..Point::new(0, 1),
13777 Point::new(1, 1)..Point::new(1, 1),
13778 ]
13779 );
13780
13781 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
13782 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13783 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
13784 });
13785 editor.backspace(&Default::default(), window, cx);
13786 assert_eq!(editor.text(cx), "Xa\nbbb");
13787 assert_eq!(
13788 editor.selections.ranges(cx),
13789 [Point::new(1, 0)..Point::new(1, 0)]
13790 );
13791
13792 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13793 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
13794 });
13795 editor.backspace(&Default::default(), window, cx);
13796 assert_eq!(editor.text(cx), "X\nbb");
13797 assert_eq!(
13798 editor.selections.ranges(cx),
13799 [Point::new(0, 1)..Point::new(0, 1)]
13800 );
13801 });
13802}
13803
13804#[gpui::test]
13805fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
13806 init_test(cx, |_| {});
13807
13808 let markers = vec![('[', ']').into(), ('(', ')').into()];
13809 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
13810 indoc! {"
13811 [aaaa
13812 (bbbb]
13813 cccc)",
13814 },
13815 markers.clone(),
13816 );
13817 let excerpt_ranges = markers.into_iter().map(|marker| {
13818 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
13819 ExcerptRange::new(context.clone())
13820 });
13821 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
13822 let multibuffer = cx.new(|cx| {
13823 let mut multibuffer = MultiBuffer::new(ReadWrite);
13824 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
13825 multibuffer
13826 });
13827
13828 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13829 editor.update_in(cx, |editor, window, cx| {
13830 let (expected_text, selection_ranges) = marked_text_ranges(
13831 indoc! {"
13832 aaaa
13833 bˇbbb
13834 bˇbbˇb
13835 cccc"
13836 },
13837 true,
13838 );
13839 assert_eq!(editor.text(cx), expected_text);
13840 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13841 s.select_ranges(selection_ranges)
13842 });
13843
13844 editor.handle_input("X", window, cx);
13845
13846 let (expected_text, expected_selections) = marked_text_ranges(
13847 indoc! {"
13848 aaaa
13849 bXˇbbXb
13850 bXˇbbXˇb
13851 cccc"
13852 },
13853 false,
13854 );
13855 assert_eq!(editor.text(cx), expected_text);
13856 assert_eq!(editor.selections.ranges(cx), expected_selections);
13857
13858 editor.newline(&Newline, window, cx);
13859 let (expected_text, expected_selections) = marked_text_ranges(
13860 indoc! {"
13861 aaaa
13862 bX
13863 ˇbbX
13864 b
13865 bX
13866 ˇbbX
13867 ˇb
13868 cccc"
13869 },
13870 false,
13871 );
13872 assert_eq!(editor.text(cx), expected_text);
13873 assert_eq!(editor.selections.ranges(cx), expected_selections);
13874 });
13875}
13876
13877#[gpui::test]
13878fn test_refresh_selections(cx: &mut TestAppContext) {
13879 init_test(cx, |_| {});
13880
13881 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13882 let mut excerpt1_id = None;
13883 let multibuffer = cx.new(|cx| {
13884 let mut multibuffer = MultiBuffer::new(ReadWrite);
13885 excerpt1_id = multibuffer
13886 .push_excerpts(
13887 buffer.clone(),
13888 [
13889 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
13890 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
13891 ],
13892 cx,
13893 )
13894 .into_iter()
13895 .next();
13896 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
13897 multibuffer
13898 });
13899
13900 let editor = cx.add_window(|window, cx| {
13901 let mut editor = build_editor(multibuffer.clone(), window, cx);
13902 let snapshot = editor.snapshot(window, cx);
13903 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13904 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
13905 });
13906 editor.begin_selection(
13907 Point::new(2, 1).to_display_point(&snapshot),
13908 true,
13909 1,
13910 window,
13911 cx,
13912 );
13913 assert_eq!(
13914 editor.selections.ranges(cx),
13915 [
13916 Point::new(1, 3)..Point::new(1, 3),
13917 Point::new(2, 1)..Point::new(2, 1),
13918 ]
13919 );
13920 editor
13921 });
13922
13923 // Refreshing selections is a no-op when excerpts haven't changed.
13924 _ = editor.update(cx, |editor, window, cx| {
13925 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
13926 assert_eq!(
13927 editor.selections.ranges(cx),
13928 [
13929 Point::new(1, 3)..Point::new(1, 3),
13930 Point::new(2, 1)..Point::new(2, 1),
13931 ]
13932 );
13933 });
13934
13935 multibuffer.update(cx, |multibuffer, cx| {
13936 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
13937 });
13938 _ = editor.update(cx, |editor, window, cx| {
13939 // Removing an excerpt causes the first selection to become degenerate.
13940 assert_eq!(
13941 editor.selections.ranges(cx),
13942 [
13943 Point::new(0, 0)..Point::new(0, 0),
13944 Point::new(0, 1)..Point::new(0, 1)
13945 ]
13946 );
13947
13948 // Refreshing selections will relocate the first selection to the original buffer
13949 // location.
13950 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
13951 assert_eq!(
13952 editor.selections.ranges(cx),
13953 [
13954 Point::new(0, 1)..Point::new(0, 1),
13955 Point::new(0, 3)..Point::new(0, 3)
13956 ]
13957 );
13958 assert!(editor.selections.pending_anchor().is_some());
13959 });
13960}
13961
13962#[gpui::test]
13963fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
13964 init_test(cx, |_| {});
13965
13966 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13967 let mut excerpt1_id = None;
13968 let multibuffer = cx.new(|cx| {
13969 let mut multibuffer = MultiBuffer::new(ReadWrite);
13970 excerpt1_id = multibuffer
13971 .push_excerpts(
13972 buffer.clone(),
13973 [
13974 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
13975 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
13976 ],
13977 cx,
13978 )
13979 .into_iter()
13980 .next();
13981 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
13982 multibuffer
13983 });
13984
13985 let editor = cx.add_window(|window, cx| {
13986 let mut editor = build_editor(multibuffer.clone(), window, cx);
13987 let snapshot = editor.snapshot(window, cx);
13988 editor.begin_selection(
13989 Point::new(1, 3).to_display_point(&snapshot),
13990 false,
13991 1,
13992 window,
13993 cx,
13994 );
13995 assert_eq!(
13996 editor.selections.ranges(cx),
13997 [Point::new(1, 3)..Point::new(1, 3)]
13998 );
13999 editor
14000 });
14001
14002 multibuffer.update(cx, |multibuffer, cx| {
14003 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14004 });
14005 _ = editor.update(cx, |editor, window, cx| {
14006 assert_eq!(
14007 editor.selections.ranges(cx),
14008 [Point::new(0, 0)..Point::new(0, 0)]
14009 );
14010
14011 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
14012 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14013 assert_eq!(
14014 editor.selections.ranges(cx),
14015 [Point::new(0, 3)..Point::new(0, 3)]
14016 );
14017 assert!(editor.selections.pending_anchor().is_some());
14018 });
14019}
14020
14021#[gpui::test]
14022async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
14023 init_test(cx, |_| {});
14024
14025 let language = Arc::new(
14026 Language::new(
14027 LanguageConfig {
14028 brackets: BracketPairConfig {
14029 pairs: vec![
14030 BracketPair {
14031 start: "{".to_string(),
14032 end: "}".to_string(),
14033 close: true,
14034 surround: true,
14035 newline: true,
14036 },
14037 BracketPair {
14038 start: "/* ".to_string(),
14039 end: " */".to_string(),
14040 close: true,
14041 surround: true,
14042 newline: true,
14043 },
14044 ],
14045 ..Default::default()
14046 },
14047 ..Default::default()
14048 },
14049 Some(tree_sitter_rust::LANGUAGE.into()),
14050 )
14051 .with_indents_query("")
14052 .unwrap(),
14053 );
14054
14055 let text = concat!(
14056 "{ }\n", //
14057 " x\n", //
14058 " /* */\n", //
14059 "x\n", //
14060 "{{} }\n", //
14061 );
14062
14063 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
14064 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14065 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14066 editor
14067 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
14068 .await;
14069
14070 editor.update_in(cx, |editor, window, cx| {
14071 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14072 s.select_display_ranges([
14073 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
14074 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
14075 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
14076 ])
14077 });
14078 editor.newline(&Newline, window, cx);
14079
14080 assert_eq!(
14081 editor.buffer().read(cx).read(cx).text(),
14082 concat!(
14083 "{ \n", // Suppress rustfmt
14084 "\n", //
14085 "}\n", //
14086 " x\n", //
14087 " /* \n", //
14088 " \n", //
14089 " */\n", //
14090 "x\n", //
14091 "{{} \n", //
14092 "}\n", //
14093 )
14094 );
14095 });
14096}
14097
14098#[gpui::test]
14099fn test_highlighted_ranges(cx: &mut TestAppContext) {
14100 init_test(cx, |_| {});
14101
14102 let editor = cx.add_window(|window, cx| {
14103 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
14104 build_editor(buffer.clone(), window, cx)
14105 });
14106
14107 _ = editor.update(cx, |editor, window, cx| {
14108 struct Type1;
14109 struct Type2;
14110
14111 let buffer = editor.buffer.read(cx).snapshot(cx);
14112
14113 let anchor_range =
14114 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
14115
14116 editor.highlight_background::<Type1>(
14117 &[
14118 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
14119 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
14120 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
14121 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
14122 ],
14123 |_| Hsla::red(),
14124 cx,
14125 );
14126 editor.highlight_background::<Type2>(
14127 &[
14128 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
14129 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
14130 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
14131 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
14132 ],
14133 |_| Hsla::green(),
14134 cx,
14135 );
14136
14137 let snapshot = editor.snapshot(window, cx);
14138 let mut highlighted_ranges = editor.background_highlights_in_range(
14139 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
14140 &snapshot,
14141 cx.theme(),
14142 );
14143 // Enforce a consistent ordering based on color without relying on the ordering of the
14144 // highlight's `TypeId` which is non-executor.
14145 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
14146 assert_eq!(
14147 highlighted_ranges,
14148 &[
14149 (
14150 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
14151 Hsla::red(),
14152 ),
14153 (
14154 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14155 Hsla::red(),
14156 ),
14157 (
14158 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
14159 Hsla::green(),
14160 ),
14161 (
14162 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
14163 Hsla::green(),
14164 ),
14165 ]
14166 );
14167 assert_eq!(
14168 editor.background_highlights_in_range(
14169 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
14170 &snapshot,
14171 cx.theme(),
14172 ),
14173 &[(
14174 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14175 Hsla::red(),
14176 )]
14177 );
14178 });
14179}
14180
14181#[gpui::test]
14182async fn test_following(cx: &mut TestAppContext) {
14183 init_test(cx, |_| {});
14184
14185 let fs = FakeFs::new(cx.executor());
14186 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14187
14188 let buffer = project.update(cx, |project, cx| {
14189 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
14190 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
14191 });
14192 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
14193 let follower = cx.update(|cx| {
14194 cx.open_window(
14195 WindowOptions {
14196 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
14197 gpui::Point::new(px(0.), px(0.)),
14198 gpui::Point::new(px(10.), px(80.)),
14199 ))),
14200 ..Default::default()
14201 },
14202 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
14203 )
14204 .unwrap()
14205 });
14206
14207 let is_still_following = Rc::new(RefCell::new(true));
14208 let follower_edit_event_count = Rc::new(RefCell::new(0));
14209 let pending_update = Rc::new(RefCell::new(None));
14210 let leader_entity = leader.root(cx).unwrap();
14211 let follower_entity = follower.root(cx).unwrap();
14212 _ = follower.update(cx, {
14213 let update = pending_update.clone();
14214 let is_still_following = is_still_following.clone();
14215 let follower_edit_event_count = follower_edit_event_count.clone();
14216 |_, window, cx| {
14217 cx.subscribe_in(
14218 &leader_entity,
14219 window,
14220 move |_, leader, event, window, cx| {
14221 leader.read(cx).add_event_to_update_proto(
14222 event,
14223 &mut update.borrow_mut(),
14224 window,
14225 cx,
14226 );
14227 },
14228 )
14229 .detach();
14230
14231 cx.subscribe_in(
14232 &follower_entity,
14233 window,
14234 move |_, _, event: &EditorEvent, _window, _cx| {
14235 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
14236 *is_still_following.borrow_mut() = false;
14237 }
14238
14239 if let EditorEvent::BufferEdited = event {
14240 *follower_edit_event_count.borrow_mut() += 1;
14241 }
14242 },
14243 )
14244 .detach();
14245 }
14246 });
14247
14248 // Update the selections only
14249 _ = leader.update(cx, |leader, window, cx| {
14250 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14251 s.select_ranges([1..1])
14252 });
14253 });
14254 follower
14255 .update(cx, |follower, window, cx| {
14256 follower.apply_update_proto(
14257 &project,
14258 pending_update.borrow_mut().take().unwrap(),
14259 window,
14260 cx,
14261 )
14262 })
14263 .unwrap()
14264 .await
14265 .unwrap();
14266 _ = follower.update(cx, |follower, _, cx| {
14267 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
14268 });
14269 assert!(*is_still_following.borrow());
14270 assert_eq!(*follower_edit_event_count.borrow(), 0);
14271
14272 // Update the scroll position only
14273 _ = leader.update(cx, |leader, window, cx| {
14274 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14275 });
14276 follower
14277 .update(cx, |follower, window, cx| {
14278 follower.apply_update_proto(
14279 &project,
14280 pending_update.borrow_mut().take().unwrap(),
14281 window,
14282 cx,
14283 )
14284 })
14285 .unwrap()
14286 .await
14287 .unwrap();
14288 assert_eq!(
14289 follower
14290 .update(cx, |follower, _, cx| follower.scroll_position(cx))
14291 .unwrap(),
14292 gpui::Point::new(1.5, 3.5)
14293 );
14294 assert!(*is_still_following.borrow());
14295 assert_eq!(*follower_edit_event_count.borrow(), 0);
14296
14297 // Update the selections and scroll position. The follower's scroll position is updated
14298 // via autoscroll, not via the leader's exact scroll position.
14299 _ = leader.update(cx, |leader, window, cx| {
14300 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14301 s.select_ranges([0..0])
14302 });
14303 leader.request_autoscroll(Autoscroll::newest(), cx);
14304 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14305 });
14306 follower
14307 .update(cx, |follower, window, cx| {
14308 follower.apply_update_proto(
14309 &project,
14310 pending_update.borrow_mut().take().unwrap(),
14311 window,
14312 cx,
14313 )
14314 })
14315 .unwrap()
14316 .await
14317 .unwrap();
14318 _ = follower.update(cx, |follower, _, cx| {
14319 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
14320 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
14321 });
14322 assert!(*is_still_following.borrow());
14323
14324 // Creating a pending selection that precedes another selection
14325 _ = leader.update(cx, |leader, window, cx| {
14326 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14327 s.select_ranges([1..1])
14328 });
14329 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
14330 });
14331 follower
14332 .update(cx, |follower, window, cx| {
14333 follower.apply_update_proto(
14334 &project,
14335 pending_update.borrow_mut().take().unwrap(),
14336 window,
14337 cx,
14338 )
14339 })
14340 .unwrap()
14341 .await
14342 .unwrap();
14343 _ = follower.update(cx, |follower, _, cx| {
14344 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
14345 });
14346 assert!(*is_still_following.borrow());
14347
14348 // Extend the pending selection so that it surrounds another selection
14349 _ = leader.update(cx, |leader, window, cx| {
14350 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
14351 });
14352 follower
14353 .update(cx, |follower, window, cx| {
14354 follower.apply_update_proto(
14355 &project,
14356 pending_update.borrow_mut().take().unwrap(),
14357 window,
14358 cx,
14359 )
14360 })
14361 .unwrap()
14362 .await
14363 .unwrap();
14364 _ = follower.update(cx, |follower, _, cx| {
14365 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
14366 });
14367
14368 // Scrolling locally breaks the follow
14369 _ = follower.update(cx, |follower, window, cx| {
14370 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
14371 follower.set_scroll_anchor(
14372 ScrollAnchor {
14373 anchor: top_anchor,
14374 offset: gpui::Point::new(0.0, 0.5),
14375 },
14376 window,
14377 cx,
14378 );
14379 });
14380 assert!(!(*is_still_following.borrow()));
14381}
14382
14383#[gpui::test]
14384async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
14385 init_test(cx, |_| {});
14386
14387 let fs = FakeFs::new(cx.executor());
14388 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14389 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14390 let pane = workspace
14391 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14392 .unwrap();
14393
14394 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14395
14396 let leader = pane.update_in(cx, |_, window, cx| {
14397 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
14398 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
14399 });
14400
14401 // Start following the editor when it has no excerpts.
14402 let mut state_message =
14403 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14404 let workspace_entity = workspace.root(cx).unwrap();
14405 let follower_1 = cx
14406 .update_window(*workspace.deref(), |_, window, cx| {
14407 Editor::from_state_proto(
14408 workspace_entity,
14409 ViewId {
14410 creator: CollaboratorId::PeerId(PeerId::default()),
14411 id: 0,
14412 },
14413 &mut state_message,
14414 window,
14415 cx,
14416 )
14417 })
14418 .unwrap()
14419 .unwrap()
14420 .await
14421 .unwrap();
14422
14423 let update_message = Rc::new(RefCell::new(None));
14424 follower_1.update_in(cx, {
14425 let update = update_message.clone();
14426 |_, window, cx| {
14427 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
14428 leader.read(cx).add_event_to_update_proto(
14429 event,
14430 &mut update.borrow_mut(),
14431 window,
14432 cx,
14433 );
14434 })
14435 .detach();
14436 }
14437 });
14438
14439 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
14440 (
14441 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
14442 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
14443 )
14444 });
14445
14446 // Insert some excerpts.
14447 leader.update(cx, |leader, cx| {
14448 leader.buffer.update(cx, |multibuffer, cx| {
14449 multibuffer.set_excerpts_for_path(
14450 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
14451 buffer_1.clone(),
14452 vec![
14453 Point::row_range(0..3),
14454 Point::row_range(1..6),
14455 Point::row_range(12..15),
14456 ],
14457 0,
14458 cx,
14459 );
14460 multibuffer.set_excerpts_for_path(
14461 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
14462 buffer_2.clone(),
14463 vec![Point::row_range(0..6), Point::row_range(8..12)],
14464 0,
14465 cx,
14466 );
14467 });
14468 });
14469
14470 // Apply the update of adding the excerpts.
14471 follower_1
14472 .update_in(cx, |follower, window, cx| {
14473 follower.apply_update_proto(
14474 &project,
14475 update_message.borrow().clone().unwrap(),
14476 window,
14477 cx,
14478 )
14479 })
14480 .await
14481 .unwrap();
14482 assert_eq!(
14483 follower_1.update(cx, |editor, cx| editor.text(cx)),
14484 leader.update(cx, |editor, cx| editor.text(cx))
14485 );
14486 update_message.borrow_mut().take();
14487
14488 // Start following separately after it already has excerpts.
14489 let mut state_message =
14490 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14491 let workspace_entity = workspace.root(cx).unwrap();
14492 let follower_2 = cx
14493 .update_window(*workspace.deref(), |_, window, cx| {
14494 Editor::from_state_proto(
14495 workspace_entity,
14496 ViewId {
14497 creator: CollaboratorId::PeerId(PeerId::default()),
14498 id: 0,
14499 },
14500 &mut state_message,
14501 window,
14502 cx,
14503 )
14504 })
14505 .unwrap()
14506 .unwrap()
14507 .await
14508 .unwrap();
14509 assert_eq!(
14510 follower_2.update(cx, |editor, cx| editor.text(cx)),
14511 leader.update(cx, |editor, cx| editor.text(cx))
14512 );
14513
14514 // Remove some excerpts.
14515 leader.update(cx, |leader, cx| {
14516 leader.buffer.update(cx, |multibuffer, cx| {
14517 let excerpt_ids = multibuffer.excerpt_ids();
14518 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
14519 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
14520 });
14521 });
14522
14523 // Apply the update of removing the excerpts.
14524 follower_1
14525 .update_in(cx, |follower, window, cx| {
14526 follower.apply_update_proto(
14527 &project,
14528 update_message.borrow().clone().unwrap(),
14529 window,
14530 cx,
14531 )
14532 })
14533 .await
14534 .unwrap();
14535 follower_2
14536 .update_in(cx, |follower, window, cx| {
14537 follower.apply_update_proto(
14538 &project,
14539 update_message.borrow().clone().unwrap(),
14540 window,
14541 cx,
14542 )
14543 })
14544 .await
14545 .unwrap();
14546 update_message.borrow_mut().take();
14547 assert_eq!(
14548 follower_1.update(cx, |editor, cx| editor.text(cx)),
14549 leader.update(cx, |editor, cx| editor.text(cx))
14550 );
14551}
14552
14553#[gpui::test]
14554async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14555 init_test(cx, |_| {});
14556
14557 let mut cx = EditorTestContext::new(cx).await;
14558 let lsp_store =
14559 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
14560
14561 cx.set_state(indoc! {"
14562 ˇfn func(abc def: i32) -> u32 {
14563 }
14564 "});
14565
14566 cx.update(|_, cx| {
14567 lsp_store.update(cx, |lsp_store, cx| {
14568 lsp_store
14569 .update_diagnostics(
14570 LanguageServerId(0),
14571 lsp::PublishDiagnosticsParams {
14572 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
14573 version: None,
14574 diagnostics: vec![
14575 lsp::Diagnostic {
14576 range: lsp::Range::new(
14577 lsp::Position::new(0, 11),
14578 lsp::Position::new(0, 12),
14579 ),
14580 severity: Some(lsp::DiagnosticSeverity::ERROR),
14581 ..Default::default()
14582 },
14583 lsp::Diagnostic {
14584 range: lsp::Range::new(
14585 lsp::Position::new(0, 12),
14586 lsp::Position::new(0, 15),
14587 ),
14588 severity: Some(lsp::DiagnosticSeverity::ERROR),
14589 ..Default::default()
14590 },
14591 lsp::Diagnostic {
14592 range: lsp::Range::new(
14593 lsp::Position::new(0, 25),
14594 lsp::Position::new(0, 28),
14595 ),
14596 severity: Some(lsp::DiagnosticSeverity::ERROR),
14597 ..Default::default()
14598 },
14599 ],
14600 },
14601 None,
14602 DiagnosticSourceKind::Pushed,
14603 &[],
14604 cx,
14605 )
14606 .unwrap()
14607 });
14608 });
14609
14610 executor.run_until_parked();
14611
14612 cx.update_editor(|editor, window, cx| {
14613 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14614 });
14615
14616 cx.assert_editor_state(indoc! {"
14617 fn func(abc def: i32) -> ˇu32 {
14618 }
14619 "});
14620
14621 cx.update_editor(|editor, window, cx| {
14622 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14623 });
14624
14625 cx.assert_editor_state(indoc! {"
14626 fn func(abc ˇdef: i32) -> u32 {
14627 }
14628 "});
14629
14630 cx.update_editor(|editor, window, cx| {
14631 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14632 });
14633
14634 cx.assert_editor_state(indoc! {"
14635 fn func(abcˇ def: i32) -> u32 {
14636 }
14637 "});
14638
14639 cx.update_editor(|editor, window, cx| {
14640 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14641 });
14642
14643 cx.assert_editor_state(indoc! {"
14644 fn func(abc def: i32) -> ˇu32 {
14645 }
14646 "});
14647}
14648
14649#[gpui::test]
14650async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14651 init_test(cx, |_| {});
14652
14653 let mut cx = EditorTestContext::new(cx).await;
14654
14655 let diff_base = r#"
14656 use some::mod;
14657
14658 const A: u32 = 42;
14659
14660 fn main() {
14661 println!("hello");
14662
14663 println!("world");
14664 }
14665 "#
14666 .unindent();
14667
14668 // Edits are modified, removed, modified, added
14669 cx.set_state(
14670 &r#"
14671 use some::modified;
14672
14673 ˇ
14674 fn main() {
14675 println!("hello there");
14676
14677 println!("around the");
14678 println!("world");
14679 }
14680 "#
14681 .unindent(),
14682 );
14683
14684 cx.set_head_text(&diff_base);
14685 executor.run_until_parked();
14686
14687 cx.update_editor(|editor, window, cx| {
14688 //Wrap around the bottom of the buffer
14689 for _ in 0..3 {
14690 editor.go_to_next_hunk(&GoToHunk, window, cx);
14691 }
14692 });
14693
14694 cx.assert_editor_state(
14695 &r#"
14696 ˇuse some::modified;
14697
14698
14699 fn main() {
14700 println!("hello there");
14701
14702 println!("around the");
14703 println!("world");
14704 }
14705 "#
14706 .unindent(),
14707 );
14708
14709 cx.update_editor(|editor, window, cx| {
14710 //Wrap around the top of the buffer
14711 for _ in 0..2 {
14712 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14713 }
14714 });
14715
14716 cx.assert_editor_state(
14717 &r#"
14718 use some::modified;
14719
14720
14721 fn main() {
14722 ˇ println!("hello there");
14723
14724 println!("around the");
14725 println!("world");
14726 }
14727 "#
14728 .unindent(),
14729 );
14730
14731 cx.update_editor(|editor, window, cx| {
14732 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14733 });
14734
14735 cx.assert_editor_state(
14736 &r#"
14737 use some::modified;
14738
14739 ˇ
14740 fn main() {
14741 println!("hello there");
14742
14743 println!("around the");
14744 println!("world");
14745 }
14746 "#
14747 .unindent(),
14748 );
14749
14750 cx.update_editor(|editor, window, cx| {
14751 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14752 });
14753
14754 cx.assert_editor_state(
14755 &r#"
14756 ˇuse some::modified;
14757
14758
14759 fn main() {
14760 println!("hello there");
14761
14762 println!("around the");
14763 println!("world");
14764 }
14765 "#
14766 .unindent(),
14767 );
14768
14769 cx.update_editor(|editor, window, cx| {
14770 for _ in 0..2 {
14771 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14772 }
14773 });
14774
14775 cx.assert_editor_state(
14776 &r#"
14777 use some::modified;
14778
14779
14780 fn main() {
14781 ˇ println!("hello there");
14782
14783 println!("around the");
14784 println!("world");
14785 }
14786 "#
14787 .unindent(),
14788 );
14789
14790 cx.update_editor(|editor, window, cx| {
14791 editor.fold(&Fold, window, cx);
14792 });
14793
14794 cx.update_editor(|editor, window, cx| {
14795 editor.go_to_next_hunk(&GoToHunk, window, cx);
14796 });
14797
14798 cx.assert_editor_state(
14799 &r#"
14800 ˇuse some::modified;
14801
14802
14803 fn main() {
14804 println!("hello there");
14805
14806 println!("around the");
14807 println!("world");
14808 }
14809 "#
14810 .unindent(),
14811 );
14812}
14813
14814#[test]
14815fn test_split_words() {
14816 fn split(text: &str) -> Vec<&str> {
14817 split_words(text).collect()
14818 }
14819
14820 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
14821 assert_eq!(split("hello_world"), &["hello_", "world"]);
14822 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
14823 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
14824 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
14825 assert_eq!(split("helloworld"), &["helloworld"]);
14826
14827 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
14828}
14829
14830#[gpui::test]
14831async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
14832 init_test(cx, |_| {});
14833
14834 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
14835 let mut assert = |before, after| {
14836 let _state_context = cx.set_state(before);
14837 cx.run_until_parked();
14838 cx.update_editor(|editor, window, cx| {
14839 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
14840 });
14841 cx.run_until_parked();
14842 cx.assert_editor_state(after);
14843 };
14844
14845 // Outside bracket jumps to outside of matching bracket
14846 assert("console.logˇ(var);", "console.log(var)ˇ;");
14847 assert("console.log(var)ˇ;", "console.logˇ(var);");
14848
14849 // Inside bracket jumps to inside of matching bracket
14850 assert("console.log(ˇvar);", "console.log(varˇ);");
14851 assert("console.log(varˇ);", "console.log(ˇvar);");
14852
14853 // When outside a bracket and inside, favor jumping to the inside bracket
14854 assert(
14855 "console.log('foo', [1, 2, 3]ˇ);",
14856 "console.log(ˇ'foo', [1, 2, 3]);",
14857 );
14858 assert(
14859 "console.log(ˇ'foo', [1, 2, 3]);",
14860 "console.log('foo', [1, 2, 3]ˇ);",
14861 );
14862
14863 // Bias forward if two options are equally likely
14864 assert(
14865 "let result = curried_fun()ˇ();",
14866 "let result = curried_fun()()ˇ;",
14867 );
14868
14869 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
14870 assert(
14871 indoc! {"
14872 function test() {
14873 console.log('test')ˇ
14874 }"},
14875 indoc! {"
14876 function test() {
14877 console.logˇ('test')
14878 }"},
14879 );
14880}
14881
14882#[gpui::test]
14883async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
14884 init_test(cx, |_| {});
14885
14886 let fs = FakeFs::new(cx.executor());
14887 fs.insert_tree(
14888 path!("/a"),
14889 json!({
14890 "main.rs": "fn main() { let a = 5; }",
14891 "other.rs": "// Test file",
14892 }),
14893 )
14894 .await;
14895 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14896
14897 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14898 language_registry.add(Arc::new(Language::new(
14899 LanguageConfig {
14900 name: "Rust".into(),
14901 matcher: LanguageMatcher {
14902 path_suffixes: vec!["rs".to_string()],
14903 ..Default::default()
14904 },
14905 brackets: BracketPairConfig {
14906 pairs: vec![BracketPair {
14907 start: "{".to_string(),
14908 end: "}".to_string(),
14909 close: true,
14910 surround: true,
14911 newline: true,
14912 }],
14913 disabled_scopes_by_bracket_ix: Vec::new(),
14914 },
14915 ..Default::default()
14916 },
14917 Some(tree_sitter_rust::LANGUAGE.into()),
14918 )));
14919 let mut fake_servers = language_registry.register_fake_lsp(
14920 "Rust",
14921 FakeLspAdapter {
14922 capabilities: lsp::ServerCapabilities {
14923 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
14924 first_trigger_character: "{".to_string(),
14925 more_trigger_character: None,
14926 }),
14927 ..Default::default()
14928 },
14929 ..Default::default()
14930 },
14931 );
14932
14933 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14934
14935 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14936
14937 let worktree_id = workspace
14938 .update(cx, |workspace, _, cx| {
14939 workspace.project().update(cx, |project, cx| {
14940 project.worktrees(cx).next().unwrap().read(cx).id()
14941 })
14942 })
14943 .unwrap();
14944
14945 let buffer = project
14946 .update(cx, |project, cx| {
14947 project.open_local_buffer(path!("/a/main.rs"), cx)
14948 })
14949 .await
14950 .unwrap();
14951 let editor_handle = workspace
14952 .update(cx, |workspace, window, cx| {
14953 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
14954 })
14955 .unwrap()
14956 .await
14957 .unwrap()
14958 .downcast::<Editor>()
14959 .unwrap();
14960
14961 cx.executor().start_waiting();
14962 let fake_server = fake_servers.next().await.unwrap();
14963
14964 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
14965 |params, _| async move {
14966 assert_eq!(
14967 params.text_document_position.text_document.uri,
14968 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
14969 );
14970 assert_eq!(
14971 params.text_document_position.position,
14972 lsp::Position::new(0, 21),
14973 );
14974
14975 Ok(Some(vec![lsp::TextEdit {
14976 new_text: "]".to_string(),
14977 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14978 }]))
14979 },
14980 );
14981
14982 editor_handle.update_in(cx, |editor, window, cx| {
14983 window.focus(&editor.focus_handle(cx));
14984 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14985 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
14986 });
14987 editor.handle_input("{", window, cx);
14988 });
14989
14990 cx.executor().run_until_parked();
14991
14992 buffer.update(cx, |buffer, _| {
14993 assert_eq!(
14994 buffer.text(),
14995 "fn main() { let a = {5}; }",
14996 "No extra braces from on type formatting should appear in the buffer"
14997 )
14998 });
14999}
15000
15001#[gpui::test(iterations = 20, seeds(31))]
15002async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
15003 init_test(cx, |_| {});
15004
15005 let mut cx = EditorLspTestContext::new_rust(
15006 lsp::ServerCapabilities {
15007 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15008 first_trigger_character: ".".to_string(),
15009 more_trigger_character: None,
15010 }),
15011 ..Default::default()
15012 },
15013 cx,
15014 )
15015 .await;
15016
15017 cx.update_buffer(|buffer, _| {
15018 // This causes autoindent to be async.
15019 buffer.set_sync_parse_timeout(Duration::ZERO)
15020 });
15021
15022 cx.set_state("fn c() {\n d()ˇ\n}\n");
15023 cx.simulate_keystroke("\n");
15024 cx.run_until_parked();
15025
15026 let buffer_cloned =
15027 cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap().clone());
15028 let mut request =
15029 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
15030 let buffer_cloned = buffer_cloned.clone();
15031 async move {
15032 buffer_cloned.update(&mut cx, |buffer, _| {
15033 assert_eq!(
15034 buffer.text(),
15035 "fn c() {\n d()\n .\n}\n",
15036 "OnTypeFormatting should triggered after autoindent applied"
15037 )
15038 })?;
15039
15040 Ok(Some(vec![]))
15041 }
15042 });
15043
15044 cx.simulate_keystroke(".");
15045 cx.run_until_parked();
15046
15047 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
15048 assert!(request.next().await.is_some());
15049 request.close();
15050 assert!(request.next().await.is_none());
15051}
15052
15053#[gpui::test]
15054async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
15055 init_test(cx, |_| {});
15056
15057 let fs = FakeFs::new(cx.executor());
15058 fs.insert_tree(
15059 path!("/a"),
15060 json!({
15061 "main.rs": "fn main() { let a = 5; }",
15062 "other.rs": "// Test file",
15063 }),
15064 )
15065 .await;
15066
15067 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15068
15069 let server_restarts = Arc::new(AtomicUsize::new(0));
15070 let closure_restarts = Arc::clone(&server_restarts);
15071 let language_server_name = "test language server";
15072 let language_name: LanguageName = "Rust".into();
15073
15074 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15075 language_registry.add(Arc::new(Language::new(
15076 LanguageConfig {
15077 name: language_name.clone(),
15078 matcher: LanguageMatcher {
15079 path_suffixes: vec!["rs".to_string()],
15080 ..Default::default()
15081 },
15082 ..Default::default()
15083 },
15084 Some(tree_sitter_rust::LANGUAGE.into()),
15085 )));
15086 let mut fake_servers = language_registry.register_fake_lsp(
15087 "Rust",
15088 FakeLspAdapter {
15089 name: language_server_name,
15090 initialization_options: Some(json!({
15091 "testOptionValue": true
15092 })),
15093 initializer: Some(Box::new(move |fake_server| {
15094 let task_restarts = Arc::clone(&closure_restarts);
15095 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
15096 task_restarts.fetch_add(1, atomic::Ordering::Release);
15097 futures::future::ready(Ok(()))
15098 });
15099 })),
15100 ..Default::default()
15101 },
15102 );
15103
15104 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15105 let _buffer = project
15106 .update(cx, |project, cx| {
15107 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
15108 })
15109 .await
15110 .unwrap();
15111 let _fake_server = fake_servers.next().await.unwrap();
15112 update_test_language_settings(cx, |language_settings| {
15113 language_settings.languages.0.insert(
15114 language_name.clone(),
15115 LanguageSettingsContent {
15116 tab_size: NonZeroU32::new(8),
15117 ..Default::default()
15118 },
15119 );
15120 });
15121 cx.executor().run_until_parked();
15122 assert_eq!(
15123 server_restarts.load(atomic::Ordering::Acquire),
15124 0,
15125 "Should not restart LSP server on an unrelated change"
15126 );
15127
15128 update_test_project_settings(cx, |project_settings| {
15129 project_settings.lsp.insert(
15130 "Some other server name".into(),
15131 LspSettings {
15132 binary: None,
15133 settings: None,
15134 initialization_options: Some(json!({
15135 "some other init value": false
15136 })),
15137 enable_lsp_tasks: false,
15138 },
15139 );
15140 });
15141 cx.executor().run_until_parked();
15142 assert_eq!(
15143 server_restarts.load(atomic::Ordering::Acquire),
15144 0,
15145 "Should not restart LSP server on an unrelated LSP settings change"
15146 );
15147
15148 update_test_project_settings(cx, |project_settings| {
15149 project_settings.lsp.insert(
15150 language_server_name.into(),
15151 LspSettings {
15152 binary: None,
15153 settings: None,
15154 initialization_options: Some(json!({
15155 "anotherInitValue": false
15156 })),
15157 enable_lsp_tasks: false,
15158 },
15159 );
15160 });
15161 cx.executor().run_until_parked();
15162 assert_eq!(
15163 server_restarts.load(atomic::Ordering::Acquire),
15164 1,
15165 "Should restart LSP server on a related LSP settings change"
15166 );
15167
15168 update_test_project_settings(cx, |project_settings| {
15169 project_settings.lsp.insert(
15170 language_server_name.into(),
15171 LspSettings {
15172 binary: None,
15173 settings: None,
15174 initialization_options: Some(json!({
15175 "anotherInitValue": false
15176 })),
15177 enable_lsp_tasks: false,
15178 },
15179 );
15180 });
15181 cx.executor().run_until_parked();
15182 assert_eq!(
15183 server_restarts.load(atomic::Ordering::Acquire),
15184 1,
15185 "Should not restart LSP server on a related LSP settings change that is the same"
15186 );
15187
15188 update_test_project_settings(cx, |project_settings| {
15189 project_settings.lsp.insert(
15190 language_server_name.into(),
15191 LspSettings {
15192 binary: None,
15193 settings: None,
15194 initialization_options: None,
15195 enable_lsp_tasks: false,
15196 },
15197 );
15198 });
15199 cx.executor().run_until_parked();
15200 assert_eq!(
15201 server_restarts.load(atomic::Ordering::Acquire),
15202 2,
15203 "Should restart LSP server on another related LSP settings change"
15204 );
15205}
15206
15207#[gpui::test]
15208async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
15209 init_test(cx, |_| {});
15210
15211 let mut cx = EditorLspTestContext::new_rust(
15212 lsp::ServerCapabilities {
15213 completion_provider: Some(lsp::CompletionOptions {
15214 trigger_characters: Some(vec![".".to_string()]),
15215 resolve_provider: Some(true),
15216 ..Default::default()
15217 }),
15218 ..Default::default()
15219 },
15220 cx,
15221 )
15222 .await;
15223
15224 cx.set_state("fn main() { let a = 2ˇ; }");
15225 cx.simulate_keystroke(".");
15226 let completion_item = lsp::CompletionItem {
15227 label: "some".into(),
15228 kind: Some(lsp::CompletionItemKind::SNIPPET),
15229 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15230 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15231 kind: lsp::MarkupKind::Markdown,
15232 value: "```rust\nSome(2)\n```".to_string(),
15233 })),
15234 deprecated: Some(false),
15235 sort_text: Some("fffffff2".to_string()),
15236 filter_text: Some("some".to_string()),
15237 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15238 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15239 range: lsp::Range {
15240 start: lsp::Position {
15241 line: 0,
15242 character: 22,
15243 },
15244 end: lsp::Position {
15245 line: 0,
15246 character: 22,
15247 },
15248 },
15249 new_text: "Some(2)".to_string(),
15250 })),
15251 additional_text_edits: Some(vec![lsp::TextEdit {
15252 range: lsp::Range {
15253 start: lsp::Position {
15254 line: 0,
15255 character: 20,
15256 },
15257 end: lsp::Position {
15258 line: 0,
15259 character: 22,
15260 },
15261 },
15262 new_text: "".to_string(),
15263 }]),
15264 ..Default::default()
15265 };
15266
15267 let closure_completion_item = completion_item.clone();
15268 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15269 let task_completion_item = closure_completion_item.clone();
15270 async move {
15271 Ok(Some(lsp::CompletionResponse::Array(vec![
15272 task_completion_item,
15273 ])))
15274 }
15275 });
15276
15277 request.next().await;
15278
15279 cx.condition(|editor, _| editor.context_menu_visible())
15280 .await;
15281 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15282 editor
15283 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15284 .unwrap()
15285 });
15286 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
15287
15288 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
15289 let task_completion_item = completion_item.clone();
15290 async move { Ok(task_completion_item) }
15291 })
15292 .next()
15293 .await
15294 .unwrap();
15295 apply_additional_edits.await.unwrap();
15296 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
15297}
15298
15299#[gpui::test]
15300async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
15301 init_test(cx, |_| {});
15302
15303 let mut cx = EditorLspTestContext::new_rust(
15304 lsp::ServerCapabilities {
15305 completion_provider: Some(lsp::CompletionOptions {
15306 trigger_characters: Some(vec![".".to_string()]),
15307 resolve_provider: Some(true),
15308 ..Default::default()
15309 }),
15310 ..Default::default()
15311 },
15312 cx,
15313 )
15314 .await;
15315
15316 cx.set_state("fn main() { let a = 2ˇ; }");
15317 cx.simulate_keystroke(".");
15318
15319 let item1 = lsp::CompletionItem {
15320 label: "method id()".to_string(),
15321 filter_text: Some("id".to_string()),
15322 detail: None,
15323 documentation: None,
15324 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15325 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15326 new_text: ".id".to_string(),
15327 })),
15328 ..lsp::CompletionItem::default()
15329 };
15330
15331 let item2 = lsp::CompletionItem {
15332 label: "other".to_string(),
15333 filter_text: Some("other".to_string()),
15334 detail: None,
15335 documentation: None,
15336 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15337 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15338 new_text: ".other".to_string(),
15339 })),
15340 ..lsp::CompletionItem::default()
15341 };
15342
15343 let item1 = item1.clone();
15344 cx.set_request_handler::<lsp::request::Completion, _, _>({
15345 let item1 = item1.clone();
15346 move |_, _, _| {
15347 let item1 = item1.clone();
15348 let item2 = item2.clone();
15349 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
15350 }
15351 })
15352 .next()
15353 .await;
15354
15355 cx.condition(|editor, _| editor.context_menu_visible())
15356 .await;
15357 cx.update_editor(|editor, _, _| {
15358 let context_menu = editor.context_menu.borrow_mut();
15359 let context_menu = context_menu
15360 .as_ref()
15361 .expect("Should have the context menu deployed");
15362 match context_menu {
15363 CodeContextMenu::Completions(completions_menu) => {
15364 let completions = completions_menu.completions.borrow_mut();
15365 assert_eq!(
15366 completions
15367 .iter()
15368 .map(|completion| &completion.label.text)
15369 .collect::<Vec<_>>(),
15370 vec!["method id()", "other"]
15371 )
15372 }
15373 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15374 }
15375 });
15376
15377 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
15378 let item1 = item1.clone();
15379 move |_, item_to_resolve, _| {
15380 let item1 = item1.clone();
15381 async move {
15382 if item1 == item_to_resolve {
15383 Ok(lsp::CompletionItem {
15384 label: "method id()".to_string(),
15385 filter_text: Some("id".to_string()),
15386 detail: Some("Now resolved!".to_string()),
15387 documentation: Some(lsp::Documentation::String("Docs".to_string())),
15388 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15389 range: lsp::Range::new(
15390 lsp::Position::new(0, 22),
15391 lsp::Position::new(0, 22),
15392 ),
15393 new_text: ".id".to_string(),
15394 })),
15395 ..lsp::CompletionItem::default()
15396 })
15397 } else {
15398 Ok(item_to_resolve)
15399 }
15400 }
15401 }
15402 })
15403 .next()
15404 .await
15405 .unwrap();
15406 cx.run_until_parked();
15407
15408 cx.update_editor(|editor, window, cx| {
15409 editor.context_menu_next(&Default::default(), window, cx);
15410 });
15411
15412 cx.update_editor(|editor, _, _| {
15413 let context_menu = editor.context_menu.borrow_mut();
15414 let context_menu = context_menu
15415 .as_ref()
15416 .expect("Should have the context menu deployed");
15417 match context_menu {
15418 CodeContextMenu::Completions(completions_menu) => {
15419 let completions = completions_menu.completions.borrow_mut();
15420 assert_eq!(
15421 completions
15422 .iter()
15423 .map(|completion| &completion.label.text)
15424 .collect::<Vec<_>>(),
15425 vec!["method id() Now resolved!", "other"],
15426 "Should update first completion label, but not second as the filter text did not match."
15427 );
15428 }
15429 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15430 }
15431 });
15432}
15433
15434#[gpui::test]
15435async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
15436 init_test(cx, |_| {});
15437 let mut cx = EditorLspTestContext::new_rust(
15438 lsp::ServerCapabilities {
15439 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
15440 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
15441 completion_provider: Some(lsp::CompletionOptions {
15442 resolve_provider: Some(true),
15443 ..Default::default()
15444 }),
15445 ..Default::default()
15446 },
15447 cx,
15448 )
15449 .await;
15450 cx.set_state(indoc! {"
15451 struct TestStruct {
15452 field: i32
15453 }
15454
15455 fn mainˇ() {
15456 let unused_var = 42;
15457 let test_struct = TestStruct { field: 42 };
15458 }
15459 "});
15460 let symbol_range = cx.lsp_range(indoc! {"
15461 struct TestStruct {
15462 field: i32
15463 }
15464
15465 «fn main»() {
15466 let unused_var = 42;
15467 let test_struct = TestStruct { field: 42 };
15468 }
15469 "});
15470 let mut hover_requests =
15471 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
15472 Ok(Some(lsp::Hover {
15473 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
15474 kind: lsp::MarkupKind::Markdown,
15475 value: "Function documentation".to_string(),
15476 }),
15477 range: Some(symbol_range),
15478 }))
15479 });
15480
15481 // Case 1: Test that code action menu hide hover popover
15482 cx.dispatch_action(Hover);
15483 hover_requests.next().await;
15484 cx.condition(|editor, _| editor.hover_state.visible()).await;
15485 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
15486 move |_, _, _| async move {
15487 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
15488 lsp::CodeAction {
15489 title: "Remove unused variable".to_string(),
15490 kind: Some(CodeActionKind::QUICKFIX),
15491 edit: Some(lsp::WorkspaceEdit {
15492 changes: Some(
15493 [(
15494 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
15495 vec![lsp::TextEdit {
15496 range: lsp::Range::new(
15497 lsp::Position::new(5, 4),
15498 lsp::Position::new(5, 27),
15499 ),
15500 new_text: "".to_string(),
15501 }],
15502 )]
15503 .into_iter()
15504 .collect(),
15505 ),
15506 ..Default::default()
15507 }),
15508 ..Default::default()
15509 },
15510 )]))
15511 },
15512 );
15513 cx.update_editor(|editor, window, cx| {
15514 editor.toggle_code_actions(
15515 &ToggleCodeActions {
15516 deployed_from: None,
15517 quick_launch: false,
15518 },
15519 window,
15520 cx,
15521 );
15522 });
15523 code_action_requests.next().await;
15524 cx.run_until_parked();
15525 cx.condition(|editor, _| editor.context_menu_visible())
15526 .await;
15527 cx.update_editor(|editor, _, _| {
15528 assert!(
15529 !editor.hover_state.visible(),
15530 "Hover popover should be hidden when code action menu is shown"
15531 );
15532 // Hide code actions
15533 editor.context_menu.take();
15534 });
15535
15536 // Case 2: Test that code completions hide hover popover
15537 cx.dispatch_action(Hover);
15538 hover_requests.next().await;
15539 cx.condition(|editor, _| editor.hover_state.visible()).await;
15540 let counter = Arc::new(AtomicUsize::new(0));
15541 let mut completion_requests =
15542 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15543 let counter = counter.clone();
15544 async move {
15545 counter.fetch_add(1, atomic::Ordering::Release);
15546 Ok(Some(lsp::CompletionResponse::Array(vec![
15547 lsp::CompletionItem {
15548 label: "main".into(),
15549 kind: Some(lsp::CompletionItemKind::FUNCTION),
15550 detail: Some("() -> ()".to_string()),
15551 ..Default::default()
15552 },
15553 lsp::CompletionItem {
15554 label: "TestStruct".into(),
15555 kind: Some(lsp::CompletionItemKind::STRUCT),
15556 detail: Some("struct TestStruct".to_string()),
15557 ..Default::default()
15558 },
15559 ])))
15560 }
15561 });
15562 cx.update_editor(|editor, window, cx| {
15563 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15564 });
15565 completion_requests.next().await;
15566 cx.condition(|editor, _| editor.context_menu_visible())
15567 .await;
15568 cx.update_editor(|editor, _, _| {
15569 assert!(
15570 !editor.hover_state.visible(),
15571 "Hover popover should be hidden when completion menu is shown"
15572 );
15573 });
15574}
15575
15576#[gpui::test]
15577async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
15578 init_test(cx, |_| {});
15579
15580 let mut cx = EditorLspTestContext::new_rust(
15581 lsp::ServerCapabilities {
15582 completion_provider: Some(lsp::CompletionOptions {
15583 trigger_characters: Some(vec![".".to_string()]),
15584 resolve_provider: Some(true),
15585 ..Default::default()
15586 }),
15587 ..Default::default()
15588 },
15589 cx,
15590 )
15591 .await;
15592
15593 cx.set_state("fn main() { let a = 2ˇ; }");
15594 cx.simulate_keystroke(".");
15595
15596 let unresolved_item_1 = lsp::CompletionItem {
15597 label: "id".to_string(),
15598 filter_text: Some("id".to_string()),
15599 detail: None,
15600 documentation: None,
15601 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15602 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15603 new_text: ".id".to_string(),
15604 })),
15605 ..lsp::CompletionItem::default()
15606 };
15607 let resolved_item_1 = lsp::CompletionItem {
15608 additional_text_edits: Some(vec![lsp::TextEdit {
15609 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15610 new_text: "!!".to_string(),
15611 }]),
15612 ..unresolved_item_1.clone()
15613 };
15614 let unresolved_item_2 = lsp::CompletionItem {
15615 label: "other".to_string(),
15616 filter_text: Some("other".to_string()),
15617 detail: None,
15618 documentation: None,
15619 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15620 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15621 new_text: ".other".to_string(),
15622 })),
15623 ..lsp::CompletionItem::default()
15624 };
15625 let resolved_item_2 = lsp::CompletionItem {
15626 additional_text_edits: Some(vec![lsp::TextEdit {
15627 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15628 new_text: "??".to_string(),
15629 }]),
15630 ..unresolved_item_2.clone()
15631 };
15632
15633 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
15634 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
15635 cx.lsp
15636 .server
15637 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15638 let unresolved_item_1 = unresolved_item_1.clone();
15639 let resolved_item_1 = resolved_item_1.clone();
15640 let unresolved_item_2 = unresolved_item_2.clone();
15641 let resolved_item_2 = resolved_item_2.clone();
15642 let resolve_requests_1 = resolve_requests_1.clone();
15643 let resolve_requests_2 = resolve_requests_2.clone();
15644 move |unresolved_request, _| {
15645 let unresolved_item_1 = unresolved_item_1.clone();
15646 let resolved_item_1 = resolved_item_1.clone();
15647 let unresolved_item_2 = unresolved_item_2.clone();
15648 let resolved_item_2 = resolved_item_2.clone();
15649 let resolve_requests_1 = resolve_requests_1.clone();
15650 let resolve_requests_2 = resolve_requests_2.clone();
15651 async move {
15652 if unresolved_request == unresolved_item_1 {
15653 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
15654 Ok(resolved_item_1.clone())
15655 } else if unresolved_request == unresolved_item_2 {
15656 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
15657 Ok(resolved_item_2.clone())
15658 } else {
15659 panic!("Unexpected completion item {unresolved_request:?}")
15660 }
15661 }
15662 }
15663 })
15664 .detach();
15665
15666 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15667 let unresolved_item_1 = unresolved_item_1.clone();
15668 let unresolved_item_2 = unresolved_item_2.clone();
15669 async move {
15670 Ok(Some(lsp::CompletionResponse::Array(vec![
15671 unresolved_item_1,
15672 unresolved_item_2,
15673 ])))
15674 }
15675 })
15676 .next()
15677 .await;
15678
15679 cx.condition(|editor, _| editor.context_menu_visible())
15680 .await;
15681 cx.update_editor(|editor, _, _| {
15682 let context_menu = editor.context_menu.borrow_mut();
15683 let context_menu = context_menu
15684 .as_ref()
15685 .expect("Should have the context menu deployed");
15686 match context_menu {
15687 CodeContextMenu::Completions(completions_menu) => {
15688 let completions = completions_menu.completions.borrow_mut();
15689 assert_eq!(
15690 completions
15691 .iter()
15692 .map(|completion| &completion.label.text)
15693 .collect::<Vec<_>>(),
15694 vec!["id", "other"]
15695 )
15696 }
15697 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15698 }
15699 });
15700 cx.run_until_parked();
15701
15702 cx.update_editor(|editor, window, cx| {
15703 editor.context_menu_next(&ContextMenuNext, window, cx);
15704 });
15705 cx.run_until_parked();
15706 cx.update_editor(|editor, window, cx| {
15707 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15708 });
15709 cx.run_until_parked();
15710 cx.update_editor(|editor, window, cx| {
15711 editor.context_menu_next(&ContextMenuNext, window, cx);
15712 });
15713 cx.run_until_parked();
15714 cx.update_editor(|editor, window, cx| {
15715 editor
15716 .compose_completion(&ComposeCompletion::default(), window, cx)
15717 .expect("No task returned")
15718 })
15719 .await
15720 .expect("Completion failed");
15721 cx.run_until_parked();
15722
15723 cx.update_editor(|editor, _, cx| {
15724 assert_eq!(
15725 resolve_requests_1.load(atomic::Ordering::Acquire),
15726 1,
15727 "Should always resolve once despite multiple selections"
15728 );
15729 assert_eq!(
15730 resolve_requests_2.load(atomic::Ordering::Acquire),
15731 1,
15732 "Should always resolve once after multiple selections and applying the completion"
15733 );
15734 assert_eq!(
15735 editor.text(cx),
15736 "fn main() { let a = ??.other; }",
15737 "Should use resolved data when applying the completion"
15738 );
15739 });
15740}
15741
15742#[gpui::test]
15743async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
15744 init_test(cx, |_| {});
15745
15746 let item_0 = lsp::CompletionItem {
15747 label: "abs".into(),
15748 insert_text: Some("abs".into()),
15749 data: Some(json!({ "very": "special"})),
15750 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
15751 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
15752 lsp::InsertReplaceEdit {
15753 new_text: "abs".to_string(),
15754 insert: lsp::Range::default(),
15755 replace: lsp::Range::default(),
15756 },
15757 )),
15758 ..lsp::CompletionItem::default()
15759 };
15760 let items = iter::once(item_0.clone())
15761 .chain((11..51).map(|i| lsp::CompletionItem {
15762 label: format!("item_{}", i),
15763 insert_text: Some(format!("item_{}", i)),
15764 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15765 ..lsp::CompletionItem::default()
15766 }))
15767 .collect::<Vec<_>>();
15768
15769 let default_commit_characters = vec!["?".to_string()];
15770 let default_data = json!({ "default": "data"});
15771 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
15772 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
15773 let default_edit_range = lsp::Range {
15774 start: lsp::Position {
15775 line: 0,
15776 character: 5,
15777 },
15778 end: lsp::Position {
15779 line: 0,
15780 character: 5,
15781 },
15782 };
15783
15784 let mut cx = EditorLspTestContext::new_rust(
15785 lsp::ServerCapabilities {
15786 completion_provider: Some(lsp::CompletionOptions {
15787 trigger_characters: Some(vec![".".to_string()]),
15788 resolve_provider: Some(true),
15789 ..Default::default()
15790 }),
15791 ..Default::default()
15792 },
15793 cx,
15794 )
15795 .await;
15796
15797 cx.set_state("fn main() { let a = 2ˇ; }");
15798 cx.simulate_keystroke(".");
15799
15800 let completion_data = default_data.clone();
15801 let completion_characters = default_commit_characters.clone();
15802 let completion_items = items.clone();
15803 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15804 let default_data = completion_data.clone();
15805 let default_commit_characters = completion_characters.clone();
15806 let items = completion_items.clone();
15807 async move {
15808 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15809 items,
15810 item_defaults: Some(lsp::CompletionListItemDefaults {
15811 data: Some(default_data.clone()),
15812 commit_characters: Some(default_commit_characters.clone()),
15813 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
15814 default_edit_range,
15815 )),
15816 insert_text_format: Some(default_insert_text_format),
15817 insert_text_mode: Some(default_insert_text_mode),
15818 }),
15819 ..lsp::CompletionList::default()
15820 })))
15821 }
15822 })
15823 .next()
15824 .await;
15825
15826 let resolved_items = Arc::new(Mutex::new(Vec::new()));
15827 cx.lsp
15828 .server
15829 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15830 let closure_resolved_items = resolved_items.clone();
15831 move |item_to_resolve, _| {
15832 let closure_resolved_items = closure_resolved_items.clone();
15833 async move {
15834 closure_resolved_items.lock().push(item_to_resolve.clone());
15835 Ok(item_to_resolve)
15836 }
15837 }
15838 })
15839 .detach();
15840
15841 cx.condition(|editor, _| editor.context_menu_visible())
15842 .await;
15843 cx.run_until_parked();
15844 cx.update_editor(|editor, _, _| {
15845 let menu = editor.context_menu.borrow_mut();
15846 match menu.as_ref().expect("should have the completions menu") {
15847 CodeContextMenu::Completions(completions_menu) => {
15848 assert_eq!(
15849 completions_menu
15850 .entries
15851 .borrow()
15852 .iter()
15853 .map(|mat| mat.string.clone())
15854 .collect::<Vec<String>>(),
15855 items
15856 .iter()
15857 .map(|completion| completion.label.clone())
15858 .collect::<Vec<String>>()
15859 );
15860 }
15861 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
15862 }
15863 });
15864 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
15865 // with 4 from the end.
15866 assert_eq!(
15867 *resolved_items.lock(),
15868 [&items[0..16], &items[items.len() - 4..items.len()]]
15869 .concat()
15870 .iter()
15871 .cloned()
15872 .map(|mut item| {
15873 if item.data.is_none() {
15874 item.data = Some(default_data.clone());
15875 }
15876 item
15877 })
15878 .collect::<Vec<lsp::CompletionItem>>(),
15879 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
15880 );
15881 resolved_items.lock().clear();
15882
15883 cx.update_editor(|editor, window, cx| {
15884 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15885 });
15886 cx.run_until_parked();
15887 // Completions that have already been resolved are skipped.
15888 assert_eq!(
15889 *resolved_items.lock(),
15890 items[items.len() - 17..items.len() - 4]
15891 .iter()
15892 .cloned()
15893 .map(|mut item| {
15894 if item.data.is_none() {
15895 item.data = Some(default_data.clone());
15896 }
15897 item
15898 })
15899 .collect::<Vec<lsp::CompletionItem>>()
15900 );
15901 resolved_items.lock().clear();
15902}
15903
15904#[gpui::test]
15905async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
15906 init_test(cx, |_| {});
15907
15908 let mut cx = EditorLspTestContext::new(
15909 Language::new(
15910 LanguageConfig {
15911 matcher: LanguageMatcher {
15912 path_suffixes: vec!["jsx".into()],
15913 ..Default::default()
15914 },
15915 overrides: [(
15916 "element".into(),
15917 LanguageConfigOverride {
15918 completion_query_characters: Override::Set(['-'].into_iter().collect()),
15919 ..Default::default()
15920 },
15921 )]
15922 .into_iter()
15923 .collect(),
15924 ..Default::default()
15925 },
15926 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15927 )
15928 .with_override_query("(jsx_self_closing_element) @element")
15929 .unwrap(),
15930 lsp::ServerCapabilities {
15931 completion_provider: Some(lsp::CompletionOptions {
15932 trigger_characters: Some(vec![":".to_string()]),
15933 ..Default::default()
15934 }),
15935 ..Default::default()
15936 },
15937 cx,
15938 )
15939 .await;
15940
15941 cx.lsp
15942 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15943 Ok(Some(lsp::CompletionResponse::Array(vec![
15944 lsp::CompletionItem {
15945 label: "bg-blue".into(),
15946 ..Default::default()
15947 },
15948 lsp::CompletionItem {
15949 label: "bg-red".into(),
15950 ..Default::default()
15951 },
15952 lsp::CompletionItem {
15953 label: "bg-yellow".into(),
15954 ..Default::default()
15955 },
15956 ])))
15957 });
15958
15959 cx.set_state(r#"<p class="bgˇ" />"#);
15960
15961 // Trigger completion when typing a dash, because the dash is an extra
15962 // word character in the 'element' scope, which contains the cursor.
15963 cx.simulate_keystroke("-");
15964 cx.executor().run_until_parked();
15965 cx.update_editor(|editor, _, _| {
15966 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15967 {
15968 assert_eq!(
15969 completion_menu_entries(&menu),
15970 &["bg-blue", "bg-red", "bg-yellow"]
15971 );
15972 } else {
15973 panic!("expected completion menu to be open");
15974 }
15975 });
15976
15977 cx.simulate_keystroke("l");
15978 cx.executor().run_until_parked();
15979 cx.update_editor(|editor, _, _| {
15980 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15981 {
15982 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
15983 } else {
15984 panic!("expected completion menu to be open");
15985 }
15986 });
15987
15988 // When filtering completions, consider the character after the '-' to
15989 // be the start of a subword.
15990 cx.set_state(r#"<p class="yelˇ" />"#);
15991 cx.simulate_keystroke("l");
15992 cx.executor().run_until_parked();
15993 cx.update_editor(|editor, _, _| {
15994 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15995 {
15996 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
15997 } else {
15998 panic!("expected completion menu to be open");
15999 }
16000 });
16001}
16002
16003fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
16004 let entries = menu.entries.borrow();
16005 entries.iter().map(|mat| mat.string.clone()).collect()
16006}
16007
16008#[gpui::test]
16009async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
16010 init_test(cx, |settings| {
16011 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
16012 Formatter::Prettier,
16013 )))
16014 });
16015
16016 let fs = FakeFs::new(cx.executor());
16017 fs.insert_file(path!("/file.ts"), Default::default()).await;
16018
16019 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
16020 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16021
16022 language_registry.add(Arc::new(Language::new(
16023 LanguageConfig {
16024 name: "TypeScript".into(),
16025 matcher: LanguageMatcher {
16026 path_suffixes: vec!["ts".to_string()],
16027 ..Default::default()
16028 },
16029 ..Default::default()
16030 },
16031 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
16032 )));
16033 update_test_language_settings(cx, |settings| {
16034 settings.defaults.prettier = Some(PrettierSettings {
16035 allowed: true,
16036 ..PrettierSettings::default()
16037 });
16038 });
16039
16040 let test_plugin = "test_plugin";
16041 let _ = language_registry.register_fake_lsp(
16042 "TypeScript",
16043 FakeLspAdapter {
16044 prettier_plugins: vec![test_plugin],
16045 ..Default::default()
16046 },
16047 );
16048
16049 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
16050 let buffer = project
16051 .update(cx, |project, cx| {
16052 project.open_local_buffer(path!("/file.ts"), cx)
16053 })
16054 .await
16055 .unwrap();
16056
16057 let buffer_text = "one\ntwo\nthree\n";
16058 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16059 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16060 editor.update_in(cx, |editor, window, cx| {
16061 editor.set_text(buffer_text, window, cx)
16062 });
16063
16064 editor
16065 .update_in(cx, |editor, window, cx| {
16066 editor.perform_format(
16067 project.clone(),
16068 FormatTrigger::Manual,
16069 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16070 window,
16071 cx,
16072 )
16073 })
16074 .unwrap()
16075 .await;
16076 assert_eq!(
16077 editor.update(cx, |editor, cx| editor.text(cx)),
16078 buffer_text.to_string() + prettier_format_suffix,
16079 "Test prettier formatting was not applied to the original buffer text",
16080 );
16081
16082 update_test_language_settings(cx, |settings| {
16083 settings.defaults.formatter = Some(SelectedFormatter::Auto)
16084 });
16085 let format = editor.update_in(cx, |editor, window, cx| {
16086 editor.perform_format(
16087 project.clone(),
16088 FormatTrigger::Manual,
16089 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16090 window,
16091 cx,
16092 )
16093 });
16094 format.await.unwrap();
16095 assert_eq!(
16096 editor.update(cx, |editor, cx| editor.text(cx)),
16097 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
16098 "Autoformatting (via test prettier) was not applied to the original buffer text",
16099 );
16100}
16101
16102#[gpui::test]
16103async fn test_addition_reverts(cx: &mut TestAppContext) {
16104 init_test(cx, |_| {});
16105 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16106 let base_text = indoc! {r#"
16107 struct Row;
16108 struct Row1;
16109 struct Row2;
16110
16111 struct Row4;
16112 struct Row5;
16113 struct Row6;
16114
16115 struct Row8;
16116 struct Row9;
16117 struct Row10;"#};
16118
16119 // When addition hunks are not adjacent to carets, no hunk revert is performed
16120 assert_hunk_revert(
16121 indoc! {r#"struct Row;
16122 struct Row1;
16123 struct Row1.1;
16124 struct Row1.2;
16125 struct Row2;ˇ
16126
16127 struct Row4;
16128 struct Row5;
16129 struct Row6;
16130
16131 struct Row8;
16132 ˇstruct Row9;
16133 struct Row9.1;
16134 struct Row9.2;
16135 struct Row9.3;
16136 struct Row10;"#},
16137 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16138 indoc! {r#"struct Row;
16139 struct Row1;
16140 struct Row1.1;
16141 struct Row1.2;
16142 struct Row2;ˇ
16143
16144 struct Row4;
16145 struct Row5;
16146 struct Row6;
16147
16148 struct Row8;
16149 ˇstruct Row9;
16150 struct Row9.1;
16151 struct Row9.2;
16152 struct Row9.3;
16153 struct Row10;"#},
16154 base_text,
16155 &mut cx,
16156 );
16157 // Same for selections
16158 assert_hunk_revert(
16159 indoc! {r#"struct Row;
16160 struct Row1;
16161 struct Row2;
16162 struct Row2.1;
16163 struct Row2.2;
16164 «ˇ
16165 struct Row4;
16166 struct» Row5;
16167 «struct Row6;
16168 ˇ»
16169 struct Row9.1;
16170 struct Row9.2;
16171 struct Row9.3;
16172 struct Row8;
16173 struct Row9;
16174 struct Row10;"#},
16175 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16176 indoc! {r#"struct Row;
16177 struct Row1;
16178 struct Row2;
16179 struct Row2.1;
16180 struct Row2.2;
16181 «ˇ
16182 struct Row4;
16183 struct» Row5;
16184 «struct Row6;
16185 ˇ»
16186 struct Row9.1;
16187 struct Row9.2;
16188 struct Row9.3;
16189 struct Row8;
16190 struct Row9;
16191 struct Row10;"#},
16192 base_text,
16193 &mut cx,
16194 );
16195
16196 // When carets and selections intersect the addition hunks, those are reverted.
16197 // Adjacent carets got merged.
16198 assert_hunk_revert(
16199 indoc! {r#"struct Row;
16200 ˇ// something on the top
16201 struct Row1;
16202 struct Row2;
16203 struct Roˇw3.1;
16204 struct Row2.2;
16205 struct Row2.3;ˇ
16206
16207 struct Row4;
16208 struct ˇRow5.1;
16209 struct Row5.2;
16210 struct «Rowˇ»5.3;
16211 struct Row5;
16212 struct Row6;
16213 ˇ
16214 struct Row9.1;
16215 struct «Rowˇ»9.2;
16216 struct «ˇRow»9.3;
16217 struct Row8;
16218 struct Row9;
16219 «ˇ// something on bottom»
16220 struct Row10;"#},
16221 vec![
16222 DiffHunkStatusKind::Added,
16223 DiffHunkStatusKind::Added,
16224 DiffHunkStatusKind::Added,
16225 DiffHunkStatusKind::Added,
16226 DiffHunkStatusKind::Added,
16227 ],
16228 indoc! {r#"struct Row;
16229 ˇstruct Row1;
16230 struct Row2;
16231 ˇ
16232 struct Row4;
16233 ˇstruct Row5;
16234 struct Row6;
16235 ˇ
16236 ˇstruct Row8;
16237 struct Row9;
16238 ˇstruct Row10;"#},
16239 base_text,
16240 &mut cx,
16241 );
16242}
16243
16244#[gpui::test]
16245async fn test_modification_reverts(cx: &mut TestAppContext) {
16246 init_test(cx, |_| {});
16247 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16248 let base_text = indoc! {r#"
16249 struct Row;
16250 struct Row1;
16251 struct Row2;
16252
16253 struct Row4;
16254 struct Row5;
16255 struct Row6;
16256
16257 struct Row8;
16258 struct Row9;
16259 struct Row10;"#};
16260
16261 // Modification hunks behave the same as the addition ones.
16262 assert_hunk_revert(
16263 indoc! {r#"struct Row;
16264 struct Row1;
16265 struct Row33;
16266 ˇ
16267 struct Row4;
16268 struct Row5;
16269 struct Row6;
16270 ˇ
16271 struct Row99;
16272 struct Row9;
16273 struct Row10;"#},
16274 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16275 indoc! {r#"struct Row;
16276 struct Row1;
16277 struct Row33;
16278 ˇ
16279 struct Row4;
16280 struct Row5;
16281 struct Row6;
16282 ˇ
16283 struct Row99;
16284 struct Row9;
16285 struct Row10;"#},
16286 base_text,
16287 &mut cx,
16288 );
16289 assert_hunk_revert(
16290 indoc! {r#"struct Row;
16291 struct Row1;
16292 struct Row33;
16293 «ˇ
16294 struct Row4;
16295 struct» Row5;
16296 «struct Row6;
16297 ˇ»
16298 struct Row99;
16299 struct Row9;
16300 struct Row10;"#},
16301 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16302 indoc! {r#"struct Row;
16303 struct Row1;
16304 struct Row33;
16305 «ˇ
16306 struct Row4;
16307 struct» Row5;
16308 «struct Row6;
16309 ˇ»
16310 struct Row99;
16311 struct Row9;
16312 struct Row10;"#},
16313 base_text,
16314 &mut cx,
16315 );
16316
16317 assert_hunk_revert(
16318 indoc! {r#"ˇstruct Row1.1;
16319 struct Row1;
16320 «ˇstr»uct Row22;
16321
16322 struct ˇRow44;
16323 struct Row5;
16324 struct «Rˇ»ow66;ˇ
16325
16326 «struˇ»ct Row88;
16327 struct Row9;
16328 struct Row1011;ˇ"#},
16329 vec![
16330 DiffHunkStatusKind::Modified,
16331 DiffHunkStatusKind::Modified,
16332 DiffHunkStatusKind::Modified,
16333 DiffHunkStatusKind::Modified,
16334 DiffHunkStatusKind::Modified,
16335 DiffHunkStatusKind::Modified,
16336 ],
16337 indoc! {r#"struct Row;
16338 ˇstruct Row1;
16339 struct Row2;
16340 ˇ
16341 struct Row4;
16342 ˇstruct Row5;
16343 struct Row6;
16344 ˇ
16345 struct Row8;
16346 ˇstruct Row9;
16347 struct Row10;ˇ"#},
16348 base_text,
16349 &mut cx,
16350 );
16351}
16352
16353#[gpui::test]
16354async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
16355 init_test(cx, |_| {});
16356 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16357 let base_text = indoc! {r#"
16358 one
16359
16360 two
16361 three
16362 "#};
16363
16364 cx.set_head_text(base_text);
16365 cx.set_state("\nˇ\n");
16366 cx.executor().run_until_parked();
16367 cx.update_editor(|editor, _window, cx| {
16368 editor.expand_selected_diff_hunks(cx);
16369 });
16370 cx.executor().run_until_parked();
16371 cx.update_editor(|editor, window, cx| {
16372 editor.backspace(&Default::default(), window, cx);
16373 });
16374 cx.run_until_parked();
16375 cx.assert_state_with_diff(
16376 indoc! {r#"
16377
16378 - two
16379 - threeˇ
16380 +
16381 "#}
16382 .to_string(),
16383 );
16384}
16385
16386#[gpui::test]
16387async fn test_deletion_reverts(cx: &mut TestAppContext) {
16388 init_test(cx, |_| {});
16389 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16390 let base_text = indoc! {r#"struct Row;
16391struct Row1;
16392struct Row2;
16393
16394struct Row4;
16395struct Row5;
16396struct Row6;
16397
16398struct Row8;
16399struct Row9;
16400struct Row10;"#};
16401
16402 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
16403 assert_hunk_revert(
16404 indoc! {r#"struct Row;
16405 struct Row2;
16406
16407 ˇstruct Row4;
16408 struct Row5;
16409 struct Row6;
16410 ˇ
16411 struct Row8;
16412 struct Row10;"#},
16413 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16414 indoc! {r#"struct Row;
16415 struct Row2;
16416
16417 ˇstruct Row4;
16418 struct Row5;
16419 struct Row6;
16420 ˇ
16421 struct Row8;
16422 struct Row10;"#},
16423 base_text,
16424 &mut cx,
16425 );
16426 assert_hunk_revert(
16427 indoc! {r#"struct Row;
16428 struct Row2;
16429
16430 «ˇstruct Row4;
16431 struct» Row5;
16432 «struct Row6;
16433 ˇ»
16434 struct Row8;
16435 struct Row10;"#},
16436 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16437 indoc! {r#"struct Row;
16438 struct Row2;
16439
16440 «ˇstruct Row4;
16441 struct» Row5;
16442 «struct Row6;
16443 ˇ»
16444 struct Row8;
16445 struct Row10;"#},
16446 base_text,
16447 &mut cx,
16448 );
16449
16450 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
16451 assert_hunk_revert(
16452 indoc! {r#"struct Row;
16453 ˇstruct Row2;
16454
16455 struct Row4;
16456 struct Row5;
16457 struct Row6;
16458
16459 struct Row8;ˇ
16460 struct Row10;"#},
16461 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16462 indoc! {r#"struct Row;
16463 struct Row1;
16464 ˇstruct Row2;
16465
16466 struct Row4;
16467 struct Row5;
16468 struct Row6;
16469
16470 struct Row8;ˇ
16471 struct Row9;
16472 struct Row10;"#},
16473 base_text,
16474 &mut cx,
16475 );
16476 assert_hunk_revert(
16477 indoc! {r#"struct Row;
16478 struct Row2«ˇ;
16479 struct Row4;
16480 struct» Row5;
16481 «struct Row6;
16482
16483 struct Row8;ˇ»
16484 struct Row10;"#},
16485 vec![
16486 DiffHunkStatusKind::Deleted,
16487 DiffHunkStatusKind::Deleted,
16488 DiffHunkStatusKind::Deleted,
16489 ],
16490 indoc! {r#"struct Row;
16491 struct Row1;
16492 struct Row2«ˇ;
16493
16494 struct Row4;
16495 struct» Row5;
16496 «struct Row6;
16497
16498 struct Row8;ˇ»
16499 struct Row9;
16500 struct Row10;"#},
16501 base_text,
16502 &mut cx,
16503 );
16504}
16505
16506#[gpui::test]
16507async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
16508 init_test(cx, |_| {});
16509
16510 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
16511 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
16512 let base_text_3 =
16513 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
16514
16515 let text_1 = edit_first_char_of_every_line(base_text_1);
16516 let text_2 = edit_first_char_of_every_line(base_text_2);
16517 let text_3 = edit_first_char_of_every_line(base_text_3);
16518
16519 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
16520 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
16521 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
16522
16523 let multibuffer = cx.new(|cx| {
16524 let mut multibuffer = MultiBuffer::new(ReadWrite);
16525 multibuffer.push_excerpts(
16526 buffer_1.clone(),
16527 [
16528 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16529 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16530 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16531 ],
16532 cx,
16533 );
16534 multibuffer.push_excerpts(
16535 buffer_2.clone(),
16536 [
16537 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16538 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16539 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16540 ],
16541 cx,
16542 );
16543 multibuffer.push_excerpts(
16544 buffer_3.clone(),
16545 [
16546 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16547 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16548 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16549 ],
16550 cx,
16551 );
16552 multibuffer
16553 });
16554
16555 let fs = FakeFs::new(cx.executor());
16556 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
16557 let (editor, cx) = cx
16558 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
16559 editor.update_in(cx, |editor, _window, cx| {
16560 for (buffer, diff_base) in [
16561 (buffer_1.clone(), base_text_1),
16562 (buffer_2.clone(), base_text_2),
16563 (buffer_3.clone(), base_text_3),
16564 ] {
16565 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16566 editor
16567 .buffer
16568 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16569 }
16570 });
16571 cx.executor().run_until_parked();
16572
16573 editor.update_in(cx, |editor, window, cx| {
16574 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}");
16575 editor.select_all(&SelectAll, window, cx);
16576 editor.git_restore(&Default::default(), window, cx);
16577 });
16578 cx.executor().run_until_parked();
16579
16580 // When all ranges are selected, all buffer hunks are reverted.
16581 editor.update(cx, |editor, cx| {
16582 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");
16583 });
16584 buffer_1.update(cx, |buffer, _| {
16585 assert_eq!(buffer.text(), base_text_1);
16586 });
16587 buffer_2.update(cx, |buffer, _| {
16588 assert_eq!(buffer.text(), base_text_2);
16589 });
16590 buffer_3.update(cx, |buffer, _| {
16591 assert_eq!(buffer.text(), base_text_3);
16592 });
16593
16594 editor.update_in(cx, |editor, window, cx| {
16595 editor.undo(&Default::default(), window, cx);
16596 });
16597
16598 editor.update_in(cx, |editor, window, cx| {
16599 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16600 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
16601 });
16602 editor.git_restore(&Default::default(), window, cx);
16603 });
16604
16605 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
16606 // but not affect buffer_2 and its related excerpts.
16607 editor.update(cx, |editor, cx| {
16608 assert_eq!(
16609 editor.text(cx),
16610 "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}"
16611 );
16612 });
16613 buffer_1.update(cx, |buffer, _| {
16614 assert_eq!(buffer.text(), base_text_1);
16615 });
16616 buffer_2.update(cx, |buffer, _| {
16617 assert_eq!(
16618 buffer.text(),
16619 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
16620 );
16621 });
16622 buffer_3.update(cx, |buffer, _| {
16623 assert_eq!(
16624 buffer.text(),
16625 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
16626 );
16627 });
16628
16629 fn edit_first_char_of_every_line(text: &str) -> String {
16630 text.split('\n')
16631 .map(|line| format!("X{}", &line[1..]))
16632 .collect::<Vec<_>>()
16633 .join("\n")
16634 }
16635}
16636
16637#[gpui::test]
16638async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
16639 init_test(cx, |_| {});
16640
16641 let cols = 4;
16642 let rows = 10;
16643 let sample_text_1 = sample_text(rows, cols, 'a');
16644 assert_eq!(
16645 sample_text_1,
16646 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
16647 );
16648 let sample_text_2 = sample_text(rows, cols, 'l');
16649 assert_eq!(
16650 sample_text_2,
16651 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
16652 );
16653 let sample_text_3 = sample_text(rows, cols, 'v');
16654 assert_eq!(
16655 sample_text_3,
16656 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
16657 );
16658
16659 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
16660 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
16661 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
16662
16663 let multi_buffer = cx.new(|cx| {
16664 let mut multibuffer = MultiBuffer::new(ReadWrite);
16665 multibuffer.push_excerpts(
16666 buffer_1.clone(),
16667 [
16668 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16669 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16670 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16671 ],
16672 cx,
16673 );
16674 multibuffer.push_excerpts(
16675 buffer_2.clone(),
16676 [
16677 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16678 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16679 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16680 ],
16681 cx,
16682 );
16683 multibuffer.push_excerpts(
16684 buffer_3.clone(),
16685 [
16686 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16687 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16688 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16689 ],
16690 cx,
16691 );
16692 multibuffer
16693 });
16694
16695 let fs = FakeFs::new(cx.executor());
16696 fs.insert_tree(
16697 "/a",
16698 json!({
16699 "main.rs": sample_text_1,
16700 "other.rs": sample_text_2,
16701 "lib.rs": sample_text_3,
16702 }),
16703 )
16704 .await;
16705 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16706 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16707 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16708 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16709 Editor::new(
16710 EditorMode::full(),
16711 multi_buffer,
16712 Some(project.clone()),
16713 window,
16714 cx,
16715 )
16716 });
16717 let multibuffer_item_id = workspace
16718 .update(cx, |workspace, window, cx| {
16719 assert!(
16720 workspace.active_item(cx).is_none(),
16721 "active item should be None before the first item is added"
16722 );
16723 workspace.add_item_to_active_pane(
16724 Box::new(multi_buffer_editor.clone()),
16725 None,
16726 true,
16727 window,
16728 cx,
16729 );
16730 let active_item = workspace
16731 .active_item(cx)
16732 .expect("should have an active item after adding the multi buffer");
16733 assert!(
16734 !active_item.is_singleton(cx),
16735 "A multi buffer was expected to active after adding"
16736 );
16737 active_item.item_id()
16738 })
16739 .unwrap();
16740 cx.executor().run_until_parked();
16741
16742 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16743 editor.change_selections(
16744 SelectionEffects::scroll(Autoscroll::Next),
16745 window,
16746 cx,
16747 |s| s.select_ranges(Some(1..2)),
16748 );
16749 editor.open_excerpts(&OpenExcerpts, window, cx);
16750 });
16751 cx.executor().run_until_parked();
16752 let first_item_id = workspace
16753 .update(cx, |workspace, window, cx| {
16754 let active_item = workspace
16755 .active_item(cx)
16756 .expect("should have an active item after navigating into the 1st buffer");
16757 let first_item_id = active_item.item_id();
16758 assert_ne!(
16759 first_item_id, multibuffer_item_id,
16760 "Should navigate into the 1st buffer and activate it"
16761 );
16762 assert!(
16763 active_item.is_singleton(cx),
16764 "New active item should be a singleton buffer"
16765 );
16766 assert_eq!(
16767 active_item
16768 .act_as::<Editor>(cx)
16769 .expect("should have navigated into an editor for the 1st buffer")
16770 .read(cx)
16771 .text(cx),
16772 sample_text_1
16773 );
16774
16775 workspace
16776 .go_back(workspace.active_pane().downgrade(), window, cx)
16777 .detach_and_log_err(cx);
16778
16779 first_item_id
16780 })
16781 .unwrap();
16782 cx.executor().run_until_parked();
16783 workspace
16784 .update(cx, |workspace, _, cx| {
16785 let active_item = workspace
16786 .active_item(cx)
16787 .expect("should have an active item after navigating back");
16788 assert_eq!(
16789 active_item.item_id(),
16790 multibuffer_item_id,
16791 "Should navigate back to the multi buffer"
16792 );
16793 assert!(!active_item.is_singleton(cx));
16794 })
16795 .unwrap();
16796
16797 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16798 editor.change_selections(
16799 SelectionEffects::scroll(Autoscroll::Next),
16800 window,
16801 cx,
16802 |s| s.select_ranges(Some(39..40)),
16803 );
16804 editor.open_excerpts(&OpenExcerpts, window, cx);
16805 });
16806 cx.executor().run_until_parked();
16807 let second_item_id = workspace
16808 .update(cx, |workspace, window, cx| {
16809 let active_item = workspace
16810 .active_item(cx)
16811 .expect("should have an active item after navigating into the 2nd buffer");
16812 let second_item_id = active_item.item_id();
16813 assert_ne!(
16814 second_item_id, multibuffer_item_id,
16815 "Should navigate away from the multibuffer"
16816 );
16817 assert_ne!(
16818 second_item_id, first_item_id,
16819 "Should navigate into the 2nd buffer and activate it"
16820 );
16821 assert!(
16822 active_item.is_singleton(cx),
16823 "New active item should be a singleton buffer"
16824 );
16825 assert_eq!(
16826 active_item
16827 .act_as::<Editor>(cx)
16828 .expect("should have navigated into an editor")
16829 .read(cx)
16830 .text(cx),
16831 sample_text_2
16832 );
16833
16834 workspace
16835 .go_back(workspace.active_pane().downgrade(), window, cx)
16836 .detach_and_log_err(cx);
16837
16838 second_item_id
16839 })
16840 .unwrap();
16841 cx.executor().run_until_parked();
16842 workspace
16843 .update(cx, |workspace, _, cx| {
16844 let active_item = workspace
16845 .active_item(cx)
16846 .expect("should have an active item after navigating back from the 2nd buffer");
16847 assert_eq!(
16848 active_item.item_id(),
16849 multibuffer_item_id,
16850 "Should navigate back from the 2nd buffer to the multi buffer"
16851 );
16852 assert!(!active_item.is_singleton(cx));
16853 })
16854 .unwrap();
16855
16856 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16857 editor.change_selections(
16858 SelectionEffects::scroll(Autoscroll::Next),
16859 window,
16860 cx,
16861 |s| s.select_ranges(Some(70..70)),
16862 );
16863 editor.open_excerpts(&OpenExcerpts, window, cx);
16864 });
16865 cx.executor().run_until_parked();
16866 workspace
16867 .update(cx, |workspace, window, cx| {
16868 let active_item = workspace
16869 .active_item(cx)
16870 .expect("should have an active item after navigating into the 3rd buffer");
16871 let third_item_id = active_item.item_id();
16872 assert_ne!(
16873 third_item_id, multibuffer_item_id,
16874 "Should navigate into the 3rd buffer and activate it"
16875 );
16876 assert_ne!(third_item_id, first_item_id);
16877 assert_ne!(third_item_id, second_item_id);
16878 assert!(
16879 active_item.is_singleton(cx),
16880 "New active item should be a singleton buffer"
16881 );
16882 assert_eq!(
16883 active_item
16884 .act_as::<Editor>(cx)
16885 .expect("should have navigated into an editor")
16886 .read(cx)
16887 .text(cx),
16888 sample_text_3
16889 );
16890
16891 workspace
16892 .go_back(workspace.active_pane().downgrade(), window, cx)
16893 .detach_and_log_err(cx);
16894 })
16895 .unwrap();
16896 cx.executor().run_until_parked();
16897 workspace
16898 .update(cx, |workspace, _, cx| {
16899 let active_item = workspace
16900 .active_item(cx)
16901 .expect("should have an active item after navigating back from the 3rd buffer");
16902 assert_eq!(
16903 active_item.item_id(),
16904 multibuffer_item_id,
16905 "Should navigate back from the 3rd buffer to the multi buffer"
16906 );
16907 assert!(!active_item.is_singleton(cx));
16908 })
16909 .unwrap();
16910}
16911
16912#[gpui::test]
16913async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16914 init_test(cx, |_| {});
16915
16916 let mut cx = EditorTestContext::new(cx).await;
16917
16918 let diff_base = r#"
16919 use some::mod;
16920
16921 const A: u32 = 42;
16922
16923 fn main() {
16924 println!("hello");
16925
16926 println!("world");
16927 }
16928 "#
16929 .unindent();
16930
16931 cx.set_state(
16932 &r#"
16933 use some::modified;
16934
16935 ˇ
16936 fn main() {
16937 println!("hello there");
16938
16939 println!("around the");
16940 println!("world");
16941 }
16942 "#
16943 .unindent(),
16944 );
16945
16946 cx.set_head_text(&diff_base);
16947 executor.run_until_parked();
16948
16949 cx.update_editor(|editor, window, cx| {
16950 editor.go_to_next_hunk(&GoToHunk, window, cx);
16951 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16952 });
16953 executor.run_until_parked();
16954 cx.assert_state_with_diff(
16955 r#"
16956 use some::modified;
16957
16958
16959 fn main() {
16960 - println!("hello");
16961 + ˇ println!("hello there");
16962
16963 println!("around the");
16964 println!("world");
16965 }
16966 "#
16967 .unindent(),
16968 );
16969
16970 cx.update_editor(|editor, window, cx| {
16971 for _ in 0..2 {
16972 editor.go_to_next_hunk(&GoToHunk, window, cx);
16973 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16974 }
16975 });
16976 executor.run_until_parked();
16977 cx.assert_state_with_diff(
16978 r#"
16979 - use some::mod;
16980 + ˇuse some::modified;
16981
16982
16983 fn main() {
16984 - println!("hello");
16985 + println!("hello there");
16986
16987 + println!("around the");
16988 println!("world");
16989 }
16990 "#
16991 .unindent(),
16992 );
16993
16994 cx.update_editor(|editor, window, cx| {
16995 editor.go_to_next_hunk(&GoToHunk, window, cx);
16996 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16997 });
16998 executor.run_until_parked();
16999 cx.assert_state_with_diff(
17000 r#"
17001 - use some::mod;
17002 + use some::modified;
17003
17004 - const A: u32 = 42;
17005 ˇ
17006 fn main() {
17007 - println!("hello");
17008 + println!("hello there");
17009
17010 + println!("around the");
17011 println!("world");
17012 }
17013 "#
17014 .unindent(),
17015 );
17016
17017 cx.update_editor(|editor, window, cx| {
17018 editor.cancel(&Cancel, window, cx);
17019 });
17020
17021 cx.assert_state_with_diff(
17022 r#"
17023 use some::modified;
17024
17025 ˇ
17026 fn main() {
17027 println!("hello there");
17028
17029 println!("around the");
17030 println!("world");
17031 }
17032 "#
17033 .unindent(),
17034 );
17035}
17036
17037#[gpui::test]
17038async fn test_diff_base_change_with_expanded_diff_hunks(
17039 executor: BackgroundExecutor,
17040 cx: &mut TestAppContext,
17041) {
17042 init_test(cx, |_| {});
17043
17044 let mut cx = EditorTestContext::new(cx).await;
17045
17046 let diff_base = r#"
17047 use some::mod1;
17048 use some::mod2;
17049
17050 const A: u32 = 42;
17051 const B: u32 = 42;
17052 const C: u32 = 42;
17053
17054 fn main() {
17055 println!("hello");
17056
17057 println!("world");
17058 }
17059 "#
17060 .unindent();
17061
17062 cx.set_state(
17063 &r#"
17064 use some::mod2;
17065
17066 const A: u32 = 42;
17067 const C: u32 = 42;
17068
17069 fn main(ˇ) {
17070 //println!("hello");
17071
17072 println!("world");
17073 //
17074 //
17075 }
17076 "#
17077 .unindent(),
17078 );
17079
17080 cx.set_head_text(&diff_base);
17081 executor.run_until_parked();
17082
17083 cx.update_editor(|editor, window, cx| {
17084 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17085 });
17086 executor.run_until_parked();
17087 cx.assert_state_with_diff(
17088 r#"
17089 - use some::mod1;
17090 use some::mod2;
17091
17092 const A: u32 = 42;
17093 - const B: u32 = 42;
17094 const C: u32 = 42;
17095
17096 fn main(ˇ) {
17097 - println!("hello");
17098 + //println!("hello");
17099
17100 println!("world");
17101 + //
17102 + //
17103 }
17104 "#
17105 .unindent(),
17106 );
17107
17108 cx.set_head_text("new diff base!");
17109 executor.run_until_parked();
17110 cx.assert_state_with_diff(
17111 r#"
17112 - new diff base!
17113 + use some::mod2;
17114 +
17115 + const A: u32 = 42;
17116 + const C: u32 = 42;
17117 +
17118 + fn main(ˇ) {
17119 + //println!("hello");
17120 +
17121 + println!("world");
17122 + //
17123 + //
17124 + }
17125 "#
17126 .unindent(),
17127 );
17128}
17129
17130#[gpui::test]
17131async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
17132 init_test(cx, |_| {});
17133
17134 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17135 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17136 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17137 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17138 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
17139 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
17140
17141 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
17142 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
17143 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
17144
17145 let multi_buffer = cx.new(|cx| {
17146 let mut multibuffer = MultiBuffer::new(ReadWrite);
17147 multibuffer.push_excerpts(
17148 buffer_1.clone(),
17149 [
17150 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17151 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17152 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17153 ],
17154 cx,
17155 );
17156 multibuffer.push_excerpts(
17157 buffer_2.clone(),
17158 [
17159 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17160 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17161 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17162 ],
17163 cx,
17164 );
17165 multibuffer.push_excerpts(
17166 buffer_3.clone(),
17167 [
17168 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17169 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17170 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17171 ],
17172 cx,
17173 );
17174 multibuffer
17175 });
17176
17177 let editor =
17178 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17179 editor
17180 .update(cx, |editor, _window, cx| {
17181 for (buffer, diff_base) in [
17182 (buffer_1.clone(), file_1_old),
17183 (buffer_2.clone(), file_2_old),
17184 (buffer_3.clone(), file_3_old),
17185 ] {
17186 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
17187 editor
17188 .buffer
17189 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
17190 }
17191 })
17192 .unwrap();
17193
17194 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17195 cx.run_until_parked();
17196
17197 cx.assert_editor_state(
17198 &"
17199 ˇaaa
17200 ccc
17201 ddd
17202
17203 ggg
17204 hhh
17205
17206
17207 lll
17208 mmm
17209 NNN
17210
17211 qqq
17212 rrr
17213
17214 uuu
17215 111
17216 222
17217 333
17218
17219 666
17220 777
17221
17222 000
17223 !!!"
17224 .unindent(),
17225 );
17226
17227 cx.update_editor(|editor, window, cx| {
17228 editor.select_all(&SelectAll, window, cx);
17229 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17230 });
17231 cx.executor().run_until_parked();
17232
17233 cx.assert_state_with_diff(
17234 "
17235 «aaa
17236 - bbb
17237 ccc
17238 ddd
17239
17240 ggg
17241 hhh
17242
17243
17244 lll
17245 mmm
17246 - nnn
17247 + NNN
17248
17249 qqq
17250 rrr
17251
17252 uuu
17253 111
17254 222
17255 333
17256
17257 + 666
17258 777
17259
17260 000
17261 !!!ˇ»"
17262 .unindent(),
17263 );
17264}
17265
17266#[gpui::test]
17267async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
17268 init_test(cx, |_| {});
17269
17270 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
17271 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
17272
17273 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
17274 let multi_buffer = cx.new(|cx| {
17275 let mut multibuffer = MultiBuffer::new(ReadWrite);
17276 multibuffer.push_excerpts(
17277 buffer.clone(),
17278 [
17279 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
17280 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
17281 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
17282 ],
17283 cx,
17284 );
17285 multibuffer
17286 });
17287
17288 let editor =
17289 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17290 editor
17291 .update(cx, |editor, _window, cx| {
17292 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
17293 editor
17294 .buffer
17295 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
17296 })
17297 .unwrap();
17298
17299 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17300 cx.run_until_parked();
17301
17302 cx.update_editor(|editor, window, cx| {
17303 editor.expand_all_diff_hunks(&Default::default(), window, cx)
17304 });
17305 cx.executor().run_until_parked();
17306
17307 // When the start of a hunk coincides with the start of its excerpt,
17308 // the hunk is expanded. When the start of a a hunk is earlier than
17309 // the start of its excerpt, the hunk is not expanded.
17310 cx.assert_state_with_diff(
17311 "
17312 ˇaaa
17313 - bbb
17314 + BBB
17315
17316 - ddd
17317 - eee
17318 + DDD
17319 + EEE
17320 fff
17321
17322 iii
17323 "
17324 .unindent(),
17325 );
17326}
17327
17328#[gpui::test]
17329async fn test_edits_around_expanded_insertion_hunks(
17330 executor: BackgroundExecutor,
17331 cx: &mut TestAppContext,
17332) {
17333 init_test(cx, |_| {});
17334
17335 let mut cx = EditorTestContext::new(cx).await;
17336
17337 let diff_base = r#"
17338 use some::mod1;
17339 use some::mod2;
17340
17341 const A: u32 = 42;
17342
17343 fn main() {
17344 println!("hello");
17345
17346 println!("world");
17347 }
17348 "#
17349 .unindent();
17350 executor.run_until_parked();
17351 cx.set_state(
17352 &r#"
17353 use some::mod1;
17354 use some::mod2;
17355
17356 const A: u32 = 42;
17357 const B: u32 = 42;
17358 const C: u32 = 42;
17359 ˇ
17360
17361 fn main() {
17362 println!("hello");
17363
17364 println!("world");
17365 }
17366 "#
17367 .unindent(),
17368 );
17369
17370 cx.set_head_text(&diff_base);
17371 executor.run_until_parked();
17372
17373 cx.update_editor(|editor, window, cx| {
17374 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17375 });
17376 executor.run_until_parked();
17377
17378 cx.assert_state_with_diff(
17379 r#"
17380 use some::mod1;
17381 use some::mod2;
17382
17383 const A: u32 = 42;
17384 + const B: u32 = 42;
17385 + const C: u32 = 42;
17386 + ˇ
17387
17388 fn main() {
17389 println!("hello");
17390
17391 println!("world");
17392 }
17393 "#
17394 .unindent(),
17395 );
17396
17397 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
17398 executor.run_until_parked();
17399
17400 cx.assert_state_with_diff(
17401 r#"
17402 use some::mod1;
17403 use some::mod2;
17404
17405 const A: u32 = 42;
17406 + const B: u32 = 42;
17407 + const C: u32 = 42;
17408 + const D: u32 = 42;
17409 + ˇ
17410
17411 fn main() {
17412 println!("hello");
17413
17414 println!("world");
17415 }
17416 "#
17417 .unindent(),
17418 );
17419
17420 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
17421 executor.run_until_parked();
17422
17423 cx.assert_state_with_diff(
17424 r#"
17425 use some::mod1;
17426 use some::mod2;
17427
17428 const A: u32 = 42;
17429 + const B: u32 = 42;
17430 + const C: u32 = 42;
17431 + const D: u32 = 42;
17432 + const E: u32 = 42;
17433 + ˇ
17434
17435 fn main() {
17436 println!("hello");
17437
17438 println!("world");
17439 }
17440 "#
17441 .unindent(),
17442 );
17443
17444 cx.update_editor(|editor, window, cx| {
17445 editor.delete_line(&DeleteLine, window, cx);
17446 });
17447 executor.run_until_parked();
17448
17449 cx.assert_state_with_diff(
17450 r#"
17451 use some::mod1;
17452 use some::mod2;
17453
17454 const A: u32 = 42;
17455 + const B: u32 = 42;
17456 + const C: u32 = 42;
17457 + const D: u32 = 42;
17458 + const E: u32 = 42;
17459 ˇ
17460 fn main() {
17461 println!("hello");
17462
17463 println!("world");
17464 }
17465 "#
17466 .unindent(),
17467 );
17468
17469 cx.update_editor(|editor, window, cx| {
17470 editor.move_up(&MoveUp, window, cx);
17471 editor.delete_line(&DeleteLine, window, cx);
17472 editor.move_up(&MoveUp, window, cx);
17473 editor.delete_line(&DeleteLine, window, cx);
17474 editor.move_up(&MoveUp, window, cx);
17475 editor.delete_line(&DeleteLine, window, cx);
17476 });
17477 executor.run_until_parked();
17478 cx.assert_state_with_diff(
17479 r#"
17480 use some::mod1;
17481 use some::mod2;
17482
17483 const A: u32 = 42;
17484 + const B: u32 = 42;
17485 ˇ
17486 fn main() {
17487 println!("hello");
17488
17489 println!("world");
17490 }
17491 "#
17492 .unindent(),
17493 );
17494
17495 cx.update_editor(|editor, window, cx| {
17496 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
17497 editor.delete_line(&DeleteLine, window, cx);
17498 });
17499 executor.run_until_parked();
17500 cx.assert_state_with_diff(
17501 r#"
17502 ˇ
17503 fn main() {
17504 println!("hello");
17505
17506 println!("world");
17507 }
17508 "#
17509 .unindent(),
17510 );
17511}
17512
17513#[gpui::test]
17514async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
17515 init_test(cx, |_| {});
17516
17517 let mut cx = EditorTestContext::new(cx).await;
17518 cx.set_head_text(indoc! { "
17519 one
17520 two
17521 three
17522 four
17523 five
17524 "
17525 });
17526 cx.set_state(indoc! { "
17527 one
17528 ˇthree
17529 five
17530 "});
17531 cx.run_until_parked();
17532 cx.update_editor(|editor, window, cx| {
17533 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17534 });
17535 cx.assert_state_with_diff(
17536 indoc! { "
17537 one
17538 - two
17539 ˇthree
17540 - four
17541 five
17542 "}
17543 .to_string(),
17544 );
17545 cx.update_editor(|editor, window, cx| {
17546 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17547 });
17548
17549 cx.assert_state_with_diff(
17550 indoc! { "
17551 one
17552 ˇthree
17553 five
17554 "}
17555 .to_string(),
17556 );
17557
17558 cx.set_state(indoc! { "
17559 one
17560 ˇTWO
17561 three
17562 four
17563 five
17564 "});
17565 cx.run_until_parked();
17566 cx.update_editor(|editor, window, cx| {
17567 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17568 });
17569
17570 cx.assert_state_with_diff(
17571 indoc! { "
17572 one
17573 - two
17574 + ˇTWO
17575 three
17576 four
17577 five
17578 "}
17579 .to_string(),
17580 );
17581 cx.update_editor(|editor, window, cx| {
17582 editor.move_up(&Default::default(), window, cx);
17583 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17584 });
17585 cx.assert_state_with_diff(
17586 indoc! { "
17587 one
17588 ˇTWO
17589 three
17590 four
17591 five
17592 "}
17593 .to_string(),
17594 );
17595}
17596
17597#[gpui::test]
17598async fn test_edits_around_expanded_deletion_hunks(
17599 executor: BackgroundExecutor,
17600 cx: &mut TestAppContext,
17601) {
17602 init_test(cx, |_| {});
17603
17604 let mut cx = EditorTestContext::new(cx).await;
17605
17606 let diff_base = r#"
17607 use some::mod1;
17608 use some::mod2;
17609
17610 const A: u32 = 42;
17611 const B: u32 = 42;
17612 const C: u32 = 42;
17613
17614
17615 fn main() {
17616 println!("hello");
17617
17618 println!("world");
17619 }
17620 "#
17621 .unindent();
17622 executor.run_until_parked();
17623 cx.set_state(
17624 &r#"
17625 use some::mod1;
17626 use some::mod2;
17627
17628 ˇconst B: u32 = 42;
17629 const C: u32 = 42;
17630
17631
17632 fn main() {
17633 println!("hello");
17634
17635 println!("world");
17636 }
17637 "#
17638 .unindent(),
17639 );
17640
17641 cx.set_head_text(&diff_base);
17642 executor.run_until_parked();
17643
17644 cx.update_editor(|editor, window, cx| {
17645 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17646 });
17647 executor.run_until_parked();
17648
17649 cx.assert_state_with_diff(
17650 r#"
17651 use some::mod1;
17652 use some::mod2;
17653
17654 - const A: u32 = 42;
17655 ˇconst B: u32 = 42;
17656 const C: u32 = 42;
17657
17658
17659 fn main() {
17660 println!("hello");
17661
17662 println!("world");
17663 }
17664 "#
17665 .unindent(),
17666 );
17667
17668 cx.update_editor(|editor, window, cx| {
17669 editor.delete_line(&DeleteLine, window, cx);
17670 });
17671 executor.run_until_parked();
17672 cx.assert_state_with_diff(
17673 r#"
17674 use some::mod1;
17675 use some::mod2;
17676
17677 - const A: u32 = 42;
17678 - const B: u32 = 42;
17679 ˇconst C: u32 = 42;
17680
17681
17682 fn main() {
17683 println!("hello");
17684
17685 println!("world");
17686 }
17687 "#
17688 .unindent(),
17689 );
17690
17691 cx.update_editor(|editor, window, cx| {
17692 editor.delete_line(&DeleteLine, window, cx);
17693 });
17694 executor.run_until_parked();
17695 cx.assert_state_with_diff(
17696 r#"
17697 use some::mod1;
17698 use some::mod2;
17699
17700 - const A: u32 = 42;
17701 - const B: u32 = 42;
17702 - const C: u32 = 42;
17703 ˇ
17704
17705 fn main() {
17706 println!("hello");
17707
17708 println!("world");
17709 }
17710 "#
17711 .unindent(),
17712 );
17713
17714 cx.update_editor(|editor, window, cx| {
17715 editor.handle_input("replacement", window, cx);
17716 });
17717 executor.run_until_parked();
17718 cx.assert_state_with_diff(
17719 r#"
17720 use some::mod1;
17721 use some::mod2;
17722
17723 - const A: u32 = 42;
17724 - const B: u32 = 42;
17725 - const C: u32 = 42;
17726 -
17727 + replacementˇ
17728
17729 fn main() {
17730 println!("hello");
17731
17732 println!("world");
17733 }
17734 "#
17735 .unindent(),
17736 );
17737}
17738
17739#[gpui::test]
17740async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17741 init_test(cx, |_| {});
17742
17743 let mut cx = EditorTestContext::new(cx).await;
17744
17745 let base_text = r#"
17746 one
17747 two
17748 three
17749 four
17750 five
17751 "#
17752 .unindent();
17753 executor.run_until_parked();
17754 cx.set_state(
17755 &r#"
17756 one
17757 two
17758 fˇour
17759 five
17760 "#
17761 .unindent(),
17762 );
17763
17764 cx.set_head_text(&base_text);
17765 executor.run_until_parked();
17766
17767 cx.update_editor(|editor, window, cx| {
17768 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17769 });
17770 executor.run_until_parked();
17771
17772 cx.assert_state_with_diff(
17773 r#"
17774 one
17775 two
17776 - three
17777 fˇour
17778 five
17779 "#
17780 .unindent(),
17781 );
17782
17783 cx.update_editor(|editor, window, cx| {
17784 editor.backspace(&Backspace, window, cx);
17785 editor.backspace(&Backspace, window, cx);
17786 });
17787 executor.run_until_parked();
17788 cx.assert_state_with_diff(
17789 r#"
17790 one
17791 two
17792 - threeˇ
17793 - four
17794 + our
17795 five
17796 "#
17797 .unindent(),
17798 );
17799}
17800
17801#[gpui::test]
17802async fn test_edit_after_expanded_modification_hunk(
17803 executor: BackgroundExecutor,
17804 cx: &mut TestAppContext,
17805) {
17806 init_test(cx, |_| {});
17807
17808 let mut cx = EditorTestContext::new(cx).await;
17809
17810 let diff_base = r#"
17811 use some::mod1;
17812 use some::mod2;
17813
17814 const A: u32 = 42;
17815 const B: u32 = 42;
17816 const C: u32 = 42;
17817 const D: u32 = 42;
17818
17819
17820 fn main() {
17821 println!("hello");
17822
17823 println!("world");
17824 }"#
17825 .unindent();
17826
17827 cx.set_state(
17828 &r#"
17829 use some::mod1;
17830 use some::mod2;
17831
17832 const A: u32 = 42;
17833 const B: u32 = 42;
17834 const C: u32 = 43ˇ
17835 const D: u32 = 42;
17836
17837
17838 fn main() {
17839 println!("hello");
17840
17841 println!("world");
17842 }"#
17843 .unindent(),
17844 );
17845
17846 cx.set_head_text(&diff_base);
17847 executor.run_until_parked();
17848 cx.update_editor(|editor, window, cx| {
17849 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17850 });
17851 executor.run_until_parked();
17852
17853 cx.assert_state_with_diff(
17854 r#"
17855 use some::mod1;
17856 use some::mod2;
17857
17858 const A: u32 = 42;
17859 const B: u32 = 42;
17860 - const C: u32 = 42;
17861 + const C: u32 = 43ˇ
17862 const D: u32 = 42;
17863
17864
17865 fn main() {
17866 println!("hello");
17867
17868 println!("world");
17869 }"#
17870 .unindent(),
17871 );
17872
17873 cx.update_editor(|editor, window, cx| {
17874 editor.handle_input("\nnew_line\n", window, cx);
17875 });
17876 executor.run_until_parked();
17877
17878 cx.assert_state_with_diff(
17879 r#"
17880 use some::mod1;
17881 use some::mod2;
17882
17883 const A: u32 = 42;
17884 const B: u32 = 42;
17885 - const C: u32 = 42;
17886 + const C: u32 = 43
17887 + new_line
17888 + ˇ
17889 const D: u32 = 42;
17890
17891
17892 fn main() {
17893 println!("hello");
17894
17895 println!("world");
17896 }"#
17897 .unindent(),
17898 );
17899}
17900
17901#[gpui::test]
17902async fn test_stage_and_unstage_added_file_hunk(
17903 executor: BackgroundExecutor,
17904 cx: &mut TestAppContext,
17905) {
17906 init_test(cx, |_| {});
17907
17908 let mut cx = EditorTestContext::new(cx).await;
17909 cx.update_editor(|editor, _, cx| {
17910 editor.set_expand_all_diff_hunks(cx);
17911 });
17912
17913 let working_copy = r#"
17914 ˇfn main() {
17915 println!("hello, world!");
17916 }
17917 "#
17918 .unindent();
17919
17920 cx.set_state(&working_copy);
17921 executor.run_until_parked();
17922
17923 cx.assert_state_with_diff(
17924 r#"
17925 + ˇfn main() {
17926 + println!("hello, world!");
17927 + }
17928 "#
17929 .unindent(),
17930 );
17931 cx.assert_index_text(None);
17932
17933 cx.update_editor(|editor, window, cx| {
17934 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17935 });
17936 executor.run_until_parked();
17937 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
17938 cx.assert_state_with_diff(
17939 r#"
17940 + ˇfn main() {
17941 + println!("hello, world!");
17942 + }
17943 "#
17944 .unindent(),
17945 );
17946
17947 cx.update_editor(|editor, window, cx| {
17948 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17949 });
17950 executor.run_until_parked();
17951 cx.assert_index_text(None);
17952}
17953
17954async fn setup_indent_guides_editor(
17955 text: &str,
17956 cx: &mut TestAppContext,
17957) -> (BufferId, EditorTestContext) {
17958 init_test(cx, |_| {});
17959
17960 let mut cx = EditorTestContext::new(cx).await;
17961
17962 let buffer_id = cx.update_editor(|editor, window, cx| {
17963 editor.set_text(text, window, cx);
17964 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
17965
17966 buffer_ids[0]
17967 });
17968
17969 (buffer_id, cx)
17970}
17971
17972fn assert_indent_guides(
17973 range: Range<u32>,
17974 expected: Vec<IndentGuide>,
17975 active_indices: Option<Vec<usize>>,
17976 cx: &mut EditorTestContext,
17977) {
17978 let indent_guides = cx.update_editor(|editor, window, cx| {
17979 let snapshot = editor.snapshot(window, cx).display_snapshot;
17980 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
17981 editor,
17982 MultiBufferRow(range.start)..MultiBufferRow(range.end),
17983 true,
17984 &snapshot,
17985 cx,
17986 );
17987
17988 indent_guides.sort_by(|a, b| {
17989 a.depth.cmp(&b.depth).then(
17990 a.start_row
17991 .cmp(&b.start_row)
17992 .then(a.end_row.cmp(&b.end_row)),
17993 )
17994 });
17995 indent_guides
17996 });
17997
17998 if let Some(expected) = active_indices {
17999 let active_indices = cx.update_editor(|editor, window, cx| {
18000 let snapshot = editor.snapshot(window, cx).display_snapshot;
18001 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
18002 });
18003
18004 assert_eq!(
18005 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
18006 expected,
18007 "Active indent guide indices do not match"
18008 );
18009 }
18010
18011 assert_eq!(indent_guides, expected, "Indent guides do not match");
18012}
18013
18014fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
18015 IndentGuide {
18016 buffer_id,
18017 start_row: MultiBufferRow(start_row),
18018 end_row: MultiBufferRow(end_row),
18019 depth,
18020 tab_size: 4,
18021 settings: IndentGuideSettings {
18022 enabled: true,
18023 line_width: 1,
18024 active_line_width: 1,
18025 ..Default::default()
18026 },
18027 }
18028}
18029
18030#[gpui::test]
18031async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
18032 let (buffer_id, mut cx) = setup_indent_guides_editor(
18033 &"
18034 fn main() {
18035 let a = 1;
18036 }"
18037 .unindent(),
18038 cx,
18039 )
18040 .await;
18041
18042 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18043}
18044
18045#[gpui::test]
18046async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
18047 let (buffer_id, mut cx) = setup_indent_guides_editor(
18048 &"
18049 fn main() {
18050 let a = 1;
18051 let b = 2;
18052 }"
18053 .unindent(),
18054 cx,
18055 )
18056 .await;
18057
18058 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
18059}
18060
18061#[gpui::test]
18062async fn test_indent_guide_nested(cx: &mut TestAppContext) {
18063 let (buffer_id, mut cx) = setup_indent_guides_editor(
18064 &"
18065 fn main() {
18066 let a = 1;
18067 if a == 3 {
18068 let b = 2;
18069 } else {
18070 let c = 3;
18071 }
18072 }"
18073 .unindent(),
18074 cx,
18075 )
18076 .await;
18077
18078 assert_indent_guides(
18079 0..8,
18080 vec![
18081 indent_guide(buffer_id, 1, 6, 0),
18082 indent_guide(buffer_id, 3, 3, 1),
18083 indent_guide(buffer_id, 5, 5, 1),
18084 ],
18085 None,
18086 &mut cx,
18087 );
18088}
18089
18090#[gpui::test]
18091async fn test_indent_guide_tab(cx: &mut TestAppContext) {
18092 let (buffer_id, mut cx) = setup_indent_guides_editor(
18093 &"
18094 fn main() {
18095 let a = 1;
18096 let b = 2;
18097 let c = 3;
18098 }"
18099 .unindent(),
18100 cx,
18101 )
18102 .await;
18103
18104 assert_indent_guides(
18105 0..5,
18106 vec![
18107 indent_guide(buffer_id, 1, 3, 0),
18108 indent_guide(buffer_id, 2, 2, 1),
18109 ],
18110 None,
18111 &mut cx,
18112 );
18113}
18114
18115#[gpui::test]
18116async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
18117 let (buffer_id, mut cx) = setup_indent_guides_editor(
18118 &"
18119 fn main() {
18120 let a = 1;
18121
18122 let c = 3;
18123 }"
18124 .unindent(),
18125 cx,
18126 )
18127 .await;
18128
18129 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
18130}
18131
18132#[gpui::test]
18133async fn test_indent_guide_complex(cx: &mut TestAppContext) {
18134 let (buffer_id, mut cx) = setup_indent_guides_editor(
18135 &"
18136 fn main() {
18137 let a = 1;
18138
18139 let c = 3;
18140
18141 if a == 3 {
18142 let b = 2;
18143 } else {
18144 let c = 3;
18145 }
18146 }"
18147 .unindent(),
18148 cx,
18149 )
18150 .await;
18151
18152 assert_indent_guides(
18153 0..11,
18154 vec![
18155 indent_guide(buffer_id, 1, 9, 0),
18156 indent_guide(buffer_id, 6, 6, 1),
18157 indent_guide(buffer_id, 8, 8, 1),
18158 ],
18159 None,
18160 &mut cx,
18161 );
18162}
18163
18164#[gpui::test]
18165async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
18166 let (buffer_id, mut cx) = setup_indent_guides_editor(
18167 &"
18168 fn main() {
18169 let a = 1;
18170
18171 let c = 3;
18172
18173 if a == 3 {
18174 let b = 2;
18175 } else {
18176 let c = 3;
18177 }
18178 }"
18179 .unindent(),
18180 cx,
18181 )
18182 .await;
18183
18184 assert_indent_guides(
18185 1..11,
18186 vec![
18187 indent_guide(buffer_id, 1, 9, 0),
18188 indent_guide(buffer_id, 6, 6, 1),
18189 indent_guide(buffer_id, 8, 8, 1),
18190 ],
18191 None,
18192 &mut cx,
18193 );
18194}
18195
18196#[gpui::test]
18197async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
18198 let (buffer_id, mut cx) = setup_indent_guides_editor(
18199 &"
18200 fn main() {
18201 let a = 1;
18202
18203 let c = 3;
18204
18205 if a == 3 {
18206 let b = 2;
18207 } else {
18208 let c = 3;
18209 }
18210 }"
18211 .unindent(),
18212 cx,
18213 )
18214 .await;
18215
18216 assert_indent_guides(
18217 1..10,
18218 vec![
18219 indent_guide(buffer_id, 1, 9, 0),
18220 indent_guide(buffer_id, 6, 6, 1),
18221 indent_guide(buffer_id, 8, 8, 1),
18222 ],
18223 None,
18224 &mut cx,
18225 );
18226}
18227
18228#[gpui::test]
18229async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
18230 let (buffer_id, mut cx) = setup_indent_guides_editor(
18231 &"
18232 fn main() {
18233 if a {
18234 b(
18235 c,
18236 d,
18237 )
18238 } else {
18239 e(
18240 f
18241 )
18242 }
18243 }"
18244 .unindent(),
18245 cx,
18246 )
18247 .await;
18248
18249 assert_indent_guides(
18250 0..11,
18251 vec![
18252 indent_guide(buffer_id, 1, 10, 0),
18253 indent_guide(buffer_id, 2, 5, 1),
18254 indent_guide(buffer_id, 7, 9, 1),
18255 indent_guide(buffer_id, 3, 4, 2),
18256 indent_guide(buffer_id, 8, 8, 2),
18257 ],
18258 None,
18259 &mut cx,
18260 );
18261
18262 cx.update_editor(|editor, window, cx| {
18263 editor.fold_at(MultiBufferRow(2), window, cx);
18264 assert_eq!(
18265 editor.display_text(cx),
18266 "
18267 fn main() {
18268 if a {
18269 b(⋯
18270 )
18271 } else {
18272 e(
18273 f
18274 )
18275 }
18276 }"
18277 .unindent()
18278 );
18279 });
18280
18281 assert_indent_guides(
18282 0..11,
18283 vec![
18284 indent_guide(buffer_id, 1, 10, 0),
18285 indent_guide(buffer_id, 2, 5, 1),
18286 indent_guide(buffer_id, 7, 9, 1),
18287 indent_guide(buffer_id, 8, 8, 2),
18288 ],
18289 None,
18290 &mut cx,
18291 );
18292}
18293
18294#[gpui::test]
18295async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
18296 let (buffer_id, mut cx) = setup_indent_guides_editor(
18297 &"
18298 block1
18299 block2
18300 block3
18301 block4
18302 block2
18303 block1
18304 block1"
18305 .unindent(),
18306 cx,
18307 )
18308 .await;
18309
18310 assert_indent_guides(
18311 1..10,
18312 vec![
18313 indent_guide(buffer_id, 1, 4, 0),
18314 indent_guide(buffer_id, 2, 3, 1),
18315 indent_guide(buffer_id, 3, 3, 2),
18316 ],
18317 None,
18318 &mut cx,
18319 );
18320}
18321
18322#[gpui::test]
18323async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
18324 let (buffer_id, mut cx) = setup_indent_guides_editor(
18325 &"
18326 block1
18327 block2
18328 block3
18329
18330 block1
18331 block1"
18332 .unindent(),
18333 cx,
18334 )
18335 .await;
18336
18337 assert_indent_guides(
18338 0..6,
18339 vec![
18340 indent_guide(buffer_id, 1, 2, 0),
18341 indent_guide(buffer_id, 2, 2, 1),
18342 ],
18343 None,
18344 &mut cx,
18345 );
18346}
18347
18348#[gpui::test]
18349async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
18350 let (buffer_id, mut cx) = setup_indent_guides_editor(
18351 &"
18352 function component() {
18353 \treturn (
18354 \t\t\t
18355 \t\t<div>
18356 \t\t\t<abc></abc>
18357 \t\t</div>
18358 \t)
18359 }"
18360 .unindent(),
18361 cx,
18362 )
18363 .await;
18364
18365 assert_indent_guides(
18366 0..8,
18367 vec![
18368 indent_guide(buffer_id, 1, 6, 0),
18369 indent_guide(buffer_id, 2, 5, 1),
18370 indent_guide(buffer_id, 4, 4, 2),
18371 ],
18372 None,
18373 &mut cx,
18374 );
18375}
18376
18377#[gpui::test]
18378async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
18379 let (buffer_id, mut cx) = setup_indent_guides_editor(
18380 &"
18381 function component() {
18382 \treturn (
18383 \t
18384 \t\t<div>
18385 \t\t\t<abc></abc>
18386 \t\t</div>
18387 \t)
18388 }"
18389 .unindent(),
18390 cx,
18391 )
18392 .await;
18393
18394 assert_indent_guides(
18395 0..8,
18396 vec![
18397 indent_guide(buffer_id, 1, 6, 0),
18398 indent_guide(buffer_id, 2, 5, 1),
18399 indent_guide(buffer_id, 4, 4, 2),
18400 ],
18401 None,
18402 &mut cx,
18403 );
18404}
18405
18406#[gpui::test]
18407async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
18408 let (buffer_id, mut cx) = setup_indent_guides_editor(
18409 &"
18410 block1
18411
18412
18413
18414 block2
18415 "
18416 .unindent(),
18417 cx,
18418 )
18419 .await;
18420
18421 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18422}
18423
18424#[gpui::test]
18425async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
18426 let (buffer_id, mut cx) = setup_indent_guides_editor(
18427 &"
18428 def a:
18429 \tb = 3
18430 \tif True:
18431 \t\tc = 4
18432 \t\td = 5
18433 \tprint(b)
18434 "
18435 .unindent(),
18436 cx,
18437 )
18438 .await;
18439
18440 assert_indent_guides(
18441 0..6,
18442 vec![
18443 indent_guide(buffer_id, 1, 5, 0),
18444 indent_guide(buffer_id, 3, 4, 1),
18445 ],
18446 None,
18447 &mut cx,
18448 );
18449}
18450
18451#[gpui::test]
18452async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
18453 let (buffer_id, mut cx) = setup_indent_guides_editor(
18454 &"
18455 fn main() {
18456 let a = 1;
18457 }"
18458 .unindent(),
18459 cx,
18460 )
18461 .await;
18462
18463 cx.update_editor(|editor, window, cx| {
18464 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18465 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18466 });
18467 });
18468
18469 assert_indent_guides(
18470 0..3,
18471 vec![indent_guide(buffer_id, 1, 1, 0)],
18472 Some(vec![0]),
18473 &mut cx,
18474 );
18475}
18476
18477#[gpui::test]
18478async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
18479 let (buffer_id, mut cx) = setup_indent_guides_editor(
18480 &"
18481 fn main() {
18482 if 1 == 2 {
18483 let a = 1;
18484 }
18485 }"
18486 .unindent(),
18487 cx,
18488 )
18489 .await;
18490
18491 cx.update_editor(|editor, window, cx| {
18492 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18493 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18494 });
18495 });
18496
18497 assert_indent_guides(
18498 0..4,
18499 vec![
18500 indent_guide(buffer_id, 1, 3, 0),
18501 indent_guide(buffer_id, 2, 2, 1),
18502 ],
18503 Some(vec![1]),
18504 &mut cx,
18505 );
18506
18507 cx.update_editor(|editor, window, cx| {
18508 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18509 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18510 });
18511 });
18512
18513 assert_indent_guides(
18514 0..4,
18515 vec![
18516 indent_guide(buffer_id, 1, 3, 0),
18517 indent_guide(buffer_id, 2, 2, 1),
18518 ],
18519 Some(vec![1]),
18520 &mut cx,
18521 );
18522
18523 cx.update_editor(|editor, window, cx| {
18524 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18525 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
18526 });
18527 });
18528
18529 assert_indent_guides(
18530 0..4,
18531 vec![
18532 indent_guide(buffer_id, 1, 3, 0),
18533 indent_guide(buffer_id, 2, 2, 1),
18534 ],
18535 Some(vec![0]),
18536 &mut cx,
18537 );
18538}
18539
18540#[gpui::test]
18541async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
18542 let (buffer_id, mut cx) = setup_indent_guides_editor(
18543 &"
18544 fn main() {
18545 let a = 1;
18546
18547 let b = 2;
18548 }"
18549 .unindent(),
18550 cx,
18551 )
18552 .await;
18553
18554 cx.update_editor(|editor, window, cx| {
18555 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18556 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18557 });
18558 });
18559
18560 assert_indent_guides(
18561 0..5,
18562 vec![indent_guide(buffer_id, 1, 3, 0)],
18563 Some(vec![0]),
18564 &mut cx,
18565 );
18566}
18567
18568#[gpui::test]
18569async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
18570 let (buffer_id, mut cx) = setup_indent_guides_editor(
18571 &"
18572 def m:
18573 a = 1
18574 pass"
18575 .unindent(),
18576 cx,
18577 )
18578 .await;
18579
18580 cx.update_editor(|editor, window, cx| {
18581 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18582 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18583 });
18584 });
18585
18586 assert_indent_guides(
18587 0..3,
18588 vec![indent_guide(buffer_id, 1, 2, 0)],
18589 Some(vec![0]),
18590 &mut cx,
18591 );
18592}
18593
18594#[gpui::test]
18595async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
18596 init_test(cx, |_| {});
18597 let mut cx = EditorTestContext::new(cx).await;
18598 let text = indoc! {
18599 "
18600 impl A {
18601 fn b() {
18602 0;
18603 3;
18604 5;
18605 6;
18606 7;
18607 }
18608 }
18609 "
18610 };
18611 let base_text = indoc! {
18612 "
18613 impl A {
18614 fn b() {
18615 0;
18616 1;
18617 2;
18618 3;
18619 4;
18620 }
18621 fn c() {
18622 5;
18623 6;
18624 7;
18625 }
18626 }
18627 "
18628 };
18629
18630 cx.update_editor(|editor, window, cx| {
18631 editor.set_text(text, window, cx);
18632
18633 editor.buffer().update(cx, |multibuffer, cx| {
18634 let buffer = multibuffer.as_singleton().unwrap();
18635 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
18636
18637 multibuffer.set_all_diff_hunks_expanded(cx);
18638 multibuffer.add_diff(diff, cx);
18639
18640 buffer.read(cx).remote_id()
18641 })
18642 });
18643 cx.run_until_parked();
18644
18645 cx.assert_state_with_diff(
18646 indoc! { "
18647 impl A {
18648 fn b() {
18649 0;
18650 - 1;
18651 - 2;
18652 3;
18653 - 4;
18654 - }
18655 - fn c() {
18656 5;
18657 6;
18658 7;
18659 }
18660 }
18661 ˇ"
18662 }
18663 .to_string(),
18664 );
18665
18666 let mut actual_guides = cx.update_editor(|editor, window, cx| {
18667 editor
18668 .snapshot(window, cx)
18669 .buffer_snapshot
18670 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
18671 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
18672 .collect::<Vec<_>>()
18673 });
18674 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
18675 assert_eq!(
18676 actual_guides,
18677 vec![
18678 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
18679 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
18680 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
18681 ]
18682 );
18683}
18684
18685#[gpui::test]
18686async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18687 init_test(cx, |_| {});
18688 let mut cx = EditorTestContext::new(cx).await;
18689
18690 let diff_base = r#"
18691 a
18692 b
18693 c
18694 "#
18695 .unindent();
18696
18697 cx.set_state(
18698 &r#"
18699 ˇA
18700 b
18701 C
18702 "#
18703 .unindent(),
18704 );
18705 cx.set_head_text(&diff_base);
18706 cx.update_editor(|editor, window, cx| {
18707 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18708 });
18709 executor.run_until_parked();
18710
18711 let both_hunks_expanded = r#"
18712 - a
18713 + ˇA
18714 b
18715 - c
18716 + C
18717 "#
18718 .unindent();
18719
18720 cx.assert_state_with_diff(both_hunks_expanded.clone());
18721
18722 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18723 let snapshot = editor.snapshot(window, cx);
18724 let hunks = editor
18725 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18726 .collect::<Vec<_>>();
18727 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18728 let buffer_id = hunks[0].buffer_id;
18729 hunks
18730 .into_iter()
18731 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18732 .collect::<Vec<_>>()
18733 });
18734 assert_eq!(hunk_ranges.len(), 2);
18735
18736 cx.update_editor(|editor, _, cx| {
18737 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18738 });
18739 executor.run_until_parked();
18740
18741 let second_hunk_expanded = r#"
18742 ˇA
18743 b
18744 - c
18745 + C
18746 "#
18747 .unindent();
18748
18749 cx.assert_state_with_diff(second_hunk_expanded);
18750
18751 cx.update_editor(|editor, _, cx| {
18752 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18753 });
18754 executor.run_until_parked();
18755
18756 cx.assert_state_with_diff(both_hunks_expanded.clone());
18757
18758 cx.update_editor(|editor, _, cx| {
18759 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18760 });
18761 executor.run_until_parked();
18762
18763 let first_hunk_expanded = r#"
18764 - a
18765 + ˇA
18766 b
18767 C
18768 "#
18769 .unindent();
18770
18771 cx.assert_state_with_diff(first_hunk_expanded);
18772
18773 cx.update_editor(|editor, _, cx| {
18774 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18775 });
18776 executor.run_until_parked();
18777
18778 cx.assert_state_with_diff(both_hunks_expanded);
18779
18780 cx.set_state(
18781 &r#"
18782 ˇA
18783 b
18784 "#
18785 .unindent(),
18786 );
18787 cx.run_until_parked();
18788
18789 // TODO this cursor position seems bad
18790 cx.assert_state_with_diff(
18791 r#"
18792 - ˇa
18793 + A
18794 b
18795 "#
18796 .unindent(),
18797 );
18798
18799 cx.update_editor(|editor, window, cx| {
18800 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18801 });
18802
18803 cx.assert_state_with_diff(
18804 r#"
18805 - ˇa
18806 + A
18807 b
18808 - c
18809 "#
18810 .unindent(),
18811 );
18812
18813 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18814 let snapshot = editor.snapshot(window, cx);
18815 let hunks = editor
18816 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18817 .collect::<Vec<_>>();
18818 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18819 let buffer_id = hunks[0].buffer_id;
18820 hunks
18821 .into_iter()
18822 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18823 .collect::<Vec<_>>()
18824 });
18825 assert_eq!(hunk_ranges.len(), 2);
18826
18827 cx.update_editor(|editor, _, cx| {
18828 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18829 });
18830 executor.run_until_parked();
18831
18832 cx.assert_state_with_diff(
18833 r#"
18834 - ˇa
18835 + A
18836 b
18837 "#
18838 .unindent(),
18839 );
18840}
18841
18842#[gpui::test]
18843async fn test_toggle_deletion_hunk_at_start_of_file(
18844 executor: BackgroundExecutor,
18845 cx: &mut TestAppContext,
18846) {
18847 init_test(cx, |_| {});
18848 let mut cx = EditorTestContext::new(cx).await;
18849
18850 let diff_base = r#"
18851 a
18852 b
18853 c
18854 "#
18855 .unindent();
18856
18857 cx.set_state(
18858 &r#"
18859 ˇb
18860 c
18861 "#
18862 .unindent(),
18863 );
18864 cx.set_head_text(&diff_base);
18865 cx.update_editor(|editor, window, cx| {
18866 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18867 });
18868 executor.run_until_parked();
18869
18870 let hunk_expanded = r#"
18871 - a
18872 ˇb
18873 c
18874 "#
18875 .unindent();
18876
18877 cx.assert_state_with_diff(hunk_expanded.clone());
18878
18879 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18880 let snapshot = editor.snapshot(window, cx);
18881 let hunks = editor
18882 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18883 .collect::<Vec<_>>();
18884 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18885 let buffer_id = hunks[0].buffer_id;
18886 hunks
18887 .into_iter()
18888 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18889 .collect::<Vec<_>>()
18890 });
18891 assert_eq!(hunk_ranges.len(), 1);
18892
18893 cx.update_editor(|editor, _, cx| {
18894 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18895 });
18896 executor.run_until_parked();
18897
18898 let hunk_collapsed = r#"
18899 ˇb
18900 c
18901 "#
18902 .unindent();
18903
18904 cx.assert_state_with_diff(hunk_collapsed);
18905
18906 cx.update_editor(|editor, _, cx| {
18907 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18908 });
18909 executor.run_until_parked();
18910
18911 cx.assert_state_with_diff(hunk_expanded.clone());
18912}
18913
18914#[gpui::test]
18915async fn test_display_diff_hunks(cx: &mut TestAppContext) {
18916 init_test(cx, |_| {});
18917
18918 let fs = FakeFs::new(cx.executor());
18919 fs.insert_tree(
18920 path!("/test"),
18921 json!({
18922 ".git": {},
18923 "file-1": "ONE\n",
18924 "file-2": "TWO\n",
18925 "file-3": "THREE\n",
18926 }),
18927 )
18928 .await;
18929
18930 fs.set_head_for_repo(
18931 path!("/test/.git").as_ref(),
18932 &[
18933 ("file-1".into(), "one\n".into()),
18934 ("file-2".into(), "two\n".into()),
18935 ("file-3".into(), "three\n".into()),
18936 ],
18937 "deadbeef",
18938 );
18939
18940 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
18941 let mut buffers = vec![];
18942 for i in 1..=3 {
18943 let buffer = project
18944 .update(cx, |project, cx| {
18945 let path = format!(path!("/test/file-{}"), i);
18946 project.open_local_buffer(path, cx)
18947 })
18948 .await
18949 .unwrap();
18950 buffers.push(buffer);
18951 }
18952
18953 let multibuffer = cx.new(|cx| {
18954 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
18955 multibuffer.set_all_diff_hunks_expanded(cx);
18956 for buffer in &buffers {
18957 let snapshot = buffer.read(cx).snapshot();
18958 multibuffer.set_excerpts_for_path(
18959 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
18960 buffer.clone(),
18961 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
18962 DEFAULT_MULTIBUFFER_CONTEXT,
18963 cx,
18964 );
18965 }
18966 multibuffer
18967 });
18968
18969 let editor = cx.add_window(|window, cx| {
18970 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
18971 });
18972 cx.run_until_parked();
18973
18974 let snapshot = editor
18975 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
18976 .unwrap();
18977 let hunks = snapshot
18978 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
18979 .map(|hunk| match hunk {
18980 DisplayDiffHunk::Unfolded {
18981 display_row_range, ..
18982 } => display_row_range,
18983 DisplayDiffHunk::Folded { .. } => unreachable!(),
18984 })
18985 .collect::<Vec<_>>();
18986 assert_eq!(
18987 hunks,
18988 [
18989 DisplayRow(2)..DisplayRow(4),
18990 DisplayRow(7)..DisplayRow(9),
18991 DisplayRow(12)..DisplayRow(14),
18992 ]
18993 );
18994}
18995
18996#[gpui::test]
18997async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
18998 init_test(cx, |_| {});
18999
19000 let mut cx = EditorTestContext::new(cx).await;
19001 cx.set_head_text(indoc! { "
19002 one
19003 two
19004 three
19005 four
19006 five
19007 "
19008 });
19009 cx.set_index_text(indoc! { "
19010 one
19011 two
19012 three
19013 four
19014 five
19015 "
19016 });
19017 cx.set_state(indoc! {"
19018 one
19019 TWO
19020 ˇTHREE
19021 FOUR
19022 five
19023 "});
19024 cx.run_until_parked();
19025 cx.update_editor(|editor, window, cx| {
19026 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19027 });
19028 cx.run_until_parked();
19029 cx.assert_index_text(Some(indoc! {"
19030 one
19031 TWO
19032 THREE
19033 FOUR
19034 five
19035 "}));
19036 cx.set_state(indoc! { "
19037 one
19038 TWO
19039 ˇTHREE-HUNDRED
19040 FOUR
19041 five
19042 "});
19043 cx.run_until_parked();
19044 cx.update_editor(|editor, window, cx| {
19045 let snapshot = editor.snapshot(window, cx);
19046 let hunks = editor
19047 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19048 .collect::<Vec<_>>();
19049 assert_eq!(hunks.len(), 1);
19050 assert_eq!(
19051 hunks[0].status(),
19052 DiffHunkStatus {
19053 kind: DiffHunkStatusKind::Modified,
19054 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
19055 }
19056 );
19057
19058 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19059 });
19060 cx.run_until_parked();
19061 cx.assert_index_text(Some(indoc! {"
19062 one
19063 TWO
19064 THREE-HUNDRED
19065 FOUR
19066 five
19067 "}));
19068}
19069
19070#[gpui::test]
19071fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
19072 init_test(cx, |_| {});
19073
19074 let editor = cx.add_window(|window, cx| {
19075 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
19076 build_editor(buffer, window, cx)
19077 });
19078
19079 let render_args = Arc::new(Mutex::new(None));
19080 let snapshot = editor
19081 .update(cx, |editor, window, cx| {
19082 let snapshot = editor.buffer().read(cx).snapshot(cx);
19083 let range =
19084 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
19085
19086 struct RenderArgs {
19087 row: MultiBufferRow,
19088 folded: bool,
19089 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
19090 }
19091
19092 let crease = Crease::inline(
19093 range,
19094 FoldPlaceholder::test(),
19095 {
19096 let toggle_callback = render_args.clone();
19097 move |row, folded, callback, _window, _cx| {
19098 *toggle_callback.lock() = Some(RenderArgs {
19099 row,
19100 folded,
19101 callback,
19102 });
19103 div()
19104 }
19105 },
19106 |_row, _folded, _window, _cx| div(),
19107 );
19108
19109 editor.insert_creases(Some(crease), cx);
19110 let snapshot = editor.snapshot(window, cx);
19111 let _div = snapshot.render_crease_toggle(
19112 MultiBufferRow(1),
19113 false,
19114 cx.entity().clone(),
19115 window,
19116 cx,
19117 );
19118 snapshot
19119 })
19120 .unwrap();
19121
19122 let render_args = render_args.lock().take().unwrap();
19123 assert_eq!(render_args.row, MultiBufferRow(1));
19124 assert!(!render_args.folded);
19125 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19126
19127 cx.update_window(*editor, |_, window, cx| {
19128 (render_args.callback)(true, window, cx)
19129 })
19130 .unwrap();
19131 let snapshot = editor
19132 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19133 .unwrap();
19134 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
19135
19136 cx.update_window(*editor, |_, window, cx| {
19137 (render_args.callback)(false, window, cx)
19138 })
19139 .unwrap();
19140 let snapshot = editor
19141 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19142 .unwrap();
19143 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19144}
19145
19146#[gpui::test]
19147async fn test_input_text(cx: &mut TestAppContext) {
19148 init_test(cx, |_| {});
19149 let mut cx = EditorTestContext::new(cx).await;
19150
19151 cx.set_state(
19152 &r#"ˇone
19153 two
19154
19155 three
19156 fourˇ
19157 five
19158
19159 siˇx"#
19160 .unindent(),
19161 );
19162
19163 cx.dispatch_action(HandleInput(String::new()));
19164 cx.assert_editor_state(
19165 &r#"ˇone
19166 two
19167
19168 three
19169 fourˇ
19170 five
19171
19172 siˇx"#
19173 .unindent(),
19174 );
19175
19176 cx.dispatch_action(HandleInput("AAAA".to_string()));
19177 cx.assert_editor_state(
19178 &r#"AAAAˇone
19179 two
19180
19181 three
19182 fourAAAAˇ
19183 five
19184
19185 siAAAAˇx"#
19186 .unindent(),
19187 );
19188}
19189
19190#[gpui::test]
19191async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
19192 init_test(cx, |_| {});
19193
19194 let mut cx = EditorTestContext::new(cx).await;
19195 cx.set_state(
19196 r#"let foo = 1;
19197let foo = 2;
19198let foo = 3;
19199let fooˇ = 4;
19200let foo = 5;
19201let foo = 6;
19202let foo = 7;
19203let foo = 8;
19204let foo = 9;
19205let foo = 10;
19206let foo = 11;
19207let foo = 12;
19208let foo = 13;
19209let foo = 14;
19210let foo = 15;"#,
19211 );
19212
19213 cx.update_editor(|e, window, cx| {
19214 assert_eq!(
19215 e.next_scroll_position,
19216 NextScrollCursorCenterTopBottom::Center,
19217 "Default next scroll direction is center",
19218 );
19219
19220 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19221 assert_eq!(
19222 e.next_scroll_position,
19223 NextScrollCursorCenterTopBottom::Top,
19224 "After center, next scroll direction should be top",
19225 );
19226
19227 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19228 assert_eq!(
19229 e.next_scroll_position,
19230 NextScrollCursorCenterTopBottom::Bottom,
19231 "After top, next scroll direction should be bottom",
19232 );
19233
19234 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19235 assert_eq!(
19236 e.next_scroll_position,
19237 NextScrollCursorCenterTopBottom::Center,
19238 "After bottom, scrolling should start over",
19239 );
19240
19241 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19242 assert_eq!(
19243 e.next_scroll_position,
19244 NextScrollCursorCenterTopBottom::Top,
19245 "Scrolling continues if retriggered fast enough"
19246 );
19247 });
19248
19249 cx.executor()
19250 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
19251 cx.executor().run_until_parked();
19252 cx.update_editor(|e, _, _| {
19253 assert_eq!(
19254 e.next_scroll_position,
19255 NextScrollCursorCenterTopBottom::Center,
19256 "If scrolling is not triggered fast enough, it should reset"
19257 );
19258 });
19259}
19260
19261#[gpui::test]
19262async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
19263 init_test(cx, |_| {});
19264 let mut cx = EditorLspTestContext::new_rust(
19265 lsp::ServerCapabilities {
19266 definition_provider: Some(lsp::OneOf::Left(true)),
19267 references_provider: Some(lsp::OneOf::Left(true)),
19268 ..lsp::ServerCapabilities::default()
19269 },
19270 cx,
19271 )
19272 .await;
19273
19274 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
19275 let go_to_definition = cx
19276 .lsp
19277 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19278 move |params, _| async move {
19279 if empty_go_to_definition {
19280 Ok(None)
19281 } else {
19282 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
19283 uri: params.text_document_position_params.text_document.uri,
19284 range: lsp::Range::new(
19285 lsp::Position::new(4, 3),
19286 lsp::Position::new(4, 6),
19287 ),
19288 })))
19289 }
19290 },
19291 );
19292 let references = cx
19293 .lsp
19294 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
19295 Ok(Some(vec![lsp::Location {
19296 uri: params.text_document_position.text_document.uri,
19297 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
19298 }]))
19299 });
19300 (go_to_definition, references)
19301 };
19302
19303 cx.set_state(
19304 &r#"fn one() {
19305 let mut a = ˇtwo();
19306 }
19307
19308 fn two() {}"#
19309 .unindent(),
19310 );
19311 set_up_lsp_handlers(false, &mut cx);
19312 let navigated = cx
19313 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19314 .await
19315 .expect("Failed to navigate to definition");
19316 assert_eq!(
19317 navigated,
19318 Navigated::Yes,
19319 "Should have navigated to definition from the GetDefinition response"
19320 );
19321 cx.assert_editor_state(
19322 &r#"fn one() {
19323 let mut a = two();
19324 }
19325
19326 fn «twoˇ»() {}"#
19327 .unindent(),
19328 );
19329
19330 let editors = cx.update_workspace(|workspace, _, cx| {
19331 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19332 });
19333 cx.update_editor(|_, _, test_editor_cx| {
19334 assert_eq!(
19335 editors.len(),
19336 1,
19337 "Initially, only one, test, editor should be open in the workspace"
19338 );
19339 assert_eq!(
19340 test_editor_cx.entity(),
19341 editors.last().expect("Asserted len is 1").clone()
19342 );
19343 });
19344
19345 set_up_lsp_handlers(true, &mut cx);
19346 let navigated = cx
19347 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19348 .await
19349 .expect("Failed to navigate to lookup references");
19350 assert_eq!(
19351 navigated,
19352 Navigated::Yes,
19353 "Should have navigated to references as a fallback after empty GoToDefinition response"
19354 );
19355 // We should not change the selections in the existing file,
19356 // if opening another milti buffer with the references
19357 cx.assert_editor_state(
19358 &r#"fn one() {
19359 let mut a = two();
19360 }
19361
19362 fn «twoˇ»() {}"#
19363 .unindent(),
19364 );
19365 let editors = cx.update_workspace(|workspace, _, cx| {
19366 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19367 });
19368 cx.update_editor(|_, _, test_editor_cx| {
19369 assert_eq!(
19370 editors.len(),
19371 2,
19372 "After falling back to references search, we open a new editor with the results"
19373 );
19374 let references_fallback_text = editors
19375 .into_iter()
19376 .find(|new_editor| *new_editor != test_editor_cx.entity())
19377 .expect("Should have one non-test editor now")
19378 .read(test_editor_cx)
19379 .text(test_editor_cx);
19380 assert_eq!(
19381 references_fallback_text, "fn one() {\n let mut a = two();\n}",
19382 "Should use the range from the references response and not the GoToDefinition one"
19383 );
19384 });
19385}
19386
19387#[gpui::test]
19388async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
19389 init_test(cx, |_| {});
19390 cx.update(|cx| {
19391 let mut editor_settings = EditorSettings::get_global(cx).clone();
19392 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
19393 EditorSettings::override_global(editor_settings, cx);
19394 });
19395 let mut cx = EditorLspTestContext::new_rust(
19396 lsp::ServerCapabilities {
19397 definition_provider: Some(lsp::OneOf::Left(true)),
19398 references_provider: Some(lsp::OneOf::Left(true)),
19399 ..lsp::ServerCapabilities::default()
19400 },
19401 cx,
19402 )
19403 .await;
19404 let original_state = r#"fn one() {
19405 let mut a = ˇtwo();
19406 }
19407
19408 fn two() {}"#
19409 .unindent();
19410 cx.set_state(&original_state);
19411
19412 let mut go_to_definition = cx
19413 .lsp
19414 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19415 move |_, _| async move { Ok(None) },
19416 );
19417 let _references = cx
19418 .lsp
19419 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
19420 panic!("Should not call for references with no go to definition fallback")
19421 });
19422
19423 let navigated = cx
19424 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19425 .await
19426 .expect("Failed to navigate to lookup references");
19427 go_to_definition
19428 .next()
19429 .await
19430 .expect("Should have called the go_to_definition handler");
19431
19432 assert_eq!(
19433 navigated,
19434 Navigated::No,
19435 "Should have navigated to references as a fallback after empty GoToDefinition response"
19436 );
19437 cx.assert_editor_state(&original_state);
19438 let editors = cx.update_workspace(|workspace, _, cx| {
19439 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19440 });
19441 cx.update_editor(|_, _, _| {
19442 assert_eq!(
19443 editors.len(),
19444 1,
19445 "After unsuccessful fallback, no other editor should have been opened"
19446 );
19447 });
19448}
19449
19450#[gpui::test]
19451async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
19452 init_test(cx, |_| {});
19453
19454 let language = Arc::new(Language::new(
19455 LanguageConfig::default(),
19456 Some(tree_sitter_rust::LANGUAGE.into()),
19457 ));
19458
19459 let text = r#"
19460 #[cfg(test)]
19461 mod tests() {
19462 #[test]
19463 fn runnable_1() {
19464 let a = 1;
19465 }
19466
19467 #[test]
19468 fn runnable_2() {
19469 let a = 1;
19470 let b = 2;
19471 }
19472 }
19473 "#
19474 .unindent();
19475
19476 let fs = FakeFs::new(cx.executor());
19477 fs.insert_file("/file.rs", Default::default()).await;
19478
19479 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19480 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19481 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19482 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
19483 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
19484
19485 let editor = cx.new_window_entity(|window, cx| {
19486 Editor::new(
19487 EditorMode::full(),
19488 multi_buffer,
19489 Some(project.clone()),
19490 window,
19491 cx,
19492 )
19493 });
19494
19495 editor.update_in(cx, |editor, window, cx| {
19496 let snapshot = editor.buffer().read(cx).snapshot(cx);
19497 editor.tasks.insert(
19498 (buffer.read(cx).remote_id(), 3),
19499 RunnableTasks {
19500 templates: vec![],
19501 offset: snapshot.anchor_before(43),
19502 column: 0,
19503 extra_variables: HashMap::default(),
19504 context_range: BufferOffset(43)..BufferOffset(85),
19505 },
19506 );
19507 editor.tasks.insert(
19508 (buffer.read(cx).remote_id(), 8),
19509 RunnableTasks {
19510 templates: vec![],
19511 offset: snapshot.anchor_before(86),
19512 column: 0,
19513 extra_variables: HashMap::default(),
19514 context_range: BufferOffset(86)..BufferOffset(191),
19515 },
19516 );
19517
19518 // Test finding task when cursor is inside function body
19519 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19520 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
19521 });
19522 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19523 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
19524
19525 // Test finding task when cursor is on function name
19526 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19527 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
19528 });
19529 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19530 assert_eq!(row, 8, "Should find task when cursor is on function name");
19531 });
19532}
19533
19534#[gpui::test]
19535async fn test_folding_buffers(cx: &mut TestAppContext) {
19536 init_test(cx, |_| {});
19537
19538 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19539 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
19540 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
19541
19542 let fs = FakeFs::new(cx.executor());
19543 fs.insert_tree(
19544 path!("/a"),
19545 json!({
19546 "first.rs": sample_text_1,
19547 "second.rs": sample_text_2,
19548 "third.rs": sample_text_3,
19549 }),
19550 )
19551 .await;
19552 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19553 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19554 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19555 let worktree = project.update(cx, |project, cx| {
19556 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19557 assert_eq!(worktrees.len(), 1);
19558 worktrees.pop().unwrap()
19559 });
19560 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19561
19562 let buffer_1 = project
19563 .update(cx, |project, cx| {
19564 project.open_buffer((worktree_id, "first.rs"), cx)
19565 })
19566 .await
19567 .unwrap();
19568 let buffer_2 = project
19569 .update(cx, |project, cx| {
19570 project.open_buffer((worktree_id, "second.rs"), cx)
19571 })
19572 .await
19573 .unwrap();
19574 let buffer_3 = project
19575 .update(cx, |project, cx| {
19576 project.open_buffer((worktree_id, "third.rs"), cx)
19577 })
19578 .await
19579 .unwrap();
19580
19581 let multi_buffer = cx.new(|cx| {
19582 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19583 multi_buffer.push_excerpts(
19584 buffer_1.clone(),
19585 [
19586 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19587 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19588 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19589 ],
19590 cx,
19591 );
19592 multi_buffer.push_excerpts(
19593 buffer_2.clone(),
19594 [
19595 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19596 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19597 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19598 ],
19599 cx,
19600 );
19601 multi_buffer.push_excerpts(
19602 buffer_3.clone(),
19603 [
19604 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19605 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19606 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19607 ],
19608 cx,
19609 );
19610 multi_buffer
19611 });
19612 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19613 Editor::new(
19614 EditorMode::full(),
19615 multi_buffer.clone(),
19616 Some(project.clone()),
19617 window,
19618 cx,
19619 )
19620 });
19621
19622 assert_eq!(
19623 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19624 "\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",
19625 );
19626
19627 multi_buffer_editor.update(cx, |editor, cx| {
19628 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19629 });
19630 assert_eq!(
19631 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19632 "\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",
19633 "After folding the first buffer, its text should not be displayed"
19634 );
19635
19636 multi_buffer_editor.update(cx, |editor, cx| {
19637 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19638 });
19639 assert_eq!(
19640 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19641 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
19642 "After folding the second buffer, its text should not be displayed"
19643 );
19644
19645 multi_buffer_editor.update(cx, |editor, cx| {
19646 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19647 });
19648 assert_eq!(
19649 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19650 "\n\n\n\n\n",
19651 "After folding the third buffer, its text should not be displayed"
19652 );
19653
19654 // Emulate selection inside the fold logic, that should work
19655 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19656 editor
19657 .snapshot(window, cx)
19658 .next_line_boundary(Point::new(0, 4));
19659 });
19660
19661 multi_buffer_editor.update(cx, |editor, cx| {
19662 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19663 });
19664 assert_eq!(
19665 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19666 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19667 "After unfolding the second buffer, its text should be displayed"
19668 );
19669
19670 // Typing inside of buffer 1 causes that buffer to be unfolded.
19671 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19672 assert_eq!(
19673 multi_buffer
19674 .read(cx)
19675 .snapshot(cx)
19676 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
19677 .collect::<String>(),
19678 "bbbb"
19679 );
19680 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
19681 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
19682 });
19683 editor.handle_input("B", window, cx);
19684 });
19685
19686 assert_eq!(
19687 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19688 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19689 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
19690 );
19691
19692 multi_buffer_editor.update(cx, |editor, cx| {
19693 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19694 });
19695 assert_eq!(
19696 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19697 "\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",
19698 "After unfolding the all buffers, all original text should be displayed"
19699 );
19700}
19701
19702#[gpui::test]
19703async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
19704 init_test(cx, |_| {});
19705
19706 let sample_text_1 = "1111\n2222\n3333".to_string();
19707 let sample_text_2 = "4444\n5555\n6666".to_string();
19708 let sample_text_3 = "7777\n8888\n9999".to_string();
19709
19710 let fs = FakeFs::new(cx.executor());
19711 fs.insert_tree(
19712 path!("/a"),
19713 json!({
19714 "first.rs": sample_text_1,
19715 "second.rs": sample_text_2,
19716 "third.rs": sample_text_3,
19717 }),
19718 )
19719 .await;
19720 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19721 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19722 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19723 let worktree = project.update(cx, |project, cx| {
19724 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19725 assert_eq!(worktrees.len(), 1);
19726 worktrees.pop().unwrap()
19727 });
19728 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19729
19730 let buffer_1 = project
19731 .update(cx, |project, cx| {
19732 project.open_buffer((worktree_id, "first.rs"), cx)
19733 })
19734 .await
19735 .unwrap();
19736 let buffer_2 = project
19737 .update(cx, |project, cx| {
19738 project.open_buffer((worktree_id, "second.rs"), cx)
19739 })
19740 .await
19741 .unwrap();
19742 let buffer_3 = project
19743 .update(cx, |project, cx| {
19744 project.open_buffer((worktree_id, "third.rs"), cx)
19745 })
19746 .await
19747 .unwrap();
19748
19749 let multi_buffer = cx.new(|cx| {
19750 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19751 multi_buffer.push_excerpts(
19752 buffer_1.clone(),
19753 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19754 cx,
19755 );
19756 multi_buffer.push_excerpts(
19757 buffer_2.clone(),
19758 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19759 cx,
19760 );
19761 multi_buffer.push_excerpts(
19762 buffer_3.clone(),
19763 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19764 cx,
19765 );
19766 multi_buffer
19767 });
19768
19769 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19770 Editor::new(
19771 EditorMode::full(),
19772 multi_buffer,
19773 Some(project.clone()),
19774 window,
19775 cx,
19776 )
19777 });
19778
19779 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
19780 assert_eq!(
19781 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19782 full_text,
19783 );
19784
19785 multi_buffer_editor.update(cx, |editor, cx| {
19786 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19787 });
19788 assert_eq!(
19789 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19790 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
19791 "After folding the first buffer, its text should not be displayed"
19792 );
19793
19794 multi_buffer_editor.update(cx, |editor, cx| {
19795 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19796 });
19797
19798 assert_eq!(
19799 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19800 "\n\n\n\n\n\n7777\n8888\n9999",
19801 "After folding the second buffer, its text should not be displayed"
19802 );
19803
19804 multi_buffer_editor.update(cx, |editor, cx| {
19805 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19806 });
19807 assert_eq!(
19808 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19809 "\n\n\n\n\n",
19810 "After folding the third buffer, its text should not be displayed"
19811 );
19812
19813 multi_buffer_editor.update(cx, |editor, cx| {
19814 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19815 });
19816 assert_eq!(
19817 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19818 "\n\n\n\n4444\n5555\n6666\n\n",
19819 "After unfolding the second buffer, its text should be displayed"
19820 );
19821
19822 multi_buffer_editor.update(cx, |editor, cx| {
19823 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
19824 });
19825 assert_eq!(
19826 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19827 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
19828 "After unfolding the first buffer, its text should be displayed"
19829 );
19830
19831 multi_buffer_editor.update(cx, |editor, cx| {
19832 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19833 });
19834 assert_eq!(
19835 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19836 full_text,
19837 "After unfolding all buffers, all original text should be displayed"
19838 );
19839}
19840
19841#[gpui::test]
19842async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
19843 init_test(cx, |_| {});
19844
19845 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19846
19847 let fs = FakeFs::new(cx.executor());
19848 fs.insert_tree(
19849 path!("/a"),
19850 json!({
19851 "main.rs": sample_text,
19852 }),
19853 )
19854 .await;
19855 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19856 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19857 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19858 let worktree = project.update(cx, |project, cx| {
19859 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19860 assert_eq!(worktrees.len(), 1);
19861 worktrees.pop().unwrap()
19862 });
19863 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19864
19865 let buffer_1 = project
19866 .update(cx, |project, cx| {
19867 project.open_buffer((worktree_id, "main.rs"), cx)
19868 })
19869 .await
19870 .unwrap();
19871
19872 let multi_buffer = cx.new(|cx| {
19873 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19874 multi_buffer.push_excerpts(
19875 buffer_1.clone(),
19876 [ExcerptRange::new(
19877 Point::new(0, 0)
19878 ..Point::new(
19879 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
19880 0,
19881 ),
19882 )],
19883 cx,
19884 );
19885 multi_buffer
19886 });
19887 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19888 Editor::new(
19889 EditorMode::full(),
19890 multi_buffer,
19891 Some(project.clone()),
19892 window,
19893 cx,
19894 )
19895 });
19896
19897 let selection_range = Point::new(1, 0)..Point::new(2, 0);
19898 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19899 enum TestHighlight {}
19900 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
19901 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
19902 editor.highlight_text::<TestHighlight>(
19903 vec![highlight_range.clone()],
19904 HighlightStyle::color(Hsla::green()),
19905 cx,
19906 );
19907 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19908 s.select_ranges(Some(highlight_range))
19909 });
19910 });
19911
19912 let full_text = format!("\n\n{sample_text}");
19913 assert_eq!(
19914 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19915 full_text,
19916 );
19917}
19918
19919#[gpui::test]
19920async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
19921 init_test(cx, |_| {});
19922 cx.update(|cx| {
19923 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
19924 "keymaps/default-linux.json",
19925 cx,
19926 )
19927 .unwrap();
19928 cx.bind_keys(default_key_bindings);
19929 });
19930
19931 let (editor, cx) = cx.add_window_view(|window, cx| {
19932 let multi_buffer = MultiBuffer::build_multi(
19933 [
19934 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
19935 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
19936 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
19937 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
19938 ],
19939 cx,
19940 );
19941 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
19942
19943 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
19944 // fold all but the second buffer, so that we test navigating between two
19945 // adjacent folded buffers, as well as folded buffers at the start and
19946 // end the multibuffer
19947 editor.fold_buffer(buffer_ids[0], cx);
19948 editor.fold_buffer(buffer_ids[2], cx);
19949 editor.fold_buffer(buffer_ids[3], cx);
19950
19951 editor
19952 });
19953 cx.simulate_resize(size(px(1000.), px(1000.)));
19954
19955 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
19956 cx.assert_excerpts_with_selections(indoc! {"
19957 [EXCERPT]
19958 ˇ[FOLDED]
19959 [EXCERPT]
19960 a1
19961 b1
19962 [EXCERPT]
19963 [FOLDED]
19964 [EXCERPT]
19965 [FOLDED]
19966 "
19967 });
19968 cx.simulate_keystroke("down");
19969 cx.assert_excerpts_with_selections(indoc! {"
19970 [EXCERPT]
19971 [FOLDED]
19972 [EXCERPT]
19973 ˇa1
19974 b1
19975 [EXCERPT]
19976 [FOLDED]
19977 [EXCERPT]
19978 [FOLDED]
19979 "
19980 });
19981 cx.simulate_keystroke("down");
19982 cx.assert_excerpts_with_selections(indoc! {"
19983 [EXCERPT]
19984 [FOLDED]
19985 [EXCERPT]
19986 a1
19987 ˇb1
19988 [EXCERPT]
19989 [FOLDED]
19990 [EXCERPT]
19991 [FOLDED]
19992 "
19993 });
19994 cx.simulate_keystroke("down");
19995 cx.assert_excerpts_with_selections(indoc! {"
19996 [EXCERPT]
19997 [FOLDED]
19998 [EXCERPT]
19999 a1
20000 b1
20001 ˇ[EXCERPT]
20002 [FOLDED]
20003 [EXCERPT]
20004 [FOLDED]
20005 "
20006 });
20007 cx.simulate_keystroke("down");
20008 cx.assert_excerpts_with_selections(indoc! {"
20009 [EXCERPT]
20010 [FOLDED]
20011 [EXCERPT]
20012 a1
20013 b1
20014 [EXCERPT]
20015 ˇ[FOLDED]
20016 [EXCERPT]
20017 [FOLDED]
20018 "
20019 });
20020 for _ in 0..5 {
20021 cx.simulate_keystroke("down");
20022 cx.assert_excerpts_with_selections(indoc! {"
20023 [EXCERPT]
20024 [FOLDED]
20025 [EXCERPT]
20026 a1
20027 b1
20028 [EXCERPT]
20029 [FOLDED]
20030 [EXCERPT]
20031 ˇ[FOLDED]
20032 "
20033 });
20034 }
20035
20036 cx.simulate_keystroke("up");
20037 cx.assert_excerpts_with_selections(indoc! {"
20038 [EXCERPT]
20039 [FOLDED]
20040 [EXCERPT]
20041 a1
20042 b1
20043 [EXCERPT]
20044 ˇ[FOLDED]
20045 [EXCERPT]
20046 [FOLDED]
20047 "
20048 });
20049 cx.simulate_keystroke("up");
20050 cx.assert_excerpts_with_selections(indoc! {"
20051 [EXCERPT]
20052 [FOLDED]
20053 [EXCERPT]
20054 a1
20055 b1
20056 ˇ[EXCERPT]
20057 [FOLDED]
20058 [EXCERPT]
20059 [FOLDED]
20060 "
20061 });
20062 cx.simulate_keystroke("up");
20063 cx.assert_excerpts_with_selections(indoc! {"
20064 [EXCERPT]
20065 [FOLDED]
20066 [EXCERPT]
20067 a1
20068 ˇb1
20069 [EXCERPT]
20070 [FOLDED]
20071 [EXCERPT]
20072 [FOLDED]
20073 "
20074 });
20075 cx.simulate_keystroke("up");
20076 cx.assert_excerpts_with_selections(indoc! {"
20077 [EXCERPT]
20078 [FOLDED]
20079 [EXCERPT]
20080 ˇa1
20081 b1
20082 [EXCERPT]
20083 [FOLDED]
20084 [EXCERPT]
20085 [FOLDED]
20086 "
20087 });
20088 for _ in 0..5 {
20089 cx.simulate_keystroke("up");
20090 cx.assert_excerpts_with_selections(indoc! {"
20091 [EXCERPT]
20092 ˇ[FOLDED]
20093 [EXCERPT]
20094 a1
20095 b1
20096 [EXCERPT]
20097 [FOLDED]
20098 [EXCERPT]
20099 [FOLDED]
20100 "
20101 });
20102 }
20103}
20104
20105#[gpui::test]
20106async fn test_inline_completion_text(cx: &mut TestAppContext) {
20107 init_test(cx, |_| {});
20108
20109 // Simple insertion
20110 assert_highlighted_edits(
20111 "Hello, world!",
20112 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
20113 true,
20114 cx,
20115 |highlighted_edits, cx| {
20116 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
20117 assert_eq!(highlighted_edits.highlights.len(), 1);
20118 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
20119 assert_eq!(
20120 highlighted_edits.highlights[0].1.background_color,
20121 Some(cx.theme().status().created_background)
20122 );
20123 },
20124 )
20125 .await;
20126
20127 // Replacement
20128 assert_highlighted_edits(
20129 "This is a test.",
20130 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
20131 false,
20132 cx,
20133 |highlighted_edits, cx| {
20134 assert_eq!(highlighted_edits.text, "That is a test.");
20135 assert_eq!(highlighted_edits.highlights.len(), 1);
20136 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
20137 assert_eq!(
20138 highlighted_edits.highlights[0].1.background_color,
20139 Some(cx.theme().status().created_background)
20140 );
20141 },
20142 )
20143 .await;
20144
20145 // Multiple edits
20146 assert_highlighted_edits(
20147 "Hello, world!",
20148 vec![
20149 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
20150 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
20151 ],
20152 false,
20153 cx,
20154 |highlighted_edits, cx| {
20155 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
20156 assert_eq!(highlighted_edits.highlights.len(), 2);
20157 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
20158 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
20159 assert_eq!(
20160 highlighted_edits.highlights[0].1.background_color,
20161 Some(cx.theme().status().created_background)
20162 );
20163 assert_eq!(
20164 highlighted_edits.highlights[1].1.background_color,
20165 Some(cx.theme().status().created_background)
20166 );
20167 },
20168 )
20169 .await;
20170
20171 // Multiple lines with edits
20172 assert_highlighted_edits(
20173 "First line\nSecond line\nThird line\nFourth line",
20174 vec![
20175 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
20176 (
20177 Point::new(2, 0)..Point::new(2, 10),
20178 "New third line".to_string(),
20179 ),
20180 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
20181 ],
20182 false,
20183 cx,
20184 |highlighted_edits, cx| {
20185 assert_eq!(
20186 highlighted_edits.text,
20187 "Second modified\nNew third line\nFourth updated line"
20188 );
20189 assert_eq!(highlighted_edits.highlights.len(), 3);
20190 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
20191 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
20192 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
20193 for highlight in &highlighted_edits.highlights {
20194 assert_eq!(
20195 highlight.1.background_color,
20196 Some(cx.theme().status().created_background)
20197 );
20198 }
20199 },
20200 )
20201 .await;
20202}
20203
20204#[gpui::test]
20205async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
20206 init_test(cx, |_| {});
20207
20208 // Deletion
20209 assert_highlighted_edits(
20210 "Hello, world!",
20211 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
20212 true,
20213 cx,
20214 |highlighted_edits, cx| {
20215 assert_eq!(highlighted_edits.text, "Hello, world!");
20216 assert_eq!(highlighted_edits.highlights.len(), 1);
20217 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
20218 assert_eq!(
20219 highlighted_edits.highlights[0].1.background_color,
20220 Some(cx.theme().status().deleted_background)
20221 );
20222 },
20223 )
20224 .await;
20225
20226 // Insertion
20227 assert_highlighted_edits(
20228 "Hello, world!",
20229 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
20230 true,
20231 cx,
20232 |highlighted_edits, cx| {
20233 assert_eq!(highlighted_edits.highlights.len(), 1);
20234 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
20235 assert_eq!(
20236 highlighted_edits.highlights[0].1.background_color,
20237 Some(cx.theme().status().created_background)
20238 );
20239 },
20240 )
20241 .await;
20242}
20243
20244async fn assert_highlighted_edits(
20245 text: &str,
20246 edits: Vec<(Range<Point>, String)>,
20247 include_deletions: bool,
20248 cx: &mut TestAppContext,
20249 assertion_fn: impl Fn(HighlightedText, &App),
20250) {
20251 let window = cx.add_window(|window, cx| {
20252 let buffer = MultiBuffer::build_simple(text, cx);
20253 Editor::new(EditorMode::full(), buffer, None, window, cx)
20254 });
20255 let cx = &mut VisualTestContext::from_window(*window, cx);
20256
20257 let (buffer, snapshot) = window
20258 .update(cx, |editor, _window, cx| {
20259 (
20260 editor.buffer().clone(),
20261 editor.buffer().read(cx).snapshot(cx),
20262 )
20263 })
20264 .unwrap();
20265
20266 let edits = edits
20267 .into_iter()
20268 .map(|(range, edit)| {
20269 (
20270 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
20271 edit,
20272 )
20273 })
20274 .collect::<Vec<_>>();
20275
20276 let text_anchor_edits = edits
20277 .clone()
20278 .into_iter()
20279 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
20280 .collect::<Vec<_>>();
20281
20282 let edit_preview = window
20283 .update(cx, |_, _window, cx| {
20284 buffer
20285 .read(cx)
20286 .as_singleton()
20287 .unwrap()
20288 .read(cx)
20289 .preview_edits(text_anchor_edits.into(), cx)
20290 })
20291 .unwrap()
20292 .await;
20293
20294 cx.update(|_window, cx| {
20295 let highlighted_edits = inline_completion_edit_text(
20296 &snapshot.as_singleton().unwrap().2,
20297 &edits,
20298 &edit_preview,
20299 include_deletions,
20300 cx,
20301 );
20302 assertion_fn(highlighted_edits, cx)
20303 });
20304}
20305
20306#[track_caller]
20307fn assert_breakpoint(
20308 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
20309 path: &Arc<Path>,
20310 expected: Vec<(u32, Breakpoint)>,
20311) {
20312 if expected.len() == 0usize {
20313 assert!(!breakpoints.contains_key(path), "{}", path.display());
20314 } else {
20315 let mut breakpoint = breakpoints
20316 .get(path)
20317 .unwrap()
20318 .into_iter()
20319 .map(|breakpoint| {
20320 (
20321 breakpoint.row,
20322 Breakpoint {
20323 message: breakpoint.message.clone(),
20324 state: breakpoint.state,
20325 condition: breakpoint.condition.clone(),
20326 hit_condition: breakpoint.hit_condition.clone(),
20327 },
20328 )
20329 })
20330 .collect::<Vec<_>>();
20331
20332 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
20333
20334 assert_eq!(expected, breakpoint);
20335 }
20336}
20337
20338fn add_log_breakpoint_at_cursor(
20339 editor: &mut Editor,
20340 log_message: &str,
20341 window: &mut Window,
20342 cx: &mut Context<Editor>,
20343) {
20344 let (anchor, bp) = editor
20345 .breakpoints_at_cursors(window, cx)
20346 .first()
20347 .and_then(|(anchor, bp)| {
20348 if let Some(bp) = bp {
20349 Some((*anchor, bp.clone()))
20350 } else {
20351 None
20352 }
20353 })
20354 .unwrap_or_else(|| {
20355 let cursor_position: Point = editor.selections.newest(cx).head();
20356
20357 let breakpoint_position = editor
20358 .snapshot(window, cx)
20359 .display_snapshot
20360 .buffer_snapshot
20361 .anchor_before(Point::new(cursor_position.row, 0));
20362
20363 (breakpoint_position, Breakpoint::new_log(&log_message))
20364 });
20365
20366 editor.edit_breakpoint_at_anchor(
20367 anchor,
20368 bp,
20369 BreakpointEditAction::EditLogMessage(log_message.into()),
20370 cx,
20371 );
20372}
20373
20374#[gpui::test]
20375async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
20376 init_test(cx, |_| {});
20377
20378 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20379 let fs = FakeFs::new(cx.executor());
20380 fs.insert_tree(
20381 path!("/a"),
20382 json!({
20383 "main.rs": sample_text,
20384 }),
20385 )
20386 .await;
20387 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20388 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20389 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20390
20391 let fs = FakeFs::new(cx.executor());
20392 fs.insert_tree(
20393 path!("/a"),
20394 json!({
20395 "main.rs": sample_text,
20396 }),
20397 )
20398 .await;
20399 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20400 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20401 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20402 let worktree_id = workspace
20403 .update(cx, |workspace, _window, cx| {
20404 workspace.project().update(cx, |project, cx| {
20405 project.worktrees(cx).next().unwrap().read(cx).id()
20406 })
20407 })
20408 .unwrap();
20409
20410 let buffer = project
20411 .update(cx, |project, cx| {
20412 project.open_buffer((worktree_id, "main.rs"), cx)
20413 })
20414 .await
20415 .unwrap();
20416
20417 let (editor, cx) = cx.add_window_view(|window, cx| {
20418 Editor::new(
20419 EditorMode::full(),
20420 MultiBuffer::build_from_buffer(buffer, cx),
20421 Some(project.clone()),
20422 window,
20423 cx,
20424 )
20425 });
20426
20427 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20428 let abs_path = project.read_with(cx, |project, cx| {
20429 project
20430 .absolute_path(&project_path, cx)
20431 .map(|path_buf| Arc::from(path_buf.to_owned()))
20432 .unwrap()
20433 });
20434
20435 // assert we can add breakpoint on the first line
20436 editor.update_in(cx, |editor, window, cx| {
20437 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20438 editor.move_to_end(&MoveToEnd, window, cx);
20439 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20440 });
20441
20442 let breakpoints = editor.update(cx, |editor, cx| {
20443 editor
20444 .breakpoint_store()
20445 .as_ref()
20446 .unwrap()
20447 .read(cx)
20448 .all_source_breakpoints(cx)
20449 .clone()
20450 });
20451
20452 assert_eq!(1, breakpoints.len());
20453 assert_breakpoint(
20454 &breakpoints,
20455 &abs_path,
20456 vec![
20457 (0, Breakpoint::new_standard()),
20458 (3, Breakpoint::new_standard()),
20459 ],
20460 );
20461
20462 editor.update_in(cx, |editor, window, cx| {
20463 editor.move_to_beginning(&MoveToBeginning, window, cx);
20464 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20465 });
20466
20467 let breakpoints = editor.update(cx, |editor, cx| {
20468 editor
20469 .breakpoint_store()
20470 .as_ref()
20471 .unwrap()
20472 .read(cx)
20473 .all_source_breakpoints(cx)
20474 .clone()
20475 });
20476
20477 assert_eq!(1, breakpoints.len());
20478 assert_breakpoint(
20479 &breakpoints,
20480 &abs_path,
20481 vec![(3, Breakpoint::new_standard())],
20482 );
20483
20484 editor.update_in(cx, |editor, window, cx| {
20485 editor.move_to_end(&MoveToEnd, window, cx);
20486 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20487 });
20488
20489 let breakpoints = editor.update(cx, |editor, cx| {
20490 editor
20491 .breakpoint_store()
20492 .as_ref()
20493 .unwrap()
20494 .read(cx)
20495 .all_source_breakpoints(cx)
20496 .clone()
20497 });
20498
20499 assert_eq!(0, breakpoints.len());
20500 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20501}
20502
20503#[gpui::test]
20504async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
20505 init_test(cx, |_| {});
20506
20507 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20508
20509 let fs = FakeFs::new(cx.executor());
20510 fs.insert_tree(
20511 path!("/a"),
20512 json!({
20513 "main.rs": sample_text,
20514 }),
20515 )
20516 .await;
20517 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20518 let (workspace, cx) =
20519 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20520
20521 let worktree_id = workspace.update(cx, |workspace, cx| {
20522 workspace.project().update(cx, |project, cx| {
20523 project.worktrees(cx).next().unwrap().read(cx).id()
20524 })
20525 });
20526
20527 let buffer = project
20528 .update(cx, |project, cx| {
20529 project.open_buffer((worktree_id, "main.rs"), cx)
20530 })
20531 .await
20532 .unwrap();
20533
20534 let (editor, cx) = cx.add_window_view(|window, cx| {
20535 Editor::new(
20536 EditorMode::full(),
20537 MultiBuffer::build_from_buffer(buffer, cx),
20538 Some(project.clone()),
20539 window,
20540 cx,
20541 )
20542 });
20543
20544 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20545 let abs_path = project.read_with(cx, |project, cx| {
20546 project
20547 .absolute_path(&project_path, cx)
20548 .map(|path_buf| Arc::from(path_buf.to_owned()))
20549 .unwrap()
20550 });
20551
20552 editor.update_in(cx, |editor, window, cx| {
20553 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20554 });
20555
20556 let breakpoints = editor.update(cx, |editor, cx| {
20557 editor
20558 .breakpoint_store()
20559 .as_ref()
20560 .unwrap()
20561 .read(cx)
20562 .all_source_breakpoints(cx)
20563 .clone()
20564 });
20565
20566 assert_breakpoint(
20567 &breakpoints,
20568 &abs_path,
20569 vec![(0, Breakpoint::new_log("hello world"))],
20570 );
20571
20572 // Removing a log message from a log breakpoint should remove it
20573 editor.update_in(cx, |editor, window, cx| {
20574 add_log_breakpoint_at_cursor(editor, "", window, cx);
20575 });
20576
20577 let breakpoints = editor.update(cx, |editor, cx| {
20578 editor
20579 .breakpoint_store()
20580 .as_ref()
20581 .unwrap()
20582 .read(cx)
20583 .all_source_breakpoints(cx)
20584 .clone()
20585 });
20586
20587 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20588
20589 editor.update_in(cx, |editor, window, cx| {
20590 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20591 editor.move_to_end(&MoveToEnd, window, cx);
20592 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20593 // Not adding a log message to a standard breakpoint shouldn't remove it
20594 add_log_breakpoint_at_cursor(editor, "", window, cx);
20595 });
20596
20597 let breakpoints = editor.update(cx, |editor, cx| {
20598 editor
20599 .breakpoint_store()
20600 .as_ref()
20601 .unwrap()
20602 .read(cx)
20603 .all_source_breakpoints(cx)
20604 .clone()
20605 });
20606
20607 assert_breakpoint(
20608 &breakpoints,
20609 &abs_path,
20610 vec![
20611 (0, Breakpoint::new_standard()),
20612 (3, Breakpoint::new_standard()),
20613 ],
20614 );
20615
20616 editor.update_in(cx, |editor, window, cx| {
20617 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20618 });
20619
20620 let breakpoints = editor.update(cx, |editor, cx| {
20621 editor
20622 .breakpoint_store()
20623 .as_ref()
20624 .unwrap()
20625 .read(cx)
20626 .all_source_breakpoints(cx)
20627 .clone()
20628 });
20629
20630 assert_breakpoint(
20631 &breakpoints,
20632 &abs_path,
20633 vec![
20634 (0, Breakpoint::new_standard()),
20635 (3, Breakpoint::new_log("hello world")),
20636 ],
20637 );
20638
20639 editor.update_in(cx, |editor, window, cx| {
20640 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
20641 });
20642
20643 let breakpoints = editor.update(cx, |editor, cx| {
20644 editor
20645 .breakpoint_store()
20646 .as_ref()
20647 .unwrap()
20648 .read(cx)
20649 .all_source_breakpoints(cx)
20650 .clone()
20651 });
20652
20653 assert_breakpoint(
20654 &breakpoints,
20655 &abs_path,
20656 vec![
20657 (0, Breakpoint::new_standard()),
20658 (3, Breakpoint::new_log("hello Earth!!")),
20659 ],
20660 );
20661}
20662
20663/// This also tests that Editor::breakpoint_at_cursor_head is working properly
20664/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
20665/// or when breakpoints were placed out of order. This tests for a regression too
20666#[gpui::test]
20667async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
20668 init_test(cx, |_| {});
20669
20670 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20671 let fs = FakeFs::new(cx.executor());
20672 fs.insert_tree(
20673 path!("/a"),
20674 json!({
20675 "main.rs": sample_text,
20676 }),
20677 )
20678 .await;
20679 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20680 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20681 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20682
20683 let fs = FakeFs::new(cx.executor());
20684 fs.insert_tree(
20685 path!("/a"),
20686 json!({
20687 "main.rs": sample_text,
20688 }),
20689 )
20690 .await;
20691 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20692 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20693 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20694 let worktree_id = workspace
20695 .update(cx, |workspace, _window, cx| {
20696 workspace.project().update(cx, |project, cx| {
20697 project.worktrees(cx).next().unwrap().read(cx).id()
20698 })
20699 })
20700 .unwrap();
20701
20702 let buffer = project
20703 .update(cx, |project, cx| {
20704 project.open_buffer((worktree_id, "main.rs"), cx)
20705 })
20706 .await
20707 .unwrap();
20708
20709 let (editor, cx) = cx.add_window_view(|window, cx| {
20710 Editor::new(
20711 EditorMode::full(),
20712 MultiBuffer::build_from_buffer(buffer, cx),
20713 Some(project.clone()),
20714 window,
20715 cx,
20716 )
20717 });
20718
20719 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20720 let abs_path = project.read_with(cx, |project, cx| {
20721 project
20722 .absolute_path(&project_path, cx)
20723 .map(|path_buf| Arc::from(path_buf.to_owned()))
20724 .unwrap()
20725 });
20726
20727 // assert we can add breakpoint on the first line
20728 editor.update_in(cx, |editor, window, cx| {
20729 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20730 editor.move_to_end(&MoveToEnd, window, cx);
20731 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20732 editor.move_up(&MoveUp, window, cx);
20733 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20734 });
20735
20736 let breakpoints = editor.update(cx, |editor, cx| {
20737 editor
20738 .breakpoint_store()
20739 .as_ref()
20740 .unwrap()
20741 .read(cx)
20742 .all_source_breakpoints(cx)
20743 .clone()
20744 });
20745
20746 assert_eq!(1, breakpoints.len());
20747 assert_breakpoint(
20748 &breakpoints,
20749 &abs_path,
20750 vec![
20751 (0, Breakpoint::new_standard()),
20752 (2, Breakpoint::new_standard()),
20753 (3, Breakpoint::new_standard()),
20754 ],
20755 );
20756
20757 editor.update_in(cx, |editor, window, cx| {
20758 editor.move_to_beginning(&MoveToBeginning, window, cx);
20759 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20760 editor.move_to_end(&MoveToEnd, window, cx);
20761 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20762 // Disabling a breakpoint that doesn't exist should do nothing
20763 editor.move_up(&MoveUp, window, cx);
20764 editor.move_up(&MoveUp, window, cx);
20765 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20766 });
20767
20768 let breakpoints = editor.update(cx, |editor, cx| {
20769 editor
20770 .breakpoint_store()
20771 .as_ref()
20772 .unwrap()
20773 .read(cx)
20774 .all_source_breakpoints(cx)
20775 .clone()
20776 });
20777
20778 let disable_breakpoint = {
20779 let mut bp = Breakpoint::new_standard();
20780 bp.state = BreakpointState::Disabled;
20781 bp
20782 };
20783
20784 assert_eq!(1, breakpoints.len());
20785 assert_breakpoint(
20786 &breakpoints,
20787 &abs_path,
20788 vec![
20789 (0, disable_breakpoint.clone()),
20790 (2, Breakpoint::new_standard()),
20791 (3, disable_breakpoint.clone()),
20792 ],
20793 );
20794
20795 editor.update_in(cx, |editor, window, cx| {
20796 editor.move_to_beginning(&MoveToBeginning, window, cx);
20797 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20798 editor.move_to_end(&MoveToEnd, window, cx);
20799 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20800 editor.move_up(&MoveUp, window, cx);
20801 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20802 });
20803
20804 let breakpoints = editor.update(cx, |editor, cx| {
20805 editor
20806 .breakpoint_store()
20807 .as_ref()
20808 .unwrap()
20809 .read(cx)
20810 .all_source_breakpoints(cx)
20811 .clone()
20812 });
20813
20814 assert_eq!(1, breakpoints.len());
20815 assert_breakpoint(
20816 &breakpoints,
20817 &abs_path,
20818 vec![
20819 (0, Breakpoint::new_standard()),
20820 (2, disable_breakpoint),
20821 (3, Breakpoint::new_standard()),
20822 ],
20823 );
20824}
20825
20826#[gpui::test]
20827async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
20828 init_test(cx, |_| {});
20829 let capabilities = lsp::ServerCapabilities {
20830 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
20831 prepare_provider: Some(true),
20832 work_done_progress_options: Default::default(),
20833 })),
20834 ..Default::default()
20835 };
20836 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
20837
20838 cx.set_state(indoc! {"
20839 struct Fˇoo {}
20840 "});
20841
20842 cx.update_editor(|editor, _, cx| {
20843 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
20844 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
20845 editor.highlight_background::<DocumentHighlightRead>(
20846 &[highlight_range],
20847 |theme| theme.colors().editor_document_highlight_read_background,
20848 cx,
20849 );
20850 });
20851
20852 let mut prepare_rename_handler = cx
20853 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
20854 move |_, _, _| async move {
20855 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
20856 start: lsp::Position {
20857 line: 0,
20858 character: 7,
20859 },
20860 end: lsp::Position {
20861 line: 0,
20862 character: 10,
20863 },
20864 })))
20865 },
20866 );
20867 let prepare_rename_task = cx
20868 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
20869 .expect("Prepare rename was not started");
20870 prepare_rename_handler.next().await.unwrap();
20871 prepare_rename_task.await.expect("Prepare rename failed");
20872
20873 let mut rename_handler =
20874 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
20875 let edit = lsp::TextEdit {
20876 range: lsp::Range {
20877 start: lsp::Position {
20878 line: 0,
20879 character: 7,
20880 },
20881 end: lsp::Position {
20882 line: 0,
20883 character: 10,
20884 },
20885 },
20886 new_text: "FooRenamed".to_string(),
20887 };
20888 Ok(Some(lsp::WorkspaceEdit::new(
20889 // Specify the same edit twice
20890 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
20891 )))
20892 });
20893 let rename_task = cx
20894 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
20895 .expect("Confirm rename was not started");
20896 rename_handler.next().await.unwrap();
20897 rename_task.await.expect("Confirm rename failed");
20898 cx.run_until_parked();
20899
20900 // Despite two edits, only one is actually applied as those are identical
20901 cx.assert_editor_state(indoc! {"
20902 struct FooRenamedˇ {}
20903 "});
20904}
20905
20906#[gpui::test]
20907async fn test_rename_without_prepare(cx: &mut TestAppContext) {
20908 init_test(cx, |_| {});
20909 // These capabilities indicate that the server does not support prepare rename.
20910 let capabilities = lsp::ServerCapabilities {
20911 rename_provider: Some(lsp::OneOf::Left(true)),
20912 ..Default::default()
20913 };
20914 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
20915
20916 cx.set_state(indoc! {"
20917 struct Fˇoo {}
20918 "});
20919
20920 cx.update_editor(|editor, _window, cx| {
20921 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
20922 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
20923 editor.highlight_background::<DocumentHighlightRead>(
20924 &[highlight_range],
20925 |theme| theme.colors().editor_document_highlight_read_background,
20926 cx,
20927 );
20928 });
20929
20930 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
20931 .expect("Prepare rename was not started")
20932 .await
20933 .expect("Prepare rename failed");
20934
20935 let mut rename_handler =
20936 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
20937 let edit = lsp::TextEdit {
20938 range: lsp::Range {
20939 start: lsp::Position {
20940 line: 0,
20941 character: 7,
20942 },
20943 end: lsp::Position {
20944 line: 0,
20945 character: 10,
20946 },
20947 },
20948 new_text: "FooRenamed".to_string(),
20949 };
20950 Ok(Some(lsp::WorkspaceEdit::new(
20951 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
20952 )))
20953 });
20954 let rename_task = cx
20955 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
20956 .expect("Confirm rename was not started");
20957 rename_handler.next().await.unwrap();
20958 rename_task.await.expect("Confirm rename failed");
20959 cx.run_until_parked();
20960
20961 // Correct range is renamed, as `surrounding_word` is used to find it.
20962 cx.assert_editor_state(indoc! {"
20963 struct FooRenamedˇ {}
20964 "});
20965}
20966
20967#[gpui::test]
20968async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
20969 init_test(cx, |_| {});
20970 let mut cx = EditorTestContext::new(cx).await;
20971
20972 let language = Arc::new(
20973 Language::new(
20974 LanguageConfig::default(),
20975 Some(tree_sitter_html::LANGUAGE.into()),
20976 )
20977 .with_brackets_query(
20978 r#"
20979 ("<" @open "/>" @close)
20980 ("</" @open ">" @close)
20981 ("<" @open ">" @close)
20982 ("\"" @open "\"" @close)
20983 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
20984 "#,
20985 )
20986 .unwrap(),
20987 );
20988 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20989
20990 cx.set_state(indoc! {"
20991 <span>ˇ</span>
20992 "});
20993 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
20994 cx.assert_editor_state(indoc! {"
20995 <span>
20996 ˇ
20997 </span>
20998 "});
20999
21000 cx.set_state(indoc! {"
21001 <span><span></span>ˇ</span>
21002 "});
21003 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21004 cx.assert_editor_state(indoc! {"
21005 <span><span></span>
21006 ˇ</span>
21007 "});
21008
21009 cx.set_state(indoc! {"
21010 <span>ˇ
21011 </span>
21012 "});
21013 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21014 cx.assert_editor_state(indoc! {"
21015 <span>
21016 ˇ
21017 </span>
21018 "});
21019}
21020
21021#[gpui::test(iterations = 10)]
21022async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
21023 init_test(cx, |_| {});
21024
21025 let fs = FakeFs::new(cx.executor());
21026 fs.insert_tree(
21027 path!("/dir"),
21028 json!({
21029 "a.ts": "a",
21030 }),
21031 )
21032 .await;
21033
21034 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
21035 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21036 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21037
21038 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21039 language_registry.add(Arc::new(Language::new(
21040 LanguageConfig {
21041 name: "TypeScript".into(),
21042 matcher: LanguageMatcher {
21043 path_suffixes: vec!["ts".to_string()],
21044 ..Default::default()
21045 },
21046 ..Default::default()
21047 },
21048 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
21049 )));
21050 let mut fake_language_servers = language_registry.register_fake_lsp(
21051 "TypeScript",
21052 FakeLspAdapter {
21053 capabilities: lsp::ServerCapabilities {
21054 code_lens_provider: Some(lsp::CodeLensOptions {
21055 resolve_provider: Some(true),
21056 }),
21057 execute_command_provider: Some(lsp::ExecuteCommandOptions {
21058 commands: vec!["_the/command".to_string()],
21059 ..lsp::ExecuteCommandOptions::default()
21060 }),
21061 ..lsp::ServerCapabilities::default()
21062 },
21063 ..FakeLspAdapter::default()
21064 },
21065 );
21066
21067 let (buffer, _handle) = project
21068 .update(cx, |p, cx| {
21069 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
21070 })
21071 .await
21072 .unwrap();
21073 cx.executor().run_until_parked();
21074
21075 let fake_server = fake_language_servers.next().await.unwrap();
21076
21077 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
21078 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
21079 drop(buffer_snapshot);
21080 let actions = cx
21081 .update_window(*workspace, |_, window, cx| {
21082 project.code_actions(&buffer, anchor..anchor, window, cx)
21083 })
21084 .unwrap();
21085
21086 fake_server
21087 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21088 Ok(Some(vec![
21089 lsp::CodeLens {
21090 range: lsp::Range::default(),
21091 command: Some(lsp::Command {
21092 title: "Code lens command".to_owned(),
21093 command: "_the/command".to_owned(),
21094 arguments: None,
21095 }),
21096 data: None,
21097 },
21098 lsp::CodeLens {
21099 range: lsp::Range::default(),
21100 command: Some(lsp::Command {
21101 title: "Command not in capabilities".to_owned(),
21102 command: "not in capabilities".to_owned(),
21103 arguments: None,
21104 }),
21105 data: None,
21106 },
21107 lsp::CodeLens {
21108 range: lsp::Range {
21109 start: lsp::Position {
21110 line: 1,
21111 character: 1,
21112 },
21113 end: lsp::Position {
21114 line: 1,
21115 character: 1,
21116 },
21117 },
21118 command: Some(lsp::Command {
21119 title: "Command not in range".to_owned(),
21120 command: "_the/command".to_owned(),
21121 arguments: None,
21122 }),
21123 data: None,
21124 },
21125 ]))
21126 })
21127 .next()
21128 .await;
21129
21130 let actions = actions.await.unwrap();
21131 assert_eq!(
21132 actions.len(),
21133 1,
21134 "Should have only one valid action for the 0..0 range"
21135 );
21136 let action = actions[0].clone();
21137 let apply = project.update(cx, |project, cx| {
21138 project.apply_code_action(buffer.clone(), action, true, cx)
21139 });
21140
21141 // Resolving the code action does not populate its edits. In absence of
21142 // edits, we must execute the given command.
21143 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
21144 |mut lens, _| async move {
21145 let lens_command = lens.command.as_mut().expect("should have a command");
21146 assert_eq!(lens_command.title, "Code lens command");
21147 lens_command.arguments = Some(vec![json!("the-argument")]);
21148 Ok(lens)
21149 },
21150 );
21151
21152 // While executing the command, the language server sends the editor
21153 // a `workspaceEdit` request.
21154 fake_server
21155 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
21156 let fake = fake_server.clone();
21157 move |params, _| {
21158 assert_eq!(params.command, "_the/command");
21159 let fake = fake.clone();
21160 async move {
21161 fake.server
21162 .request::<lsp::request::ApplyWorkspaceEdit>(
21163 lsp::ApplyWorkspaceEditParams {
21164 label: None,
21165 edit: lsp::WorkspaceEdit {
21166 changes: Some(
21167 [(
21168 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
21169 vec![lsp::TextEdit {
21170 range: lsp::Range::new(
21171 lsp::Position::new(0, 0),
21172 lsp::Position::new(0, 0),
21173 ),
21174 new_text: "X".into(),
21175 }],
21176 )]
21177 .into_iter()
21178 .collect(),
21179 ),
21180 ..Default::default()
21181 },
21182 },
21183 )
21184 .await
21185 .into_response()
21186 .unwrap();
21187 Ok(Some(json!(null)))
21188 }
21189 }
21190 })
21191 .next()
21192 .await;
21193
21194 // Applying the code lens command returns a project transaction containing the edits
21195 // sent by the language server in its `workspaceEdit` request.
21196 let transaction = apply.await.unwrap();
21197 assert!(transaction.0.contains_key(&buffer));
21198 buffer.update(cx, |buffer, cx| {
21199 assert_eq!(buffer.text(), "Xa");
21200 buffer.undo(cx);
21201 assert_eq!(buffer.text(), "a");
21202 });
21203}
21204
21205#[gpui::test]
21206async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
21207 init_test(cx, |_| {});
21208
21209 let fs = FakeFs::new(cx.executor());
21210 let main_text = r#"fn main() {
21211println!("1");
21212println!("2");
21213println!("3");
21214println!("4");
21215println!("5");
21216}"#;
21217 let lib_text = "mod foo {}";
21218 fs.insert_tree(
21219 path!("/a"),
21220 json!({
21221 "lib.rs": lib_text,
21222 "main.rs": main_text,
21223 }),
21224 )
21225 .await;
21226
21227 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21228 let (workspace, cx) =
21229 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21230 let worktree_id = workspace.update(cx, |workspace, cx| {
21231 workspace.project().update(cx, |project, cx| {
21232 project.worktrees(cx).next().unwrap().read(cx).id()
21233 })
21234 });
21235
21236 let expected_ranges = vec![
21237 Point::new(0, 0)..Point::new(0, 0),
21238 Point::new(1, 0)..Point::new(1, 1),
21239 Point::new(2, 0)..Point::new(2, 2),
21240 Point::new(3, 0)..Point::new(3, 3),
21241 ];
21242
21243 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21244 let editor_1 = workspace
21245 .update_in(cx, |workspace, window, cx| {
21246 workspace.open_path(
21247 (worktree_id, "main.rs"),
21248 Some(pane_1.downgrade()),
21249 true,
21250 window,
21251 cx,
21252 )
21253 })
21254 .unwrap()
21255 .await
21256 .downcast::<Editor>()
21257 .unwrap();
21258 pane_1.update(cx, |pane, cx| {
21259 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21260 open_editor.update(cx, |editor, cx| {
21261 assert_eq!(
21262 editor.display_text(cx),
21263 main_text,
21264 "Original main.rs text on initial open",
21265 );
21266 assert_eq!(
21267 editor
21268 .selections
21269 .all::<Point>(cx)
21270 .into_iter()
21271 .map(|s| s.range())
21272 .collect::<Vec<_>>(),
21273 vec![Point::zero()..Point::zero()],
21274 "Default selections on initial open",
21275 );
21276 })
21277 });
21278 editor_1.update_in(cx, |editor, window, cx| {
21279 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21280 s.select_ranges(expected_ranges.clone());
21281 });
21282 });
21283
21284 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
21285 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
21286 });
21287 let editor_2 = workspace
21288 .update_in(cx, |workspace, window, cx| {
21289 workspace.open_path(
21290 (worktree_id, "main.rs"),
21291 Some(pane_2.downgrade()),
21292 true,
21293 window,
21294 cx,
21295 )
21296 })
21297 .unwrap()
21298 .await
21299 .downcast::<Editor>()
21300 .unwrap();
21301 pane_2.update(cx, |pane, cx| {
21302 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21303 open_editor.update(cx, |editor, cx| {
21304 assert_eq!(
21305 editor.display_text(cx),
21306 main_text,
21307 "Original main.rs text on initial open in another panel",
21308 );
21309 assert_eq!(
21310 editor
21311 .selections
21312 .all::<Point>(cx)
21313 .into_iter()
21314 .map(|s| s.range())
21315 .collect::<Vec<_>>(),
21316 vec![Point::zero()..Point::zero()],
21317 "Default selections on initial open in another panel",
21318 );
21319 })
21320 });
21321
21322 editor_2.update_in(cx, |editor, window, cx| {
21323 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
21324 });
21325
21326 let _other_editor_1 = workspace
21327 .update_in(cx, |workspace, window, cx| {
21328 workspace.open_path(
21329 (worktree_id, "lib.rs"),
21330 Some(pane_1.downgrade()),
21331 true,
21332 window,
21333 cx,
21334 )
21335 })
21336 .unwrap()
21337 .await
21338 .downcast::<Editor>()
21339 .unwrap();
21340 pane_1
21341 .update_in(cx, |pane, window, cx| {
21342 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
21343 })
21344 .await
21345 .unwrap();
21346 drop(editor_1);
21347 pane_1.update(cx, |pane, cx| {
21348 pane.active_item()
21349 .unwrap()
21350 .downcast::<Editor>()
21351 .unwrap()
21352 .update(cx, |editor, cx| {
21353 assert_eq!(
21354 editor.display_text(cx),
21355 lib_text,
21356 "Other file should be open and active",
21357 );
21358 });
21359 assert_eq!(pane.items().count(), 1, "No other editors should be open");
21360 });
21361
21362 let _other_editor_2 = workspace
21363 .update_in(cx, |workspace, window, cx| {
21364 workspace.open_path(
21365 (worktree_id, "lib.rs"),
21366 Some(pane_2.downgrade()),
21367 true,
21368 window,
21369 cx,
21370 )
21371 })
21372 .unwrap()
21373 .await
21374 .downcast::<Editor>()
21375 .unwrap();
21376 pane_2
21377 .update_in(cx, |pane, window, cx| {
21378 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
21379 })
21380 .await
21381 .unwrap();
21382 drop(editor_2);
21383 pane_2.update(cx, |pane, cx| {
21384 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21385 open_editor.update(cx, |editor, cx| {
21386 assert_eq!(
21387 editor.display_text(cx),
21388 lib_text,
21389 "Other file should be open and active in another panel too",
21390 );
21391 });
21392 assert_eq!(
21393 pane.items().count(),
21394 1,
21395 "No other editors should be open in another pane",
21396 );
21397 });
21398
21399 let _editor_1_reopened = workspace
21400 .update_in(cx, |workspace, window, cx| {
21401 workspace.open_path(
21402 (worktree_id, "main.rs"),
21403 Some(pane_1.downgrade()),
21404 true,
21405 window,
21406 cx,
21407 )
21408 })
21409 .unwrap()
21410 .await
21411 .downcast::<Editor>()
21412 .unwrap();
21413 let _editor_2_reopened = workspace
21414 .update_in(cx, |workspace, window, cx| {
21415 workspace.open_path(
21416 (worktree_id, "main.rs"),
21417 Some(pane_2.downgrade()),
21418 true,
21419 window,
21420 cx,
21421 )
21422 })
21423 .unwrap()
21424 .await
21425 .downcast::<Editor>()
21426 .unwrap();
21427 pane_1.update(cx, |pane, cx| {
21428 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21429 open_editor.update(cx, |editor, cx| {
21430 assert_eq!(
21431 editor.display_text(cx),
21432 main_text,
21433 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
21434 );
21435 assert_eq!(
21436 editor
21437 .selections
21438 .all::<Point>(cx)
21439 .into_iter()
21440 .map(|s| s.range())
21441 .collect::<Vec<_>>(),
21442 expected_ranges,
21443 "Previous editor in the 1st panel had selections and should get them restored on reopen",
21444 );
21445 })
21446 });
21447 pane_2.update(cx, |pane, cx| {
21448 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21449 open_editor.update(cx, |editor, cx| {
21450 assert_eq!(
21451 editor.display_text(cx),
21452 r#"fn main() {
21453⋯rintln!("1");
21454⋯intln!("2");
21455⋯ntln!("3");
21456println!("4");
21457println!("5");
21458}"#,
21459 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
21460 );
21461 assert_eq!(
21462 editor
21463 .selections
21464 .all::<Point>(cx)
21465 .into_iter()
21466 .map(|s| s.range())
21467 .collect::<Vec<_>>(),
21468 vec![Point::zero()..Point::zero()],
21469 "Previous editor in the 2nd pane had no selections changed hence should restore none",
21470 );
21471 })
21472 });
21473}
21474
21475#[gpui::test]
21476async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
21477 init_test(cx, |_| {});
21478
21479 let fs = FakeFs::new(cx.executor());
21480 let main_text = r#"fn main() {
21481println!("1");
21482println!("2");
21483println!("3");
21484println!("4");
21485println!("5");
21486}"#;
21487 let lib_text = "mod foo {}";
21488 fs.insert_tree(
21489 path!("/a"),
21490 json!({
21491 "lib.rs": lib_text,
21492 "main.rs": main_text,
21493 }),
21494 )
21495 .await;
21496
21497 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21498 let (workspace, cx) =
21499 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21500 let worktree_id = workspace.update(cx, |workspace, cx| {
21501 workspace.project().update(cx, |project, cx| {
21502 project.worktrees(cx).next().unwrap().read(cx).id()
21503 })
21504 });
21505
21506 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21507 let editor = workspace
21508 .update_in(cx, |workspace, window, cx| {
21509 workspace.open_path(
21510 (worktree_id, "main.rs"),
21511 Some(pane.downgrade()),
21512 true,
21513 window,
21514 cx,
21515 )
21516 })
21517 .unwrap()
21518 .await
21519 .downcast::<Editor>()
21520 .unwrap();
21521 pane.update(cx, |pane, cx| {
21522 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21523 open_editor.update(cx, |editor, cx| {
21524 assert_eq!(
21525 editor.display_text(cx),
21526 main_text,
21527 "Original main.rs text on initial open",
21528 );
21529 })
21530 });
21531 editor.update_in(cx, |editor, window, cx| {
21532 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
21533 });
21534
21535 cx.update_global(|store: &mut SettingsStore, cx| {
21536 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21537 s.restore_on_file_reopen = Some(false);
21538 });
21539 });
21540 editor.update_in(cx, |editor, window, cx| {
21541 editor.fold_ranges(
21542 vec![
21543 Point::new(1, 0)..Point::new(1, 1),
21544 Point::new(2, 0)..Point::new(2, 2),
21545 Point::new(3, 0)..Point::new(3, 3),
21546 ],
21547 false,
21548 window,
21549 cx,
21550 );
21551 });
21552 pane.update_in(cx, |pane, window, cx| {
21553 pane.close_all_items(&CloseAllItems::default(), window, cx)
21554 })
21555 .await
21556 .unwrap();
21557 pane.update(cx, |pane, _| {
21558 assert!(pane.active_item().is_none());
21559 });
21560 cx.update_global(|store: &mut SettingsStore, cx| {
21561 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21562 s.restore_on_file_reopen = Some(true);
21563 });
21564 });
21565
21566 let _editor_reopened = workspace
21567 .update_in(cx, |workspace, window, cx| {
21568 workspace.open_path(
21569 (worktree_id, "main.rs"),
21570 Some(pane.downgrade()),
21571 true,
21572 window,
21573 cx,
21574 )
21575 })
21576 .unwrap()
21577 .await
21578 .downcast::<Editor>()
21579 .unwrap();
21580 pane.update(cx, |pane, cx| {
21581 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21582 open_editor.update(cx, |editor, cx| {
21583 assert_eq!(
21584 editor.display_text(cx),
21585 main_text,
21586 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
21587 );
21588 })
21589 });
21590}
21591
21592#[gpui::test]
21593async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
21594 struct EmptyModalView {
21595 focus_handle: gpui::FocusHandle,
21596 }
21597 impl EventEmitter<DismissEvent> for EmptyModalView {}
21598 impl Render for EmptyModalView {
21599 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
21600 div()
21601 }
21602 }
21603 impl Focusable for EmptyModalView {
21604 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
21605 self.focus_handle.clone()
21606 }
21607 }
21608 impl workspace::ModalView for EmptyModalView {}
21609 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
21610 EmptyModalView {
21611 focus_handle: cx.focus_handle(),
21612 }
21613 }
21614
21615 init_test(cx, |_| {});
21616
21617 let fs = FakeFs::new(cx.executor());
21618 let project = Project::test(fs, [], cx).await;
21619 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21620 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
21621 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21622 let editor = cx.new_window_entity(|window, cx| {
21623 Editor::new(
21624 EditorMode::full(),
21625 buffer,
21626 Some(project.clone()),
21627 window,
21628 cx,
21629 )
21630 });
21631 workspace
21632 .update(cx, |workspace, window, cx| {
21633 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
21634 })
21635 .unwrap();
21636 editor.update_in(cx, |editor, window, cx| {
21637 editor.open_context_menu(&OpenContextMenu, window, cx);
21638 assert!(editor.mouse_context_menu.is_some());
21639 });
21640 workspace
21641 .update(cx, |workspace, window, cx| {
21642 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
21643 })
21644 .unwrap();
21645 cx.read(|cx| {
21646 assert!(editor.read(cx).mouse_context_menu.is_none());
21647 });
21648}
21649
21650#[gpui::test]
21651async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
21652 init_test(cx, |_| {});
21653
21654 let fs = FakeFs::new(cx.executor());
21655 fs.insert_file(path!("/file.html"), Default::default())
21656 .await;
21657
21658 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
21659
21660 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21661 let html_language = Arc::new(Language::new(
21662 LanguageConfig {
21663 name: "HTML".into(),
21664 matcher: LanguageMatcher {
21665 path_suffixes: vec!["html".to_string()],
21666 ..LanguageMatcher::default()
21667 },
21668 brackets: BracketPairConfig {
21669 pairs: vec![BracketPair {
21670 start: "<".into(),
21671 end: ">".into(),
21672 close: true,
21673 ..Default::default()
21674 }],
21675 ..Default::default()
21676 },
21677 ..Default::default()
21678 },
21679 Some(tree_sitter_html::LANGUAGE.into()),
21680 ));
21681 language_registry.add(html_language);
21682 let mut fake_servers = language_registry.register_fake_lsp(
21683 "HTML",
21684 FakeLspAdapter {
21685 capabilities: lsp::ServerCapabilities {
21686 completion_provider: Some(lsp::CompletionOptions {
21687 resolve_provider: Some(true),
21688 ..Default::default()
21689 }),
21690 ..Default::default()
21691 },
21692 ..Default::default()
21693 },
21694 );
21695
21696 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21697 let cx = &mut VisualTestContext::from_window(*workspace, cx);
21698
21699 let worktree_id = workspace
21700 .update(cx, |workspace, _window, cx| {
21701 workspace.project().update(cx, |project, cx| {
21702 project.worktrees(cx).next().unwrap().read(cx).id()
21703 })
21704 })
21705 .unwrap();
21706 project
21707 .update(cx, |project, cx| {
21708 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
21709 })
21710 .await
21711 .unwrap();
21712 let editor = workspace
21713 .update(cx, |workspace, window, cx| {
21714 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
21715 })
21716 .unwrap()
21717 .await
21718 .unwrap()
21719 .downcast::<Editor>()
21720 .unwrap();
21721
21722 let fake_server = fake_servers.next().await.unwrap();
21723 editor.update_in(cx, |editor, window, cx| {
21724 editor.set_text("<ad></ad>", window, cx);
21725 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21726 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
21727 });
21728 let Some((buffer, _)) = editor
21729 .buffer
21730 .read(cx)
21731 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
21732 else {
21733 panic!("Failed to get buffer for selection position");
21734 };
21735 let buffer = buffer.read(cx);
21736 let buffer_id = buffer.remote_id();
21737 let opening_range =
21738 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
21739 let closing_range =
21740 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
21741 let mut linked_ranges = HashMap::default();
21742 linked_ranges.insert(
21743 buffer_id,
21744 vec![(opening_range.clone(), vec![closing_range.clone()])],
21745 );
21746 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
21747 });
21748 let mut completion_handle =
21749 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
21750 Ok(Some(lsp::CompletionResponse::Array(vec![
21751 lsp::CompletionItem {
21752 label: "head".to_string(),
21753 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21754 lsp::InsertReplaceEdit {
21755 new_text: "head".to_string(),
21756 insert: lsp::Range::new(
21757 lsp::Position::new(0, 1),
21758 lsp::Position::new(0, 3),
21759 ),
21760 replace: lsp::Range::new(
21761 lsp::Position::new(0, 1),
21762 lsp::Position::new(0, 3),
21763 ),
21764 },
21765 )),
21766 ..Default::default()
21767 },
21768 ])))
21769 });
21770 editor.update_in(cx, |editor, window, cx| {
21771 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
21772 });
21773 cx.run_until_parked();
21774 completion_handle.next().await.unwrap();
21775 editor.update(cx, |editor, _| {
21776 assert!(
21777 editor.context_menu_visible(),
21778 "Completion menu should be visible"
21779 );
21780 });
21781 editor.update_in(cx, |editor, window, cx| {
21782 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
21783 });
21784 cx.executor().run_until_parked();
21785 editor.update(cx, |editor, cx| {
21786 assert_eq!(editor.text(cx), "<head></head>");
21787 });
21788}
21789
21790#[gpui::test]
21791async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
21792 init_test(cx, |_| {});
21793
21794 let fs = FakeFs::new(cx.executor());
21795 fs.insert_tree(
21796 path!("/root"),
21797 json!({
21798 "a": {
21799 "main.rs": "fn main() {}",
21800 },
21801 "foo": {
21802 "bar": {
21803 "external_file.rs": "pub mod external {}",
21804 }
21805 }
21806 }),
21807 )
21808 .await;
21809
21810 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
21811 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21812 language_registry.add(rust_lang());
21813 let _fake_servers = language_registry.register_fake_lsp(
21814 "Rust",
21815 FakeLspAdapter {
21816 ..FakeLspAdapter::default()
21817 },
21818 );
21819 let (workspace, cx) =
21820 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21821 let worktree_id = workspace.update(cx, |workspace, cx| {
21822 workspace.project().update(cx, |project, cx| {
21823 project.worktrees(cx).next().unwrap().read(cx).id()
21824 })
21825 });
21826
21827 let assert_language_servers_count =
21828 |expected: usize, context: &str, cx: &mut VisualTestContext| {
21829 project.update(cx, |project, cx| {
21830 let current = project
21831 .lsp_store()
21832 .read(cx)
21833 .as_local()
21834 .unwrap()
21835 .language_servers
21836 .len();
21837 assert_eq!(expected, current, "{context}");
21838 });
21839 };
21840
21841 assert_language_servers_count(
21842 0,
21843 "No servers should be running before any file is open",
21844 cx,
21845 );
21846 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21847 let main_editor = workspace
21848 .update_in(cx, |workspace, window, cx| {
21849 workspace.open_path(
21850 (worktree_id, "main.rs"),
21851 Some(pane.downgrade()),
21852 true,
21853 window,
21854 cx,
21855 )
21856 })
21857 .unwrap()
21858 .await
21859 .downcast::<Editor>()
21860 .unwrap();
21861 pane.update(cx, |pane, cx| {
21862 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21863 open_editor.update(cx, |editor, cx| {
21864 assert_eq!(
21865 editor.display_text(cx),
21866 "fn main() {}",
21867 "Original main.rs text on initial open",
21868 );
21869 });
21870 assert_eq!(open_editor, main_editor);
21871 });
21872 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
21873
21874 let external_editor = workspace
21875 .update_in(cx, |workspace, window, cx| {
21876 workspace.open_abs_path(
21877 PathBuf::from("/root/foo/bar/external_file.rs"),
21878 OpenOptions::default(),
21879 window,
21880 cx,
21881 )
21882 })
21883 .await
21884 .expect("opening external file")
21885 .downcast::<Editor>()
21886 .expect("downcasted external file's open element to editor");
21887 pane.update(cx, |pane, cx| {
21888 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21889 open_editor.update(cx, |editor, cx| {
21890 assert_eq!(
21891 editor.display_text(cx),
21892 "pub mod external {}",
21893 "External file is open now",
21894 );
21895 });
21896 assert_eq!(open_editor, external_editor);
21897 });
21898 assert_language_servers_count(
21899 1,
21900 "Second, external, *.rs file should join the existing server",
21901 cx,
21902 );
21903
21904 pane.update_in(cx, |pane, window, cx| {
21905 pane.close_active_item(&CloseActiveItem::default(), window, cx)
21906 })
21907 .await
21908 .unwrap();
21909 pane.update_in(cx, |pane, window, cx| {
21910 pane.navigate_backward(window, cx);
21911 });
21912 cx.run_until_parked();
21913 pane.update(cx, |pane, cx| {
21914 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21915 open_editor.update(cx, |editor, cx| {
21916 assert_eq!(
21917 editor.display_text(cx),
21918 "pub mod external {}",
21919 "External file is open now",
21920 );
21921 });
21922 });
21923 assert_language_servers_count(
21924 1,
21925 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
21926 cx,
21927 );
21928
21929 cx.update(|_, cx| {
21930 workspace::reload(&workspace::Reload::default(), cx);
21931 });
21932 assert_language_servers_count(
21933 1,
21934 "After reloading the worktree with local and external files opened, only one project should be started",
21935 cx,
21936 );
21937}
21938
21939#[gpui::test]
21940async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
21941 init_test(cx, |_| {});
21942
21943 let mut cx = EditorTestContext::new(cx).await;
21944 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21945 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21946
21947 // test cursor move to start of each line on tab
21948 // for `if`, `elif`, `else`, `while`, `with` and `for`
21949 cx.set_state(indoc! {"
21950 def main():
21951 ˇ for item in items:
21952 ˇ while item.active:
21953 ˇ if item.value > 10:
21954 ˇ continue
21955 ˇ elif item.value < 0:
21956 ˇ break
21957 ˇ else:
21958 ˇ with item.context() as ctx:
21959 ˇ yield count
21960 ˇ else:
21961 ˇ log('while else')
21962 ˇ else:
21963 ˇ log('for else')
21964 "});
21965 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21966 cx.assert_editor_state(indoc! {"
21967 def main():
21968 ˇfor item in items:
21969 ˇwhile item.active:
21970 ˇif item.value > 10:
21971 ˇcontinue
21972 ˇelif item.value < 0:
21973 ˇbreak
21974 ˇelse:
21975 ˇwith item.context() as ctx:
21976 ˇyield count
21977 ˇelse:
21978 ˇlog('while else')
21979 ˇelse:
21980 ˇlog('for else')
21981 "});
21982 // test relative indent is preserved when tab
21983 // for `if`, `elif`, `else`, `while`, `with` and `for`
21984 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
21985 cx.assert_editor_state(indoc! {"
21986 def main():
21987 ˇfor item in items:
21988 ˇwhile item.active:
21989 ˇif item.value > 10:
21990 ˇcontinue
21991 ˇelif item.value < 0:
21992 ˇbreak
21993 ˇelse:
21994 ˇwith item.context() as ctx:
21995 ˇyield count
21996 ˇelse:
21997 ˇlog('while else')
21998 ˇelse:
21999 ˇlog('for else')
22000 "});
22001
22002 // test cursor move to start of each line on tab
22003 // for `try`, `except`, `else`, `finally`, `match` and `def`
22004 cx.set_state(indoc! {"
22005 def main():
22006 ˇ try:
22007 ˇ fetch()
22008 ˇ except ValueError:
22009 ˇ handle_error()
22010 ˇ else:
22011 ˇ match value:
22012 ˇ case _:
22013 ˇ finally:
22014 ˇ def status():
22015 ˇ return 0
22016 "});
22017 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22018 cx.assert_editor_state(indoc! {"
22019 def main():
22020 ˇtry:
22021 ˇfetch()
22022 ˇexcept ValueError:
22023 ˇhandle_error()
22024 ˇelse:
22025 ˇmatch value:
22026 ˇcase _:
22027 ˇfinally:
22028 ˇdef status():
22029 ˇreturn 0
22030 "});
22031 // test relative indent is preserved when tab
22032 // for `try`, `except`, `else`, `finally`, `match` and `def`
22033 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22034 cx.assert_editor_state(indoc! {"
22035 def main():
22036 ˇtry:
22037 ˇfetch()
22038 ˇexcept ValueError:
22039 ˇhandle_error()
22040 ˇelse:
22041 ˇmatch value:
22042 ˇcase _:
22043 ˇfinally:
22044 ˇdef status():
22045 ˇreturn 0
22046 "});
22047}
22048
22049#[gpui::test]
22050async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
22051 init_test(cx, |_| {});
22052
22053 let mut cx = EditorTestContext::new(cx).await;
22054 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22055 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22056
22057 // test `else` auto outdents when typed inside `if` block
22058 cx.set_state(indoc! {"
22059 def main():
22060 if i == 2:
22061 return
22062 ˇ
22063 "});
22064 cx.update_editor(|editor, window, cx| {
22065 editor.handle_input("else:", window, cx);
22066 });
22067 cx.assert_editor_state(indoc! {"
22068 def main():
22069 if i == 2:
22070 return
22071 else:ˇ
22072 "});
22073
22074 // test `except` auto outdents when typed inside `try` block
22075 cx.set_state(indoc! {"
22076 def main():
22077 try:
22078 i = 2
22079 ˇ
22080 "});
22081 cx.update_editor(|editor, window, cx| {
22082 editor.handle_input("except:", window, cx);
22083 });
22084 cx.assert_editor_state(indoc! {"
22085 def main():
22086 try:
22087 i = 2
22088 except:ˇ
22089 "});
22090
22091 // test `else` auto outdents when typed inside `except` block
22092 cx.set_state(indoc! {"
22093 def main():
22094 try:
22095 i = 2
22096 except:
22097 j = 2
22098 ˇ
22099 "});
22100 cx.update_editor(|editor, window, cx| {
22101 editor.handle_input("else:", window, cx);
22102 });
22103 cx.assert_editor_state(indoc! {"
22104 def main():
22105 try:
22106 i = 2
22107 except:
22108 j = 2
22109 else:ˇ
22110 "});
22111
22112 // test `finally` auto outdents when typed inside `else` block
22113 cx.set_state(indoc! {"
22114 def main():
22115 try:
22116 i = 2
22117 except:
22118 j = 2
22119 else:
22120 k = 2
22121 ˇ
22122 "});
22123 cx.update_editor(|editor, window, cx| {
22124 editor.handle_input("finally:", window, cx);
22125 });
22126 cx.assert_editor_state(indoc! {"
22127 def main():
22128 try:
22129 i = 2
22130 except:
22131 j = 2
22132 else:
22133 k = 2
22134 finally:ˇ
22135 "});
22136
22137 // test `else` does not outdents when typed inside `except` block right after for block
22138 cx.set_state(indoc! {"
22139 def main():
22140 try:
22141 i = 2
22142 except:
22143 for i in range(n):
22144 pass
22145 ˇ
22146 "});
22147 cx.update_editor(|editor, window, cx| {
22148 editor.handle_input("else:", window, cx);
22149 });
22150 cx.assert_editor_state(indoc! {"
22151 def main():
22152 try:
22153 i = 2
22154 except:
22155 for i in range(n):
22156 pass
22157 else:ˇ
22158 "});
22159
22160 // test `finally` auto outdents when typed inside `else` block right after for block
22161 cx.set_state(indoc! {"
22162 def main():
22163 try:
22164 i = 2
22165 except:
22166 j = 2
22167 else:
22168 for i in range(n):
22169 pass
22170 ˇ
22171 "});
22172 cx.update_editor(|editor, window, cx| {
22173 editor.handle_input("finally:", window, cx);
22174 });
22175 cx.assert_editor_state(indoc! {"
22176 def main():
22177 try:
22178 i = 2
22179 except:
22180 j = 2
22181 else:
22182 for i in range(n):
22183 pass
22184 finally:ˇ
22185 "});
22186
22187 // test `except` outdents to inner "try" block
22188 cx.set_state(indoc! {"
22189 def main():
22190 try:
22191 i = 2
22192 if i == 2:
22193 try:
22194 i = 3
22195 ˇ
22196 "});
22197 cx.update_editor(|editor, window, cx| {
22198 editor.handle_input("except:", window, cx);
22199 });
22200 cx.assert_editor_state(indoc! {"
22201 def main():
22202 try:
22203 i = 2
22204 if i == 2:
22205 try:
22206 i = 3
22207 except:ˇ
22208 "});
22209
22210 // test `except` outdents to outer "try" block
22211 cx.set_state(indoc! {"
22212 def main():
22213 try:
22214 i = 2
22215 if i == 2:
22216 try:
22217 i = 3
22218 ˇ
22219 "});
22220 cx.update_editor(|editor, window, cx| {
22221 editor.handle_input("except:", window, cx);
22222 });
22223 cx.assert_editor_state(indoc! {"
22224 def main():
22225 try:
22226 i = 2
22227 if i == 2:
22228 try:
22229 i = 3
22230 except:ˇ
22231 "});
22232
22233 // test `else` stays at correct indent when typed after `for` block
22234 cx.set_state(indoc! {"
22235 def main():
22236 for i in range(10):
22237 if i == 3:
22238 break
22239 ˇ
22240 "});
22241 cx.update_editor(|editor, window, cx| {
22242 editor.handle_input("else:", window, cx);
22243 });
22244 cx.assert_editor_state(indoc! {"
22245 def main():
22246 for i in range(10):
22247 if i == 3:
22248 break
22249 else:ˇ
22250 "});
22251
22252 // test does not outdent on typing after line with square brackets
22253 cx.set_state(indoc! {"
22254 def f() -> list[str]:
22255 ˇ
22256 "});
22257 cx.update_editor(|editor, window, cx| {
22258 editor.handle_input("a", window, cx);
22259 });
22260 cx.assert_editor_state(indoc! {"
22261 def f() -> list[str]:
22262 aˇ
22263 "});
22264}
22265
22266#[gpui::test]
22267async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
22268 init_test(cx, |_| {});
22269 update_test_language_settings(cx, |settings| {
22270 settings.defaults.extend_comment_on_newline = Some(false);
22271 });
22272 let mut cx = EditorTestContext::new(cx).await;
22273 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22274 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22275
22276 // test correct indent after newline on comment
22277 cx.set_state(indoc! {"
22278 # COMMENT:ˇ
22279 "});
22280 cx.update_editor(|editor, window, cx| {
22281 editor.newline(&Newline, window, cx);
22282 });
22283 cx.assert_editor_state(indoc! {"
22284 # COMMENT:
22285 ˇ
22286 "});
22287
22288 // test correct indent after newline in brackets
22289 cx.set_state(indoc! {"
22290 {ˇ}
22291 "});
22292 cx.update_editor(|editor, window, cx| {
22293 editor.newline(&Newline, window, cx);
22294 });
22295 cx.run_until_parked();
22296 cx.assert_editor_state(indoc! {"
22297 {
22298 ˇ
22299 }
22300 "});
22301
22302 cx.set_state(indoc! {"
22303 (ˇ)
22304 "});
22305 cx.update_editor(|editor, window, cx| {
22306 editor.newline(&Newline, window, cx);
22307 });
22308 cx.run_until_parked();
22309 cx.assert_editor_state(indoc! {"
22310 (
22311 ˇ
22312 )
22313 "});
22314
22315 // do not indent after empty lists or dictionaries
22316 cx.set_state(indoc! {"
22317 a = []ˇ
22318 "});
22319 cx.update_editor(|editor, window, cx| {
22320 editor.newline(&Newline, window, cx);
22321 });
22322 cx.run_until_parked();
22323 cx.assert_editor_state(indoc! {"
22324 a = []
22325 ˇ
22326 "});
22327}
22328
22329fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
22330 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
22331 point..point
22332}
22333
22334#[track_caller]
22335fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
22336 let (text, ranges) = marked_text_ranges(marked_text, true);
22337 assert_eq!(editor.text(cx), text);
22338 assert_eq!(
22339 editor.selections.ranges(cx),
22340 ranges,
22341 "Assert selections are {}",
22342 marked_text
22343 );
22344}
22345
22346pub fn handle_signature_help_request(
22347 cx: &mut EditorLspTestContext,
22348 mocked_response: lsp::SignatureHelp,
22349) -> impl Future<Output = ()> + use<> {
22350 let mut request =
22351 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
22352 let mocked_response = mocked_response.clone();
22353 async move { Ok(Some(mocked_response)) }
22354 });
22355
22356 async move {
22357 request.next().await;
22358 }
22359}
22360
22361#[track_caller]
22362pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
22363 cx.update_editor(|editor, _, _| {
22364 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
22365 let entries = menu.entries.borrow();
22366 let entries = entries
22367 .iter()
22368 .map(|entry| entry.string.as_str())
22369 .collect::<Vec<_>>();
22370 assert_eq!(entries, expected);
22371 } else {
22372 panic!("Expected completions menu");
22373 }
22374 });
22375}
22376
22377/// Handle completion request passing a marked string specifying where the completion
22378/// should be triggered from using '|' character, what range should be replaced, and what completions
22379/// should be returned using '<' and '>' to delimit the range.
22380///
22381/// Also see `handle_completion_request_with_insert_and_replace`.
22382#[track_caller]
22383pub fn handle_completion_request(
22384 marked_string: &str,
22385 completions: Vec<&'static str>,
22386 is_incomplete: bool,
22387 counter: Arc<AtomicUsize>,
22388 cx: &mut EditorLspTestContext,
22389) -> impl Future<Output = ()> {
22390 let complete_from_marker: TextRangeMarker = '|'.into();
22391 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22392 let (_, mut marked_ranges) = marked_text_ranges_by(
22393 marked_string,
22394 vec![complete_from_marker.clone(), replace_range_marker.clone()],
22395 );
22396
22397 let complete_from_position =
22398 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22399 let replace_range =
22400 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22401
22402 let mut request =
22403 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22404 let completions = completions.clone();
22405 counter.fetch_add(1, atomic::Ordering::Release);
22406 async move {
22407 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22408 assert_eq!(
22409 params.text_document_position.position,
22410 complete_from_position
22411 );
22412 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
22413 is_incomplete: is_incomplete,
22414 item_defaults: None,
22415 items: completions
22416 .iter()
22417 .map(|completion_text| lsp::CompletionItem {
22418 label: completion_text.to_string(),
22419 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
22420 range: replace_range,
22421 new_text: completion_text.to_string(),
22422 })),
22423 ..Default::default()
22424 })
22425 .collect(),
22426 })))
22427 }
22428 });
22429
22430 async move {
22431 request.next().await;
22432 }
22433}
22434
22435/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
22436/// given instead, which also contains an `insert` range.
22437///
22438/// This function uses markers to define ranges:
22439/// - `|` marks the cursor position
22440/// - `<>` marks the replace range
22441/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
22442pub fn handle_completion_request_with_insert_and_replace(
22443 cx: &mut EditorLspTestContext,
22444 marked_string: &str,
22445 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
22446 counter: Arc<AtomicUsize>,
22447) -> impl Future<Output = ()> {
22448 let complete_from_marker: TextRangeMarker = '|'.into();
22449 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22450 let insert_range_marker: TextRangeMarker = ('{', '}').into();
22451
22452 let (_, mut marked_ranges) = marked_text_ranges_by(
22453 marked_string,
22454 vec![
22455 complete_from_marker.clone(),
22456 replace_range_marker.clone(),
22457 insert_range_marker.clone(),
22458 ],
22459 );
22460
22461 let complete_from_position =
22462 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22463 let replace_range =
22464 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22465
22466 let insert_range = match marked_ranges.remove(&insert_range_marker) {
22467 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
22468 _ => lsp::Range {
22469 start: replace_range.start,
22470 end: complete_from_position,
22471 },
22472 };
22473
22474 let mut request =
22475 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22476 let completions = completions.clone();
22477 counter.fetch_add(1, atomic::Ordering::Release);
22478 async move {
22479 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22480 assert_eq!(
22481 params.text_document_position.position, complete_from_position,
22482 "marker `|` position doesn't match",
22483 );
22484 Ok(Some(lsp::CompletionResponse::Array(
22485 completions
22486 .iter()
22487 .map(|(label, new_text)| lsp::CompletionItem {
22488 label: label.to_string(),
22489 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22490 lsp::InsertReplaceEdit {
22491 insert: insert_range,
22492 replace: replace_range,
22493 new_text: new_text.to_string(),
22494 },
22495 )),
22496 ..Default::default()
22497 })
22498 .collect(),
22499 )))
22500 }
22501 });
22502
22503 async move {
22504 request.next().await;
22505 }
22506}
22507
22508fn handle_resolve_completion_request(
22509 cx: &mut EditorLspTestContext,
22510 edits: Option<Vec<(&'static str, &'static str)>>,
22511) -> impl Future<Output = ()> {
22512 let edits = edits.map(|edits| {
22513 edits
22514 .iter()
22515 .map(|(marked_string, new_text)| {
22516 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
22517 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
22518 lsp::TextEdit::new(replace_range, new_text.to_string())
22519 })
22520 .collect::<Vec<_>>()
22521 });
22522
22523 let mut request =
22524 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
22525 let edits = edits.clone();
22526 async move {
22527 Ok(lsp::CompletionItem {
22528 additional_text_edits: edits,
22529 ..Default::default()
22530 })
22531 }
22532 });
22533
22534 async move {
22535 request.next().await;
22536 }
22537}
22538
22539pub(crate) fn update_test_language_settings(
22540 cx: &mut TestAppContext,
22541 f: impl Fn(&mut AllLanguageSettingsContent),
22542) {
22543 cx.update(|cx| {
22544 SettingsStore::update_global(cx, |store, cx| {
22545 store.update_user_settings::<AllLanguageSettings>(cx, f);
22546 });
22547 });
22548}
22549
22550pub(crate) fn update_test_project_settings(
22551 cx: &mut TestAppContext,
22552 f: impl Fn(&mut ProjectSettings),
22553) {
22554 cx.update(|cx| {
22555 SettingsStore::update_global(cx, |store, cx| {
22556 store.update_user_settings::<ProjectSettings>(cx, f);
22557 });
22558 });
22559}
22560
22561pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
22562 cx.update(|cx| {
22563 assets::Assets.load_test_fonts(cx);
22564 let store = SettingsStore::test(cx);
22565 cx.set_global(store);
22566 theme::init(theme::LoadThemes::JustBase, cx);
22567 release_channel::init(SemanticVersion::default(), cx);
22568 client::init_settings(cx);
22569 language::init(cx);
22570 Project::init_settings(cx);
22571 workspace::init_settings(cx);
22572 crate::init(cx);
22573 });
22574
22575 update_test_language_settings(cx, f);
22576}
22577
22578#[track_caller]
22579fn assert_hunk_revert(
22580 not_reverted_text_with_selections: &str,
22581 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
22582 expected_reverted_text_with_selections: &str,
22583 base_text: &str,
22584 cx: &mut EditorLspTestContext,
22585) {
22586 cx.set_state(not_reverted_text_with_selections);
22587 cx.set_head_text(base_text);
22588 cx.executor().run_until_parked();
22589
22590 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
22591 let snapshot = editor.snapshot(window, cx);
22592 let reverted_hunk_statuses = snapshot
22593 .buffer_snapshot
22594 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
22595 .map(|hunk| hunk.status().kind)
22596 .collect::<Vec<_>>();
22597
22598 editor.git_restore(&Default::default(), window, cx);
22599 reverted_hunk_statuses
22600 });
22601 cx.executor().run_until_parked();
22602 cx.assert_editor_state(expected_reverted_text_with_selections);
22603 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
22604}
22605
22606#[gpui::test(iterations = 10)]
22607async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
22608 init_test(cx, |_| {});
22609
22610 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
22611 let counter = diagnostic_requests.clone();
22612
22613 let fs = FakeFs::new(cx.executor());
22614 fs.insert_tree(
22615 path!("/a"),
22616 json!({
22617 "first.rs": "fn main() { let a = 5; }",
22618 "second.rs": "// Test file",
22619 }),
22620 )
22621 .await;
22622
22623 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22624 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22625 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22626
22627 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22628 language_registry.add(rust_lang());
22629 let mut fake_servers = language_registry.register_fake_lsp(
22630 "Rust",
22631 FakeLspAdapter {
22632 capabilities: lsp::ServerCapabilities {
22633 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
22634 lsp::DiagnosticOptions {
22635 identifier: None,
22636 inter_file_dependencies: true,
22637 workspace_diagnostics: true,
22638 work_done_progress_options: Default::default(),
22639 },
22640 )),
22641 ..Default::default()
22642 },
22643 ..Default::default()
22644 },
22645 );
22646
22647 let editor = workspace
22648 .update(cx, |workspace, window, cx| {
22649 workspace.open_abs_path(
22650 PathBuf::from(path!("/a/first.rs")),
22651 OpenOptions::default(),
22652 window,
22653 cx,
22654 )
22655 })
22656 .unwrap()
22657 .await
22658 .unwrap()
22659 .downcast::<Editor>()
22660 .unwrap();
22661 let fake_server = fake_servers.next().await.unwrap();
22662 let server_id = fake_server.server.server_id();
22663 let mut first_request = fake_server
22664 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
22665 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
22666 let result_id = Some(new_result_id.to_string());
22667 assert_eq!(
22668 params.text_document.uri,
22669 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
22670 );
22671 async move {
22672 Ok(lsp::DocumentDiagnosticReportResult::Report(
22673 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
22674 related_documents: None,
22675 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
22676 items: Vec::new(),
22677 result_id,
22678 },
22679 }),
22680 ))
22681 }
22682 });
22683
22684 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
22685 project.update(cx, |project, cx| {
22686 let buffer_id = editor
22687 .read(cx)
22688 .buffer()
22689 .read(cx)
22690 .as_singleton()
22691 .expect("created a singleton buffer")
22692 .read(cx)
22693 .remote_id();
22694 let buffer_result_id = project
22695 .lsp_store()
22696 .read(cx)
22697 .result_id(server_id, buffer_id, cx);
22698 assert_eq!(expected, buffer_result_id);
22699 });
22700 };
22701
22702 ensure_result_id(None, cx);
22703 cx.executor().advance_clock(Duration::from_millis(60));
22704 cx.executor().run_until_parked();
22705 assert_eq!(
22706 diagnostic_requests.load(atomic::Ordering::Acquire),
22707 1,
22708 "Opening file should trigger diagnostic request"
22709 );
22710 first_request
22711 .next()
22712 .await
22713 .expect("should have sent the first diagnostics pull request");
22714 ensure_result_id(Some("1".to_string()), cx);
22715
22716 // Editing should trigger diagnostics
22717 editor.update_in(cx, |editor, window, cx| {
22718 editor.handle_input("2", window, cx)
22719 });
22720 cx.executor().advance_clock(Duration::from_millis(60));
22721 cx.executor().run_until_parked();
22722 assert_eq!(
22723 diagnostic_requests.load(atomic::Ordering::Acquire),
22724 2,
22725 "Editing should trigger diagnostic request"
22726 );
22727 ensure_result_id(Some("2".to_string()), cx);
22728
22729 // Moving cursor should not trigger diagnostic request
22730 editor.update_in(cx, |editor, window, cx| {
22731 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22732 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
22733 });
22734 });
22735 cx.executor().advance_clock(Duration::from_millis(60));
22736 cx.executor().run_until_parked();
22737 assert_eq!(
22738 diagnostic_requests.load(atomic::Ordering::Acquire),
22739 2,
22740 "Cursor movement should not trigger diagnostic request"
22741 );
22742 ensure_result_id(Some("2".to_string()), cx);
22743 // Multiple rapid edits should be debounced
22744 for _ in 0..5 {
22745 editor.update_in(cx, |editor, window, cx| {
22746 editor.handle_input("x", window, cx)
22747 });
22748 }
22749 cx.executor().advance_clock(Duration::from_millis(60));
22750 cx.executor().run_until_parked();
22751
22752 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
22753 assert!(
22754 final_requests <= 4,
22755 "Multiple rapid edits should be debounced (got {final_requests} requests)",
22756 );
22757 ensure_result_id(Some(final_requests.to_string()), cx);
22758}
22759
22760#[gpui::test]
22761async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
22762 // Regression test for issue #11671
22763 // Previously, adding a cursor after moving multiple cursors would reset
22764 // the cursor count instead of adding to the existing cursors.
22765 init_test(cx, |_| {});
22766 let mut cx = EditorTestContext::new(cx).await;
22767
22768 // Create a simple buffer with cursor at start
22769 cx.set_state(indoc! {"
22770 ˇaaaa
22771 bbbb
22772 cccc
22773 dddd
22774 eeee
22775 ffff
22776 gggg
22777 hhhh"});
22778
22779 // Add 2 cursors below (so we have 3 total)
22780 cx.update_editor(|editor, window, cx| {
22781 editor.add_selection_below(&Default::default(), window, cx);
22782 editor.add_selection_below(&Default::default(), window, cx);
22783 });
22784
22785 // Verify we have 3 cursors
22786 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
22787 assert_eq!(
22788 initial_count, 3,
22789 "Should have 3 cursors after adding 2 below"
22790 );
22791
22792 // Move down one line
22793 cx.update_editor(|editor, window, cx| {
22794 editor.move_down(&MoveDown, window, cx);
22795 });
22796
22797 // Add another cursor below
22798 cx.update_editor(|editor, window, cx| {
22799 editor.add_selection_below(&Default::default(), window, cx);
22800 });
22801
22802 // Should now have 4 cursors (3 original + 1 new)
22803 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
22804 assert_eq!(
22805 final_count, 4,
22806 "Should have 4 cursors after moving and adding another"
22807 );
22808}
22809
22810#[gpui::test(iterations = 10)]
22811async fn test_document_colors(cx: &mut TestAppContext) {
22812 let expected_color = Rgba {
22813 r: 0.33,
22814 g: 0.33,
22815 b: 0.33,
22816 a: 0.33,
22817 };
22818
22819 init_test(cx, |_| {});
22820
22821 let fs = FakeFs::new(cx.executor());
22822 fs.insert_tree(
22823 path!("/a"),
22824 json!({
22825 "first.rs": "fn main() { let a = 5; }",
22826 }),
22827 )
22828 .await;
22829
22830 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22831 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22832 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22833
22834 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22835 language_registry.add(rust_lang());
22836 let mut fake_servers = language_registry.register_fake_lsp(
22837 "Rust",
22838 FakeLspAdapter {
22839 capabilities: lsp::ServerCapabilities {
22840 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
22841 ..lsp::ServerCapabilities::default()
22842 },
22843 name: "rust-analyzer",
22844 ..FakeLspAdapter::default()
22845 },
22846 );
22847 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
22848 "Rust",
22849 FakeLspAdapter {
22850 capabilities: lsp::ServerCapabilities {
22851 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
22852 ..lsp::ServerCapabilities::default()
22853 },
22854 name: "not-rust-analyzer",
22855 ..FakeLspAdapter::default()
22856 },
22857 );
22858
22859 let editor = workspace
22860 .update(cx, |workspace, window, cx| {
22861 workspace.open_abs_path(
22862 PathBuf::from(path!("/a/first.rs")),
22863 OpenOptions::default(),
22864 window,
22865 cx,
22866 )
22867 })
22868 .unwrap()
22869 .await
22870 .unwrap()
22871 .downcast::<Editor>()
22872 .unwrap();
22873 let fake_language_server = fake_servers.next().await.unwrap();
22874 let fake_language_server_without_capabilities =
22875 fake_servers_without_capabilities.next().await.unwrap();
22876 let requests_made = Arc::new(AtomicUsize::new(0));
22877 let closure_requests_made = Arc::clone(&requests_made);
22878 let mut color_request_handle = fake_language_server
22879 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
22880 let requests_made = Arc::clone(&closure_requests_made);
22881 async move {
22882 assert_eq!(
22883 params.text_document.uri,
22884 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
22885 );
22886 requests_made.fetch_add(1, atomic::Ordering::Release);
22887 Ok(vec![
22888 lsp::ColorInformation {
22889 range: lsp::Range {
22890 start: lsp::Position {
22891 line: 0,
22892 character: 0,
22893 },
22894 end: lsp::Position {
22895 line: 0,
22896 character: 1,
22897 },
22898 },
22899 color: lsp::Color {
22900 red: 0.33,
22901 green: 0.33,
22902 blue: 0.33,
22903 alpha: 0.33,
22904 },
22905 },
22906 lsp::ColorInformation {
22907 range: lsp::Range {
22908 start: lsp::Position {
22909 line: 0,
22910 character: 0,
22911 },
22912 end: lsp::Position {
22913 line: 0,
22914 character: 1,
22915 },
22916 },
22917 color: lsp::Color {
22918 red: 0.33,
22919 green: 0.33,
22920 blue: 0.33,
22921 alpha: 0.33,
22922 },
22923 },
22924 ])
22925 }
22926 });
22927
22928 let _handle = fake_language_server_without_capabilities
22929 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
22930 panic!("Should not be called");
22931 });
22932 cx.executor().advance_clock(Duration::from_millis(100));
22933 color_request_handle.next().await.unwrap();
22934 cx.run_until_parked();
22935 assert_eq!(
22936 1,
22937 requests_made.load(atomic::Ordering::Acquire),
22938 "Should query for colors once per editor open"
22939 );
22940 editor.update_in(cx, |editor, _, cx| {
22941 assert_eq!(
22942 vec![expected_color],
22943 extract_color_inlays(editor, cx),
22944 "Should have an initial inlay"
22945 );
22946 });
22947
22948 // opening another file in a split should not influence the LSP query counter
22949 workspace
22950 .update(cx, |workspace, window, cx| {
22951 assert_eq!(
22952 workspace.panes().len(),
22953 1,
22954 "Should have one pane with one editor"
22955 );
22956 workspace.move_item_to_pane_in_direction(
22957 &MoveItemToPaneInDirection {
22958 direction: SplitDirection::Right,
22959 focus: false,
22960 clone: true,
22961 },
22962 window,
22963 cx,
22964 );
22965 })
22966 .unwrap();
22967 cx.run_until_parked();
22968 workspace
22969 .update(cx, |workspace, _, cx| {
22970 let panes = workspace.panes();
22971 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
22972 for pane in panes {
22973 let editor = pane
22974 .read(cx)
22975 .active_item()
22976 .and_then(|item| item.downcast::<Editor>())
22977 .expect("Should have opened an editor in each split");
22978 let editor_file = editor
22979 .read(cx)
22980 .buffer()
22981 .read(cx)
22982 .as_singleton()
22983 .expect("test deals with singleton buffers")
22984 .read(cx)
22985 .file()
22986 .expect("test buffese should have a file")
22987 .path();
22988 assert_eq!(
22989 editor_file.as_ref(),
22990 Path::new("first.rs"),
22991 "Both editors should be opened for the same file"
22992 )
22993 }
22994 })
22995 .unwrap();
22996
22997 cx.executor().advance_clock(Duration::from_millis(500));
22998 let save = editor.update_in(cx, |editor, window, cx| {
22999 editor.move_to_end(&MoveToEnd, window, cx);
23000 editor.handle_input("dirty", window, cx);
23001 editor.save(
23002 SaveOptions {
23003 format: true,
23004 autosave: true,
23005 },
23006 project.clone(),
23007 window,
23008 cx,
23009 )
23010 });
23011 save.await.unwrap();
23012
23013 color_request_handle.next().await.unwrap();
23014 cx.run_until_parked();
23015 assert_eq!(
23016 3,
23017 requests_made.load(atomic::Ordering::Acquire),
23018 "Should query for colors once per save and once per formatting after save"
23019 );
23020
23021 drop(editor);
23022 let close = workspace
23023 .update(cx, |workspace, window, cx| {
23024 workspace.active_pane().update(cx, |pane, cx| {
23025 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23026 })
23027 })
23028 .unwrap();
23029 close.await.unwrap();
23030 let close = workspace
23031 .update(cx, |workspace, window, cx| {
23032 workspace.active_pane().update(cx, |pane, cx| {
23033 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23034 })
23035 })
23036 .unwrap();
23037 close.await.unwrap();
23038 assert_eq!(
23039 3,
23040 requests_made.load(atomic::Ordering::Acquire),
23041 "After saving and closing all editors, no extra requests should be made"
23042 );
23043 workspace
23044 .update(cx, |workspace, _, cx| {
23045 assert!(
23046 workspace.active_item(cx).is_none(),
23047 "Should close all editors"
23048 )
23049 })
23050 .unwrap();
23051
23052 workspace
23053 .update(cx, |workspace, window, cx| {
23054 workspace.active_pane().update(cx, |pane, cx| {
23055 pane.navigate_backward(window, cx);
23056 })
23057 })
23058 .unwrap();
23059 cx.executor().advance_clock(Duration::from_millis(100));
23060 cx.run_until_parked();
23061 let editor = workspace
23062 .update(cx, |workspace, _, cx| {
23063 workspace
23064 .active_item(cx)
23065 .expect("Should have reopened the editor again after navigating back")
23066 .downcast::<Editor>()
23067 .expect("Should be an editor")
23068 })
23069 .unwrap();
23070 color_request_handle.next().await.unwrap();
23071 assert_eq!(
23072 3,
23073 requests_made.load(atomic::Ordering::Acquire),
23074 "Cache should be reused on buffer close and reopen"
23075 );
23076 editor.update(cx, |editor, cx| {
23077 assert_eq!(
23078 vec![expected_color],
23079 extract_color_inlays(editor, cx),
23080 "Should have an initial inlay"
23081 );
23082 });
23083}
23084
23085#[gpui::test]
23086async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
23087 init_test(cx, |_| {});
23088 let (editor, cx) = cx.add_window_view(Editor::single_line);
23089 editor.update_in(cx, |editor, window, cx| {
23090 editor.set_text("oops\n\nwow\n", window, cx)
23091 });
23092 cx.run_until_parked();
23093 editor.update(cx, |editor, cx| {
23094 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
23095 });
23096 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
23097 cx.run_until_parked();
23098 editor.update(cx, |editor, cx| {
23099 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
23100 });
23101}
23102
23103#[track_caller]
23104fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
23105 editor
23106 .all_inlays(cx)
23107 .into_iter()
23108 .filter_map(|inlay| inlay.get_color())
23109 .map(Rgba::from)
23110 .collect()
23111}