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, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
59 OpenOptions, ViewId,
60 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
61};
62
63#[gpui::test]
64fn test_edit_events(cx: &mut TestAppContext) {
65 init_test(cx, |_| {});
66
67 let buffer = cx.new(|cx| {
68 let mut buffer = language::Buffer::local("123456", cx);
69 buffer.set_group_interval(Duration::from_secs(1));
70 buffer
71 });
72
73 let events = Rc::new(RefCell::new(Vec::new()));
74 let editor1 = cx.add_window({
75 let events = events.clone();
76 |window, cx| {
77 let entity = cx.entity().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_comment: Some(language::BlockCommentConfig {
2879 start: "/**".into(),
2880 end: "*/".into(),
2881 prefix: "* ".into(),
2882 tab_size: 1,
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]
3084async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3085 init_test(cx, |settings| {
3086 settings.defaults.tab_size = NonZeroU32::new(4)
3087 });
3088
3089 let lua_language = Arc::new(Language::new(
3090 LanguageConfig {
3091 line_comments: vec!["--".into()],
3092 block_comment: Some(language::BlockCommentConfig {
3093 start: "--[[".into(),
3094 prefix: "".into(),
3095 end: "]]".into(),
3096 tab_size: 0,
3097 }),
3098 ..LanguageConfig::default()
3099 },
3100 None,
3101 ));
3102
3103 let mut cx = EditorTestContext::new(cx).await;
3104 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3105
3106 // Line with line comment should extend
3107 cx.set_state(indoc! {"
3108 --ˇ
3109 "});
3110 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3111 cx.assert_editor_state(indoc! {"
3112 --
3113 --ˇ
3114 "});
3115
3116 // Line with block comment that matches line comment should not extend
3117 cx.set_state(indoc! {"
3118 --[[ˇ
3119 "});
3120 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3121 cx.assert_editor_state(indoc! {"
3122 --[[
3123 ˇ
3124 "});
3125}
3126
3127#[gpui::test]
3128fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3129 init_test(cx, |_| {});
3130
3131 let editor = cx.add_window(|window, cx| {
3132 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3133 let mut editor = build_editor(buffer.clone(), window, cx);
3134 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3135 s.select_ranges([3..4, 11..12, 19..20])
3136 });
3137 editor
3138 });
3139
3140 _ = editor.update(cx, |editor, window, cx| {
3141 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3142 editor.buffer.update(cx, |buffer, cx| {
3143 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3144 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3145 });
3146 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3147
3148 editor.insert("Z", window, cx);
3149 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3150
3151 // The selections are moved after the inserted characters
3152 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3153 });
3154}
3155
3156#[gpui::test]
3157async fn test_tab(cx: &mut TestAppContext) {
3158 init_test(cx, |settings| {
3159 settings.defaults.tab_size = NonZeroU32::new(3)
3160 });
3161
3162 let mut cx = EditorTestContext::new(cx).await;
3163 cx.set_state(indoc! {"
3164 ˇabˇc
3165 ˇ🏀ˇ🏀ˇefg
3166 dˇ
3167 "});
3168 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3169 cx.assert_editor_state(indoc! {"
3170 ˇab ˇc
3171 ˇ🏀 ˇ🏀 ˇefg
3172 d ˇ
3173 "});
3174
3175 cx.set_state(indoc! {"
3176 a
3177 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3178 "});
3179 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3180 cx.assert_editor_state(indoc! {"
3181 a
3182 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3183 "});
3184}
3185
3186#[gpui::test]
3187async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3188 init_test(cx, |_| {});
3189
3190 let mut cx = EditorTestContext::new(cx).await;
3191 let language = Arc::new(
3192 Language::new(
3193 LanguageConfig::default(),
3194 Some(tree_sitter_rust::LANGUAGE.into()),
3195 )
3196 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3197 .unwrap(),
3198 );
3199 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3200
3201 // test when all cursors are not at suggested indent
3202 // then simply move to their suggested indent location
3203 cx.set_state(indoc! {"
3204 const a: B = (
3205 c(
3206 ˇ
3207 ˇ )
3208 );
3209 "});
3210 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3211 cx.assert_editor_state(indoc! {"
3212 const a: B = (
3213 c(
3214 ˇ
3215 ˇ)
3216 );
3217 "});
3218
3219 // test cursor already at suggested indent not moving when
3220 // other cursors are yet to reach their suggested indents
3221 cx.set_state(indoc! {"
3222 ˇ
3223 const a: B = (
3224 c(
3225 d(
3226 ˇ
3227 )
3228 ˇ
3229 ˇ )
3230 );
3231 "});
3232 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3233 cx.assert_editor_state(indoc! {"
3234 ˇ
3235 const a: B = (
3236 c(
3237 d(
3238 ˇ
3239 )
3240 ˇ
3241 ˇ)
3242 );
3243 "});
3244 // test when all cursors are at suggested indent then tab is inserted
3245 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3246 cx.assert_editor_state(indoc! {"
3247 ˇ
3248 const a: B = (
3249 c(
3250 d(
3251 ˇ
3252 )
3253 ˇ
3254 ˇ)
3255 );
3256 "});
3257
3258 // test when current indent is less than suggested indent,
3259 // we adjust line to match suggested indent and move cursor to it
3260 //
3261 // when no other cursor is at word boundary, all of them should move
3262 cx.set_state(indoc! {"
3263 const a: B = (
3264 c(
3265 d(
3266 ˇ
3267 ˇ )
3268 ˇ )
3269 );
3270 "});
3271 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3272 cx.assert_editor_state(indoc! {"
3273 const a: B = (
3274 c(
3275 d(
3276 ˇ
3277 ˇ)
3278 ˇ)
3279 );
3280 "});
3281
3282 // test when current indent is less than suggested indent,
3283 // we adjust line to match suggested indent and move cursor to it
3284 //
3285 // when some other cursor is at word boundary, it should not move
3286 cx.set_state(indoc! {"
3287 const a: B = (
3288 c(
3289 d(
3290 ˇ
3291 ˇ )
3292 ˇ)
3293 );
3294 "});
3295 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3296 cx.assert_editor_state(indoc! {"
3297 const a: B = (
3298 c(
3299 d(
3300 ˇ
3301 ˇ)
3302 ˇ)
3303 );
3304 "});
3305
3306 // test when current indent is more than suggested indent,
3307 // we just move cursor to current indent instead of suggested indent
3308 //
3309 // when no other cursor is at word boundary, all of them should move
3310 cx.set_state(indoc! {"
3311 const a: B = (
3312 c(
3313 d(
3314 ˇ
3315 ˇ )
3316 ˇ )
3317 );
3318 "});
3319 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3320 cx.assert_editor_state(indoc! {"
3321 const a: B = (
3322 c(
3323 d(
3324 ˇ
3325 ˇ)
3326 ˇ)
3327 );
3328 "});
3329 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3330 cx.assert_editor_state(indoc! {"
3331 const a: B = (
3332 c(
3333 d(
3334 ˇ
3335 ˇ)
3336 ˇ)
3337 );
3338 "});
3339
3340 // test when current indent is more than suggested indent,
3341 // we just move cursor to current indent instead of suggested indent
3342 //
3343 // when some other cursor is at word boundary, it doesn't move
3344 cx.set_state(indoc! {"
3345 const a: B = (
3346 c(
3347 d(
3348 ˇ
3349 ˇ )
3350 ˇ)
3351 );
3352 "});
3353 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3354 cx.assert_editor_state(indoc! {"
3355 const a: B = (
3356 c(
3357 d(
3358 ˇ
3359 ˇ)
3360 ˇ)
3361 );
3362 "});
3363
3364 // handle auto-indent when there are multiple cursors on the same line
3365 cx.set_state(indoc! {"
3366 const a: B = (
3367 c(
3368 ˇ ˇ
3369 ˇ )
3370 );
3371 "});
3372 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3373 cx.assert_editor_state(indoc! {"
3374 const a: B = (
3375 c(
3376 ˇ
3377 ˇ)
3378 );
3379 "});
3380}
3381
3382#[gpui::test]
3383async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3384 init_test(cx, |settings| {
3385 settings.defaults.tab_size = NonZeroU32::new(3)
3386 });
3387
3388 let mut cx = EditorTestContext::new(cx).await;
3389 cx.set_state(indoc! {"
3390 ˇ
3391 \t ˇ
3392 \t ˇ
3393 \t ˇ
3394 \t \t\t \t \t\t \t\t \t \t ˇ
3395 "});
3396
3397 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3398 cx.assert_editor_state(indoc! {"
3399 ˇ
3400 \t ˇ
3401 \t ˇ
3402 \t ˇ
3403 \t \t\t \t \t\t \t\t \t \t ˇ
3404 "});
3405}
3406
3407#[gpui::test]
3408async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3409 init_test(cx, |settings| {
3410 settings.defaults.tab_size = NonZeroU32::new(4)
3411 });
3412
3413 let language = Arc::new(
3414 Language::new(
3415 LanguageConfig::default(),
3416 Some(tree_sitter_rust::LANGUAGE.into()),
3417 )
3418 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3419 .unwrap(),
3420 );
3421
3422 let mut cx = EditorTestContext::new(cx).await;
3423 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3424 cx.set_state(indoc! {"
3425 fn a() {
3426 if b {
3427 \t ˇc
3428 }
3429 }
3430 "});
3431
3432 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3433 cx.assert_editor_state(indoc! {"
3434 fn a() {
3435 if b {
3436 ˇc
3437 }
3438 }
3439 "});
3440}
3441
3442#[gpui::test]
3443async fn test_indent_outdent(cx: &mut TestAppContext) {
3444 init_test(cx, |settings| {
3445 settings.defaults.tab_size = NonZeroU32::new(4);
3446 });
3447
3448 let mut cx = EditorTestContext::new(cx).await;
3449
3450 cx.set_state(indoc! {"
3451 «oneˇ» «twoˇ»
3452 three
3453 four
3454 "});
3455 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3456 cx.assert_editor_state(indoc! {"
3457 «oneˇ» «twoˇ»
3458 three
3459 four
3460 "});
3461
3462 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3463 cx.assert_editor_state(indoc! {"
3464 «oneˇ» «twoˇ»
3465 three
3466 four
3467 "});
3468
3469 // select across line ending
3470 cx.set_state(indoc! {"
3471 one two
3472 t«hree
3473 ˇ» four
3474 "});
3475 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3476 cx.assert_editor_state(indoc! {"
3477 one two
3478 t«hree
3479 ˇ» four
3480 "});
3481
3482 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3483 cx.assert_editor_state(indoc! {"
3484 one two
3485 t«hree
3486 ˇ» four
3487 "});
3488
3489 // Ensure that indenting/outdenting works when the cursor is at column 0.
3490 cx.set_state(indoc! {"
3491 one two
3492 ˇthree
3493 four
3494 "});
3495 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3496 cx.assert_editor_state(indoc! {"
3497 one two
3498 ˇthree
3499 four
3500 "});
3501
3502 cx.set_state(indoc! {"
3503 one two
3504 ˇ three
3505 four
3506 "});
3507 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3508 cx.assert_editor_state(indoc! {"
3509 one two
3510 ˇthree
3511 four
3512 "});
3513}
3514
3515#[gpui::test]
3516async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3517 // This is a regression test for issue #33761
3518 init_test(cx, |_| {});
3519
3520 let mut cx = EditorTestContext::new(cx).await;
3521 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3522 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3523
3524 cx.set_state(
3525 r#"ˇ# ingress:
3526ˇ# api:
3527ˇ# enabled: false
3528ˇ# pathType: Prefix
3529ˇ# console:
3530ˇ# enabled: false
3531ˇ# pathType: Prefix
3532"#,
3533 );
3534
3535 // Press tab to indent all lines
3536 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3537
3538 cx.assert_editor_state(
3539 r#" ˇ# ingress:
3540 ˇ# api:
3541 ˇ# enabled: false
3542 ˇ# pathType: Prefix
3543 ˇ# console:
3544 ˇ# enabled: false
3545 ˇ# pathType: Prefix
3546"#,
3547 );
3548}
3549
3550#[gpui::test]
3551async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3552 // This is a test to make sure our fix for issue #33761 didn't break anything
3553 init_test(cx, |_| {});
3554
3555 let mut cx = EditorTestContext::new(cx).await;
3556 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3557 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3558
3559 cx.set_state(
3560 r#"ˇingress:
3561ˇ api:
3562ˇ enabled: false
3563ˇ pathType: Prefix
3564"#,
3565 );
3566
3567 // Press tab to indent all lines
3568 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3569
3570 cx.assert_editor_state(
3571 r#"ˇingress:
3572 ˇapi:
3573 ˇenabled: false
3574 ˇpathType: Prefix
3575"#,
3576 );
3577}
3578
3579#[gpui::test]
3580async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3581 init_test(cx, |settings| {
3582 settings.defaults.hard_tabs = Some(true);
3583 });
3584
3585 let mut cx = EditorTestContext::new(cx).await;
3586
3587 // select two ranges on one line
3588 cx.set_state(indoc! {"
3589 «oneˇ» «twoˇ»
3590 three
3591 four
3592 "});
3593 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3594 cx.assert_editor_state(indoc! {"
3595 \t«oneˇ» «twoˇ»
3596 three
3597 four
3598 "});
3599 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3600 cx.assert_editor_state(indoc! {"
3601 \t\t«oneˇ» «twoˇ»
3602 three
3603 four
3604 "});
3605 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3606 cx.assert_editor_state(indoc! {"
3607 \t«oneˇ» «twoˇ»
3608 three
3609 four
3610 "});
3611 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3612 cx.assert_editor_state(indoc! {"
3613 «oneˇ» «twoˇ»
3614 three
3615 four
3616 "});
3617
3618 // select across a line ending
3619 cx.set_state(indoc! {"
3620 one two
3621 t«hree
3622 ˇ»four
3623 "});
3624 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3625 cx.assert_editor_state(indoc! {"
3626 one two
3627 \tt«hree
3628 ˇ»four
3629 "});
3630 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3631 cx.assert_editor_state(indoc! {"
3632 one two
3633 \t\tt«hree
3634 ˇ»four
3635 "});
3636 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3637 cx.assert_editor_state(indoc! {"
3638 one two
3639 \tt«hree
3640 ˇ»four
3641 "});
3642 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3643 cx.assert_editor_state(indoc! {"
3644 one two
3645 t«hree
3646 ˇ»four
3647 "});
3648
3649 // Ensure that indenting/outdenting works when the cursor is at column 0.
3650 cx.set_state(indoc! {"
3651 one two
3652 ˇthree
3653 four
3654 "});
3655 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3656 cx.assert_editor_state(indoc! {"
3657 one two
3658 ˇthree
3659 four
3660 "});
3661 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3662 cx.assert_editor_state(indoc! {"
3663 one two
3664 \tˇthree
3665 four
3666 "});
3667 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3668 cx.assert_editor_state(indoc! {"
3669 one two
3670 ˇthree
3671 four
3672 "});
3673}
3674
3675#[gpui::test]
3676fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3677 init_test(cx, |settings| {
3678 settings.languages.0.extend([
3679 (
3680 "TOML".into(),
3681 LanguageSettingsContent {
3682 tab_size: NonZeroU32::new(2),
3683 ..Default::default()
3684 },
3685 ),
3686 (
3687 "Rust".into(),
3688 LanguageSettingsContent {
3689 tab_size: NonZeroU32::new(4),
3690 ..Default::default()
3691 },
3692 ),
3693 ]);
3694 });
3695
3696 let toml_language = Arc::new(Language::new(
3697 LanguageConfig {
3698 name: "TOML".into(),
3699 ..Default::default()
3700 },
3701 None,
3702 ));
3703 let rust_language = Arc::new(Language::new(
3704 LanguageConfig {
3705 name: "Rust".into(),
3706 ..Default::default()
3707 },
3708 None,
3709 ));
3710
3711 let toml_buffer =
3712 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3713 let rust_buffer =
3714 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3715 let multibuffer = cx.new(|cx| {
3716 let mut multibuffer = MultiBuffer::new(ReadWrite);
3717 multibuffer.push_excerpts(
3718 toml_buffer.clone(),
3719 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3720 cx,
3721 );
3722 multibuffer.push_excerpts(
3723 rust_buffer.clone(),
3724 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3725 cx,
3726 );
3727 multibuffer
3728 });
3729
3730 cx.add_window(|window, cx| {
3731 let mut editor = build_editor(multibuffer, window, cx);
3732
3733 assert_eq!(
3734 editor.text(cx),
3735 indoc! {"
3736 a = 1
3737 b = 2
3738
3739 const c: usize = 3;
3740 "}
3741 );
3742
3743 select_ranges(
3744 &mut editor,
3745 indoc! {"
3746 «aˇ» = 1
3747 b = 2
3748
3749 «const c:ˇ» usize = 3;
3750 "},
3751 window,
3752 cx,
3753 );
3754
3755 editor.tab(&Tab, window, cx);
3756 assert_text_with_selections(
3757 &mut editor,
3758 indoc! {"
3759 «aˇ» = 1
3760 b = 2
3761
3762 «const c:ˇ» usize = 3;
3763 "},
3764 cx,
3765 );
3766 editor.backtab(&Backtab, window, cx);
3767 assert_text_with_selections(
3768 &mut editor,
3769 indoc! {"
3770 «aˇ» = 1
3771 b = 2
3772
3773 «const c:ˇ» usize = 3;
3774 "},
3775 cx,
3776 );
3777
3778 editor
3779 });
3780}
3781
3782#[gpui::test]
3783async fn test_backspace(cx: &mut TestAppContext) {
3784 init_test(cx, |_| {});
3785
3786 let mut cx = EditorTestContext::new(cx).await;
3787
3788 // Basic backspace
3789 cx.set_state(indoc! {"
3790 onˇe two three
3791 fou«rˇ» five six
3792 seven «ˇeight nine
3793 »ten
3794 "});
3795 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3796 cx.assert_editor_state(indoc! {"
3797 oˇe two three
3798 fouˇ five six
3799 seven ˇten
3800 "});
3801
3802 // Test backspace inside and around indents
3803 cx.set_state(indoc! {"
3804 zero
3805 ˇone
3806 ˇtwo
3807 ˇ ˇ ˇ three
3808 ˇ ˇ four
3809 "});
3810 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3811 cx.assert_editor_state(indoc! {"
3812 zero
3813 ˇone
3814 ˇtwo
3815 ˇ threeˇ four
3816 "});
3817}
3818
3819#[gpui::test]
3820async fn test_delete(cx: &mut TestAppContext) {
3821 init_test(cx, |_| {});
3822
3823 let mut cx = EditorTestContext::new(cx).await;
3824 cx.set_state(indoc! {"
3825 onˇe two three
3826 fou«rˇ» five six
3827 seven «ˇeight nine
3828 »ten
3829 "});
3830 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3831 cx.assert_editor_state(indoc! {"
3832 onˇ two three
3833 fouˇ five six
3834 seven ˇten
3835 "});
3836}
3837
3838#[gpui::test]
3839fn test_delete_line(cx: &mut TestAppContext) {
3840 init_test(cx, |_| {});
3841
3842 let editor = cx.add_window(|window, cx| {
3843 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3844 build_editor(buffer, window, cx)
3845 });
3846 _ = editor.update(cx, |editor, window, cx| {
3847 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3848 s.select_display_ranges([
3849 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3850 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3851 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3852 ])
3853 });
3854 editor.delete_line(&DeleteLine, window, cx);
3855 assert_eq!(editor.display_text(cx), "ghi");
3856 assert_eq!(
3857 editor.selections.display_ranges(cx),
3858 vec![
3859 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3860 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3861 ]
3862 );
3863 });
3864
3865 let editor = cx.add_window(|window, cx| {
3866 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3867 build_editor(buffer, window, cx)
3868 });
3869 _ = editor.update(cx, |editor, window, cx| {
3870 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3871 s.select_display_ranges([
3872 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3873 ])
3874 });
3875 editor.delete_line(&DeleteLine, window, cx);
3876 assert_eq!(editor.display_text(cx), "ghi\n");
3877 assert_eq!(
3878 editor.selections.display_ranges(cx),
3879 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3880 );
3881 });
3882}
3883
3884#[gpui::test]
3885fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3886 init_test(cx, |_| {});
3887
3888 cx.add_window(|window, cx| {
3889 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3890 let mut editor = build_editor(buffer.clone(), window, cx);
3891 let buffer = buffer.read(cx).as_singleton().unwrap();
3892
3893 assert_eq!(
3894 editor.selections.ranges::<Point>(cx),
3895 &[Point::new(0, 0)..Point::new(0, 0)]
3896 );
3897
3898 // When on single line, replace newline at end by space
3899 editor.join_lines(&JoinLines, window, cx);
3900 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3901 assert_eq!(
3902 editor.selections.ranges::<Point>(cx),
3903 &[Point::new(0, 3)..Point::new(0, 3)]
3904 );
3905
3906 // When multiple lines are selected, remove newlines that are spanned by the selection
3907 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3908 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3909 });
3910 editor.join_lines(&JoinLines, window, cx);
3911 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3912 assert_eq!(
3913 editor.selections.ranges::<Point>(cx),
3914 &[Point::new(0, 11)..Point::new(0, 11)]
3915 );
3916
3917 // Undo should be transactional
3918 editor.undo(&Undo, window, cx);
3919 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3920 assert_eq!(
3921 editor.selections.ranges::<Point>(cx),
3922 &[Point::new(0, 5)..Point::new(2, 2)]
3923 );
3924
3925 // When joining an empty line don't insert a space
3926 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3927 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3928 });
3929 editor.join_lines(&JoinLines, window, cx);
3930 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3931 assert_eq!(
3932 editor.selections.ranges::<Point>(cx),
3933 [Point::new(2, 3)..Point::new(2, 3)]
3934 );
3935
3936 // We can remove trailing newlines
3937 editor.join_lines(&JoinLines, window, cx);
3938 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3939 assert_eq!(
3940 editor.selections.ranges::<Point>(cx),
3941 [Point::new(2, 3)..Point::new(2, 3)]
3942 );
3943
3944 // We don't blow up on the last line
3945 editor.join_lines(&JoinLines, window, cx);
3946 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3947 assert_eq!(
3948 editor.selections.ranges::<Point>(cx),
3949 [Point::new(2, 3)..Point::new(2, 3)]
3950 );
3951
3952 // reset to test indentation
3953 editor.buffer.update(cx, |buffer, cx| {
3954 buffer.edit(
3955 [
3956 (Point::new(1, 0)..Point::new(1, 2), " "),
3957 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3958 ],
3959 None,
3960 cx,
3961 )
3962 });
3963
3964 // We remove any leading spaces
3965 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3966 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3967 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3968 });
3969 editor.join_lines(&JoinLines, window, cx);
3970 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3971
3972 // We don't insert a space for a line containing only spaces
3973 editor.join_lines(&JoinLines, window, cx);
3974 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3975
3976 // We ignore any leading tabs
3977 editor.join_lines(&JoinLines, window, cx);
3978 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3979
3980 editor
3981 });
3982}
3983
3984#[gpui::test]
3985fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3986 init_test(cx, |_| {});
3987
3988 cx.add_window(|window, cx| {
3989 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3990 let mut editor = build_editor(buffer.clone(), window, cx);
3991 let buffer = buffer.read(cx).as_singleton().unwrap();
3992
3993 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3994 s.select_ranges([
3995 Point::new(0, 2)..Point::new(1, 1),
3996 Point::new(1, 2)..Point::new(1, 2),
3997 Point::new(3, 1)..Point::new(3, 2),
3998 ])
3999 });
4000
4001 editor.join_lines(&JoinLines, window, cx);
4002 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4003
4004 assert_eq!(
4005 editor.selections.ranges::<Point>(cx),
4006 [
4007 Point::new(0, 7)..Point::new(0, 7),
4008 Point::new(1, 3)..Point::new(1, 3)
4009 ]
4010 );
4011 editor
4012 });
4013}
4014
4015#[gpui::test]
4016async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4017 init_test(cx, |_| {});
4018
4019 let mut cx = EditorTestContext::new(cx).await;
4020
4021 let diff_base = r#"
4022 Line 0
4023 Line 1
4024 Line 2
4025 Line 3
4026 "#
4027 .unindent();
4028
4029 cx.set_state(
4030 &r#"
4031 ˇLine 0
4032 Line 1
4033 Line 2
4034 Line 3
4035 "#
4036 .unindent(),
4037 );
4038
4039 cx.set_head_text(&diff_base);
4040 executor.run_until_parked();
4041
4042 // Join lines
4043 cx.update_editor(|editor, window, cx| {
4044 editor.join_lines(&JoinLines, window, cx);
4045 });
4046 executor.run_until_parked();
4047
4048 cx.assert_editor_state(
4049 &r#"
4050 Line 0ˇ Line 1
4051 Line 2
4052 Line 3
4053 "#
4054 .unindent(),
4055 );
4056 // Join again
4057 cx.update_editor(|editor, window, cx| {
4058 editor.join_lines(&JoinLines, window, cx);
4059 });
4060 executor.run_until_parked();
4061
4062 cx.assert_editor_state(
4063 &r#"
4064 Line 0 Line 1ˇ Line 2
4065 Line 3
4066 "#
4067 .unindent(),
4068 );
4069}
4070
4071#[gpui::test]
4072async fn test_custom_newlines_cause_no_false_positive_diffs(
4073 executor: BackgroundExecutor,
4074 cx: &mut TestAppContext,
4075) {
4076 init_test(cx, |_| {});
4077 let mut cx = EditorTestContext::new(cx).await;
4078 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4079 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4080 executor.run_until_parked();
4081
4082 cx.update_editor(|editor, window, cx| {
4083 let snapshot = editor.snapshot(window, cx);
4084 assert_eq!(
4085 snapshot
4086 .buffer_snapshot
4087 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
4088 .collect::<Vec<_>>(),
4089 Vec::new(),
4090 "Should not have any diffs for files with custom newlines"
4091 );
4092 });
4093}
4094
4095#[gpui::test]
4096async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4097 init_test(cx, |_| {});
4098
4099 let mut cx = EditorTestContext::new(cx).await;
4100
4101 // Test sort_lines_case_insensitive()
4102 cx.set_state(indoc! {"
4103 «z
4104 y
4105 x
4106 Z
4107 Y
4108 Xˇ»
4109 "});
4110 cx.update_editor(|e, window, cx| {
4111 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4112 });
4113 cx.assert_editor_state(indoc! {"
4114 «x
4115 X
4116 y
4117 Y
4118 z
4119 Zˇ»
4120 "});
4121
4122 // Test sort_lines_by_length()
4123 //
4124 // Demonstrates:
4125 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4126 // - sort is stable
4127 cx.set_state(indoc! {"
4128 «123
4129 æ
4130 12
4131 ∞
4132 1
4133 æˇ»
4134 "});
4135 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4136 cx.assert_editor_state(indoc! {"
4137 «æ
4138 ∞
4139 1
4140 æ
4141 12
4142 123ˇ»
4143 "});
4144
4145 // Test reverse_lines()
4146 cx.set_state(indoc! {"
4147 «5
4148 4
4149 3
4150 2
4151 1ˇ»
4152 "});
4153 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4154 cx.assert_editor_state(indoc! {"
4155 «1
4156 2
4157 3
4158 4
4159 5ˇ»
4160 "});
4161
4162 // Skip testing shuffle_line()
4163
4164 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4165 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4166
4167 // Don't manipulate when cursor is on single line, but expand the selection
4168 cx.set_state(indoc! {"
4169 ddˇdd
4170 ccc
4171 bb
4172 a
4173 "});
4174 cx.update_editor(|e, window, cx| {
4175 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4176 });
4177 cx.assert_editor_state(indoc! {"
4178 «ddddˇ»
4179 ccc
4180 bb
4181 a
4182 "});
4183
4184 // Basic manipulate case
4185 // Start selection moves to column 0
4186 // End of selection shrinks to fit shorter line
4187 cx.set_state(indoc! {"
4188 dd«d
4189 ccc
4190 bb
4191 aaaaaˇ»
4192 "});
4193 cx.update_editor(|e, window, cx| {
4194 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4195 });
4196 cx.assert_editor_state(indoc! {"
4197 «aaaaa
4198 bb
4199 ccc
4200 dddˇ»
4201 "});
4202
4203 // Manipulate case with newlines
4204 cx.set_state(indoc! {"
4205 dd«d
4206 ccc
4207
4208 bb
4209 aaaaa
4210
4211 ˇ»
4212 "});
4213 cx.update_editor(|e, window, cx| {
4214 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4215 });
4216 cx.assert_editor_state(indoc! {"
4217 «
4218
4219 aaaaa
4220 bb
4221 ccc
4222 dddˇ»
4223
4224 "});
4225
4226 // Adding new line
4227 cx.set_state(indoc! {"
4228 aa«a
4229 bbˇ»b
4230 "});
4231 cx.update_editor(|e, window, cx| {
4232 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4233 });
4234 cx.assert_editor_state(indoc! {"
4235 «aaa
4236 bbb
4237 added_lineˇ»
4238 "});
4239
4240 // Removing line
4241 cx.set_state(indoc! {"
4242 aa«a
4243 bbbˇ»
4244 "});
4245 cx.update_editor(|e, window, cx| {
4246 e.manipulate_immutable_lines(window, cx, |lines| {
4247 lines.pop();
4248 })
4249 });
4250 cx.assert_editor_state(indoc! {"
4251 «aaaˇ»
4252 "});
4253
4254 // Removing all lines
4255 cx.set_state(indoc! {"
4256 aa«a
4257 bbbˇ»
4258 "});
4259 cx.update_editor(|e, window, cx| {
4260 e.manipulate_immutable_lines(window, cx, |lines| {
4261 lines.drain(..);
4262 })
4263 });
4264 cx.assert_editor_state(indoc! {"
4265 ˇ
4266 "});
4267}
4268
4269#[gpui::test]
4270async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4271 init_test(cx, |_| {});
4272
4273 let mut cx = EditorTestContext::new(cx).await;
4274
4275 // Consider continuous selection as single selection
4276 cx.set_state(indoc! {"
4277 Aaa«aa
4278 cˇ»c«c
4279 bb
4280 aaaˇ»aa
4281 "});
4282 cx.update_editor(|e, window, cx| {
4283 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4284 });
4285 cx.assert_editor_state(indoc! {"
4286 «Aaaaa
4287 ccc
4288 bb
4289 aaaaaˇ»
4290 "});
4291
4292 cx.set_state(indoc! {"
4293 Aaa«aa
4294 cˇ»c«c
4295 bb
4296 aaaˇ»aa
4297 "});
4298 cx.update_editor(|e, window, cx| {
4299 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4300 });
4301 cx.assert_editor_state(indoc! {"
4302 «Aaaaa
4303 ccc
4304 bbˇ»
4305 "});
4306
4307 // Consider non continuous selection as distinct dedup operations
4308 cx.set_state(indoc! {"
4309 «aaaaa
4310 bb
4311 aaaaa
4312 aaaaaˇ»
4313
4314 aaa«aaˇ»
4315 "});
4316 cx.update_editor(|e, window, cx| {
4317 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4318 });
4319 cx.assert_editor_state(indoc! {"
4320 «aaaaa
4321 bbˇ»
4322
4323 «aaaaaˇ»
4324 "});
4325}
4326
4327#[gpui::test]
4328async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4329 init_test(cx, |_| {});
4330
4331 let mut cx = EditorTestContext::new(cx).await;
4332
4333 cx.set_state(indoc! {"
4334 «Aaa
4335 aAa
4336 Aaaˇ»
4337 "});
4338 cx.update_editor(|e, window, cx| {
4339 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4340 });
4341 cx.assert_editor_state(indoc! {"
4342 «Aaa
4343 aAaˇ»
4344 "});
4345
4346 cx.set_state(indoc! {"
4347 «Aaa
4348 aAa
4349 aaAˇ»
4350 "});
4351 cx.update_editor(|e, window, cx| {
4352 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4353 });
4354 cx.assert_editor_state(indoc! {"
4355 «Aaaˇ»
4356 "});
4357}
4358
4359#[gpui::test]
4360async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4361 init_test(cx, |_| {});
4362
4363 let mut cx = EditorTestContext::new(cx).await;
4364
4365 // Manipulate with multiple selections on a single line
4366 cx.set_state(indoc! {"
4367 dd«dd
4368 cˇ»c«c
4369 bb
4370 aaaˇ»aa
4371 "});
4372 cx.update_editor(|e, window, cx| {
4373 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4374 });
4375 cx.assert_editor_state(indoc! {"
4376 «aaaaa
4377 bb
4378 ccc
4379 ddddˇ»
4380 "});
4381
4382 // Manipulate with multiple disjoin selections
4383 cx.set_state(indoc! {"
4384 5«
4385 4
4386 3
4387 2
4388 1ˇ»
4389
4390 dd«dd
4391 ccc
4392 bb
4393 aaaˇ»aa
4394 "});
4395 cx.update_editor(|e, window, cx| {
4396 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4397 });
4398 cx.assert_editor_state(indoc! {"
4399 «1
4400 2
4401 3
4402 4
4403 5ˇ»
4404
4405 «aaaaa
4406 bb
4407 ccc
4408 ddddˇ»
4409 "});
4410
4411 // Adding lines on each selection
4412 cx.set_state(indoc! {"
4413 2«
4414 1ˇ»
4415
4416 bb«bb
4417 aaaˇ»aa
4418 "});
4419 cx.update_editor(|e, window, cx| {
4420 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4421 });
4422 cx.assert_editor_state(indoc! {"
4423 «2
4424 1
4425 added lineˇ»
4426
4427 «bbbb
4428 aaaaa
4429 added lineˇ»
4430 "});
4431
4432 // Removing lines on each selection
4433 cx.set_state(indoc! {"
4434 2«
4435 1ˇ»
4436
4437 bb«bb
4438 aaaˇ»aa
4439 "});
4440 cx.update_editor(|e, window, cx| {
4441 e.manipulate_immutable_lines(window, cx, |lines| {
4442 lines.pop();
4443 })
4444 });
4445 cx.assert_editor_state(indoc! {"
4446 «2ˇ»
4447
4448 «bbbbˇ»
4449 "});
4450}
4451
4452#[gpui::test]
4453async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4454 init_test(cx, |settings| {
4455 settings.defaults.tab_size = NonZeroU32::new(3)
4456 });
4457
4458 let mut cx = EditorTestContext::new(cx).await;
4459
4460 // MULTI SELECTION
4461 // Ln.1 "«" tests empty lines
4462 // Ln.9 tests just leading whitespace
4463 cx.set_state(indoc! {"
4464 «
4465 abc // No indentationˇ»
4466 «\tabc // 1 tabˇ»
4467 \t\tabc « ˇ» // 2 tabs
4468 \t ab«c // Tab followed by space
4469 \tabc // Space followed by tab (3 spaces should be the result)
4470 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4471 abˇ»ˇc ˇ ˇ // Already space indented«
4472 \t
4473 \tabc\tdef // Only the leading tab is manipulatedˇ»
4474 "});
4475 cx.update_editor(|e, window, cx| {
4476 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4477 });
4478 cx.assert_editor_state(
4479 indoc! {"
4480 «
4481 abc // No indentation
4482 abc // 1 tab
4483 abc // 2 tabs
4484 abc // Tab followed by space
4485 abc // Space followed by tab (3 spaces should be the result)
4486 abc // Mixed indentation (tab conversion depends on the column)
4487 abc // Already space indented
4488 ·
4489 abc\tdef // Only the leading tab is manipulatedˇ»
4490 "}
4491 .replace("·", "")
4492 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4493 );
4494
4495 // Test on just a few lines, the others should remain unchanged
4496 // Only lines (3, 5, 10, 11) should change
4497 cx.set_state(
4498 indoc! {"
4499 ·
4500 abc // No indentation
4501 \tabcˇ // 1 tab
4502 \t\tabc // 2 tabs
4503 \t abcˇ // Tab followed by space
4504 \tabc // Space followed by tab (3 spaces should be the result)
4505 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4506 abc // Already space indented
4507 «\t
4508 \tabc\tdef // Only the leading tab is manipulatedˇ»
4509 "}
4510 .replace("·", "")
4511 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4512 );
4513 cx.update_editor(|e, window, cx| {
4514 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4515 });
4516 cx.assert_editor_state(
4517 indoc! {"
4518 ·
4519 abc // No indentation
4520 « abc // 1 tabˇ»
4521 \t\tabc // 2 tabs
4522 « abc // Tab followed by spaceˇ»
4523 \tabc // Space followed by tab (3 spaces should be the result)
4524 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4525 abc // Already space indented
4526 « ·
4527 abc\tdef // Only the leading tab is manipulatedˇ»
4528 "}
4529 .replace("·", "")
4530 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4531 );
4532
4533 // SINGLE SELECTION
4534 // Ln.1 "«" tests empty lines
4535 // Ln.9 tests just leading whitespace
4536 cx.set_state(indoc! {"
4537 «
4538 abc // No indentation
4539 \tabc // 1 tab
4540 \t\tabc // 2 tabs
4541 \t abc // Tab followed by space
4542 \tabc // Space followed by tab (3 spaces should be the result)
4543 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4544 abc // Already space indented
4545 \t
4546 \tabc\tdef // Only the leading tab is manipulatedˇ»
4547 "});
4548 cx.update_editor(|e, window, cx| {
4549 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4550 });
4551 cx.assert_editor_state(
4552 indoc! {"
4553 «
4554 abc // No indentation
4555 abc // 1 tab
4556 abc // 2 tabs
4557 abc // Tab followed by space
4558 abc // Space followed by tab (3 spaces should be the result)
4559 abc // Mixed indentation (tab conversion depends on the column)
4560 abc // Already space indented
4561 ·
4562 abc\tdef // Only the leading tab is manipulatedˇ»
4563 "}
4564 .replace("·", "")
4565 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4566 );
4567}
4568
4569#[gpui::test]
4570async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
4571 init_test(cx, |settings| {
4572 settings.defaults.tab_size = NonZeroU32::new(3)
4573 });
4574
4575 let mut cx = EditorTestContext::new(cx).await;
4576
4577 // MULTI SELECTION
4578 // Ln.1 "«" tests empty lines
4579 // Ln.11 tests just leading whitespace
4580 cx.set_state(indoc! {"
4581 «
4582 abˇ»ˇc // No indentation
4583 abc ˇ ˇ // 1 space (< 3 so dont convert)
4584 abc « // 2 spaces (< 3 so dont convert)
4585 abc // 3 spaces (convert)
4586 abc ˇ» // 5 spaces (1 tab + 2 spaces)
4587 «\tˇ»\t«\tˇ»abc // Already tab indented
4588 «\t abc // Tab followed by space
4589 \tabc // Space followed by tab (should be consumed due to tab)
4590 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4591 \tˇ» «\t
4592 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
4593 "});
4594 cx.update_editor(|e, window, cx| {
4595 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4596 });
4597 cx.assert_editor_state(indoc! {"
4598 «
4599 abc // No indentation
4600 abc // 1 space (< 3 so dont convert)
4601 abc // 2 spaces (< 3 so dont convert)
4602 \tabc // 3 spaces (convert)
4603 \t abc // 5 spaces (1 tab + 2 spaces)
4604 \t\t\tabc // Already tab indented
4605 \t abc // Tab followed by space
4606 \tabc // Space followed by tab (should be consumed due to tab)
4607 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4608 \t\t\t
4609 \tabc \t // Only the leading spaces should be convertedˇ»
4610 "});
4611
4612 // Test on just a few lines, the other should remain unchanged
4613 // Only lines (4, 8, 11, 12) should change
4614 cx.set_state(
4615 indoc! {"
4616 ·
4617 abc // No indentation
4618 abc // 1 space (< 3 so dont convert)
4619 abc // 2 spaces (< 3 so dont convert)
4620 « abc // 3 spaces (convert)ˇ»
4621 abc // 5 spaces (1 tab + 2 spaces)
4622 \t\t\tabc // Already tab indented
4623 \t abc // Tab followed by space
4624 \tabc ˇ // Space followed by tab (should be consumed due to tab)
4625 \t\t \tabc // Mixed indentation
4626 \t \t \t \tabc // Mixed indentation
4627 \t \tˇ
4628 « abc \t // Only the leading spaces should be convertedˇ»
4629 "}
4630 .replace("·", "")
4631 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4632 );
4633 cx.update_editor(|e, window, cx| {
4634 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4635 });
4636 cx.assert_editor_state(
4637 indoc! {"
4638 ·
4639 abc // No indentation
4640 abc // 1 space (< 3 so dont convert)
4641 abc // 2 spaces (< 3 so dont convert)
4642 «\tabc // 3 spaces (convert)ˇ»
4643 abc // 5 spaces (1 tab + 2 spaces)
4644 \t\t\tabc // Already tab indented
4645 \t abc // Tab followed by space
4646 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
4647 \t\t \tabc // Mixed indentation
4648 \t \t \t \tabc // Mixed indentation
4649 «\t\t\t
4650 \tabc \t // Only the leading spaces should be convertedˇ»
4651 "}
4652 .replace("·", "")
4653 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4654 );
4655
4656 // SINGLE SELECTION
4657 // Ln.1 "«" tests empty lines
4658 // Ln.11 tests just leading whitespace
4659 cx.set_state(indoc! {"
4660 «
4661 abc // No indentation
4662 abc // 1 space (< 3 so dont convert)
4663 abc // 2 spaces (< 3 so dont convert)
4664 abc // 3 spaces (convert)
4665 abc // 5 spaces (1 tab + 2 spaces)
4666 \t\t\tabc // Already tab indented
4667 \t abc // Tab followed by space
4668 \tabc // Space followed by tab (should be consumed due to tab)
4669 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4670 \t \t
4671 abc \t // Only the leading spaces should be convertedˇ»
4672 "});
4673 cx.update_editor(|e, window, cx| {
4674 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4675 });
4676 cx.assert_editor_state(indoc! {"
4677 «
4678 abc // No indentation
4679 abc // 1 space (< 3 so dont convert)
4680 abc // 2 spaces (< 3 so dont convert)
4681 \tabc // 3 spaces (convert)
4682 \t abc // 5 spaces (1 tab + 2 spaces)
4683 \t\t\tabc // Already tab indented
4684 \t abc // Tab followed by space
4685 \tabc // Space followed by tab (should be consumed due to tab)
4686 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4687 \t\t\t
4688 \tabc \t // Only the leading spaces should be convertedˇ»
4689 "});
4690}
4691
4692#[gpui::test]
4693async fn test_toggle_case(cx: &mut TestAppContext) {
4694 init_test(cx, |_| {});
4695
4696 let mut cx = EditorTestContext::new(cx).await;
4697
4698 // If all lower case -> upper case
4699 cx.set_state(indoc! {"
4700 «hello worldˇ»
4701 "});
4702 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4703 cx.assert_editor_state(indoc! {"
4704 «HELLO WORLDˇ»
4705 "});
4706
4707 // If all upper case -> lower case
4708 cx.set_state(indoc! {"
4709 «HELLO WORLDˇ»
4710 "});
4711 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4712 cx.assert_editor_state(indoc! {"
4713 «hello worldˇ»
4714 "});
4715
4716 // If any upper case characters are identified -> lower case
4717 // This matches JetBrains IDEs
4718 cx.set_state(indoc! {"
4719 «hEllo worldˇ»
4720 "});
4721 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4722 cx.assert_editor_state(indoc! {"
4723 «hello worldˇ»
4724 "});
4725}
4726
4727#[gpui::test]
4728async fn test_manipulate_text(cx: &mut TestAppContext) {
4729 init_test(cx, |_| {});
4730
4731 let mut cx = EditorTestContext::new(cx).await;
4732
4733 // Test convert_to_upper_case()
4734 cx.set_state(indoc! {"
4735 «hello worldˇ»
4736 "});
4737 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4738 cx.assert_editor_state(indoc! {"
4739 «HELLO WORLDˇ»
4740 "});
4741
4742 // Test convert_to_lower_case()
4743 cx.set_state(indoc! {"
4744 «HELLO WORLDˇ»
4745 "});
4746 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4747 cx.assert_editor_state(indoc! {"
4748 «hello worldˇ»
4749 "});
4750
4751 // Test multiple line, single selection case
4752 cx.set_state(indoc! {"
4753 «The quick brown
4754 fox jumps over
4755 the lazy dogˇ»
4756 "});
4757 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4758 cx.assert_editor_state(indoc! {"
4759 «The Quick Brown
4760 Fox Jumps Over
4761 The Lazy Dogˇ»
4762 "});
4763
4764 // Test multiple line, single selection case
4765 cx.set_state(indoc! {"
4766 «The quick brown
4767 fox jumps over
4768 the lazy dogˇ»
4769 "});
4770 cx.update_editor(|e, window, cx| {
4771 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4772 });
4773 cx.assert_editor_state(indoc! {"
4774 «TheQuickBrown
4775 FoxJumpsOver
4776 TheLazyDogˇ»
4777 "});
4778
4779 // From here on out, test more complex cases of manipulate_text()
4780
4781 // Test no selection case - should affect words cursors are in
4782 // Cursor at beginning, middle, and end of word
4783 cx.set_state(indoc! {"
4784 ˇhello big beauˇtiful worldˇ
4785 "});
4786 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4787 cx.assert_editor_state(indoc! {"
4788 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4789 "});
4790
4791 // Test multiple selections on a single line and across multiple lines
4792 cx.set_state(indoc! {"
4793 «Theˇ» quick «brown
4794 foxˇ» jumps «overˇ»
4795 the «lazyˇ» dog
4796 "});
4797 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4798 cx.assert_editor_state(indoc! {"
4799 «THEˇ» quick «BROWN
4800 FOXˇ» jumps «OVERˇ»
4801 the «LAZYˇ» dog
4802 "});
4803
4804 // Test case where text length grows
4805 cx.set_state(indoc! {"
4806 «tschüߡ»
4807 "});
4808 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4809 cx.assert_editor_state(indoc! {"
4810 «TSCHÜSSˇ»
4811 "});
4812
4813 // Test to make sure we don't crash when text shrinks
4814 cx.set_state(indoc! {"
4815 aaa_bbbˇ
4816 "});
4817 cx.update_editor(|e, window, cx| {
4818 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4819 });
4820 cx.assert_editor_state(indoc! {"
4821 «aaaBbbˇ»
4822 "});
4823
4824 // Test to make sure we all aware of the fact that each word can grow and shrink
4825 // Final selections should be aware of this fact
4826 cx.set_state(indoc! {"
4827 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4828 "});
4829 cx.update_editor(|e, window, cx| {
4830 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4831 });
4832 cx.assert_editor_state(indoc! {"
4833 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4834 "});
4835
4836 cx.set_state(indoc! {"
4837 «hElLo, WoRld!ˇ»
4838 "});
4839 cx.update_editor(|e, window, cx| {
4840 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4841 });
4842 cx.assert_editor_state(indoc! {"
4843 «HeLlO, wOrLD!ˇ»
4844 "});
4845}
4846
4847#[gpui::test]
4848fn test_duplicate_line(cx: &mut TestAppContext) {
4849 init_test(cx, |_| {});
4850
4851 let editor = cx.add_window(|window, cx| {
4852 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4853 build_editor(buffer, window, cx)
4854 });
4855 _ = editor.update(cx, |editor, window, cx| {
4856 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4857 s.select_display_ranges([
4858 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4859 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4860 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4861 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4862 ])
4863 });
4864 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4865 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4866 assert_eq!(
4867 editor.selections.display_ranges(cx),
4868 vec![
4869 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4870 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4871 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4872 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4873 ]
4874 );
4875 });
4876
4877 let editor = cx.add_window(|window, cx| {
4878 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4879 build_editor(buffer, window, cx)
4880 });
4881 _ = editor.update(cx, |editor, window, cx| {
4882 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4883 s.select_display_ranges([
4884 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4885 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4886 ])
4887 });
4888 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4889 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4890 assert_eq!(
4891 editor.selections.display_ranges(cx),
4892 vec![
4893 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4894 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4895 ]
4896 );
4897 });
4898
4899 // With `move_upwards` the selections stay in place, except for
4900 // the lines inserted above them
4901 let editor = cx.add_window(|window, cx| {
4902 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4903 build_editor(buffer, window, cx)
4904 });
4905 _ = editor.update(cx, |editor, window, cx| {
4906 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4907 s.select_display_ranges([
4908 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4909 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4910 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4911 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4912 ])
4913 });
4914 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4915 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4916 assert_eq!(
4917 editor.selections.display_ranges(cx),
4918 vec![
4919 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4920 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4921 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4922 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4923 ]
4924 );
4925 });
4926
4927 let editor = cx.add_window(|window, cx| {
4928 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4929 build_editor(buffer, window, cx)
4930 });
4931 _ = editor.update(cx, |editor, window, cx| {
4932 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4933 s.select_display_ranges([
4934 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4935 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4936 ])
4937 });
4938 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4939 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4940 assert_eq!(
4941 editor.selections.display_ranges(cx),
4942 vec![
4943 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4944 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4945 ]
4946 );
4947 });
4948
4949 let editor = cx.add_window(|window, cx| {
4950 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4951 build_editor(buffer, window, cx)
4952 });
4953 _ = editor.update(cx, |editor, window, cx| {
4954 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4955 s.select_display_ranges([
4956 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4957 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4958 ])
4959 });
4960 editor.duplicate_selection(&DuplicateSelection, window, cx);
4961 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4962 assert_eq!(
4963 editor.selections.display_ranges(cx),
4964 vec![
4965 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4966 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4967 ]
4968 );
4969 });
4970}
4971
4972#[gpui::test]
4973fn test_move_line_up_down(cx: &mut TestAppContext) {
4974 init_test(cx, |_| {});
4975
4976 let editor = cx.add_window(|window, cx| {
4977 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4978 build_editor(buffer, window, cx)
4979 });
4980 _ = editor.update(cx, |editor, window, cx| {
4981 editor.fold_creases(
4982 vec![
4983 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4984 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4985 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4986 ],
4987 true,
4988 window,
4989 cx,
4990 );
4991 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4992 s.select_display_ranges([
4993 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4994 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4995 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4996 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4997 ])
4998 });
4999 assert_eq!(
5000 editor.display_text(cx),
5001 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5002 );
5003
5004 editor.move_line_up(&MoveLineUp, window, cx);
5005 assert_eq!(
5006 editor.display_text(cx),
5007 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5008 );
5009 assert_eq!(
5010 editor.selections.display_ranges(cx),
5011 vec![
5012 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5013 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5014 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5015 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5016 ]
5017 );
5018 });
5019
5020 _ = editor.update(cx, |editor, window, cx| {
5021 editor.move_line_down(&MoveLineDown, window, cx);
5022 assert_eq!(
5023 editor.display_text(cx),
5024 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5025 );
5026 assert_eq!(
5027 editor.selections.display_ranges(cx),
5028 vec![
5029 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5030 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5031 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5032 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5033 ]
5034 );
5035 });
5036
5037 _ = editor.update(cx, |editor, window, cx| {
5038 editor.move_line_down(&MoveLineDown, window, cx);
5039 assert_eq!(
5040 editor.display_text(cx),
5041 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5042 );
5043 assert_eq!(
5044 editor.selections.display_ranges(cx),
5045 vec![
5046 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5047 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5048 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5049 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5050 ]
5051 );
5052 });
5053
5054 _ = editor.update(cx, |editor, window, cx| {
5055 editor.move_line_up(&MoveLineUp, window, cx);
5056 assert_eq!(
5057 editor.display_text(cx),
5058 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5059 );
5060 assert_eq!(
5061 editor.selections.display_ranges(cx),
5062 vec![
5063 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5064 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5065 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5066 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5067 ]
5068 );
5069 });
5070}
5071
5072#[gpui::test]
5073fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5074 init_test(cx, |_| {});
5075 let editor = cx.add_window(|window, cx| {
5076 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5077 build_editor(buffer, window, cx)
5078 });
5079 _ = editor.update(cx, |editor, window, cx| {
5080 editor.fold_creases(
5081 vec![Crease::simple(
5082 Point::new(6, 4)..Point::new(7, 4),
5083 FoldPlaceholder::test(),
5084 )],
5085 true,
5086 window,
5087 cx,
5088 );
5089 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5090 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5091 });
5092 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5093 editor.move_line_up(&MoveLineUp, window, cx);
5094 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5095 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5096 });
5097}
5098
5099#[gpui::test]
5100fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5101 init_test(cx, |_| {});
5102
5103 let editor = cx.add_window(|window, cx| {
5104 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5105 build_editor(buffer, window, cx)
5106 });
5107 _ = editor.update(cx, |editor, window, cx| {
5108 let snapshot = editor.buffer.read(cx).snapshot(cx);
5109 editor.insert_blocks(
5110 [BlockProperties {
5111 style: BlockStyle::Fixed,
5112 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5113 height: Some(1),
5114 render: Arc::new(|_| div().into_any()),
5115 priority: 0,
5116 }],
5117 Some(Autoscroll::fit()),
5118 cx,
5119 );
5120 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5121 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5122 });
5123 editor.move_line_down(&MoveLineDown, window, cx);
5124 });
5125}
5126
5127#[gpui::test]
5128async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5129 init_test(cx, |_| {});
5130
5131 let mut cx = EditorTestContext::new(cx).await;
5132 cx.set_state(
5133 &"
5134 ˇzero
5135 one
5136 two
5137 three
5138 four
5139 five
5140 "
5141 .unindent(),
5142 );
5143
5144 // Create a four-line block that replaces three lines of text.
5145 cx.update_editor(|editor, window, cx| {
5146 let snapshot = editor.snapshot(window, cx);
5147 let snapshot = &snapshot.buffer_snapshot;
5148 let placement = BlockPlacement::Replace(
5149 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5150 );
5151 editor.insert_blocks(
5152 [BlockProperties {
5153 placement,
5154 height: Some(4),
5155 style: BlockStyle::Sticky,
5156 render: Arc::new(|_| gpui::div().into_any_element()),
5157 priority: 0,
5158 }],
5159 None,
5160 cx,
5161 );
5162 });
5163
5164 // Move down so that the cursor touches the block.
5165 cx.update_editor(|editor, window, cx| {
5166 editor.move_down(&Default::default(), window, cx);
5167 });
5168 cx.assert_editor_state(
5169 &"
5170 zero
5171 «one
5172 two
5173 threeˇ»
5174 four
5175 five
5176 "
5177 .unindent(),
5178 );
5179
5180 // Move down past the block.
5181 cx.update_editor(|editor, window, cx| {
5182 editor.move_down(&Default::default(), window, cx);
5183 });
5184 cx.assert_editor_state(
5185 &"
5186 zero
5187 one
5188 two
5189 three
5190 ˇfour
5191 five
5192 "
5193 .unindent(),
5194 );
5195}
5196
5197#[gpui::test]
5198fn test_transpose(cx: &mut TestAppContext) {
5199 init_test(cx, |_| {});
5200
5201 _ = cx.add_window(|window, cx| {
5202 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5203 editor.set_style(EditorStyle::default(), window, cx);
5204 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5205 s.select_ranges([1..1])
5206 });
5207 editor.transpose(&Default::default(), window, cx);
5208 assert_eq!(editor.text(cx), "bac");
5209 assert_eq!(editor.selections.ranges(cx), [2..2]);
5210
5211 editor.transpose(&Default::default(), window, cx);
5212 assert_eq!(editor.text(cx), "bca");
5213 assert_eq!(editor.selections.ranges(cx), [3..3]);
5214
5215 editor.transpose(&Default::default(), window, cx);
5216 assert_eq!(editor.text(cx), "bac");
5217 assert_eq!(editor.selections.ranges(cx), [3..3]);
5218
5219 editor
5220 });
5221
5222 _ = cx.add_window(|window, cx| {
5223 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5224 editor.set_style(EditorStyle::default(), window, cx);
5225 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5226 s.select_ranges([3..3])
5227 });
5228 editor.transpose(&Default::default(), window, cx);
5229 assert_eq!(editor.text(cx), "acb\nde");
5230 assert_eq!(editor.selections.ranges(cx), [3..3]);
5231
5232 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5233 s.select_ranges([4..4])
5234 });
5235 editor.transpose(&Default::default(), window, cx);
5236 assert_eq!(editor.text(cx), "acbd\ne");
5237 assert_eq!(editor.selections.ranges(cx), [5..5]);
5238
5239 editor.transpose(&Default::default(), window, cx);
5240 assert_eq!(editor.text(cx), "acbde\n");
5241 assert_eq!(editor.selections.ranges(cx), [6..6]);
5242
5243 editor.transpose(&Default::default(), window, cx);
5244 assert_eq!(editor.text(cx), "acbd\ne");
5245 assert_eq!(editor.selections.ranges(cx), [6..6]);
5246
5247 editor
5248 });
5249
5250 _ = cx.add_window(|window, cx| {
5251 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5252 editor.set_style(EditorStyle::default(), window, cx);
5253 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5254 s.select_ranges([1..1, 2..2, 4..4])
5255 });
5256 editor.transpose(&Default::default(), window, cx);
5257 assert_eq!(editor.text(cx), "bacd\ne");
5258 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5259
5260 editor.transpose(&Default::default(), window, cx);
5261 assert_eq!(editor.text(cx), "bcade\n");
5262 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5263
5264 editor.transpose(&Default::default(), window, cx);
5265 assert_eq!(editor.text(cx), "bcda\ne");
5266 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5267
5268 editor.transpose(&Default::default(), window, cx);
5269 assert_eq!(editor.text(cx), "bcade\n");
5270 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5271
5272 editor.transpose(&Default::default(), window, cx);
5273 assert_eq!(editor.text(cx), "bcaed\n");
5274 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5275
5276 editor
5277 });
5278
5279 _ = cx.add_window(|window, cx| {
5280 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5281 editor.set_style(EditorStyle::default(), window, cx);
5282 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5283 s.select_ranges([4..4])
5284 });
5285 editor.transpose(&Default::default(), window, cx);
5286 assert_eq!(editor.text(cx), "🏀🍐✋");
5287 assert_eq!(editor.selections.ranges(cx), [8..8]);
5288
5289 editor.transpose(&Default::default(), window, cx);
5290 assert_eq!(editor.text(cx), "🏀✋🍐");
5291 assert_eq!(editor.selections.ranges(cx), [11..11]);
5292
5293 editor.transpose(&Default::default(), window, cx);
5294 assert_eq!(editor.text(cx), "🏀🍐✋");
5295 assert_eq!(editor.selections.ranges(cx), [11..11]);
5296
5297 editor
5298 });
5299}
5300
5301#[gpui::test]
5302async fn test_rewrap(cx: &mut TestAppContext) {
5303 init_test(cx, |settings| {
5304 settings.languages.0.extend([
5305 (
5306 "Markdown".into(),
5307 LanguageSettingsContent {
5308 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5309 preferred_line_length: Some(40),
5310 ..Default::default()
5311 },
5312 ),
5313 (
5314 "Plain Text".into(),
5315 LanguageSettingsContent {
5316 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5317 preferred_line_length: Some(40),
5318 ..Default::default()
5319 },
5320 ),
5321 (
5322 "C++".into(),
5323 LanguageSettingsContent {
5324 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5325 preferred_line_length: Some(40),
5326 ..Default::default()
5327 },
5328 ),
5329 (
5330 "Python".into(),
5331 LanguageSettingsContent {
5332 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5333 preferred_line_length: Some(40),
5334 ..Default::default()
5335 },
5336 ),
5337 (
5338 "Rust".into(),
5339 LanguageSettingsContent {
5340 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5341 preferred_line_length: Some(40),
5342 ..Default::default()
5343 },
5344 ),
5345 ])
5346 });
5347
5348 let mut cx = EditorTestContext::new(cx).await;
5349
5350 let cpp_language = Arc::new(Language::new(
5351 LanguageConfig {
5352 name: "C++".into(),
5353 line_comments: vec!["// ".into()],
5354 ..LanguageConfig::default()
5355 },
5356 None,
5357 ));
5358 let python_language = Arc::new(Language::new(
5359 LanguageConfig {
5360 name: "Python".into(),
5361 line_comments: vec!["# ".into()],
5362 ..LanguageConfig::default()
5363 },
5364 None,
5365 ));
5366 let markdown_language = Arc::new(Language::new(
5367 LanguageConfig {
5368 name: "Markdown".into(),
5369 rewrap_prefixes: vec![
5370 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5371 regex::Regex::new("[-*+]\\s+").unwrap(),
5372 ],
5373 ..LanguageConfig::default()
5374 },
5375 None,
5376 ));
5377 let rust_language = Arc::new(Language::new(
5378 LanguageConfig {
5379 name: "Rust".into(),
5380 line_comments: vec!["// ".into(), "/// ".into()],
5381 ..LanguageConfig::default()
5382 },
5383 Some(tree_sitter_rust::LANGUAGE.into()),
5384 ));
5385
5386 let plaintext_language = Arc::new(Language::new(
5387 LanguageConfig {
5388 name: "Plain Text".into(),
5389 ..LanguageConfig::default()
5390 },
5391 None,
5392 ));
5393
5394 // Test basic rewrapping of a long line with a cursor
5395 assert_rewrap(
5396 indoc! {"
5397 // ˇThis is a long comment that needs to be wrapped.
5398 "},
5399 indoc! {"
5400 // ˇThis is a long comment that needs to
5401 // be wrapped.
5402 "},
5403 cpp_language.clone(),
5404 &mut cx,
5405 );
5406
5407 // Test rewrapping a full selection
5408 assert_rewrap(
5409 indoc! {"
5410 «// This selected long comment needs to be wrapped.ˇ»"
5411 },
5412 indoc! {"
5413 «// This selected long comment needs to
5414 // be wrapped.ˇ»"
5415 },
5416 cpp_language.clone(),
5417 &mut cx,
5418 );
5419
5420 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5421 assert_rewrap(
5422 indoc! {"
5423 // ˇThis is the first line.
5424 // Thisˇ is the second line.
5425 // This is the thirdˇ line, all part of one paragraph.
5426 "},
5427 indoc! {"
5428 // ˇThis is the first line. Thisˇ is the
5429 // second line. This is the thirdˇ line,
5430 // all part of one paragraph.
5431 "},
5432 cpp_language.clone(),
5433 &mut cx,
5434 );
5435
5436 // Test multiple cursors in different paragraphs trigger separate rewraps
5437 assert_rewrap(
5438 indoc! {"
5439 // ˇThis is the first paragraph, first line.
5440 // ˇThis is the first paragraph, second line.
5441
5442 // ˇThis is the second paragraph, first line.
5443 // ˇThis is the second paragraph, second line.
5444 "},
5445 indoc! {"
5446 // ˇThis is the first paragraph, first
5447 // line. ˇThis is the first paragraph,
5448 // second line.
5449
5450 // ˇThis is the second paragraph, first
5451 // line. ˇThis is the second paragraph,
5452 // second line.
5453 "},
5454 cpp_language.clone(),
5455 &mut cx,
5456 );
5457
5458 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
5459 assert_rewrap(
5460 indoc! {"
5461 «// A regular long long comment to be wrapped.
5462 /// A documentation long comment to be wrapped.ˇ»
5463 "},
5464 indoc! {"
5465 «// A regular long long comment to be
5466 // wrapped.
5467 /// A documentation long comment to be
5468 /// wrapped.ˇ»
5469 "},
5470 rust_language.clone(),
5471 &mut cx,
5472 );
5473
5474 // Test that change in indentation level trigger seperate rewraps
5475 assert_rewrap(
5476 indoc! {"
5477 fn foo() {
5478 «// This is a long comment at the base indent.
5479 // This is a long comment at the next indent.ˇ»
5480 }
5481 "},
5482 indoc! {"
5483 fn foo() {
5484 «// This is a long comment at the
5485 // base indent.
5486 // This is a long comment at the
5487 // next indent.ˇ»
5488 }
5489 "},
5490 rust_language.clone(),
5491 &mut cx,
5492 );
5493
5494 // Test that different comment prefix characters (e.g., '#') are handled correctly
5495 assert_rewrap(
5496 indoc! {"
5497 # ˇThis is a long comment using a pound sign.
5498 "},
5499 indoc! {"
5500 # ˇThis is a long comment using a pound
5501 # sign.
5502 "},
5503 python_language.clone(),
5504 &mut cx,
5505 );
5506
5507 // Test rewrapping only affects comments, not code even when selected
5508 assert_rewrap(
5509 indoc! {"
5510 «/// This doc comment is long and should be wrapped.
5511 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5512 "},
5513 indoc! {"
5514 «/// This doc comment is long and should
5515 /// be wrapped.
5516 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5517 "},
5518 rust_language.clone(),
5519 &mut cx,
5520 );
5521
5522 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
5523 assert_rewrap(
5524 indoc! {"
5525 # Header
5526
5527 A long long long line of markdown text to wrap.ˇ
5528 "},
5529 indoc! {"
5530 # Header
5531
5532 A long long long line of markdown text
5533 to wrap.ˇ
5534 "},
5535 markdown_language.clone(),
5536 &mut cx,
5537 );
5538
5539 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
5540 assert_rewrap(
5541 indoc! {"
5542 «1. This is a numbered list item that is very long and needs to be wrapped properly.
5543 2. This is a numbered list item that is very long and needs to be wrapped properly.
5544 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
5545 "},
5546 indoc! {"
5547 «1. This is a numbered list item that is
5548 very long and needs to be wrapped
5549 properly.
5550 2. This is a numbered list item that is
5551 very long and needs to be wrapped
5552 properly.
5553 - This is an unordered list item that is
5554 also very long and should not merge
5555 with the numbered item.ˇ»
5556 "},
5557 markdown_language.clone(),
5558 &mut cx,
5559 );
5560
5561 // Test that rewrapping add indents for rewrapping boundary if not exists already.
5562 assert_rewrap(
5563 indoc! {"
5564 «1. This is a numbered list item that is
5565 very long and needs to be wrapped
5566 properly.
5567 2. This is a numbered list item that is
5568 very long and needs to be wrapped
5569 properly.
5570 - This is an unordered list item that is
5571 also very long and should not merge with
5572 the numbered item.ˇ»
5573 "},
5574 indoc! {"
5575 «1. This is a numbered list item that is
5576 very long and needs to be wrapped
5577 properly.
5578 2. This is a numbered list item that is
5579 very long and needs to be wrapped
5580 properly.
5581 - This is an unordered list item that is
5582 also very long and should not merge
5583 with the numbered item.ˇ»
5584 "},
5585 markdown_language.clone(),
5586 &mut cx,
5587 );
5588
5589 // Test that rewrapping maintain indents even when they already exists.
5590 assert_rewrap(
5591 indoc! {"
5592 «1. This is a numbered list
5593 item that is very long and needs to be wrapped properly.
5594 2. This is a numbered list
5595 item that is very long and needs to be wrapped properly.
5596 - This is an unordered list item that is also very long and
5597 should not merge with the numbered item.ˇ»
5598 "},
5599 indoc! {"
5600 «1. This is a numbered list item that is
5601 very long and needs to be wrapped
5602 properly.
5603 2. This is a numbered list item that is
5604 very long and needs to be wrapped
5605 properly.
5606 - This is an unordered list item that is
5607 also very long and should not merge
5608 with the numbered item.ˇ»
5609 "},
5610 markdown_language.clone(),
5611 &mut cx,
5612 );
5613
5614 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
5615 assert_rewrap(
5616 indoc! {"
5617 ˇThis is a very long line of plain text that will be wrapped.
5618 "},
5619 indoc! {"
5620 ˇThis is a very long line of plain text
5621 that will be wrapped.
5622 "},
5623 plaintext_language.clone(),
5624 &mut cx,
5625 );
5626
5627 // Test that non-commented code acts as a paragraph boundary within a selection
5628 assert_rewrap(
5629 indoc! {"
5630 «// This is the first long comment block to be wrapped.
5631 fn my_func(a: u32);
5632 // This is the second long comment block to be wrapped.ˇ»
5633 "},
5634 indoc! {"
5635 «// This is the first long comment block
5636 // to be wrapped.
5637 fn my_func(a: u32);
5638 // This is the second long comment block
5639 // to be wrapped.ˇ»
5640 "},
5641 rust_language.clone(),
5642 &mut cx,
5643 );
5644
5645 // Test rewrapping multiple selections, including ones with blank lines or tabs
5646 assert_rewrap(
5647 indoc! {"
5648 «ˇThis is a very long line that will be wrapped.
5649
5650 This is another paragraph in the same selection.»
5651
5652 «\tThis is a very long indented line that will be wrapped.ˇ»
5653 "},
5654 indoc! {"
5655 «ˇThis is a very long line that will be
5656 wrapped.
5657
5658 This is another paragraph in the same
5659 selection.»
5660
5661 «\tThis is a very long indented line
5662 \tthat will be wrapped.ˇ»
5663 "},
5664 plaintext_language.clone(),
5665 &mut cx,
5666 );
5667
5668 // Test that an empty comment line acts as a paragraph boundary
5669 assert_rewrap(
5670 indoc! {"
5671 // ˇThis is a long comment that will be wrapped.
5672 //
5673 // And this is another long comment that will also be wrapped.ˇ
5674 "},
5675 indoc! {"
5676 // ˇThis is a long comment that will be
5677 // wrapped.
5678 //
5679 // And this is another long comment that
5680 // will also be wrapped.ˇ
5681 "},
5682 cpp_language,
5683 &mut cx,
5684 );
5685
5686 #[track_caller]
5687 fn assert_rewrap(
5688 unwrapped_text: &str,
5689 wrapped_text: &str,
5690 language: Arc<Language>,
5691 cx: &mut EditorTestContext,
5692 ) {
5693 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5694 cx.set_state(unwrapped_text);
5695 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5696 cx.assert_editor_state(wrapped_text);
5697 }
5698}
5699
5700#[gpui::test]
5701async fn test_hard_wrap(cx: &mut TestAppContext) {
5702 init_test(cx, |_| {});
5703 let mut cx = EditorTestContext::new(cx).await;
5704
5705 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5706 cx.update_editor(|editor, _, cx| {
5707 editor.set_hard_wrap(Some(14), cx);
5708 });
5709
5710 cx.set_state(indoc!(
5711 "
5712 one two three ˇ
5713 "
5714 ));
5715 cx.simulate_input("four");
5716 cx.run_until_parked();
5717
5718 cx.assert_editor_state(indoc!(
5719 "
5720 one two three
5721 fourˇ
5722 "
5723 ));
5724
5725 cx.update_editor(|editor, window, cx| {
5726 editor.newline(&Default::default(), window, cx);
5727 });
5728 cx.run_until_parked();
5729 cx.assert_editor_state(indoc!(
5730 "
5731 one two three
5732 four
5733 ˇ
5734 "
5735 ));
5736
5737 cx.simulate_input("five");
5738 cx.run_until_parked();
5739 cx.assert_editor_state(indoc!(
5740 "
5741 one two three
5742 four
5743 fiveˇ
5744 "
5745 ));
5746
5747 cx.update_editor(|editor, window, cx| {
5748 editor.newline(&Default::default(), window, cx);
5749 });
5750 cx.run_until_parked();
5751 cx.simulate_input("# ");
5752 cx.run_until_parked();
5753 cx.assert_editor_state(indoc!(
5754 "
5755 one two three
5756 four
5757 five
5758 # ˇ
5759 "
5760 ));
5761
5762 cx.update_editor(|editor, window, cx| {
5763 editor.newline(&Default::default(), window, cx);
5764 });
5765 cx.run_until_parked();
5766 cx.assert_editor_state(indoc!(
5767 "
5768 one two three
5769 four
5770 five
5771 #\x20
5772 #ˇ
5773 "
5774 ));
5775
5776 cx.simulate_input(" 6");
5777 cx.run_until_parked();
5778 cx.assert_editor_state(indoc!(
5779 "
5780 one two three
5781 four
5782 five
5783 #
5784 # 6ˇ
5785 "
5786 ));
5787}
5788
5789#[gpui::test]
5790async fn test_clipboard(cx: &mut TestAppContext) {
5791 init_test(cx, |_| {});
5792
5793 let mut cx = EditorTestContext::new(cx).await;
5794
5795 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5796 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5797 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5798
5799 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5800 cx.set_state("two ˇfour ˇsix ˇ");
5801 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5802 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5803
5804 // Paste again but with only two cursors. Since the number of cursors doesn't
5805 // match the number of slices in the clipboard, the entire clipboard text
5806 // is pasted at each cursor.
5807 cx.set_state("ˇtwo one✅ four three six five ˇ");
5808 cx.update_editor(|e, window, cx| {
5809 e.handle_input("( ", window, cx);
5810 e.paste(&Paste, window, cx);
5811 e.handle_input(") ", window, cx);
5812 });
5813 cx.assert_editor_state(
5814 &([
5815 "( one✅ ",
5816 "three ",
5817 "five ) ˇtwo one✅ four three six five ( one✅ ",
5818 "three ",
5819 "five ) ˇ",
5820 ]
5821 .join("\n")),
5822 );
5823
5824 // Cut with three selections, one of which is full-line.
5825 cx.set_state(indoc! {"
5826 1«2ˇ»3
5827 4ˇ567
5828 «8ˇ»9"});
5829 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5830 cx.assert_editor_state(indoc! {"
5831 1ˇ3
5832 ˇ9"});
5833
5834 // Paste with three selections, noticing how the copied selection that was full-line
5835 // gets inserted before the second cursor.
5836 cx.set_state(indoc! {"
5837 1ˇ3
5838 9ˇ
5839 «oˇ»ne"});
5840 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5841 cx.assert_editor_state(indoc! {"
5842 12ˇ3
5843 4567
5844 9ˇ
5845 8ˇne"});
5846
5847 // Copy with a single cursor only, which writes the whole line into the clipboard.
5848 cx.set_state(indoc! {"
5849 The quick brown
5850 fox juˇmps over
5851 the lazy dog"});
5852 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5853 assert_eq!(
5854 cx.read_from_clipboard()
5855 .and_then(|item| item.text().as_deref().map(str::to_string)),
5856 Some("fox jumps over\n".to_string())
5857 );
5858
5859 // Paste with three selections, noticing how the copied full-line selection is inserted
5860 // before the empty selections but replaces the selection that is non-empty.
5861 cx.set_state(indoc! {"
5862 Tˇhe quick brown
5863 «foˇ»x jumps over
5864 tˇhe lazy dog"});
5865 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5866 cx.assert_editor_state(indoc! {"
5867 fox jumps over
5868 Tˇhe quick brown
5869 fox jumps over
5870 ˇx jumps over
5871 fox jumps over
5872 tˇhe lazy dog"});
5873}
5874
5875#[gpui::test]
5876async fn test_copy_trim(cx: &mut TestAppContext) {
5877 init_test(cx, |_| {});
5878
5879 let mut cx = EditorTestContext::new(cx).await;
5880 cx.set_state(
5881 r#" «for selection 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 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5888 }
5889 "#,
5890 );
5891 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5892 assert_eq!(
5893 cx.read_from_clipboard()
5894 .and_then(|item| item.text().as_deref().map(str::to_string)),
5895 Some(
5896 "for selection in selections.iter() {
5897 let mut start = selection.start;
5898 let mut end = selection.end;
5899 let is_entire_line = selection.is_empty();
5900 if is_entire_line {
5901 start = Point::new(start.row, 0);"
5902 .to_string()
5903 ),
5904 "Regular copying preserves all indentation selected",
5905 );
5906 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5907 assert_eq!(
5908 cx.read_from_clipboard()
5909 .and_then(|item| item.text().as_deref().map(str::to_string)),
5910 Some(
5911 "for selection in selections.iter() {
5912let mut start = selection.start;
5913let mut end = selection.end;
5914let is_entire_line = selection.is_empty();
5915if is_entire_line {
5916 start = Point::new(start.row, 0);"
5917 .to_string()
5918 ),
5919 "Copying with stripping should strip all leading whitespaces"
5920 );
5921
5922 cx.set_state(
5923 r#" « for selection in selections.iter() {
5924 let mut start = selection.start;
5925 let mut end = selection.end;
5926 let is_entire_line = selection.is_empty();
5927 if is_entire_line {
5928 start = Point::new(start.row, 0);ˇ»
5929 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5930 }
5931 "#,
5932 );
5933 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5934 assert_eq!(
5935 cx.read_from_clipboard()
5936 .and_then(|item| item.text().as_deref().map(str::to_string)),
5937 Some(
5938 " for selection in selections.iter() {
5939 let mut start = selection.start;
5940 let mut end = selection.end;
5941 let is_entire_line = selection.is_empty();
5942 if is_entire_line {
5943 start = Point::new(start.row, 0);"
5944 .to_string()
5945 ),
5946 "Regular copying preserves all indentation selected",
5947 );
5948 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5949 assert_eq!(
5950 cx.read_from_clipboard()
5951 .and_then(|item| item.text().as_deref().map(str::to_string)),
5952 Some(
5953 "for selection in selections.iter() {
5954let mut start = selection.start;
5955let mut end = selection.end;
5956let is_entire_line = selection.is_empty();
5957if is_entire_line {
5958 start = Point::new(start.row, 0);"
5959 .to_string()
5960 ),
5961 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5962 );
5963
5964 cx.set_state(
5965 r#" «ˇ for selection in selections.iter() {
5966 let mut start = selection.start;
5967 let mut end = selection.end;
5968 let is_entire_line = selection.is_empty();
5969 if is_entire_line {
5970 start = Point::new(start.row, 0);»
5971 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5972 }
5973 "#,
5974 );
5975 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5976 assert_eq!(
5977 cx.read_from_clipboard()
5978 .and_then(|item| item.text().as_deref().map(str::to_string)),
5979 Some(
5980 " for selection in selections.iter() {
5981 let mut start = selection.start;
5982 let mut end = selection.end;
5983 let is_entire_line = selection.is_empty();
5984 if is_entire_line {
5985 start = Point::new(start.row, 0);"
5986 .to_string()
5987 ),
5988 "Regular copying for reverse selection works the same",
5989 );
5990 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5991 assert_eq!(
5992 cx.read_from_clipboard()
5993 .and_then(|item| item.text().as_deref().map(str::to_string)),
5994 Some(
5995 "for selection in selections.iter() {
5996let mut start = selection.start;
5997let mut end = selection.end;
5998let is_entire_line = selection.is_empty();
5999if is_entire_line {
6000 start = Point::new(start.row, 0);"
6001 .to_string()
6002 ),
6003 "Copying with stripping for reverse selection works the same"
6004 );
6005
6006 cx.set_state(
6007 r#" for selection «in selections.iter() {
6008 let mut start = selection.start;
6009 let mut end = selection.end;
6010 let is_entire_line = selection.is_empty();
6011 if is_entire_line {
6012 start = Point::new(start.row, 0);ˇ»
6013 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6014 }
6015 "#,
6016 );
6017 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6018 assert_eq!(
6019 cx.read_from_clipboard()
6020 .and_then(|item| item.text().as_deref().map(str::to_string)),
6021 Some(
6022 "in selections.iter() {
6023 let mut start = selection.start;
6024 let mut end = selection.end;
6025 let is_entire_line = selection.is_empty();
6026 if is_entire_line {
6027 start = Point::new(start.row, 0);"
6028 .to_string()
6029 ),
6030 "When selecting past the indent, the copying works as usual",
6031 );
6032 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6033 assert_eq!(
6034 cx.read_from_clipboard()
6035 .and_then(|item| item.text().as_deref().map(str::to_string)),
6036 Some(
6037 "in selections.iter() {
6038 let mut start = selection.start;
6039 let mut end = selection.end;
6040 let is_entire_line = selection.is_empty();
6041 if is_entire_line {
6042 start = Point::new(start.row, 0);"
6043 .to_string()
6044 ),
6045 "When selecting past the indent, nothing is trimmed"
6046 );
6047
6048 cx.set_state(
6049 r#" «for selection in selections.iter() {
6050 let mut start = selection.start;
6051
6052 let mut end = selection.end;
6053 let is_entire_line = selection.is_empty();
6054 if is_entire_line {
6055 start = Point::new(start.row, 0);
6056ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
6057 }
6058 "#,
6059 );
6060 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6061 assert_eq!(
6062 cx.read_from_clipboard()
6063 .and_then(|item| item.text().as_deref().map(str::to_string)),
6064 Some(
6065 "for selection in selections.iter() {
6066let mut start = selection.start;
6067
6068let mut end = selection.end;
6069let is_entire_line = selection.is_empty();
6070if is_entire_line {
6071 start = Point::new(start.row, 0);
6072"
6073 .to_string()
6074 ),
6075 "Copying with stripping should ignore empty lines"
6076 );
6077}
6078
6079#[gpui::test]
6080async fn test_paste_multiline(cx: &mut TestAppContext) {
6081 init_test(cx, |_| {});
6082
6083 let mut cx = EditorTestContext::new(cx).await;
6084 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6085
6086 // Cut an indented block, without the leading whitespace.
6087 cx.set_state(indoc! {"
6088 const a: B = (
6089 c(),
6090 «d(
6091 e,
6092 f
6093 )ˇ»
6094 );
6095 "});
6096 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6097 cx.assert_editor_state(indoc! {"
6098 const a: B = (
6099 c(),
6100 ˇ
6101 );
6102 "});
6103
6104 // Paste it at the same position.
6105 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6106 cx.assert_editor_state(indoc! {"
6107 const a: B = (
6108 c(),
6109 d(
6110 e,
6111 f
6112 )ˇ
6113 );
6114 "});
6115
6116 // Paste it at a line with a lower indent level.
6117 cx.set_state(indoc! {"
6118 ˇ
6119 const a: B = (
6120 c(),
6121 );
6122 "});
6123 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6124 cx.assert_editor_state(indoc! {"
6125 d(
6126 e,
6127 f
6128 )ˇ
6129 const a: B = (
6130 c(),
6131 );
6132 "});
6133
6134 // Cut an indented block, with the leading whitespace.
6135 cx.set_state(indoc! {"
6136 const a: B = (
6137 c(),
6138 « d(
6139 e,
6140 f
6141 )
6142 ˇ»);
6143 "});
6144 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6145 cx.assert_editor_state(indoc! {"
6146 const a: B = (
6147 c(),
6148 ˇ);
6149 "});
6150
6151 // Paste it at the same position.
6152 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6153 cx.assert_editor_state(indoc! {"
6154 const a: B = (
6155 c(),
6156 d(
6157 e,
6158 f
6159 )
6160 ˇ);
6161 "});
6162
6163 // Paste it at a line with a higher indent level.
6164 cx.set_state(indoc! {"
6165 const a: B = (
6166 c(),
6167 d(
6168 e,
6169 fˇ
6170 )
6171 );
6172 "});
6173 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6174 cx.assert_editor_state(indoc! {"
6175 const a: B = (
6176 c(),
6177 d(
6178 e,
6179 f d(
6180 e,
6181 f
6182 )
6183 ˇ
6184 )
6185 );
6186 "});
6187
6188 // Copy an indented block, starting mid-line
6189 cx.set_state(indoc! {"
6190 const a: B = (
6191 c(),
6192 somethin«g(
6193 e,
6194 f
6195 )ˇ»
6196 );
6197 "});
6198 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6199
6200 // Paste it on a line with a lower indent level
6201 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
6202 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6203 cx.assert_editor_state(indoc! {"
6204 const a: B = (
6205 c(),
6206 something(
6207 e,
6208 f
6209 )
6210 );
6211 g(
6212 e,
6213 f
6214 )ˇ"});
6215}
6216
6217#[gpui::test]
6218async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
6219 init_test(cx, |_| {});
6220
6221 cx.write_to_clipboard(ClipboardItem::new_string(
6222 " d(\n e\n );\n".into(),
6223 ));
6224
6225 let mut cx = EditorTestContext::new(cx).await;
6226 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6227
6228 cx.set_state(indoc! {"
6229 fn a() {
6230 b();
6231 if c() {
6232 ˇ
6233 }
6234 }
6235 "});
6236
6237 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6238 cx.assert_editor_state(indoc! {"
6239 fn a() {
6240 b();
6241 if c() {
6242 d(
6243 e
6244 );
6245 ˇ
6246 }
6247 }
6248 "});
6249
6250 cx.set_state(indoc! {"
6251 fn a() {
6252 b();
6253 ˇ
6254 }
6255 "});
6256
6257 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6258 cx.assert_editor_state(indoc! {"
6259 fn a() {
6260 b();
6261 d(
6262 e
6263 );
6264 ˇ
6265 }
6266 "});
6267}
6268
6269#[gpui::test]
6270fn test_select_all(cx: &mut TestAppContext) {
6271 init_test(cx, |_| {});
6272
6273 let editor = cx.add_window(|window, cx| {
6274 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
6275 build_editor(buffer, window, cx)
6276 });
6277 _ = editor.update(cx, |editor, window, cx| {
6278 editor.select_all(&SelectAll, window, cx);
6279 assert_eq!(
6280 editor.selections.display_ranges(cx),
6281 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
6282 );
6283 });
6284}
6285
6286#[gpui::test]
6287fn test_select_line(cx: &mut TestAppContext) {
6288 init_test(cx, |_| {});
6289
6290 let editor = cx.add_window(|window, cx| {
6291 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
6292 build_editor(buffer, window, cx)
6293 });
6294 _ = editor.update(cx, |editor, window, cx| {
6295 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6296 s.select_display_ranges([
6297 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6298 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6299 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6300 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
6301 ])
6302 });
6303 editor.select_line(&SelectLine, window, cx);
6304 assert_eq!(
6305 editor.selections.display_ranges(cx),
6306 vec![
6307 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
6308 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
6309 ]
6310 );
6311 });
6312
6313 _ = editor.update(cx, |editor, window, cx| {
6314 editor.select_line(&SelectLine, window, cx);
6315 assert_eq!(
6316 editor.selections.display_ranges(cx),
6317 vec![
6318 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
6319 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
6320 ]
6321 );
6322 });
6323
6324 _ = editor.update(cx, |editor, window, cx| {
6325 editor.select_line(&SelectLine, window, cx);
6326 assert_eq!(
6327 editor.selections.display_ranges(cx),
6328 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
6329 );
6330 });
6331}
6332
6333#[gpui::test]
6334async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
6335 init_test(cx, |_| {});
6336 let mut cx = EditorTestContext::new(cx).await;
6337
6338 #[track_caller]
6339 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
6340 cx.set_state(initial_state);
6341 cx.update_editor(|e, window, cx| {
6342 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
6343 });
6344 cx.assert_editor_state(expected_state);
6345 }
6346
6347 // Selection starts and ends at the middle of lines, left-to-right
6348 test(
6349 &mut cx,
6350 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
6351 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6352 );
6353 // Same thing, right-to-left
6354 test(
6355 &mut cx,
6356 "aa\nb«b\ncc\ndd\neˇ»e\nff",
6357 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6358 );
6359
6360 // Whole buffer, left-to-right, last line *doesn't* end with newline
6361 test(
6362 &mut cx,
6363 "«ˇaa\nbb\ncc\ndd\nee\nff»",
6364 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6365 );
6366 // Same thing, right-to-left
6367 test(
6368 &mut cx,
6369 "«aa\nbb\ncc\ndd\nee\nffˇ»",
6370 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6371 );
6372
6373 // Whole buffer, left-to-right, last line ends with newline
6374 test(
6375 &mut cx,
6376 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
6377 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6378 );
6379 // Same thing, right-to-left
6380 test(
6381 &mut cx,
6382 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
6383 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6384 );
6385
6386 // Starts at the end of a line, ends at the start of another
6387 test(
6388 &mut cx,
6389 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
6390 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
6391 );
6392}
6393
6394#[gpui::test]
6395async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
6396 init_test(cx, |_| {});
6397
6398 let editor = cx.add_window(|window, cx| {
6399 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
6400 build_editor(buffer, window, cx)
6401 });
6402
6403 // setup
6404 _ = editor.update(cx, |editor, window, cx| {
6405 editor.fold_creases(
6406 vec![
6407 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
6408 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
6409 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
6410 ],
6411 true,
6412 window,
6413 cx,
6414 );
6415 assert_eq!(
6416 editor.display_text(cx),
6417 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6418 );
6419 });
6420
6421 _ = editor.update(cx, |editor, window, cx| {
6422 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6423 s.select_display_ranges([
6424 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6425 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6426 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6427 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
6428 ])
6429 });
6430 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6431 assert_eq!(
6432 editor.display_text(cx),
6433 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6434 );
6435 });
6436 EditorTestContext::for_editor(editor, cx)
6437 .await
6438 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
6439
6440 _ = editor.update(cx, |editor, window, cx| {
6441 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6442 s.select_display_ranges([
6443 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
6444 ])
6445 });
6446 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6447 assert_eq!(
6448 editor.display_text(cx),
6449 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
6450 );
6451 assert_eq!(
6452 editor.selections.display_ranges(cx),
6453 [
6454 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
6455 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
6456 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
6457 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
6458 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
6459 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
6460 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
6461 ]
6462 );
6463 });
6464 EditorTestContext::for_editor(editor, cx)
6465 .await
6466 .assert_editor_state(
6467 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
6468 );
6469}
6470
6471#[gpui::test]
6472async fn test_add_selection_above_below(cx: &mut TestAppContext) {
6473 init_test(cx, |_| {});
6474
6475 let mut cx = EditorTestContext::new(cx).await;
6476
6477 cx.set_state(indoc!(
6478 r#"abc
6479 defˇghi
6480
6481 jk
6482 nlmo
6483 "#
6484 ));
6485
6486 cx.update_editor(|editor, window, cx| {
6487 editor.add_selection_above(&Default::default(), window, cx);
6488 });
6489
6490 cx.assert_editor_state(indoc!(
6491 r#"abcˇ
6492 defˇghi
6493
6494 jk
6495 nlmo
6496 "#
6497 ));
6498
6499 cx.update_editor(|editor, window, cx| {
6500 editor.add_selection_above(&Default::default(), window, cx);
6501 });
6502
6503 cx.assert_editor_state(indoc!(
6504 r#"abcˇ
6505 defˇghi
6506
6507 jk
6508 nlmo
6509 "#
6510 ));
6511
6512 cx.update_editor(|editor, window, cx| {
6513 editor.add_selection_below(&Default::default(), window, cx);
6514 });
6515
6516 cx.assert_editor_state(indoc!(
6517 r#"abc
6518 defˇghi
6519
6520 jk
6521 nlmo
6522 "#
6523 ));
6524
6525 cx.update_editor(|editor, window, cx| {
6526 editor.undo_selection(&Default::default(), window, cx);
6527 });
6528
6529 cx.assert_editor_state(indoc!(
6530 r#"abcˇ
6531 defˇghi
6532
6533 jk
6534 nlmo
6535 "#
6536 ));
6537
6538 cx.update_editor(|editor, window, cx| {
6539 editor.redo_selection(&Default::default(), window, cx);
6540 });
6541
6542 cx.assert_editor_state(indoc!(
6543 r#"abc
6544 defˇghi
6545
6546 jk
6547 nlmo
6548 "#
6549 ));
6550
6551 cx.update_editor(|editor, window, cx| {
6552 editor.add_selection_below(&Default::default(), window, cx);
6553 });
6554
6555 cx.assert_editor_state(indoc!(
6556 r#"abc
6557 defˇghi
6558 ˇ
6559 jk
6560 nlmo
6561 "#
6562 ));
6563
6564 cx.update_editor(|editor, window, cx| {
6565 editor.add_selection_below(&Default::default(), window, cx);
6566 });
6567
6568 cx.assert_editor_state(indoc!(
6569 r#"abc
6570 defˇghi
6571 ˇ
6572 jkˇ
6573 nlmo
6574 "#
6575 ));
6576
6577 cx.update_editor(|editor, window, cx| {
6578 editor.add_selection_below(&Default::default(), window, cx);
6579 });
6580
6581 cx.assert_editor_state(indoc!(
6582 r#"abc
6583 defˇghi
6584 ˇ
6585 jkˇ
6586 nlmˇo
6587 "#
6588 ));
6589
6590 cx.update_editor(|editor, window, cx| {
6591 editor.add_selection_below(&Default::default(), window, cx);
6592 });
6593
6594 cx.assert_editor_state(indoc!(
6595 r#"abc
6596 defˇghi
6597 ˇ
6598 jkˇ
6599 nlmˇo
6600 ˇ"#
6601 ));
6602
6603 // change selections
6604 cx.set_state(indoc!(
6605 r#"abc
6606 def«ˇg»hi
6607
6608 jk
6609 nlmo
6610 "#
6611 ));
6612
6613 cx.update_editor(|editor, window, cx| {
6614 editor.add_selection_below(&Default::default(), window, cx);
6615 });
6616
6617 cx.assert_editor_state(indoc!(
6618 r#"abc
6619 def«ˇg»hi
6620
6621 jk
6622 nlm«ˇo»
6623 "#
6624 ));
6625
6626 cx.update_editor(|editor, window, cx| {
6627 editor.add_selection_below(&Default::default(), window, cx);
6628 });
6629
6630 cx.assert_editor_state(indoc!(
6631 r#"abc
6632 def«ˇg»hi
6633
6634 jk
6635 nlm«ˇo»
6636 "#
6637 ));
6638
6639 cx.update_editor(|editor, window, cx| {
6640 editor.add_selection_above(&Default::default(), window, cx);
6641 });
6642
6643 cx.assert_editor_state(indoc!(
6644 r#"abc
6645 def«ˇg»hi
6646
6647 jk
6648 nlmo
6649 "#
6650 ));
6651
6652 cx.update_editor(|editor, window, cx| {
6653 editor.add_selection_above(&Default::default(), window, cx);
6654 });
6655
6656 cx.assert_editor_state(indoc!(
6657 r#"abc
6658 def«ˇg»hi
6659
6660 jk
6661 nlmo
6662 "#
6663 ));
6664
6665 // Change selections again
6666 cx.set_state(indoc!(
6667 r#"a«bc
6668 defgˇ»hi
6669
6670 jk
6671 nlmo
6672 "#
6673 ));
6674
6675 cx.update_editor(|editor, window, cx| {
6676 editor.add_selection_below(&Default::default(), window, cx);
6677 });
6678
6679 cx.assert_editor_state(indoc!(
6680 r#"a«bcˇ»
6681 d«efgˇ»hi
6682
6683 j«kˇ»
6684 nlmo
6685 "#
6686 ));
6687
6688 cx.update_editor(|editor, window, cx| {
6689 editor.add_selection_below(&Default::default(), window, cx);
6690 });
6691 cx.assert_editor_state(indoc!(
6692 r#"a«bcˇ»
6693 d«efgˇ»hi
6694
6695 j«kˇ»
6696 n«lmoˇ»
6697 "#
6698 ));
6699 cx.update_editor(|editor, window, cx| {
6700 editor.add_selection_above(&Default::default(), window, cx);
6701 });
6702
6703 cx.assert_editor_state(indoc!(
6704 r#"a«bcˇ»
6705 d«efgˇ»hi
6706
6707 j«kˇ»
6708 nlmo
6709 "#
6710 ));
6711
6712 // Change selections again
6713 cx.set_state(indoc!(
6714 r#"abc
6715 d«ˇefghi
6716
6717 jk
6718 nlm»o
6719 "#
6720 ));
6721
6722 cx.update_editor(|editor, window, cx| {
6723 editor.add_selection_above(&Default::default(), window, cx);
6724 });
6725
6726 cx.assert_editor_state(indoc!(
6727 r#"a«ˇbc»
6728 d«ˇef»ghi
6729
6730 j«ˇk»
6731 n«ˇlm»o
6732 "#
6733 ));
6734
6735 cx.update_editor(|editor, window, cx| {
6736 editor.add_selection_below(&Default::default(), window, cx);
6737 });
6738
6739 cx.assert_editor_state(indoc!(
6740 r#"abc
6741 d«ˇef»ghi
6742
6743 j«ˇk»
6744 n«ˇlm»o
6745 "#
6746 ));
6747}
6748
6749#[gpui::test]
6750async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
6751 init_test(cx, |_| {});
6752 let mut cx = EditorTestContext::new(cx).await;
6753
6754 cx.set_state(indoc!(
6755 r#"line onˇe
6756 liˇne two
6757 line three
6758 line four"#
6759 ));
6760
6761 cx.update_editor(|editor, window, cx| {
6762 editor.add_selection_below(&Default::default(), window, cx);
6763 });
6764
6765 // test multiple cursors expand in the same direction
6766 cx.assert_editor_state(indoc!(
6767 r#"line onˇe
6768 liˇne twˇo
6769 liˇne three
6770 line four"#
6771 ));
6772
6773 cx.update_editor(|editor, window, cx| {
6774 editor.add_selection_below(&Default::default(), window, cx);
6775 });
6776
6777 cx.update_editor(|editor, window, cx| {
6778 editor.add_selection_below(&Default::default(), window, cx);
6779 });
6780
6781 // test multiple cursors expand below overflow
6782 cx.assert_editor_state(indoc!(
6783 r#"line onˇe
6784 liˇne twˇo
6785 liˇne thˇree
6786 liˇne foˇur"#
6787 ));
6788
6789 cx.update_editor(|editor, window, cx| {
6790 editor.add_selection_above(&Default::default(), window, cx);
6791 });
6792
6793 // test multiple cursors retrieves back correctly
6794 cx.assert_editor_state(indoc!(
6795 r#"line onˇe
6796 liˇne twˇo
6797 liˇne thˇree
6798 line four"#
6799 ));
6800
6801 cx.update_editor(|editor, window, cx| {
6802 editor.add_selection_above(&Default::default(), window, cx);
6803 });
6804
6805 cx.update_editor(|editor, window, cx| {
6806 editor.add_selection_above(&Default::default(), window, cx);
6807 });
6808
6809 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
6810 cx.assert_editor_state(indoc!(
6811 r#"liˇne onˇe
6812 liˇne two
6813 line three
6814 line four"#
6815 ));
6816
6817 cx.update_editor(|editor, window, cx| {
6818 editor.undo_selection(&Default::default(), window, cx);
6819 });
6820
6821 // test undo
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 cx.update_editor(|editor, window, cx| {
6830 editor.redo_selection(&Default::default(), window, cx);
6831 });
6832
6833 // test redo
6834 cx.assert_editor_state(indoc!(
6835 r#"liˇne onˇe
6836 liˇne two
6837 line three
6838 line four"#
6839 ));
6840
6841 cx.set_state(indoc!(
6842 r#"abcd
6843 ef«ghˇ»
6844 ijkl
6845 «mˇ»nop"#
6846 ));
6847
6848 cx.update_editor(|editor, window, cx| {
6849 editor.add_selection_above(&Default::default(), window, cx);
6850 });
6851
6852 // test multiple selections expand in the same direction
6853 cx.assert_editor_state(indoc!(
6854 r#"ab«cdˇ»
6855 ef«ghˇ»
6856 «iˇ»jkl
6857 «mˇ»nop"#
6858 ));
6859
6860 cx.update_editor(|editor, window, cx| {
6861 editor.add_selection_above(&Default::default(), window, cx);
6862 });
6863
6864 // test multiple selection upward overflow
6865 cx.assert_editor_state(indoc!(
6866 r#"ab«cdˇ»
6867 «eˇ»f«ghˇ»
6868 «iˇ»jkl
6869 «mˇ»nop"#
6870 ));
6871
6872 cx.update_editor(|editor, window, cx| {
6873 editor.add_selection_below(&Default::default(), window, cx);
6874 });
6875
6876 // test multiple selection retrieves back correctly
6877 cx.assert_editor_state(indoc!(
6878 r#"abcd
6879 ef«ghˇ»
6880 «iˇ»jkl
6881 «mˇ»nop"#
6882 ));
6883
6884 cx.update_editor(|editor, window, cx| {
6885 editor.add_selection_below(&Default::default(), window, cx);
6886 });
6887
6888 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
6889 cx.assert_editor_state(indoc!(
6890 r#"abcd
6891 ef«ghˇ»
6892 ij«klˇ»
6893 «mˇ»nop"#
6894 ));
6895
6896 cx.update_editor(|editor, window, cx| {
6897 editor.undo_selection(&Default::default(), window, cx);
6898 });
6899
6900 // test undo
6901 cx.assert_editor_state(indoc!(
6902 r#"abcd
6903 ef«ghˇ»
6904 «iˇ»jkl
6905 «mˇ»nop"#
6906 ));
6907
6908 cx.update_editor(|editor, window, cx| {
6909 editor.redo_selection(&Default::default(), window, cx);
6910 });
6911
6912 // test redo
6913 cx.assert_editor_state(indoc!(
6914 r#"abcd
6915 ef«ghˇ»
6916 ij«klˇ»
6917 «mˇ»nop"#
6918 ));
6919}
6920
6921#[gpui::test]
6922async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
6923 init_test(cx, |_| {});
6924 let mut cx = EditorTestContext::new(cx).await;
6925
6926 cx.set_state(indoc!(
6927 r#"line onˇe
6928 liˇne two
6929 line three
6930 line four"#
6931 ));
6932
6933 cx.update_editor(|editor, window, cx| {
6934 editor.add_selection_below(&Default::default(), window, cx);
6935 editor.add_selection_below(&Default::default(), window, cx);
6936 editor.add_selection_below(&Default::default(), window, cx);
6937 });
6938
6939 // initial state with two multi cursor groups
6940 cx.assert_editor_state(indoc!(
6941 r#"line onˇe
6942 liˇne twˇo
6943 liˇne thˇree
6944 liˇne foˇur"#
6945 ));
6946
6947 // add single cursor in middle - simulate opt click
6948 cx.update_editor(|editor, window, cx| {
6949 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
6950 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6951 editor.end_selection(window, cx);
6952 });
6953
6954 cx.assert_editor_state(indoc!(
6955 r#"line onˇe
6956 liˇne twˇo
6957 liˇneˇ thˇree
6958 liˇne foˇur"#
6959 ));
6960
6961 cx.update_editor(|editor, window, cx| {
6962 editor.add_selection_above(&Default::default(), window, cx);
6963 });
6964
6965 // test new added selection expands above and existing selection shrinks
6966 cx.assert_editor_state(indoc!(
6967 r#"line onˇe
6968 liˇneˇ twˇo
6969 liˇneˇ thˇree
6970 line four"#
6971 ));
6972
6973 cx.update_editor(|editor, window, cx| {
6974 editor.add_selection_above(&Default::default(), window, cx);
6975 });
6976
6977 // test new added selection expands above and existing selection shrinks
6978 cx.assert_editor_state(indoc!(
6979 r#"lineˇ onˇe
6980 liˇneˇ twˇo
6981 lineˇ three
6982 line four"#
6983 ));
6984
6985 // intial state with two selection groups
6986 cx.set_state(indoc!(
6987 r#"abcd
6988 ef«ghˇ»
6989 ijkl
6990 «mˇ»nop"#
6991 ));
6992
6993 cx.update_editor(|editor, window, cx| {
6994 editor.add_selection_above(&Default::default(), window, cx);
6995 editor.add_selection_above(&Default::default(), window, cx);
6996 });
6997
6998 cx.assert_editor_state(indoc!(
6999 r#"ab«cdˇ»
7000 «eˇ»f«ghˇ»
7001 «iˇ»jkl
7002 «mˇ»nop"#
7003 ));
7004
7005 // add single selection in middle - simulate opt drag
7006 cx.update_editor(|editor, window, cx| {
7007 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
7008 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7009 editor.update_selection(
7010 DisplayPoint::new(DisplayRow(2), 4),
7011 0,
7012 gpui::Point::<f32>::default(),
7013 window,
7014 cx,
7015 );
7016 editor.end_selection(window, cx);
7017 });
7018
7019 cx.assert_editor_state(indoc!(
7020 r#"ab«cdˇ»
7021 «eˇ»f«ghˇ»
7022 «iˇ»jk«lˇ»
7023 «mˇ»nop"#
7024 ));
7025
7026 cx.update_editor(|editor, window, cx| {
7027 editor.add_selection_below(&Default::default(), window, cx);
7028 });
7029
7030 // test new added selection expands below, others shrinks from above
7031 cx.assert_editor_state(indoc!(
7032 r#"abcd
7033 ef«ghˇ»
7034 «iˇ»jk«lˇ»
7035 «mˇ»no«pˇ»"#
7036 ));
7037}
7038
7039#[gpui::test]
7040async fn test_select_next(cx: &mut TestAppContext) {
7041 init_test(cx, |_| {});
7042
7043 let mut cx = EditorTestContext::new(cx).await;
7044 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7045
7046 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7047 .unwrap();
7048 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7049
7050 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7051 .unwrap();
7052 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7053
7054 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7055 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7056
7057 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7058 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7059
7060 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7061 .unwrap();
7062 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7063
7064 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7065 .unwrap();
7066 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7067
7068 // Test selection direction should be preserved
7069 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7070
7071 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7072 .unwrap();
7073 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
7074}
7075
7076#[gpui::test]
7077async fn test_select_all_matches(cx: &mut TestAppContext) {
7078 init_test(cx, |_| {});
7079
7080 let mut cx = EditorTestContext::new(cx).await;
7081
7082 // Test caret-only selections
7083 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7084 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7085 .unwrap();
7086 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7087
7088 // Test left-to-right selections
7089 cx.set_state("abc\n«abcˇ»\nabc");
7090 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7091 .unwrap();
7092 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
7093
7094 // Test right-to-left selections
7095 cx.set_state("abc\n«ˇabc»\nabc");
7096 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7097 .unwrap();
7098 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
7099
7100 // Test selecting whitespace with caret selection
7101 cx.set_state("abc\nˇ abc\nabc");
7102 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7103 .unwrap();
7104 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7105
7106 // Test selecting whitespace with left-to-right selection
7107 cx.set_state("abc\n«ˇ »abc\nabc");
7108 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7109 .unwrap();
7110 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
7111
7112 // Test no matches with right-to-left selection
7113 cx.set_state("abc\n« ˇ»abc\nabc");
7114 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7115 .unwrap();
7116 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7117
7118 // Test with a single word and clip_at_line_ends=true (#29823)
7119 cx.set_state("aˇbc");
7120 cx.update_editor(|e, window, cx| {
7121 e.set_clip_at_line_ends(true, cx);
7122 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
7123 e.set_clip_at_line_ends(false, cx);
7124 });
7125 cx.assert_editor_state("«abcˇ»");
7126}
7127
7128#[gpui::test]
7129async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
7130 init_test(cx, |_| {});
7131
7132 let mut cx = EditorTestContext::new(cx).await;
7133
7134 let large_body_1 = "\nd".repeat(200);
7135 let large_body_2 = "\ne".repeat(200);
7136
7137 cx.set_state(&format!(
7138 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
7139 ));
7140 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
7141 let scroll_position = editor.scroll_position(cx);
7142 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
7143 scroll_position
7144 });
7145
7146 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7147 .unwrap();
7148 cx.assert_editor_state(&format!(
7149 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
7150 ));
7151 let scroll_position_after_selection =
7152 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
7153 assert_eq!(
7154 initial_scroll_position, scroll_position_after_selection,
7155 "Scroll position should not change after selecting all matches"
7156 );
7157}
7158
7159#[gpui::test]
7160async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
7161 init_test(cx, |_| {});
7162
7163 let mut cx = EditorLspTestContext::new_rust(
7164 lsp::ServerCapabilities {
7165 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7166 ..Default::default()
7167 },
7168 cx,
7169 )
7170 .await;
7171
7172 cx.set_state(indoc! {"
7173 line 1
7174 line 2
7175 linˇe 3
7176 line 4
7177 line 5
7178 "});
7179
7180 // Make an edit
7181 cx.update_editor(|editor, window, cx| {
7182 editor.handle_input("X", window, cx);
7183 });
7184
7185 // Move cursor to a different position
7186 cx.update_editor(|editor, window, cx| {
7187 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7188 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
7189 });
7190 });
7191
7192 cx.assert_editor_state(indoc! {"
7193 line 1
7194 line 2
7195 linXe 3
7196 line 4
7197 liˇne 5
7198 "});
7199
7200 cx.lsp
7201 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
7202 Ok(Some(vec![lsp::TextEdit::new(
7203 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
7204 "PREFIX ".to_string(),
7205 )]))
7206 });
7207
7208 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
7209 .unwrap()
7210 .await
7211 .unwrap();
7212
7213 cx.assert_editor_state(indoc! {"
7214 PREFIX line 1
7215 line 2
7216 linXe 3
7217 line 4
7218 liˇne 5
7219 "});
7220
7221 // Undo formatting
7222 cx.update_editor(|editor, window, cx| {
7223 editor.undo(&Default::default(), window, cx);
7224 });
7225
7226 // Verify cursor moved back to position after edit
7227 cx.assert_editor_state(indoc! {"
7228 line 1
7229 line 2
7230 linXˇe 3
7231 line 4
7232 line 5
7233 "});
7234}
7235
7236#[gpui::test]
7237async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) {
7238 init_test(cx, |_| {});
7239
7240 let mut cx = EditorTestContext::new(cx).await;
7241
7242 let provider = cx.new(|_| FakeInlineCompletionProvider::default());
7243 cx.update_editor(|editor, window, cx| {
7244 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
7245 });
7246
7247 cx.set_state(indoc! {"
7248 line 1
7249 line 2
7250 linˇe 3
7251 line 4
7252 line 5
7253 line 6
7254 line 7
7255 line 8
7256 line 9
7257 line 10
7258 "});
7259
7260 let snapshot = cx.buffer_snapshot();
7261 let edit_position = snapshot.anchor_after(Point::new(2, 4));
7262
7263 cx.update(|_, cx| {
7264 provider.update(cx, |provider, _| {
7265 provider.set_inline_completion(Some(inline_completion::InlineCompletion {
7266 id: None,
7267 edits: vec![(edit_position..edit_position, "X".into())],
7268 edit_preview: None,
7269 }))
7270 })
7271 });
7272
7273 cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
7274 cx.update_editor(|editor, window, cx| {
7275 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
7276 });
7277
7278 cx.assert_editor_state(indoc! {"
7279 line 1
7280 line 2
7281 lineXˇ 3
7282 line 4
7283 line 5
7284 line 6
7285 line 7
7286 line 8
7287 line 9
7288 line 10
7289 "});
7290
7291 cx.update_editor(|editor, window, cx| {
7292 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7293 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
7294 });
7295 });
7296
7297 cx.assert_editor_state(indoc! {"
7298 line 1
7299 line 2
7300 lineX 3
7301 line 4
7302 line 5
7303 line 6
7304 line 7
7305 line 8
7306 line 9
7307 liˇne 10
7308 "});
7309
7310 cx.update_editor(|editor, window, cx| {
7311 editor.undo(&Default::default(), window, cx);
7312 });
7313
7314 cx.assert_editor_state(indoc! {"
7315 line 1
7316 line 2
7317 lineˇ 3
7318 line 4
7319 line 5
7320 line 6
7321 line 7
7322 line 8
7323 line 9
7324 line 10
7325 "});
7326}
7327
7328#[gpui::test]
7329async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
7330 init_test(cx, |_| {});
7331
7332 let mut cx = EditorTestContext::new(cx).await;
7333 cx.set_state(
7334 r#"let foo = 2;
7335lˇet foo = 2;
7336let fooˇ = 2;
7337let foo = 2;
7338let foo = ˇ2;"#,
7339 );
7340
7341 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7342 .unwrap();
7343 cx.assert_editor_state(
7344 r#"let foo = 2;
7345«letˇ» foo = 2;
7346let «fooˇ» = 2;
7347let foo = 2;
7348let foo = «2ˇ»;"#,
7349 );
7350
7351 // noop for multiple selections with different contents
7352 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7353 .unwrap();
7354 cx.assert_editor_state(
7355 r#"let foo = 2;
7356«letˇ» foo = 2;
7357let «fooˇ» = 2;
7358let foo = 2;
7359let foo = «2ˇ»;"#,
7360 );
7361
7362 // Test last selection direction should be preserved
7363 cx.set_state(
7364 r#"let foo = 2;
7365let foo = 2;
7366let «fooˇ» = 2;
7367let «ˇfoo» = 2;
7368let foo = 2;"#,
7369 );
7370
7371 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7372 .unwrap();
7373 cx.assert_editor_state(
7374 r#"let foo = 2;
7375let foo = 2;
7376let «fooˇ» = 2;
7377let «ˇfoo» = 2;
7378let «ˇfoo» = 2;"#,
7379 );
7380}
7381
7382#[gpui::test]
7383async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
7384 init_test(cx, |_| {});
7385
7386 let mut cx =
7387 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
7388
7389 cx.assert_editor_state(indoc! {"
7390 ˇbbb
7391 ccc
7392
7393 bbb
7394 ccc
7395 "});
7396 cx.dispatch_action(SelectPrevious::default());
7397 cx.assert_editor_state(indoc! {"
7398 «bbbˇ»
7399 ccc
7400
7401 bbb
7402 ccc
7403 "});
7404 cx.dispatch_action(SelectPrevious::default());
7405 cx.assert_editor_state(indoc! {"
7406 «bbbˇ»
7407 ccc
7408
7409 «bbbˇ»
7410 ccc
7411 "});
7412}
7413
7414#[gpui::test]
7415async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
7416 init_test(cx, |_| {});
7417
7418 let mut cx = EditorTestContext::new(cx).await;
7419 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7420
7421 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7422 .unwrap();
7423 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7424
7425 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7426 .unwrap();
7427 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7428
7429 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7430 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7431
7432 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7433 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7434
7435 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7436 .unwrap();
7437 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
7438
7439 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7440 .unwrap();
7441 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7442}
7443
7444#[gpui::test]
7445async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
7446 init_test(cx, |_| {});
7447
7448 let mut cx = EditorTestContext::new(cx).await;
7449 cx.set_state("aˇ");
7450
7451 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7452 .unwrap();
7453 cx.assert_editor_state("«aˇ»");
7454 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7455 .unwrap();
7456 cx.assert_editor_state("«aˇ»");
7457}
7458
7459#[gpui::test]
7460async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
7461 init_test(cx, |_| {});
7462
7463 let mut cx = EditorTestContext::new(cx).await;
7464 cx.set_state(
7465 r#"let foo = 2;
7466lˇet foo = 2;
7467let fooˇ = 2;
7468let foo = 2;
7469let foo = ˇ2;"#,
7470 );
7471
7472 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7473 .unwrap();
7474 cx.assert_editor_state(
7475 r#"let foo = 2;
7476«letˇ» foo = 2;
7477let «fooˇ» = 2;
7478let foo = 2;
7479let foo = «2ˇ»;"#,
7480 );
7481
7482 // noop for multiple selections with different contents
7483 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7484 .unwrap();
7485 cx.assert_editor_state(
7486 r#"let foo = 2;
7487«letˇ» foo = 2;
7488let «fooˇ» = 2;
7489let foo = 2;
7490let foo = «2ˇ»;"#,
7491 );
7492}
7493
7494#[gpui::test]
7495async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
7496 init_test(cx, |_| {});
7497
7498 let mut cx = EditorTestContext::new(cx).await;
7499 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7500
7501 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7502 .unwrap();
7503 // selection direction is preserved
7504 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7505
7506 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7507 .unwrap();
7508 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7509
7510 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7511 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7512
7513 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7514 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7515
7516 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7517 .unwrap();
7518 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
7519
7520 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7521 .unwrap();
7522 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
7523}
7524
7525#[gpui::test]
7526async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
7527 init_test(cx, |_| {});
7528
7529 let language = Arc::new(Language::new(
7530 LanguageConfig::default(),
7531 Some(tree_sitter_rust::LANGUAGE.into()),
7532 ));
7533
7534 let text = r#"
7535 use mod1::mod2::{mod3, mod4};
7536
7537 fn fn_1(param1: bool, param2: &str) {
7538 let var1 = "text";
7539 }
7540 "#
7541 .unindent();
7542
7543 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7544 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7545 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7546
7547 editor
7548 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7549 .await;
7550
7551 editor.update_in(cx, |editor, window, cx| {
7552 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7553 s.select_display_ranges([
7554 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
7555 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
7556 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
7557 ]);
7558 });
7559 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7560 });
7561 editor.update(cx, |editor, cx| {
7562 assert_text_with_selections(
7563 editor,
7564 indoc! {r#"
7565 use mod1::mod2::{mod3, «mod4ˇ»};
7566
7567 fn fn_1«ˇ(param1: bool, param2: &str)» {
7568 let var1 = "«ˇtext»";
7569 }
7570 "#},
7571 cx,
7572 );
7573 });
7574
7575 editor.update_in(cx, |editor, window, cx| {
7576 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7577 });
7578 editor.update(cx, |editor, cx| {
7579 assert_text_with_selections(
7580 editor,
7581 indoc! {r#"
7582 use mod1::mod2::«{mod3, mod4}ˇ»;
7583
7584 «ˇfn fn_1(param1: bool, param2: &str) {
7585 let var1 = "text";
7586 }»
7587 "#},
7588 cx,
7589 );
7590 });
7591
7592 editor.update_in(cx, |editor, window, cx| {
7593 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7594 });
7595 assert_eq!(
7596 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7597 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7598 );
7599
7600 // Trying to expand the selected syntax node one more time has no effect.
7601 editor.update_in(cx, |editor, window, cx| {
7602 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7603 });
7604 assert_eq!(
7605 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7606 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7607 );
7608
7609 editor.update_in(cx, |editor, window, cx| {
7610 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7611 });
7612 editor.update(cx, |editor, cx| {
7613 assert_text_with_selections(
7614 editor,
7615 indoc! {r#"
7616 use mod1::mod2::«{mod3, mod4}ˇ»;
7617
7618 «ˇfn fn_1(param1: bool, param2: &str) {
7619 let var1 = "text";
7620 }»
7621 "#},
7622 cx,
7623 );
7624 });
7625
7626 editor.update_in(cx, |editor, window, cx| {
7627 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7628 });
7629 editor.update(cx, |editor, cx| {
7630 assert_text_with_selections(
7631 editor,
7632 indoc! {r#"
7633 use mod1::mod2::{mod3, «mod4ˇ»};
7634
7635 fn fn_1«ˇ(param1: bool, param2: &str)» {
7636 let var1 = "«ˇtext»";
7637 }
7638 "#},
7639 cx,
7640 );
7641 });
7642
7643 editor.update_in(cx, |editor, window, cx| {
7644 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7645 });
7646 editor.update(cx, |editor, cx| {
7647 assert_text_with_selections(
7648 editor,
7649 indoc! {r#"
7650 use mod1::mod2::{mod3, mo«ˇ»d4};
7651
7652 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7653 let var1 = "te«ˇ»xt";
7654 }
7655 "#},
7656 cx,
7657 );
7658 });
7659
7660 // Trying to shrink the selected syntax node one more time has no effect.
7661 editor.update_in(cx, |editor, window, cx| {
7662 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7663 });
7664 editor.update_in(cx, |editor, _, cx| {
7665 assert_text_with_selections(
7666 editor,
7667 indoc! {r#"
7668 use mod1::mod2::{mod3, mo«ˇ»d4};
7669
7670 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7671 let var1 = "te«ˇ»xt";
7672 }
7673 "#},
7674 cx,
7675 );
7676 });
7677
7678 // Ensure that we keep expanding the selection if the larger selection starts or ends within
7679 // a fold.
7680 editor.update_in(cx, |editor, window, cx| {
7681 editor.fold_creases(
7682 vec![
7683 Crease::simple(
7684 Point::new(0, 21)..Point::new(0, 24),
7685 FoldPlaceholder::test(),
7686 ),
7687 Crease::simple(
7688 Point::new(3, 20)..Point::new(3, 22),
7689 FoldPlaceholder::test(),
7690 ),
7691 ],
7692 true,
7693 window,
7694 cx,
7695 );
7696 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7697 });
7698 editor.update(cx, |editor, cx| {
7699 assert_text_with_selections(
7700 editor,
7701 indoc! {r#"
7702 use mod1::mod2::«{mod3, mod4}ˇ»;
7703
7704 fn fn_1«ˇ(param1: bool, param2: &str)» {
7705 let var1 = "«ˇtext»";
7706 }
7707 "#},
7708 cx,
7709 );
7710 });
7711}
7712
7713#[gpui::test]
7714async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
7715 init_test(cx, |_| {});
7716
7717 let language = Arc::new(Language::new(
7718 LanguageConfig::default(),
7719 Some(tree_sitter_rust::LANGUAGE.into()),
7720 ));
7721
7722 let text = "let a = 2;";
7723
7724 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7725 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7726 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7727
7728 editor
7729 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7730 .await;
7731
7732 // Test case 1: Cursor at end of word
7733 editor.update_in(cx, |editor, window, cx| {
7734 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7735 s.select_display_ranges([
7736 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
7737 ]);
7738 });
7739 });
7740 editor.update(cx, |editor, cx| {
7741 assert_text_with_selections(editor, "let aˇ = 2;", cx);
7742 });
7743 editor.update_in(cx, |editor, window, cx| {
7744 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7745 });
7746 editor.update(cx, |editor, cx| {
7747 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
7748 });
7749 editor.update_in(cx, |editor, window, cx| {
7750 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7751 });
7752 editor.update(cx, |editor, cx| {
7753 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7754 });
7755
7756 // Test case 2: Cursor at end of statement
7757 editor.update_in(cx, |editor, window, cx| {
7758 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7759 s.select_display_ranges([
7760 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
7761 ]);
7762 });
7763 });
7764 editor.update(cx, |editor, cx| {
7765 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
7766 });
7767 editor.update_in(cx, |editor, window, cx| {
7768 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7769 });
7770 editor.update(cx, |editor, cx| {
7771 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7772 });
7773}
7774
7775#[gpui::test]
7776async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
7777 init_test(cx, |_| {});
7778
7779 let language = Arc::new(Language::new(
7780 LanguageConfig::default(),
7781 Some(tree_sitter_rust::LANGUAGE.into()),
7782 ));
7783
7784 let text = r#"
7785 use mod1::mod2::{mod3, mod4};
7786
7787 fn fn_1(param1: bool, param2: &str) {
7788 let var1 = "hello world";
7789 }
7790 "#
7791 .unindent();
7792
7793 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7794 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7795 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7796
7797 editor
7798 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7799 .await;
7800
7801 // Test 1: Cursor on a letter of a string word
7802 editor.update_in(cx, |editor, window, cx| {
7803 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7804 s.select_display_ranges([
7805 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
7806 ]);
7807 });
7808 });
7809 editor.update_in(cx, |editor, window, cx| {
7810 assert_text_with_selections(
7811 editor,
7812 indoc! {r#"
7813 use mod1::mod2::{mod3, mod4};
7814
7815 fn fn_1(param1: bool, param2: &str) {
7816 let var1 = "hˇello world";
7817 }
7818 "#},
7819 cx,
7820 );
7821 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7822 assert_text_with_selections(
7823 editor,
7824 indoc! {r#"
7825 use mod1::mod2::{mod3, mod4};
7826
7827 fn fn_1(param1: bool, param2: &str) {
7828 let var1 = "«ˇhello» world";
7829 }
7830 "#},
7831 cx,
7832 );
7833 });
7834
7835 // Test 2: Partial selection within a word
7836 editor.update_in(cx, |editor, window, cx| {
7837 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7838 s.select_display_ranges([
7839 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7840 ]);
7841 });
7842 });
7843 editor.update_in(cx, |editor, window, cx| {
7844 assert_text_with_selections(
7845 editor,
7846 indoc! {r#"
7847 use mod1::mod2::{mod3, mod4};
7848
7849 fn fn_1(param1: bool, param2: &str) {
7850 let var1 = "h«elˇ»lo world";
7851 }
7852 "#},
7853 cx,
7854 );
7855 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7856 assert_text_with_selections(
7857 editor,
7858 indoc! {r#"
7859 use mod1::mod2::{mod3, mod4};
7860
7861 fn fn_1(param1: bool, param2: &str) {
7862 let var1 = "«ˇhello» world";
7863 }
7864 "#},
7865 cx,
7866 );
7867 });
7868
7869 // Test 3: Complete word already selected
7870 editor.update_in(cx, |editor, window, cx| {
7871 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7872 s.select_display_ranges([
7873 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7874 ]);
7875 });
7876 });
7877 editor.update_in(cx, |editor, window, cx| {
7878 assert_text_with_selections(
7879 editor,
7880 indoc! {r#"
7881 use mod1::mod2::{mod3, mod4};
7882
7883 fn fn_1(param1: bool, param2: &str) {
7884 let var1 = "«helloˇ» world";
7885 }
7886 "#},
7887 cx,
7888 );
7889 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7890 assert_text_with_selections(
7891 editor,
7892 indoc! {r#"
7893 use mod1::mod2::{mod3, mod4};
7894
7895 fn fn_1(param1: bool, param2: &str) {
7896 let var1 = "«hello worldˇ»";
7897 }
7898 "#},
7899 cx,
7900 );
7901 });
7902
7903 // Test 4: Selection spanning across words
7904 editor.update_in(cx, |editor, window, cx| {
7905 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7906 s.select_display_ranges([
7907 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7908 ]);
7909 });
7910 });
7911 editor.update_in(cx, |editor, window, cx| {
7912 assert_text_with_selections(
7913 editor,
7914 indoc! {r#"
7915 use mod1::mod2::{mod3, mod4};
7916
7917 fn fn_1(param1: bool, param2: &str) {
7918 let var1 = "hel«lo woˇ»rld";
7919 }
7920 "#},
7921 cx,
7922 );
7923 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7924 assert_text_with_selections(
7925 editor,
7926 indoc! {r#"
7927 use mod1::mod2::{mod3, mod4};
7928
7929 fn fn_1(param1: bool, param2: &str) {
7930 let var1 = "«ˇhello world»";
7931 }
7932 "#},
7933 cx,
7934 );
7935 });
7936
7937 // Test 5: Expansion beyond string
7938 editor.update_in(cx, |editor, window, cx| {
7939 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7940 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7941 assert_text_with_selections(
7942 editor,
7943 indoc! {r#"
7944 use mod1::mod2::{mod3, mod4};
7945
7946 fn fn_1(param1: bool, param2: &str) {
7947 «ˇlet var1 = "hello world";»
7948 }
7949 "#},
7950 cx,
7951 );
7952 });
7953}
7954
7955#[gpui::test]
7956async fn test_fold_function_bodies(cx: &mut TestAppContext) {
7957 init_test(cx, |_| {});
7958
7959 let base_text = r#"
7960 impl A {
7961 // this is an uncommitted comment
7962
7963 fn b() {
7964 c();
7965 }
7966
7967 // this is another uncommitted comment
7968
7969 fn d() {
7970 // e
7971 // f
7972 }
7973 }
7974
7975 fn g() {
7976 // h
7977 }
7978 "#
7979 .unindent();
7980
7981 let text = r#"
7982 ˇimpl A {
7983
7984 fn b() {
7985 c();
7986 }
7987
7988 fn d() {
7989 // e
7990 // f
7991 }
7992 }
7993
7994 fn g() {
7995 // h
7996 }
7997 "#
7998 .unindent();
7999
8000 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8001 cx.set_state(&text);
8002 cx.set_head_text(&base_text);
8003 cx.update_editor(|editor, window, cx| {
8004 editor.expand_all_diff_hunks(&Default::default(), window, cx);
8005 });
8006
8007 cx.assert_state_with_diff(
8008 "
8009 ˇimpl A {
8010 - // this is an uncommitted comment
8011
8012 fn b() {
8013 c();
8014 }
8015
8016 - // this is another uncommitted comment
8017 -
8018 fn d() {
8019 // e
8020 // f
8021 }
8022 }
8023
8024 fn g() {
8025 // h
8026 }
8027 "
8028 .unindent(),
8029 );
8030
8031 let expected_display_text = "
8032 impl A {
8033 // this is an uncommitted comment
8034
8035 fn b() {
8036 ⋯
8037 }
8038
8039 // this is another uncommitted comment
8040
8041 fn d() {
8042 ⋯
8043 }
8044 }
8045
8046 fn g() {
8047 ⋯
8048 }
8049 "
8050 .unindent();
8051
8052 cx.update_editor(|editor, window, cx| {
8053 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
8054 assert_eq!(editor.display_text(cx), expected_display_text);
8055 });
8056}
8057
8058#[gpui::test]
8059async fn test_autoindent(cx: &mut TestAppContext) {
8060 init_test(cx, |_| {});
8061
8062 let language = Arc::new(
8063 Language::new(
8064 LanguageConfig {
8065 brackets: BracketPairConfig {
8066 pairs: vec![
8067 BracketPair {
8068 start: "{".to_string(),
8069 end: "}".to_string(),
8070 close: false,
8071 surround: false,
8072 newline: true,
8073 },
8074 BracketPair {
8075 start: "(".to_string(),
8076 end: ")".to_string(),
8077 close: false,
8078 surround: false,
8079 newline: true,
8080 },
8081 ],
8082 ..Default::default()
8083 },
8084 ..Default::default()
8085 },
8086 Some(tree_sitter_rust::LANGUAGE.into()),
8087 )
8088 .with_indents_query(
8089 r#"
8090 (_ "(" ")" @end) @indent
8091 (_ "{" "}" @end) @indent
8092 "#,
8093 )
8094 .unwrap(),
8095 );
8096
8097 let text = "fn a() {}";
8098
8099 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8100 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8101 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8102 editor
8103 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8104 .await;
8105
8106 editor.update_in(cx, |editor, window, cx| {
8107 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8108 s.select_ranges([5..5, 8..8, 9..9])
8109 });
8110 editor.newline(&Newline, window, cx);
8111 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
8112 assert_eq!(
8113 editor.selections.ranges(cx),
8114 &[
8115 Point::new(1, 4)..Point::new(1, 4),
8116 Point::new(3, 4)..Point::new(3, 4),
8117 Point::new(5, 0)..Point::new(5, 0)
8118 ]
8119 );
8120 });
8121}
8122
8123#[gpui::test]
8124async fn test_autoindent_selections(cx: &mut TestAppContext) {
8125 init_test(cx, |_| {});
8126
8127 {
8128 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8129 cx.set_state(indoc! {"
8130 impl A {
8131
8132 fn b() {}
8133
8134 «fn c() {
8135
8136 }ˇ»
8137 }
8138 "});
8139
8140 cx.update_editor(|editor, window, cx| {
8141 editor.autoindent(&Default::default(), window, cx);
8142 });
8143
8144 cx.assert_editor_state(indoc! {"
8145 impl A {
8146
8147 fn b() {}
8148
8149 «fn c() {
8150
8151 }ˇ»
8152 }
8153 "});
8154 }
8155
8156 {
8157 let mut cx = EditorTestContext::new_multibuffer(
8158 cx,
8159 [indoc! { "
8160 impl A {
8161 «
8162 // a
8163 fn b(){}
8164 »
8165 «
8166 }
8167 fn c(){}
8168 »
8169 "}],
8170 );
8171
8172 let buffer = cx.update_editor(|editor, _, cx| {
8173 let buffer = editor.buffer().update(cx, |buffer, _| {
8174 buffer.all_buffers().iter().next().unwrap().clone()
8175 });
8176 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
8177 buffer
8178 });
8179
8180 cx.run_until_parked();
8181 cx.update_editor(|editor, window, cx| {
8182 editor.select_all(&Default::default(), window, cx);
8183 editor.autoindent(&Default::default(), window, cx)
8184 });
8185 cx.run_until_parked();
8186
8187 cx.update(|_, cx| {
8188 assert_eq!(
8189 buffer.read(cx).text(),
8190 indoc! { "
8191 impl A {
8192
8193 // a
8194 fn b(){}
8195
8196
8197 }
8198 fn c(){}
8199
8200 " }
8201 )
8202 });
8203 }
8204}
8205
8206#[gpui::test]
8207async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
8208 init_test(cx, |_| {});
8209
8210 let mut cx = EditorTestContext::new(cx).await;
8211
8212 let language = Arc::new(Language::new(
8213 LanguageConfig {
8214 brackets: BracketPairConfig {
8215 pairs: vec![
8216 BracketPair {
8217 start: "{".to_string(),
8218 end: "}".to_string(),
8219 close: true,
8220 surround: true,
8221 newline: true,
8222 },
8223 BracketPair {
8224 start: "(".to_string(),
8225 end: ")".to_string(),
8226 close: true,
8227 surround: true,
8228 newline: true,
8229 },
8230 BracketPair {
8231 start: "/*".to_string(),
8232 end: " */".to_string(),
8233 close: true,
8234 surround: true,
8235 newline: true,
8236 },
8237 BracketPair {
8238 start: "[".to_string(),
8239 end: "]".to_string(),
8240 close: false,
8241 surround: false,
8242 newline: true,
8243 },
8244 BracketPair {
8245 start: "\"".to_string(),
8246 end: "\"".to_string(),
8247 close: true,
8248 surround: true,
8249 newline: false,
8250 },
8251 BracketPair {
8252 start: "<".to_string(),
8253 end: ">".to_string(),
8254 close: false,
8255 surround: true,
8256 newline: true,
8257 },
8258 ],
8259 ..Default::default()
8260 },
8261 autoclose_before: "})]".to_string(),
8262 ..Default::default()
8263 },
8264 Some(tree_sitter_rust::LANGUAGE.into()),
8265 ));
8266
8267 cx.language_registry().add(language.clone());
8268 cx.update_buffer(|buffer, cx| {
8269 buffer.set_language(Some(language), cx);
8270 });
8271
8272 cx.set_state(
8273 &r#"
8274 🏀ˇ
8275 εˇ
8276 ❤️ˇ
8277 "#
8278 .unindent(),
8279 );
8280
8281 // autoclose multiple nested brackets at multiple cursors
8282 cx.update_editor(|editor, window, cx| {
8283 editor.handle_input("{", window, cx);
8284 editor.handle_input("{", window, cx);
8285 editor.handle_input("{", window, cx);
8286 });
8287 cx.assert_editor_state(
8288 &"
8289 🏀{{{ˇ}}}
8290 ε{{{ˇ}}}
8291 ❤️{{{ˇ}}}
8292 "
8293 .unindent(),
8294 );
8295
8296 // insert a different closing bracket
8297 cx.update_editor(|editor, window, cx| {
8298 editor.handle_input(")", window, cx);
8299 });
8300 cx.assert_editor_state(
8301 &"
8302 🏀{{{)ˇ}}}
8303 ε{{{)ˇ}}}
8304 ❤️{{{)ˇ}}}
8305 "
8306 .unindent(),
8307 );
8308
8309 // skip over the auto-closed brackets when typing a closing bracket
8310 cx.update_editor(|editor, window, cx| {
8311 editor.move_right(&MoveRight, window, cx);
8312 editor.handle_input("}", window, cx);
8313 editor.handle_input("}", window, cx);
8314 editor.handle_input("}", window, cx);
8315 });
8316 cx.assert_editor_state(
8317 &"
8318 🏀{{{)}}}}ˇ
8319 ε{{{)}}}}ˇ
8320 ❤️{{{)}}}}ˇ
8321 "
8322 .unindent(),
8323 );
8324
8325 // autoclose multi-character pairs
8326 cx.set_state(
8327 &"
8328 ˇ
8329 ˇ
8330 "
8331 .unindent(),
8332 );
8333 cx.update_editor(|editor, window, cx| {
8334 editor.handle_input("/", window, cx);
8335 editor.handle_input("*", window, cx);
8336 });
8337 cx.assert_editor_state(
8338 &"
8339 /*ˇ */
8340 /*ˇ */
8341 "
8342 .unindent(),
8343 );
8344
8345 // one cursor autocloses a multi-character pair, one cursor
8346 // does not autoclose.
8347 cx.set_state(
8348 &"
8349 /ˇ
8350 ˇ
8351 "
8352 .unindent(),
8353 );
8354 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
8355 cx.assert_editor_state(
8356 &"
8357 /*ˇ */
8358 *ˇ
8359 "
8360 .unindent(),
8361 );
8362
8363 // Don't autoclose if the next character isn't whitespace and isn't
8364 // listed in the language's "autoclose_before" section.
8365 cx.set_state("ˇa b");
8366 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8367 cx.assert_editor_state("{ˇa b");
8368
8369 // Don't autoclose if `close` is false for the bracket pair
8370 cx.set_state("ˇ");
8371 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
8372 cx.assert_editor_state("[ˇ");
8373
8374 // Surround with brackets if text is selected
8375 cx.set_state("«aˇ» b");
8376 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8377 cx.assert_editor_state("{«aˇ»} b");
8378
8379 // Autoclose when not immediately after a word character
8380 cx.set_state("a ˇ");
8381 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8382 cx.assert_editor_state("a \"ˇ\"");
8383
8384 // Autoclose pair where the start and end characters are the same
8385 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8386 cx.assert_editor_state("a \"\"ˇ");
8387
8388 // Don't autoclose when immediately after a word character
8389 cx.set_state("aˇ");
8390 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8391 cx.assert_editor_state("a\"ˇ");
8392
8393 // Do autoclose when after a non-word character
8394 cx.set_state("{ˇ");
8395 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8396 cx.assert_editor_state("{\"ˇ\"");
8397
8398 // Non identical pairs autoclose regardless of preceding character
8399 cx.set_state("aˇ");
8400 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8401 cx.assert_editor_state("a{ˇ}");
8402
8403 // Don't autoclose pair if autoclose is disabled
8404 cx.set_state("ˇ");
8405 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8406 cx.assert_editor_state("<ˇ");
8407
8408 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
8409 cx.set_state("«aˇ» b");
8410 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8411 cx.assert_editor_state("<«aˇ»> b");
8412}
8413
8414#[gpui::test]
8415async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
8416 init_test(cx, |settings| {
8417 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8418 });
8419
8420 let mut cx = EditorTestContext::new(cx).await;
8421
8422 let language = Arc::new(Language::new(
8423 LanguageConfig {
8424 brackets: BracketPairConfig {
8425 pairs: vec![
8426 BracketPair {
8427 start: "{".to_string(),
8428 end: "}".to_string(),
8429 close: true,
8430 surround: true,
8431 newline: true,
8432 },
8433 BracketPair {
8434 start: "(".to_string(),
8435 end: ")".to_string(),
8436 close: true,
8437 surround: true,
8438 newline: true,
8439 },
8440 BracketPair {
8441 start: "[".to_string(),
8442 end: "]".to_string(),
8443 close: false,
8444 surround: false,
8445 newline: true,
8446 },
8447 ],
8448 ..Default::default()
8449 },
8450 autoclose_before: "})]".to_string(),
8451 ..Default::default()
8452 },
8453 Some(tree_sitter_rust::LANGUAGE.into()),
8454 ));
8455
8456 cx.language_registry().add(language.clone());
8457 cx.update_buffer(|buffer, cx| {
8458 buffer.set_language(Some(language), cx);
8459 });
8460
8461 cx.set_state(
8462 &"
8463 ˇ
8464 ˇ
8465 ˇ
8466 "
8467 .unindent(),
8468 );
8469
8470 // ensure only matching closing brackets are skipped over
8471 cx.update_editor(|editor, window, cx| {
8472 editor.handle_input("}", window, cx);
8473 editor.move_left(&MoveLeft, window, cx);
8474 editor.handle_input(")", window, cx);
8475 editor.move_left(&MoveLeft, window, cx);
8476 });
8477 cx.assert_editor_state(
8478 &"
8479 ˇ)}
8480 ˇ)}
8481 ˇ)}
8482 "
8483 .unindent(),
8484 );
8485
8486 // skip-over closing brackets at multiple cursors
8487 cx.update_editor(|editor, window, cx| {
8488 editor.handle_input(")", window, cx);
8489 editor.handle_input("}", window, cx);
8490 });
8491 cx.assert_editor_state(
8492 &"
8493 )}ˇ
8494 )}ˇ
8495 )}ˇ
8496 "
8497 .unindent(),
8498 );
8499
8500 // ignore non-close brackets
8501 cx.update_editor(|editor, window, cx| {
8502 editor.handle_input("]", window, cx);
8503 editor.move_left(&MoveLeft, window, cx);
8504 editor.handle_input("]", window, cx);
8505 });
8506 cx.assert_editor_state(
8507 &"
8508 )}]ˇ]
8509 )}]ˇ]
8510 )}]ˇ]
8511 "
8512 .unindent(),
8513 );
8514}
8515
8516#[gpui::test]
8517async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8518 init_test(cx, |_| {});
8519
8520 let mut cx = EditorTestContext::new(cx).await;
8521
8522 let html_language = Arc::new(
8523 Language::new(
8524 LanguageConfig {
8525 name: "HTML".into(),
8526 brackets: BracketPairConfig {
8527 pairs: vec![
8528 BracketPair {
8529 start: "<".into(),
8530 end: ">".into(),
8531 close: true,
8532 ..Default::default()
8533 },
8534 BracketPair {
8535 start: "{".into(),
8536 end: "}".into(),
8537 close: true,
8538 ..Default::default()
8539 },
8540 BracketPair {
8541 start: "(".into(),
8542 end: ")".into(),
8543 close: true,
8544 ..Default::default()
8545 },
8546 ],
8547 ..Default::default()
8548 },
8549 autoclose_before: "})]>".into(),
8550 ..Default::default()
8551 },
8552 Some(tree_sitter_html::LANGUAGE.into()),
8553 )
8554 .with_injection_query(
8555 r#"
8556 (script_element
8557 (raw_text) @injection.content
8558 (#set! injection.language "javascript"))
8559 "#,
8560 )
8561 .unwrap(),
8562 );
8563
8564 let javascript_language = Arc::new(Language::new(
8565 LanguageConfig {
8566 name: "JavaScript".into(),
8567 brackets: BracketPairConfig {
8568 pairs: vec![
8569 BracketPair {
8570 start: "/*".into(),
8571 end: " */".into(),
8572 close: true,
8573 ..Default::default()
8574 },
8575 BracketPair {
8576 start: "{".into(),
8577 end: "}".into(),
8578 close: true,
8579 ..Default::default()
8580 },
8581 BracketPair {
8582 start: "(".into(),
8583 end: ")".into(),
8584 close: true,
8585 ..Default::default()
8586 },
8587 ],
8588 ..Default::default()
8589 },
8590 autoclose_before: "})]>".into(),
8591 ..Default::default()
8592 },
8593 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8594 ));
8595
8596 cx.language_registry().add(html_language.clone());
8597 cx.language_registry().add(javascript_language.clone());
8598
8599 cx.update_buffer(|buffer, cx| {
8600 buffer.set_language(Some(html_language), cx);
8601 });
8602
8603 cx.set_state(
8604 &r#"
8605 <body>ˇ
8606 <script>
8607 var x = 1;ˇ
8608 </script>
8609 </body>ˇ
8610 "#
8611 .unindent(),
8612 );
8613
8614 // Precondition: different languages are active at different locations.
8615 cx.update_editor(|editor, window, cx| {
8616 let snapshot = editor.snapshot(window, cx);
8617 let cursors = editor.selections.ranges::<usize>(cx);
8618 let languages = cursors
8619 .iter()
8620 .map(|c| snapshot.language_at(c.start).unwrap().name())
8621 .collect::<Vec<_>>();
8622 assert_eq!(
8623 languages,
8624 &["HTML".into(), "JavaScript".into(), "HTML".into()]
8625 );
8626 });
8627
8628 // Angle brackets autoclose in HTML, but not JavaScript.
8629 cx.update_editor(|editor, window, cx| {
8630 editor.handle_input("<", window, cx);
8631 editor.handle_input("a", window, cx);
8632 });
8633 cx.assert_editor_state(
8634 &r#"
8635 <body><aˇ>
8636 <script>
8637 var x = 1;<aˇ
8638 </script>
8639 </body><aˇ>
8640 "#
8641 .unindent(),
8642 );
8643
8644 // Curly braces and parens autoclose in both HTML and JavaScript.
8645 cx.update_editor(|editor, window, cx| {
8646 editor.handle_input(" b=", window, cx);
8647 editor.handle_input("{", window, cx);
8648 editor.handle_input("c", window, cx);
8649 editor.handle_input("(", window, cx);
8650 });
8651 cx.assert_editor_state(
8652 &r#"
8653 <body><a b={c(ˇ)}>
8654 <script>
8655 var x = 1;<a b={c(ˇ)}
8656 </script>
8657 </body><a b={c(ˇ)}>
8658 "#
8659 .unindent(),
8660 );
8661
8662 // Brackets that were already autoclosed are skipped.
8663 cx.update_editor(|editor, window, cx| {
8664 editor.handle_input(")", window, cx);
8665 editor.handle_input("d", window, cx);
8666 editor.handle_input("}", window, cx);
8667 });
8668 cx.assert_editor_state(
8669 &r#"
8670 <body><a b={c()d}ˇ>
8671 <script>
8672 var x = 1;<a b={c()d}ˇ
8673 </script>
8674 </body><a b={c()d}ˇ>
8675 "#
8676 .unindent(),
8677 );
8678 cx.update_editor(|editor, window, cx| {
8679 editor.handle_input(">", window, cx);
8680 });
8681 cx.assert_editor_state(
8682 &r#"
8683 <body><a b={c()d}>ˇ
8684 <script>
8685 var x = 1;<a b={c()d}>ˇ
8686 </script>
8687 </body><a b={c()d}>ˇ
8688 "#
8689 .unindent(),
8690 );
8691
8692 // Reset
8693 cx.set_state(
8694 &r#"
8695 <body>ˇ
8696 <script>
8697 var x = 1;ˇ
8698 </script>
8699 </body>ˇ
8700 "#
8701 .unindent(),
8702 );
8703
8704 cx.update_editor(|editor, window, cx| {
8705 editor.handle_input("<", window, cx);
8706 });
8707 cx.assert_editor_state(
8708 &r#"
8709 <body><ˇ>
8710 <script>
8711 var x = 1;<ˇ
8712 </script>
8713 </body><ˇ>
8714 "#
8715 .unindent(),
8716 );
8717
8718 // When backspacing, the closing angle brackets are removed.
8719 cx.update_editor(|editor, window, cx| {
8720 editor.backspace(&Backspace, window, cx);
8721 });
8722 cx.assert_editor_state(
8723 &r#"
8724 <body>ˇ
8725 <script>
8726 var x = 1;ˇ
8727 </script>
8728 </body>ˇ
8729 "#
8730 .unindent(),
8731 );
8732
8733 // Block comments autoclose in JavaScript, but not HTML.
8734 cx.update_editor(|editor, window, cx| {
8735 editor.handle_input("/", window, cx);
8736 editor.handle_input("*", window, cx);
8737 });
8738 cx.assert_editor_state(
8739 &r#"
8740 <body>/*ˇ
8741 <script>
8742 var x = 1;/*ˇ */
8743 </script>
8744 </body>/*ˇ
8745 "#
8746 .unindent(),
8747 );
8748}
8749
8750#[gpui::test]
8751async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
8752 init_test(cx, |_| {});
8753
8754 let mut cx = EditorTestContext::new(cx).await;
8755
8756 let rust_language = Arc::new(
8757 Language::new(
8758 LanguageConfig {
8759 name: "Rust".into(),
8760 brackets: serde_json::from_value(json!([
8761 { "start": "{", "end": "}", "close": true, "newline": true },
8762 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
8763 ]))
8764 .unwrap(),
8765 autoclose_before: "})]>".into(),
8766 ..Default::default()
8767 },
8768 Some(tree_sitter_rust::LANGUAGE.into()),
8769 )
8770 .with_override_query("(string_literal) @string")
8771 .unwrap(),
8772 );
8773
8774 cx.language_registry().add(rust_language.clone());
8775 cx.update_buffer(|buffer, cx| {
8776 buffer.set_language(Some(rust_language), cx);
8777 });
8778
8779 cx.set_state(
8780 &r#"
8781 let x = ˇ
8782 "#
8783 .unindent(),
8784 );
8785
8786 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
8787 cx.update_editor(|editor, window, cx| {
8788 editor.handle_input("\"", window, cx);
8789 });
8790 cx.assert_editor_state(
8791 &r#"
8792 let x = "ˇ"
8793 "#
8794 .unindent(),
8795 );
8796
8797 // Inserting another quotation mark. The cursor moves across the existing
8798 // automatically-inserted quotation mark.
8799 cx.update_editor(|editor, window, cx| {
8800 editor.handle_input("\"", window, cx);
8801 });
8802 cx.assert_editor_state(
8803 &r#"
8804 let x = ""ˇ
8805 "#
8806 .unindent(),
8807 );
8808
8809 // Reset
8810 cx.set_state(
8811 &r#"
8812 let x = ˇ
8813 "#
8814 .unindent(),
8815 );
8816
8817 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
8818 cx.update_editor(|editor, window, cx| {
8819 editor.handle_input("\"", window, cx);
8820 editor.handle_input(" ", window, cx);
8821 editor.move_left(&Default::default(), window, cx);
8822 editor.handle_input("\\", window, cx);
8823 editor.handle_input("\"", window, cx);
8824 });
8825 cx.assert_editor_state(
8826 &r#"
8827 let x = "\"ˇ "
8828 "#
8829 .unindent(),
8830 );
8831
8832 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8833 // mark. Nothing is inserted.
8834 cx.update_editor(|editor, window, cx| {
8835 editor.move_right(&Default::default(), window, cx);
8836 editor.handle_input("\"", window, cx);
8837 });
8838 cx.assert_editor_state(
8839 &r#"
8840 let x = "\" "ˇ
8841 "#
8842 .unindent(),
8843 );
8844}
8845
8846#[gpui::test]
8847async fn test_surround_with_pair(cx: &mut TestAppContext) {
8848 init_test(cx, |_| {});
8849
8850 let language = Arc::new(Language::new(
8851 LanguageConfig {
8852 brackets: BracketPairConfig {
8853 pairs: vec![
8854 BracketPair {
8855 start: "{".to_string(),
8856 end: "}".to_string(),
8857 close: true,
8858 surround: true,
8859 newline: true,
8860 },
8861 BracketPair {
8862 start: "/* ".to_string(),
8863 end: "*/".to_string(),
8864 close: true,
8865 surround: true,
8866 ..Default::default()
8867 },
8868 ],
8869 ..Default::default()
8870 },
8871 ..Default::default()
8872 },
8873 Some(tree_sitter_rust::LANGUAGE.into()),
8874 ));
8875
8876 let text = r#"
8877 a
8878 b
8879 c
8880 "#
8881 .unindent();
8882
8883 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8884 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8885 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8886 editor
8887 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8888 .await;
8889
8890 editor.update_in(cx, |editor, window, cx| {
8891 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8892 s.select_display_ranges([
8893 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8894 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8895 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8896 ])
8897 });
8898
8899 editor.handle_input("{", window, cx);
8900 editor.handle_input("{", window, cx);
8901 editor.handle_input("{", window, cx);
8902 assert_eq!(
8903 editor.text(cx),
8904 "
8905 {{{a}}}
8906 {{{b}}}
8907 {{{c}}}
8908 "
8909 .unindent()
8910 );
8911 assert_eq!(
8912 editor.selections.display_ranges(cx),
8913 [
8914 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
8915 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
8916 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
8917 ]
8918 );
8919
8920 editor.undo(&Undo, window, cx);
8921 editor.undo(&Undo, window, cx);
8922 editor.undo(&Undo, window, cx);
8923 assert_eq!(
8924 editor.text(cx),
8925 "
8926 a
8927 b
8928 c
8929 "
8930 .unindent()
8931 );
8932 assert_eq!(
8933 editor.selections.display_ranges(cx),
8934 [
8935 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8936 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8937 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8938 ]
8939 );
8940
8941 // Ensure inserting the first character of a multi-byte bracket pair
8942 // doesn't surround the selections with the bracket.
8943 editor.handle_input("/", window, cx);
8944 assert_eq!(
8945 editor.text(cx),
8946 "
8947 /
8948 /
8949 /
8950 "
8951 .unindent()
8952 );
8953 assert_eq!(
8954 editor.selections.display_ranges(cx),
8955 [
8956 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8957 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8958 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8959 ]
8960 );
8961
8962 editor.undo(&Undo, window, cx);
8963 assert_eq!(
8964 editor.text(cx),
8965 "
8966 a
8967 b
8968 c
8969 "
8970 .unindent()
8971 );
8972 assert_eq!(
8973 editor.selections.display_ranges(cx),
8974 [
8975 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8976 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8977 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8978 ]
8979 );
8980
8981 // Ensure inserting the last character of a multi-byte bracket pair
8982 // doesn't surround the selections with the bracket.
8983 editor.handle_input("*", window, cx);
8984 assert_eq!(
8985 editor.text(cx),
8986 "
8987 *
8988 *
8989 *
8990 "
8991 .unindent()
8992 );
8993 assert_eq!(
8994 editor.selections.display_ranges(cx),
8995 [
8996 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8997 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8998 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8999 ]
9000 );
9001 });
9002}
9003
9004#[gpui::test]
9005async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
9006 init_test(cx, |_| {});
9007
9008 let language = Arc::new(Language::new(
9009 LanguageConfig {
9010 brackets: BracketPairConfig {
9011 pairs: vec![BracketPair {
9012 start: "{".to_string(),
9013 end: "}".to_string(),
9014 close: true,
9015 surround: true,
9016 newline: true,
9017 }],
9018 ..Default::default()
9019 },
9020 autoclose_before: "}".to_string(),
9021 ..Default::default()
9022 },
9023 Some(tree_sitter_rust::LANGUAGE.into()),
9024 ));
9025
9026 let text = r#"
9027 a
9028 b
9029 c
9030 "#
9031 .unindent();
9032
9033 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9034 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9035 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9036 editor
9037 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9038 .await;
9039
9040 editor.update_in(cx, |editor, window, cx| {
9041 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9042 s.select_ranges([
9043 Point::new(0, 1)..Point::new(0, 1),
9044 Point::new(1, 1)..Point::new(1, 1),
9045 Point::new(2, 1)..Point::new(2, 1),
9046 ])
9047 });
9048
9049 editor.handle_input("{", window, cx);
9050 editor.handle_input("{", window, cx);
9051 editor.handle_input("_", window, cx);
9052 assert_eq!(
9053 editor.text(cx),
9054 "
9055 a{{_}}
9056 b{{_}}
9057 c{{_}}
9058 "
9059 .unindent()
9060 );
9061 assert_eq!(
9062 editor.selections.ranges::<Point>(cx),
9063 [
9064 Point::new(0, 4)..Point::new(0, 4),
9065 Point::new(1, 4)..Point::new(1, 4),
9066 Point::new(2, 4)..Point::new(2, 4)
9067 ]
9068 );
9069
9070 editor.backspace(&Default::default(), window, cx);
9071 editor.backspace(&Default::default(), window, cx);
9072 assert_eq!(
9073 editor.text(cx),
9074 "
9075 a{}
9076 b{}
9077 c{}
9078 "
9079 .unindent()
9080 );
9081 assert_eq!(
9082 editor.selections.ranges::<Point>(cx),
9083 [
9084 Point::new(0, 2)..Point::new(0, 2),
9085 Point::new(1, 2)..Point::new(1, 2),
9086 Point::new(2, 2)..Point::new(2, 2)
9087 ]
9088 );
9089
9090 editor.delete_to_previous_word_start(&Default::default(), window, cx);
9091 assert_eq!(
9092 editor.text(cx),
9093 "
9094 a
9095 b
9096 c
9097 "
9098 .unindent()
9099 );
9100 assert_eq!(
9101 editor.selections.ranges::<Point>(cx),
9102 [
9103 Point::new(0, 1)..Point::new(0, 1),
9104 Point::new(1, 1)..Point::new(1, 1),
9105 Point::new(2, 1)..Point::new(2, 1)
9106 ]
9107 );
9108 });
9109}
9110
9111#[gpui::test]
9112async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
9113 init_test(cx, |settings| {
9114 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9115 });
9116
9117 let mut cx = EditorTestContext::new(cx).await;
9118
9119 let language = Arc::new(Language::new(
9120 LanguageConfig {
9121 brackets: BracketPairConfig {
9122 pairs: vec![
9123 BracketPair {
9124 start: "{".to_string(),
9125 end: "}".to_string(),
9126 close: true,
9127 surround: true,
9128 newline: true,
9129 },
9130 BracketPair {
9131 start: "(".to_string(),
9132 end: ")".to_string(),
9133 close: true,
9134 surround: true,
9135 newline: true,
9136 },
9137 BracketPair {
9138 start: "[".to_string(),
9139 end: "]".to_string(),
9140 close: false,
9141 surround: true,
9142 newline: true,
9143 },
9144 ],
9145 ..Default::default()
9146 },
9147 autoclose_before: "})]".to_string(),
9148 ..Default::default()
9149 },
9150 Some(tree_sitter_rust::LANGUAGE.into()),
9151 ));
9152
9153 cx.language_registry().add(language.clone());
9154 cx.update_buffer(|buffer, cx| {
9155 buffer.set_language(Some(language), cx);
9156 });
9157
9158 cx.set_state(
9159 &"
9160 {(ˇ)}
9161 [[ˇ]]
9162 {(ˇ)}
9163 "
9164 .unindent(),
9165 );
9166
9167 cx.update_editor(|editor, window, cx| {
9168 editor.backspace(&Default::default(), window, cx);
9169 editor.backspace(&Default::default(), window, cx);
9170 });
9171
9172 cx.assert_editor_state(
9173 &"
9174 ˇ
9175 ˇ]]
9176 ˇ
9177 "
9178 .unindent(),
9179 );
9180
9181 cx.update_editor(|editor, window, cx| {
9182 editor.handle_input("{", window, cx);
9183 editor.handle_input("{", window, cx);
9184 editor.move_right(&MoveRight, window, cx);
9185 editor.move_right(&MoveRight, window, cx);
9186 editor.move_left(&MoveLeft, window, cx);
9187 editor.move_left(&MoveLeft, window, cx);
9188 editor.backspace(&Default::default(), window, cx);
9189 });
9190
9191 cx.assert_editor_state(
9192 &"
9193 {ˇ}
9194 {ˇ}]]
9195 {ˇ}
9196 "
9197 .unindent(),
9198 );
9199
9200 cx.update_editor(|editor, window, cx| {
9201 editor.backspace(&Default::default(), window, cx);
9202 });
9203
9204 cx.assert_editor_state(
9205 &"
9206 ˇ
9207 ˇ]]
9208 ˇ
9209 "
9210 .unindent(),
9211 );
9212}
9213
9214#[gpui::test]
9215async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
9216 init_test(cx, |_| {});
9217
9218 let language = Arc::new(Language::new(
9219 LanguageConfig::default(),
9220 Some(tree_sitter_rust::LANGUAGE.into()),
9221 ));
9222
9223 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
9224 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9225 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9226 editor
9227 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9228 .await;
9229
9230 editor.update_in(cx, |editor, window, cx| {
9231 editor.set_auto_replace_emoji_shortcode(true);
9232
9233 editor.handle_input("Hello ", window, cx);
9234 editor.handle_input(":wave", window, cx);
9235 assert_eq!(editor.text(cx), "Hello :wave".unindent());
9236
9237 editor.handle_input(":", window, cx);
9238 assert_eq!(editor.text(cx), "Hello 👋".unindent());
9239
9240 editor.handle_input(" :smile", window, cx);
9241 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
9242
9243 editor.handle_input(":", window, cx);
9244 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
9245
9246 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
9247 editor.handle_input(":wave", window, cx);
9248 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
9249
9250 editor.handle_input(":", window, cx);
9251 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
9252
9253 editor.handle_input(":1", window, cx);
9254 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
9255
9256 editor.handle_input(":", window, cx);
9257 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
9258
9259 // Ensure shortcode does not get replaced when it is part of a word
9260 editor.handle_input(" Test:wave", window, cx);
9261 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
9262
9263 editor.handle_input(":", window, cx);
9264 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
9265
9266 editor.set_auto_replace_emoji_shortcode(false);
9267
9268 // Ensure shortcode does not get replaced when auto replace is off
9269 editor.handle_input(" :wave", window, cx);
9270 assert_eq!(
9271 editor.text(cx),
9272 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
9273 );
9274
9275 editor.handle_input(":", window, cx);
9276 assert_eq!(
9277 editor.text(cx),
9278 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
9279 );
9280 });
9281}
9282
9283#[gpui::test]
9284async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
9285 init_test(cx, |_| {});
9286
9287 let (text, insertion_ranges) = marked_text_ranges(
9288 indoc! {"
9289 ˇ
9290 "},
9291 false,
9292 );
9293
9294 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
9295 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9296
9297 _ = editor.update_in(cx, |editor, window, cx| {
9298 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
9299
9300 editor
9301 .insert_snippet(&insertion_ranges, snippet, window, cx)
9302 .unwrap();
9303
9304 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
9305 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
9306 assert_eq!(editor.text(cx), expected_text);
9307 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
9308 }
9309
9310 assert(
9311 editor,
9312 cx,
9313 indoc! {"
9314 type «» =•
9315 "},
9316 );
9317
9318 assert!(editor.context_menu_visible(), "There should be a matches");
9319 });
9320}
9321
9322#[gpui::test]
9323async fn test_snippets(cx: &mut TestAppContext) {
9324 init_test(cx, |_| {});
9325
9326 let mut cx = EditorTestContext::new(cx).await;
9327
9328 cx.set_state(indoc! {"
9329 a.ˇ b
9330 a.ˇ b
9331 a.ˇ b
9332 "});
9333
9334 cx.update_editor(|editor, window, cx| {
9335 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
9336 let insertion_ranges = editor
9337 .selections
9338 .all(cx)
9339 .iter()
9340 .map(|s| s.range().clone())
9341 .collect::<Vec<_>>();
9342 editor
9343 .insert_snippet(&insertion_ranges, snippet, window, cx)
9344 .unwrap();
9345 });
9346
9347 cx.assert_editor_state(indoc! {"
9348 a.f(«oneˇ», two, «threeˇ») b
9349 a.f(«oneˇ», two, «threeˇ») b
9350 a.f(«oneˇ», two, «threeˇ») b
9351 "});
9352
9353 // Can't move earlier than the first tab stop
9354 cx.update_editor(|editor, window, cx| {
9355 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9356 });
9357 cx.assert_editor_state(indoc! {"
9358 a.f(«oneˇ», two, «threeˇ») b
9359 a.f(«oneˇ», two, «threeˇ») b
9360 a.f(«oneˇ», two, «threeˇ») b
9361 "});
9362
9363 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9364 cx.assert_editor_state(indoc! {"
9365 a.f(one, «twoˇ», three) b
9366 a.f(one, «twoˇ», three) b
9367 a.f(one, «twoˇ», three) b
9368 "});
9369
9370 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
9371 cx.assert_editor_state(indoc! {"
9372 a.f(«oneˇ», two, «threeˇ») b
9373 a.f(«oneˇ», two, «threeˇ») b
9374 a.f(«oneˇ», two, «threeˇ») b
9375 "});
9376
9377 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9378 cx.assert_editor_state(indoc! {"
9379 a.f(one, «twoˇ», three) b
9380 a.f(one, «twoˇ», three) b
9381 a.f(one, «twoˇ», three) b
9382 "});
9383 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9384 cx.assert_editor_state(indoc! {"
9385 a.f(one, two, three)ˇ b
9386 a.f(one, two, three)ˇ b
9387 a.f(one, two, three)ˇ b
9388 "});
9389
9390 // As soon as the last tab stop is reached, snippet state is gone
9391 cx.update_editor(|editor, window, cx| {
9392 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9393 });
9394 cx.assert_editor_state(indoc! {"
9395 a.f(one, two, three)ˇ b
9396 a.f(one, two, three)ˇ b
9397 a.f(one, two, three)ˇ b
9398 "});
9399}
9400
9401#[gpui::test]
9402async fn test_snippet_indentation(cx: &mut TestAppContext) {
9403 init_test(cx, |_| {});
9404
9405 let mut cx = EditorTestContext::new(cx).await;
9406
9407 cx.update_editor(|editor, window, cx| {
9408 let snippet = Snippet::parse(indoc! {"
9409 /*
9410 * Multiline comment with leading indentation
9411 *
9412 * $1
9413 */
9414 $0"})
9415 .unwrap();
9416 let insertion_ranges = editor
9417 .selections
9418 .all(cx)
9419 .iter()
9420 .map(|s| s.range().clone())
9421 .collect::<Vec<_>>();
9422 editor
9423 .insert_snippet(&insertion_ranges, snippet, window, cx)
9424 .unwrap();
9425 });
9426
9427 cx.assert_editor_state(indoc! {"
9428 /*
9429 * Multiline comment with leading indentation
9430 *
9431 * ˇ
9432 */
9433 "});
9434
9435 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9436 cx.assert_editor_state(indoc! {"
9437 /*
9438 * Multiline comment with leading indentation
9439 *
9440 *•
9441 */
9442 ˇ"});
9443}
9444
9445#[gpui::test]
9446async fn test_document_format_during_save(cx: &mut TestAppContext) {
9447 init_test(cx, |_| {});
9448
9449 let fs = FakeFs::new(cx.executor());
9450 fs.insert_file(path!("/file.rs"), Default::default()).await;
9451
9452 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
9453
9454 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9455 language_registry.add(rust_lang());
9456 let mut fake_servers = language_registry.register_fake_lsp(
9457 "Rust",
9458 FakeLspAdapter {
9459 capabilities: lsp::ServerCapabilities {
9460 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9461 ..Default::default()
9462 },
9463 ..Default::default()
9464 },
9465 );
9466
9467 let buffer = project
9468 .update(cx, |project, cx| {
9469 project.open_local_buffer(path!("/file.rs"), cx)
9470 })
9471 .await
9472 .unwrap();
9473
9474 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9475 let (editor, cx) = cx.add_window_view(|window, cx| {
9476 build_editor_with_project(project.clone(), buffer, window, cx)
9477 });
9478 editor.update_in(cx, |editor, window, cx| {
9479 editor.set_text("one\ntwo\nthree\n", window, cx)
9480 });
9481 assert!(cx.read(|cx| editor.is_dirty(cx)));
9482
9483 cx.executor().start_waiting();
9484 let fake_server = fake_servers.next().await.unwrap();
9485
9486 {
9487 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9488 move |params, _| async move {
9489 assert_eq!(
9490 params.text_document.uri,
9491 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9492 );
9493 assert_eq!(params.options.tab_size, 4);
9494 Ok(Some(vec![lsp::TextEdit::new(
9495 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9496 ", ".to_string(),
9497 )]))
9498 },
9499 );
9500 let save = editor
9501 .update_in(cx, |editor, window, cx| {
9502 editor.save(
9503 SaveOptions {
9504 format: true,
9505 autosave: false,
9506 },
9507 project.clone(),
9508 window,
9509 cx,
9510 )
9511 })
9512 .unwrap();
9513 cx.executor().start_waiting();
9514 save.await;
9515
9516 assert_eq!(
9517 editor.update(cx, |editor, cx| editor.text(cx)),
9518 "one, two\nthree\n"
9519 );
9520 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9521 }
9522
9523 {
9524 editor.update_in(cx, |editor, window, cx| {
9525 editor.set_text("one\ntwo\nthree\n", window, cx)
9526 });
9527 assert!(cx.read(|cx| editor.is_dirty(cx)));
9528
9529 // Ensure we can still save even if formatting hangs.
9530 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9531 move |params, _| async move {
9532 assert_eq!(
9533 params.text_document.uri,
9534 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9535 );
9536 futures::future::pending::<()>().await;
9537 unreachable!()
9538 },
9539 );
9540 let save = editor
9541 .update_in(cx, |editor, window, cx| {
9542 editor.save(
9543 SaveOptions {
9544 format: true,
9545 autosave: false,
9546 },
9547 project.clone(),
9548 window,
9549 cx,
9550 )
9551 })
9552 .unwrap();
9553 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9554 cx.executor().start_waiting();
9555 save.await;
9556 assert_eq!(
9557 editor.update(cx, |editor, cx| editor.text(cx)),
9558 "one\ntwo\nthree\n"
9559 );
9560 }
9561
9562 // Set rust language override and assert overridden tabsize is sent to language server
9563 update_test_language_settings(cx, |settings| {
9564 settings.languages.0.insert(
9565 "Rust".into(),
9566 LanguageSettingsContent {
9567 tab_size: NonZeroU32::new(8),
9568 ..Default::default()
9569 },
9570 );
9571 });
9572
9573 {
9574 editor.update_in(cx, |editor, window, cx| {
9575 editor.set_text("somehting_new\n", window, cx)
9576 });
9577 assert!(cx.read(|cx| editor.is_dirty(cx)));
9578 let _formatting_request_signal = fake_server
9579 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9580 assert_eq!(
9581 params.text_document.uri,
9582 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9583 );
9584 assert_eq!(params.options.tab_size, 8);
9585 Ok(Some(vec![]))
9586 });
9587 let save = editor
9588 .update_in(cx, |editor, window, cx| {
9589 editor.save(
9590 SaveOptions {
9591 format: true,
9592 autosave: false,
9593 },
9594 project.clone(),
9595 window,
9596 cx,
9597 )
9598 })
9599 .unwrap();
9600 cx.executor().start_waiting();
9601 save.await;
9602 }
9603}
9604
9605#[gpui::test]
9606async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
9607 init_test(cx, |settings| {
9608 settings.defaults.ensure_final_newline_on_save = Some(false);
9609 });
9610
9611 let fs = FakeFs::new(cx.executor());
9612 fs.insert_file(path!("/file.txt"), "foo".into()).await;
9613
9614 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
9615
9616 let buffer = project
9617 .update(cx, |project, cx| {
9618 project.open_local_buffer(path!("/file.txt"), cx)
9619 })
9620 .await
9621 .unwrap();
9622
9623 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9624 let (editor, cx) = cx.add_window_view(|window, cx| {
9625 build_editor_with_project(project.clone(), buffer, window, cx)
9626 });
9627 editor.update_in(cx, |editor, window, cx| {
9628 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9629 s.select_ranges([0..0])
9630 });
9631 });
9632 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9633
9634 editor.update_in(cx, |editor, window, cx| {
9635 editor.handle_input("\n", window, cx)
9636 });
9637 cx.run_until_parked();
9638 save(&editor, &project, cx).await;
9639 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9640
9641 editor.update_in(cx, |editor, window, cx| {
9642 editor.undo(&Default::default(), window, cx);
9643 });
9644 save(&editor, &project, cx).await;
9645 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9646
9647 editor.update_in(cx, |editor, window, cx| {
9648 editor.redo(&Default::default(), window, cx);
9649 });
9650 cx.run_until_parked();
9651 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9652
9653 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
9654 let save = editor
9655 .update_in(cx, |editor, window, cx| {
9656 editor.save(
9657 SaveOptions {
9658 format: true,
9659 autosave: false,
9660 },
9661 project.clone(),
9662 window,
9663 cx,
9664 )
9665 })
9666 .unwrap();
9667 cx.executor().start_waiting();
9668 save.await;
9669 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9670 }
9671}
9672
9673#[gpui::test]
9674async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9675 init_test(cx, |_| {});
9676
9677 let cols = 4;
9678 let rows = 10;
9679 let sample_text_1 = sample_text(rows, cols, 'a');
9680 assert_eq!(
9681 sample_text_1,
9682 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9683 );
9684 let sample_text_2 = sample_text(rows, cols, 'l');
9685 assert_eq!(
9686 sample_text_2,
9687 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9688 );
9689 let sample_text_3 = sample_text(rows, cols, 'v');
9690 assert_eq!(
9691 sample_text_3,
9692 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9693 );
9694
9695 let fs = FakeFs::new(cx.executor());
9696 fs.insert_tree(
9697 path!("/a"),
9698 json!({
9699 "main.rs": sample_text_1,
9700 "other.rs": sample_text_2,
9701 "lib.rs": sample_text_3,
9702 }),
9703 )
9704 .await;
9705
9706 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9707 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9708 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9709
9710 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9711 language_registry.add(rust_lang());
9712 let mut fake_servers = language_registry.register_fake_lsp(
9713 "Rust",
9714 FakeLspAdapter {
9715 capabilities: lsp::ServerCapabilities {
9716 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9717 ..Default::default()
9718 },
9719 ..Default::default()
9720 },
9721 );
9722
9723 let worktree = project.update(cx, |project, cx| {
9724 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
9725 assert_eq!(worktrees.len(), 1);
9726 worktrees.pop().unwrap()
9727 });
9728 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9729
9730 let buffer_1 = project
9731 .update(cx, |project, cx| {
9732 project.open_buffer((worktree_id, "main.rs"), cx)
9733 })
9734 .await
9735 .unwrap();
9736 let buffer_2 = project
9737 .update(cx, |project, cx| {
9738 project.open_buffer((worktree_id, "other.rs"), cx)
9739 })
9740 .await
9741 .unwrap();
9742 let buffer_3 = project
9743 .update(cx, |project, cx| {
9744 project.open_buffer((worktree_id, "lib.rs"), cx)
9745 })
9746 .await
9747 .unwrap();
9748
9749 let multi_buffer = cx.new(|cx| {
9750 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9751 multi_buffer.push_excerpts(
9752 buffer_1.clone(),
9753 [
9754 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9755 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9756 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9757 ],
9758 cx,
9759 );
9760 multi_buffer.push_excerpts(
9761 buffer_2.clone(),
9762 [
9763 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9764 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9765 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9766 ],
9767 cx,
9768 );
9769 multi_buffer.push_excerpts(
9770 buffer_3.clone(),
9771 [
9772 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9773 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9774 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9775 ],
9776 cx,
9777 );
9778 multi_buffer
9779 });
9780 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
9781 Editor::new(
9782 EditorMode::full(),
9783 multi_buffer,
9784 Some(project.clone()),
9785 window,
9786 cx,
9787 )
9788 });
9789
9790 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9791 editor.change_selections(
9792 SelectionEffects::scroll(Autoscroll::Next),
9793 window,
9794 cx,
9795 |s| s.select_ranges(Some(1..2)),
9796 );
9797 editor.insert("|one|two|three|", window, cx);
9798 });
9799 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9800 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9801 editor.change_selections(
9802 SelectionEffects::scroll(Autoscroll::Next),
9803 window,
9804 cx,
9805 |s| s.select_ranges(Some(60..70)),
9806 );
9807 editor.insert("|four|five|six|", window, cx);
9808 });
9809 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9810
9811 // First two buffers should be edited, but not the third one.
9812 assert_eq!(
9813 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9814 "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}",
9815 );
9816 buffer_1.update(cx, |buffer, _| {
9817 assert!(buffer.is_dirty());
9818 assert_eq!(
9819 buffer.text(),
9820 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
9821 )
9822 });
9823 buffer_2.update(cx, |buffer, _| {
9824 assert!(buffer.is_dirty());
9825 assert_eq!(
9826 buffer.text(),
9827 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
9828 )
9829 });
9830 buffer_3.update(cx, |buffer, _| {
9831 assert!(!buffer.is_dirty());
9832 assert_eq!(buffer.text(), sample_text_3,)
9833 });
9834 cx.executor().run_until_parked();
9835
9836 cx.executor().start_waiting();
9837 let save = multi_buffer_editor
9838 .update_in(cx, |editor, window, cx| {
9839 editor.save(
9840 SaveOptions {
9841 format: true,
9842 autosave: false,
9843 },
9844 project.clone(),
9845 window,
9846 cx,
9847 )
9848 })
9849 .unwrap();
9850
9851 let fake_server = fake_servers.next().await.unwrap();
9852 fake_server
9853 .server
9854 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
9855 Ok(Some(vec![lsp::TextEdit::new(
9856 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9857 format!("[{} formatted]", params.text_document.uri),
9858 )]))
9859 })
9860 .detach();
9861 save.await;
9862
9863 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
9864 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
9865 assert_eq!(
9866 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9867 uri!(
9868 "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}"
9869 ),
9870 );
9871 buffer_1.update(cx, |buffer, _| {
9872 assert!(!buffer.is_dirty());
9873 assert_eq!(
9874 buffer.text(),
9875 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
9876 )
9877 });
9878 buffer_2.update(cx, |buffer, _| {
9879 assert!(!buffer.is_dirty());
9880 assert_eq!(
9881 buffer.text(),
9882 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
9883 )
9884 });
9885 buffer_3.update(cx, |buffer, _| {
9886 assert!(!buffer.is_dirty());
9887 assert_eq!(buffer.text(), sample_text_3,)
9888 });
9889}
9890
9891#[gpui::test]
9892async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
9893 init_test(cx, |_| {});
9894
9895 let fs = FakeFs::new(cx.executor());
9896 fs.insert_tree(
9897 path!("/dir"),
9898 json!({
9899 "file1.rs": "fn main() { println!(\"hello\"); }",
9900 "file2.rs": "fn test() { println!(\"test\"); }",
9901 "file3.rs": "fn other() { println!(\"other\"); }\n",
9902 }),
9903 )
9904 .await;
9905
9906 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
9907 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9908 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9909
9910 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9911 language_registry.add(rust_lang());
9912
9913 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
9914 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9915
9916 // Open three buffers
9917 let buffer_1 = project
9918 .update(cx, |project, cx| {
9919 project.open_buffer((worktree_id, "file1.rs"), cx)
9920 })
9921 .await
9922 .unwrap();
9923 let buffer_2 = project
9924 .update(cx, |project, cx| {
9925 project.open_buffer((worktree_id, "file2.rs"), cx)
9926 })
9927 .await
9928 .unwrap();
9929 let buffer_3 = project
9930 .update(cx, |project, cx| {
9931 project.open_buffer((worktree_id, "file3.rs"), cx)
9932 })
9933 .await
9934 .unwrap();
9935
9936 // Create a multi-buffer with all three buffers
9937 let multi_buffer = cx.new(|cx| {
9938 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9939 multi_buffer.push_excerpts(
9940 buffer_1.clone(),
9941 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9942 cx,
9943 );
9944 multi_buffer.push_excerpts(
9945 buffer_2.clone(),
9946 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9947 cx,
9948 );
9949 multi_buffer.push_excerpts(
9950 buffer_3.clone(),
9951 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9952 cx,
9953 );
9954 multi_buffer
9955 });
9956
9957 let editor = cx.new_window_entity(|window, cx| {
9958 Editor::new(
9959 EditorMode::full(),
9960 multi_buffer,
9961 Some(project.clone()),
9962 window,
9963 cx,
9964 )
9965 });
9966
9967 // Edit only the first buffer
9968 editor.update_in(cx, |editor, window, cx| {
9969 editor.change_selections(
9970 SelectionEffects::scroll(Autoscroll::Next),
9971 window,
9972 cx,
9973 |s| s.select_ranges(Some(10..10)),
9974 );
9975 editor.insert("// edited", window, cx);
9976 });
9977
9978 // Verify that only buffer 1 is dirty
9979 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
9980 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9981 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9982
9983 // Get write counts after file creation (files were created with initial content)
9984 // We expect each file to have been written once during creation
9985 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
9986 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
9987 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
9988
9989 // Perform autosave
9990 let save_task = editor.update_in(cx, |editor, window, cx| {
9991 editor.save(
9992 SaveOptions {
9993 format: true,
9994 autosave: true,
9995 },
9996 project.clone(),
9997 window,
9998 cx,
9999 )
10000 });
10001 save_task.await.unwrap();
10002
10003 // Only the dirty buffer should have been saved
10004 assert_eq!(
10005 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
10006 1,
10007 "Buffer 1 was dirty, so it should have been written once during autosave"
10008 );
10009 assert_eq!(
10010 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
10011 0,
10012 "Buffer 2 was clean, so it should not have been written during autosave"
10013 );
10014 assert_eq!(
10015 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
10016 0,
10017 "Buffer 3 was clean, so it should not have been written during autosave"
10018 );
10019
10020 // Verify buffer states after autosave
10021 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10022 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10023 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10024
10025 // Now perform a manual save (format = true)
10026 let save_task = editor.update_in(cx, |editor, window, cx| {
10027 editor.save(
10028 SaveOptions {
10029 format: true,
10030 autosave: false,
10031 },
10032 project.clone(),
10033 window,
10034 cx,
10035 )
10036 });
10037 save_task.await.unwrap();
10038
10039 // During manual save, clean buffers don't get written to disk
10040 // They just get did_save called for language server notifications
10041 assert_eq!(
10042 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
10043 1,
10044 "Buffer 1 should only have been written once total (during autosave, not manual save)"
10045 );
10046 assert_eq!(
10047 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
10048 0,
10049 "Buffer 2 should not have been written at all"
10050 );
10051 assert_eq!(
10052 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
10053 0,
10054 "Buffer 3 should not have been written at all"
10055 );
10056}
10057
10058#[gpui::test]
10059async fn test_range_format_during_save(cx: &mut TestAppContext) {
10060 init_test(cx, |_| {});
10061
10062 let fs = FakeFs::new(cx.executor());
10063 fs.insert_file(path!("/file.rs"), Default::default()).await;
10064
10065 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10066
10067 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10068 language_registry.add(rust_lang());
10069 let mut fake_servers = language_registry.register_fake_lsp(
10070 "Rust",
10071 FakeLspAdapter {
10072 capabilities: lsp::ServerCapabilities {
10073 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
10074 ..Default::default()
10075 },
10076 ..Default::default()
10077 },
10078 );
10079
10080 let buffer = project
10081 .update(cx, |project, cx| {
10082 project.open_local_buffer(path!("/file.rs"), cx)
10083 })
10084 .await
10085 .unwrap();
10086
10087 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10088 let (editor, cx) = cx.add_window_view(|window, cx| {
10089 build_editor_with_project(project.clone(), buffer, window, cx)
10090 });
10091 editor.update_in(cx, |editor, window, cx| {
10092 editor.set_text("one\ntwo\nthree\n", window, cx)
10093 });
10094 assert!(cx.read(|cx| editor.is_dirty(cx)));
10095
10096 cx.executor().start_waiting();
10097 let fake_server = fake_servers.next().await.unwrap();
10098
10099 let save = editor
10100 .update_in(cx, |editor, window, cx| {
10101 editor.save(
10102 SaveOptions {
10103 format: true,
10104 autosave: false,
10105 },
10106 project.clone(),
10107 window,
10108 cx,
10109 )
10110 })
10111 .unwrap();
10112 fake_server
10113 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10114 assert_eq!(
10115 params.text_document.uri,
10116 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10117 );
10118 assert_eq!(params.options.tab_size, 4);
10119 Ok(Some(vec![lsp::TextEdit::new(
10120 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10121 ", ".to_string(),
10122 )]))
10123 })
10124 .next()
10125 .await;
10126 cx.executor().start_waiting();
10127 save.await;
10128 assert_eq!(
10129 editor.update(cx, |editor, cx| editor.text(cx)),
10130 "one, two\nthree\n"
10131 );
10132 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10133
10134 editor.update_in(cx, |editor, window, cx| {
10135 editor.set_text("one\ntwo\nthree\n", window, cx)
10136 });
10137 assert!(cx.read(|cx| editor.is_dirty(cx)));
10138
10139 // Ensure we can still save even if formatting hangs.
10140 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
10141 move |params, _| async move {
10142 assert_eq!(
10143 params.text_document.uri,
10144 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10145 );
10146 futures::future::pending::<()>().await;
10147 unreachable!()
10148 },
10149 );
10150 let save = editor
10151 .update_in(cx, |editor, window, cx| {
10152 editor.save(
10153 SaveOptions {
10154 format: true,
10155 autosave: false,
10156 },
10157 project.clone(),
10158 window,
10159 cx,
10160 )
10161 })
10162 .unwrap();
10163 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10164 cx.executor().start_waiting();
10165 save.await;
10166 assert_eq!(
10167 editor.update(cx, |editor, cx| editor.text(cx)),
10168 "one\ntwo\nthree\n"
10169 );
10170 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10171
10172 // For non-dirty buffer, no formatting request should be sent
10173 let save = editor
10174 .update_in(cx, |editor, window, cx| {
10175 editor.save(
10176 SaveOptions {
10177 format: false,
10178 autosave: false,
10179 },
10180 project.clone(),
10181 window,
10182 cx,
10183 )
10184 })
10185 .unwrap();
10186 let _pending_format_request = fake_server
10187 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
10188 panic!("Should not be invoked");
10189 })
10190 .next();
10191 cx.executor().start_waiting();
10192 save.await;
10193
10194 // Set Rust language override and assert overridden tabsize is sent to language server
10195 update_test_language_settings(cx, |settings| {
10196 settings.languages.0.insert(
10197 "Rust".into(),
10198 LanguageSettingsContent {
10199 tab_size: NonZeroU32::new(8),
10200 ..Default::default()
10201 },
10202 );
10203 });
10204
10205 editor.update_in(cx, |editor, window, cx| {
10206 editor.set_text("somehting_new\n", window, cx)
10207 });
10208 assert!(cx.read(|cx| editor.is_dirty(cx)));
10209 let save = editor
10210 .update_in(cx, |editor, window, cx| {
10211 editor.save(
10212 SaveOptions {
10213 format: true,
10214 autosave: false,
10215 },
10216 project.clone(),
10217 window,
10218 cx,
10219 )
10220 })
10221 .unwrap();
10222 fake_server
10223 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10224 assert_eq!(
10225 params.text_document.uri,
10226 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10227 );
10228 assert_eq!(params.options.tab_size, 8);
10229 Ok(Some(Vec::new()))
10230 })
10231 .next()
10232 .await;
10233 save.await;
10234}
10235
10236#[gpui::test]
10237async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
10238 init_test(cx, |settings| {
10239 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
10240 Formatter::LanguageServer { name: None },
10241 )))
10242 });
10243
10244 let fs = FakeFs::new(cx.executor());
10245 fs.insert_file(path!("/file.rs"), Default::default()).await;
10246
10247 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10248
10249 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10250 language_registry.add(Arc::new(Language::new(
10251 LanguageConfig {
10252 name: "Rust".into(),
10253 matcher: LanguageMatcher {
10254 path_suffixes: vec!["rs".to_string()],
10255 ..Default::default()
10256 },
10257 ..LanguageConfig::default()
10258 },
10259 Some(tree_sitter_rust::LANGUAGE.into()),
10260 )));
10261 update_test_language_settings(cx, |settings| {
10262 // Enable Prettier formatting for the same buffer, and ensure
10263 // LSP is called instead of Prettier.
10264 settings.defaults.prettier = Some(PrettierSettings {
10265 allowed: true,
10266 ..PrettierSettings::default()
10267 });
10268 });
10269 let mut fake_servers = language_registry.register_fake_lsp(
10270 "Rust",
10271 FakeLspAdapter {
10272 capabilities: lsp::ServerCapabilities {
10273 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10274 ..Default::default()
10275 },
10276 ..Default::default()
10277 },
10278 );
10279
10280 let buffer = project
10281 .update(cx, |project, cx| {
10282 project.open_local_buffer(path!("/file.rs"), cx)
10283 })
10284 .await
10285 .unwrap();
10286
10287 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10288 let (editor, cx) = cx.add_window_view(|window, cx| {
10289 build_editor_with_project(project.clone(), buffer, window, cx)
10290 });
10291 editor.update_in(cx, |editor, window, cx| {
10292 editor.set_text("one\ntwo\nthree\n", window, cx)
10293 });
10294
10295 cx.executor().start_waiting();
10296 let fake_server = fake_servers.next().await.unwrap();
10297
10298 let format = editor
10299 .update_in(cx, |editor, window, cx| {
10300 editor.perform_format(
10301 project.clone(),
10302 FormatTrigger::Manual,
10303 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10304 window,
10305 cx,
10306 )
10307 })
10308 .unwrap();
10309 fake_server
10310 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10311 assert_eq!(
10312 params.text_document.uri,
10313 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10314 );
10315 assert_eq!(params.options.tab_size, 4);
10316 Ok(Some(vec![lsp::TextEdit::new(
10317 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10318 ", ".to_string(),
10319 )]))
10320 })
10321 .next()
10322 .await;
10323 cx.executor().start_waiting();
10324 format.await;
10325 assert_eq!(
10326 editor.update(cx, |editor, cx| editor.text(cx)),
10327 "one, two\nthree\n"
10328 );
10329
10330 editor.update_in(cx, |editor, window, cx| {
10331 editor.set_text("one\ntwo\nthree\n", window, cx)
10332 });
10333 // Ensure we don't lock if formatting hangs.
10334 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10335 move |params, _| async move {
10336 assert_eq!(
10337 params.text_document.uri,
10338 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10339 );
10340 futures::future::pending::<()>().await;
10341 unreachable!()
10342 },
10343 );
10344 let format = editor
10345 .update_in(cx, |editor, window, cx| {
10346 editor.perform_format(
10347 project,
10348 FormatTrigger::Manual,
10349 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10350 window,
10351 cx,
10352 )
10353 })
10354 .unwrap();
10355 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10356 cx.executor().start_waiting();
10357 format.await;
10358 assert_eq!(
10359 editor.update(cx, |editor, cx| editor.text(cx)),
10360 "one\ntwo\nthree\n"
10361 );
10362}
10363
10364#[gpui::test]
10365async fn test_multiple_formatters(cx: &mut TestAppContext) {
10366 init_test(cx, |settings| {
10367 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
10368 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10369 Formatter::LanguageServer { name: None },
10370 Formatter::CodeActions(
10371 [
10372 ("code-action-1".into(), true),
10373 ("code-action-2".into(), true),
10374 ]
10375 .into_iter()
10376 .collect(),
10377 ),
10378 ])))
10379 });
10380
10381 let fs = FakeFs::new(cx.executor());
10382 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
10383 .await;
10384
10385 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10386 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10387 language_registry.add(rust_lang());
10388
10389 let mut fake_servers = language_registry.register_fake_lsp(
10390 "Rust",
10391 FakeLspAdapter {
10392 capabilities: lsp::ServerCapabilities {
10393 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10394 execute_command_provider: Some(lsp::ExecuteCommandOptions {
10395 commands: vec!["the-command-for-code-action-1".into()],
10396 ..Default::default()
10397 }),
10398 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10399 ..Default::default()
10400 },
10401 ..Default::default()
10402 },
10403 );
10404
10405 let buffer = project
10406 .update(cx, |project, cx| {
10407 project.open_local_buffer(path!("/file.rs"), cx)
10408 })
10409 .await
10410 .unwrap();
10411
10412 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10413 let (editor, cx) = cx.add_window_view(|window, cx| {
10414 build_editor_with_project(project.clone(), buffer, window, cx)
10415 });
10416
10417 cx.executor().start_waiting();
10418
10419 let fake_server = fake_servers.next().await.unwrap();
10420 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10421 move |_params, _| async move {
10422 Ok(Some(vec![lsp::TextEdit::new(
10423 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10424 "applied-formatting\n".to_string(),
10425 )]))
10426 },
10427 );
10428 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10429 move |params, _| async move {
10430 assert_eq!(
10431 params.context.only,
10432 Some(vec!["code-action-1".into(), "code-action-2".into()])
10433 );
10434 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
10435 Ok(Some(vec![
10436 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10437 kind: Some("code-action-1".into()),
10438 edit: Some(lsp::WorkspaceEdit::new(
10439 [(
10440 uri.clone(),
10441 vec![lsp::TextEdit::new(
10442 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10443 "applied-code-action-1-edit\n".to_string(),
10444 )],
10445 )]
10446 .into_iter()
10447 .collect(),
10448 )),
10449 command: Some(lsp::Command {
10450 command: "the-command-for-code-action-1".into(),
10451 ..Default::default()
10452 }),
10453 ..Default::default()
10454 }),
10455 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10456 kind: Some("code-action-2".into()),
10457 edit: Some(lsp::WorkspaceEdit::new(
10458 [(
10459 uri.clone(),
10460 vec![lsp::TextEdit::new(
10461 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10462 "applied-code-action-2-edit\n".to_string(),
10463 )],
10464 )]
10465 .into_iter()
10466 .collect(),
10467 )),
10468 ..Default::default()
10469 }),
10470 ]))
10471 },
10472 );
10473
10474 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
10475 move |params, _| async move { Ok(params) }
10476 });
10477
10478 let command_lock = Arc::new(futures::lock::Mutex::new(()));
10479 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
10480 let fake = fake_server.clone();
10481 let lock = command_lock.clone();
10482 move |params, _| {
10483 assert_eq!(params.command, "the-command-for-code-action-1");
10484 let fake = fake.clone();
10485 let lock = lock.clone();
10486 async move {
10487 lock.lock().await;
10488 fake.server
10489 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
10490 label: None,
10491 edit: lsp::WorkspaceEdit {
10492 changes: Some(
10493 [(
10494 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
10495 vec![lsp::TextEdit {
10496 range: lsp::Range::new(
10497 lsp::Position::new(0, 0),
10498 lsp::Position::new(0, 0),
10499 ),
10500 new_text: "applied-code-action-1-command\n".into(),
10501 }],
10502 )]
10503 .into_iter()
10504 .collect(),
10505 ),
10506 ..Default::default()
10507 },
10508 })
10509 .await
10510 .into_response()
10511 .unwrap();
10512 Ok(Some(json!(null)))
10513 }
10514 }
10515 });
10516
10517 cx.executor().start_waiting();
10518 editor
10519 .update_in(cx, |editor, window, cx| {
10520 editor.perform_format(
10521 project.clone(),
10522 FormatTrigger::Manual,
10523 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10524 window,
10525 cx,
10526 )
10527 })
10528 .unwrap()
10529 .await;
10530 editor.update(cx, |editor, cx| {
10531 assert_eq!(
10532 editor.text(cx),
10533 r#"
10534 applied-code-action-2-edit
10535 applied-code-action-1-command
10536 applied-code-action-1-edit
10537 applied-formatting
10538 one
10539 two
10540 three
10541 "#
10542 .unindent()
10543 );
10544 });
10545
10546 editor.update_in(cx, |editor, window, cx| {
10547 editor.undo(&Default::default(), window, cx);
10548 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10549 });
10550
10551 // Perform a manual edit while waiting for an LSP command
10552 // that's being run as part of a formatting code action.
10553 let lock_guard = command_lock.lock().await;
10554 let format = editor
10555 .update_in(cx, |editor, window, cx| {
10556 editor.perform_format(
10557 project.clone(),
10558 FormatTrigger::Manual,
10559 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10560 window,
10561 cx,
10562 )
10563 })
10564 .unwrap();
10565 cx.run_until_parked();
10566 editor.update(cx, |editor, cx| {
10567 assert_eq!(
10568 editor.text(cx),
10569 r#"
10570 applied-code-action-1-edit
10571 applied-formatting
10572 one
10573 two
10574 three
10575 "#
10576 .unindent()
10577 );
10578
10579 editor.buffer.update(cx, |buffer, cx| {
10580 let ix = buffer.len(cx);
10581 buffer.edit([(ix..ix, "edited\n")], None, cx);
10582 });
10583 });
10584
10585 // Allow the LSP command to proceed. Because the buffer was edited,
10586 // the second code action will not be run.
10587 drop(lock_guard);
10588 format.await;
10589 editor.update_in(cx, |editor, window, cx| {
10590 assert_eq!(
10591 editor.text(cx),
10592 r#"
10593 applied-code-action-1-command
10594 applied-code-action-1-edit
10595 applied-formatting
10596 one
10597 two
10598 three
10599 edited
10600 "#
10601 .unindent()
10602 );
10603
10604 // The manual edit is undone first, because it is the last thing the user did
10605 // (even though the command completed afterwards).
10606 editor.undo(&Default::default(), window, cx);
10607 assert_eq!(
10608 editor.text(cx),
10609 r#"
10610 applied-code-action-1-command
10611 applied-code-action-1-edit
10612 applied-formatting
10613 one
10614 two
10615 three
10616 "#
10617 .unindent()
10618 );
10619
10620 // All the formatting (including the command, which completed after the manual edit)
10621 // is undone together.
10622 editor.undo(&Default::default(), window, cx);
10623 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10624 });
10625}
10626
10627#[gpui::test]
10628async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
10629 init_test(cx, |settings| {
10630 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10631 Formatter::LanguageServer { name: None },
10632 ])))
10633 });
10634
10635 let fs = FakeFs::new(cx.executor());
10636 fs.insert_file(path!("/file.ts"), Default::default()).await;
10637
10638 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10639
10640 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10641 language_registry.add(Arc::new(Language::new(
10642 LanguageConfig {
10643 name: "TypeScript".into(),
10644 matcher: LanguageMatcher {
10645 path_suffixes: vec!["ts".to_string()],
10646 ..Default::default()
10647 },
10648 ..LanguageConfig::default()
10649 },
10650 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10651 )));
10652 update_test_language_settings(cx, |settings| {
10653 settings.defaults.prettier = Some(PrettierSettings {
10654 allowed: true,
10655 ..PrettierSettings::default()
10656 });
10657 });
10658 let mut fake_servers = language_registry.register_fake_lsp(
10659 "TypeScript",
10660 FakeLspAdapter {
10661 capabilities: lsp::ServerCapabilities {
10662 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10663 ..Default::default()
10664 },
10665 ..Default::default()
10666 },
10667 );
10668
10669 let buffer = project
10670 .update(cx, |project, cx| {
10671 project.open_local_buffer(path!("/file.ts"), cx)
10672 })
10673 .await
10674 .unwrap();
10675
10676 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10677 let (editor, cx) = cx.add_window_view(|window, cx| {
10678 build_editor_with_project(project.clone(), buffer, window, cx)
10679 });
10680 editor.update_in(cx, |editor, window, cx| {
10681 editor.set_text(
10682 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10683 window,
10684 cx,
10685 )
10686 });
10687
10688 cx.executor().start_waiting();
10689 let fake_server = fake_servers.next().await.unwrap();
10690
10691 let format = editor
10692 .update_in(cx, |editor, window, cx| {
10693 editor.perform_code_action_kind(
10694 project.clone(),
10695 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10696 window,
10697 cx,
10698 )
10699 })
10700 .unwrap();
10701 fake_server
10702 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
10703 assert_eq!(
10704 params.text_document.uri,
10705 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10706 );
10707 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
10708 lsp::CodeAction {
10709 title: "Organize Imports".to_string(),
10710 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
10711 edit: Some(lsp::WorkspaceEdit {
10712 changes: Some(
10713 [(
10714 params.text_document.uri.clone(),
10715 vec![lsp::TextEdit::new(
10716 lsp::Range::new(
10717 lsp::Position::new(1, 0),
10718 lsp::Position::new(2, 0),
10719 ),
10720 "".to_string(),
10721 )],
10722 )]
10723 .into_iter()
10724 .collect(),
10725 ),
10726 ..Default::default()
10727 }),
10728 ..Default::default()
10729 },
10730 )]))
10731 })
10732 .next()
10733 .await;
10734 cx.executor().start_waiting();
10735 format.await;
10736 assert_eq!(
10737 editor.update(cx, |editor, cx| editor.text(cx)),
10738 "import { a } from 'module';\n\nconst x = a;\n"
10739 );
10740
10741 editor.update_in(cx, |editor, window, cx| {
10742 editor.set_text(
10743 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10744 window,
10745 cx,
10746 )
10747 });
10748 // Ensure we don't lock if code action hangs.
10749 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10750 move |params, _| async move {
10751 assert_eq!(
10752 params.text_document.uri,
10753 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10754 );
10755 futures::future::pending::<()>().await;
10756 unreachable!()
10757 },
10758 );
10759 let format = editor
10760 .update_in(cx, |editor, window, cx| {
10761 editor.perform_code_action_kind(
10762 project,
10763 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10764 window,
10765 cx,
10766 )
10767 })
10768 .unwrap();
10769 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
10770 cx.executor().start_waiting();
10771 format.await;
10772 assert_eq!(
10773 editor.update(cx, |editor, cx| editor.text(cx)),
10774 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
10775 );
10776}
10777
10778#[gpui::test]
10779async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
10780 init_test(cx, |_| {});
10781
10782 let mut cx = EditorLspTestContext::new_rust(
10783 lsp::ServerCapabilities {
10784 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10785 ..Default::default()
10786 },
10787 cx,
10788 )
10789 .await;
10790
10791 cx.set_state(indoc! {"
10792 one.twoˇ
10793 "});
10794
10795 // The format request takes a long time. When it completes, it inserts
10796 // a newline and an indent before the `.`
10797 cx.lsp
10798 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
10799 let executor = cx.background_executor().clone();
10800 async move {
10801 executor.timer(Duration::from_millis(100)).await;
10802 Ok(Some(vec![lsp::TextEdit {
10803 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
10804 new_text: "\n ".into(),
10805 }]))
10806 }
10807 });
10808
10809 // Submit a format request.
10810 let format_1 = cx
10811 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10812 .unwrap();
10813 cx.executor().run_until_parked();
10814
10815 // Submit a second format request.
10816 let format_2 = cx
10817 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10818 .unwrap();
10819 cx.executor().run_until_parked();
10820
10821 // Wait for both format requests to complete
10822 cx.executor().advance_clock(Duration::from_millis(200));
10823 cx.executor().start_waiting();
10824 format_1.await.unwrap();
10825 cx.executor().start_waiting();
10826 format_2.await.unwrap();
10827
10828 // The formatting edits only happens once.
10829 cx.assert_editor_state(indoc! {"
10830 one
10831 .twoˇ
10832 "});
10833}
10834
10835#[gpui::test]
10836async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
10837 init_test(cx, |settings| {
10838 settings.defaults.formatter = Some(SelectedFormatter::Auto)
10839 });
10840
10841 let mut cx = EditorLspTestContext::new_rust(
10842 lsp::ServerCapabilities {
10843 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10844 ..Default::default()
10845 },
10846 cx,
10847 )
10848 .await;
10849
10850 // Set up a buffer white some trailing whitespace and no trailing newline.
10851 cx.set_state(
10852 &[
10853 "one ", //
10854 "twoˇ", //
10855 "three ", //
10856 "four", //
10857 ]
10858 .join("\n"),
10859 );
10860
10861 // Submit a format request.
10862 let format = cx
10863 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10864 .unwrap();
10865
10866 // Record which buffer changes have been sent to the language server
10867 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
10868 cx.lsp
10869 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
10870 let buffer_changes = buffer_changes.clone();
10871 move |params, _| {
10872 buffer_changes.lock().extend(
10873 params
10874 .content_changes
10875 .into_iter()
10876 .map(|e| (e.range.unwrap(), e.text)),
10877 );
10878 }
10879 });
10880
10881 // Handle formatting requests to the language server.
10882 cx.lsp
10883 .set_request_handler::<lsp::request::Formatting, _, _>({
10884 let buffer_changes = buffer_changes.clone();
10885 move |_, _| {
10886 // When formatting is requested, trailing whitespace has already been stripped,
10887 // and the trailing newline has already been added.
10888 assert_eq!(
10889 &buffer_changes.lock()[1..],
10890 &[
10891 (
10892 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
10893 "".into()
10894 ),
10895 (
10896 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
10897 "".into()
10898 ),
10899 (
10900 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
10901 "\n".into()
10902 ),
10903 ]
10904 );
10905
10906 // Insert blank lines between each line of the buffer.
10907 async move {
10908 Ok(Some(vec![
10909 lsp::TextEdit {
10910 range: lsp::Range::new(
10911 lsp::Position::new(1, 0),
10912 lsp::Position::new(1, 0),
10913 ),
10914 new_text: "\n".into(),
10915 },
10916 lsp::TextEdit {
10917 range: lsp::Range::new(
10918 lsp::Position::new(2, 0),
10919 lsp::Position::new(2, 0),
10920 ),
10921 new_text: "\n".into(),
10922 },
10923 ]))
10924 }
10925 }
10926 });
10927
10928 // After formatting the buffer, the trailing whitespace is stripped,
10929 // a newline is appended, and the edits provided by the language server
10930 // have been applied.
10931 format.await.unwrap();
10932 cx.assert_editor_state(
10933 &[
10934 "one", //
10935 "", //
10936 "twoˇ", //
10937 "", //
10938 "three", //
10939 "four", //
10940 "", //
10941 ]
10942 .join("\n"),
10943 );
10944
10945 // Undoing the formatting undoes the trailing whitespace removal, the
10946 // trailing newline, and the LSP edits.
10947 cx.update_buffer(|buffer, cx| buffer.undo(cx));
10948 cx.assert_editor_state(
10949 &[
10950 "one ", //
10951 "twoˇ", //
10952 "three ", //
10953 "four", //
10954 ]
10955 .join("\n"),
10956 );
10957}
10958
10959#[gpui::test]
10960async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
10961 cx: &mut TestAppContext,
10962) {
10963 init_test(cx, |_| {});
10964
10965 cx.update(|cx| {
10966 cx.update_global::<SettingsStore, _>(|settings, cx| {
10967 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10968 settings.auto_signature_help = Some(true);
10969 });
10970 });
10971 });
10972
10973 let mut cx = EditorLspTestContext::new_rust(
10974 lsp::ServerCapabilities {
10975 signature_help_provider: Some(lsp::SignatureHelpOptions {
10976 ..Default::default()
10977 }),
10978 ..Default::default()
10979 },
10980 cx,
10981 )
10982 .await;
10983
10984 let language = Language::new(
10985 LanguageConfig {
10986 name: "Rust".into(),
10987 brackets: BracketPairConfig {
10988 pairs: vec![
10989 BracketPair {
10990 start: "{".to_string(),
10991 end: "}".to_string(),
10992 close: true,
10993 surround: true,
10994 newline: true,
10995 },
10996 BracketPair {
10997 start: "(".to_string(),
10998 end: ")".to_string(),
10999 close: true,
11000 surround: true,
11001 newline: true,
11002 },
11003 BracketPair {
11004 start: "/*".to_string(),
11005 end: " */".to_string(),
11006 close: true,
11007 surround: true,
11008 newline: true,
11009 },
11010 BracketPair {
11011 start: "[".to_string(),
11012 end: "]".to_string(),
11013 close: false,
11014 surround: false,
11015 newline: true,
11016 },
11017 BracketPair {
11018 start: "\"".to_string(),
11019 end: "\"".to_string(),
11020 close: true,
11021 surround: true,
11022 newline: false,
11023 },
11024 BracketPair {
11025 start: "<".to_string(),
11026 end: ">".to_string(),
11027 close: false,
11028 surround: true,
11029 newline: true,
11030 },
11031 ],
11032 ..Default::default()
11033 },
11034 autoclose_before: "})]".to_string(),
11035 ..Default::default()
11036 },
11037 Some(tree_sitter_rust::LANGUAGE.into()),
11038 );
11039 let language = Arc::new(language);
11040
11041 cx.language_registry().add(language.clone());
11042 cx.update_buffer(|buffer, cx| {
11043 buffer.set_language(Some(language), cx);
11044 });
11045
11046 cx.set_state(
11047 &r#"
11048 fn main() {
11049 sampleˇ
11050 }
11051 "#
11052 .unindent(),
11053 );
11054
11055 cx.update_editor(|editor, window, cx| {
11056 editor.handle_input("(", window, cx);
11057 });
11058 cx.assert_editor_state(
11059 &"
11060 fn main() {
11061 sample(ˇ)
11062 }
11063 "
11064 .unindent(),
11065 );
11066
11067 let mocked_response = lsp::SignatureHelp {
11068 signatures: vec![lsp::SignatureInformation {
11069 label: "fn sample(param1: u8, param2: u8)".to_string(),
11070 documentation: None,
11071 parameters: Some(vec![
11072 lsp::ParameterInformation {
11073 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11074 documentation: None,
11075 },
11076 lsp::ParameterInformation {
11077 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11078 documentation: None,
11079 },
11080 ]),
11081 active_parameter: None,
11082 }],
11083 active_signature: Some(0),
11084 active_parameter: Some(0),
11085 };
11086 handle_signature_help_request(&mut cx, mocked_response).await;
11087
11088 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11089 .await;
11090
11091 cx.editor(|editor, _, _| {
11092 let signature_help_state = editor.signature_help_state.popover().cloned();
11093 let signature = signature_help_state.unwrap();
11094 assert_eq!(
11095 signature.signatures[signature.current_signature].label,
11096 "fn sample(param1: u8, param2: u8)"
11097 );
11098 });
11099}
11100
11101#[gpui::test]
11102async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
11103 init_test(cx, |_| {});
11104
11105 cx.update(|cx| {
11106 cx.update_global::<SettingsStore, _>(|settings, cx| {
11107 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11108 settings.auto_signature_help = Some(false);
11109 settings.show_signature_help_after_edits = Some(false);
11110 });
11111 });
11112 });
11113
11114 let mut cx = EditorLspTestContext::new_rust(
11115 lsp::ServerCapabilities {
11116 signature_help_provider: Some(lsp::SignatureHelpOptions {
11117 ..Default::default()
11118 }),
11119 ..Default::default()
11120 },
11121 cx,
11122 )
11123 .await;
11124
11125 let language = Language::new(
11126 LanguageConfig {
11127 name: "Rust".into(),
11128 brackets: BracketPairConfig {
11129 pairs: vec![
11130 BracketPair {
11131 start: "{".to_string(),
11132 end: "}".to_string(),
11133 close: true,
11134 surround: true,
11135 newline: true,
11136 },
11137 BracketPair {
11138 start: "(".to_string(),
11139 end: ")".to_string(),
11140 close: true,
11141 surround: true,
11142 newline: true,
11143 },
11144 BracketPair {
11145 start: "/*".to_string(),
11146 end: " */".to_string(),
11147 close: true,
11148 surround: true,
11149 newline: true,
11150 },
11151 BracketPair {
11152 start: "[".to_string(),
11153 end: "]".to_string(),
11154 close: false,
11155 surround: false,
11156 newline: true,
11157 },
11158 BracketPair {
11159 start: "\"".to_string(),
11160 end: "\"".to_string(),
11161 close: true,
11162 surround: true,
11163 newline: false,
11164 },
11165 BracketPair {
11166 start: "<".to_string(),
11167 end: ">".to_string(),
11168 close: false,
11169 surround: true,
11170 newline: true,
11171 },
11172 ],
11173 ..Default::default()
11174 },
11175 autoclose_before: "})]".to_string(),
11176 ..Default::default()
11177 },
11178 Some(tree_sitter_rust::LANGUAGE.into()),
11179 );
11180 let language = Arc::new(language);
11181
11182 cx.language_registry().add(language.clone());
11183 cx.update_buffer(|buffer, cx| {
11184 buffer.set_language(Some(language), cx);
11185 });
11186
11187 // Ensure that signature_help is not called when no signature help is enabled.
11188 cx.set_state(
11189 &r#"
11190 fn main() {
11191 sampleˇ
11192 }
11193 "#
11194 .unindent(),
11195 );
11196 cx.update_editor(|editor, window, cx| {
11197 editor.handle_input("(", window, cx);
11198 });
11199 cx.assert_editor_state(
11200 &"
11201 fn main() {
11202 sample(ˇ)
11203 }
11204 "
11205 .unindent(),
11206 );
11207 cx.editor(|editor, _, _| {
11208 assert!(editor.signature_help_state.task().is_none());
11209 });
11210
11211 let mocked_response = lsp::SignatureHelp {
11212 signatures: vec![lsp::SignatureInformation {
11213 label: "fn sample(param1: u8, param2: u8)".to_string(),
11214 documentation: None,
11215 parameters: Some(vec![
11216 lsp::ParameterInformation {
11217 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11218 documentation: None,
11219 },
11220 lsp::ParameterInformation {
11221 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11222 documentation: None,
11223 },
11224 ]),
11225 active_parameter: None,
11226 }],
11227 active_signature: Some(0),
11228 active_parameter: Some(0),
11229 };
11230
11231 // Ensure that signature_help is called when enabled afte edits
11232 cx.update(|_, cx| {
11233 cx.update_global::<SettingsStore, _>(|settings, cx| {
11234 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11235 settings.auto_signature_help = Some(false);
11236 settings.show_signature_help_after_edits = Some(true);
11237 });
11238 });
11239 });
11240 cx.set_state(
11241 &r#"
11242 fn main() {
11243 sampleˇ
11244 }
11245 "#
11246 .unindent(),
11247 );
11248 cx.update_editor(|editor, window, cx| {
11249 editor.handle_input("(", window, cx);
11250 });
11251 cx.assert_editor_state(
11252 &"
11253 fn main() {
11254 sample(ˇ)
11255 }
11256 "
11257 .unindent(),
11258 );
11259 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11260 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11261 .await;
11262 cx.update_editor(|editor, _, _| {
11263 let signature_help_state = editor.signature_help_state.popover().cloned();
11264 assert!(signature_help_state.is_some());
11265 let signature = signature_help_state.unwrap();
11266 assert_eq!(
11267 signature.signatures[signature.current_signature].label,
11268 "fn sample(param1: u8, param2: u8)"
11269 );
11270 editor.signature_help_state = SignatureHelpState::default();
11271 });
11272
11273 // Ensure that signature_help is called when auto signature help override is enabled
11274 cx.update(|_, cx| {
11275 cx.update_global::<SettingsStore, _>(|settings, cx| {
11276 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11277 settings.auto_signature_help = Some(true);
11278 settings.show_signature_help_after_edits = Some(false);
11279 });
11280 });
11281 });
11282 cx.set_state(
11283 &r#"
11284 fn main() {
11285 sampleˇ
11286 }
11287 "#
11288 .unindent(),
11289 );
11290 cx.update_editor(|editor, window, cx| {
11291 editor.handle_input("(", window, cx);
11292 });
11293 cx.assert_editor_state(
11294 &"
11295 fn main() {
11296 sample(ˇ)
11297 }
11298 "
11299 .unindent(),
11300 );
11301 handle_signature_help_request(&mut cx, mocked_response).await;
11302 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11303 .await;
11304 cx.editor(|editor, _, _| {
11305 let signature_help_state = editor.signature_help_state.popover().cloned();
11306 assert!(signature_help_state.is_some());
11307 let signature = signature_help_state.unwrap();
11308 assert_eq!(
11309 signature.signatures[signature.current_signature].label,
11310 "fn sample(param1: u8, param2: u8)"
11311 );
11312 });
11313}
11314
11315#[gpui::test]
11316async fn test_signature_help(cx: &mut TestAppContext) {
11317 init_test(cx, |_| {});
11318 cx.update(|cx| {
11319 cx.update_global::<SettingsStore, _>(|settings, cx| {
11320 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11321 settings.auto_signature_help = Some(true);
11322 });
11323 });
11324 });
11325
11326 let mut cx = EditorLspTestContext::new_rust(
11327 lsp::ServerCapabilities {
11328 signature_help_provider: Some(lsp::SignatureHelpOptions {
11329 ..Default::default()
11330 }),
11331 ..Default::default()
11332 },
11333 cx,
11334 )
11335 .await;
11336
11337 // A test that directly calls `show_signature_help`
11338 cx.update_editor(|editor, window, cx| {
11339 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11340 });
11341
11342 let mocked_response = lsp::SignatureHelp {
11343 signatures: vec![lsp::SignatureInformation {
11344 label: "fn sample(param1: u8, param2: u8)".to_string(),
11345 documentation: None,
11346 parameters: Some(vec![
11347 lsp::ParameterInformation {
11348 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11349 documentation: None,
11350 },
11351 lsp::ParameterInformation {
11352 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11353 documentation: None,
11354 },
11355 ]),
11356 active_parameter: None,
11357 }],
11358 active_signature: Some(0),
11359 active_parameter: Some(0),
11360 };
11361 handle_signature_help_request(&mut cx, mocked_response).await;
11362
11363 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11364 .await;
11365
11366 cx.editor(|editor, _, _| {
11367 let signature_help_state = editor.signature_help_state.popover().cloned();
11368 assert!(signature_help_state.is_some());
11369 let signature = signature_help_state.unwrap();
11370 assert_eq!(
11371 signature.signatures[signature.current_signature].label,
11372 "fn sample(param1: u8, param2: u8)"
11373 );
11374 });
11375
11376 // When exiting outside from inside the brackets, `signature_help` is closed.
11377 cx.set_state(indoc! {"
11378 fn main() {
11379 sample(ˇ);
11380 }
11381
11382 fn sample(param1: u8, param2: u8) {}
11383 "});
11384
11385 cx.update_editor(|editor, window, cx| {
11386 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11387 s.select_ranges([0..0])
11388 });
11389 });
11390
11391 let mocked_response = lsp::SignatureHelp {
11392 signatures: Vec::new(),
11393 active_signature: None,
11394 active_parameter: None,
11395 };
11396 handle_signature_help_request(&mut cx, mocked_response).await;
11397
11398 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11399 .await;
11400
11401 cx.editor(|editor, _, _| {
11402 assert!(!editor.signature_help_state.is_shown());
11403 });
11404
11405 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
11406 cx.set_state(indoc! {"
11407 fn main() {
11408 sample(ˇ);
11409 }
11410
11411 fn sample(param1: u8, param2: u8) {}
11412 "});
11413
11414 let mocked_response = lsp::SignatureHelp {
11415 signatures: vec![lsp::SignatureInformation {
11416 label: "fn sample(param1: u8, param2: u8)".to_string(),
11417 documentation: None,
11418 parameters: Some(vec![
11419 lsp::ParameterInformation {
11420 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11421 documentation: None,
11422 },
11423 lsp::ParameterInformation {
11424 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11425 documentation: None,
11426 },
11427 ]),
11428 active_parameter: None,
11429 }],
11430 active_signature: Some(0),
11431 active_parameter: Some(0),
11432 };
11433 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11434 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11435 .await;
11436 cx.editor(|editor, _, _| {
11437 assert!(editor.signature_help_state.is_shown());
11438 });
11439
11440 // Restore the popover with more parameter input
11441 cx.set_state(indoc! {"
11442 fn main() {
11443 sample(param1, param2ˇ);
11444 }
11445
11446 fn sample(param1: u8, param2: u8) {}
11447 "});
11448
11449 let mocked_response = lsp::SignatureHelp {
11450 signatures: vec![lsp::SignatureInformation {
11451 label: "fn sample(param1: u8, param2: u8)".to_string(),
11452 documentation: None,
11453 parameters: Some(vec![
11454 lsp::ParameterInformation {
11455 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11456 documentation: None,
11457 },
11458 lsp::ParameterInformation {
11459 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11460 documentation: None,
11461 },
11462 ]),
11463 active_parameter: None,
11464 }],
11465 active_signature: Some(0),
11466 active_parameter: Some(1),
11467 };
11468 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11469 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11470 .await;
11471
11472 // When selecting a range, the popover is gone.
11473 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
11474 cx.update_editor(|editor, window, cx| {
11475 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11476 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11477 })
11478 });
11479 cx.assert_editor_state(indoc! {"
11480 fn main() {
11481 sample(param1, «ˇparam2»);
11482 }
11483
11484 fn sample(param1: u8, param2: u8) {}
11485 "});
11486 cx.editor(|editor, _, _| {
11487 assert!(!editor.signature_help_state.is_shown());
11488 });
11489
11490 // When unselecting again, the popover is back if within the brackets.
11491 cx.update_editor(|editor, window, cx| {
11492 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11493 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11494 })
11495 });
11496 cx.assert_editor_state(indoc! {"
11497 fn main() {
11498 sample(param1, ˇparam2);
11499 }
11500
11501 fn sample(param1: u8, param2: u8) {}
11502 "});
11503 handle_signature_help_request(&mut cx, mocked_response).await;
11504 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11505 .await;
11506 cx.editor(|editor, _, _| {
11507 assert!(editor.signature_help_state.is_shown());
11508 });
11509
11510 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
11511 cx.update_editor(|editor, window, cx| {
11512 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11513 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
11514 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11515 })
11516 });
11517 cx.assert_editor_state(indoc! {"
11518 fn main() {
11519 sample(param1, ˇparam2);
11520 }
11521
11522 fn sample(param1: u8, param2: u8) {}
11523 "});
11524
11525 let mocked_response = lsp::SignatureHelp {
11526 signatures: vec![lsp::SignatureInformation {
11527 label: "fn sample(param1: u8, param2: u8)".to_string(),
11528 documentation: None,
11529 parameters: Some(vec![
11530 lsp::ParameterInformation {
11531 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11532 documentation: None,
11533 },
11534 lsp::ParameterInformation {
11535 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11536 documentation: None,
11537 },
11538 ]),
11539 active_parameter: None,
11540 }],
11541 active_signature: Some(0),
11542 active_parameter: Some(1),
11543 };
11544 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11545 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11546 .await;
11547 cx.update_editor(|editor, _, cx| {
11548 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
11549 });
11550 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11551 .await;
11552 cx.update_editor(|editor, window, cx| {
11553 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11554 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11555 })
11556 });
11557 cx.assert_editor_state(indoc! {"
11558 fn main() {
11559 sample(param1, «ˇparam2»);
11560 }
11561
11562 fn sample(param1: u8, param2: u8) {}
11563 "});
11564 cx.update_editor(|editor, window, cx| {
11565 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11566 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11567 })
11568 });
11569 cx.assert_editor_state(indoc! {"
11570 fn main() {
11571 sample(param1, ˇparam2);
11572 }
11573
11574 fn sample(param1: u8, param2: u8) {}
11575 "});
11576 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
11577 .await;
11578}
11579
11580#[gpui::test]
11581async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
11582 init_test(cx, |_| {});
11583
11584 let mut cx = EditorLspTestContext::new_rust(
11585 lsp::ServerCapabilities {
11586 signature_help_provider: Some(lsp::SignatureHelpOptions {
11587 ..Default::default()
11588 }),
11589 ..Default::default()
11590 },
11591 cx,
11592 )
11593 .await;
11594
11595 cx.set_state(indoc! {"
11596 fn main() {
11597 overloadedˇ
11598 }
11599 "});
11600
11601 cx.update_editor(|editor, window, cx| {
11602 editor.handle_input("(", window, cx);
11603 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11604 });
11605
11606 // Mock response with 3 signatures
11607 let mocked_response = lsp::SignatureHelp {
11608 signatures: vec![
11609 lsp::SignatureInformation {
11610 label: "fn overloaded(x: i32)".to_string(),
11611 documentation: None,
11612 parameters: Some(vec![lsp::ParameterInformation {
11613 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11614 documentation: None,
11615 }]),
11616 active_parameter: None,
11617 },
11618 lsp::SignatureInformation {
11619 label: "fn overloaded(x: i32, y: i32)".to_string(),
11620 documentation: None,
11621 parameters: Some(vec![
11622 lsp::ParameterInformation {
11623 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11624 documentation: None,
11625 },
11626 lsp::ParameterInformation {
11627 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11628 documentation: None,
11629 },
11630 ]),
11631 active_parameter: None,
11632 },
11633 lsp::SignatureInformation {
11634 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
11635 documentation: None,
11636 parameters: Some(vec![
11637 lsp::ParameterInformation {
11638 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11639 documentation: None,
11640 },
11641 lsp::ParameterInformation {
11642 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11643 documentation: None,
11644 },
11645 lsp::ParameterInformation {
11646 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
11647 documentation: None,
11648 },
11649 ]),
11650 active_parameter: None,
11651 },
11652 ],
11653 active_signature: Some(1),
11654 active_parameter: Some(0),
11655 };
11656 handle_signature_help_request(&mut cx, mocked_response).await;
11657
11658 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11659 .await;
11660
11661 // Verify we have multiple signatures and the right one is selected
11662 cx.editor(|editor, _, _| {
11663 let popover = editor.signature_help_state.popover().cloned().unwrap();
11664 assert_eq!(popover.signatures.len(), 3);
11665 // active_signature was 1, so that should be the current
11666 assert_eq!(popover.current_signature, 1);
11667 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
11668 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
11669 assert_eq!(
11670 popover.signatures[2].label,
11671 "fn overloaded(x: i32, y: i32, z: i32)"
11672 );
11673 });
11674
11675 // Test navigation functionality
11676 cx.update_editor(|editor, window, cx| {
11677 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11678 });
11679
11680 cx.editor(|editor, _, _| {
11681 let popover = editor.signature_help_state.popover().cloned().unwrap();
11682 assert_eq!(popover.current_signature, 2);
11683 });
11684
11685 // Test wrap around
11686 cx.update_editor(|editor, window, cx| {
11687 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11688 });
11689
11690 cx.editor(|editor, _, _| {
11691 let popover = editor.signature_help_state.popover().cloned().unwrap();
11692 assert_eq!(popover.current_signature, 0);
11693 });
11694
11695 // Test previous navigation
11696 cx.update_editor(|editor, window, cx| {
11697 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
11698 });
11699
11700 cx.editor(|editor, _, _| {
11701 let popover = editor.signature_help_state.popover().cloned().unwrap();
11702 assert_eq!(popover.current_signature, 2);
11703 });
11704}
11705
11706#[gpui::test]
11707async fn test_completion_mode(cx: &mut TestAppContext) {
11708 init_test(cx, |_| {});
11709 let mut cx = EditorLspTestContext::new_rust(
11710 lsp::ServerCapabilities {
11711 completion_provider: Some(lsp::CompletionOptions {
11712 resolve_provider: Some(true),
11713 ..Default::default()
11714 }),
11715 ..Default::default()
11716 },
11717 cx,
11718 )
11719 .await;
11720
11721 struct Run {
11722 run_description: &'static str,
11723 initial_state: String,
11724 buffer_marked_text: String,
11725 completion_label: &'static str,
11726 completion_text: &'static str,
11727 expected_with_insert_mode: String,
11728 expected_with_replace_mode: String,
11729 expected_with_replace_subsequence_mode: String,
11730 expected_with_replace_suffix_mode: String,
11731 }
11732
11733 let runs = [
11734 Run {
11735 run_description: "Start of word matches completion text",
11736 initial_state: "before ediˇ after".into(),
11737 buffer_marked_text: "before <edi|> after".into(),
11738 completion_label: "editor",
11739 completion_text: "editor",
11740 expected_with_insert_mode: "before editorˇ after".into(),
11741 expected_with_replace_mode: "before editorˇ after".into(),
11742 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11743 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11744 },
11745 Run {
11746 run_description: "Accept same text at the middle of the word",
11747 initial_state: "before ediˇtor after".into(),
11748 buffer_marked_text: "before <edi|tor> after".into(),
11749 completion_label: "editor",
11750 completion_text: "editor",
11751 expected_with_insert_mode: "before editorˇtor after".into(),
11752 expected_with_replace_mode: "before editorˇ after".into(),
11753 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11754 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11755 },
11756 Run {
11757 run_description: "End of word matches completion text -- cursor at end",
11758 initial_state: "before torˇ after".into(),
11759 buffer_marked_text: "before <tor|> after".into(),
11760 completion_label: "editor",
11761 completion_text: "editor",
11762 expected_with_insert_mode: "before editorˇ after".into(),
11763 expected_with_replace_mode: "before editorˇ after".into(),
11764 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11765 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11766 },
11767 Run {
11768 run_description: "End of word matches completion text -- cursor at start",
11769 initial_state: "before ˇtor after".into(),
11770 buffer_marked_text: "before <|tor> after".into(),
11771 completion_label: "editor",
11772 completion_text: "editor",
11773 expected_with_insert_mode: "before editorˇtor after".into(),
11774 expected_with_replace_mode: "before editorˇ after".into(),
11775 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11776 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11777 },
11778 Run {
11779 run_description: "Prepend text containing whitespace",
11780 initial_state: "pˇfield: bool".into(),
11781 buffer_marked_text: "<p|field>: bool".into(),
11782 completion_label: "pub ",
11783 completion_text: "pub ",
11784 expected_with_insert_mode: "pub ˇfield: bool".into(),
11785 expected_with_replace_mode: "pub ˇ: bool".into(),
11786 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
11787 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
11788 },
11789 Run {
11790 run_description: "Add element to start of list",
11791 initial_state: "[element_ˇelement_2]".into(),
11792 buffer_marked_text: "[<element_|element_2>]".into(),
11793 completion_label: "element_1",
11794 completion_text: "element_1",
11795 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
11796 expected_with_replace_mode: "[element_1ˇ]".into(),
11797 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
11798 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
11799 },
11800 Run {
11801 run_description: "Add element to start of list -- first and second elements are equal",
11802 initial_state: "[elˇelement]".into(),
11803 buffer_marked_text: "[<el|element>]".into(),
11804 completion_label: "element",
11805 completion_text: "element",
11806 expected_with_insert_mode: "[elementˇelement]".into(),
11807 expected_with_replace_mode: "[elementˇ]".into(),
11808 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
11809 expected_with_replace_suffix_mode: "[elementˇ]".into(),
11810 },
11811 Run {
11812 run_description: "Ends with matching suffix",
11813 initial_state: "SubˇError".into(),
11814 buffer_marked_text: "<Sub|Error>".into(),
11815 completion_label: "SubscriptionError",
11816 completion_text: "SubscriptionError",
11817 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
11818 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11819 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11820 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
11821 },
11822 Run {
11823 run_description: "Suffix is a subsequence -- contiguous",
11824 initial_state: "SubˇErr".into(),
11825 buffer_marked_text: "<Sub|Err>".into(),
11826 completion_label: "SubscriptionError",
11827 completion_text: "SubscriptionError",
11828 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
11829 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11830 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11831 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
11832 },
11833 Run {
11834 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
11835 initial_state: "Suˇscrirr".into(),
11836 buffer_marked_text: "<Su|scrirr>".into(),
11837 completion_label: "SubscriptionError",
11838 completion_text: "SubscriptionError",
11839 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
11840 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11841 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11842 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
11843 },
11844 Run {
11845 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
11846 initial_state: "foo(indˇix)".into(),
11847 buffer_marked_text: "foo(<ind|ix>)".into(),
11848 completion_label: "node_index",
11849 completion_text: "node_index",
11850 expected_with_insert_mode: "foo(node_indexˇix)".into(),
11851 expected_with_replace_mode: "foo(node_indexˇ)".into(),
11852 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
11853 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
11854 },
11855 Run {
11856 run_description: "Replace range ends before cursor - should extend to cursor",
11857 initial_state: "before editˇo after".into(),
11858 buffer_marked_text: "before <{ed}>it|o after".into(),
11859 completion_label: "editor",
11860 completion_text: "editor",
11861 expected_with_insert_mode: "before editorˇo after".into(),
11862 expected_with_replace_mode: "before editorˇo after".into(),
11863 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
11864 expected_with_replace_suffix_mode: "before editorˇo after".into(),
11865 },
11866 Run {
11867 run_description: "Uses label for suffix matching",
11868 initial_state: "before ediˇtor after".into(),
11869 buffer_marked_text: "before <edi|tor> after".into(),
11870 completion_label: "editor",
11871 completion_text: "editor()",
11872 expected_with_insert_mode: "before editor()ˇtor after".into(),
11873 expected_with_replace_mode: "before editor()ˇ after".into(),
11874 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
11875 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
11876 },
11877 Run {
11878 run_description: "Case insensitive subsequence and suffix matching",
11879 initial_state: "before EDiˇtoR after".into(),
11880 buffer_marked_text: "before <EDi|toR> after".into(),
11881 completion_label: "editor",
11882 completion_text: "editor",
11883 expected_with_insert_mode: "before editorˇtoR after".into(),
11884 expected_with_replace_mode: "before editorˇ after".into(),
11885 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11886 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11887 },
11888 ];
11889
11890 for run in runs {
11891 let run_variations = [
11892 (LspInsertMode::Insert, run.expected_with_insert_mode),
11893 (LspInsertMode::Replace, run.expected_with_replace_mode),
11894 (
11895 LspInsertMode::ReplaceSubsequence,
11896 run.expected_with_replace_subsequence_mode,
11897 ),
11898 (
11899 LspInsertMode::ReplaceSuffix,
11900 run.expected_with_replace_suffix_mode,
11901 ),
11902 ];
11903
11904 for (lsp_insert_mode, expected_text) in run_variations {
11905 eprintln!(
11906 "run = {:?}, mode = {lsp_insert_mode:.?}",
11907 run.run_description,
11908 );
11909
11910 update_test_language_settings(&mut cx, |settings| {
11911 settings.defaults.completions = Some(CompletionSettings {
11912 lsp_insert_mode,
11913 words: WordsCompletionMode::Disabled,
11914 lsp: true,
11915 lsp_fetch_timeout_ms: 0,
11916 });
11917 });
11918
11919 cx.set_state(&run.initial_state);
11920 cx.update_editor(|editor, window, cx| {
11921 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11922 });
11923
11924 let counter = Arc::new(AtomicUsize::new(0));
11925 handle_completion_request_with_insert_and_replace(
11926 &mut cx,
11927 &run.buffer_marked_text,
11928 vec![(run.completion_label, run.completion_text)],
11929 counter.clone(),
11930 )
11931 .await;
11932 cx.condition(|editor, _| editor.context_menu_visible())
11933 .await;
11934 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11935
11936 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11937 editor
11938 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11939 .unwrap()
11940 });
11941 cx.assert_editor_state(&expected_text);
11942 handle_resolve_completion_request(&mut cx, None).await;
11943 apply_additional_edits.await.unwrap();
11944 }
11945 }
11946}
11947
11948#[gpui::test]
11949async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
11950 init_test(cx, |_| {});
11951 let mut cx = EditorLspTestContext::new_rust(
11952 lsp::ServerCapabilities {
11953 completion_provider: Some(lsp::CompletionOptions {
11954 resolve_provider: Some(true),
11955 ..Default::default()
11956 }),
11957 ..Default::default()
11958 },
11959 cx,
11960 )
11961 .await;
11962
11963 let initial_state = "SubˇError";
11964 let buffer_marked_text = "<Sub|Error>";
11965 let completion_text = "SubscriptionError";
11966 let expected_with_insert_mode = "SubscriptionErrorˇError";
11967 let expected_with_replace_mode = "SubscriptionErrorˇ";
11968
11969 update_test_language_settings(&mut cx, |settings| {
11970 settings.defaults.completions = Some(CompletionSettings {
11971 words: WordsCompletionMode::Disabled,
11972 // set the opposite here to ensure that the action is overriding the default behavior
11973 lsp_insert_mode: LspInsertMode::Insert,
11974 lsp: true,
11975 lsp_fetch_timeout_ms: 0,
11976 });
11977 });
11978
11979 cx.set_state(initial_state);
11980 cx.update_editor(|editor, window, cx| {
11981 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11982 });
11983
11984 let counter = Arc::new(AtomicUsize::new(0));
11985 handle_completion_request_with_insert_and_replace(
11986 &mut cx,
11987 &buffer_marked_text,
11988 vec![(completion_text, completion_text)],
11989 counter.clone(),
11990 )
11991 .await;
11992 cx.condition(|editor, _| editor.context_menu_visible())
11993 .await;
11994 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11995
11996 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11997 editor
11998 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11999 .unwrap()
12000 });
12001 cx.assert_editor_state(&expected_with_replace_mode);
12002 handle_resolve_completion_request(&mut cx, None).await;
12003 apply_additional_edits.await.unwrap();
12004
12005 update_test_language_settings(&mut cx, |settings| {
12006 settings.defaults.completions = Some(CompletionSettings {
12007 words: WordsCompletionMode::Disabled,
12008 // set the opposite here to ensure that the action is overriding the default behavior
12009 lsp_insert_mode: LspInsertMode::Replace,
12010 lsp: true,
12011 lsp_fetch_timeout_ms: 0,
12012 });
12013 });
12014
12015 cx.set_state(initial_state);
12016 cx.update_editor(|editor, window, cx| {
12017 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12018 });
12019 handle_completion_request_with_insert_and_replace(
12020 &mut cx,
12021 &buffer_marked_text,
12022 vec![(completion_text, completion_text)],
12023 counter.clone(),
12024 )
12025 .await;
12026 cx.condition(|editor, _| editor.context_menu_visible())
12027 .await;
12028 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12029
12030 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12031 editor
12032 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
12033 .unwrap()
12034 });
12035 cx.assert_editor_state(&expected_with_insert_mode);
12036 handle_resolve_completion_request(&mut cx, None).await;
12037 apply_additional_edits.await.unwrap();
12038}
12039
12040#[gpui::test]
12041async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
12042 init_test(cx, |_| {});
12043 let mut cx = EditorLspTestContext::new_rust(
12044 lsp::ServerCapabilities {
12045 completion_provider: Some(lsp::CompletionOptions {
12046 resolve_provider: Some(true),
12047 ..Default::default()
12048 }),
12049 ..Default::default()
12050 },
12051 cx,
12052 )
12053 .await;
12054
12055 // scenario: surrounding text matches completion text
12056 let completion_text = "to_offset";
12057 let initial_state = indoc! {"
12058 1. buf.to_offˇsuffix
12059 2. buf.to_offˇsuf
12060 3. buf.to_offˇfix
12061 4. buf.to_offˇ
12062 5. into_offˇensive
12063 6. ˇsuffix
12064 7. let ˇ //
12065 8. aaˇzz
12066 9. buf.to_off«zzzzzˇ»suffix
12067 10. buf.«ˇzzzzz»suffix
12068 11. to_off«ˇzzzzz»
12069
12070 buf.to_offˇsuffix // newest cursor
12071 "};
12072 let completion_marked_buffer = indoc! {"
12073 1. buf.to_offsuffix
12074 2. buf.to_offsuf
12075 3. buf.to_offfix
12076 4. buf.to_off
12077 5. into_offensive
12078 6. suffix
12079 7. let //
12080 8. aazz
12081 9. buf.to_offzzzzzsuffix
12082 10. buf.zzzzzsuffix
12083 11. to_offzzzzz
12084
12085 buf.<to_off|suffix> // newest cursor
12086 "};
12087 let expected = indoc! {"
12088 1. buf.to_offsetˇ
12089 2. buf.to_offsetˇsuf
12090 3. buf.to_offsetˇfix
12091 4. buf.to_offsetˇ
12092 5. into_offsetˇensive
12093 6. to_offsetˇsuffix
12094 7. let to_offsetˇ //
12095 8. aato_offsetˇzz
12096 9. buf.to_offsetˇ
12097 10. buf.to_offsetˇsuffix
12098 11. to_offsetˇ
12099
12100 buf.to_offsetˇ // newest cursor
12101 "};
12102 cx.set_state(initial_state);
12103 cx.update_editor(|editor, window, cx| {
12104 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12105 });
12106 handle_completion_request_with_insert_and_replace(
12107 &mut cx,
12108 completion_marked_buffer,
12109 vec![(completion_text, completion_text)],
12110 Arc::new(AtomicUsize::new(0)),
12111 )
12112 .await;
12113 cx.condition(|editor, _| editor.context_menu_visible())
12114 .await;
12115 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12116 editor
12117 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12118 .unwrap()
12119 });
12120 cx.assert_editor_state(expected);
12121 handle_resolve_completion_request(&mut cx, None).await;
12122 apply_additional_edits.await.unwrap();
12123
12124 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
12125 let completion_text = "foo_and_bar";
12126 let initial_state = indoc! {"
12127 1. ooanbˇ
12128 2. zooanbˇ
12129 3. ooanbˇz
12130 4. zooanbˇz
12131 5. ooanˇ
12132 6. oanbˇ
12133
12134 ooanbˇ
12135 "};
12136 let completion_marked_buffer = indoc! {"
12137 1. ooanb
12138 2. zooanb
12139 3. ooanbz
12140 4. zooanbz
12141 5. ooan
12142 6. oanb
12143
12144 <ooanb|>
12145 "};
12146 let expected = indoc! {"
12147 1. foo_and_barˇ
12148 2. zfoo_and_barˇ
12149 3. foo_and_barˇz
12150 4. zfoo_and_barˇz
12151 5. ooanfoo_and_barˇ
12152 6. oanbfoo_and_barˇ
12153
12154 foo_and_barˇ
12155 "};
12156 cx.set_state(initial_state);
12157 cx.update_editor(|editor, window, cx| {
12158 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12159 });
12160 handle_completion_request_with_insert_and_replace(
12161 &mut cx,
12162 completion_marked_buffer,
12163 vec![(completion_text, completion_text)],
12164 Arc::new(AtomicUsize::new(0)),
12165 )
12166 .await;
12167 cx.condition(|editor, _| editor.context_menu_visible())
12168 .await;
12169 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12170 editor
12171 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12172 .unwrap()
12173 });
12174 cx.assert_editor_state(expected);
12175 handle_resolve_completion_request(&mut cx, None).await;
12176 apply_additional_edits.await.unwrap();
12177
12178 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
12179 // (expects the same as if it was inserted at the end)
12180 let completion_text = "foo_and_bar";
12181 let initial_state = indoc! {"
12182 1. ooˇanb
12183 2. zooˇanb
12184 3. ooˇanbz
12185 4. zooˇanbz
12186
12187 ooˇanb
12188 "};
12189 let completion_marked_buffer = indoc! {"
12190 1. ooanb
12191 2. zooanb
12192 3. ooanbz
12193 4. zooanbz
12194
12195 <oo|anb>
12196 "};
12197 let expected = indoc! {"
12198 1. foo_and_barˇ
12199 2. zfoo_and_barˇ
12200 3. foo_and_barˇz
12201 4. zfoo_and_barˇz
12202
12203 foo_and_barˇ
12204 "};
12205 cx.set_state(initial_state);
12206 cx.update_editor(|editor, window, cx| {
12207 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12208 });
12209 handle_completion_request_with_insert_and_replace(
12210 &mut cx,
12211 completion_marked_buffer,
12212 vec![(completion_text, completion_text)],
12213 Arc::new(AtomicUsize::new(0)),
12214 )
12215 .await;
12216 cx.condition(|editor, _| editor.context_menu_visible())
12217 .await;
12218 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12219 editor
12220 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12221 .unwrap()
12222 });
12223 cx.assert_editor_state(expected);
12224 handle_resolve_completion_request(&mut cx, None).await;
12225 apply_additional_edits.await.unwrap();
12226}
12227
12228// This used to crash
12229#[gpui::test]
12230async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
12231 init_test(cx, |_| {});
12232
12233 let buffer_text = indoc! {"
12234 fn main() {
12235 10.satu;
12236
12237 //
12238 // separate cursors so they open in different excerpts (manually reproducible)
12239 //
12240
12241 10.satu20;
12242 }
12243 "};
12244 let multibuffer_text_with_selections = indoc! {"
12245 fn main() {
12246 10.satuˇ;
12247
12248 //
12249
12250 //
12251
12252 10.satuˇ20;
12253 }
12254 "};
12255 let expected_multibuffer = indoc! {"
12256 fn main() {
12257 10.saturating_sub()ˇ;
12258
12259 //
12260
12261 //
12262
12263 10.saturating_sub()ˇ;
12264 }
12265 "};
12266
12267 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
12268 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
12269
12270 let fs = FakeFs::new(cx.executor());
12271 fs.insert_tree(
12272 path!("/a"),
12273 json!({
12274 "main.rs": buffer_text,
12275 }),
12276 )
12277 .await;
12278
12279 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12280 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12281 language_registry.add(rust_lang());
12282 let mut fake_servers = language_registry.register_fake_lsp(
12283 "Rust",
12284 FakeLspAdapter {
12285 capabilities: lsp::ServerCapabilities {
12286 completion_provider: Some(lsp::CompletionOptions {
12287 resolve_provider: None,
12288 ..lsp::CompletionOptions::default()
12289 }),
12290 ..lsp::ServerCapabilities::default()
12291 },
12292 ..FakeLspAdapter::default()
12293 },
12294 );
12295 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12296 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12297 let buffer = project
12298 .update(cx, |project, cx| {
12299 project.open_local_buffer(path!("/a/main.rs"), cx)
12300 })
12301 .await
12302 .unwrap();
12303
12304 let multi_buffer = cx.new(|cx| {
12305 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
12306 multi_buffer.push_excerpts(
12307 buffer.clone(),
12308 [ExcerptRange::new(0..first_excerpt_end)],
12309 cx,
12310 );
12311 multi_buffer.push_excerpts(
12312 buffer.clone(),
12313 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
12314 cx,
12315 );
12316 multi_buffer
12317 });
12318
12319 let editor = workspace
12320 .update(cx, |_, window, cx| {
12321 cx.new(|cx| {
12322 Editor::new(
12323 EditorMode::Full {
12324 scale_ui_elements_with_buffer_font_size: false,
12325 show_active_line_background: false,
12326 sized_by_content: false,
12327 },
12328 multi_buffer.clone(),
12329 Some(project.clone()),
12330 window,
12331 cx,
12332 )
12333 })
12334 })
12335 .unwrap();
12336
12337 let pane = workspace
12338 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12339 .unwrap();
12340 pane.update_in(cx, |pane, window, cx| {
12341 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
12342 });
12343
12344 let fake_server = fake_servers.next().await.unwrap();
12345
12346 editor.update_in(cx, |editor, window, cx| {
12347 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12348 s.select_ranges([
12349 Point::new(1, 11)..Point::new(1, 11),
12350 Point::new(7, 11)..Point::new(7, 11),
12351 ])
12352 });
12353
12354 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
12355 });
12356
12357 editor.update_in(cx, |editor, window, cx| {
12358 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12359 });
12360
12361 fake_server
12362 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12363 let completion_item = lsp::CompletionItem {
12364 label: "saturating_sub()".into(),
12365 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
12366 lsp::InsertReplaceEdit {
12367 new_text: "saturating_sub()".to_owned(),
12368 insert: lsp::Range::new(
12369 lsp::Position::new(7, 7),
12370 lsp::Position::new(7, 11),
12371 ),
12372 replace: lsp::Range::new(
12373 lsp::Position::new(7, 7),
12374 lsp::Position::new(7, 13),
12375 ),
12376 },
12377 )),
12378 ..lsp::CompletionItem::default()
12379 };
12380
12381 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
12382 })
12383 .next()
12384 .await
12385 .unwrap();
12386
12387 cx.condition(&editor, |editor, _| editor.context_menu_visible())
12388 .await;
12389
12390 editor
12391 .update_in(cx, |editor, window, cx| {
12392 editor
12393 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12394 .unwrap()
12395 })
12396 .await
12397 .unwrap();
12398
12399 editor.update(cx, |editor, cx| {
12400 assert_text_with_selections(editor, expected_multibuffer, cx);
12401 })
12402}
12403
12404#[gpui::test]
12405async fn test_completion(cx: &mut TestAppContext) {
12406 init_test(cx, |_| {});
12407
12408 let mut cx = EditorLspTestContext::new_rust(
12409 lsp::ServerCapabilities {
12410 completion_provider: Some(lsp::CompletionOptions {
12411 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12412 resolve_provider: Some(true),
12413 ..Default::default()
12414 }),
12415 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12416 ..Default::default()
12417 },
12418 cx,
12419 )
12420 .await;
12421 let counter = Arc::new(AtomicUsize::new(0));
12422
12423 cx.set_state(indoc! {"
12424 oneˇ
12425 two
12426 three
12427 "});
12428 cx.simulate_keystroke(".");
12429 handle_completion_request(
12430 indoc! {"
12431 one.|<>
12432 two
12433 three
12434 "},
12435 vec!["first_completion", "second_completion"],
12436 true,
12437 counter.clone(),
12438 &mut cx,
12439 )
12440 .await;
12441 cx.condition(|editor, _| editor.context_menu_visible())
12442 .await;
12443 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12444
12445 let _handler = handle_signature_help_request(
12446 &mut cx,
12447 lsp::SignatureHelp {
12448 signatures: vec![lsp::SignatureInformation {
12449 label: "test signature".to_string(),
12450 documentation: None,
12451 parameters: Some(vec![lsp::ParameterInformation {
12452 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
12453 documentation: None,
12454 }]),
12455 active_parameter: None,
12456 }],
12457 active_signature: None,
12458 active_parameter: None,
12459 },
12460 );
12461 cx.update_editor(|editor, window, cx| {
12462 assert!(
12463 !editor.signature_help_state.is_shown(),
12464 "No signature help was called for"
12465 );
12466 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12467 });
12468 cx.run_until_parked();
12469 cx.update_editor(|editor, _, _| {
12470 assert!(
12471 !editor.signature_help_state.is_shown(),
12472 "No signature help should be shown when completions menu is open"
12473 );
12474 });
12475
12476 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12477 editor.context_menu_next(&Default::default(), window, cx);
12478 editor
12479 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12480 .unwrap()
12481 });
12482 cx.assert_editor_state(indoc! {"
12483 one.second_completionˇ
12484 two
12485 three
12486 "});
12487
12488 handle_resolve_completion_request(
12489 &mut cx,
12490 Some(vec![
12491 (
12492 //This overlaps with the primary completion edit which is
12493 //misbehavior from the LSP spec, test that we filter it out
12494 indoc! {"
12495 one.second_ˇcompletion
12496 two
12497 threeˇ
12498 "},
12499 "overlapping additional edit",
12500 ),
12501 (
12502 indoc! {"
12503 one.second_completion
12504 two
12505 threeˇ
12506 "},
12507 "\nadditional edit",
12508 ),
12509 ]),
12510 )
12511 .await;
12512 apply_additional_edits.await.unwrap();
12513 cx.assert_editor_state(indoc! {"
12514 one.second_completionˇ
12515 two
12516 three
12517 additional edit
12518 "});
12519
12520 cx.set_state(indoc! {"
12521 one.second_completion
12522 twoˇ
12523 threeˇ
12524 additional edit
12525 "});
12526 cx.simulate_keystroke(" ");
12527 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12528 cx.simulate_keystroke("s");
12529 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12530
12531 cx.assert_editor_state(indoc! {"
12532 one.second_completion
12533 two sˇ
12534 three sˇ
12535 additional edit
12536 "});
12537 handle_completion_request(
12538 indoc! {"
12539 one.second_completion
12540 two s
12541 three <s|>
12542 additional edit
12543 "},
12544 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12545 true,
12546 counter.clone(),
12547 &mut cx,
12548 )
12549 .await;
12550 cx.condition(|editor, _| editor.context_menu_visible())
12551 .await;
12552 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12553
12554 cx.simulate_keystroke("i");
12555
12556 handle_completion_request(
12557 indoc! {"
12558 one.second_completion
12559 two si
12560 three <si|>
12561 additional edit
12562 "},
12563 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12564 true,
12565 counter.clone(),
12566 &mut cx,
12567 )
12568 .await;
12569 cx.condition(|editor, _| editor.context_menu_visible())
12570 .await;
12571 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12572
12573 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12574 editor
12575 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12576 .unwrap()
12577 });
12578 cx.assert_editor_state(indoc! {"
12579 one.second_completion
12580 two sixth_completionˇ
12581 three sixth_completionˇ
12582 additional edit
12583 "});
12584
12585 apply_additional_edits.await.unwrap();
12586
12587 update_test_language_settings(&mut cx, |settings| {
12588 settings.defaults.show_completions_on_input = Some(false);
12589 });
12590 cx.set_state("editorˇ");
12591 cx.simulate_keystroke(".");
12592 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12593 cx.simulate_keystrokes("c l o");
12594 cx.assert_editor_state("editor.cloˇ");
12595 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12596 cx.update_editor(|editor, window, cx| {
12597 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12598 });
12599 handle_completion_request(
12600 "editor.<clo|>",
12601 vec!["close", "clobber"],
12602 true,
12603 counter.clone(),
12604 &mut cx,
12605 )
12606 .await;
12607 cx.condition(|editor, _| editor.context_menu_visible())
12608 .await;
12609 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
12610
12611 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12612 editor
12613 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12614 .unwrap()
12615 });
12616 cx.assert_editor_state("editor.clobberˇ");
12617 handle_resolve_completion_request(&mut cx, None).await;
12618 apply_additional_edits.await.unwrap();
12619}
12620
12621#[gpui::test]
12622async fn test_completion_reuse(cx: &mut TestAppContext) {
12623 init_test(cx, |_| {});
12624
12625 let mut cx = EditorLspTestContext::new_rust(
12626 lsp::ServerCapabilities {
12627 completion_provider: Some(lsp::CompletionOptions {
12628 trigger_characters: Some(vec![".".to_string()]),
12629 ..Default::default()
12630 }),
12631 ..Default::default()
12632 },
12633 cx,
12634 )
12635 .await;
12636
12637 let counter = Arc::new(AtomicUsize::new(0));
12638 cx.set_state("objˇ");
12639 cx.simulate_keystroke(".");
12640
12641 // Initial completion request returns complete results
12642 let is_incomplete = false;
12643 handle_completion_request(
12644 "obj.|<>",
12645 vec!["a", "ab", "abc"],
12646 is_incomplete,
12647 counter.clone(),
12648 &mut cx,
12649 )
12650 .await;
12651 cx.run_until_parked();
12652 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12653 cx.assert_editor_state("obj.ˇ");
12654 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12655
12656 // Type "a" - filters existing completions
12657 cx.simulate_keystroke("a");
12658 cx.run_until_parked();
12659 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12660 cx.assert_editor_state("obj.aˇ");
12661 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12662
12663 // Type "b" - filters existing completions
12664 cx.simulate_keystroke("b");
12665 cx.run_until_parked();
12666 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12667 cx.assert_editor_state("obj.abˇ");
12668 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12669
12670 // Type "c" - filters existing completions
12671 cx.simulate_keystroke("c");
12672 cx.run_until_parked();
12673 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12674 cx.assert_editor_state("obj.abcˇ");
12675 check_displayed_completions(vec!["abc"], &mut cx);
12676
12677 // Backspace to delete "c" - filters existing completions
12678 cx.update_editor(|editor, window, cx| {
12679 editor.backspace(&Backspace, window, cx);
12680 });
12681 cx.run_until_parked();
12682 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12683 cx.assert_editor_state("obj.abˇ");
12684 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12685
12686 // Moving cursor to the left dismisses menu.
12687 cx.update_editor(|editor, window, cx| {
12688 editor.move_left(&MoveLeft, window, cx);
12689 });
12690 cx.run_until_parked();
12691 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12692 cx.assert_editor_state("obj.aˇb");
12693 cx.update_editor(|editor, _, _| {
12694 assert_eq!(editor.context_menu_visible(), false);
12695 });
12696
12697 // Type "b" - new request
12698 cx.simulate_keystroke("b");
12699 let is_incomplete = false;
12700 handle_completion_request(
12701 "obj.<ab|>a",
12702 vec!["ab", "abc"],
12703 is_incomplete,
12704 counter.clone(),
12705 &mut cx,
12706 )
12707 .await;
12708 cx.run_until_parked();
12709 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12710 cx.assert_editor_state("obj.abˇb");
12711 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12712
12713 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
12714 cx.update_editor(|editor, window, cx| {
12715 editor.backspace(&Backspace, window, cx);
12716 });
12717 let is_incomplete = false;
12718 handle_completion_request(
12719 "obj.<a|>b",
12720 vec!["a", "ab", "abc"],
12721 is_incomplete,
12722 counter.clone(),
12723 &mut cx,
12724 )
12725 .await;
12726 cx.run_until_parked();
12727 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12728 cx.assert_editor_state("obj.aˇb");
12729 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12730
12731 // Backspace to delete "a" - dismisses menu.
12732 cx.update_editor(|editor, window, cx| {
12733 editor.backspace(&Backspace, window, cx);
12734 });
12735 cx.run_until_parked();
12736 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12737 cx.assert_editor_state("obj.ˇb");
12738 cx.update_editor(|editor, _, _| {
12739 assert_eq!(editor.context_menu_visible(), false);
12740 });
12741}
12742
12743#[gpui::test]
12744async fn test_word_completion(cx: &mut TestAppContext) {
12745 let lsp_fetch_timeout_ms = 10;
12746 init_test(cx, |language_settings| {
12747 language_settings.defaults.completions = Some(CompletionSettings {
12748 words: WordsCompletionMode::Fallback,
12749 lsp: true,
12750 lsp_fetch_timeout_ms: 10,
12751 lsp_insert_mode: LspInsertMode::Insert,
12752 });
12753 });
12754
12755 let mut cx = EditorLspTestContext::new_rust(
12756 lsp::ServerCapabilities {
12757 completion_provider: Some(lsp::CompletionOptions {
12758 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12759 ..lsp::CompletionOptions::default()
12760 }),
12761 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12762 ..lsp::ServerCapabilities::default()
12763 },
12764 cx,
12765 )
12766 .await;
12767
12768 let throttle_completions = Arc::new(AtomicBool::new(false));
12769
12770 let lsp_throttle_completions = throttle_completions.clone();
12771 let _completion_requests_handler =
12772 cx.lsp
12773 .server
12774 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
12775 let lsp_throttle_completions = lsp_throttle_completions.clone();
12776 let cx = cx.clone();
12777 async move {
12778 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
12779 cx.background_executor()
12780 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
12781 .await;
12782 }
12783 Ok(Some(lsp::CompletionResponse::Array(vec![
12784 lsp::CompletionItem {
12785 label: "first".into(),
12786 ..lsp::CompletionItem::default()
12787 },
12788 lsp::CompletionItem {
12789 label: "last".into(),
12790 ..lsp::CompletionItem::default()
12791 },
12792 ])))
12793 }
12794 });
12795
12796 cx.set_state(indoc! {"
12797 oneˇ
12798 two
12799 three
12800 "});
12801 cx.simulate_keystroke(".");
12802 cx.executor().run_until_parked();
12803 cx.condition(|editor, _| editor.context_menu_visible())
12804 .await;
12805 cx.update_editor(|editor, window, cx| {
12806 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12807 {
12808 assert_eq!(
12809 completion_menu_entries(&menu),
12810 &["first", "last"],
12811 "When LSP server is fast to reply, no fallback word completions are used"
12812 );
12813 } else {
12814 panic!("expected completion menu to be open");
12815 }
12816 editor.cancel(&Cancel, window, cx);
12817 });
12818 cx.executor().run_until_parked();
12819 cx.condition(|editor, _| !editor.context_menu_visible())
12820 .await;
12821
12822 throttle_completions.store(true, atomic::Ordering::Release);
12823 cx.simulate_keystroke(".");
12824 cx.executor()
12825 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
12826 cx.executor().run_until_parked();
12827 cx.condition(|editor, _| editor.context_menu_visible())
12828 .await;
12829 cx.update_editor(|editor, _, _| {
12830 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12831 {
12832 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
12833 "When LSP server is slow, document words can be shown instead, if configured accordingly");
12834 } else {
12835 panic!("expected completion menu to be open");
12836 }
12837 });
12838}
12839
12840#[gpui::test]
12841async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
12842 init_test(cx, |language_settings| {
12843 language_settings.defaults.completions = Some(CompletionSettings {
12844 words: WordsCompletionMode::Enabled,
12845 lsp: true,
12846 lsp_fetch_timeout_ms: 0,
12847 lsp_insert_mode: LspInsertMode::Insert,
12848 });
12849 });
12850
12851 let mut cx = EditorLspTestContext::new_rust(
12852 lsp::ServerCapabilities {
12853 completion_provider: Some(lsp::CompletionOptions {
12854 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12855 ..lsp::CompletionOptions::default()
12856 }),
12857 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12858 ..lsp::ServerCapabilities::default()
12859 },
12860 cx,
12861 )
12862 .await;
12863
12864 let _completion_requests_handler =
12865 cx.lsp
12866 .server
12867 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12868 Ok(Some(lsp::CompletionResponse::Array(vec![
12869 lsp::CompletionItem {
12870 label: "first".into(),
12871 ..lsp::CompletionItem::default()
12872 },
12873 lsp::CompletionItem {
12874 label: "last".into(),
12875 ..lsp::CompletionItem::default()
12876 },
12877 ])))
12878 });
12879
12880 cx.set_state(indoc! {"ˇ
12881 first
12882 last
12883 second
12884 "});
12885 cx.simulate_keystroke(".");
12886 cx.executor().run_until_parked();
12887 cx.condition(|editor, _| editor.context_menu_visible())
12888 .await;
12889 cx.update_editor(|editor, _, _| {
12890 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12891 {
12892 assert_eq!(
12893 completion_menu_entries(&menu),
12894 &["first", "last", "second"],
12895 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
12896 );
12897 } else {
12898 panic!("expected completion menu to be open");
12899 }
12900 });
12901}
12902
12903#[gpui::test]
12904async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
12905 init_test(cx, |language_settings| {
12906 language_settings.defaults.completions = Some(CompletionSettings {
12907 words: WordsCompletionMode::Disabled,
12908 lsp: true,
12909 lsp_fetch_timeout_ms: 0,
12910 lsp_insert_mode: LspInsertMode::Insert,
12911 });
12912 });
12913
12914 let mut cx = EditorLspTestContext::new_rust(
12915 lsp::ServerCapabilities {
12916 completion_provider: Some(lsp::CompletionOptions {
12917 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12918 ..lsp::CompletionOptions::default()
12919 }),
12920 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12921 ..lsp::ServerCapabilities::default()
12922 },
12923 cx,
12924 )
12925 .await;
12926
12927 let _completion_requests_handler =
12928 cx.lsp
12929 .server
12930 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12931 panic!("LSP completions should not be queried when dealing with word completions")
12932 });
12933
12934 cx.set_state(indoc! {"ˇ
12935 first
12936 last
12937 second
12938 "});
12939 cx.update_editor(|editor, window, cx| {
12940 editor.show_word_completions(&ShowWordCompletions, window, cx);
12941 });
12942 cx.executor().run_until_parked();
12943 cx.condition(|editor, _| editor.context_menu_visible())
12944 .await;
12945 cx.update_editor(|editor, _, _| {
12946 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12947 {
12948 assert_eq!(
12949 completion_menu_entries(&menu),
12950 &["first", "last", "second"],
12951 "`ShowWordCompletions` action should show word completions"
12952 );
12953 } else {
12954 panic!("expected completion menu to be open");
12955 }
12956 });
12957
12958 cx.simulate_keystroke("l");
12959 cx.executor().run_until_parked();
12960 cx.condition(|editor, _| editor.context_menu_visible())
12961 .await;
12962 cx.update_editor(|editor, _, _| {
12963 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12964 {
12965 assert_eq!(
12966 completion_menu_entries(&menu),
12967 &["last"],
12968 "After showing word completions, further editing should filter them and not query the LSP"
12969 );
12970 } else {
12971 panic!("expected completion menu to be open");
12972 }
12973 });
12974}
12975
12976#[gpui::test]
12977async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
12978 init_test(cx, |language_settings| {
12979 language_settings.defaults.completions = Some(CompletionSettings {
12980 words: WordsCompletionMode::Fallback,
12981 lsp: false,
12982 lsp_fetch_timeout_ms: 0,
12983 lsp_insert_mode: LspInsertMode::Insert,
12984 });
12985 });
12986
12987 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12988
12989 cx.set_state(indoc! {"ˇ
12990 0_usize
12991 let
12992 33
12993 4.5f32
12994 "});
12995 cx.update_editor(|editor, window, cx| {
12996 editor.show_completions(&ShowCompletions::default(), window, cx);
12997 });
12998 cx.executor().run_until_parked();
12999 cx.condition(|editor, _| editor.context_menu_visible())
13000 .await;
13001 cx.update_editor(|editor, window, cx| {
13002 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13003 {
13004 assert_eq!(
13005 completion_menu_entries(&menu),
13006 &["let"],
13007 "With no digits in the completion query, no digits should be in the word completions"
13008 );
13009 } else {
13010 panic!("expected completion menu to be open");
13011 }
13012 editor.cancel(&Cancel, window, cx);
13013 });
13014
13015 cx.set_state(indoc! {"3ˇ
13016 0_usize
13017 let
13018 3
13019 33.35f32
13020 "});
13021 cx.update_editor(|editor, window, cx| {
13022 editor.show_completions(&ShowCompletions::default(), window, cx);
13023 });
13024 cx.executor().run_until_parked();
13025 cx.condition(|editor, _| editor.context_menu_visible())
13026 .await;
13027 cx.update_editor(|editor, _, _| {
13028 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13029 {
13030 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
13031 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
13032 } else {
13033 panic!("expected completion menu to be open");
13034 }
13035 });
13036}
13037
13038fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
13039 let position = || lsp::Position {
13040 line: params.text_document_position.position.line,
13041 character: params.text_document_position.position.character,
13042 };
13043 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13044 range: lsp::Range {
13045 start: position(),
13046 end: position(),
13047 },
13048 new_text: text.to_string(),
13049 }))
13050}
13051
13052#[gpui::test]
13053async fn test_multiline_completion(cx: &mut TestAppContext) {
13054 init_test(cx, |_| {});
13055
13056 let fs = FakeFs::new(cx.executor());
13057 fs.insert_tree(
13058 path!("/a"),
13059 json!({
13060 "main.ts": "a",
13061 }),
13062 )
13063 .await;
13064
13065 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13066 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13067 let typescript_language = Arc::new(Language::new(
13068 LanguageConfig {
13069 name: "TypeScript".into(),
13070 matcher: LanguageMatcher {
13071 path_suffixes: vec!["ts".to_string()],
13072 ..LanguageMatcher::default()
13073 },
13074 line_comments: vec!["// ".into()],
13075 ..LanguageConfig::default()
13076 },
13077 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13078 ));
13079 language_registry.add(typescript_language.clone());
13080 let mut fake_servers = language_registry.register_fake_lsp(
13081 "TypeScript",
13082 FakeLspAdapter {
13083 capabilities: lsp::ServerCapabilities {
13084 completion_provider: Some(lsp::CompletionOptions {
13085 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13086 ..lsp::CompletionOptions::default()
13087 }),
13088 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13089 ..lsp::ServerCapabilities::default()
13090 },
13091 // Emulate vtsls label generation
13092 label_for_completion: Some(Box::new(|item, _| {
13093 let text = if let Some(description) = item
13094 .label_details
13095 .as_ref()
13096 .and_then(|label_details| label_details.description.as_ref())
13097 {
13098 format!("{} {}", item.label, description)
13099 } else if let Some(detail) = &item.detail {
13100 format!("{} {}", item.label, detail)
13101 } else {
13102 item.label.clone()
13103 };
13104 let len = text.len();
13105 Some(language::CodeLabel {
13106 text,
13107 runs: Vec::new(),
13108 filter_range: 0..len,
13109 })
13110 })),
13111 ..FakeLspAdapter::default()
13112 },
13113 );
13114 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13115 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13116 let worktree_id = workspace
13117 .update(cx, |workspace, _window, cx| {
13118 workspace.project().update(cx, |project, cx| {
13119 project.worktrees(cx).next().unwrap().read(cx).id()
13120 })
13121 })
13122 .unwrap();
13123 let _buffer = project
13124 .update(cx, |project, cx| {
13125 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
13126 })
13127 .await
13128 .unwrap();
13129 let editor = workspace
13130 .update(cx, |workspace, window, cx| {
13131 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
13132 })
13133 .unwrap()
13134 .await
13135 .unwrap()
13136 .downcast::<Editor>()
13137 .unwrap();
13138 let fake_server = fake_servers.next().await.unwrap();
13139
13140 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
13141 let multiline_label_2 = "a\nb\nc\n";
13142 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
13143 let multiline_description = "d\ne\nf\n";
13144 let multiline_detail_2 = "g\nh\ni\n";
13145
13146 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
13147 move |params, _| async move {
13148 Ok(Some(lsp::CompletionResponse::Array(vec![
13149 lsp::CompletionItem {
13150 label: multiline_label.to_string(),
13151 text_edit: gen_text_edit(¶ms, "new_text_1"),
13152 ..lsp::CompletionItem::default()
13153 },
13154 lsp::CompletionItem {
13155 label: "single line label 1".to_string(),
13156 detail: Some(multiline_detail.to_string()),
13157 text_edit: gen_text_edit(¶ms, "new_text_2"),
13158 ..lsp::CompletionItem::default()
13159 },
13160 lsp::CompletionItem {
13161 label: "single line label 2".to_string(),
13162 label_details: Some(lsp::CompletionItemLabelDetails {
13163 description: Some(multiline_description.to_string()),
13164 detail: None,
13165 }),
13166 text_edit: gen_text_edit(¶ms, "new_text_2"),
13167 ..lsp::CompletionItem::default()
13168 },
13169 lsp::CompletionItem {
13170 label: multiline_label_2.to_string(),
13171 detail: Some(multiline_detail_2.to_string()),
13172 text_edit: gen_text_edit(¶ms, "new_text_3"),
13173 ..lsp::CompletionItem::default()
13174 },
13175 lsp::CompletionItem {
13176 label: "Label with many spaces and \t but without newlines".to_string(),
13177 detail: Some(
13178 "Details with many spaces and \t but without newlines".to_string(),
13179 ),
13180 text_edit: gen_text_edit(¶ms, "new_text_4"),
13181 ..lsp::CompletionItem::default()
13182 },
13183 ])))
13184 },
13185 );
13186
13187 editor.update_in(cx, |editor, window, cx| {
13188 cx.focus_self(window);
13189 editor.move_to_end(&MoveToEnd, window, cx);
13190 editor.handle_input(".", window, cx);
13191 });
13192 cx.run_until_parked();
13193 completion_handle.next().await.unwrap();
13194
13195 editor.update(cx, |editor, _| {
13196 assert!(editor.context_menu_visible());
13197 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13198 {
13199 let completion_labels = menu
13200 .completions
13201 .borrow()
13202 .iter()
13203 .map(|c| c.label.text.clone())
13204 .collect::<Vec<_>>();
13205 assert_eq!(
13206 completion_labels,
13207 &[
13208 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
13209 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
13210 "single line label 2 d e f ",
13211 "a b c g h i ",
13212 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
13213 ],
13214 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
13215 );
13216
13217 for completion in menu
13218 .completions
13219 .borrow()
13220 .iter() {
13221 assert_eq!(
13222 completion.label.filter_range,
13223 0..completion.label.text.len(),
13224 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
13225 );
13226 }
13227 } else {
13228 panic!("expected completion menu to be open");
13229 }
13230 });
13231}
13232
13233#[gpui::test]
13234async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
13235 init_test(cx, |_| {});
13236 let mut cx = EditorLspTestContext::new_rust(
13237 lsp::ServerCapabilities {
13238 completion_provider: Some(lsp::CompletionOptions {
13239 trigger_characters: Some(vec![".".to_string()]),
13240 ..Default::default()
13241 }),
13242 ..Default::default()
13243 },
13244 cx,
13245 )
13246 .await;
13247 cx.lsp
13248 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13249 Ok(Some(lsp::CompletionResponse::Array(vec![
13250 lsp::CompletionItem {
13251 label: "first".into(),
13252 ..Default::default()
13253 },
13254 lsp::CompletionItem {
13255 label: "last".into(),
13256 ..Default::default()
13257 },
13258 ])))
13259 });
13260 cx.set_state("variableˇ");
13261 cx.simulate_keystroke(".");
13262 cx.executor().run_until_parked();
13263
13264 cx.update_editor(|editor, _, _| {
13265 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13266 {
13267 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
13268 } else {
13269 panic!("expected completion menu to be open");
13270 }
13271 });
13272
13273 cx.update_editor(|editor, window, cx| {
13274 editor.move_page_down(&MovePageDown::default(), window, cx);
13275 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13276 {
13277 assert!(
13278 menu.selected_item == 1,
13279 "expected PageDown to select the last item from the context menu"
13280 );
13281 } else {
13282 panic!("expected completion menu to stay open after PageDown");
13283 }
13284 });
13285
13286 cx.update_editor(|editor, window, cx| {
13287 editor.move_page_up(&MovePageUp::default(), window, cx);
13288 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13289 {
13290 assert!(
13291 menu.selected_item == 0,
13292 "expected PageUp to select the first item from the context menu"
13293 );
13294 } else {
13295 panic!("expected completion menu to stay open after PageUp");
13296 }
13297 });
13298}
13299
13300#[gpui::test]
13301async fn test_as_is_completions(cx: &mut TestAppContext) {
13302 init_test(cx, |_| {});
13303 let mut cx = EditorLspTestContext::new_rust(
13304 lsp::ServerCapabilities {
13305 completion_provider: Some(lsp::CompletionOptions {
13306 ..Default::default()
13307 }),
13308 ..Default::default()
13309 },
13310 cx,
13311 )
13312 .await;
13313 cx.lsp
13314 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13315 Ok(Some(lsp::CompletionResponse::Array(vec![
13316 lsp::CompletionItem {
13317 label: "unsafe".into(),
13318 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13319 range: lsp::Range {
13320 start: lsp::Position {
13321 line: 1,
13322 character: 2,
13323 },
13324 end: lsp::Position {
13325 line: 1,
13326 character: 3,
13327 },
13328 },
13329 new_text: "unsafe".to_string(),
13330 })),
13331 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
13332 ..Default::default()
13333 },
13334 ])))
13335 });
13336 cx.set_state("fn a() {}\n nˇ");
13337 cx.executor().run_until_parked();
13338 cx.update_editor(|editor, window, cx| {
13339 editor.show_completions(
13340 &ShowCompletions {
13341 trigger: Some("\n".into()),
13342 },
13343 window,
13344 cx,
13345 );
13346 });
13347 cx.executor().run_until_parked();
13348
13349 cx.update_editor(|editor, window, cx| {
13350 editor.confirm_completion(&Default::default(), window, cx)
13351 });
13352 cx.executor().run_until_parked();
13353 cx.assert_editor_state("fn a() {}\n unsafeˇ");
13354}
13355
13356#[gpui::test]
13357async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
13358 init_test(cx, |_| {});
13359
13360 let mut cx = EditorLspTestContext::new_rust(
13361 lsp::ServerCapabilities {
13362 completion_provider: Some(lsp::CompletionOptions {
13363 trigger_characters: Some(vec![".".to_string()]),
13364 resolve_provider: Some(true),
13365 ..Default::default()
13366 }),
13367 ..Default::default()
13368 },
13369 cx,
13370 )
13371 .await;
13372
13373 cx.set_state("fn main() { let a = 2ˇ; }");
13374 cx.simulate_keystroke(".");
13375 let completion_item = lsp::CompletionItem {
13376 label: "Some".into(),
13377 kind: Some(lsp::CompletionItemKind::SNIPPET),
13378 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13379 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13380 kind: lsp::MarkupKind::Markdown,
13381 value: "```rust\nSome(2)\n```".to_string(),
13382 })),
13383 deprecated: Some(false),
13384 sort_text: Some("Some".to_string()),
13385 filter_text: Some("Some".to_string()),
13386 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13387 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13388 range: lsp::Range {
13389 start: lsp::Position {
13390 line: 0,
13391 character: 22,
13392 },
13393 end: lsp::Position {
13394 line: 0,
13395 character: 22,
13396 },
13397 },
13398 new_text: "Some(2)".to_string(),
13399 })),
13400 additional_text_edits: Some(vec![lsp::TextEdit {
13401 range: lsp::Range {
13402 start: lsp::Position {
13403 line: 0,
13404 character: 20,
13405 },
13406 end: lsp::Position {
13407 line: 0,
13408 character: 22,
13409 },
13410 },
13411 new_text: "".to_string(),
13412 }]),
13413 ..Default::default()
13414 };
13415
13416 let closure_completion_item = completion_item.clone();
13417 let counter = Arc::new(AtomicUsize::new(0));
13418 let counter_clone = counter.clone();
13419 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13420 let task_completion_item = closure_completion_item.clone();
13421 counter_clone.fetch_add(1, atomic::Ordering::Release);
13422 async move {
13423 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13424 is_incomplete: true,
13425 item_defaults: None,
13426 items: vec![task_completion_item],
13427 })))
13428 }
13429 });
13430
13431 cx.condition(|editor, _| editor.context_menu_visible())
13432 .await;
13433 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
13434 assert!(request.next().await.is_some());
13435 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13436
13437 cx.simulate_keystrokes("S o m");
13438 cx.condition(|editor, _| editor.context_menu_visible())
13439 .await;
13440 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
13441 assert!(request.next().await.is_some());
13442 assert!(request.next().await.is_some());
13443 assert!(request.next().await.is_some());
13444 request.close();
13445 assert!(request.next().await.is_none());
13446 assert_eq!(
13447 counter.load(atomic::Ordering::Acquire),
13448 4,
13449 "With the completions menu open, only one LSP request should happen per input"
13450 );
13451}
13452
13453#[gpui::test]
13454async fn test_toggle_comment(cx: &mut TestAppContext) {
13455 init_test(cx, |_| {});
13456 let mut cx = EditorTestContext::new(cx).await;
13457 let language = Arc::new(Language::new(
13458 LanguageConfig {
13459 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13460 ..Default::default()
13461 },
13462 Some(tree_sitter_rust::LANGUAGE.into()),
13463 ));
13464 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13465
13466 // If multiple selections intersect a line, the line is only toggled once.
13467 cx.set_state(indoc! {"
13468 fn a() {
13469 «//b();
13470 ˇ»// «c();
13471 //ˇ» d();
13472 }
13473 "});
13474
13475 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13476
13477 cx.assert_editor_state(indoc! {"
13478 fn a() {
13479 «b();
13480 c();
13481 ˇ» d();
13482 }
13483 "});
13484
13485 // The comment prefix is inserted at the same column for every line in a
13486 // selection.
13487 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13488
13489 cx.assert_editor_state(indoc! {"
13490 fn a() {
13491 // «b();
13492 // c();
13493 ˇ»// d();
13494 }
13495 "});
13496
13497 // If a selection ends at the beginning of a line, that line is not toggled.
13498 cx.set_selections_state(indoc! {"
13499 fn a() {
13500 // b();
13501 «// c();
13502 ˇ» // d();
13503 }
13504 "});
13505
13506 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13507
13508 cx.assert_editor_state(indoc! {"
13509 fn a() {
13510 // b();
13511 «c();
13512 ˇ» // d();
13513 }
13514 "});
13515
13516 // If a selection span a single line and is empty, the line is toggled.
13517 cx.set_state(indoc! {"
13518 fn a() {
13519 a();
13520 b();
13521 ˇ
13522 }
13523 "});
13524
13525 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13526
13527 cx.assert_editor_state(indoc! {"
13528 fn a() {
13529 a();
13530 b();
13531 //•ˇ
13532 }
13533 "});
13534
13535 // If a selection span multiple lines, empty lines are not toggled.
13536 cx.set_state(indoc! {"
13537 fn a() {
13538 «a();
13539
13540 c();ˇ»
13541 }
13542 "});
13543
13544 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13545
13546 cx.assert_editor_state(indoc! {"
13547 fn a() {
13548 // «a();
13549
13550 // c();ˇ»
13551 }
13552 "});
13553
13554 // If a selection includes multiple comment prefixes, all lines are uncommented.
13555 cx.set_state(indoc! {"
13556 fn a() {
13557 «// a();
13558 /// b();
13559 //! c();ˇ»
13560 }
13561 "});
13562
13563 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13564
13565 cx.assert_editor_state(indoc! {"
13566 fn a() {
13567 «a();
13568 b();
13569 c();ˇ»
13570 }
13571 "});
13572}
13573
13574#[gpui::test]
13575async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
13576 init_test(cx, |_| {});
13577 let mut cx = EditorTestContext::new(cx).await;
13578 let language = Arc::new(Language::new(
13579 LanguageConfig {
13580 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13581 ..Default::default()
13582 },
13583 Some(tree_sitter_rust::LANGUAGE.into()),
13584 ));
13585 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13586
13587 let toggle_comments = &ToggleComments {
13588 advance_downwards: false,
13589 ignore_indent: true,
13590 };
13591
13592 // If multiple selections intersect a line, the line is only toggled once.
13593 cx.set_state(indoc! {"
13594 fn a() {
13595 // «b();
13596 // c();
13597 // ˇ» d();
13598 }
13599 "});
13600
13601 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13602
13603 cx.assert_editor_state(indoc! {"
13604 fn a() {
13605 «b();
13606 c();
13607 ˇ» d();
13608 }
13609 "});
13610
13611 // The comment prefix is inserted at the beginning of each line
13612 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13613
13614 cx.assert_editor_state(indoc! {"
13615 fn a() {
13616 // «b();
13617 // c();
13618 // ˇ» d();
13619 }
13620 "});
13621
13622 // If a selection ends at the beginning of a line, that line is not toggled.
13623 cx.set_selections_state(indoc! {"
13624 fn a() {
13625 // b();
13626 // «c();
13627 ˇ»// d();
13628 }
13629 "});
13630
13631 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13632
13633 cx.assert_editor_state(indoc! {"
13634 fn a() {
13635 // b();
13636 «c();
13637 ˇ»// d();
13638 }
13639 "});
13640
13641 // If a selection span a single line and is empty, the line is toggled.
13642 cx.set_state(indoc! {"
13643 fn a() {
13644 a();
13645 b();
13646 ˇ
13647 }
13648 "});
13649
13650 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13651
13652 cx.assert_editor_state(indoc! {"
13653 fn a() {
13654 a();
13655 b();
13656 //ˇ
13657 }
13658 "});
13659
13660 // If a selection span multiple lines, empty lines are not toggled.
13661 cx.set_state(indoc! {"
13662 fn a() {
13663 «a();
13664
13665 c();ˇ»
13666 }
13667 "});
13668
13669 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13670
13671 cx.assert_editor_state(indoc! {"
13672 fn a() {
13673 // «a();
13674
13675 // c();ˇ»
13676 }
13677 "});
13678
13679 // If a selection includes multiple comment prefixes, all lines are uncommented.
13680 cx.set_state(indoc! {"
13681 fn a() {
13682 // «a();
13683 /// b();
13684 //! c();ˇ»
13685 }
13686 "});
13687
13688 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13689
13690 cx.assert_editor_state(indoc! {"
13691 fn a() {
13692 «a();
13693 b();
13694 c();ˇ»
13695 }
13696 "});
13697}
13698
13699#[gpui::test]
13700async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
13701 init_test(cx, |_| {});
13702
13703 let language = Arc::new(Language::new(
13704 LanguageConfig {
13705 line_comments: vec!["// ".into()],
13706 ..Default::default()
13707 },
13708 Some(tree_sitter_rust::LANGUAGE.into()),
13709 ));
13710
13711 let mut cx = EditorTestContext::new(cx).await;
13712
13713 cx.language_registry().add(language.clone());
13714 cx.update_buffer(|buffer, cx| {
13715 buffer.set_language(Some(language), cx);
13716 });
13717
13718 let toggle_comments = &ToggleComments {
13719 advance_downwards: true,
13720 ignore_indent: false,
13721 };
13722
13723 // Single cursor on one line -> advance
13724 // Cursor moves horizontally 3 characters as well on non-blank line
13725 cx.set_state(indoc!(
13726 "fn a() {
13727 ˇdog();
13728 cat();
13729 }"
13730 ));
13731 cx.update_editor(|editor, window, cx| {
13732 editor.toggle_comments(toggle_comments, window, cx);
13733 });
13734 cx.assert_editor_state(indoc!(
13735 "fn a() {
13736 // dog();
13737 catˇ();
13738 }"
13739 ));
13740
13741 // Single selection on one line -> don't advance
13742 cx.set_state(indoc!(
13743 "fn a() {
13744 «dog()ˇ»;
13745 cat();
13746 }"
13747 ));
13748 cx.update_editor(|editor, window, cx| {
13749 editor.toggle_comments(toggle_comments, window, cx);
13750 });
13751 cx.assert_editor_state(indoc!(
13752 "fn a() {
13753 // «dog()ˇ»;
13754 cat();
13755 }"
13756 ));
13757
13758 // Multiple cursors on one line -> advance
13759 cx.set_state(indoc!(
13760 "fn a() {
13761 ˇdˇog();
13762 cat();
13763 }"
13764 ));
13765 cx.update_editor(|editor, window, cx| {
13766 editor.toggle_comments(toggle_comments, window, cx);
13767 });
13768 cx.assert_editor_state(indoc!(
13769 "fn a() {
13770 // dog();
13771 catˇ(ˇ);
13772 }"
13773 ));
13774
13775 // Multiple cursors on one line, with selection -> don't advance
13776 cx.set_state(indoc!(
13777 "fn a() {
13778 ˇdˇog«()ˇ»;
13779 cat();
13780 }"
13781 ));
13782 cx.update_editor(|editor, window, cx| {
13783 editor.toggle_comments(toggle_comments, window, cx);
13784 });
13785 cx.assert_editor_state(indoc!(
13786 "fn a() {
13787 // ˇdˇog«()ˇ»;
13788 cat();
13789 }"
13790 ));
13791
13792 // Single cursor on one line -> advance
13793 // Cursor moves to column 0 on blank line
13794 cx.set_state(indoc!(
13795 "fn a() {
13796 ˇdog();
13797
13798 cat();
13799 }"
13800 ));
13801 cx.update_editor(|editor, window, cx| {
13802 editor.toggle_comments(toggle_comments, window, cx);
13803 });
13804 cx.assert_editor_state(indoc!(
13805 "fn a() {
13806 // dog();
13807 ˇ
13808 cat();
13809 }"
13810 ));
13811
13812 // Single cursor on one line -> advance
13813 // Cursor starts and ends at column 0
13814 cx.set_state(indoc!(
13815 "fn a() {
13816 ˇ dog();
13817 cat();
13818 }"
13819 ));
13820 cx.update_editor(|editor, window, cx| {
13821 editor.toggle_comments(toggle_comments, window, cx);
13822 });
13823 cx.assert_editor_state(indoc!(
13824 "fn a() {
13825 // dog();
13826 ˇ cat();
13827 }"
13828 ));
13829}
13830
13831#[gpui::test]
13832async fn test_toggle_block_comment(cx: &mut TestAppContext) {
13833 init_test(cx, |_| {});
13834
13835 let mut cx = EditorTestContext::new(cx).await;
13836
13837 let html_language = Arc::new(
13838 Language::new(
13839 LanguageConfig {
13840 name: "HTML".into(),
13841 block_comment: Some(BlockCommentConfig {
13842 start: "<!-- ".into(),
13843 prefix: "".into(),
13844 end: " -->".into(),
13845 tab_size: 0,
13846 }),
13847 ..Default::default()
13848 },
13849 Some(tree_sitter_html::LANGUAGE.into()),
13850 )
13851 .with_injection_query(
13852 r#"
13853 (script_element
13854 (raw_text) @injection.content
13855 (#set! injection.language "javascript"))
13856 "#,
13857 )
13858 .unwrap(),
13859 );
13860
13861 let javascript_language = Arc::new(Language::new(
13862 LanguageConfig {
13863 name: "JavaScript".into(),
13864 line_comments: vec!["// ".into()],
13865 ..Default::default()
13866 },
13867 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13868 ));
13869
13870 cx.language_registry().add(html_language.clone());
13871 cx.language_registry().add(javascript_language.clone());
13872 cx.update_buffer(|buffer, cx| {
13873 buffer.set_language(Some(html_language), cx);
13874 });
13875
13876 // Toggle comments for empty selections
13877 cx.set_state(
13878 &r#"
13879 <p>A</p>ˇ
13880 <p>B</p>ˇ
13881 <p>C</p>ˇ
13882 "#
13883 .unindent(),
13884 );
13885 cx.update_editor(|editor, window, cx| {
13886 editor.toggle_comments(&ToggleComments::default(), window, cx)
13887 });
13888 cx.assert_editor_state(
13889 &r#"
13890 <!-- <p>A</p>ˇ -->
13891 <!-- <p>B</p>ˇ -->
13892 <!-- <p>C</p>ˇ -->
13893 "#
13894 .unindent(),
13895 );
13896 cx.update_editor(|editor, window, cx| {
13897 editor.toggle_comments(&ToggleComments::default(), window, cx)
13898 });
13899 cx.assert_editor_state(
13900 &r#"
13901 <p>A</p>ˇ
13902 <p>B</p>ˇ
13903 <p>C</p>ˇ
13904 "#
13905 .unindent(),
13906 );
13907
13908 // Toggle comments for mixture of empty and non-empty selections, where
13909 // multiple selections occupy a given line.
13910 cx.set_state(
13911 &r#"
13912 <p>A«</p>
13913 <p>ˇ»B</p>ˇ
13914 <p>C«</p>
13915 <p>ˇ»D</p>ˇ
13916 "#
13917 .unindent(),
13918 );
13919
13920 cx.update_editor(|editor, window, cx| {
13921 editor.toggle_comments(&ToggleComments::default(), window, cx)
13922 });
13923 cx.assert_editor_state(
13924 &r#"
13925 <!-- <p>A«</p>
13926 <p>ˇ»B</p>ˇ -->
13927 <!-- <p>C«</p>
13928 <p>ˇ»D</p>ˇ -->
13929 "#
13930 .unindent(),
13931 );
13932 cx.update_editor(|editor, window, cx| {
13933 editor.toggle_comments(&ToggleComments::default(), window, cx)
13934 });
13935 cx.assert_editor_state(
13936 &r#"
13937 <p>A«</p>
13938 <p>ˇ»B</p>ˇ
13939 <p>C«</p>
13940 <p>ˇ»D</p>ˇ
13941 "#
13942 .unindent(),
13943 );
13944
13945 // Toggle comments when different languages are active for different
13946 // selections.
13947 cx.set_state(
13948 &r#"
13949 ˇ<script>
13950 ˇvar x = new Y();
13951 ˇ</script>
13952 "#
13953 .unindent(),
13954 );
13955 cx.executor().run_until_parked();
13956 cx.update_editor(|editor, window, cx| {
13957 editor.toggle_comments(&ToggleComments::default(), window, cx)
13958 });
13959 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
13960 // Uncommenting and commenting from this position brings in even more wrong artifacts.
13961 cx.assert_editor_state(
13962 &r#"
13963 <!-- ˇ<script> -->
13964 // ˇvar x = new Y();
13965 <!-- ˇ</script> -->
13966 "#
13967 .unindent(),
13968 );
13969}
13970
13971#[gpui::test]
13972fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
13973 init_test(cx, |_| {});
13974
13975 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13976 let multibuffer = cx.new(|cx| {
13977 let mut multibuffer = MultiBuffer::new(ReadWrite);
13978 multibuffer.push_excerpts(
13979 buffer.clone(),
13980 [
13981 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
13982 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
13983 ],
13984 cx,
13985 );
13986 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
13987 multibuffer
13988 });
13989
13990 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13991 editor.update_in(cx, |editor, window, cx| {
13992 assert_eq!(editor.text(cx), "aaaa\nbbbb");
13993 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13994 s.select_ranges([
13995 Point::new(0, 0)..Point::new(0, 0),
13996 Point::new(1, 0)..Point::new(1, 0),
13997 ])
13998 });
13999
14000 editor.handle_input("X", window, cx);
14001 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
14002 assert_eq!(
14003 editor.selections.ranges(cx),
14004 [
14005 Point::new(0, 1)..Point::new(0, 1),
14006 Point::new(1, 1)..Point::new(1, 1),
14007 ]
14008 );
14009
14010 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
14011 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14012 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
14013 });
14014 editor.backspace(&Default::default(), window, cx);
14015 assert_eq!(editor.text(cx), "Xa\nbbb");
14016 assert_eq!(
14017 editor.selections.ranges(cx),
14018 [Point::new(1, 0)..Point::new(1, 0)]
14019 );
14020
14021 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14022 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
14023 });
14024 editor.backspace(&Default::default(), window, cx);
14025 assert_eq!(editor.text(cx), "X\nbb");
14026 assert_eq!(
14027 editor.selections.ranges(cx),
14028 [Point::new(0, 1)..Point::new(0, 1)]
14029 );
14030 });
14031}
14032
14033#[gpui::test]
14034fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
14035 init_test(cx, |_| {});
14036
14037 let markers = vec![('[', ']').into(), ('(', ')').into()];
14038 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
14039 indoc! {"
14040 [aaaa
14041 (bbbb]
14042 cccc)",
14043 },
14044 markers.clone(),
14045 );
14046 let excerpt_ranges = markers.into_iter().map(|marker| {
14047 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
14048 ExcerptRange::new(context.clone())
14049 });
14050 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
14051 let multibuffer = cx.new(|cx| {
14052 let mut multibuffer = MultiBuffer::new(ReadWrite);
14053 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
14054 multibuffer
14055 });
14056
14057 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
14058 editor.update_in(cx, |editor, window, cx| {
14059 let (expected_text, selection_ranges) = marked_text_ranges(
14060 indoc! {"
14061 aaaa
14062 bˇbbb
14063 bˇbbˇb
14064 cccc"
14065 },
14066 true,
14067 );
14068 assert_eq!(editor.text(cx), expected_text);
14069 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14070 s.select_ranges(selection_ranges)
14071 });
14072
14073 editor.handle_input("X", window, cx);
14074
14075 let (expected_text, expected_selections) = marked_text_ranges(
14076 indoc! {"
14077 aaaa
14078 bXˇbbXb
14079 bXˇbbXˇb
14080 cccc"
14081 },
14082 false,
14083 );
14084 assert_eq!(editor.text(cx), expected_text);
14085 assert_eq!(editor.selections.ranges(cx), expected_selections);
14086
14087 editor.newline(&Newline, window, cx);
14088 let (expected_text, expected_selections) = marked_text_ranges(
14089 indoc! {"
14090 aaaa
14091 bX
14092 ˇbbX
14093 b
14094 bX
14095 ˇbbX
14096 ˇb
14097 cccc"
14098 },
14099 false,
14100 );
14101 assert_eq!(editor.text(cx), expected_text);
14102 assert_eq!(editor.selections.ranges(cx), expected_selections);
14103 });
14104}
14105
14106#[gpui::test]
14107fn test_refresh_selections(cx: &mut TestAppContext) {
14108 init_test(cx, |_| {});
14109
14110 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14111 let mut excerpt1_id = None;
14112 let multibuffer = cx.new(|cx| {
14113 let mut multibuffer = MultiBuffer::new(ReadWrite);
14114 excerpt1_id = multibuffer
14115 .push_excerpts(
14116 buffer.clone(),
14117 [
14118 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14119 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14120 ],
14121 cx,
14122 )
14123 .into_iter()
14124 .next();
14125 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14126 multibuffer
14127 });
14128
14129 let editor = cx.add_window(|window, cx| {
14130 let mut editor = build_editor(multibuffer.clone(), window, cx);
14131 let snapshot = editor.snapshot(window, cx);
14132 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14133 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
14134 });
14135 editor.begin_selection(
14136 Point::new(2, 1).to_display_point(&snapshot),
14137 true,
14138 1,
14139 window,
14140 cx,
14141 );
14142 assert_eq!(
14143 editor.selections.ranges(cx),
14144 [
14145 Point::new(1, 3)..Point::new(1, 3),
14146 Point::new(2, 1)..Point::new(2, 1),
14147 ]
14148 );
14149 editor
14150 });
14151
14152 // Refreshing selections is a no-op when excerpts haven't changed.
14153 _ = editor.update(cx, |editor, window, cx| {
14154 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14155 assert_eq!(
14156 editor.selections.ranges(cx),
14157 [
14158 Point::new(1, 3)..Point::new(1, 3),
14159 Point::new(2, 1)..Point::new(2, 1),
14160 ]
14161 );
14162 });
14163
14164 multibuffer.update(cx, |multibuffer, cx| {
14165 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14166 });
14167 _ = editor.update(cx, |editor, window, cx| {
14168 // Removing an excerpt causes the first selection to become degenerate.
14169 assert_eq!(
14170 editor.selections.ranges(cx),
14171 [
14172 Point::new(0, 0)..Point::new(0, 0),
14173 Point::new(0, 1)..Point::new(0, 1)
14174 ]
14175 );
14176
14177 // Refreshing selections will relocate the first selection to the original buffer
14178 // location.
14179 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14180 assert_eq!(
14181 editor.selections.ranges(cx),
14182 [
14183 Point::new(0, 1)..Point::new(0, 1),
14184 Point::new(0, 3)..Point::new(0, 3)
14185 ]
14186 );
14187 assert!(editor.selections.pending_anchor().is_some());
14188 });
14189}
14190
14191#[gpui::test]
14192fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
14193 init_test(cx, |_| {});
14194
14195 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14196 let mut excerpt1_id = None;
14197 let multibuffer = cx.new(|cx| {
14198 let mut multibuffer = MultiBuffer::new(ReadWrite);
14199 excerpt1_id = multibuffer
14200 .push_excerpts(
14201 buffer.clone(),
14202 [
14203 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14204 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14205 ],
14206 cx,
14207 )
14208 .into_iter()
14209 .next();
14210 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14211 multibuffer
14212 });
14213
14214 let editor = cx.add_window(|window, cx| {
14215 let mut editor = build_editor(multibuffer.clone(), window, cx);
14216 let snapshot = editor.snapshot(window, cx);
14217 editor.begin_selection(
14218 Point::new(1, 3).to_display_point(&snapshot),
14219 false,
14220 1,
14221 window,
14222 cx,
14223 );
14224 assert_eq!(
14225 editor.selections.ranges(cx),
14226 [Point::new(1, 3)..Point::new(1, 3)]
14227 );
14228 editor
14229 });
14230
14231 multibuffer.update(cx, |multibuffer, cx| {
14232 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14233 });
14234 _ = editor.update(cx, |editor, window, cx| {
14235 assert_eq!(
14236 editor.selections.ranges(cx),
14237 [Point::new(0, 0)..Point::new(0, 0)]
14238 );
14239
14240 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
14241 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14242 assert_eq!(
14243 editor.selections.ranges(cx),
14244 [Point::new(0, 3)..Point::new(0, 3)]
14245 );
14246 assert!(editor.selections.pending_anchor().is_some());
14247 });
14248}
14249
14250#[gpui::test]
14251async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
14252 init_test(cx, |_| {});
14253
14254 let language = Arc::new(
14255 Language::new(
14256 LanguageConfig {
14257 brackets: BracketPairConfig {
14258 pairs: vec![
14259 BracketPair {
14260 start: "{".to_string(),
14261 end: "}".to_string(),
14262 close: true,
14263 surround: true,
14264 newline: true,
14265 },
14266 BracketPair {
14267 start: "/* ".to_string(),
14268 end: " */".to_string(),
14269 close: true,
14270 surround: true,
14271 newline: true,
14272 },
14273 ],
14274 ..Default::default()
14275 },
14276 ..Default::default()
14277 },
14278 Some(tree_sitter_rust::LANGUAGE.into()),
14279 )
14280 .with_indents_query("")
14281 .unwrap(),
14282 );
14283
14284 let text = concat!(
14285 "{ }\n", //
14286 " x\n", //
14287 " /* */\n", //
14288 "x\n", //
14289 "{{} }\n", //
14290 );
14291
14292 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
14293 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14294 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14295 editor
14296 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
14297 .await;
14298
14299 editor.update_in(cx, |editor, window, cx| {
14300 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14301 s.select_display_ranges([
14302 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
14303 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
14304 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
14305 ])
14306 });
14307 editor.newline(&Newline, window, cx);
14308
14309 assert_eq!(
14310 editor.buffer().read(cx).read(cx).text(),
14311 concat!(
14312 "{ \n", // Suppress rustfmt
14313 "\n", //
14314 "}\n", //
14315 " x\n", //
14316 " /* \n", //
14317 " \n", //
14318 " */\n", //
14319 "x\n", //
14320 "{{} \n", //
14321 "}\n", //
14322 )
14323 );
14324 });
14325}
14326
14327#[gpui::test]
14328fn test_highlighted_ranges(cx: &mut TestAppContext) {
14329 init_test(cx, |_| {});
14330
14331 let editor = cx.add_window(|window, cx| {
14332 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
14333 build_editor(buffer.clone(), window, cx)
14334 });
14335
14336 _ = editor.update(cx, |editor, window, cx| {
14337 struct Type1;
14338 struct Type2;
14339
14340 let buffer = editor.buffer.read(cx).snapshot(cx);
14341
14342 let anchor_range =
14343 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
14344
14345 editor.highlight_background::<Type1>(
14346 &[
14347 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
14348 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
14349 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
14350 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
14351 ],
14352 |_| Hsla::red(),
14353 cx,
14354 );
14355 editor.highlight_background::<Type2>(
14356 &[
14357 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
14358 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
14359 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
14360 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
14361 ],
14362 |_| Hsla::green(),
14363 cx,
14364 );
14365
14366 let snapshot = editor.snapshot(window, cx);
14367 let mut highlighted_ranges = editor.background_highlights_in_range(
14368 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
14369 &snapshot,
14370 cx.theme(),
14371 );
14372 // Enforce a consistent ordering based on color without relying on the ordering of the
14373 // highlight's `TypeId` which is non-executor.
14374 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
14375 assert_eq!(
14376 highlighted_ranges,
14377 &[
14378 (
14379 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
14380 Hsla::red(),
14381 ),
14382 (
14383 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14384 Hsla::red(),
14385 ),
14386 (
14387 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
14388 Hsla::green(),
14389 ),
14390 (
14391 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
14392 Hsla::green(),
14393 ),
14394 ]
14395 );
14396 assert_eq!(
14397 editor.background_highlights_in_range(
14398 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
14399 &snapshot,
14400 cx.theme(),
14401 ),
14402 &[(
14403 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14404 Hsla::red(),
14405 )]
14406 );
14407 });
14408}
14409
14410#[gpui::test]
14411async fn test_following(cx: &mut TestAppContext) {
14412 init_test(cx, |_| {});
14413
14414 let fs = FakeFs::new(cx.executor());
14415 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14416
14417 let buffer = project.update(cx, |project, cx| {
14418 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
14419 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
14420 });
14421 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
14422 let follower = cx.update(|cx| {
14423 cx.open_window(
14424 WindowOptions {
14425 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
14426 gpui::Point::new(px(0.), px(0.)),
14427 gpui::Point::new(px(10.), px(80.)),
14428 ))),
14429 ..Default::default()
14430 },
14431 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
14432 )
14433 .unwrap()
14434 });
14435
14436 let is_still_following = Rc::new(RefCell::new(true));
14437 let follower_edit_event_count = Rc::new(RefCell::new(0));
14438 let pending_update = Rc::new(RefCell::new(None));
14439 let leader_entity = leader.root(cx).unwrap();
14440 let follower_entity = follower.root(cx).unwrap();
14441 _ = follower.update(cx, {
14442 let update = pending_update.clone();
14443 let is_still_following = is_still_following.clone();
14444 let follower_edit_event_count = follower_edit_event_count.clone();
14445 |_, window, cx| {
14446 cx.subscribe_in(
14447 &leader_entity,
14448 window,
14449 move |_, leader, event, window, cx| {
14450 leader.read(cx).add_event_to_update_proto(
14451 event,
14452 &mut update.borrow_mut(),
14453 window,
14454 cx,
14455 );
14456 },
14457 )
14458 .detach();
14459
14460 cx.subscribe_in(
14461 &follower_entity,
14462 window,
14463 move |_, _, event: &EditorEvent, _window, _cx| {
14464 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
14465 *is_still_following.borrow_mut() = false;
14466 }
14467
14468 if let EditorEvent::BufferEdited = event {
14469 *follower_edit_event_count.borrow_mut() += 1;
14470 }
14471 },
14472 )
14473 .detach();
14474 }
14475 });
14476
14477 // Update the selections only
14478 _ = leader.update(cx, |leader, window, cx| {
14479 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14480 s.select_ranges([1..1])
14481 });
14482 });
14483 follower
14484 .update(cx, |follower, window, cx| {
14485 follower.apply_update_proto(
14486 &project,
14487 pending_update.borrow_mut().take().unwrap(),
14488 window,
14489 cx,
14490 )
14491 })
14492 .unwrap()
14493 .await
14494 .unwrap();
14495 _ = follower.update(cx, |follower, _, cx| {
14496 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
14497 });
14498 assert!(*is_still_following.borrow());
14499 assert_eq!(*follower_edit_event_count.borrow(), 0);
14500
14501 // Update the scroll position only
14502 _ = leader.update(cx, |leader, window, cx| {
14503 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14504 });
14505 follower
14506 .update(cx, |follower, window, cx| {
14507 follower.apply_update_proto(
14508 &project,
14509 pending_update.borrow_mut().take().unwrap(),
14510 window,
14511 cx,
14512 )
14513 })
14514 .unwrap()
14515 .await
14516 .unwrap();
14517 assert_eq!(
14518 follower
14519 .update(cx, |follower, _, cx| follower.scroll_position(cx))
14520 .unwrap(),
14521 gpui::Point::new(1.5, 3.5)
14522 );
14523 assert!(*is_still_following.borrow());
14524 assert_eq!(*follower_edit_event_count.borrow(), 0);
14525
14526 // Update the selections and scroll position. The follower's scroll position is updated
14527 // via autoscroll, not via the leader's exact scroll position.
14528 _ = leader.update(cx, |leader, window, cx| {
14529 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14530 s.select_ranges([0..0])
14531 });
14532 leader.request_autoscroll(Autoscroll::newest(), cx);
14533 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14534 });
14535 follower
14536 .update(cx, |follower, window, cx| {
14537 follower.apply_update_proto(
14538 &project,
14539 pending_update.borrow_mut().take().unwrap(),
14540 window,
14541 cx,
14542 )
14543 })
14544 .unwrap()
14545 .await
14546 .unwrap();
14547 _ = follower.update(cx, |follower, _, cx| {
14548 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
14549 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
14550 });
14551 assert!(*is_still_following.borrow());
14552
14553 // Creating a pending selection that precedes another selection
14554 _ = leader.update(cx, |leader, window, cx| {
14555 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14556 s.select_ranges([1..1])
14557 });
14558 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
14559 });
14560 follower
14561 .update(cx, |follower, window, cx| {
14562 follower.apply_update_proto(
14563 &project,
14564 pending_update.borrow_mut().take().unwrap(),
14565 window,
14566 cx,
14567 )
14568 })
14569 .unwrap()
14570 .await
14571 .unwrap();
14572 _ = follower.update(cx, |follower, _, cx| {
14573 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
14574 });
14575 assert!(*is_still_following.borrow());
14576
14577 // Extend the pending selection so that it surrounds another selection
14578 _ = leader.update(cx, |leader, window, cx| {
14579 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
14580 });
14581 follower
14582 .update(cx, |follower, window, cx| {
14583 follower.apply_update_proto(
14584 &project,
14585 pending_update.borrow_mut().take().unwrap(),
14586 window,
14587 cx,
14588 )
14589 })
14590 .unwrap()
14591 .await
14592 .unwrap();
14593 _ = follower.update(cx, |follower, _, cx| {
14594 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
14595 });
14596
14597 // Scrolling locally breaks the follow
14598 _ = follower.update(cx, |follower, window, cx| {
14599 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
14600 follower.set_scroll_anchor(
14601 ScrollAnchor {
14602 anchor: top_anchor,
14603 offset: gpui::Point::new(0.0, 0.5),
14604 },
14605 window,
14606 cx,
14607 );
14608 });
14609 assert!(!(*is_still_following.borrow()));
14610}
14611
14612#[gpui::test]
14613async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
14614 init_test(cx, |_| {});
14615
14616 let fs = FakeFs::new(cx.executor());
14617 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14618 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14619 let pane = workspace
14620 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14621 .unwrap();
14622
14623 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14624
14625 let leader = pane.update_in(cx, |_, window, cx| {
14626 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
14627 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
14628 });
14629
14630 // Start following the editor when it has no excerpts.
14631 let mut state_message =
14632 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14633 let workspace_entity = workspace.root(cx).unwrap();
14634 let follower_1 = cx
14635 .update_window(*workspace.deref(), |_, window, cx| {
14636 Editor::from_state_proto(
14637 workspace_entity,
14638 ViewId {
14639 creator: CollaboratorId::PeerId(PeerId::default()),
14640 id: 0,
14641 },
14642 &mut state_message,
14643 window,
14644 cx,
14645 )
14646 })
14647 .unwrap()
14648 .unwrap()
14649 .await
14650 .unwrap();
14651
14652 let update_message = Rc::new(RefCell::new(None));
14653 follower_1.update_in(cx, {
14654 let update = update_message.clone();
14655 |_, window, cx| {
14656 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
14657 leader.read(cx).add_event_to_update_proto(
14658 event,
14659 &mut update.borrow_mut(),
14660 window,
14661 cx,
14662 );
14663 })
14664 .detach();
14665 }
14666 });
14667
14668 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
14669 (
14670 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
14671 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
14672 )
14673 });
14674
14675 // Insert some excerpts.
14676 leader.update(cx, |leader, cx| {
14677 leader.buffer.update(cx, |multibuffer, cx| {
14678 multibuffer.set_excerpts_for_path(
14679 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
14680 buffer_1.clone(),
14681 vec![
14682 Point::row_range(0..3),
14683 Point::row_range(1..6),
14684 Point::row_range(12..15),
14685 ],
14686 0,
14687 cx,
14688 );
14689 multibuffer.set_excerpts_for_path(
14690 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
14691 buffer_2.clone(),
14692 vec![Point::row_range(0..6), Point::row_range(8..12)],
14693 0,
14694 cx,
14695 );
14696 });
14697 });
14698
14699 // Apply the update of adding the excerpts.
14700 follower_1
14701 .update_in(cx, |follower, window, cx| {
14702 follower.apply_update_proto(
14703 &project,
14704 update_message.borrow().clone().unwrap(),
14705 window,
14706 cx,
14707 )
14708 })
14709 .await
14710 .unwrap();
14711 assert_eq!(
14712 follower_1.update(cx, |editor, cx| editor.text(cx)),
14713 leader.update(cx, |editor, cx| editor.text(cx))
14714 );
14715 update_message.borrow_mut().take();
14716
14717 // Start following separately after it already has excerpts.
14718 let mut state_message =
14719 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14720 let workspace_entity = workspace.root(cx).unwrap();
14721 let follower_2 = cx
14722 .update_window(*workspace.deref(), |_, window, cx| {
14723 Editor::from_state_proto(
14724 workspace_entity,
14725 ViewId {
14726 creator: CollaboratorId::PeerId(PeerId::default()),
14727 id: 0,
14728 },
14729 &mut state_message,
14730 window,
14731 cx,
14732 )
14733 })
14734 .unwrap()
14735 .unwrap()
14736 .await
14737 .unwrap();
14738 assert_eq!(
14739 follower_2.update(cx, |editor, cx| editor.text(cx)),
14740 leader.update(cx, |editor, cx| editor.text(cx))
14741 );
14742
14743 // Remove some excerpts.
14744 leader.update(cx, |leader, cx| {
14745 leader.buffer.update(cx, |multibuffer, cx| {
14746 let excerpt_ids = multibuffer.excerpt_ids();
14747 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
14748 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
14749 });
14750 });
14751
14752 // Apply the update of removing the excerpts.
14753 follower_1
14754 .update_in(cx, |follower, window, cx| {
14755 follower.apply_update_proto(
14756 &project,
14757 update_message.borrow().clone().unwrap(),
14758 window,
14759 cx,
14760 )
14761 })
14762 .await
14763 .unwrap();
14764 follower_2
14765 .update_in(cx, |follower, window, cx| {
14766 follower.apply_update_proto(
14767 &project,
14768 update_message.borrow().clone().unwrap(),
14769 window,
14770 cx,
14771 )
14772 })
14773 .await
14774 .unwrap();
14775 update_message.borrow_mut().take();
14776 assert_eq!(
14777 follower_1.update(cx, |editor, cx| editor.text(cx)),
14778 leader.update(cx, |editor, cx| editor.text(cx))
14779 );
14780}
14781
14782#[gpui::test]
14783async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14784 init_test(cx, |_| {});
14785
14786 let mut cx = EditorTestContext::new(cx).await;
14787 let lsp_store =
14788 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
14789
14790 cx.set_state(indoc! {"
14791 ˇfn func(abc def: i32) -> u32 {
14792 }
14793 "});
14794
14795 cx.update(|_, cx| {
14796 lsp_store.update(cx, |lsp_store, cx| {
14797 lsp_store
14798 .update_diagnostics(
14799 LanguageServerId(0),
14800 lsp::PublishDiagnosticsParams {
14801 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
14802 version: None,
14803 diagnostics: vec![
14804 lsp::Diagnostic {
14805 range: lsp::Range::new(
14806 lsp::Position::new(0, 11),
14807 lsp::Position::new(0, 12),
14808 ),
14809 severity: Some(lsp::DiagnosticSeverity::ERROR),
14810 ..Default::default()
14811 },
14812 lsp::Diagnostic {
14813 range: lsp::Range::new(
14814 lsp::Position::new(0, 12),
14815 lsp::Position::new(0, 15),
14816 ),
14817 severity: Some(lsp::DiagnosticSeverity::ERROR),
14818 ..Default::default()
14819 },
14820 lsp::Diagnostic {
14821 range: lsp::Range::new(
14822 lsp::Position::new(0, 25),
14823 lsp::Position::new(0, 28),
14824 ),
14825 severity: Some(lsp::DiagnosticSeverity::ERROR),
14826 ..Default::default()
14827 },
14828 ],
14829 },
14830 None,
14831 DiagnosticSourceKind::Pushed,
14832 &[],
14833 cx,
14834 )
14835 .unwrap()
14836 });
14837 });
14838
14839 executor.run_until_parked();
14840
14841 cx.update_editor(|editor, window, cx| {
14842 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14843 });
14844
14845 cx.assert_editor_state(indoc! {"
14846 fn func(abc def: i32) -> ˇu32 {
14847 }
14848 "});
14849
14850 cx.update_editor(|editor, window, cx| {
14851 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14852 });
14853
14854 cx.assert_editor_state(indoc! {"
14855 fn func(abc ˇdef: i32) -> u32 {
14856 }
14857 "});
14858
14859 cx.update_editor(|editor, window, cx| {
14860 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14861 });
14862
14863 cx.assert_editor_state(indoc! {"
14864 fn func(abcˇ def: i32) -> u32 {
14865 }
14866 "});
14867
14868 cx.update_editor(|editor, window, cx| {
14869 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14870 });
14871
14872 cx.assert_editor_state(indoc! {"
14873 fn func(abc def: i32) -> ˇu32 {
14874 }
14875 "});
14876}
14877
14878#[gpui::test]
14879async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14880 init_test(cx, |_| {});
14881
14882 let mut cx = EditorTestContext::new(cx).await;
14883
14884 let diff_base = r#"
14885 use some::mod;
14886
14887 const A: u32 = 42;
14888
14889 fn main() {
14890 println!("hello");
14891
14892 println!("world");
14893 }
14894 "#
14895 .unindent();
14896
14897 // Edits are modified, removed, modified, added
14898 cx.set_state(
14899 &r#"
14900 use some::modified;
14901
14902 ˇ
14903 fn main() {
14904 println!("hello there");
14905
14906 println!("around the");
14907 println!("world");
14908 }
14909 "#
14910 .unindent(),
14911 );
14912
14913 cx.set_head_text(&diff_base);
14914 executor.run_until_parked();
14915
14916 cx.update_editor(|editor, window, cx| {
14917 //Wrap around the bottom of the buffer
14918 for _ in 0..3 {
14919 editor.go_to_next_hunk(&GoToHunk, window, cx);
14920 }
14921 });
14922
14923 cx.assert_editor_state(
14924 &r#"
14925 ˇuse some::modified;
14926
14927
14928 fn main() {
14929 println!("hello there");
14930
14931 println!("around the");
14932 println!("world");
14933 }
14934 "#
14935 .unindent(),
14936 );
14937
14938 cx.update_editor(|editor, window, cx| {
14939 //Wrap around the top of the buffer
14940 for _ in 0..2 {
14941 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14942 }
14943 });
14944
14945 cx.assert_editor_state(
14946 &r#"
14947 use some::modified;
14948
14949
14950 fn main() {
14951 ˇ println!("hello there");
14952
14953 println!("around the");
14954 println!("world");
14955 }
14956 "#
14957 .unindent(),
14958 );
14959
14960 cx.update_editor(|editor, window, cx| {
14961 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14962 });
14963
14964 cx.assert_editor_state(
14965 &r#"
14966 use some::modified;
14967
14968 ˇ
14969 fn main() {
14970 println!("hello there");
14971
14972 println!("around the");
14973 println!("world");
14974 }
14975 "#
14976 .unindent(),
14977 );
14978
14979 cx.update_editor(|editor, window, cx| {
14980 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14981 });
14982
14983 cx.assert_editor_state(
14984 &r#"
14985 ˇuse some::modified;
14986
14987
14988 fn main() {
14989 println!("hello there");
14990
14991 println!("around the");
14992 println!("world");
14993 }
14994 "#
14995 .unindent(),
14996 );
14997
14998 cx.update_editor(|editor, window, cx| {
14999 for _ in 0..2 {
15000 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15001 }
15002 });
15003
15004 cx.assert_editor_state(
15005 &r#"
15006 use some::modified;
15007
15008
15009 fn main() {
15010 ˇ println!("hello there");
15011
15012 println!("around the");
15013 println!("world");
15014 }
15015 "#
15016 .unindent(),
15017 );
15018
15019 cx.update_editor(|editor, window, cx| {
15020 editor.fold(&Fold, window, cx);
15021 });
15022
15023 cx.update_editor(|editor, window, cx| {
15024 editor.go_to_next_hunk(&GoToHunk, window, cx);
15025 });
15026
15027 cx.assert_editor_state(
15028 &r#"
15029 ˇuse some::modified;
15030
15031
15032 fn main() {
15033 println!("hello there");
15034
15035 println!("around the");
15036 println!("world");
15037 }
15038 "#
15039 .unindent(),
15040 );
15041}
15042
15043#[test]
15044fn test_split_words() {
15045 fn split(text: &str) -> Vec<&str> {
15046 split_words(text).collect()
15047 }
15048
15049 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
15050 assert_eq!(split("hello_world"), &["hello_", "world"]);
15051 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
15052 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
15053 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
15054 assert_eq!(split("helloworld"), &["helloworld"]);
15055
15056 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
15057}
15058
15059#[gpui::test]
15060async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
15061 init_test(cx, |_| {});
15062
15063 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
15064 let mut assert = |before, after| {
15065 let _state_context = cx.set_state(before);
15066 cx.run_until_parked();
15067 cx.update_editor(|editor, window, cx| {
15068 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
15069 });
15070 cx.run_until_parked();
15071 cx.assert_editor_state(after);
15072 };
15073
15074 // Outside bracket jumps to outside of matching bracket
15075 assert("console.logˇ(var);", "console.log(var)ˇ;");
15076 assert("console.log(var)ˇ;", "console.logˇ(var);");
15077
15078 // Inside bracket jumps to inside of matching bracket
15079 assert("console.log(ˇvar);", "console.log(varˇ);");
15080 assert("console.log(varˇ);", "console.log(ˇvar);");
15081
15082 // When outside a bracket and inside, favor jumping to the inside bracket
15083 assert(
15084 "console.log('foo', [1, 2, 3]ˇ);",
15085 "console.log(ˇ'foo', [1, 2, 3]);",
15086 );
15087 assert(
15088 "console.log(ˇ'foo', [1, 2, 3]);",
15089 "console.log('foo', [1, 2, 3]ˇ);",
15090 );
15091
15092 // Bias forward if two options are equally likely
15093 assert(
15094 "let result = curried_fun()ˇ();",
15095 "let result = curried_fun()()ˇ;",
15096 );
15097
15098 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
15099 assert(
15100 indoc! {"
15101 function test() {
15102 console.log('test')ˇ
15103 }"},
15104 indoc! {"
15105 function test() {
15106 console.logˇ('test')
15107 }"},
15108 );
15109}
15110
15111#[gpui::test]
15112async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
15113 init_test(cx, |_| {});
15114
15115 let fs = FakeFs::new(cx.executor());
15116 fs.insert_tree(
15117 path!("/a"),
15118 json!({
15119 "main.rs": "fn main() { let a = 5; }",
15120 "other.rs": "// Test file",
15121 }),
15122 )
15123 .await;
15124 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15125
15126 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15127 language_registry.add(Arc::new(Language::new(
15128 LanguageConfig {
15129 name: "Rust".into(),
15130 matcher: LanguageMatcher {
15131 path_suffixes: vec!["rs".to_string()],
15132 ..Default::default()
15133 },
15134 brackets: BracketPairConfig {
15135 pairs: vec![BracketPair {
15136 start: "{".to_string(),
15137 end: "}".to_string(),
15138 close: true,
15139 surround: true,
15140 newline: true,
15141 }],
15142 disabled_scopes_by_bracket_ix: Vec::new(),
15143 },
15144 ..Default::default()
15145 },
15146 Some(tree_sitter_rust::LANGUAGE.into()),
15147 )));
15148 let mut fake_servers = language_registry.register_fake_lsp(
15149 "Rust",
15150 FakeLspAdapter {
15151 capabilities: lsp::ServerCapabilities {
15152 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15153 first_trigger_character: "{".to_string(),
15154 more_trigger_character: None,
15155 }),
15156 ..Default::default()
15157 },
15158 ..Default::default()
15159 },
15160 );
15161
15162 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15163
15164 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15165
15166 let worktree_id = workspace
15167 .update(cx, |workspace, _, cx| {
15168 workspace.project().update(cx, |project, cx| {
15169 project.worktrees(cx).next().unwrap().read(cx).id()
15170 })
15171 })
15172 .unwrap();
15173
15174 let buffer = project
15175 .update(cx, |project, cx| {
15176 project.open_local_buffer(path!("/a/main.rs"), cx)
15177 })
15178 .await
15179 .unwrap();
15180 let editor_handle = workspace
15181 .update(cx, |workspace, window, cx| {
15182 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
15183 })
15184 .unwrap()
15185 .await
15186 .unwrap()
15187 .downcast::<Editor>()
15188 .unwrap();
15189
15190 cx.executor().start_waiting();
15191 let fake_server = fake_servers.next().await.unwrap();
15192
15193 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
15194 |params, _| async move {
15195 assert_eq!(
15196 params.text_document_position.text_document.uri,
15197 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
15198 );
15199 assert_eq!(
15200 params.text_document_position.position,
15201 lsp::Position::new(0, 21),
15202 );
15203
15204 Ok(Some(vec![lsp::TextEdit {
15205 new_text: "]".to_string(),
15206 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15207 }]))
15208 },
15209 );
15210
15211 editor_handle.update_in(cx, |editor, window, cx| {
15212 window.focus(&editor.focus_handle(cx));
15213 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15214 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
15215 });
15216 editor.handle_input("{", window, cx);
15217 });
15218
15219 cx.executor().run_until_parked();
15220
15221 buffer.update(cx, |buffer, _| {
15222 assert_eq!(
15223 buffer.text(),
15224 "fn main() { let a = {5}; }",
15225 "No extra braces from on type formatting should appear in the buffer"
15226 )
15227 });
15228}
15229
15230#[gpui::test(iterations = 20, seeds(31))]
15231async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
15232 init_test(cx, |_| {});
15233
15234 let mut cx = EditorLspTestContext::new_rust(
15235 lsp::ServerCapabilities {
15236 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15237 first_trigger_character: ".".to_string(),
15238 more_trigger_character: None,
15239 }),
15240 ..Default::default()
15241 },
15242 cx,
15243 )
15244 .await;
15245
15246 cx.update_buffer(|buffer, _| {
15247 // This causes autoindent to be async.
15248 buffer.set_sync_parse_timeout(Duration::ZERO)
15249 });
15250
15251 cx.set_state("fn c() {\n d()ˇ\n}\n");
15252 cx.simulate_keystroke("\n");
15253 cx.run_until_parked();
15254
15255 let buffer_cloned =
15256 cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap().clone());
15257 let mut request =
15258 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
15259 let buffer_cloned = buffer_cloned.clone();
15260 async move {
15261 buffer_cloned.update(&mut cx, |buffer, _| {
15262 assert_eq!(
15263 buffer.text(),
15264 "fn c() {\n d()\n .\n}\n",
15265 "OnTypeFormatting should triggered after autoindent applied"
15266 )
15267 })?;
15268
15269 Ok(Some(vec![]))
15270 }
15271 });
15272
15273 cx.simulate_keystroke(".");
15274 cx.run_until_parked();
15275
15276 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
15277 assert!(request.next().await.is_some());
15278 request.close();
15279 assert!(request.next().await.is_none());
15280}
15281
15282#[gpui::test]
15283async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
15284 init_test(cx, |_| {});
15285
15286 let fs = FakeFs::new(cx.executor());
15287 fs.insert_tree(
15288 path!("/a"),
15289 json!({
15290 "main.rs": "fn main() { let a = 5; }",
15291 "other.rs": "// Test file",
15292 }),
15293 )
15294 .await;
15295
15296 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15297
15298 let server_restarts = Arc::new(AtomicUsize::new(0));
15299 let closure_restarts = Arc::clone(&server_restarts);
15300 let language_server_name = "test language server";
15301 let language_name: LanguageName = "Rust".into();
15302
15303 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15304 language_registry.add(Arc::new(Language::new(
15305 LanguageConfig {
15306 name: language_name.clone(),
15307 matcher: LanguageMatcher {
15308 path_suffixes: vec!["rs".to_string()],
15309 ..Default::default()
15310 },
15311 ..Default::default()
15312 },
15313 Some(tree_sitter_rust::LANGUAGE.into()),
15314 )));
15315 let mut fake_servers = language_registry.register_fake_lsp(
15316 "Rust",
15317 FakeLspAdapter {
15318 name: language_server_name,
15319 initialization_options: Some(json!({
15320 "testOptionValue": true
15321 })),
15322 initializer: Some(Box::new(move |fake_server| {
15323 let task_restarts = Arc::clone(&closure_restarts);
15324 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
15325 task_restarts.fetch_add(1, atomic::Ordering::Release);
15326 futures::future::ready(Ok(()))
15327 });
15328 })),
15329 ..Default::default()
15330 },
15331 );
15332
15333 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15334 let _buffer = project
15335 .update(cx, |project, cx| {
15336 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
15337 })
15338 .await
15339 .unwrap();
15340 let _fake_server = fake_servers.next().await.unwrap();
15341 update_test_language_settings(cx, |language_settings| {
15342 language_settings.languages.0.insert(
15343 language_name.clone(),
15344 LanguageSettingsContent {
15345 tab_size: NonZeroU32::new(8),
15346 ..Default::default()
15347 },
15348 );
15349 });
15350 cx.executor().run_until_parked();
15351 assert_eq!(
15352 server_restarts.load(atomic::Ordering::Acquire),
15353 0,
15354 "Should not restart LSP server on an unrelated change"
15355 );
15356
15357 update_test_project_settings(cx, |project_settings| {
15358 project_settings.lsp.insert(
15359 "Some other server name".into(),
15360 LspSettings {
15361 binary: None,
15362 settings: None,
15363 initialization_options: Some(json!({
15364 "some other init value": false
15365 })),
15366 enable_lsp_tasks: false,
15367 },
15368 );
15369 });
15370 cx.executor().run_until_parked();
15371 assert_eq!(
15372 server_restarts.load(atomic::Ordering::Acquire),
15373 0,
15374 "Should not restart LSP server on an unrelated LSP settings change"
15375 );
15376
15377 update_test_project_settings(cx, |project_settings| {
15378 project_settings.lsp.insert(
15379 language_server_name.into(),
15380 LspSettings {
15381 binary: None,
15382 settings: None,
15383 initialization_options: Some(json!({
15384 "anotherInitValue": false
15385 })),
15386 enable_lsp_tasks: false,
15387 },
15388 );
15389 });
15390 cx.executor().run_until_parked();
15391 assert_eq!(
15392 server_restarts.load(atomic::Ordering::Acquire),
15393 1,
15394 "Should restart LSP server on a related LSP settings change"
15395 );
15396
15397 update_test_project_settings(cx, |project_settings| {
15398 project_settings.lsp.insert(
15399 language_server_name.into(),
15400 LspSettings {
15401 binary: None,
15402 settings: None,
15403 initialization_options: Some(json!({
15404 "anotherInitValue": false
15405 })),
15406 enable_lsp_tasks: false,
15407 },
15408 );
15409 });
15410 cx.executor().run_until_parked();
15411 assert_eq!(
15412 server_restarts.load(atomic::Ordering::Acquire),
15413 1,
15414 "Should not restart LSP server on a related LSP settings change that is the same"
15415 );
15416
15417 update_test_project_settings(cx, |project_settings| {
15418 project_settings.lsp.insert(
15419 language_server_name.into(),
15420 LspSettings {
15421 binary: None,
15422 settings: None,
15423 initialization_options: None,
15424 enable_lsp_tasks: false,
15425 },
15426 );
15427 });
15428 cx.executor().run_until_parked();
15429 assert_eq!(
15430 server_restarts.load(atomic::Ordering::Acquire),
15431 2,
15432 "Should restart LSP server on another related LSP settings change"
15433 );
15434}
15435
15436#[gpui::test]
15437async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
15438 init_test(cx, |_| {});
15439
15440 let mut cx = EditorLspTestContext::new_rust(
15441 lsp::ServerCapabilities {
15442 completion_provider: Some(lsp::CompletionOptions {
15443 trigger_characters: Some(vec![".".to_string()]),
15444 resolve_provider: Some(true),
15445 ..Default::default()
15446 }),
15447 ..Default::default()
15448 },
15449 cx,
15450 )
15451 .await;
15452
15453 cx.set_state("fn main() { let a = 2ˇ; }");
15454 cx.simulate_keystroke(".");
15455 let completion_item = lsp::CompletionItem {
15456 label: "some".into(),
15457 kind: Some(lsp::CompletionItemKind::SNIPPET),
15458 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15459 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15460 kind: lsp::MarkupKind::Markdown,
15461 value: "```rust\nSome(2)\n```".to_string(),
15462 })),
15463 deprecated: Some(false),
15464 sort_text: Some("fffffff2".to_string()),
15465 filter_text: Some("some".to_string()),
15466 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15467 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15468 range: lsp::Range {
15469 start: lsp::Position {
15470 line: 0,
15471 character: 22,
15472 },
15473 end: lsp::Position {
15474 line: 0,
15475 character: 22,
15476 },
15477 },
15478 new_text: "Some(2)".to_string(),
15479 })),
15480 additional_text_edits: Some(vec![lsp::TextEdit {
15481 range: lsp::Range {
15482 start: lsp::Position {
15483 line: 0,
15484 character: 20,
15485 },
15486 end: lsp::Position {
15487 line: 0,
15488 character: 22,
15489 },
15490 },
15491 new_text: "".to_string(),
15492 }]),
15493 ..Default::default()
15494 };
15495
15496 let closure_completion_item = completion_item.clone();
15497 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15498 let task_completion_item = closure_completion_item.clone();
15499 async move {
15500 Ok(Some(lsp::CompletionResponse::Array(vec![
15501 task_completion_item,
15502 ])))
15503 }
15504 });
15505
15506 request.next().await;
15507
15508 cx.condition(|editor, _| editor.context_menu_visible())
15509 .await;
15510 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15511 editor
15512 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15513 .unwrap()
15514 });
15515 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
15516
15517 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
15518 let task_completion_item = completion_item.clone();
15519 async move { Ok(task_completion_item) }
15520 })
15521 .next()
15522 .await
15523 .unwrap();
15524 apply_additional_edits.await.unwrap();
15525 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
15526}
15527
15528#[gpui::test]
15529async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
15530 init_test(cx, |_| {});
15531
15532 let mut cx = EditorLspTestContext::new_rust(
15533 lsp::ServerCapabilities {
15534 completion_provider: Some(lsp::CompletionOptions {
15535 trigger_characters: Some(vec![".".to_string()]),
15536 resolve_provider: Some(true),
15537 ..Default::default()
15538 }),
15539 ..Default::default()
15540 },
15541 cx,
15542 )
15543 .await;
15544
15545 cx.set_state("fn main() { let a = 2ˇ; }");
15546 cx.simulate_keystroke(".");
15547
15548 let item1 = lsp::CompletionItem {
15549 label: "method id()".to_string(),
15550 filter_text: Some("id".to_string()),
15551 detail: None,
15552 documentation: None,
15553 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15554 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15555 new_text: ".id".to_string(),
15556 })),
15557 ..lsp::CompletionItem::default()
15558 };
15559
15560 let item2 = lsp::CompletionItem {
15561 label: "other".to_string(),
15562 filter_text: Some("other".to_string()),
15563 detail: None,
15564 documentation: None,
15565 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15566 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15567 new_text: ".other".to_string(),
15568 })),
15569 ..lsp::CompletionItem::default()
15570 };
15571
15572 let item1 = item1.clone();
15573 cx.set_request_handler::<lsp::request::Completion, _, _>({
15574 let item1 = item1.clone();
15575 move |_, _, _| {
15576 let item1 = item1.clone();
15577 let item2 = item2.clone();
15578 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
15579 }
15580 })
15581 .next()
15582 .await;
15583
15584 cx.condition(|editor, _| editor.context_menu_visible())
15585 .await;
15586 cx.update_editor(|editor, _, _| {
15587 let context_menu = editor.context_menu.borrow_mut();
15588 let context_menu = context_menu
15589 .as_ref()
15590 .expect("Should have the context menu deployed");
15591 match context_menu {
15592 CodeContextMenu::Completions(completions_menu) => {
15593 let completions = completions_menu.completions.borrow_mut();
15594 assert_eq!(
15595 completions
15596 .iter()
15597 .map(|completion| &completion.label.text)
15598 .collect::<Vec<_>>(),
15599 vec!["method id()", "other"]
15600 )
15601 }
15602 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15603 }
15604 });
15605
15606 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
15607 let item1 = item1.clone();
15608 move |_, item_to_resolve, _| {
15609 let item1 = item1.clone();
15610 async move {
15611 if item1 == item_to_resolve {
15612 Ok(lsp::CompletionItem {
15613 label: "method id()".to_string(),
15614 filter_text: Some("id".to_string()),
15615 detail: Some("Now resolved!".to_string()),
15616 documentation: Some(lsp::Documentation::String("Docs".to_string())),
15617 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15618 range: lsp::Range::new(
15619 lsp::Position::new(0, 22),
15620 lsp::Position::new(0, 22),
15621 ),
15622 new_text: ".id".to_string(),
15623 })),
15624 ..lsp::CompletionItem::default()
15625 })
15626 } else {
15627 Ok(item_to_resolve)
15628 }
15629 }
15630 }
15631 })
15632 .next()
15633 .await
15634 .unwrap();
15635 cx.run_until_parked();
15636
15637 cx.update_editor(|editor, window, cx| {
15638 editor.context_menu_next(&Default::default(), window, cx);
15639 });
15640
15641 cx.update_editor(|editor, _, _| {
15642 let context_menu = editor.context_menu.borrow_mut();
15643 let context_menu = context_menu
15644 .as_ref()
15645 .expect("Should have the context menu deployed");
15646 match context_menu {
15647 CodeContextMenu::Completions(completions_menu) => {
15648 let completions = completions_menu.completions.borrow_mut();
15649 assert_eq!(
15650 completions
15651 .iter()
15652 .map(|completion| &completion.label.text)
15653 .collect::<Vec<_>>(),
15654 vec!["method id() Now resolved!", "other"],
15655 "Should update first completion label, but not second as the filter text did not match."
15656 );
15657 }
15658 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15659 }
15660 });
15661}
15662
15663#[gpui::test]
15664async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
15665 init_test(cx, |_| {});
15666 let mut cx = EditorLspTestContext::new_rust(
15667 lsp::ServerCapabilities {
15668 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
15669 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
15670 completion_provider: Some(lsp::CompletionOptions {
15671 resolve_provider: Some(true),
15672 ..Default::default()
15673 }),
15674 ..Default::default()
15675 },
15676 cx,
15677 )
15678 .await;
15679 cx.set_state(indoc! {"
15680 struct TestStruct {
15681 field: i32
15682 }
15683
15684 fn mainˇ() {
15685 let unused_var = 42;
15686 let test_struct = TestStruct { field: 42 };
15687 }
15688 "});
15689 let symbol_range = cx.lsp_range(indoc! {"
15690 struct TestStruct {
15691 field: i32
15692 }
15693
15694 «fn main»() {
15695 let unused_var = 42;
15696 let test_struct = TestStruct { field: 42 };
15697 }
15698 "});
15699 let mut hover_requests =
15700 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
15701 Ok(Some(lsp::Hover {
15702 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
15703 kind: lsp::MarkupKind::Markdown,
15704 value: "Function documentation".to_string(),
15705 }),
15706 range: Some(symbol_range),
15707 }))
15708 });
15709
15710 // Case 1: Test that code action menu hide hover popover
15711 cx.dispatch_action(Hover);
15712 hover_requests.next().await;
15713 cx.condition(|editor, _| editor.hover_state.visible()).await;
15714 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
15715 move |_, _, _| async move {
15716 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
15717 lsp::CodeAction {
15718 title: "Remove unused variable".to_string(),
15719 kind: Some(CodeActionKind::QUICKFIX),
15720 edit: Some(lsp::WorkspaceEdit {
15721 changes: Some(
15722 [(
15723 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
15724 vec![lsp::TextEdit {
15725 range: lsp::Range::new(
15726 lsp::Position::new(5, 4),
15727 lsp::Position::new(5, 27),
15728 ),
15729 new_text: "".to_string(),
15730 }],
15731 )]
15732 .into_iter()
15733 .collect(),
15734 ),
15735 ..Default::default()
15736 }),
15737 ..Default::default()
15738 },
15739 )]))
15740 },
15741 );
15742 cx.update_editor(|editor, window, cx| {
15743 editor.toggle_code_actions(
15744 &ToggleCodeActions {
15745 deployed_from: None,
15746 quick_launch: false,
15747 },
15748 window,
15749 cx,
15750 );
15751 });
15752 code_action_requests.next().await;
15753 cx.run_until_parked();
15754 cx.condition(|editor, _| editor.context_menu_visible())
15755 .await;
15756 cx.update_editor(|editor, _, _| {
15757 assert!(
15758 !editor.hover_state.visible(),
15759 "Hover popover should be hidden when code action menu is shown"
15760 );
15761 // Hide code actions
15762 editor.context_menu.take();
15763 });
15764
15765 // Case 2: Test that code completions hide hover popover
15766 cx.dispatch_action(Hover);
15767 hover_requests.next().await;
15768 cx.condition(|editor, _| editor.hover_state.visible()).await;
15769 let counter = Arc::new(AtomicUsize::new(0));
15770 let mut completion_requests =
15771 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15772 let counter = counter.clone();
15773 async move {
15774 counter.fetch_add(1, atomic::Ordering::Release);
15775 Ok(Some(lsp::CompletionResponse::Array(vec![
15776 lsp::CompletionItem {
15777 label: "main".into(),
15778 kind: Some(lsp::CompletionItemKind::FUNCTION),
15779 detail: Some("() -> ()".to_string()),
15780 ..Default::default()
15781 },
15782 lsp::CompletionItem {
15783 label: "TestStruct".into(),
15784 kind: Some(lsp::CompletionItemKind::STRUCT),
15785 detail: Some("struct TestStruct".to_string()),
15786 ..Default::default()
15787 },
15788 ])))
15789 }
15790 });
15791 cx.update_editor(|editor, window, cx| {
15792 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15793 });
15794 completion_requests.next().await;
15795 cx.condition(|editor, _| editor.context_menu_visible())
15796 .await;
15797 cx.update_editor(|editor, _, _| {
15798 assert!(
15799 !editor.hover_state.visible(),
15800 "Hover popover should be hidden when completion menu is shown"
15801 );
15802 });
15803}
15804
15805#[gpui::test]
15806async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
15807 init_test(cx, |_| {});
15808
15809 let mut cx = EditorLspTestContext::new_rust(
15810 lsp::ServerCapabilities {
15811 completion_provider: Some(lsp::CompletionOptions {
15812 trigger_characters: Some(vec![".".to_string()]),
15813 resolve_provider: Some(true),
15814 ..Default::default()
15815 }),
15816 ..Default::default()
15817 },
15818 cx,
15819 )
15820 .await;
15821
15822 cx.set_state("fn main() { let a = 2ˇ; }");
15823 cx.simulate_keystroke(".");
15824
15825 let unresolved_item_1 = lsp::CompletionItem {
15826 label: "id".to_string(),
15827 filter_text: Some("id".to_string()),
15828 detail: None,
15829 documentation: None,
15830 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15831 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15832 new_text: ".id".to_string(),
15833 })),
15834 ..lsp::CompletionItem::default()
15835 };
15836 let resolved_item_1 = lsp::CompletionItem {
15837 additional_text_edits: Some(vec![lsp::TextEdit {
15838 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15839 new_text: "!!".to_string(),
15840 }]),
15841 ..unresolved_item_1.clone()
15842 };
15843 let unresolved_item_2 = lsp::CompletionItem {
15844 label: "other".to_string(),
15845 filter_text: Some("other".to_string()),
15846 detail: None,
15847 documentation: None,
15848 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15849 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15850 new_text: ".other".to_string(),
15851 })),
15852 ..lsp::CompletionItem::default()
15853 };
15854 let resolved_item_2 = lsp::CompletionItem {
15855 additional_text_edits: Some(vec![lsp::TextEdit {
15856 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15857 new_text: "??".to_string(),
15858 }]),
15859 ..unresolved_item_2.clone()
15860 };
15861
15862 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
15863 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
15864 cx.lsp
15865 .server
15866 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15867 let unresolved_item_1 = unresolved_item_1.clone();
15868 let resolved_item_1 = resolved_item_1.clone();
15869 let unresolved_item_2 = unresolved_item_2.clone();
15870 let resolved_item_2 = resolved_item_2.clone();
15871 let resolve_requests_1 = resolve_requests_1.clone();
15872 let resolve_requests_2 = resolve_requests_2.clone();
15873 move |unresolved_request, _| {
15874 let unresolved_item_1 = unresolved_item_1.clone();
15875 let resolved_item_1 = resolved_item_1.clone();
15876 let unresolved_item_2 = unresolved_item_2.clone();
15877 let resolved_item_2 = resolved_item_2.clone();
15878 let resolve_requests_1 = resolve_requests_1.clone();
15879 let resolve_requests_2 = resolve_requests_2.clone();
15880 async move {
15881 if unresolved_request == unresolved_item_1 {
15882 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
15883 Ok(resolved_item_1.clone())
15884 } else if unresolved_request == unresolved_item_2 {
15885 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
15886 Ok(resolved_item_2.clone())
15887 } else {
15888 panic!("Unexpected completion item {unresolved_request:?}")
15889 }
15890 }
15891 }
15892 })
15893 .detach();
15894
15895 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15896 let unresolved_item_1 = unresolved_item_1.clone();
15897 let unresolved_item_2 = unresolved_item_2.clone();
15898 async move {
15899 Ok(Some(lsp::CompletionResponse::Array(vec![
15900 unresolved_item_1,
15901 unresolved_item_2,
15902 ])))
15903 }
15904 })
15905 .next()
15906 .await;
15907
15908 cx.condition(|editor, _| editor.context_menu_visible())
15909 .await;
15910 cx.update_editor(|editor, _, _| {
15911 let context_menu = editor.context_menu.borrow_mut();
15912 let context_menu = context_menu
15913 .as_ref()
15914 .expect("Should have the context menu deployed");
15915 match context_menu {
15916 CodeContextMenu::Completions(completions_menu) => {
15917 let completions = completions_menu.completions.borrow_mut();
15918 assert_eq!(
15919 completions
15920 .iter()
15921 .map(|completion| &completion.label.text)
15922 .collect::<Vec<_>>(),
15923 vec!["id", "other"]
15924 )
15925 }
15926 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15927 }
15928 });
15929 cx.run_until_parked();
15930
15931 cx.update_editor(|editor, window, cx| {
15932 editor.context_menu_next(&ContextMenuNext, window, cx);
15933 });
15934 cx.run_until_parked();
15935 cx.update_editor(|editor, window, cx| {
15936 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15937 });
15938 cx.run_until_parked();
15939 cx.update_editor(|editor, window, cx| {
15940 editor.context_menu_next(&ContextMenuNext, window, cx);
15941 });
15942 cx.run_until_parked();
15943 cx.update_editor(|editor, window, cx| {
15944 editor
15945 .compose_completion(&ComposeCompletion::default(), window, cx)
15946 .expect("No task returned")
15947 })
15948 .await
15949 .expect("Completion failed");
15950 cx.run_until_parked();
15951
15952 cx.update_editor(|editor, _, cx| {
15953 assert_eq!(
15954 resolve_requests_1.load(atomic::Ordering::Acquire),
15955 1,
15956 "Should always resolve once despite multiple selections"
15957 );
15958 assert_eq!(
15959 resolve_requests_2.load(atomic::Ordering::Acquire),
15960 1,
15961 "Should always resolve once after multiple selections and applying the completion"
15962 );
15963 assert_eq!(
15964 editor.text(cx),
15965 "fn main() { let a = ??.other; }",
15966 "Should use resolved data when applying the completion"
15967 );
15968 });
15969}
15970
15971#[gpui::test]
15972async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
15973 init_test(cx, |_| {});
15974
15975 let item_0 = lsp::CompletionItem {
15976 label: "abs".into(),
15977 insert_text: Some("abs".into()),
15978 data: Some(json!({ "very": "special"})),
15979 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
15980 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
15981 lsp::InsertReplaceEdit {
15982 new_text: "abs".to_string(),
15983 insert: lsp::Range::default(),
15984 replace: lsp::Range::default(),
15985 },
15986 )),
15987 ..lsp::CompletionItem::default()
15988 };
15989 let items = iter::once(item_0.clone())
15990 .chain((11..51).map(|i| lsp::CompletionItem {
15991 label: format!("item_{}", i),
15992 insert_text: Some(format!("item_{}", i)),
15993 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15994 ..lsp::CompletionItem::default()
15995 }))
15996 .collect::<Vec<_>>();
15997
15998 let default_commit_characters = vec!["?".to_string()];
15999 let default_data = json!({ "default": "data"});
16000 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
16001 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
16002 let default_edit_range = lsp::Range {
16003 start: lsp::Position {
16004 line: 0,
16005 character: 5,
16006 },
16007 end: lsp::Position {
16008 line: 0,
16009 character: 5,
16010 },
16011 };
16012
16013 let mut cx = EditorLspTestContext::new_rust(
16014 lsp::ServerCapabilities {
16015 completion_provider: Some(lsp::CompletionOptions {
16016 trigger_characters: Some(vec![".".to_string()]),
16017 resolve_provider: Some(true),
16018 ..Default::default()
16019 }),
16020 ..Default::default()
16021 },
16022 cx,
16023 )
16024 .await;
16025
16026 cx.set_state("fn main() { let a = 2ˇ; }");
16027 cx.simulate_keystroke(".");
16028
16029 let completion_data = default_data.clone();
16030 let completion_characters = default_commit_characters.clone();
16031 let completion_items = items.clone();
16032 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16033 let default_data = completion_data.clone();
16034 let default_commit_characters = completion_characters.clone();
16035 let items = completion_items.clone();
16036 async move {
16037 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16038 items,
16039 item_defaults: Some(lsp::CompletionListItemDefaults {
16040 data: Some(default_data.clone()),
16041 commit_characters: Some(default_commit_characters.clone()),
16042 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
16043 default_edit_range,
16044 )),
16045 insert_text_format: Some(default_insert_text_format),
16046 insert_text_mode: Some(default_insert_text_mode),
16047 }),
16048 ..lsp::CompletionList::default()
16049 })))
16050 }
16051 })
16052 .next()
16053 .await;
16054
16055 let resolved_items = Arc::new(Mutex::new(Vec::new()));
16056 cx.lsp
16057 .server
16058 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
16059 let closure_resolved_items = resolved_items.clone();
16060 move |item_to_resolve, _| {
16061 let closure_resolved_items = closure_resolved_items.clone();
16062 async move {
16063 closure_resolved_items.lock().push(item_to_resolve.clone());
16064 Ok(item_to_resolve)
16065 }
16066 }
16067 })
16068 .detach();
16069
16070 cx.condition(|editor, _| editor.context_menu_visible())
16071 .await;
16072 cx.run_until_parked();
16073 cx.update_editor(|editor, _, _| {
16074 let menu = editor.context_menu.borrow_mut();
16075 match menu.as_ref().expect("should have the completions menu") {
16076 CodeContextMenu::Completions(completions_menu) => {
16077 assert_eq!(
16078 completions_menu
16079 .entries
16080 .borrow()
16081 .iter()
16082 .map(|mat| mat.string.clone())
16083 .collect::<Vec<String>>(),
16084 items
16085 .iter()
16086 .map(|completion| completion.label.clone())
16087 .collect::<Vec<String>>()
16088 );
16089 }
16090 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
16091 }
16092 });
16093 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
16094 // with 4 from the end.
16095 assert_eq!(
16096 *resolved_items.lock(),
16097 [&items[0..16], &items[items.len() - 4..items.len()]]
16098 .concat()
16099 .iter()
16100 .cloned()
16101 .map(|mut item| {
16102 if item.data.is_none() {
16103 item.data = Some(default_data.clone());
16104 }
16105 item
16106 })
16107 .collect::<Vec<lsp::CompletionItem>>(),
16108 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
16109 );
16110 resolved_items.lock().clear();
16111
16112 cx.update_editor(|editor, window, cx| {
16113 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
16114 });
16115 cx.run_until_parked();
16116 // Completions that have already been resolved are skipped.
16117 assert_eq!(
16118 *resolved_items.lock(),
16119 items[items.len() - 17..items.len() - 4]
16120 .iter()
16121 .cloned()
16122 .map(|mut item| {
16123 if item.data.is_none() {
16124 item.data = Some(default_data.clone());
16125 }
16126 item
16127 })
16128 .collect::<Vec<lsp::CompletionItem>>()
16129 );
16130 resolved_items.lock().clear();
16131}
16132
16133#[gpui::test]
16134async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
16135 init_test(cx, |_| {});
16136
16137 let mut cx = EditorLspTestContext::new(
16138 Language::new(
16139 LanguageConfig {
16140 matcher: LanguageMatcher {
16141 path_suffixes: vec!["jsx".into()],
16142 ..Default::default()
16143 },
16144 overrides: [(
16145 "element".into(),
16146 LanguageConfigOverride {
16147 completion_query_characters: Override::Set(['-'].into_iter().collect()),
16148 ..Default::default()
16149 },
16150 )]
16151 .into_iter()
16152 .collect(),
16153 ..Default::default()
16154 },
16155 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16156 )
16157 .with_override_query("(jsx_self_closing_element) @element")
16158 .unwrap(),
16159 lsp::ServerCapabilities {
16160 completion_provider: Some(lsp::CompletionOptions {
16161 trigger_characters: Some(vec![":".to_string()]),
16162 ..Default::default()
16163 }),
16164 ..Default::default()
16165 },
16166 cx,
16167 )
16168 .await;
16169
16170 cx.lsp
16171 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16172 Ok(Some(lsp::CompletionResponse::Array(vec![
16173 lsp::CompletionItem {
16174 label: "bg-blue".into(),
16175 ..Default::default()
16176 },
16177 lsp::CompletionItem {
16178 label: "bg-red".into(),
16179 ..Default::default()
16180 },
16181 lsp::CompletionItem {
16182 label: "bg-yellow".into(),
16183 ..Default::default()
16184 },
16185 ])))
16186 });
16187
16188 cx.set_state(r#"<p class="bgˇ" />"#);
16189
16190 // Trigger completion when typing a dash, because the dash is an extra
16191 // word character in the 'element' scope, which contains the cursor.
16192 cx.simulate_keystroke("-");
16193 cx.executor().run_until_parked();
16194 cx.update_editor(|editor, _, _| {
16195 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16196 {
16197 assert_eq!(
16198 completion_menu_entries(&menu),
16199 &["bg-blue", "bg-red", "bg-yellow"]
16200 );
16201 } else {
16202 panic!("expected completion menu to be open");
16203 }
16204 });
16205
16206 cx.simulate_keystroke("l");
16207 cx.executor().run_until_parked();
16208 cx.update_editor(|editor, _, _| {
16209 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16210 {
16211 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
16212 } else {
16213 panic!("expected completion menu to be open");
16214 }
16215 });
16216
16217 // When filtering completions, consider the character after the '-' to
16218 // be the start of a subword.
16219 cx.set_state(r#"<p class="yelˇ" />"#);
16220 cx.simulate_keystroke("l");
16221 cx.executor().run_until_parked();
16222 cx.update_editor(|editor, _, _| {
16223 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16224 {
16225 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
16226 } else {
16227 panic!("expected completion menu to be open");
16228 }
16229 });
16230}
16231
16232fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
16233 let entries = menu.entries.borrow();
16234 entries.iter().map(|mat| mat.string.clone()).collect()
16235}
16236
16237#[gpui::test]
16238async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
16239 init_test(cx, |settings| {
16240 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
16241 Formatter::Prettier,
16242 )))
16243 });
16244
16245 let fs = FakeFs::new(cx.executor());
16246 fs.insert_file(path!("/file.ts"), Default::default()).await;
16247
16248 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
16249 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16250
16251 language_registry.add(Arc::new(Language::new(
16252 LanguageConfig {
16253 name: "TypeScript".into(),
16254 matcher: LanguageMatcher {
16255 path_suffixes: vec!["ts".to_string()],
16256 ..Default::default()
16257 },
16258 ..Default::default()
16259 },
16260 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
16261 )));
16262 update_test_language_settings(cx, |settings| {
16263 settings.defaults.prettier = Some(PrettierSettings {
16264 allowed: true,
16265 ..PrettierSettings::default()
16266 });
16267 });
16268
16269 let test_plugin = "test_plugin";
16270 let _ = language_registry.register_fake_lsp(
16271 "TypeScript",
16272 FakeLspAdapter {
16273 prettier_plugins: vec![test_plugin],
16274 ..Default::default()
16275 },
16276 );
16277
16278 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
16279 let buffer = project
16280 .update(cx, |project, cx| {
16281 project.open_local_buffer(path!("/file.ts"), cx)
16282 })
16283 .await
16284 .unwrap();
16285
16286 let buffer_text = "one\ntwo\nthree\n";
16287 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16288 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16289 editor.update_in(cx, |editor, window, cx| {
16290 editor.set_text(buffer_text, window, cx)
16291 });
16292
16293 editor
16294 .update_in(cx, |editor, window, cx| {
16295 editor.perform_format(
16296 project.clone(),
16297 FormatTrigger::Manual,
16298 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16299 window,
16300 cx,
16301 )
16302 })
16303 .unwrap()
16304 .await;
16305 assert_eq!(
16306 editor.update(cx, |editor, cx| editor.text(cx)),
16307 buffer_text.to_string() + prettier_format_suffix,
16308 "Test prettier formatting was not applied to the original buffer text",
16309 );
16310
16311 update_test_language_settings(cx, |settings| {
16312 settings.defaults.formatter = Some(SelectedFormatter::Auto)
16313 });
16314 let format = editor.update_in(cx, |editor, window, cx| {
16315 editor.perform_format(
16316 project.clone(),
16317 FormatTrigger::Manual,
16318 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16319 window,
16320 cx,
16321 )
16322 });
16323 format.await.unwrap();
16324 assert_eq!(
16325 editor.update(cx, |editor, cx| editor.text(cx)),
16326 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
16327 "Autoformatting (via test prettier) was not applied to the original buffer text",
16328 );
16329}
16330
16331#[gpui::test]
16332async fn test_addition_reverts(cx: &mut TestAppContext) {
16333 init_test(cx, |_| {});
16334 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16335 let base_text = indoc! {r#"
16336 struct Row;
16337 struct Row1;
16338 struct Row2;
16339
16340 struct Row4;
16341 struct Row5;
16342 struct Row6;
16343
16344 struct Row8;
16345 struct Row9;
16346 struct Row10;"#};
16347
16348 // When addition hunks are not adjacent to carets, no hunk revert is performed
16349 assert_hunk_revert(
16350 indoc! {r#"struct Row;
16351 struct Row1;
16352 struct Row1.1;
16353 struct Row1.2;
16354 struct Row2;ˇ
16355
16356 struct Row4;
16357 struct Row5;
16358 struct Row6;
16359
16360 struct Row8;
16361 ˇstruct Row9;
16362 struct Row9.1;
16363 struct Row9.2;
16364 struct Row9.3;
16365 struct Row10;"#},
16366 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16367 indoc! {r#"struct Row;
16368 struct Row1;
16369 struct Row1.1;
16370 struct Row1.2;
16371 struct Row2;ˇ
16372
16373 struct Row4;
16374 struct Row5;
16375 struct Row6;
16376
16377 struct Row8;
16378 ˇstruct Row9;
16379 struct Row9.1;
16380 struct Row9.2;
16381 struct Row9.3;
16382 struct Row10;"#},
16383 base_text,
16384 &mut cx,
16385 );
16386 // Same for selections
16387 assert_hunk_revert(
16388 indoc! {r#"struct Row;
16389 struct Row1;
16390 struct Row2;
16391 struct Row2.1;
16392 struct Row2.2;
16393 «ˇ
16394 struct Row4;
16395 struct» Row5;
16396 «struct Row6;
16397 ˇ»
16398 struct Row9.1;
16399 struct Row9.2;
16400 struct Row9.3;
16401 struct Row8;
16402 struct Row9;
16403 struct Row10;"#},
16404 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16405 indoc! {r#"struct Row;
16406 struct Row1;
16407 struct Row2;
16408 struct Row2.1;
16409 struct Row2.2;
16410 «ˇ
16411 struct Row4;
16412 struct» Row5;
16413 «struct Row6;
16414 ˇ»
16415 struct Row9.1;
16416 struct Row9.2;
16417 struct Row9.3;
16418 struct Row8;
16419 struct Row9;
16420 struct Row10;"#},
16421 base_text,
16422 &mut cx,
16423 );
16424
16425 // When carets and selections intersect the addition hunks, those are reverted.
16426 // Adjacent carets got merged.
16427 assert_hunk_revert(
16428 indoc! {r#"struct Row;
16429 ˇ// something on the top
16430 struct Row1;
16431 struct Row2;
16432 struct Roˇw3.1;
16433 struct Row2.2;
16434 struct Row2.3;ˇ
16435
16436 struct Row4;
16437 struct ˇRow5.1;
16438 struct Row5.2;
16439 struct «Rowˇ»5.3;
16440 struct Row5;
16441 struct Row6;
16442 ˇ
16443 struct Row9.1;
16444 struct «Rowˇ»9.2;
16445 struct «ˇRow»9.3;
16446 struct Row8;
16447 struct Row9;
16448 «ˇ// something on bottom»
16449 struct Row10;"#},
16450 vec![
16451 DiffHunkStatusKind::Added,
16452 DiffHunkStatusKind::Added,
16453 DiffHunkStatusKind::Added,
16454 DiffHunkStatusKind::Added,
16455 DiffHunkStatusKind::Added,
16456 ],
16457 indoc! {r#"struct Row;
16458 ˇstruct Row1;
16459 struct Row2;
16460 ˇ
16461 struct Row4;
16462 ˇstruct Row5;
16463 struct Row6;
16464 ˇ
16465 ˇstruct Row8;
16466 struct Row9;
16467 ˇstruct Row10;"#},
16468 base_text,
16469 &mut cx,
16470 );
16471}
16472
16473#[gpui::test]
16474async fn test_modification_reverts(cx: &mut TestAppContext) {
16475 init_test(cx, |_| {});
16476 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16477 let base_text = indoc! {r#"
16478 struct Row;
16479 struct Row1;
16480 struct Row2;
16481
16482 struct Row4;
16483 struct Row5;
16484 struct Row6;
16485
16486 struct Row8;
16487 struct Row9;
16488 struct Row10;"#};
16489
16490 // Modification hunks behave the same as the addition ones.
16491 assert_hunk_revert(
16492 indoc! {r#"struct Row;
16493 struct Row1;
16494 struct Row33;
16495 ˇ
16496 struct Row4;
16497 struct Row5;
16498 struct Row6;
16499 ˇ
16500 struct Row99;
16501 struct Row9;
16502 struct Row10;"#},
16503 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16504 indoc! {r#"struct Row;
16505 struct Row1;
16506 struct Row33;
16507 ˇ
16508 struct Row4;
16509 struct Row5;
16510 struct Row6;
16511 ˇ
16512 struct Row99;
16513 struct Row9;
16514 struct Row10;"#},
16515 base_text,
16516 &mut cx,
16517 );
16518 assert_hunk_revert(
16519 indoc! {r#"struct Row;
16520 struct Row1;
16521 struct Row33;
16522 «ˇ
16523 struct Row4;
16524 struct» Row5;
16525 «struct Row6;
16526 ˇ»
16527 struct Row99;
16528 struct Row9;
16529 struct Row10;"#},
16530 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16531 indoc! {r#"struct Row;
16532 struct Row1;
16533 struct Row33;
16534 «ˇ
16535 struct Row4;
16536 struct» Row5;
16537 «struct Row6;
16538 ˇ»
16539 struct Row99;
16540 struct Row9;
16541 struct Row10;"#},
16542 base_text,
16543 &mut cx,
16544 );
16545
16546 assert_hunk_revert(
16547 indoc! {r#"ˇstruct Row1.1;
16548 struct Row1;
16549 «ˇstr»uct Row22;
16550
16551 struct ˇRow44;
16552 struct Row5;
16553 struct «Rˇ»ow66;ˇ
16554
16555 «struˇ»ct Row88;
16556 struct Row9;
16557 struct Row1011;ˇ"#},
16558 vec![
16559 DiffHunkStatusKind::Modified,
16560 DiffHunkStatusKind::Modified,
16561 DiffHunkStatusKind::Modified,
16562 DiffHunkStatusKind::Modified,
16563 DiffHunkStatusKind::Modified,
16564 DiffHunkStatusKind::Modified,
16565 ],
16566 indoc! {r#"struct Row;
16567 ˇstruct Row1;
16568 struct Row2;
16569 ˇ
16570 struct Row4;
16571 ˇstruct Row5;
16572 struct Row6;
16573 ˇ
16574 struct Row8;
16575 ˇstruct Row9;
16576 struct Row10;ˇ"#},
16577 base_text,
16578 &mut cx,
16579 );
16580}
16581
16582#[gpui::test]
16583async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
16584 init_test(cx, |_| {});
16585 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16586 let base_text = indoc! {r#"
16587 one
16588
16589 two
16590 three
16591 "#};
16592
16593 cx.set_head_text(base_text);
16594 cx.set_state("\nˇ\n");
16595 cx.executor().run_until_parked();
16596 cx.update_editor(|editor, _window, cx| {
16597 editor.expand_selected_diff_hunks(cx);
16598 });
16599 cx.executor().run_until_parked();
16600 cx.update_editor(|editor, window, cx| {
16601 editor.backspace(&Default::default(), window, cx);
16602 });
16603 cx.run_until_parked();
16604 cx.assert_state_with_diff(
16605 indoc! {r#"
16606
16607 - two
16608 - threeˇ
16609 +
16610 "#}
16611 .to_string(),
16612 );
16613}
16614
16615#[gpui::test]
16616async fn test_deletion_reverts(cx: &mut TestAppContext) {
16617 init_test(cx, |_| {});
16618 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16619 let base_text = indoc! {r#"struct Row;
16620struct Row1;
16621struct Row2;
16622
16623struct Row4;
16624struct Row5;
16625struct Row6;
16626
16627struct Row8;
16628struct Row9;
16629struct Row10;"#};
16630
16631 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
16632 assert_hunk_revert(
16633 indoc! {r#"struct Row;
16634 struct Row2;
16635
16636 ˇstruct Row4;
16637 struct Row5;
16638 struct Row6;
16639 ˇ
16640 struct Row8;
16641 struct Row10;"#},
16642 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16643 indoc! {r#"struct Row;
16644 struct Row2;
16645
16646 ˇstruct Row4;
16647 struct Row5;
16648 struct Row6;
16649 ˇ
16650 struct Row8;
16651 struct Row10;"#},
16652 base_text,
16653 &mut cx,
16654 );
16655 assert_hunk_revert(
16656 indoc! {r#"struct Row;
16657 struct Row2;
16658
16659 «ˇstruct Row4;
16660 struct» Row5;
16661 «struct Row6;
16662 ˇ»
16663 struct Row8;
16664 struct Row10;"#},
16665 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16666 indoc! {r#"struct Row;
16667 struct Row2;
16668
16669 «ˇstruct Row4;
16670 struct» Row5;
16671 «struct Row6;
16672 ˇ»
16673 struct Row8;
16674 struct Row10;"#},
16675 base_text,
16676 &mut cx,
16677 );
16678
16679 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
16680 assert_hunk_revert(
16681 indoc! {r#"struct Row;
16682 ˇstruct Row2;
16683
16684 struct Row4;
16685 struct Row5;
16686 struct Row6;
16687
16688 struct Row8;ˇ
16689 struct Row10;"#},
16690 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16691 indoc! {r#"struct Row;
16692 struct Row1;
16693 ˇstruct Row2;
16694
16695 struct Row4;
16696 struct Row5;
16697 struct Row6;
16698
16699 struct Row8;ˇ
16700 struct Row9;
16701 struct Row10;"#},
16702 base_text,
16703 &mut cx,
16704 );
16705 assert_hunk_revert(
16706 indoc! {r#"struct Row;
16707 struct Row2«ˇ;
16708 struct Row4;
16709 struct» Row5;
16710 «struct Row6;
16711
16712 struct Row8;ˇ»
16713 struct Row10;"#},
16714 vec![
16715 DiffHunkStatusKind::Deleted,
16716 DiffHunkStatusKind::Deleted,
16717 DiffHunkStatusKind::Deleted,
16718 ],
16719 indoc! {r#"struct Row;
16720 struct Row1;
16721 struct Row2«ˇ;
16722
16723 struct Row4;
16724 struct» Row5;
16725 «struct Row6;
16726
16727 struct Row8;ˇ»
16728 struct Row9;
16729 struct Row10;"#},
16730 base_text,
16731 &mut cx,
16732 );
16733}
16734
16735#[gpui::test]
16736async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
16737 init_test(cx, |_| {});
16738
16739 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
16740 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
16741 let base_text_3 =
16742 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
16743
16744 let text_1 = edit_first_char_of_every_line(base_text_1);
16745 let text_2 = edit_first_char_of_every_line(base_text_2);
16746 let text_3 = edit_first_char_of_every_line(base_text_3);
16747
16748 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
16749 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
16750 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
16751
16752 let multibuffer = cx.new(|cx| {
16753 let mut multibuffer = MultiBuffer::new(ReadWrite);
16754 multibuffer.push_excerpts(
16755 buffer_1.clone(),
16756 [
16757 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16758 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16759 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16760 ],
16761 cx,
16762 );
16763 multibuffer.push_excerpts(
16764 buffer_2.clone(),
16765 [
16766 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16767 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16768 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16769 ],
16770 cx,
16771 );
16772 multibuffer.push_excerpts(
16773 buffer_3.clone(),
16774 [
16775 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16776 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16777 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16778 ],
16779 cx,
16780 );
16781 multibuffer
16782 });
16783
16784 let fs = FakeFs::new(cx.executor());
16785 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
16786 let (editor, cx) = cx
16787 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
16788 editor.update_in(cx, |editor, _window, cx| {
16789 for (buffer, diff_base) in [
16790 (buffer_1.clone(), base_text_1),
16791 (buffer_2.clone(), base_text_2),
16792 (buffer_3.clone(), base_text_3),
16793 ] {
16794 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16795 editor
16796 .buffer
16797 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16798 }
16799 });
16800 cx.executor().run_until_parked();
16801
16802 editor.update_in(cx, |editor, window, cx| {
16803 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}");
16804 editor.select_all(&SelectAll, window, cx);
16805 editor.git_restore(&Default::default(), window, cx);
16806 });
16807 cx.executor().run_until_parked();
16808
16809 // When all ranges are selected, all buffer hunks are reverted.
16810 editor.update(cx, |editor, cx| {
16811 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");
16812 });
16813 buffer_1.update(cx, |buffer, _| {
16814 assert_eq!(buffer.text(), base_text_1);
16815 });
16816 buffer_2.update(cx, |buffer, _| {
16817 assert_eq!(buffer.text(), base_text_2);
16818 });
16819 buffer_3.update(cx, |buffer, _| {
16820 assert_eq!(buffer.text(), base_text_3);
16821 });
16822
16823 editor.update_in(cx, |editor, window, cx| {
16824 editor.undo(&Default::default(), window, cx);
16825 });
16826
16827 editor.update_in(cx, |editor, window, cx| {
16828 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16829 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
16830 });
16831 editor.git_restore(&Default::default(), window, cx);
16832 });
16833
16834 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
16835 // but not affect buffer_2 and its related excerpts.
16836 editor.update(cx, |editor, cx| {
16837 assert_eq!(
16838 editor.text(cx),
16839 "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}"
16840 );
16841 });
16842 buffer_1.update(cx, |buffer, _| {
16843 assert_eq!(buffer.text(), base_text_1);
16844 });
16845 buffer_2.update(cx, |buffer, _| {
16846 assert_eq!(
16847 buffer.text(),
16848 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
16849 );
16850 });
16851 buffer_3.update(cx, |buffer, _| {
16852 assert_eq!(
16853 buffer.text(),
16854 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
16855 );
16856 });
16857
16858 fn edit_first_char_of_every_line(text: &str) -> String {
16859 text.split('\n')
16860 .map(|line| format!("X{}", &line[1..]))
16861 .collect::<Vec<_>>()
16862 .join("\n")
16863 }
16864}
16865
16866#[gpui::test]
16867async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
16868 init_test(cx, |_| {});
16869
16870 let cols = 4;
16871 let rows = 10;
16872 let sample_text_1 = sample_text(rows, cols, 'a');
16873 assert_eq!(
16874 sample_text_1,
16875 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
16876 );
16877 let sample_text_2 = sample_text(rows, cols, 'l');
16878 assert_eq!(
16879 sample_text_2,
16880 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
16881 );
16882 let sample_text_3 = sample_text(rows, cols, 'v');
16883 assert_eq!(
16884 sample_text_3,
16885 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
16886 );
16887
16888 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
16889 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
16890 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
16891
16892 let multi_buffer = cx.new(|cx| {
16893 let mut multibuffer = MultiBuffer::new(ReadWrite);
16894 multibuffer.push_excerpts(
16895 buffer_1.clone(),
16896 [
16897 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16898 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16899 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16900 ],
16901 cx,
16902 );
16903 multibuffer.push_excerpts(
16904 buffer_2.clone(),
16905 [
16906 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16907 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16908 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16909 ],
16910 cx,
16911 );
16912 multibuffer.push_excerpts(
16913 buffer_3.clone(),
16914 [
16915 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16916 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16917 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16918 ],
16919 cx,
16920 );
16921 multibuffer
16922 });
16923
16924 let fs = FakeFs::new(cx.executor());
16925 fs.insert_tree(
16926 "/a",
16927 json!({
16928 "main.rs": sample_text_1,
16929 "other.rs": sample_text_2,
16930 "lib.rs": sample_text_3,
16931 }),
16932 )
16933 .await;
16934 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16935 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16936 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16937 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16938 Editor::new(
16939 EditorMode::full(),
16940 multi_buffer,
16941 Some(project.clone()),
16942 window,
16943 cx,
16944 )
16945 });
16946 let multibuffer_item_id = workspace
16947 .update(cx, |workspace, window, cx| {
16948 assert!(
16949 workspace.active_item(cx).is_none(),
16950 "active item should be None before the first item is added"
16951 );
16952 workspace.add_item_to_active_pane(
16953 Box::new(multi_buffer_editor.clone()),
16954 None,
16955 true,
16956 window,
16957 cx,
16958 );
16959 let active_item = workspace
16960 .active_item(cx)
16961 .expect("should have an active item after adding the multi buffer");
16962 assert!(
16963 !active_item.is_singleton(cx),
16964 "A multi buffer was expected to active after adding"
16965 );
16966 active_item.item_id()
16967 })
16968 .unwrap();
16969 cx.executor().run_until_parked();
16970
16971 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16972 editor.change_selections(
16973 SelectionEffects::scroll(Autoscroll::Next),
16974 window,
16975 cx,
16976 |s| s.select_ranges(Some(1..2)),
16977 );
16978 editor.open_excerpts(&OpenExcerpts, window, cx);
16979 });
16980 cx.executor().run_until_parked();
16981 let first_item_id = workspace
16982 .update(cx, |workspace, window, cx| {
16983 let active_item = workspace
16984 .active_item(cx)
16985 .expect("should have an active item after navigating into the 1st buffer");
16986 let first_item_id = active_item.item_id();
16987 assert_ne!(
16988 first_item_id, multibuffer_item_id,
16989 "Should navigate into the 1st buffer and activate it"
16990 );
16991 assert!(
16992 active_item.is_singleton(cx),
16993 "New active item should be a singleton buffer"
16994 );
16995 assert_eq!(
16996 active_item
16997 .act_as::<Editor>(cx)
16998 .expect("should have navigated into an editor for the 1st buffer")
16999 .read(cx)
17000 .text(cx),
17001 sample_text_1
17002 );
17003
17004 workspace
17005 .go_back(workspace.active_pane().downgrade(), window, cx)
17006 .detach_and_log_err(cx);
17007
17008 first_item_id
17009 })
17010 .unwrap();
17011 cx.executor().run_until_parked();
17012 workspace
17013 .update(cx, |workspace, _, cx| {
17014 let active_item = workspace
17015 .active_item(cx)
17016 .expect("should have an active item after navigating back");
17017 assert_eq!(
17018 active_item.item_id(),
17019 multibuffer_item_id,
17020 "Should navigate back to the multi buffer"
17021 );
17022 assert!(!active_item.is_singleton(cx));
17023 })
17024 .unwrap();
17025
17026 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17027 editor.change_selections(
17028 SelectionEffects::scroll(Autoscroll::Next),
17029 window,
17030 cx,
17031 |s| s.select_ranges(Some(39..40)),
17032 );
17033 editor.open_excerpts(&OpenExcerpts, window, cx);
17034 });
17035 cx.executor().run_until_parked();
17036 let second_item_id = workspace
17037 .update(cx, |workspace, window, cx| {
17038 let active_item = workspace
17039 .active_item(cx)
17040 .expect("should have an active item after navigating into the 2nd buffer");
17041 let second_item_id = active_item.item_id();
17042 assert_ne!(
17043 second_item_id, multibuffer_item_id,
17044 "Should navigate away from the multibuffer"
17045 );
17046 assert_ne!(
17047 second_item_id, first_item_id,
17048 "Should navigate into the 2nd buffer and activate it"
17049 );
17050 assert!(
17051 active_item.is_singleton(cx),
17052 "New active item should be a singleton buffer"
17053 );
17054 assert_eq!(
17055 active_item
17056 .act_as::<Editor>(cx)
17057 .expect("should have navigated into an editor")
17058 .read(cx)
17059 .text(cx),
17060 sample_text_2
17061 );
17062
17063 workspace
17064 .go_back(workspace.active_pane().downgrade(), window, cx)
17065 .detach_and_log_err(cx);
17066
17067 second_item_id
17068 })
17069 .unwrap();
17070 cx.executor().run_until_parked();
17071 workspace
17072 .update(cx, |workspace, _, cx| {
17073 let active_item = workspace
17074 .active_item(cx)
17075 .expect("should have an active item after navigating back from the 2nd buffer");
17076 assert_eq!(
17077 active_item.item_id(),
17078 multibuffer_item_id,
17079 "Should navigate back from the 2nd buffer to the multi buffer"
17080 );
17081 assert!(!active_item.is_singleton(cx));
17082 })
17083 .unwrap();
17084
17085 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17086 editor.change_selections(
17087 SelectionEffects::scroll(Autoscroll::Next),
17088 window,
17089 cx,
17090 |s| s.select_ranges(Some(70..70)),
17091 );
17092 editor.open_excerpts(&OpenExcerpts, window, cx);
17093 });
17094 cx.executor().run_until_parked();
17095 workspace
17096 .update(cx, |workspace, window, cx| {
17097 let active_item = workspace
17098 .active_item(cx)
17099 .expect("should have an active item after navigating into the 3rd buffer");
17100 let third_item_id = active_item.item_id();
17101 assert_ne!(
17102 third_item_id, multibuffer_item_id,
17103 "Should navigate into the 3rd buffer and activate it"
17104 );
17105 assert_ne!(third_item_id, first_item_id);
17106 assert_ne!(third_item_id, second_item_id);
17107 assert!(
17108 active_item.is_singleton(cx),
17109 "New active item should be a singleton buffer"
17110 );
17111 assert_eq!(
17112 active_item
17113 .act_as::<Editor>(cx)
17114 .expect("should have navigated into an editor")
17115 .read(cx)
17116 .text(cx),
17117 sample_text_3
17118 );
17119
17120 workspace
17121 .go_back(workspace.active_pane().downgrade(), window, cx)
17122 .detach_and_log_err(cx);
17123 })
17124 .unwrap();
17125 cx.executor().run_until_parked();
17126 workspace
17127 .update(cx, |workspace, _, cx| {
17128 let active_item = workspace
17129 .active_item(cx)
17130 .expect("should have an active item after navigating back from the 3rd buffer");
17131 assert_eq!(
17132 active_item.item_id(),
17133 multibuffer_item_id,
17134 "Should navigate back from the 3rd buffer to the multi buffer"
17135 );
17136 assert!(!active_item.is_singleton(cx));
17137 })
17138 .unwrap();
17139}
17140
17141#[gpui::test]
17142async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17143 init_test(cx, |_| {});
17144
17145 let mut cx = EditorTestContext::new(cx).await;
17146
17147 let diff_base = r#"
17148 use some::mod;
17149
17150 const A: u32 = 42;
17151
17152 fn main() {
17153 println!("hello");
17154
17155 println!("world");
17156 }
17157 "#
17158 .unindent();
17159
17160 cx.set_state(
17161 &r#"
17162 use some::modified;
17163
17164 ˇ
17165 fn main() {
17166 println!("hello there");
17167
17168 println!("around the");
17169 println!("world");
17170 }
17171 "#
17172 .unindent(),
17173 );
17174
17175 cx.set_head_text(&diff_base);
17176 executor.run_until_parked();
17177
17178 cx.update_editor(|editor, window, cx| {
17179 editor.go_to_next_hunk(&GoToHunk, window, cx);
17180 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17181 });
17182 executor.run_until_parked();
17183 cx.assert_state_with_diff(
17184 r#"
17185 use some::modified;
17186
17187
17188 fn main() {
17189 - println!("hello");
17190 + ˇ println!("hello there");
17191
17192 println!("around the");
17193 println!("world");
17194 }
17195 "#
17196 .unindent(),
17197 );
17198
17199 cx.update_editor(|editor, window, cx| {
17200 for _ in 0..2 {
17201 editor.go_to_next_hunk(&GoToHunk, window, cx);
17202 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17203 }
17204 });
17205 executor.run_until_parked();
17206 cx.assert_state_with_diff(
17207 r#"
17208 - use some::mod;
17209 + ˇuse some::modified;
17210
17211
17212 fn main() {
17213 - println!("hello");
17214 + println!("hello there");
17215
17216 + println!("around the");
17217 println!("world");
17218 }
17219 "#
17220 .unindent(),
17221 );
17222
17223 cx.update_editor(|editor, window, cx| {
17224 editor.go_to_next_hunk(&GoToHunk, window, cx);
17225 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17226 });
17227 executor.run_until_parked();
17228 cx.assert_state_with_diff(
17229 r#"
17230 - use some::mod;
17231 + use some::modified;
17232
17233 - const A: u32 = 42;
17234 ˇ
17235 fn main() {
17236 - println!("hello");
17237 + println!("hello there");
17238
17239 + println!("around the");
17240 println!("world");
17241 }
17242 "#
17243 .unindent(),
17244 );
17245
17246 cx.update_editor(|editor, window, cx| {
17247 editor.cancel(&Cancel, window, cx);
17248 });
17249
17250 cx.assert_state_with_diff(
17251 r#"
17252 use some::modified;
17253
17254 ˇ
17255 fn main() {
17256 println!("hello there");
17257
17258 println!("around the");
17259 println!("world");
17260 }
17261 "#
17262 .unindent(),
17263 );
17264}
17265
17266#[gpui::test]
17267async fn test_diff_base_change_with_expanded_diff_hunks(
17268 executor: BackgroundExecutor,
17269 cx: &mut TestAppContext,
17270) {
17271 init_test(cx, |_| {});
17272
17273 let mut cx = EditorTestContext::new(cx).await;
17274
17275 let diff_base = r#"
17276 use some::mod1;
17277 use some::mod2;
17278
17279 const A: u32 = 42;
17280 const B: u32 = 42;
17281 const C: u32 = 42;
17282
17283 fn main() {
17284 println!("hello");
17285
17286 println!("world");
17287 }
17288 "#
17289 .unindent();
17290
17291 cx.set_state(
17292 &r#"
17293 use some::mod2;
17294
17295 const A: u32 = 42;
17296 const C: u32 = 42;
17297
17298 fn main(ˇ) {
17299 //println!("hello");
17300
17301 println!("world");
17302 //
17303 //
17304 }
17305 "#
17306 .unindent(),
17307 );
17308
17309 cx.set_head_text(&diff_base);
17310 executor.run_until_parked();
17311
17312 cx.update_editor(|editor, window, cx| {
17313 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17314 });
17315 executor.run_until_parked();
17316 cx.assert_state_with_diff(
17317 r#"
17318 - use some::mod1;
17319 use some::mod2;
17320
17321 const A: u32 = 42;
17322 - const B: u32 = 42;
17323 const C: u32 = 42;
17324
17325 fn main(ˇ) {
17326 - println!("hello");
17327 + //println!("hello");
17328
17329 println!("world");
17330 + //
17331 + //
17332 }
17333 "#
17334 .unindent(),
17335 );
17336
17337 cx.set_head_text("new diff base!");
17338 executor.run_until_parked();
17339 cx.assert_state_with_diff(
17340 r#"
17341 - new diff base!
17342 + use some::mod2;
17343 +
17344 + const A: u32 = 42;
17345 + const C: u32 = 42;
17346 +
17347 + fn main(ˇ) {
17348 + //println!("hello");
17349 +
17350 + println!("world");
17351 + //
17352 + //
17353 + }
17354 "#
17355 .unindent(),
17356 );
17357}
17358
17359#[gpui::test]
17360async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
17361 init_test(cx, |_| {});
17362
17363 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17364 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17365 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17366 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17367 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
17368 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
17369
17370 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
17371 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
17372 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
17373
17374 let multi_buffer = cx.new(|cx| {
17375 let mut multibuffer = MultiBuffer::new(ReadWrite);
17376 multibuffer.push_excerpts(
17377 buffer_1.clone(),
17378 [
17379 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17380 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17381 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17382 ],
17383 cx,
17384 );
17385 multibuffer.push_excerpts(
17386 buffer_2.clone(),
17387 [
17388 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17389 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17390 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17391 ],
17392 cx,
17393 );
17394 multibuffer.push_excerpts(
17395 buffer_3.clone(),
17396 [
17397 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17398 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17399 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17400 ],
17401 cx,
17402 );
17403 multibuffer
17404 });
17405
17406 let editor =
17407 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17408 editor
17409 .update(cx, |editor, _window, cx| {
17410 for (buffer, diff_base) in [
17411 (buffer_1.clone(), file_1_old),
17412 (buffer_2.clone(), file_2_old),
17413 (buffer_3.clone(), file_3_old),
17414 ] {
17415 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
17416 editor
17417 .buffer
17418 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
17419 }
17420 })
17421 .unwrap();
17422
17423 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17424 cx.run_until_parked();
17425
17426 cx.assert_editor_state(
17427 &"
17428 ˇaaa
17429 ccc
17430 ddd
17431
17432 ggg
17433 hhh
17434
17435
17436 lll
17437 mmm
17438 NNN
17439
17440 qqq
17441 rrr
17442
17443 uuu
17444 111
17445 222
17446 333
17447
17448 666
17449 777
17450
17451 000
17452 !!!"
17453 .unindent(),
17454 );
17455
17456 cx.update_editor(|editor, window, cx| {
17457 editor.select_all(&SelectAll, window, cx);
17458 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17459 });
17460 cx.executor().run_until_parked();
17461
17462 cx.assert_state_with_diff(
17463 "
17464 «aaa
17465 - bbb
17466 ccc
17467 ddd
17468
17469 ggg
17470 hhh
17471
17472
17473 lll
17474 mmm
17475 - nnn
17476 + NNN
17477
17478 qqq
17479 rrr
17480
17481 uuu
17482 111
17483 222
17484 333
17485
17486 + 666
17487 777
17488
17489 000
17490 !!!ˇ»"
17491 .unindent(),
17492 );
17493}
17494
17495#[gpui::test]
17496async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
17497 init_test(cx, |_| {});
17498
17499 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
17500 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
17501
17502 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
17503 let multi_buffer = cx.new(|cx| {
17504 let mut multibuffer = MultiBuffer::new(ReadWrite);
17505 multibuffer.push_excerpts(
17506 buffer.clone(),
17507 [
17508 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
17509 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
17510 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
17511 ],
17512 cx,
17513 );
17514 multibuffer
17515 });
17516
17517 let editor =
17518 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17519 editor
17520 .update(cx, |editor, _window, cx| {
17521 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
17522 editor
17523 .buffer
17524 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
17525 })
17526 .unwrap();
17527
17528 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17529 cx.run_until_parked();
17530
17531 cx.update_editor(|editor, window, cx| {
17532 editor.expand_all_diff_hunks(&Default::default(), window, cx)
17533 });
17534 cx.executor().run_until_parked();
17535
17536 // When the start of a hunk coincides with the start of its excerpt,
17537 // the hunk is expanded. When the start of a a hunk is earlier than
17538 // the start of its excerpt, the hunk is not expanded.
17539 cx.assert_state_with_diff(
17540 "
17541 ˇaaa
17542 - bbb
17543 + BBB
17544
17545 - ddd
17546 - eee
17547 + DDD
17548 + EEE
17549 fff
17550
17551 iii
17552 "
17553 .unindent(),
17554 );
17555}
17556
17557#[gpui::test]
17558async fn test_edits_around_expanded_insertion_hunks(
17559 executor: BackgroundExecutor,
17560 cx: &mut TestAppContext,
17561) {
17562 init_test(cx, |_| {});
17563
17564 let mut cx = EditorTestContext::new(cx).await;
17565
17566 let diff_base = r#"
17567 use some::mod1;
17568 use some::mod2;
17569
17570 const A: u32 = 42;
17571
17572 fn main() {
17573 println!("hello");
17574
17575 println!("world");
17576 }
17577 "#
17578 .unindent();
17579 executor.run_until_parked();
17580 cx.set_state(
17581 &r#"
17582 use some::mod1;
17583 use some::mod2;
17584
17585 const A: u32 = 42;
17586 const B: u32 = 42;
17587 const C: u32 = 42;
17588 ˇ
17589
17590 fn main() {
17591 println!("hello");
17592
17593 println!("world");
17594 }
17595 "#
17596 .unindent(),
17597 );
17598
17599 cx.set_head_text(&diff_base);
17600 executor.run_until_parked();
17601
17602 cx.update_editor(|editor, window, cx| {
17603 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17604 });
17605 executor.run_until_parked();
17606
17607 cx.assert_state_with_diff(
17608 r#"
17609 use some::mod1;
17610 use some::mod2;
17611
17612 const A: u32 = 42;
17613 + const B: u32 = 42;
17614 + const C: u32 = 42;
17615 + ˇ
17616
17617 fn main() {
17618 println!("hello");
17619
17620 println!("world");
17621 }
17622 "#
17623 .unindent(),
17624 );
17625
17626 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
17627 executor.run_until_parked();
17628
17629 cx.assert_state_with_diff(
17630 r#"
17631 use some::mod1;
17632 use some::mod2;
17633
17634 const A: u32 = 42;
17635 + const B: u32 = 42;
17636 + const C: u32 = 42;
17637 + const D: u32 = 42;
17638 + ˇ
17639
17640 fn main() {
17641 println!("hello");
17642
17643 println!("world");
17644 }
17645 "#
17646 .unindent(),
17647 );
17648
17649 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
17650 executor.run_until_parked();
17651
17652 cx.assert_state_with_diff(
17653 r#"
17654 use some::mod1;
17655 use some::mod2;
17656
17657 const A: u32 = 42;
17658 + const B: u32 = 42;
17659 + const C: u32 = 42;
17660 + const D: u32 = 42;
17661 + const E: u32 = 42;
17662 + ˇ
17663
17664 fn main() {
17665 println!("hello");
17666
17667 println!("world");
17668 }
17669 "#
17670 .unindent(),
17671 );
17672
17673 cx.update_editor(|editor, window, cx| {
17674 editor.delete_line(&DeleteLine, window, cx);
17675 });
17676 executor.run_until_parked();
17677
17678 cx.assert_state_with_diff(
17679 r#"
17680 use some::mod1;
17681 use some::mod2;
17682
17683 const A: u32 = 42;
17684 + const B: u32 = 42;
17685 + const C: u32 = 42;
17686 + const D: u32 = 42;
17687 + const E: u32 = 42;
17688 ˇ
17689 fn main() {
17690 println!("hello");
17691
17692 println!("world");
17693 }
17694 "#
17695 .unindent(),
17696 );
17697
17698 cx.update_editor(|editor, window, cx| {
17699 editor.move_up(&MoveUp, window, cx);
17700 editor.delete_line(&DeleteLine, window, cx);
17701 editor.move_up(&MoveUp, window, cx);
17702 editor.delete_line(&DeleteLine, window, cx);
17703 editor.move_up(&MoveUp, window, cx);
17704 editor.delete_line(&DeleteLine, window, cx);
17705 });
17706 executor.run_until_parked();
17707 cx.assert_state_with_diff(
17708 r#"
17709 use some::mod1;
17710 use some::mod2;
17711
17712 const A: u32 = 42;
17713 + const B: u32 = 42;
17714 ˇ
17715 fn main() {
17716 println!("hello");
17717
17718 println!("world");
17719 }
17720 "#
17721 .unindent(),
17722 );
17723
17724 cx.update_editor(|editor, window, cx| {
17725 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
17726 editor.delete_line(&DeleteLine, window, cx);
17727 });
17728 executor.run_until_parked();
17729 cx.assert_state_with_diff(
17730 r#"
17731 ˇ
17732 fn main() {
17733 println!("hello");
17734
17735 println!("world");
17736 }
17737 "#
17738 .unindent(),
17739 );
17740}
17741
17742#[gpui::test]
17743async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
17744 init_test(cx, |_| {});
17745
17746 let mut cx = EditorTestContext::new(cx).await;
17747 cx.set_head_text(indoc! { "
17748 one
17749 two
17750 three
17751 four
17752 five
17753 "
17754 });
17755 cx.set_state(indoc! { "
17756 one
17757 ˇthree
17758 five
17759 "});
17760 cx.run_until_parked();
17761 cx.update_editor(|editor, window, cx| {
17762 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17763 });
17764 cx.assert_state_with_diff(
17765 indoc! { "
17766 one
17767 - two
17768 ˇthree
17769 - four
17770 five
17771 "}
17772 .to_string(),
17773 );
17774 cx.update_editor(|editor, window, cx| {
17775 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17776 });
17777
17778 cx.assert_state_with_diff(
17779 indoc! { "
17780 one
17781 ˇthree
17782 five
17783 "}
17784 .to_string(),
17785 );
17786
17787 cx.set_state(indoc! { "
17788 one
17789 ˇTWO
17790 three
17791 four
17792 five
17793 "});
17794 cx.run_until_parked();
17795 cx.update_editor(|editor, window, cx| {
17796 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17797 });
17798
17799 cx.assert_state_with_diff(
17800 indoc! { "
17801 one
17802 - two
17803 + ˇTWO
17804 three
17805 four
17806 five
17807 "}
17808 .to_string(),
17809 );
17810 cx.update_editor(|editor, window, cx| {
17811 editor.move_up(&Default::default(), window, cx);
17812 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17813 });
17814 cx.assert_state_with_diff(
17815 indoc! { "
17816 one
17817 ˇTWO
17818 three
17819 four
17820 five
17821 "}
17822 .to_string(),
17823 );
17824}
17825
17826#[gpui::test]
17827async fn test_edits_around_expanded_deletion_hunks(
17828 executor: BackgroundExecutor,
17829 cx: &mut TestAppContext,
17830) {
17831 init_test(cx, |_| {});
17832
17833 let mut cx = EditorTestContext::new(cx).await;
17834
17835 let diff_base = r#"
17836 use some::mod1;
17837 use some::mod2;
17838
17839 const A: u32 = 42;
17840 const B: u32 = 42;
17841 const C: u32 = 42;
17842
17843
17844 fn main() {
17845 println!("hello");
17846
17847 println!("world");
17848 }
17849 "#
17850 .unindent();
17851 executor.run_until_parked();
17852 cx.set_state(
17853 &r#"
17854 use some::mod1;
17855 use some::mod2;
17856
17857 ˇconst B: u32 = 42;
17858 const C: u32 = 42;
17859
17860
17861 fn main() {
17862 println!("hello");
17863
17864 println!("world");
17865 }
17866 "#
17867 .unindent(),
17868 );
17869
17870 cx.set_head_text(&diff_base);
17871 executor.run_until_parked();
17872
17873 cx.update_editor(|editor, window, cx| {
17874 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, 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
17887
17888 fn main() {
17889 println!("hello");
17890
17891 println!("world");
17892 }
17893 "#
17894 .unindent(),
17895 );
17896
17897 cx.update_editor(|editor, window, cx| {
17898 editor.delete_line(&DeleteLine, window, cx);
17899 });
17900 executor.run_until_parked();
17901 cx.assert_state_with_diff(
17902 r#"
17903 use some::mod1;
17904 use some::mod2;
17905
17906 - const A: u32 = 42;
17907 - const B: u32 = 42;
17908 ˇconst C: u32 = 42;
17909
17910
17911 fn main() {
17912 println!("hello");
17913
17914 println!("world");
17915 }
17916 "#
17917 .unindent(),
17918 );
17919
17920 cx.update_editor(|editor, window, cx| {
17921 editor.delete_line(&DeleteLine, window, cx);
17922 });
17923 executor.run_until_parked();
17924 cx.assert_state_with_diff(
17925 r#"
17926 use some::mod1;
17927 use some::mod2;
17928
17929 - const A: u32 = 42;
17930 - const B: u32 = 42;
17931 - const C: u32 = 42;
17932 ˇ
17933
17934 fn main() {
17935 println!("hello");
17936
17937 println!("world");
17938 }
17939 "#
17940 .unindent(),
17941 );
17942
17943 cx.update_editor(|editor, window, cx| {
17944 editor.handle_input("replacement", window, cx);
17945 });
17946 executor.run_until_parked();
17947 cx.assert_state_with_diff(
17948 r#"
17949 use some::mod1;
17950 use some::mod2;
17951
17952 - const A: u32 = 42;
17953 - const B: u32 = 42;
17954 - const C: u32 = 42;
17955 -
17956 + replacementˇ
17957
17958 fn main() {
17959 println!("hello");
17960
17961 println!("world");
17962 }
17963 "#
17964 .unindent(),
17965 );
17966}
17967
17968#[gpui::test]
17969async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17970 init_test(cx, |_| {});
17971
17972 let mut cx = EditorTestContext::new(cx).await;
17973
17974 let base_text = r#"
17975 one
17976 two
17977 three
17978 four
17979 five
17980 "#
17981 .unindent();
17982 executor.run_until_parked();
17983 cx.set_state(
17984 &r#"
17985 one
17986 two
17987 fˇour
17988 five
17989 "#
17990 .unindent(),
17991 );
17992
17993 cx.set_head_text(&base_text);
17994 executor.run_until_parked();
17995
17996 cx.update_editor(|editor, window, cx| {
17997 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17998 });
17999 executor.run_until_parked();
18000
18001 cx.assert_state_with_diff(
18002 r#"
18003 one
18004 two
18005 - three
18006 fˇour
18007 five
18008 "#
18009 .unindent(),
18010 );
18011
18012 cx.update_editor(|editor, window, cx| {
18013 editor.backspace(&Backspace, window, cx);
18014 editor.backspace(&Backspace, window, cx);
18015 });
18016 executor.run_until_parked();
18017 cx.assert_state_with_diff(
18018 r#"
18019 one
18020 two
18021 - threeˇ
18022 - four
18023 + our
18024 five
18025 "#
18026 .unindent(),
18027 );
18028}
18029
18030#[gpui::test]
18031async fn test_edit_after_expanded_modification_hunk(
18032 executor: BackgroundExecutor,
18033 cx: &mut TestAppContext,
18034) {
18035 init_test(cx, |_| {});
18036
18037 let mut cx = EditorTestContext::new(cx).await;
18038
18039 let diff_base = r#"
18040 use some::mod1;
18041 use some::mod2;
18042
18043 const A: u32 = 42;
18044 const B: u32 = 42;
18045 const C: u32 = 42;
18046 const D: u32 = 42;
18047
18048
18049 fn main() {
18050 println!("hello");
18051
18052 println!("world");
18053 }"#
18054 .unindent();
18055
18056 cx.set_state(
18057 &r#"
18058 use some::mod1;
18059 use some::mod2;
18060
18061 const A: u32 = 42;
18062 const B: u32 = 42;
18063 const C: u32 = 43ˇ
18064 const D: u32 = 42;
18065
18066
18067 fn main() {
18068 println!("hello");
18069
18070 println!("world");
18071 }"#
18072 .unindent(),
18073 );
18074
18075 cx.set_head_text(&diff_base);
18076 executor.run_until_parked();
18077 cx.update_editor(|editor, window, cx| {
18078 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18079 });
18080 executor.run_until_parked();
18081
18082 cx.assert_state_with_diff(
18083 r#"
18084 use some::mod1;
18085 use some::mod2;
18086
18087 const A: u32 = 42;
18088 const B: u32 = 42;
18089 - const C: u32 = 42;
18090 + const C: u32 = 43ˇ
18091 const D: u32 = 42;
18092
18093
18094 fn main() {
18095 println!("hello");
18096
18097 println!("world");
18098 }"#
18099 .unindent(),
18100 );
18101
18102 cx.update_editor(|editor, window, cx| {
18103 editor.handle_input("\nnew_line\n", window, cx);
18104 });
18105 executor.run_until_parked();
18106
18107 cx.assert_state_with_diff(
18108 r#"
18109 use some::mod1;
18110 use some::mod2;
18111
18112 const A: u32 = 42;
18113 const B: u32 = 42;
18114 - const C: u32 = 42;
18115 + const C: u32 = 43
18116 + new_line
18117 + ˇ
18118 const D: u32 = 42;
18119
18120
18121 fn main() {
18122 println!("hello");
18123
18124 println!("world");
18125 }"#
18126 .unindent(),
18127 );
18128}
18129
18130#[gpui::test]
18131async fn test_stage_and_unstage_added_file_hunk(
18132 executor: BackgroundExecutor,
18133 cx: &mut TestAppContext,
18134) {
18135 init_test(cx, |_| {});
18136
18137 let mut cx = EditorTestContext::new(cx).await;
18138 cx.update_editor(|editor, _, cx| {
18139 editor.set_expand_all_diff_hunks(cx);
18140 });
18141
18142 let working_copy = r#"
18143 ˇfn main() {
18144 println!("hello, world!");
18145 }
18146 "#
18147 .unindent();
18148
18149 cx.set_state(&working_copy);
18150 executor.run_until_parked();
18151
18152 cx.assert_state_with_diff(
18153 r#"
18154 + ˇfn main() {
18155 + println!("hello, world!");
18156 + }
18157 "#
18158 .unindent(),
18159 );
18160 cx.assert_index_text(None);
18161
18162 cx.update_editor(|editor, window, cx| {
18163 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18164 });
18165 executor.run_until_parked();
18166 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
18167 cx.assert_state_with_diff(
18168 r#"
18169 + ˇfn main() {
18170 + println!("hello, world!");
18171 + }
18172 "#
18173 .unindent(),
18174 );
18175
18176 cx.update_editor(|editor, window, cx| {
18177 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18178 });
18179 executor.run_until_parked();
18180 cx.assert_index_text(None);
18181}
18182
18183async fn setup_indent_guides_editor(
18184 text: &str,
18185 cx: &mut TestAppContext,
18186) -> (BufferId, EditorTestContext) {
18187 init_test(cx, |_| {});
18188
18189 let mut cx = EditorTestContext::new(cx).await;
18190
18191 let buffer_id = cx.update_editor(|editor, window, cx| {
18192 editor.set_text(text, window, cx);
18193 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
18194
18195 buffer_ids[0]
18196 });
18197
18198 (buffer_id, cx)
18199}
18200
18201fn assert_indent_guides(
18202 range: Range<u32>,
18203 expected: Vec<IndentGuide>,
18204 active_indices: Option<Vec<usize>>,
18205 cx: &mut EditorTestContext,
18206) {
18207 let indent_guides = cx.update_editor(|editor, window, cx| {
18208 let snapshot = editor.snapshot(window, cx).display_snapshot;
18209 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
18210 editor,
18211 MultiBufferRow(range.start)..MultiBufferRow(range.end),
18212 true,
18213 &snapshot,
18214 cx,
18215 );
18216
18217 indent_guides.sort_by(|a, b| {
18218 a.depth.cmp(&b.depth).then(
18219 a.start_row
18220 .cmp(&b.start_row)
18221 .then(a.end_row.cmp(&b.end_row)),
18222 )
18223 });
18224 indent_guides
18225 });
18226
18227 if let Some(expected) = active_indices {
18228 let active_indices = cx.update_editor(|editor, window, cx| {
18229 let snapshot = editor.snapshot(window, cx).display_snapshot;
18230 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
18231 });
18232
18233 assert_eq!(
18234 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
18235 expected,
18236 "Active indent guide indices do not match"
18237 );
18238 }
18239
18240 assert_eq!(indent_guides, expected, "Indent guides do not match");
18241}
18242
18243fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
18244 IndentGuide {
18245 buffer_id,
18246 start_row: MultiBufferRow(start_row),
18247 end_row: MultiBufferRow(end_row),
18248 depth,
18249 tab_size: 4,
18250 settings: IndentGuideSettings {
18251 enabled: true,
18252 line_width: 1,
18253 active_line_width: 1,
18254 ..Default::default()
18255 },
18256 }
18257}
18258
18259#[gpui::test]
18260async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
18261 let (buffer_id, mut cx) = setup_indent_guides_editor(
18262 &"
18263 fn main() {
18264 let a = 1;
18265 }"
18266 .unindent(),
18267 cx,
18268 )
18269 .await;
18270
18271 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18272}
18273
18274#[gpui::test]
18275async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
18276 let (buffer_id, mut cx) = setup_indent_guides_editor(
18277 &"
18278 fn main() {
18279 let a = 1;
18280 let b = 2;
18281 }"
18282 .unindent(),
18283 cx,
18284 )
18285 .await;
18286
18287 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
18288}
18289
18290#[gpui::test]
18291async fn test_indent_guide_nested(cx: &mut TestAppContext) {
18292 let (buffer_id, mut cx) = setup_indent_guides_editor(
18293 &"
18294 fn main() {
18295 let a = 1;
18296 if a == 3 {
18297 let b = 2;
18298 } else {
18299 let c = 3;
18300 }
18301 }"
18302 .unindent(),
18303 cx,
18304 )
18305 .await;
18306
18307 assert_indent_guides(
18308 0..8,
18309 vec![
18310 indent_guide(buffer_id, 1, 6, 0),
18311 indent_guide(buffer_id, 3, 3, 1),
18312 indent_guide(buffer_id, 5, 5, 1),
18313 ],
18314 None,
18315 &mut cx,
18316 );
18317}
18318
18319#[gpui::test]
18320async fn test_indent_guide_tab(cx: &mut TestAppContext) {
18321 let (buffer_id, mut cx) = setup_indent_guides_editor(
18322 &"
18323 fn main() {
18324 let a = 1;
18325 let b = 2;
18326 let c = 3;
18327 }"
18328 .unindent(),
18329 cx,
18330 )
18331 .await;
18332
18333 assert_indent_guides(
18334 0..5,
18335 vec![
18336 indent_guide(buffer_id, 1, 3, 0),
18337 indent_guide(buffer_id, 2, 2, 1),
18338 ],
18339 None,
18340 &mut cx,
18341 );
18342}
18343
18344#[gpui::test]
18345async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
18346 let (buffer_id, mut cx) = setup_indent_guides_editor(
18347 &"
18348 fn main() {
18349 let a = 1;
18350
18351 let c = 3;
18352 }"
18353 .unindent(),
18354 cx,
18355 )
18356 .await;
18357
18358 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
18359}
18360
18361#[gpui::test]
18362async fn test_indent_guide_complex(cx: &mut TestAppContext) {
18363 let (buffer_id, mut cx) = setup_indent_guides_editor(
18364 &"
18365 fn main() {
18366 let a = 1;
18367
18368 let c = 3;
18369
18370 if a == 3 {
18371 let b = 2;
18372 } else {
18373 let c = 3;
18374 }
18375 }"
18376 .unindent(),
18377 cx,
18378 )
18379 .await;
18380
18381 assert_indent_guides(
18382 0..11,
18383 vec![
18384 indent_guide(buffer_id, 1, 9, 0),
18385 indent_guide(buffer_id, 6, 6, 1),
18386 indent_guide(buffer_id, 8, 8, 1),
18387 ],
18388 None,
18389 &mut cx,
18390 );
18391}
18392
18393#[gpui::test]
18394async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
18395 let (buffer_id, mut cx) = setup_indent_guides_editor(
18396 &"
18397 fn main() {
18398 let a = 1;
18399
18400 let c = 3;
18401
18402 if a == 3 {
18403 let b = 2;
18404 } else {
18405 let c = 3;
18406 }
18407 }"
18408 .unindent(),
18409 cx,
18410 )
18411 .await;
18412
18413 assert_indent_guides(
18414 1..11,
18415 vec![
18416 indent_guide(buffer_id, 1, 9, 0),
18417 indent_guide(buffer_id, 6, 6, 1),
18418 indent_guide(buffer_id, 8, 8, 1),
18419 ],
18420 None,
18421 &mut cx,
18422 );
18423}
18424
18425#[gpui::test]
18426async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
18427 let (buffer_id, mut cx) = setup_indent_guides_editor(
18428 &"
18429 fn main() {
18430 let a = 1;
18431
18432 let c = 3;
18433
18434 if a == 3 {
18435 let b = 2;
18436 } else {
18437 let c = 3;
18438 }
18439 }"
18440 .unindent(),
18441 cx,
18442 )
18443 .await;
18444
18445 assert_indent_guides(
18446 1..10,
18447 vec![
18448 indent_guide(buffer_id, 1, 9, 0),
18449 indent_guide(buffer_id, 6, 6, 1),
18450 indent_guide(buffer_id, 8, 8, 1),
18451 ],
18452 None,
18453 &mut cx,
18454 );
18455}
18456
18457#[gpui::test]
18458async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
18459 let (buffer_id, mut cx) = setup_indent_guides_editor(
18460 &"
18461 fn main() {
18462 if a {
18463 b(
18464 c,
18465 d,
18466 )
18467 } else {
18468 e(
18469 f
18470 )
18471 }
18472 }"
18473 .unindent(),
18474 cx,
18475 )
18476 .await;
18477
18478 assert_indent_guides(
18479 0..11,
18480 vec![
18481 indent_guide(buffer_id, 1, 10, 0),
18482 indent_guide(buffer_id, 2, 5, 1),
18483 indent_guide(buffer_id, 7, 9, 1),
18484 indent_guide(buffer_id, 3, 4, 2),
18485 indent_guide(buffer_id, 8, 8, 2),
18486 ],
18487 None,
18488 &mut cx,
18489 );
18490
18491 cx.update_editor(|editor, window, cx| {
18492 editor.fold_at(MultiBufferRow(2), window, cx);
18493 assert_eq!(
18494 editor.display_text(cx),
18495 "
18496 fn main() {
18497 if a {
18498 b(⋯
18499 )
18500 } else {
18501 e(
18502 f
18503 )
18504 }
18505 }"
18506 .unindent()
18507 );
18508 });
18509
18510 assert_indent_guides(
18511 0..11,
18512 vec![
18513 indent_guide(buffer_id, 1, 10, 0),
18514 indent_guide(buffer_id, 2, 5, 1),
18515 indent_guide(buffer_id, 7, 9, 1),
18516 indent_guide(buffer_id, 8, 8, 2),
18517 ],
18518 None,
18519 &mut cx,
18520 );
18521}
18522
18523#[gpui::test]
18524async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
18525 let (buffer_id, mut cx) = setup_indent_guides_editor(
18526 &"
18527 block1
18528 block2
18529 block3
18530 block4
18531 block2
18532 block1
18533 block1"
18534 .unindent(),
18535 cx,
18536 )
18537 .await;
18538
18539 assert_indent_guides(
18540 1..10,
18541 vec![
18542 indent_guide(buffer_id, 1, 4, 0),
18543 indent_guide(buffer_id, 2, 3, 1),
18544 indent_guide(buffer_id, 3, 3, 2),
18545 ],
18546 None,
18547 &mut cx,
18548 );
18549}
18550
18551#[gpui::test]
18552async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
18553 let (buffer_id, mut cx) = setup_indent_guides_editor(
18554 &"
18555 block1
18556 block2
18557 block3
18558
18559 block1
18560 block1"
18561 .unindent(),
18562 cx,
18563 )
18564 .await;
18565
18566 assert_indent_guides(
18567 0..6,
18568 vec![
18569 indent_guide(buffer_id, 1, 2, 0),
18570 indent_guide(buffer_id, 2, 2, 1),
18571 ],
18572 None,
18573 &mut cx,
18574 );
18575}
18576
18577#[gpui::test]
18578async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
18579 let (buffer_id, mut cx) = setup_indent_guides_editor(
18580 &"
18581 function component() {
18582 \treturn (
18583 \t\t\t
18584 \t\t<div>
18585 \t\t\t<abc></abc>
18586 \t\t</div>
18587 \t)
18588 }"
18589 .unindent(),
18590 cx,
18591 )
18592 .await;
18593
18594 assert_indent_guides(
18595 0..8,
18596 vec![
18597 indent_guide(buffer_id, 1, 6, 0),
18598 indent_guide(buffer_id, 2, 5, 1),
18599 indent_guide(buffer_id, 4, 4, 2),
18600 ],
18601 None,
18602 &mut cx,
18603 );
18604}
18605
18606#[gpui::test]
18607async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
18608 let (buffer_id, mut cx) = setup_indent_guides_editor(
18609 &"
18610 function component() {
18611 \treturn (
18612 \t
18613 \t\t<div>
18614 \t\t\t<abc></abc>
18615 \t\t</div>
18616 \t)
18617 }"
18618 .unindent(),
18619 cx,
18620 )
18621 .await;
18622
18623 assert_indent_guides(
18624 0..8,
18625 vec![
18626 indent_guide(buffer_id, 1, 6, 0),
18627 indent_guide(buffer_id, 2, 5, 1),
18628 indent_guide(buffer_id, 4, 4, 2),
18629 ],
18630 None,
18631 &mut cx,
18632 );
18633}
18634
18635#[gpui::test]
18636async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
18637 let (buffer_id, mut cx) = setup_indent_guides_editor(
18638 &"
18639 block1
18640
18641
18642
18643 block2
18644 "
18645 .unindent(),
18646 cx,
18647 )
18648 .await;
18649
18650 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18651}
18652
18653#[gpui::test]
18654async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
18655 let (buffer_id, mut cx) = setup_indent_guides_editor(
18656 &"
18657 def a:
18658 \tb = 3
18659 \tif True:
18660 \t\tc = 4
18661 \t\td = 5
18662 \tprint(b)
18663 "
18664 .unindent(),
18665 cx,
18666 )
18667 .await;
18668
18669 assert_indent_guides(
18670 0..6,
18671 vec![
18672 indent_guide(buffer_id, 1, 5, 0),
18673 indent_guide(buffer_id, 3, 4, 1),
18674 ],
18675 None,
18676 &mut cx,
18677 );
18678}
18679
18680#[gpui::test]
18681async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
18682 let (buffer_id, mut cx) = setup_indent_guides_editor(
18683 &"
18684 fn main() {
18685 let a = 1;
18686 }"
18687 .unindent(),
18688 cx,
18689 )
18690 .await;
18691
18692 cx.update_editor(|editor, window, cx| {
18693 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18694 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18695 });
18696 });
18697
18698 assert_indent_guides(
18699 0..3,
18700 vec![indent_guide(buffer_id, 1, 1, 0)],
18701 Some(vec![0]),
18702 &mut cx,
18703 );
18704}
18705
18706#[gpui::test]
18707async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
18708 let (buffer_id, mut cx) = setup_indent_guides_editor(
18709 &"
18710 fn main() {
18711 if 1 == 2 {
18712 let a = 1;
18713 }
18714 }"
18715 .unindent(),
18716 cx,
18717 )
18718 .await;
18719
18720 cx.update_editor(|editor, window, cx| {
18721 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18722 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18723 });
18724 });
18725
18726 assert_indent_guides(
18727 0..4,
18728 vec![
18729 indent_guide(buffer_id, 1, 3, 0),
18730 indent_guide(buffer_id, 2, 2, 1),
18731 ],
18732 Some(vec![1]),
18733 &mut cx,
18734 );
18735
18736 cx.update_editor(|editor, window, cx| {
18737 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18738 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18739 });
18740 });
18741
18742 assert_indent_guides(
18743 0..4,
18744 vec![
18745 indent_guide(buffer_id, 1, 3, 0),
18746 indent_guide(buffer_id, 2, 2, 1),
18747 ],
18748 Some(vec![1]),
18749 &mut cx,
18750 );
18751
18752 cx.update_editor(|editor, window, cx| {
18753 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18754 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
18755 });
18756 });
18757
18758 assert_indent_guides(
18759 0..4,
18760 vec![
18761 indent_guide(buffer_id, 1, 3, 0),
18762 indent_guide(buffer_id, 2, 2, 1),
18763 ],
18764 Some(vec![0]),
18765 &mut cx,
18766 );
18767}
18768
18769#[gpui::test]
18770async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
18771 let (buffer_id, mut cx) = setup_indent_guides_editor(
18772 &"
18773 fn main() {
18774 let a = 1;
18775
18776 let b = 2;
18777 }"
18778 .unindent(),
18779 cx,
18780 )
18781 .await;
18782
18783 cx.update_editor(|editor, window, cx| {
18784 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18785 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18786 });
18787 });
18788
18789 assert_indent_guides(
18790 0..5,
18791 vec![indent_guide(buffer_id, 1, 3, 0)],
18792 Some(vec![0]),
18793 &mut cx,
18794 );
18795}
18796
18797#[gpui::test]
18798async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
18799 let (buffer_id, mut cx) = setup_indent_guides_editor(
18800 &"
18801 def m:
18802 a = 1
18803 pass"
18804 .unindent(),
18805 cx,
18806 )
18807 .await;
18808
18809 cx.update_editor(|editor, window, cx| {
18810 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18811 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18812 });
18813 });
18814
18815 assert_indent_guides(
18816 0..3,
18817 vec![indent_guide(buffer_id, 1, 2, 0)],
18818 Some(vec![0]),
18819 &mut cx,
18820 );
18821}
18822
18823#[gpui::test]
18824async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
18825 init_test(cx, |_| {});
18826 let mut cx = EditorTestContext::new(cx).await;
18827 let text = indoc! {
18828 "
18829 impl A {
18830 fn b() {
18831 0;
18832 3;
18833 5;
18834 6;
18835 7;
18836 }
18837 }
18838 "
18839 };
18840 let base_text = indoc! {
18841 "
18842 impl A {
18843 fn b() {
18844 0;
18845 1;
18846 2;
18847 3;
18848 4;
18849 }
18850 fn c() {
18851 5;
18852 6;
18853 7;
18854 }
18855 }
18856 "
18857 };
18858
18859 cx.update_editor(|editor, window, cx| {
18860 editor.set_text(text, window, cx);
18861
18862 editor.buffer().update(cx, |multibuffer, cx| {
18863 let buffer = multibuffer.as_singleton().unwrap();
18864 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
18865
18866 multibuffer.set_all_diff_hunks_expanded(cx);
18867 multibuffer.add_diff(diff, cx);
18868
18869 buffer.read(cx).remote_id()
18870 })
18871 });
18872 cx.run_until_parked();
18873
18874 cx.assert_state_with_diff(
18875 indoc! { "
18876 impl A {
18877 fn b() {
18878 0;
18879 - 1;
18880 - 2;
18881 3;
18882 - 4;
18883 - }
18884 - fn c() {
18885 5;
18886 6;
18887 7;
18888 }
18889 }
18890 ˇ"
18891 }
18892 .to_string(),
18893 );
18894
18895 let mut actual_guides = cx.update_editor(|editor, window, cx| {
18896 editor
18897 .snapshot(window, cx)
18898 .buffer_snapshot
18899 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
18900 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
18901 .collect::<Vec<_>>()
18902 });
18903 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
18904 assert_eq!(
18905 actual_guides,
18906 vec![
18907 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
18908 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
18909 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
18910 ]
18911 );
18912}
18913
18914#[gpui::test]
18915async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18916 init_test(cx, |_| {});
18917 let mut cx = EditorTestContext::new(cx).await;
18918
18919 let diff_base = r#"
18920 a
18921 b
18922 c
18923 "#
18924 .unindent();
18925
18926 cx.set_state(
18927 &r#"
18928 ˇA
18929 b
18930 C
18931 "#
18932 .unindent(),
18933 );
18934 cx.set_head_text(&diff_base);
18935 cx.update_editor(|editor, window, cx| {
18936 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18937 });
18938 executor.run_until_parked();
18939
18940 let both_hunks_expanded = r#"
18941 - a
18942 + ˇA
18943 b
18944 - c
18945 + C
18946 "#
18947 .unindent();
18948
18949 cx.assert_state_with_diff(both_hunks_expanded.clone());
18950
18951 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18952 let snapshot = editor.snapshot(window, cx);
18953 let hunks = editor
18954 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18955 .collect::<Vec<_>>();
18956 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18957 let buffer_id = hunks[0].buffer_id;
18958 hunks
18959 .into_iter()
18960 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18961 .collect::<Vec<_>>()
18962 });
18963 assert_eq!(hunk_ranges.len(), 2);
18964
18965 cx.update_editor(|editor, _, cx| {
18966 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18967 });
18968 executor.run_until_parked();
18969
18970 let second_hunk_expanded = r#"
18971 ˇA
18972 b
18973 - c
18974 + C
18975 "#
18976 .unindent();
18977
18978 cx.assert_state_with_diff(second_hunk_expanded);
18979
18980 cx.update_editor(|editor, _, cx| {
18981 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18982 });
18983 executor.run_until_parked();
18984
18985 cx.assert_state_with_diff(both_hunks_expanded.clone());
18986
18987 cx.update_editor(|editor, _, cx| {
18988 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18989 });
18990 executor.run_until_parked();
18991
18992 let first_hunk_expanded = r#"
18993 - a
18994 + ˇA
18995 b
18996 C
18997 "#
18998 .unindent();
18999
19000 cx.assert_state_with_diff(first_hunk_expanded);
19001
19002 cx.update_editor(|editor, _, cx| {
19003 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19004 });
19005 executor.run_until_parked();
19006
19007 cx.assert_state_with_diff(both_hunks_expanded);
19008
19009 cx.set_state(
19010 &r#"
19011 ˇA
19012 b
19013 "#
19014 .unindent(),
19015 );
19016 cx.run_until_parked();
19017
19018 // TODO this cursor position seems bad
19019 cx.assert_state_with_diff(
19020 r#"
19021 - ˇa
19022 + A
19023 b
19024 "#
19025 .unindent(),
19026 );
19027
19028 cx.update_editor(|editor, window, cx| {
19029 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19030 });
19031
19032 cx.assert_state_with_diff(
19033 r#"
19034 - ˇa
19035 + A
19036 b
19037 - c
19038 "#
19039 .unindent(),
19040 );
19041
19042 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19043 let snapshot = editor.snapshot(window, cx);
19044 let hunks = editor
19045 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19046 .collect::<Vec<_>>();
19047 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19048 let buffer_id = hunks[0].buffer_id;
19049 hunks
19050 .into_iter()
19051 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19052 .collect::<Vec<_>>()
19053 });
19054 assert_eq!(hunk_ranges.len(), 2);
19055
19056 cx.update_editor(|editor, _, cx| {
19057 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19058 });
19059 executor.run_until_parked();
19060
19061 cx.assert_state_with_diff(
19062 r#"
19063 - ˇa
19064 + A
19065 b
19066 "#
19067 .unindent(),
19068 );
19069}
19070
19071#[gpui::test]
19072async fn test_toggle_deletion_hunk_at_start_of_file(
19073 executor: BackgroundExecutor,
19074 cx: &mut TestAppContext,
19075) {
19076 init_test(cx, |_| {});
19077 let mut cx = EditorTestContext::new(cx).await;
19078
19079 let diff_base = r#"
19080 a
19081 b
19082 c
19083 "#
19084 .unindent();
19085
19086 cx.set_state(
19087 &r#"
19088 ˇb
19089 c
19090 "#
19091 .unindent(),
19092 );
19093 cx.set_head_text(&diff_base);
19094 cx.update_editor(|editor, window, cx| {
19095 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19096 });
19097 executor.run_until_parked();
19098
19099 let hunk_expanded = r#"
19100 - a
19101 ˇb
19102 c
19103 "#
19104 .unindent();
19105
19106 cx.assert_state_with_diff(hunk_expanded.clone());
19107
19108 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19109 let snapshot = editor.snapshot(window, cx);
19110 let hunks = editor
19111 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19112 .collect::<Vec<_>>();
19113 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19114 let buffer_id = hunks[0].buffer_id;
19115 hunks
19116 .into_iter()
19117 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19118 .collect::<Vec<_>>()
19119 });
19120 assert_eq!(hunk_ranges.len(), 1);
19121
19122 cx.update_editor(|editor, _, cx| {
19123 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19124 });
19125 executor.run_until_parked();
19126
19127 let hunk_collapsed = r#"
19128 ˇb
19129 c
19130 "#
19131 .unindent();
19132
19133 cx.assert_state_with_diff(hunk_collapsed);
19134
19135 cx.update_editor(|editor, _, cx| {
19136 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19137 });
19138 executor.run_until_parked();
19139
19140 cx.assert_state_with_diff(hunk_expanded.clone());
19141}
19142
19143#[gpui::test]
19144async fn test_display_diff_hunks(cx: &mut TestAppContext) {
19145 init_test(cx, |_| {});
19146
19147 let fs = FakeFs::new(cx.executor());
19148 fs.insert_tree(
19149 path!("/test"),
19150 json!({
19151 ".git": {},
19152 "file-1": "ONE\n",
19153 "file-2": "TWO\n",
19154 "file-3": "THREE\n",
19155 }),
19156 )
19157 .await;
19158
19159 fs.set_head_for_repo(
19160 path!("/test/.git").as_ref(),
19161 &[
19162 ("file-1".into(), "one\n".into()),
19163 ("file-2".into(), "two\n".into()),
19164 ("file-3".into(), "three\n".into()),
19165 ],
19166 "deadbeef",
19167 );
19168
19169 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
19170 let mut buffers = vec![];
19171 for i in 1..=3 {
19172 let buffer = project
19173 .update(cx, |project, cx| {
19174 let path = format!(path!("/test/file-{}"), i);
19175 project.open_local_buffer(path, cx)
19176 })
19177 .await
19178 .unwrap();
19179 buffers.push(buffer);
19180 }
19181
19182 let multibuffer = cx.new(|cx| {
19183 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
19184 multibuffer.set_all_diff_hunks_expanded(cx);
19185 for buffer in &buffers {
19186 let snapshot = buffer.read(cx).snapshot();
19187 multibuffer.set_excerpts_for_path(
19188 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
19189 buffer.clone(),
19190 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
19191 DEFAULT_MULTIBUFFER_CONTEXT,
19192 cx,
19193 );
19194 }
19195 multibuffer
19196 });
19197
19198 let editor = cx.add_window(|window, cx| {
19199 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
19200 });
19201 cx.run_until_parked();
19202
19203 let snapshot = editor
19204 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19205 .unwrap();
19206 let hunks = snapshot
19207 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
19208 .map(|hunk| match hunk {
19209 DisplayDiffHunk::Unfolded {
19210 display_row_range, ..
19211 } => display_row_range,
19212 DisplayDiffHunk::Folded { .. } => unreachable!(),
19213 })
19214 .collect::<Vec<_>>();
19215 assert_eq!(
19216 hunks,
19217 [
19218 DisplayRow(2)..DisplayRow(4),
19219 DisplayRow(7)..DisplayRow(9),
19220 DisplayRow(12)..DisplayRow(14),
19221 ]
19222 );
19223}
19224
19225#[gpui::test]
19226async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
19227 init_test(cx, |_| {});
19228
19229 let mut cx = EditorTestContext::new(cx).await;
19230 cx.set_head_text(indoc! { "
19231 one
19232 two
19233 three
19234 four
19235 five
19236 "
19237 });
19238 cx.set_index_text(indoc! { "
19239 one
19240 two
19241 three
19242 four
19243 five
19244 "
19245 });
19246 cx.set_state(indoc! {"
19247 one
19248 TWO
19249 ˇTHREE
19250 FOUR
19251 five
19252 "});
19253 cx.run_until_parked();
19254 cx.update_editor(|editor, window, cx| {
19255 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19256 });
19257 cx.run_until_parked();
19258 cx.assert_index_text(Some(indoc! {"
19259 one
19260 TWO
19261 THREE
19262 FOUR
19263 five
19264 "}));
19265 cx.set_state(indoc! { "
19266 one
19267 TWO
19268 ˇTHREE-HUNDRED
19269 FOUR
19270 five
19271 "});
19272 cx.run_until_parked();
19273 cx.update_editor(|editor, window, cx| {
19274 let snapshot = editor.snapshot(window, cx);
19275 let hunks = editor
19276 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19277 .collect::<Vec<_>>();
19278 assert_eq!(hunks.len(), 1);
19279 assert_eq!(
19280 hunks[0].status(),
19281 DiffHunkStatus {
19282 kind: DiffHunkStatusKind::Modified,
19283 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
19284 }
19285 );
19286
19287 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19288 });
19289 cx.run_until_parked();
19290 cx.assert_index_text(Some(indoc! {"
19291 one
19292 TWO
19293 THREE-HUNDRED
19294 FOUR
19295 five
19296 "}));
19297}
19298
19299#[gpui::test]
19300fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
19301 init_test(cx, |_| {});
19302
19303 let editor = cx.add_window(|window, cx| {
19304 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
19305 build_editor(buffer, window, cx)
19306 });
19307
19308 let render_args = Arc::new(Mutex::new(None));
19309 let snapshot = editor
19310 .update(cx, |editor, window, cx| {
19311 let snapshot = editor.buffer().read(cx).snapshot(cx);
19312 let range =
19313 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
19314
19315 struct RenderArgs {
19316 row: MultiBufferRow,
19317 folded: bool,
19318 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
19319 }
19320
19321 let crease = Crease::inline(
19322 range,
19323 FoldPlaceholder::test(),
19324 {
19325 let toggle_callback = render_args.clone();
19326 move |row, folded, callback, _window, _cx| {
19327 *toggle_callback.lock() = Some(RenderArgs {
19328 row,
19329 folded,
19330 callback,
19331 });
19332 div()
19333 }
19334 },
19335 |_row, _folded, _window, _cx| div(),
19336 );
19337
19338 editor.insert_creases(Some(crease), cx);
19339 let snapshot = editor.snapshot(window, cx);
19340 let _div = snapshot.render_crease_toggle(
19341 MultiBufferRow(1),
19342 false,
19343 cx.entity().clone(),
19344 window,
19345 cx,
19346 );
19347 snapshot
19348 })
19349 .unwrap();
19350
19351 let render_args = render_args.lock().take().unwrap();
19352 assert_eq!(render_args.row, MultiBufferRow(1));
19353 assert!(!render_args.folded);
19354 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19355
19356 cx.update_window(*editor, |_, window, cx| {
19357 (render_args.callback)(true, window, cx)
19358 })
19359 .unwrap();
19360 let snapshot = editor
19361 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19362 .unwrap();
19363 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
19364
19365 cx.update_window(*editor, |_, window, cx| {
19366 (render_args.callback)(false, window, cx)
19367 })
19368 .unwrap();
19369 let snapshot = editor
19370 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19371 .unwrap();
19372 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19373}
19374
19375#[gpui::test]
19376async fn test_input_text(cx: &mut TestAppContext) {
19377 init_test(cx, |_| {});
19378 let mut cx = EditorTestContext::new(cx).await;
19379
19380 cx.set_state(
19381 &r#"ˇone
19382 two
19383
19384 three
19385 fourˇ
19386 five
19387
19388 siˇx"#
19389 .unindent(),
19390 );
19391
19392 cx.dispatch_action(HandleInput(String::new()));
19393 cx.assert_editor_state(
19394 &r#"ˇone
19395 two
19396
19397 three
19398 fourˇ
19399 five
19400
19401 siˇx"#
19402 .unindent(),
19403 );
19404
19405 cx.dispatch_action(HandleInput("AAAA".to_string()));
19406 cx.assert_editor_state(
19407 &r#"AAAAˇone
19408 two
19409
19410 three
19411 fourAAAAˇ
19412 five
19413
19414 siAAAAˇx"#
19415 .unindent(),
19416 );
19417}
19418
19419#[gpui::test]
19420async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
19421 init_test(cx, |_| {});
19422
19423 let mut cx = EditorTestContext::new(cx).await;
19424 cx.set_state(
19425 r#"let foo = 1;
19426let foo = 2;
19427let foo = 3;
19428let fooˇ = 4;
19429let foo = 5;
19430let foo = 6;
19431let foo = 7;
19432let foo = 8;
19433let foo = 9;
19434let foo = 10;
19435let foo = 11;
19436let foo = 12;
19437let foo = 13;
19438let foo = 14;
19439let foo = 15;"#,
19440 );
19441
19442 cx.update_editor(|e, window, cx| {
19443 assert_eq!(
19444 e.next_scroll_position,
19445 NextScrollCursorCenterTopBottom::Center,
19446 "Default next scroll direction is center",
19447 );
19448
19449 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19450 assert_eq!(
19451 e.next_scroll_position,
19452 NextScrollCursorCenterTopBottom::Top,
19453 "After center, next scroll direction should be top",
19454 );
19455
19456 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19457 assert_eq!(
19458 e.next_scroll_position,
19459 NextScrollCursorCenterTopBottom::Bottom,
19460 "After top, next scroll direction should be bottom",
19461 );
19462
19463 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19464 assert_eq!(
19465 e.next_scroll_position,
19466 NextScrollCursorCenterTopBottom::Center,
19467 "After bottom, scrolling should start over",
19468 );
19469
19470 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19471 assert_eq!(
19472 e.next_scroll_position,
19473 NextScrollCursorCenterTopBottom::Top,
19474 "Scrolling continues if retriggered fast enough"
19475 );
19476 });
19477
19478 cx.executor()
19479 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
19480 cx.executor().run_until_parked();
19481 cx.update_editor(|e, _, _| {
19482 assert_eq!(
19483 e.next_scroll_position,
19484 NextScrollCursorCenterTopBottom::Center,
19485 "If scrolling is not triggered fast enough, it should reset"
19486 );
19487 });
19488}
19489
19490#[gpui::test]
19491async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
19492 init_test(cx, |_| {});
19493 let mut cx = EditorLspTestContext::new_rust(
19494 lsp::ServerCapabilities {
19495 definition_provider: Some(lsp::OneOf::Left(true)),
19496 references_provider: Some(lsp::OneOf::Left(true)),
19497 ..lsp::ServerCapabilities::default()
19498 },
19499 cx,
19500 )
19501 .await;
19502
19503 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
19504 let go_to_definition = cx
19505 .lsp
19506 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19507 move |params, _| async move {
19508 if empty_go_to_definition {
19509 Ok(None)
19510 } else {
19511 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
19512 uri: params.text_document_position_params.text_document.uri,
19513 range: lsp::Range::new(
19514 lsp::Position::new(4, 3),
19515 lsp::Position::new(4, 6),
19516 ),
19517 })))
19518 }
19519 },
19520 );
19521 let references = cx
19522 .lsp
19523 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
19524 Ok(Some(vec![lsp::Location {
19525 uri: params.text_document_position.text_document.uri,
19526 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
19527 }]))
19528 });
19529 (go_to_definition, references)
19530 };
19531
19532 cx.set_state(
19533 &r#"fn one() {
19534 let mut a = ˇtwo();
19535 }
19536
19537 fn two() {}"#
19538 .unindent(),
19539 );
19540 set_up_lsp_handlers(false, &mut cx);
19541 let navigated = cx
19542 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19543 .await
19544 .expect("Failed to navigate to definition");
19545 assert_eq!(
19546 navigated,
19547 Navigated::Yes,
19548 "Should have navigated to definition from the GetDefinition response"
19549 );
19550 cx.assert_editor_state(
19551 &r#"fn one() {
19552 let mut a = two();
19553 }
19554
19555 fn «twoˇ»() {}"#
19556 .unindent(),
19557 );
19558
19559 let editors = cx.update_workspace(|workspace, _, cx| {
19560 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19561 });
19562 cx.update_editor(|_, _, test_editor_cx| {
19563 assert_eq!(
19564 editors.len(),
19565 1,
19566 "Initially, only one, test, editor should be open in the workspace"
19567 );
19568 assert_eq!(
19569 test_editor_cx.entity(),
19570 editors.last().expect("Asserted len is 1").clone()
19571 );
19572 });
19573
19574 set_up_lsp_handlers(true, &mut cx);
19575 let navigated = cx
19576 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19577 .await
19578 .expect("Failed to navigate to lookup references");
19579 assert_eq!(
19580 navigated,
19581 Navigated::Yes,
19582 "Should have navigated to references as a fallback after empty GoToDefinition response"
19583 );
19584 // We should not change the selections in the existing file,
19585 // if opening another milti buffer with the references
19586 cx.assert_editor_state(
19587 &r#"fn one() {
19588 let mut a = two();
19589 }
19590
19591 fn «twoˇ»() {}"#
19592 .unindent(),
19593 );
19594 let editors = cx.update_workspace(|workspace, _, cx| {
19595 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19596 });
19597 cx.update_editor(|_, _, test_editor_cx| {
19598 assert_eq!(
19599 editors.len(),
19600 2,
19601 "After falling back to references search, we open a new editor with the results"
19602 );
19603 let references_fallback_text = editors
19604 .into_iter()
19605 .find(|new_editor| *new_editor != test_editor_cx.entity())
19606 .expect("Should have one non-test editor now")
19607 .read(test_editor_cx)
19608 .text(test_editor_cx);
19609 assert_eq!(
19610 references_fallback_text, "fn one() {\n let mut a = two();\n}",
19611 "Should use the range from the references response and not the GoToDefinition one"
19612 );
19613 });
19614}
19615
19616#[gpui::test]
19617async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
19618 init_test(cx, |_| {});
19619 cx.update(|cx| {
19620 let mut editor_settings = EditorSettings::get_global(cx).clone();
19621 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
19622 EditorSettings::override_global(editor_settings, cx);
19623 });
19624 let mut cx = EditorLspTestContext::new_rust(
19625 lsp::ServerCapabilities {
19626 definition_provider: Some(lsp::OneOf::Left(true)),
19627 references_provider: Some(lsp::OneOf::Left(true)),
19628 ..lsp::ServerCapabilities::default()
19629 },
19630 cx,
19631 )
19632 .await;
19633 let original_state = r#"fn one() {
19634 let mut a = ˇtwo();
19635 }
19636
19637 fn two() {}"#
19638 .unindent();
19639 cx.set_state(&original_state);
19640
19641 let mut go_to_definition = cx
19642 .lsp
19643 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19644 move |_, _| async move { Ok(None) },
19645 );
19646 let _references = cx
19647 .lsp
19648 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
19649 panic!("Should not call for references with no go to definition fallback")
19650 });
19651
19652 let navigated = cx
19653 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19654 .await
19655 .expect("Failed to navigate to lookup references");
19656 go_to_definition
19657 .next()
19658 .await
19659 .expect("Should have called the go_to_definition handler");
19660
19661 assert_eq!(
19662 navigated,
19663 Navigated::No,
19664 "Should have navigated to references as a fallback after empty GoToDefinition response"
19665 );
19666 cx.assert_editor_state(&original_state);
19667 let editors = cx.update_workspace(|workspace, _, cx| {
19668 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19669 });
19670 cx.update_editor(|_, _, _| {
19671 assert_eq!(
19672 editors.len(),
19673 1,
19674 "After unsuccessful fallback, no other editor should have been opened"
19675 );
19676 });
19677}
19678
19679#[gpui::test]
19680async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
19681 init_test(cx, |_| {});
19682
19683 let language = Arc::new(Language::new(
19684 LanguageConfig::default(),
19685 Some(tree_sitter_rust::LANGUAGE.into()),
19686 ));
19687
19688 let text = r#"
19689 #[cfg(test)]
19690 mod tests() {
19691 #[test]
19692 fn runnable_1() {
19693 let a = 1;
19694 }
19695
19696 #[test]
19697 fn runnable_2() {
19698 let a = 1;
19699 let b = 2;
19700 }
19701 }
19702 "#
19703 .unindent();
19704
19705 let fs = FakeFs::new(cx.executor());
19706 fs.insert_file("/file.rs", Default::default()).await;
19707
19708 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19709 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19710 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19711 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
19712 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
19713
19714 let editor = cx.new_window_entity(|window, cx| {
19715 Editor::new(
19716 EditorMode::full(),
19717 multi_buffer,
19718 Some(project.clone()),
19719 window,
19720 cx,
19721 )
19722 });
19723
19724 editor.update_in(cx, |editor, window, cx| {
19725 let snapshot = editor.buffer().read(cx).snapshot(cx);
19726 editor.tasks.insert(
19727 (buffer.read(cx).remote_id(), 3),
19728 RunnableTasks {
19729 templates: vec![],
19730 offset: snapshot.anchor_before(43),
19731 column: 0,
19732 extra_variables: HashMap::default(),
19733 context_range: BufferOffset(43)..BufferOffset(85),
19734 },
19735 );
19736 editor.tasks.insert(
19737 (buffer.read(cx).remote_id(), 8),
19738 RunnableTasks {
19739 templates: vec![],
19740 offset: snapshot.anchor_before(86),
19741 column: 0,
19742 extra_variables: HashMap::default(),
19743 context_range: BufferOffset(86)..BufferOffset(191),
19744 },
19745 );
19746
19747 // Test finding task when cursor is inside function body
19748 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19749 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
19750 });
19751 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19752 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
19753
19754 // Test finding task when cursor is on function name
19755 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19756 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
19757 });
19758 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19759 assert_eq!(row, 8, "Should find task when cursor is on function name");
19760 });
19761}
19762
19763#[gpui::test]
19764async fn test_folding_buffers(cx: &mut TestAppContext) {
19765 init_test(cx, |_| {});
19766
19767 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19768 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
19769 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
19770
19771 let fs = FakeFs::new(cx.executor());
19772 fs.insert_tree(
19773 path!("/a"),
19774 json!({
19775 "first.rs": sample_text_1,
19776 "second.rs": sample_text_2,
19777 "third.rs": sample_text_3,
19778 }),
19779 )
19780 .await;
19781 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19782 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19783 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19784 let worktree = project.update(cx, |project, cx| {
19785 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19786 assert_eq!(worktrees.len(), 1);
19787 worktrees.pop().unwrap()
19788 });
19789 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19790
19791 let buffer_1 = project
19792 .update(cx, |project, cx| {
19793 project.open_buffer((worktree_id, "first.rs"), cx)
19794 })
19795 .await
19796 .unwrap();
19797 let buffer_2 = project
19798 .update(cx, |project, cx| {
19799 project.open_buffer((worktree_id, "second.rs"), cx)
19800 })
19801 .await
19802 .unwrap();
19803 let buffer_3 = project
19804 .update(cx, |project, cx| {
19805 project.open_buffer((worktree_id, "third.rs"), cx)
19806 })
19807 .await
19808 .unwrap();
19809
19810 let multi_buffer = cx.new(|cx| {
19811 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19812 multi_buffer.push_excerpts(
19813 buffer_1.clone(),
19814 [
19815 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19816 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19817 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19818 ],
19819 cx,
19820 );
19821 multi_buffer.push_excerpts(
19822 buffer_2.clone(),
19823 [
19824 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19825 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19826 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19827 ],
19828 cx,
19829 );
19830 multi_buffer.push_excerpts(
19831 buffer_3.clone(),
19832 [
19833 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19834 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19835 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19836 ],
19837 cx,
19838 );
19839 multi_buffer
19840 });
19841 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19842 Editor::new(
19843 EditorMode::full(),
19844 multi_buffer.clone(),
19845 Some(project.clone()),
19846 window,
19847 cx,
19848 )
19849 });
19850
19851 assert_eq!(
19852 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19853 "\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",
19854 );
19855
19856 multi_buffer_editor.update(cx, |editor, cx| {
19857 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19858 });
19859 assert_eq!(
19860 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19861 "\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",
19862 "After folding the first buffer, its text should not be displayed"
19863 );
19864
19865 multi_buffer_editor.update(cx, |editor, cx| {
19866 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19867 });
19868 assert_eq!(
19869 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19870 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
19871 "After folding the second buffer, its text should not be displayed"
19872 );
19873
19874 multi_buffer_editor.update(cx, |editor, cx| {
19875 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19876 });
19877 assert_eq!(
19878 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19879 "\n\n\n\n\n",
19880 "After folding the third buffer, its text should not be displayed"
19881 );
19882
19883 // Emulate selection inside the fold logic, that should work
19884 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19885 editor
19886 .snapshot(window, cx)
19887 .next_line_boundary(Point::new(0, 4));
19888 });
19889
19890 multi_buffer_editor.update(cx, |editor, cx| {
19891 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19892 });
19893 assert_eq!(
19894 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19895 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19896 "After unfolding the second buffer, its text should be displayed"
19897 );
19898
19899 // Typing inside of buffer 1 causes that buffer to be unfolded.
19900 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19901 assert_eq!(
19902 multi_buffer
19903 .read(cx)
19904 .snapshot(cx)
19905 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
19906 .collect::<String>(),
19907 "bbbb"
19908 );
19909 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
19910 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
19911 });
19912 editor.handle_input("B", window, cx);
19913 });
19914
19915 assert_eq!(
19916 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19917 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19918 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
19919 );
19920
19921 multi_buffer_editor.update(cx, |editor, cx| {
19922 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19923 });
19924 assert_eq!(
19925 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19926 "\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",
19927 "After unfolding the all buffers, all original text should be displayed"
19928 );
19929}
19930
19931#[gpui::test]
19932async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
19933 init_test(cx, |_| {});
19934
19935 let sample_text_1 = "1111\n2222\n3333".to_string();
19936 let sample_text_2 = "4444\n5555\n6666".to_string();
19937 let sample_text_3 = "7777\n8888\n9999".to_string();
19938
19939 let fs = FakeFs::new(cx.executor());
19940 fs.insert_tree(
19941 path!("/a"),
19942 json!({
19943 "first.rs": sample_text_1,
19944 "second.rs": sample_text_2,
19945 "third.rs": sample_text_3,
19946 }),
19947 )
19948 .await;
19949 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19950 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19951 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19952 let worktree = project.update(cx, |project, cx| {
19953 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19954 assert_eq!(worktrees.len(), 1);
19955 worktrees.pop().unwrap()
19956 });
19957 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19958
19959 let buffer_1 = project
19960 .update(cx, |project, cx| {
19961 project.open_buffer((worktree_id, "first.rs"), cx)
19962 })
19963 .await
19964 .unwrap();
19965 let buffer_2 = project
19966 .update(cx, |project, cx| {
19967 project.open_buffer((worktree_id, "second.rs"), cx)
19968 })
19969 .await
19970 .unwrap();
19971 let buffer_3 = project
19972 .update(cx, |project, cx| {
19973 project.open_buffer((worktree_id, "third.rs"), cx)
19974 })
19975 .await
19976 .unwrap();
19977
19978 let multi_buffer = cx.new(|cx| {
19979 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19980 multi_buffer.push_excerpts(
19981 buffer_1.clone(),
19982 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19983 cx,
19984 );
19985 multi_buffer.push_excerpts(
19986 buffer_2.clone(),
19987 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19988 cx,
19989 );
19990 multi_buffer.push_excerpts(
19991 buffer_3.clone(),
19992 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19993 cx,
19994 );
19995 multi_buffer
19996 });
19997
19998 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19999 Editor::new(
20000 EditorMode::full(),
20001 multi_buffer,
20002 Some(project.clone()),
20003 window,
20004 cx,
20005 )
20006 });
20007
20008 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
20009 assert_eq!(
20010 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20011 full_text,
20012 );
20013
20014 multi_buffer_editor.update(cx, |editor, cx| {
20015 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
20016 });
20017 assert_eq!(
20018 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20019 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
20020 "After folding the first buffer, its text should not be displayed"
20021 );
20022
20023 multi_buffer_editor.update(cx, |editor, cx| {
20024 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
20025 });
20026
20027 assert_eq!(
20028 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20029 "\n\n\n\n\n\n7777\n8888\n9999",
20030 "After folding the second buffer, its text should not be displayed"
20031 );
20032
20033 multi_buffer_editor.update(cx, |editor, cx| {
20034 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
20035 });
20036 assert_eq!(
20037 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20038 "\n\n\n\n\n",
20039 "After folding the third buffer, its text should not be displayed"
20040 );
20041
20042 multi_buffer_editor.update(cx, |editor, cx| {
20043 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
20044 });
20045 assert_eq!(
20046 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20047 "\n\n\n\n4444\n5555\n6666\n\n",
20048 "After unfolding the second buffer, its text should be displayed"
20049 );
20050
20051 multi_buffer_editor.update(cx, |editor, cx| {
20052 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
20053 });
20054 assert_eq!(
20055 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20056 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
20057 "After unfolding the first buffer, its text should be displayed"
20058 );
20059
20060 multi_buffer_editor.update(cx, |editor, cx| {
20061 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
20062 });
20063 assert_eq!(
20064 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20065 full_text,
20066 "After unfolding all buffers, all original text should be displayed"
20067 );
20068}
20069
20070#[gpui::test]
20071async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
20072 init_test(cx, |_| {});
20073
20074 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
20075
20076 let fs = FakeFs::new(cx.executor());
20077 fs.insert_tree(
20078 path!("/a"),
20079 json!({
20080 "main.rs": sample_text,
20081 }),
20082 )
20083 .await;
20084 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20085 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20086 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20087 let worktree = project.update(cx, |project, cx| {
20088 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20089 assert_eq!(worktrees.len(), 1);
20090 worktrees.pop().unwrap()
20091 });
20092 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20093
20094 let buffer_1 = project
20095 .update(cx, |project, cx| {
20096 project.open_buffer((worktree_id, "main.rs"), cx)
20097 })
20098 .await
20099 .unwrap();
20100
20101 let multi_buffer = cx.new(|cx| {
20102 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20103 multi_buffer.push_excerpts(
20104 buffer_1.clone(),
20105 [ExcerptRange::new(
20106 Point::new(0, 0)
20107 ..Point::new(
20108 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
20109 0,
20110 ),
20111 )],
20112 cx,
20113 );
20114 multi_buffer
20115 });
20116 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20117 Editor::new(
20118 EditorMode::full(),
20119 multi_buffer,
20120 Some(project.clone()),
20121 window,
20122 cx,
20123 )
20124 });
20125
20126 let selection_range = Point::new(1, 0)..Point::new(2, 0);
20127 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20128 enum TestHighlight {}
20129 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
20130 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
20131 editor.highlight_text::<TestHighlight>(
20132 vec![highlight_range.clone()],
20133 HighlightStyle::color(Hsla::green()),
20134 cx,
20135 );
20136 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20137 s.select_ranges(Some(highlight_range))
20138 });
20139 });
20140
20141 let full_text = format!("\n\n{sample_text}");
20142 assert_eq!(
20143 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20144 full_text,
20145 );
20146}
20147
20148#[gpui::test]
20149async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
20150 init_test(cx, |_| {});
20151 cx.update(|cx| {
20152 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
20153 "keymaps/default-linux.json",
20154 cx,
20155 )
20156 .unwrap();
20157 cx.bind_keys(default_key_bindings);
20158 });
20159
20160 let (editor, cx) = cx.add_window_view(|window, cx| {
20161 let multi_buffer = MultiBuffer::build_multi(
20162 [
20163 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
20164 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
20165 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
20166 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
20167 ],
20168 cx,
20169 );
20170 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
20171
20172 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
20173 // fold all but the second buffer, so that we test navigating between two
20174 // adjacent folded buffers, as well as folded buffers at the start and
20175 // end the multibuffer
20176 editor.fold_buffer(buffer_ids[0], cx);
20177 editor.fold_buffer(buffer_ids[2], cx);
20178 editor.fold_buffer(buffer_ids[3], cx);
20179
20180 editor
20181 });
20182 cx.simulate_resize(size(px(1000.), px(1000.)));
20183
20184 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
20185 cx.assert_excerpts_with_selections(indoc! {"
20186 [EXCERPT]
20187 ˇ[FOLDED]
20188 [EXCERPT]
20189 a1
20190 b1
20191 [EXCERPT]
20192 [FOLDED]
20193 [EXCERPT]
20194 [FOLDED]
20195 "
20196 });
20197 cx.simulate_keystroke("down");
20198 cx.assert_excerpts_with_selections(indoc! {"
20199 [EXCERPT]
20200 [FOLDED]
20201 [EXCERPT]
20202 ˇa1
20203 b1
20204 [EXCERPT]
20205 [FOLDED]
20206 [EXCERPT]
20207 [FOLDED]
20208 "
20209 });
20210 cx.simulate_keystroke("down");
20211 cx.assert_excerpts_with_selections(indoc! {"
20212 [EXCERPT]
20213 [FOLDED]
20214 [EXCERPT]
20215 a1
20216 ˇb1
20217 [EXCERPT]
20218 [FOLDED]
20219 [EXCERPT]
20220 [FOLDED]
20221 "
20222 });
20223 cx.simulate_keystroke("down");
20224 cx.assert_excerpts_with_selections(indoc! {"
20225 [EXCERPT]
20226 [FOLDED]
20227 [EXCERPT]
20228 a1
20229 b1
20230 ˇ[EXCERPT]
20231 [FOLDED]
20232 [EXCERPT]
20233 [FOLDED]
20234 "
20235 });
20236 cx.simulate_keystroke("down");
20237 cx.assert_excerpts_with_selections(indoc! {"
20238 [EXCERPT]
20239 [FOLDED]
20240 [EXCERPT]
20241 a1
20242 b1
20243 [EXCERPT]
20244 ˇ[FOLDED]
20245 [EXCERPT]
20246 [FOLDED]
20247 "
20248 });
20249 for _ in 0..5 {
20250 cx.simulate_keystroke("down");
20251 cx.assert_excerpts_with_selections(indoc! {"
20252 [EXCERPT]
20253 [FOLDED]
20254 [EXCERPT]
20255 a1
20256 b1
20257 [EXCERPT]
20258 [FOLDED]
20259 [EXCERPT]
20260 ˇ[FOLDED]
20261 "
20262 });
20263 }
20264
20265 cx.simulate_keystroke("up");
20266 cx.assert_excerpts_with_selections(indoc! {"
20267 [EXCERPT]
20268 [FOLDED]
20269 [EXCERPT]
20270 a1
20271 b1
20272 [EXCERPT]
20273 ˇ[FOLDED]
20274 [EXCERPT]
20275 [FOLDED]
20276 "
20277 });
20278 cx.simulate_keystroke("up");
20279 cx.assert_excerpts_with_selections(indoc! {"
20280 [EXCERPT]
20281 [FOLDED]
20282 [EXCERPT]
20283 a1
20284 b1
20285 ˇ[EXCERPT]
20286 [FOLDED]
20287 [EXCERPT]
20288 [FOLDED]
20289 "
20290 });
20291 cx.simulate_keystroke("up");
20292 cx.assert_excerpts_with_selections(indoc! {"
20293 [EXCERPT]
20294 [FOLDED]
20295 [EXCERPT]
20296 a1
20297 ˇb1
20298 [EXCERPT]
20299 [FOLDED]
20300 [EXCERPT]
20301 [FOLDED]
20302 "
20303 });
20304 cx.simulate_keystroke("up");
20305 cx.assert_excerpts_with_selections(indoc! {"
20306 [EXCERPT]
20307 [FOLDED]
20308 [EXCERPT]
20309 ˇa1
20310 b1
20311 [EXCERPT]
20312 [FOLDED]
20313 [EXCERPT]
20314 [FOLDED]
20315 "
20316 });
20317 for _ in 0..5 {
20318 cx.simulate_keystroke("up");
20319 cx.assert_excerpts_with_selections(indoc! {"
20320 [EXCERPT]
20321 ˇ[FOLDED]
20322 [EXCERPT]
20323 a1
20324 b1
20325 [EXCERPT]
20326 [FOLDED]
20327 [EXCERPT]
20328 [FOLDED]
20329 "
20330 });
20331 }
20332}
20333
20334#[gpui::test]
20335async fn test_inline_completion_text(cx: &mut TestAppContext) {
20336 init_test(cx, |_| {});
20337
20338 // Simple insertion
20339 assert_highlighted_edits(
20340 "Hello, world!",
20341 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
20342 true,
20343 cx,
20344 |highlighted_edits, cx| {
20345 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
20346 assert_eq!(highlighted_edits.highlights.len(), 1);
20347 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
20348 assert_eq!(
20349 highlighted_edits.highlights[0].1.background_color,
20350 Some(cx.theme().status().created_background)
20351 );
20352 },
20353 )
20354 .await;
20355
20356 // Replacement
20357 assert_highlighted_edits(
20358 "This is a test.",
20359 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
20360 false,
20361 cx,
20362 |highlighted_edits, cx| {
20363 assert_eq!(highlighted_edits.text, "That is a test.");
20364 assert_eq!(highlighted_edits.highlights.len(), 1);
20365 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
20366 assert_eq!(
20367 highlighted_edits.highlights[0].1.background_color,
20368 Some(cx.theme().status().created_background)
20369 );
20370 },
20371 )
20372 .await;
20373
20374 // Multiple edits
20375 assert_highlighted_edits(
20376 "Hello, world!",
20377 vec![
20378 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
20379 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
20380 ],
20381 false,
20382 cx,
20383 |highlighted_edits, cx| {
20384 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
20385 assert_eq!(highlighted_edits.highlights.len(), 2);
20386 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
20387 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
20388 assert_eq!(
20389 highlighted_edits.highlights[0].1.background_color,
20390 Some(cx.theme().status().created_background)
20391 );
20392 assert_eq!(
20393 highlighted_edits.highlights[1].1.background_color,
20394 Some(cx.theme().status().created_background)
20395 );
20396 },
20397 )
20398 .await;
20399
20400 // Multiple lines with edits
20401 assert_highlighted_edits(
20402 "First line\nSecond line\nThird line\nFourth line",
20403 vec![
20404 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
20405 (
20406 Point::new(2, 0)..Point::new(2, 10),
20407 "New third line".to_string(),
20408 ),
20409 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
20410 ],
20411 false,
20412 cx,
20413 |highlighted_edits, cx| {
20414 assert_eq!(
20415 highlighted_edits.text,
20416 "Second modified\nNew third line\nFourth updated line"
20417 );
20418 assert_eq!(highlighted_edits.highlights.len(), 3);
20419 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
20420 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
20421 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
20422 for highlight in &highlighted_edits.highlights {
20423 assert_eq!(
20424 highlight.1.background_color,
20425 Some(cx.theme().status().created_background)
20426 );
20427 }
20428 },
20429 )
20430 .await;
20431}
20432
20433#[gpui::test]
20434async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
20435 init_test(cx, |_| {});
20436
20437 // Deletion
20438 assert_highlighted_edits(
20439 "Hello, world!",
20440 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
20441 true,
20442 cx,
20443 |highlighted_edits, cx| {
20444 assert_eq!(highlighted_edits.text, "Hello, world!");
20445 assert_eq!(highlighted_edits.highlights.len(), 1);
20446 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
20447 assert_eq!(
20448 highlighted_edits.highlights[0].1.background_color,
20449 Some(cx.theme().status().deleted_background)
20450 );
20451 },
20452 )
20453 .await;
20454
20455 // Insertion
20456 assert_highlighted_edits(
20457 "Hello, world!",
20458 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
20459 true,
20460 cx,
20461 |highlighted_edits, cx| {
20462 assert_eq!(highlighted_edits.highlights.len(), 1);
20463 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
20464 assert_eq!(
20465 highlighted_edits.highlights[0].1.background_color,
20466 Some(cx.theme().status().created_background)
20467 );
20468 },
20469 )
20470 .await;
20471}
20472
20473async fn assert_highlighted_edits(
20474 text: &str,
20475 edits: Vec<(Range<Point>, String)>,
20476 include_deletions: bool,
20477 cx: &mut TestAppContext,
20478 assertion_fn: impl Fn(HighlightedText, &App),
20479) {
20480 let window = cx.add_window(|window, cx| {
20481 let buffer = MultiBuffer::build_simple(text, cx);
20482 Editor::new(EditorMode::full(), buffer, None, window, cx)
20483 });
20484 let cx = &mut VisualTestContext::from_window(*window, cx);
20485
20486 let (buffer, snapshot) = window
20487 .update(cx, |editor, _window, cx| {
20488 (
20489 editor.buffer().clone(),
20490 editor.buffer().read(cx).snapshot(cx),
20491 )
20492 })
20493 .unwrap();
20494
20495 let edits = edits
20496 .into_iter()
20497 .map(|(range, edit)| {
20498 (
20499 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
20500 edit,
20501 )
20502 })
20503 .collect::<Vec<_>>();
20504
20505 let text_anchor_edits = edits
20506 .clone()
20507 .into_iter()
20508 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
20509 .collect::<Vec<_>>();
20510
20511 let edit_preview = window
20512 .update(cx, |_, _window, cx| {
20513 buffer
20514 .read(cx)
20515 .as_singleton()
20516 .unwrap()
20517 .read(cx)
20518 .preview_edits(text_anchor_edits.into(), cx)
20519 })
20520 .unwrap()
20521 .await;
20522
20523 cx.update(|_window, cx| {
20524 let highlighted_edits = inline_completion_edit_text(
20525 &snapshot.as_singleton().unwrap().2,
20526 &edits,
20527 &edit_preview,
20528 include_deletions,
20529 cx,
20530 );
20531 assertion_fn(highlighted_edits, cx)
20532 });
20533}
20534
20535#[track_caller]
20536fn assert_breakpoint(
20537 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
20538 path: &Arc<Path>,
20539 expected: Vec<(u32, Breakpoint)>,
20540) {
20541 if expected.len() == 0usize {
20542 assert!(!breakpoints.contains_key(path), "{}", path.display());
20543 } else {
20544 let mut breakpoint = breakpoints
20545 .get(path)
20546 .unwrap()
20547 .into_iter()
20548 .map(|breakpoint| {
20549 (
20550 breakpoint.row,
20551 Breakpoint {
20552 message: breakpoint.message.clone(),
20553 state: breakpoint.state,
20554 condition: breakpoint.condition.clone(),
20555 hit_condition: breakpoint.hit_condition.clone(),
20556 },
20557 )
20558 })
20559 .collect::<Vec<_>>();
20560
20561 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
20562
20563 assert_eq!(expected, breakpoint);
20564 }
20565}
20566
20567fn add_log_breakpoint_at_cursor(
20568 editor: &mut Editor,
20569 log_message: &str,
20570 window: &mut Window,
20571 cx: &mut Context<Editor>,
20572) {
20573 let (anchor, bp) = editor
20574 .breakpoints_at_cursors(window, cx)
20575 .first()
20576 .and_then(|(anchor, bp)| {
20577 if let Some(bp) = bp {
20578 Some((*anchor, bp.clone()))
20579 } else {
20580 None
20581 }
20582 })
20583 .unwrap_or_else(|| {
20584 let cursor_position: Point = editor.selections.newest(cx).head();
20585
20586 let breakpoint_position = editor
20587 .snapshot(window, cx)
20588 .display_snapshot
20589 .buffer_snapshot
20590 .anchor_before(Point::new(cursor_position.row, 0));
20591
20592 (breakpoint_position, Breakpoint::new_log(&log_message))
20593 });
20594
20595 editor.edit_breakpoint_at_anchor(
20596 anchor,
20597 bp,
20598 BreakpointEditAction::EditLogMessage(log_message.into()),
20599 cx,
20600 );
20601}
20602
20603#[gpui::test]
20604async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
20605 init_test(cx, |_| {});
20606
20607 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20608 let fs = FakeFs::new(cx.executor());
20609 fs.insert_tree(
20610 path!("/a"),
20611 json!({
20612 "main.rs": sample_text,
20613 }),
20614 )
20615 .await;
20616 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20617 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20618 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20619
20620 let fs = FakeFs::new(cx.executor());
20621 fs.insert_tree(
20622 path!("/a"),
20623 json!({
20624 "main.rs": sample_text,
20625 }),
20626 )
20627 .await;
20628 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20629 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20630 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20631 let worktree_id = workspace
20632 .update(cx, |workspace, _window, cx| {
20633 workspace.project().update(cx, |project, cx| {
20634 project.worktrees(cx).next().unwrap().read(cx).id()
20635 })
20636 })
20637 .unwrap();
20638
20639 let buffer = project
20640 .update(cx, |project, cx| {
20641 project.open_buffer((worktree_id, "main.rs"), cx)
20642 })
20643 .await
20644 .unwrap();
20645
20646 let (editor, cx) = cx.add_window_view(|window, cx| {
20647 Editor::new(
20648 EditorMode::full(),
20649 MultiBuffer::build_from_buffer(buffer, cx),
20650 Some(project.clone()),
20651 window,
20652 cx,
20653 )
20654 });
20655
20656 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20657 let abs_path = project.read_with(cx, |project, cx| {
20658 project
20659 .absolute_path(&project_path, cx)
20660 .map(|path_buf| Arc::from(path_buf.to_owned()))
20661 .unwrap()
20662 });
20663
20664 // assert we can add breakpoint on the first line
20665 editor.update_in(cx, |editor, window, cx| {
20666 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20667 editor.move_to_end(&MoveToEnd, window, cx);
20668 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20669 });
20670
20671 let breakpoints = editor.update(cx, |editor, cx| {
20672 editor
20673 .breakpoint_store()
20674 .as_ref()
20675 .unwrap()
20676 .read(cx)
20677 .all_source_breakpoints(cx)
20678 .clone()
20679 });
20680
20681 assert_eq!(1, breakpoints.len());
20682 assert_breakpoint(
20683 &breakpoints,
20684 &abs_path,
20685 vec![
20686 (0, Breakpoint::new_standard()),
20687 (3, Breakpoint::new_standard()),
20688 ],
20689 );
20690
20691 editor.update_in(cx, |editor, window, cx| {
20692 editor.move_to_beginning(&MoveToBeginning, window, cx);
20693 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20694 });
20695
20696 let breakpoints = editor.update(cx, |editor, cx| {
20697 editor
20698 .breakpoint_store()
20699 .as_ref()
20700 .unwrap()
20701 .read(cx)
20702 .all_source_breakpoints(cx)
20703 .clone()
20704 });
20705
20706 assert_eq!(1, breakpoints.len());
20707 assert_breakpoint(
20708 &breakpoints,
20709 &abs_path,
20710 vec![(3, Breakpoint::new_standard())],
20711 );
20712
20713 editor.update_in(cx, |editor, window, cx| {
20714 editor.move_to_end(&MoveToEnd, window, cx);
20715 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20716 });
20717
20718 let breakpoints = editor.update(cx, |editor, cx| {
20719 editor
20720 .breakpoint_store()
20721 .as_ref()
20722 .unwrap()
20723 .read(cx)
20724 .all_source_breakpoints(cx)
20725 .clone()
20726 });
20727
20728 assert_eq!(0, breakpoints.len());
20729 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20730}
20731
20732#[gpui::test]
20733async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
20734 init_test(cx, |_| {});
20735
20736 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20737
20738 let fs = FakeFs::new(cx.executor());
20739 fs.insert_tree(
20740 path!("/a"),
20741 json!({
20742 "main.rs": sample_text,
20743 }),
20744 )
20745 .await;
20746 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20747 let (workspace, cx) =
20748 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20749
20750 let worktree_id = workspace.update(cx, |workspace, cx| {
20751 workspace.project().update(cx, |project, cx| {
20752 project.worktrees(cx).next().unwrap().read(cx).id()
20753 })
20754 });
20755
20756 let buffer = project
20757 .update(cx, |project, cx| {
20758 project.open_buffer((worktree_id, "main.rs"), cx)
20759 })
20760 .await
20761 .unwrap();
20762
20763 let (editor, cx) = cx.add_window_view(|window, cx| {
20764 Editor::new(
20765 EditorMode::full(),
20766 MultiBuffer::build_from_buffer(buffer, cx),
20767 Some(project.clone()),
20768 window,
20769 cx,
20770 )
20771 });
20772
20773 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20774 let abs_path = project.read_with(cx, |project, cx| {
20775 project
20776 .absolute_path(&project_path, cx)
20777 .map(|path_buf| Arc::from(path_buf.to_owned()))
20778 .unwrap()
20779 });
20780
20781 editor.update_in(cx, |editor, window, cx| {
20782 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20783 });
20784
20785 let breakpoints = editor.update(cx, |editor, cx| {
20786 editor
20787 .breakpoint_store()
20788 .as_ref()
20789 .unwrap()
20790 .read(cx)
20791 .all_source_breakpoints(cx)
20792 .clone()
20793 });
20794
20795 assert_breakpoint(
20796 &breakpoints,
20797 &abs_path,
20798 vec![(0, Breakpoint::new_log("hello world"))],
20799 );
20800
20801 // Removing a log message from a log breakpoint should remove it
20802 editor.update_in(cx, |editor, window, cx| {
20803 add_log_breakpoint_at_cursor(editor, "", window, cx);
20804 });
20805
20806 let breakpoints = editor.update(cx, |editor, cx| {
20807 editor
20808 .breakpoint_store()
20809 .as_ref()
20810 .unwrap()
20811 .read(cx)
20812 .all_source_breakpoints(cx)
20813 .clone()
20814 });
20815
20816 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20817
20818 editor.update_in(cx, |editor, window, cx| {
20819 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20820 editor.move_to_end(&MoveToEnd, window, cx);
20821 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20822 // Not adding a log message to a standard breakpoint shouldn't remove it
20823 add_log_breakpoint_at_cursor(editor, "", window, cx);
20824 });
20825
20826 let breakpoints = editor.update(cx, |editor, cx| {
20827 editor
20828 .breakpoint_store()
20829 .as_ref()
20830 .unwrap()
20831 .read(cx)
20832 .all_source_breakpoints(cx)
20833 .clone()
20834 });
20835
20836 assert_breakpoint(
20837 &breakpoints,
20838 &abs_path,
20839 vec![
20840 (0, Breakpoint::new_standard()),
20841 (3, Breakpoint::new_standard()),
20842 ],
20843 );
20844
20845 editor.update_in(cx, |editor, window, cx| {
20846 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20847 });
20848
20849 let breakpoints = editor.update(cx, |editor, cx| {
20850 editor
20851 .breakpoint_store()
20852 .as_ref()
20853 .unwrap()
20854 .read(cx)
20855 .all_source_breakpoints(cx)
20856 .clone()
20857 });
20858
20859 assert_breakpoint(
20860 &breakpoints,
20861 &abs_path,
20862 vec![
20863 (0, Breakpoint::new_standard()),
20864 (3, Breakpoint::new_log("hello world")),
20865 ],
20866 );
20867
20868 editor.update_in(cx, |editor, window, cx| {
20869 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
20870 });
20871
20872 let breakpoints = editor.update(cx, |editor, cx| {
20873 editor
20874 .breakpoint_store()
20875 .as_ref()
20876 .unwrap()
20877 .read(cx)
20878 .all_source_breakpoints(cx)
20879 .clone()
20880 });
20881
20882 assert_breakpoint(
20883 &breakpoints,
20884 &abs_path,
20885 vec![
20886 (0, Breakpoint::new_standard()),
20887 (3, Breakpoint::new_log("hello Earth!!")),
20888 ],
20889 );
20890}
20891
20892/// This also tests that Editor::breakpoint_at_cursor_head is working properly
20893/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
20894/// or when breakpoints were placed out of order. This tests for a regression too
20895#[gpui::test]
20896async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
20897 init_test(cx, |_| {});
20898
20899 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20900 let fs = FakeFs::new(cx.executor());
20901 fs.insert_tree(
20902 path!("/a"),
20903 json!({
20904 "main.rs": sample_text,
20905 }),
20906 )
20907 .await;
20908 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20909 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20910 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20911
20912 let fs = FakeFs::new(cx.executor());
20913 fs.insert_tree(
20914 path!("/a"),
20915 json!({
20916 "main.rs": sample_text,
20917 }),
20918 )
20919 .await;
20920 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20921 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20922 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20923 let worktree_id = workspace
20924 .update(cx, |workspace, _window, cx| {
20925 workspace.project().update(cx, |project, cx| {
20926 project.worktrees(cx).next().unwrap().read(cx).id()
20927 })
20928 })
20929 .unwrap();
20930
20931 let buffer = project
20932 .update(cx, |project, cx| {
20933 project.open_buffer((worktree_id, "main.rs"), cx)
20934 })
20935 .await
20936 .unwrap();
20937
20938 let (editor, cx) = cx.add_window_view(|window, cx| {
20939 Editor::new(
20940 EditorMode::full(),
20941 MultiBuffer::build_from_buffer(buffer, cx),
20942 Some(project.clone()),
20943 window,
20944 cx,
20945 )
20946 });
20947
20948 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20949 let abs_path = project.read_with(cx, |project, cx| {
20950 project
20951 .absolute_path(&project_path, cx)
20952 .map(|path_buf| Arc::from(path_buf.to_owned()))
20953 .unwrap()
20954 });
20955
20956 // assert we can add breakpoint on the first line
20957 editor.update_in(cx, |editor, window, cx| {
20958 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20959 editor.move_to_end(&MoveToEnd, window, cx);
20960 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20961 editor.move_up(&MoveUp, window, cx);
20962 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20963 });
20964
20965 let breakpoints = editor.update(cx, |editor, cx| {
20966 editor
20967 .breakpoint_store()
20968 .as_ref()
20969 .unwrap()
20970 .read(cx)
20971 .all_source_breakpoints(cx)
20972 .clone()
20973 });
20974
20975 assert_eq!(1, breakpoints.len());
20976 assert_breakpoint(
20977 &breakpoints,
20978 &abs_path,
20979 vec![
20980 (0, Breakpoint::new_standard()),
20981 (2, Breakpoint::new_standard()),
20982 (3, Breakpoint::new_standard()),
20983 ],
20984 );
20985
20986 editor.update_in(cx, |editor, window, cx| {
20987 editor.move_to_beginning(&MoveToBeginning, window, cx);
20988 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20989 editor.move_to_end(&MoveToEnd, window, cx);
20990 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20991 // Disabling a breakpoint that doesn't exist should do nothing
20992 editor.move_up(&MoveUp, window, cx);
20993 editor.move_up(&MoveUp, window, cx);
20994 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20995 });
20996
20997 let breakpoints = editor.update(cx, |editor, cx| {
20998 editor
20999 .breakpoint_store()
21000 .as_ref()
21001 .unwrap()
21002 .read(cx)
21003 .all_source_breakpoints(cx)
21004 .clone()
21005 });
21006
21007 let disable_breakpoint = {
21008 let mut bp = Breakpoint::new_standard();
21009 bp.state = BreakpointState::Disabled;
21010 bp
21011 };
21012
21013 assert_eq!(1, breakpoints.len());
21014 assert_breakpoint(
21015 &breakpoints,
21016 &abs_path,
21017 vec![
21018 (0, disable_breakpoint.clone()),
21019 (2, Breakpoint::new_standard()),
21020 (3, disable_breakpoint.clone()),
21021 ],
21022 );
21023
21024 editor.update_in(cx, |editor, window, cx| {
21025 editor.move_to_beginning(&MoveToBeginning, window, cx);
21026 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
21027 editor.move_to_end(&MoveToEnd, window, cx);
21028 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
21029 editor.move_up(&MoveUp, window, cx);
21030 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21031 });
21032
21033 let breakpoints = editor.update(cx, |editor, cx| {
21034 editor
21035 .breakpoint_store()
21036 .as_ref()
21037 .unwrap()
21038 .read(cx)
21039 .all_source_breakpoints(cx)
21040 .clone()
21041 });
21042
21043 assert_eq!(1, breakpoints.len());
21044 assert_breakpoint(
21045 &breakpoints,
21046 &abs_path,
21047 vec![
21048 (0, Breakpoint::new_standard()),
21049 (2, disable_breakpoint),
21050 (3, Breakpoint::new_standard()),
21051 ],
21052 );
21053}
21054
21055#[gpui::test]
21056async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
21057 init_test(cx, |_| {});
21058 let capabilities = lsp::ServerCapabilities {
21059 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
21060 prepare_provider: Some(true),
21061 work_done_progress_options: Default::default(),
21062 })),
21063 ..Default::default()
21064 };
21065 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21066
21067 cx.set_state(indoc! {"
21068 struct Fˇoo {}
21069 "});
21070
21071 cx.update_editor(|editor, _, cx| {
21072 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21073 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21074 editor.highlight_background::<DocumentHighlightRead>(
21075 &[highlight_range],
21076 |theme| theme.colors().editor_document_highlight_read_background,
21077 cx,
21078 );
21079 });
21080
21081 let mut prepare_rename_handler = cx
21082 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
21083 move |_, _, _| async move {
21084 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
21085 start: lsp::Position {
21086 line: 0,
21087 character: 7,
21088 },
21089 end: lsp::Position {
21090 line: 0,
21091 character: 10,
21092 },
21093 })))
21094 },
21095 );
21096 let prepare_rename_task = cx
21097 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21098 .expect("Prepare rename was not started");
21099 prepare_rename_handler.next().await.unwrap();
21100 prepare_rename_task.await.expect("Prepare rename failed");
21101
21102 let mut rename_handler =
21103 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21104 let edit = lsp::TextEdit {
21105 range: lsp::Range {
21106 start: lsp::Position {
21107 line: 0,
21108 character: 7,
21109 },
21110 end: lsp::Position {
21111 line: 0,
21112 character: 10,
21113 },
21114 },
21115 new_text: "FooRenamed".to_string(),
21116 };
21117 Ok(Some(lsp::WorkspaceEdit::new(
21118 // Specify the same edit twice
21119 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
21120 )))
21121 });
21122 let rename_task = cx
21123 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21124 .expect("Confirm rename was not started");
21125 rename_handler.next().await.unwrap();
21126 rename_task.await.expect("Confirm rename failed");
21127 cx.run_until_parked();
21128
21129 // Despite two edits, only one is actually applied as those are identical
21130 cx.assert_editor_state(indoc! {"
21131 struct FooRenamedˇ {}
21132 "});
21133}
21134
21135#[gpui::test]
21136async fn test_rename_without_prepare(cx: &mut TestAppContext) {
21137 init_test(cx, |_| {});
21138 // These capabilities indicate that the server does not support prepare rename.
21139 let capabilities = lsp::ServerCapabilities {
21140 rename_provider: Some(lsp::OneOf::Left(true)),
21141 ..Default::default()
21142 };
21143 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21144
21145 cx.set_state(indoc! {"
21146 struct Fˇoo {}
21147 "});
21148
21149 cx.update_editor(|editor, _window, cx| {
21150 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21151 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21152 editor.highlight_background::<DocumentHighlightRead>(
21153 &[highlight_range],
21154 |theme| theme.colors().editor_document_highlight_read_background,
21155 cx,
21156 );
21157 });
21158
21159 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21160 .expect("Prepare rename was not started")
21161 .await
21162 .expect("Prepare rename failed");
21163
21164 let mut rename_handler =
21165 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21166 let edit = lsp::TextEdit {
21167 range: lsp::Range {
21168 start: lsp::Position {
21169 line: 0,
21170 character: 7,
21171 },
21172 end: lsp::Position {
21173 line: 0,
21174 character: 10,
21175 },
21176 },
21177 new_text: "FooRenamed".to_string(),
21178 };
21179 Ok(Some(lsp::WorkspaceEdit::new(
21180 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
21181 )))
21182 });
21183 let rename_task = cx
21184 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21185 .expect("Confirm rename was not started");
21186 rename_handler.next().await.unwrap();
21187 rename_task.await.expect("Confirm rename failed");
21188 cx.run_until_parked();
21189
21190 // Correct range is renamed, as `surrounding_word` is used to find it.
21191 cx.assert_editor_state(indoc! {"
21192 struct FooRenamedˇ {}
21193 "});
21194}
21195
21196#[gpui::test]
21197async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
21198 init_test(cx, |_| {});
21199 let mut cx = EditorTestContext::new(cx).await;
21200
21201 let language = Arc::new(
21202 Language::new(
21203 LanguageConfig::default(),
21204 Some(tree_sitter_html::LANGUAGE.into()),
21205 )
21206 .with_brackets_query(
21207 r#"
21208 ("<" @open "/>" @close)
21209 ("</" @open ">" @close)
21210 ("<" @open ">" @close)
21211 ("\"" @open "\"" @close)
21212 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
21213 "#,
21214 )
21215 .unwrap(),
21216 );
21217 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21218
21219 cx.set_state(indoc! {"
21220 <span>ˇ</span>
21221 "});
21222 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21223 cx.assert_editor_state(indoc! {"
21224 <span>
21225 ˇ
21226 </span>
21227 "});
21228
21229 cx.set_state(indoc! {"
21230 <span><span></span>ˇ</span>
21231 "});
21232 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21233 cx.assert_editor_state(indoc! {"
21234 <span><span></span>
21235 ˇ</span>
21236 "});
21237
21238 cx.set_state(indoc! {"
21239 <span>ˇ
21240 </span>
21241 "});
21242 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21243 cx.assert_editor_state(indoc! {"
21244 <span>
21245 ˇ
21246 </span>
21247 "});
21248}
21249
21250#[gpui::test(iterations = 10)]
21251async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
21252 init_test(cx, |_| {});
21253
21254 let fs = FakeFs::new(cx.executor());
21255 fs.insert_tree(
21256 path!("/dir"),
21257 json!({
21258 "a.ts": "a",
21259 }),
21260 )
21261 .await;
21262
21263 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
21264 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21265 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21266
21267 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21268 language_registry.add(Arc::new(Language::new(
21269 LanguageConfig {
21270 name: "TypeScript".into(),
21271 matcher: LanguageMatcher {
21272 path_suffixes: vec!["ts".to_string()],
21273 ..Default::default()
21274 },
21275 ..Default::default()
21276 },
21277 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
21278 )));
21279 let mut fake_language_servers = language_registry.register_fake_lsp(
21280 "TypeScript",
21281 FakeLspAdapter {
21282 capabilities: lsp::ServerCapabilities {
21283 code_lens_provider: Some(lsp::CodeLensOptions {
21284 resolve_provider: Some(true),
21285 }),
21286 execute_command_provider: Some(lsp::ExecuteCommandOptions {
21287 commands: vec!["_the/command".to_string()],
21288 ..lsp::ExecuteCommandOptions::default()
21289 }),
21290 ..lsp::ServerCapabilities::default()
21291 },
21292 ..FakeLspAdapter::default()
21293 },
21294 );
21295
21296 let (buffer, _handle) = project
21297 .update(cx, |p, cx| {
21298 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
21299 })
21300 .await
21301 .unwrap();
21302 cx.executor().run_until_parked();
21303
21304 let fake_server = fake_language_servers.next().await.unwrap();
21305
21306 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
21307 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
21308 drop(buffer_snapshot);
21309 let actions = cx
21310 .update_window(*workspace, |_, window, cx| {
21311 project.code_actions(&buffer, anchor..anchor, window, cx)
21312 })
21313 .unwrap();
21314
21315 fake_server
21316 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21317 Ok(Some(vec![
21318 lsp::CodeLens {
21319 range: lsp::Range::default(),
21320 command: Some(lsp::Command {
21321 title: "Code lens command".to_owned(),
21322 command: "_the/command".to_owned(),
21323 arguments: None,
21324 }),
21325 data: None,
21326 },
21327 lsp::CodeLens {
21328 range: lsp::Range::default(),
21329 command: Some(lsp::Command {
21330 title: "Command not in capabilities".to_owned(),
21331 command: "not in capabilities".to_owned(),
21332 arguments: None,
21333 }),
21334 data: None,
21335 },
21336 lsp::CodeLens {
21337 range: lsp::Range {
21338 start: lsp::Position {
21339 line: 1,
21340 character: 1,
21341 },
21342 end: lsp::Position {
21343 line: 1,
21344 character: 1,
21345 },
21346 },
21347 command: Some(lsp::Command {
21348 title: "Command not in range".to_owned(),
21349 command: "_the/command".to_owned(),
21350 arguments: None,
21351 }),
21352 data: None,
21353 },
21354 ]))
21355 })
21356 .next()
21357 .await;
21358
21359 let actions = actions.await.unwrap();
21360 assert_eq!(
21361 actions.len(),
21362 1,
21363 "Should have only one valid action for the 0..0 range"
21364 );
21365 let action = actions[0].clone();
21366 let apply = project.update(cx, |project, cx| {
21367 project.apply_code_action(buffer.clone(), action, true, cx)
21368 });
21369
21370 // Resolving the code action does not populate its edits. In absence of
21371 // edits, we must execute the given command.
21372 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
21373 |mut lens, _| async move {
21374 let lens_command = lens.command.as_mut().expect("should have a command");
21375 assert_eq!(lens_command.title, "Code lens command");
21376 lens_command.arguments = Some(vec![json!("the-argument")]);
21377 Ok(lens)
21378 },
21379 );
21380
21381 // While executing the command, the language server sends the editor
21382 // a `workspaceEdit` request.
21383 fake_server
21384 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
21385 let fake = fake_server.clone();
21386 move |params, _| {
21387 assert_eq!(params.command, "_the/command");
21388 let fake = fake.clone();
21389 async move {
21390 fake.server
21391 .request::<lsp::request::ApplyWorkspaceEdit>(
21392 lsp::ApplyWorkspaceEditParams {
21393 label: None,
21394 edit: lsp::WorkspaceEdit {
21395 changes: Some(
21396 [(
21397 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
21398 vec![lsp::TextEdit {
21399 range: lsp::Range::new(
21400 lsp::Position::new(0, 0),
21401 lsp::Position::new(0, 0),
21402 ),
21403 new_text: "X".into(),
21404 }],
21405 )]
21406 .into_iter()
21407 .collect(),
21408 ),
21409 ..Default::default()
21410 },
21411 },
21412 )
21413 .await
21414 .into_response()
21415 .unwrap();
21416 Ok(Some(json!(null)))
21417 }
21418 }
21419 })
21420 .next()
21421 .await;
21422
21423 // Applying the code lens command returns a project transaction containing the edits
21424 // sent by the language server in its `workspaceEdit` request.
21425 let transaction = apply.await.unwrap();
21426 assert!(transaction.0.contains_key(&buffer));
21427 buffer.update(cx, |buffer, cx| {
21428 assert_eq!(buffer.text(), "Xa");
21429 buffer.undo(cx);
21430 assert_eq!(buffer.text(), "a");
21431 });
21432}
21433
21434#[gpui::test]
21435async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
21436 init_test(cx, |_| {});
21437
21438 let fs = FakeFs::new(cx.executor());
21439 let main_text = r#"fn main() {
21440println!("1");
21441println!("2");
21442println!("3");
21443println!("4");
21444println!("5");
21445}"#;
21446 let lib_text = "mod foo {}";
21447 fs.insert_tree(
21448 path!("/a"),
21449 json!({
21450 "lib.rs": lib_text,
21451 "main.rs": main_text,
21452 }),
21453 )
21454 .await;
21455
21456 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21457 let (workspace, cx) =
21458 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21459 let worktree_id = workspace.update(cx, |workspace, cx| {
21460 workspace.project().update(cx, |project, cx| {
21461 project.worktrees(cx).next().unwrap().read(cx).id()
21462 })
21463 });
21464
21465 let expected_ranges = vec![
21466 Point::new(0, 0)..Point::new(0, 0),
21467 Point::new(1, 0)..Point::new(1, 1),
21468 Point::new(2, 0)..Point::new(2, 2),
21469 Point::new(3, 0)..Point::new(3, 3),
21470 ];
21471
21472 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21473 let editor_1 = workspace
21474 .update_in(cx, |workspace, window, cx| {
21475 workspace.open_path(
21476 (worktree_id, "main.rs"),
21477 Some(pane_1.downgrade()),
21478 true,
21479 window,
21480 cx,
21481 )
21482 })
21483 .unwrap()
21484 .await
21485 .downcast::<Editor>()
21486 .unwrap();
21487 pane_1.update(cx, |pane, cx| {
21488 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21489 open_editor.update(cx, |editor, cx| {
21490 assert_eq!(
21491 editor.display_text(cx),
21492 main_text,
21493 "Original main.rs text on initial open",
21494 );
21495 assert_eq!(
21496 editor
21497 .selections
21498 .all::<Point>(cx)
21499 .into_iter()
21500 .map(|s| s.range())
21501 .collect::<Vec<_>>(),
21502 vec![Point::zero()..Point::zero()],
21503 "Default selections on initial open",
21504 );
21505 })
21506 });
21507 editor_1.update_in(cx, |editor, window, cx| {
21508 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21509 s.select_ranges(expected_ranges.clone());
21510 });
21511 });
21512
21513 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
21514 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
21515 });
21516 let editor_2 = workspace
21517 .update_in(cx, |workspace, window, cx| {
21518 workspace.open_path(
21519 (worktree_id, "main.rs"),
21520 Some(pane_2.downgrade()),
21521 true,
21522 window,
21523 cx,
21524 )
21525 })
21526 .unwrap()
21527 .await
21528 .downcast::<Editor>()
21529 .unwrap();
21530 pane_2.update(cx, |pane, cx| {
21531 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21532 open_editor.update(cx, |editor, cx| {
21533 assert_eq!(
21534 editor.display_text(cx),
21535 main_text,
21536 "Original main.rs text on initial open in another panel",
21537 );
21538 assert_eq!(
21539 editor
21540 .selections
21541 .all::<Point>(cx)
21542 .into_iter()
21543 .map(|s| s.range())
21544 .collect::<Vec<_>>(),
21545 vec![Point::zero()..Point::zero()],
21546 "Default selections on initial open in another panel",
21547 );
21548 })
21549 });
21550
21551 editor_2.update_in(cx, |editor, window, cx| {
21552 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
21553 });
21554
21555 let _other_editor_1 = workspace
21556 .update_in(cx, |workspace, window, cx| {
21557 workspace.open_path(
21558 (worktree_id, "lib.rs"),
21559 Some(pane_1.downgrade()),
21560 true,
21561 window,
21562 cx,
21563 )
21564 })
21565 .unwrap()
21566 .await
21567 .downcast::<Editor>()
21568 .unwrap();
21569 pane_1
21570 .update_in(cx, |pane, window, cx| {
21571 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
21572 })
21573 .await
21574 .unwrap();
21575 drop(editor_1);
21576 pane_1.update(cx, |pane, cx| {
21577 pane.active_item()
21578 .unwrap()
21579 .downcast::<Editor>()
21580 .unwrap()
21581 .update(cx, |editor, cx| {
21582 assert_eq!(
21583 editor.display_text(cx),
21584 lib_text,
21585 "Other file should be open and active",
21586 );
21587 });
21588 assert_eq!(pane.items().count(), 1, "No other editors should be open");
21589 });
21590
21591 let _other_editor_2 = workspace
21592 .update_in(cx, |workspace, window, cx| {
21593 workspace.open_path(
21594 (worktree_id, "lib.rs"),
21595 Some(pane_2.downgrade()),
21596 true,
21597 window,
21598 cx,
21599 )
21600 })
21601 .unwrap()
21602 .await
21603 .downcast::<Editor>()
21604 .unwrap();
21605 pane_2
21606 .update_in(cx, |pane, window, cx| {
21607 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
21608 })
21609 .await
21610 .unwrap();
21611 drop(editor_2);
21612 pane_2.update(cx, |pane, cx| {
21613 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21614 open_editor.update(cx, |editor, cx| {
21615 assert_eq!(
21616 editor.display_text(cx),
21617 lib_text,
21618 "Other file should be open and active in another panel too",
21619 );
21620 });
21621 assert_eq!(
21622 pane.items().count(),
21623 1,
21624 "No other editors should be open in another pane",
21625 );
21626 });
21627
21628 let _editor_1_reopened = workspace
21629 .update_in(cx, |workspace, window, cx| {
21630 workspace.open_path(
21631 (worktree_id, "main.rs"),
21632 Some(pane_1.downgrade()),
21633 true,
21634 window,
21635 cx,
21636 )
21637 })
21638 .unwrap()
21639 .await
21640 .downcast::<Editor>()
21641 .unwrap();
21642 let _editor_2_reopened = workspace
21643 .update_in(cx, |workspace, window, cx| {
21644 workspace.open_path(
21645 (worktree_id, "main.rs"),
21646 Some(pane_2.downgrade()),
21647 true,
21648 window,
21649 cx,
21650 )
21651 })
21652 .unwrap()
21653 .await
21654 .downcast::<Editor>()
21655 .unwrap();
21656 pane_1.update(cx, |pane, cx| {
21657 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21658 open_editor.update(cx, |editor, cx| {
21659 assert_eq!(
21660 editor.display_text(cx),
21661 main_text,
21662 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
21663 );
21664 assert_eq!(
21665 editor
21666 .selections
21667 .all::<Point>(cx)
21668 .into_iter()
21669 .map(|s| s.range())
21670 .collect::<Vec<_>>(),
21671 expected_ranges,
21672 "Previous editor in the 1st panel had selections and should get them restored on reopen",
21673 );
21674 })
21675 });
21676 pane_2.update(cx, |pane, cx| {
21677 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21678 open_editor.update(cx, |editor, cx| {
21679 assert_eq!(
21680 editor.display_text(cx),
21681 r#"fn main() {
21682⋯rintln!("1");
21683⋯intln!("2");
21684⋯ntln!("3");
21685println!("4");
21686println!("5");
21687}"#,
21688 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
21689 );
21690 assert_eq!(
21691 editor
21692 .selections
21693 .all::<Point>(cx)
21694 .into_iter()
21695 .map(|s| s.range())
21696 .collect::<Vec<_>>(),
21697 vec![Point::zero()..Point::zero()],
21698 "Previous editor in the 2nd pane had no selections changed hence should restore none",
21699 );
21700 })
21701 });
21702}
21703
21704#[gpui::test]
21705async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
21706 init_test(cx, |_| {});
21707
21708 let fs = FakeFs::new(cx.executor());
21709 let main_text = r#"fn main() {
21710println!("1");
21711println!("2");
21712println!("3");
21713println!("4");
21714println!("5");
21715}"#;
21716 let lib_text = "mod foo {}";
21717 fs.insert_tree(
21718 path!("/a"),
21719 json!({
21720 "lib.rs": lib_text,
21721 "main.rs": main_text,
21722 }),
21723 )
21724 .await;
21725
21726 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21727 let (workspace, cx) =
21728 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21729 let worktree_id = workspace.update(cx, |workspace, cx| {
21730 workspace.project().update(cx, |project, cx| {
21731 project.worktrees(cx).next().unwrap().read(cx).id()
21732 })
21733 });
21734
21735 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21736 let editor = workspace
21737 .update_in(cx, |workspace, window, cx| {
21738 workspace.open_path(
21739 (worktree_id, "main.rs"),
21740 Some(pane.downgrade()),
21741 true,
21742 window,
21743 cx,
21744 )
21745 })
21746 .unwrap()
21747 .await
21748 .downcast::<Editor>()
21749 .unwrap();
21750 pane.update(cx, |pane, cx| {
21751 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21752 open_editor.update(cx, |editor, cx| {
21753 assert_eq!(
21754 editor.display_text(cx),
21755 main_text,
21756 "Original main.rs text on initial open",
21757 );
21758 })
21759 });
21760 editor.update_in(cx, |editor, window, cx| {
21761 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
21762 });
21763
21764 cx.update_global(|store: &mut SettingsStore, cx| {
21765 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21766 s.restore_on_file_reopen = Some(false);
21767 });
21768 });
21769 editor.update_in(cx, |editor, window, cx| {
21770 editor.fold_ranges(
21771 vec![
21772 Point::new(1, 0)..Point::new(1, 1),
21773 Point::new(2, 0)..Point::new(2, 2),
21774 Point::new(3, 0)..Point::new(3, 3),
21775 ],
21776 false,
21777 window,
21778 cx,
21779 );
21780 });
21781 pane.update_in(cx, |pane, window, cx| {
21782 pane.close_all_items(&CloseAllItems::default(), window, cx)
21783 })
21784 .await
21785 .unwrap();
21786 pane.update(cx, |pane, _| {
21787 assert!(pane.active_item().is_none());
21788 });
21789 cx.update_global(|store: &mut SettingsStore, cx| {
21790 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21791 s.restore_on_file_reopen = Some(true);
21792 });
21793 });
21794
21795 let _editor_reopened = workspace
21796 .update_in(cx, |workspace, window, cx| {
21797 workspace.open_path(
21798 (worktree_id, "main.rs"),
21799 Some(pane.downgrade()),
21800 true,
21801 window,
21802 cx,
21803 )
21804 })
21805 .unwrap()
21806 .await
21807 .downcast::<Editor>()
21808 .unwrap();
21809 pane.update(cx, |pane, cx| {
21810 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21811 open_editor.update(cx, |editor, cx| {
21812 assert_eq!(
21813 editor.display_text(cx),
21814 main_text,
21815 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
21816 );
21817 })
21818 });
21819}
21820
21821#[gpui::test]
21822async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
21823 struct EmptyModalView {
21824 focus_handle: gpui::FocusHandle,
21825 }
21826 impl EventEmitter<DismissEvent> for EmptyModalView {}
21827 impl Render for EmptyModalView {
21828 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
21829 div()
21830 }
21831 }
21832 impl Focusable for EmptyModalView {
21833 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
21834 self.focus_handle.clone()
21835 }
21836 }
21837 impl workspace::ModalView for EmptyModalView {}
21838 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
21839 EmptyModalView {
21840 focus_handle: cx.focus_handle(),
21841 }
21842 }
21843
21844 init_test(cx, |_| {});
21845
21846 let fs = FakeFs::new(cx.executor());
21847 let project = Project::test(fs, [], cx).await;
21848 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21849 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
21850 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21851 let editor = cx.new_window_entity(|window, cx| {
21852 Editor::new(
21853 EditorMode::full(),
21854 buffer,
21855 Some(project.clone()),
21856 window,
21857 cx,
21858 )
21859 });
21860 workspace
21861 .update(cx, |workspace, window, cx| {
21862 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
21863 })
21864 .unwrap();
21865 editor.update_in(cx, |editor, window, cx| {
21866 editor.open_context_menu(&OpenContextMenu, window, cx);
21867 assert!(editor.mouse_context_menu.is_some());
21868 });
21869 workspace
21870 .update(cx, |workspace, window, cx| {
21871 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
21872 })
21873 .unwrap();
21874 cx.read(|cx| {
21875 assert!(editor.read(cx).mouse_context_menu.is_none());
21876 });
21877}
21878
21879#[gpui::test]
21880async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
21881 init_test(cx, |_| {});
21882
21883 let fs = FakeFs::new(cx.executor());
21884 fs.insert_file(path!("/file.html"), Default::default())
21885 .await;
21886
21887 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
21888
21889 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21890 let html_language = Arc::new(Language::new(
21891 LanguageConfig {
21892 name: "HTML".into(),
21893 matcher: LanguageMatcher {
21894 path_suffixes: vec!["html".to_string()],
21895 ..LanguageMatcher::default()
21896 },
21897 brackets: BracketPairConfig {
21898 pairs: vec![BracketPair {
21899 start: "<".into(),
21900 end: ">".into(),
21901 close: true,
21902 ..Default::default()
21903 }],
21904 ..Default::default()
21905 },
21906 ..Default::default()
21907 },
21908 Some(tree_sitter_html::LANGUAGE.into()),
21909 ));
21910 language_registry.add(html_language);
21911 let mut fake_servers = language_registry.register_fake_lsp(
21912 "HTML",
21913 FakeLspAdapter {
21914 capabilities: lsp::ServerCapabilities {
21915 completion_provider: Some(lsp::CompletionOptions {
21916 resolve_provider: Some(true),
21917 ..Default::default()
21918 }),
21919 ..Default::default()
21920 },
21921 ..Default::default()
21922 },
21923 );
21924
21925 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21926 let cx = &mut VisualTestContext::from_window(*workspace, cx);
21927
21928 let worktree_id = workspace
21929 .update(cx, |workspace, _window, cx| {
21930 workspace.project().update(cx, |project, cx| {
21931 project.worktrees(cx).next().unwrap().read(cx).id()
21932 })
21933 })
21934 .unwrap();
21935 project
21936 .update(cx, |project, cx| {
21937 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
21938 })
21939 .await
21940 .unwrap();
21941 let editor = workspace
21942 .update(cx, |workspace, window, cx| {
21943 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
21944 })
21945 .unwrap()
21946 .await
21947 .unwrap()
21948 .downcast::<Editor>()
21949 .unwrap();
21950
21951 let fake_server = fake_servers.next().await.unwrap();
21952 editor.update_in(cx, |editor, window, cx| {
21953 editor.set_text("<ad></ad>", window, cx);
21954 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21955 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
21956 });
21957 let Some((buffer, _)) = editor
21958 .buffer
21959 .read(cx)
21960 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
21961 else {
21962 panic!("Failed to get buffer for selection position");
21963 };
21964 let buffer = buffer.read(cx);
21965 let buffer_id = buffer.remote_id();
21966 let opening_range =
21967 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
21968 let closing_range =
21969 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
21970 let mut linked_ranges = HashMap::default();
21971 linked_ranges.insert(
21972 buffer_id,
21973 vec![(opening_range.clone(), vec![closing_range.clone()])],
21974 );
21975 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
21976 });
21977 let mut completion_handle =
21978 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
21979 Ok(Some(lsp::CompletionResponse::Array(vec![
21980 lsp::CompletionItem {
21981 label: "head".to_string(),
21982 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21983 lsp::InsertReplaceEdit {
21984 new_text: "head".to_string(),
21985 insert: lsp::Range::new(
21986 lsp::Position::new(0, 1),
21987 lsp::Position::new(0, 3),
21988 ),
21989 replace: lsp::Range::new(
21990 lsp::Position::new(0, 1),
21991 lsp::Position::new(0, 3),
21992 ),
21993 },
21994 )),
21995 ..Default::default()
21996 },
21997 ])))
21998 });
21999 editor.update_in(cx, |editor, window, cx| {
22000 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
22001 });
22002 cx.run_until_parked();
22003 completion_handle.next().await.unwrap();
22004 editor.update(cx, |editor, _| {
22005 assert!(
22006 editor.context_menu_visible(),
22007 "Completion menu should be visible"
22008 );
22009 });
22010 editor.update_in(cx, |editor, window, cx| {
22011 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
22012 });
22013 cx.executor().run_until_parked();
22014 editor.update(cx, |editor, cx| {
22015 assert_eq!(editor.text(cx), "<head></head>");
22016 });
22017}
22018
22019#[gpui::test]
22020async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
22021 init_test(cx, |_| {});
22022
22023 let fs = FakeFs::new(cx.executor());
22024 fs.insert_tree(
22025 path!("/root"),
22026 json!({
22027 "a": {
22028 "main.rs": "fn main() {}",
22029 },
22030 "foo": {
22031 "bar": {
22032 "external_file.rs": "pub mod external {}",
22033 }
22034 }
22035 }),
22036 )
22037 .await;
22038
22039 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
22040 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22041 language_registry.add(rust_lang());
22042 let _fake_servers = language_registry.register_fake_lsp(
22043 "Rust",
22044 FakeLspAdapter {
22045 ..FakeLspAdapter::default()
22046 },
22047 );
22048 let (workspace, cx) =
22049 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22050 let worktree_id = workspace.update(cx, |workspace, cx| {
22051 workspace.project().update(cx, |project, cx| {
22052 project.worktrees(cx).next().unwrap().read(cx).id()
22053 })
22054 });
22055
22056 let assert_language_servers_count =
22057 |expected: usize, context: &str, cx: &mut VisualTestContext| {
22058 project.update(cx, |project, cx| {
22059 let current = project
22060 .lsp_store()
22061 .read(cx)
22062 .as_local()
22063 .unwrap()
22064 .language_servers
22065 .len();
22066 assert_eq!(expected, current, "{context}");
22067 });
22068 };
22069
22070 assert_language_servers_count(
22071 0,
22072 "No servers should be running before any file is open",
22073 cx,
22074 );
22075 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22076 let main_editor = workspace
22077 .update_in(cx, |workspace, window, cx| {
22078 workspace.open_path(
22079 (worktree_id, "main.rs"),
22080 Some(pane.downgrade()),
22081 true,
22082 window,
22083 cx,
22084 )
22085 })
22086 .unwrap()
22087 .await
22088 .downcast::<Editor>()
22089 .unwrap();
22090 pane.update(cx, |pane, cx| {
22091 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22092 open_editor.update(cx, |editor, cx| {
22093 assert_eq!(
22094 editor.display_text(cx),
22095 "fn main() {}",
22096 "Original main.rs text on initial open",
22097 );
22098 });
22099 assert_eq!(open_editor, main_editor);
22100 });
22101 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
22102
22103 let external_editor = workspace
22104 .update_in(cx, |workspace, window, cx| {
22105 workspace.open_abs_path(
22106 PathBuf::from("/root/foo/bar/external_file.rs"),
22107 OpenOptions::default(),
22108 window,
22109 cx,
22110 )
22111 })
22112 .await
22113 .expect("opening external file")
22114 .downcast::<Editor>()
22115 .expect("downcasted external file's open element to editor");
22116 pane.update(cx, |pane, cx| {
22117 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22118 open_editor.update(cx, |editor, cx| {
22119 assert_eq!(
22120 editor.display_text(cx),
22121 "pub mod external {}",
22122 "External file is open now",
22123 );
22124 });
22125 assert_eq!(open_editor, external_editor);
22126 });
22127 assert_language_servers_count(
22128 1,
22129 "Second, external, *.rs file should join the existing server",
22130 cx,
22131 );
22132
22133 pane.update_in(cx, |pane, window, cx| {
22134 pane.close_active_item(&CloseActiveItem::default(), window, cx)
22135 })
22136 .await
22137 .unwrap();
22138 pane.update_in(cx, |pane, window, cx| {
22139 pane.navigate_backward(window, cx);
22140 });
22141 cx.run_until_parked();
22142 pane.update(cx, |pane, cx| {
22143 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22144 open_editor.update(cx, |editor, cx| {
22145 assert_eq!(
22146 editor.display_text(cx),
22147 "pub mod external {}",
22148 "External file is open now",
22149 );
22150 });
22151 });
22152 assert_language_servers_count(
22153 1,
22154 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
22155 cx,
22156 );
22157
22158 cx.update(|_, cx| {
22159 workspace::reload(&workspace::Reload::default(), cx);
22160 });
22161 assert_language_servers_count(
22162 1,
22163 "After reloading the worktree with local and external files opened, only one project should be started",
22164 cx,
22165 );
22166}
22167
22168#[gpui::test]
22169async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
22170 init_test(cx, |_| {});
22171
22172 let mut cx = EditorTestContext::new(cx).await;
22173 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22174 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22175
22176 // test cursor move to start of each line on tab
22177 // for `if`, `elif`, `else`, `while`, `with` and `for`
22178 cx.set_state(indoc! {"
22179 def main():
22180 ˇ for item in items:
22181 ˇ while item.active:
22182 ˇ if item.value > 10:
22183 ˇ continue
22184 ˇ elif item.value < 0:
22185 ˇ break
22186 ˇ else:
22187 ˇ with item.context() as ctx:
22188 ˇ yield count
22189 ˇ else:
22190 ˇ log('while else')
22191 ˇ else:
22192 ˇ log('for else')
22193 "});
22194 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22195 cx.assert_editor_state(indoc! {"
22196 def main():
22197 ˇfor item in items:
22198 ˇwhile item.active:
22199 ˇif item.value > 10:
22200 ˇcontinue
22201 ˇelif item.value < 0:
22202 ˇbreak
22203 ˇelse:
22204 ˇwith item.context() as ctx:
22205 ˇyield count
22206 ˇelse:
22207 ˇlog('while else')
22208 ˇelse:
22209 ˇlog('for else')
22210 "});
22211 // test relative indent is preserved when tab
22212 // for `if`, `elif`, `else`, `while`, `with` and `for`
22213 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22214 cx.assert_editor_state(indoc! {"
22215 def main():
22216 ˇfor item in items:
22217 ˇwhile item.active:
22218 ˇif item.value > 10:
22219 ˇcontinue
22220 ˇelif item.value < 0:
22221 ˇbreak
22222 ˇelse:
22223 ˇwith item.context() as ctx:
22224 ˇyield count
22225 ˇelse:
22226 ˇlog('while else')
22227 ˇelse:
22228 ˇlog('for else')
22229 "});
22230
22231 // test cursor move to start of each line on tab
22232 // for `try`, `except`, `else`, `finally`, `match` and `def`
22233 cx.set_state(indoc! {"
22234 def main():
22235 ˇ try:
22236 ˇ fetch()
22237 ˇ except ValueError:
22238 ˇ handle_error()
22239 ˇ else:
22240 ˇ match value:
22241 ˇ case _:
22242 ˇ finally:
22243 ˇ def status():
22244 ˇ return 0
22245 "});
22246 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22247 cx.assert_editor_state(indoc! {"
22248 def main():
22249 ˇtry:
22250 ˇfetch()
22251 ˇexcept ValueError:
22252 ˇhandle_error()
22253 ˇelse:
22254 ˇmatch value:
22255 ˇcase _:
22256 ˇfinally:
22257 ˇdef status():
22258 ˇreturn 0
22259 "});
22260 // test relative indent is preserved when tab
22261 // for `try`, `except`, `else`, `finally`, `match` and `def`
22262 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22263 cx.assert_editor_state(indoc! {"
22264 def main():
22265 ˇtry:
22266 ˇfetch()
22267 ˇexcept ValueError:
22268 ˇhandle_error()
22269 ˇelse:
22270 ˇmatch value:
22271 ˇcase _:
22272 ˇfinally:
22273 ˇdef status():
22274 ˇreturn 0
22275 "});
22276}
22277
22278#[gpui::test]
22279async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
22280 init_test(cx, |_| {});
22281
22282 let mut cx = EditorTestContext::new(cx).await;
22283 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22284 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22285
22286 // test `else` auto outdents when typed inside `if` block
22287 cx.set_state(indoc! {"
22288 def main():
22289 if i == 2:
22290 return
22291 ˇ
22292 "});
22293 cx.update_editor(|editor, window, cx| {
22294 editor.handle_input("else:", window, cx);
22295 });
22296 cx.assert_editor_state(indoc! {"
22297 def main():
22298 if i == 2:
22299 return
22300 else:ˇ
22301 "});
22302
22303 // test `except` auto outdents when typed inside `try` block
22304 cx.set_state(indoc! {"
22305 def main():
22306 try:
22307 i = 2
22308 ˇ
22309 "});
22310 cx.update_editor(|editor, window, cx| {
22311 editor.handle_input("except:", window, cx);
22312 });
22313 cx.assert_editor_state(indoc! {"
22314 def main():
22315 try:
22316 i = 2
22317 except:ˇ
22318 "});
22319
22320 // test `else` auto outdents when typed inside `except` block
22321 cx.set_state(indoc! {"
22322 def main():
22323 try:
22324 i = 2
22325 except:
22326 j = 2
22327 ˇ
22328 "});
22329 cx.update_editor(|editor, window, cx| {
22330 editor.handle_input("else:", window, cx);
22331 });
22332 cx.assert_editor_state(indoc! {"
22333 def main():
22334 try:
22335 i = 2
22336 except:
22337 j = 2
22338 else:ˇ
22339 "});
22340
22341 // test `finally` auto outdents when typed inside `else` block
22342 cx.set_state(indoc! {"
22343 def main():
22344 try:
22345 i = 2
22346 except:
22347 j = 2
22348 else:
22349 k = 2
22350 ˇ
22351 "});
22352 cx.update_editor(|editor, window, cx| {
22353 editor.handle_input("finally:", window, cx);
22354 });
22355 cx.assert_editor_state(indoc! {"
22356 def main():
22357 try:
22358 i = 2
22359 except:
22360 j = 2
22361 else:
22362 k = 2
22363 finally:ˇ
22364 "});
22365
22366 // test `else` does not outdents when typed inside `except` block right after for block
22367 cx.set_state(indoc! {"
22368 def main():
22369 try:
22370 i = 2
22371 except:
22372 for i in range(n):
22373 pass
22374 ˇ
22375 "});
22376 cx.update_editor(|editor, window, cx| {
22377 editor.handle_input("else:", window, cx);
22378 });
22379 cx.assert_editor_state(indoc! {"
22380 def main():
22381 try:
22382 i = 2
22383 except:
22384 for i in range(n):
22385 pass
22386 else:ˇ
22387 "});
22388
22389 // test `finally` auto outdents when typed inside `else` block right after for block
22390 cx.set_state(indoc! {"
22391 def main():
22392 try:
22393 i = 2
22394 except:
22395 j = 2
22396 else:
22397 for i in range(n):
22398 pass
22399 ˇ
22400 "});
22401 cx.update_editor(|editor, window, cx| {
22402 editor.handle_input("finally:", window, cx);
22403 });
22404 cx.assert_editor_state(indoc! {"
22405 def main():
22406 try:
22407 i = 2
22408 except:
22409 j = 2
22410 else:
22411 for i in range(n):
22412 pass
22413 finally:ˇ
22414 "});
22415
22416 // test `except` outdents to inner "try" block
22417 cx.set_state(indoc! {"
22418 def main():
22419 try:
22420 i = 2
22421 if i == 2:
22422 try:
22423 i = 3
22424 ˇ
22425 "});
22426 cx.update_editor(|editor, window, cx| {
22427 editor.handle_input("except:", window, cx);
22428 });
22429 cx.assert_editor_state(indoc! {"
22430 def main():
22431 try:
22432 i = 2
22433 if i == 2:
22434 try:
22435 i = 3
22436 except:ˇ
22437 "});
22438
22439 // test `except` outdents to outer "try" block
22440 cx.set_state(indoc! {"
22441 def main():
22442 try:
22443 i = 2
22444 if i == 2:
22445 try:
22446 i = 3
22447 ˇ
22448 "});
22449 cx.update_editor(|editor, window, cx| {
22450 editor.handle_input("except:", window, cx);
22451 });
22452 cx.assert_editor_state(indoc! {"
22453 def main():
22454 try:
22455 i = 2
22456 if i == 2:
22457 try:
22458 i = 3
22459 except:ˇ
22460 "});
22461
22462 // test `else` stays at correct indent when typed after `for` block
22463 cx.set_state(indoc! {"
22464 def main():
22465 for i in range(10):
22466 if i == 3:
22467 break
22468 ˇ
22469 "});
22470 cx.update_editor(|editor, window, cx| {
22471 editor.handle_input("else:", window, cx);
22472 });
22473 cx.assert_editor_state(indoc! {"
22474 def main():
22475 for i in range(10):
22476 if i == 3:
22477 break
22478 else:ˇ
22479 "});
22480
22481 // test does not outdent on typing after line with square brackets
22482 cx.set_state(indoc! {"
22483 def f() -> list[str]:
22484 ˇ
22485 "});
22486 cx.update_editor(|editor, window, cx| {
22487 editor.handle_input("a", window, cx);
22488 });
22489 cx.assert_editor_state(indoc! {"
22490 def f() -> list[str]:
22491 aˇ
22492 "});
22493
22494 // test does not outdent on typing : after case keyword
22495 cx.set_state(indoc! {"
22496 match 1:
22497 caseˇ
22498 "});
22499 cx.update_editor(|editor, window, cx| {
22500 editor.handle_input(":", window, cx);
22501 });
22502 cx.assert_editor_state(indoc! {"
22503 match 1:
22504 case:ˇ
22505 "});
22506}
22507
22508#[gpui::test]
22509async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
22510 init_test(cx, |_| {});
22511 update_test_language_settings(cx, |settings| {
22512 settings.defaults.extend_comment_on_newline = Some(false);
22513 });
22514 let mut cx = EditorTestContext::new(cx).await;
22515 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22516 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22517
22518 // test correct indent after newline on comment
22519 cx.set_state(indoc! {"
22520 # COMMENT:ˇ
22521 "});
22522 cx.update_editor(|editor, window, cx| {
22523 editor.newline(&Newline, window, cx);
22524 });
22525 cx.assert_editor_state(indoc! {"
22526 # COMMENT:
22527 ˇ
22528 "});
22529
22530 // test correct indent after newline in brackets
22531 cx.set_state(indoc! {"
22532 {ˇ}
22533 "});
22534 cx.update_editor(|editor, window, cx| {
22535 editor.newline(&Newline, window, cx);
22536 });
22537 cx.run_until_parked();
22538 cx.assert_editor_state(indoc! {"
22539 {
22540 ˇ
22541 }
22542 "});
22543
22544 cx.set_state(indoc! {"
22545 (ˇ)
22546 "});
22547 cx.update_editor(|editor, window, cx| {
22548 editor.newline(&Newline, window, cx);
22549 });
22550 cx.run_until_parked();
22551 cx.assert_editor_state(indoc! {"
22552 (
22553 ˇ
22554 )
22555 "});
22556
22557 // do not indent after empty lists or dictionaries
22558 cx.set_state(indoc! {"
22559 a = []ˇ
22560 "});
22561 cx.update_editor(|editor, window, cx| {
22562 editor.newline(&Newline, window, cx);
22563 });
22564 cx.run_until_parked();
22565 cx.assert_editor_state(indoc! {"
22566 a = []
22567 ˇ
22568 "});
22569}
22570
22571fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
22572 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
22573 point..point
22574}
22575
22576#[track_caller]
22577fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
22578 let (text, ranges) = marked_text_ranges(marked_text, true);
22579 assert_eq!(editor.text(cx), text);
22580 assert_eq!(
22581 editor.selections.ranges(cx),
22582 ranges,
22583 "Assert selections are {}",
22584 marked_text
22585 );
22586}
22587
22588pub fn handle_signature_help_request(
22589 cx: &mut EditorLspTestContext,
22590 mocked_response: lsp::SignatureHelp,
22591) -> impl Future<Output = ()> + use<> {
22592 let mut request =
22593 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
22594 let mocked_response = mocked_response.clone();
22595 async move { Ok(Some(mocked_response)) }
22596 });
22597
22598 async move {
22599 request.next().await;
22600 }
22601}
22602
22603#[track_caller]
22604pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
22605 cx.update_editor(|editor, _, _| {
22606 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
22607 let entries = menu.entries.borrow();
22608 let entries = entries
22609 .iter()
22610 .map(|entry| entry.string.as_str())
22611 .collect::<Vec<_>>();
22612 assert_eq!(entries, expected);
22613 } else {
22614 panic!("Expected completions menu");
22615 }
22616 });
22617}
22618
22619/// Handle completion request passing a marked string specifying where the completion
22620/// should be triggered from using '|' character, what range should be replaced, and what completions
22621/// should be returned using '<' and '>' to delimit the range.
22622///
22623/// Also see `handle_completion_request_with_insert_and_replace`.
22624#[track_caller]
22625pub fn handle_completion_request(
22626 marked_string: &str,
22627 completions: Vec<&'static str>,
22628 is_incomplete: bool,
22629 counter: Arc<AtomicUsize>,
22630 cx: &mut EditorLspTestContext,
22631) -> impl Future<Output = ()> {
22632 let complete_from_marker: TextRangeMarker = '|'.into();
22633 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22634 let (_, mut marked_ranges) = marked_text_ranges_by(
22635 marked_string,
22636 vec![complete_from_marker.clone(), replace_range_marker.clone()],
22637 );
22638
22639 let complete_from_position =
22640 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22641 let replace_range =
22642 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22643
22644 let mut request =
22645 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22646 let completions = completions.clone();
22647 counter.fetch_add(1, atomic::Ordering::Release);
22648 async move {
22649 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22650 assert_eq!(
22651 params.text_document_position.position,
22652 complete_from_position
22653 );
22654 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
22655 is_incomplete: is_incomplete,
22656 item_defaults: None,
22657 items: completions
22658 .iter()
22659 .map(|completion_text| lsp::CompletionItem {
22660 label: completion_text.to_string(),
22661 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
22662 range: replace_range,
22663 new_text: completion_text.to_string(),
22664 })),
22665 ..Default::default()
22666 })
22667 .collect(),
22668 })))
22669 }
22670 });
22671
22672 async move {
22673 request.next().await;
22674 }
22675}
22676
22677/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
22678/// given instead, which also contains an `insert` range.
22679///
22680/// This function uses markers to define ranges:
22681/// - `|` marks the cursor position
22682/// - `<>` marks the replace range
22683/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
22684pub fn handle_completion_request_with_insert_and_replace(
22685 cx: &mut EditorLspTestContext,
22686 marked_string: &str,
22687 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
22688 counter: Arc<AtomicUsize>,
22689) -> impl Future<Output = ()> {
22690 let complete_from_marker: TextRangeMarker = '|'.into();
22691 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22692 let insert_range_marker: TextRangeMarker = ('{', '}').into();
22693
22694 let (_, mut marked_ranges) = marked_text_ranges_by(
22695 marked_string,
22696 vec![
22697 complete_from_marker.clone(),
22698 replace_range_marker.clone(),
22699 insert_range_marker.clone(),
22700 ],
22701 );
22702
22703 let complete_from_position =
22704 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22705 let replace_range =
22706 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22707
22708 let insert_range = match marked_ranges.remove(&insert_range_marker) {
22709 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
22710 _ => lsp::Range {
22711 start: replace_range.start,
22712 end: complete_from_position,
22713 },
22714 };
22715
22716 let mut request =
22717 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22718 let completions = completions.clone();
22719 counter.fetch_add(1, atomic::Ordering::Release);
22720 async move {
22721 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22722 assert_eq!(
22723 params.text_document_position.position, complete_from_position,
22724 "marker `|` position doesn't match",
22725 );
22726 Ok(Some(lsp::CompletionResponse::Array(
22727 completions
22728 .iter()
22729 .map(|(label, new_text)| lsp::CompletionItem {
22730 label: label.to_string(),
22731 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22732 lsp::InsertReplaceEdit {
22733 insert: insert_range,
22734 replace: replace_range,
22735 new_text: new_text.to_string(),
22736 },
22737 )),
22738 ..Default::default()
22739 })
22740 .collect(),
22741 )))
22742 }
22743 });
22744
22745 async move {
22746 request.next().await;
22747 }
22748}
22749
22750fn handle_resolve_completion_request(
22751 cx: &mut EditorLspTestContext,
22752 edits: Option<Vec<(&'static str, &'static str)>>,
22753) -> impl Future<Output = ()> {
22754 let edits = edits.map(|edits| {
22755 edits
22756 .iter()
22757 .map(|(marked_string, new_text)| {
22758 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
22759 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
22760 lsp::TextEdit::new(replace_range, new_text.to_string())
22761 })
22762 .collect::<Vec<_>>()
22763 });
22764
22765 let mut request =
22766 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
22767 let edits = edits.clone();
22768 async move {
22769 Ok(lsp::CompletionItem {
22770 additional_text_edits: edits,
22771 ..Default::default()
22772 })
22773 }
22774 });
22775
22776 async move {
22777 request.next().await;
22778 }
22779}
22780
22781pub(crate) fn update_test_language_settings(
22782 cx: &mut TestAppContext,
22783 f: impl Fn(&mut AllLanguageSettingsContent),
22784) {
22785 cx.update(|cx| {
22786 SettingsStore::update_global(cx, |store, cx| {
22787 store.update_user_settings::<AllLanguageSettings>(cx, f);
22788 });
22789 });
22790}
22791
22792pub(crate) fn update_test_project_settings(
22793 cx: &mut TestAppContext,
22794 f: impl Fn(&mut ProjectSettings),
22795) {
22796 cx.update(|cx| {
22797 SettingsStore::update_global(cx, |store, cx| {
22798 store.update_user_settings::<ProjectSettings>(cx, f);
22799 });
22800 });
22801}
22802
22803pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
22804 cx.update(|cx| {
22805 assets::Assets.load_test_fonts(cx);
22806 let store = SettingsStore::test(cx);
22807 cx.set_global(store);
22808 theme::init(theme::LoadThemes::JustBase, cx);
22809 release_channel::init(SemanticVersion::default(), cx);
22810 client::init_settings(cx);
22811 language::init(cx);
22812 Project::init_settings(cx);
22813 workspace::init_settings(cx);
22814 crate::init(cx);
22815 });
22816 zlog::init_test();
22817 update_test_language_settings(cx, f);
22818}
22819
22820#[track_caller]
22821fn assert_hunk_revert(
22822 not_reverted_text_with_selections: &str,
22823 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
22824 expected_reverted_text_with_selections: &str,
22825 base_text: &str,
22826 cx: &mut EditorLspTestContext,
22827) {
22828 cx.set_state(not_reverted_text_with_selections);
22829 cx.set_head_text(base_text);
22830 cx.executor().run_until_parked();
22831
22832 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
22833 let snapshot = editor.snapshot(window, cx);
22834 let reverted_hunk_statuses = snapshot
22835 .buffer_snapshot
22836 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
22837 .map(|hunk| hunk.status().kind)
22838 .collect::<Vec<_>>();
22839
22840 editor.git_restore(&Default::default(), window, cx);
22841 reverted_hunk_statuses
22842 });
22843 cx.executor().run_until_parked();
22844 cx.assert_editor_state(expected_reverted_text_with_selections);
22845 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
22846}
22847
22848#[gpui::test(iterations = 10)]
22849async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
22850 init_test(cx, |_| {});
22851
22852 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
22853 let counter = diagnostic_requests.clone();
22854
22855 let fs = FakeFs::new(cx.executor());
22856 fs.insert_tree(
22857 path!("/a"),
22858 json!({
22859 "first.rs": "fn main() { let a = 5; }",
22860 "second.rs": "// Test file",
22861 }),
22862 )
22863 .await;
22864
22865 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22866 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22867 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22868
22869 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22870 language_registry.add(rust_lang());
22871 let mut fake_servers = language_registry.register_fake_lsp(
22872 "Rust",
22873 FakeLspAdapter {
22874 capabilities: lsp::ServerCapabilities {
22875 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
22876 lsp::DiagnosticOptions {
22877 identifier: None,
22878 inter_file_dependencies: true,
22879 workspace_diagnostics: true,
22880 work_done_progress_options: Default::default(),
22881 },
22882 )),
22883 ..Default::default()
22884 },
22885 ..Default::default()
22886 },
22887 );
22888
22889 let editor = workspace
22890 .update(cx, |workspace, window, cx| {
22891 workspace.open_abs_path(
22892 PathBuf::from(path!("/a/first.rs")),
22893 OpenOptions::default(),
22894 window,
22895 cx,
22896 )
22897 })
22898 .unwrap()
22899 .await
22900 .unwrap()
22901 .downcast::<Editor>()
22902 .unwrap();
22903 let fake_server = fake_servers.next().await.unwrap();
22904 let server_id = fake_server.server.server_id();
22905 let mut first_request = fake_server
22906 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
22907 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
22908 let result_id = Some(new_result_id.to_string());
22909 assert_eq!(
22910 params.text_document.uri,
22911 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
22912 );
22913 async move {
22914 Ok(lsp::DocumentDiagnosticReportResult::Report(
22915 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
22916 related_documents: None,
22917 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
22918 items: Vec::new(),
22919 result_id,
22920 },
22921 }),
22922 ))
22923 }
22924 });
22925
22926 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
22927 project.update(cx, |project, cx| {
22928 let buffer_id = editor
22929 .read(cx)
22930 .buffer()
22931 .read(cx)
22932 .as_singleton()
22933 .expect("created a singleton buffer")
22934 .read(cx)
22935 .remote_id();
22936 let buffer_result_id = project
22937 .lsp_store()
22938 .read(cx)
22939 .result_id(server_id, buffer_id, cx);
22940 assert_eq!(expected, buffer_result_id);
22941 });
22942 };
22943
22944 ensure_result_id(None, cx);
22945 cx.executor().advance_clock(Duration::from_millis(60));
22946 cx.executor().run_until_parked();
22947 assert_eq!(
22948 diagnostic_requests.load(atomic::Ordering::Acquire),
22949 1,
22950 "Opening file should trigger diagnostic request"
22951 );
22952 first_request
22953 .next()
22954 .await
22955 .expect("should have sent the first diagnostics pull request");
22956 ensure_result_id(Some("1".to_string()), cx);
22957
22958 // Editing should trigger diagnostics
22959 editor.update_in(cx, |editor, window, cx| {
22960 editor.handle_input("2", window, cx)
22961 });
22962 cx.executor().advance_clock(Duration::from_millis(60));
22963 cx.executor().run_until_parked();
22964 assert_eq!(
22965 diagnostic_requests.load(atomic::Ordering::Acquire),
22966 2,
22967 "Editing should trigger diagnostic request"
22968 );
22969 ensure_result_id(Some("2".to_string()), cx);
22970
22971 // Moving cursor should not trigger diagnostic request
22972 editor.update_in(cx, |editor, window, cx| {
22973 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22974 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
22975 });
22976 });
22977 cx.executor().advance_clock(Duration::from_millis(60));
22978 cx.executor().run_until_parked();
22979 assert_eq!(
22980 diagnostic_requests.load(atomic::Ordering::Acquire),
22981 2,
22982 "Cursor movement should not trigger diagnostic request"
22983 );
22984 ensure_result_id(Some("2".to_string()), cx);
22985 // Multiple rapid edits should be debounced
22986 for _ in 0..5 {
22987 editor.update_in(cx, |editor, window, cx| {
22988 editor.handle_input("x", window, cx)
22989 });
22990 }
22991 cx.executor().advance_clock(Duration::from_millis(60));
22992 cx.executor().run_until_parked();
22993
22994 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
22995 assert!(
22996 final_requests <= 4,
22997 "Multiple rapid edits should be debounced (got {final_requests} requests)",
22998 );
22999 ensure_result_id(Some(final_requests.to_string()), cx);
23000}
23001
23002#[gpui::test]
23003async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
23004 // Regression test for issue #11671
23005 // Previously, adding a cursor after moving multiple cursors would reset
23006 // the cursor count instead of adding to the existing cursors.
23007 init_test(cx, |_| {});
23008 let mut cx = EditorTestContext::new(cx).await;
23009
23010 // Create a simple buffer with cursor at start
23011 cx.set_state(indoc! {"
23012 ˇaaaa
23013 bbbb
23014 cccc
23015 dddd
23016 eeee
23017 ffff
23018 gggg
23019 hhhh"});
23020
23021 // Add 2 cursors below (so we have 3 total)
23022 cx.update_editor(|editor, window, cx| {
23023 editor.add_selection_below(&Default::default(), window, cx);
23024 editor.add_selection_below(&Default::default(), window, cx);
23025 });
23026
23027 // Verify we have 3 cursors
23028 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
23029 assert_eq!(
23030 initial_count, 3,
23031 "Should have 3 cursors after adding 2 below"
23032 );
23033
23034 // Move down one line
23035 cx.update_editor(|editor, window, cx| {
23036 editor.move_down(&MoveDown, window, cx);
23037 });
23038
23039 // Add another cursor below
23040 cx.update_editor(|editor, window, cx| {
23041 editor.add_selection_below(&Default::default(), window, cx);
23042 });
23043
23044 // Should now have 4 cursors (3 original + 1 new)
23045 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
23046 assert_eq!(
23047 final_count, 4,
23048 "Should have 4 cursors after moving and adding another"
23049 );
23050}
23051
23052#[gpui::test(iterations = 10)]
23053async fn test_document_colors(cx: &mut TestAppContext) {
23054 let expected_color = Rgba {
23055 r: 0.33,
23056 g: 0.33,
23057 b: 0.33,
23058 a: 0.33,
23059 };
23060
23061 init_test(cx, |_| {});
23062
23063 let fs = FakeFs::new(cx.executor());
23064 fs.insert_tree(
23065 path!("/a"),
23066 json!({
23067 "first.rs": "fn main() { let a = 5; }",
23068 }),
23069 )
23070 .await;
23071
23072 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23073 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23074 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23075
23076 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23077 language_registry.add(rust_lang());
23078 let mut fake_servers = language_registry.register_fake_lsp(
23079 "Rust",
23080 FakeLspAdapter {
23081 capabilities: lsp::ServerCapabilities {
23082 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
23083 ..lsp::ServerCapabilities::default()
23084 },
23085 name: "rust-analyzer",
23086 ..FakeLspAdapter::default()
23087 },
23088 );
23089 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
23090 "Rust",
23091 FakeLspAdapter {
23092 capabilities: lsp::ServerCapabilities {
23093 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
23094 ..lsp::ServerCapabilities::default()
23095 },
23096 name: "not-rust-analyzer",
23097 ..FakeLspAdapter::default()
23098 },
23099 );
23100
23101 let editor = workspace
23102 .update(cx, |workspace, window, cx| {
23103 workspace.open_abs_path(
23104 PathBuf::from(path!("/a/first.rs")),
23105 OpenOptions::default(),
23106 window,
23107 cx,
23108 )
23109 })
23110 .unwrap()
23111 .await
23112 .unwrap()
23113 .downcast::<Editor>()
23114 .unwrap();
23115 let fake_language_server = fake_servers.next().await.unwrap();
23116 let fake_language_server_without_capabilities =
23117 fake_servers_without_capabilities.next().await.unwrap();
23118 let requests_made = Arc::new(AtomicUsize::new(0));
23119 let closure_requests_made = Arc::clone(&requests_made);
23120 let mut color_request_handle = fake_language_server
23121 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
23122 let requests_made = Arc::clone(&closure_requests_made);
23123 async move {
23124 assert_eq!(
23125 params.text_document.uri,
23126 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
23127 );
23128 requests_made.fetch_add(1, atomic::Ordering::Release);
23129 Ok(vec![
23130 lsp::ColorInformation {
23131 range: lsp::Range {
23132 start: lsp::Position {
23133 line: 0,
23134 character: 0,
23135 },
23136 end: lsp::Position {
23137 line: 0,
23138 character: 1,
23139 },
23140 },
23141 color: lsp::Color {
23142 red: 0.33,
23143 green: 0.33,
23144 blue: 0.33,
23145 alpha: 0.33,
23146 },
23147 },
23148 lsp::ColorInformation {
23149 range: lsp::Range {
23150 start: lsp::Position {
23151 line: 0,
23152 character: 0,
23153 },
23154 end: lsp::Position {
23155 line: 0,
23156 character: 1,
23157 },
23158 },
23159 color: lsp::Color {
23160 red: 0.33,
23161 green: 0.33,
23162 blue: 0.33,
23163 alpha: 0.33,
23164 },
23165 },
23166 ])
23167 }
23168 });
23169
23170 let _handle = fake_language_server_without_capabilities
23171 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
23172 panic!("Should not be called");
23173 });
23174 cx.executor().advance_clock(Duration::from_millis(100));
23175 color_request_handle.next().await.unwrap();
23176 cx.run_until_parked();
23177 assert_eq!(
23178 1,
23179 requests_made.load(atomic::Ordering::Acquire),
23180 "Should query for colors once per editor open"
23181 );
23182 editor.update_in(cx, |editor, _, cx| {
23183 assert_eq!(
23184 vec![expected_color],
23185 extract_color_inlays(editor, cx),
23186 "Should have an initial inlay"
23187 );
23188 });
23189
23190 // opening another file in a split should not influence the LSP query counter
23191 workspace
23192 .update(cx, |workspace, window, cx| {
23193 assert_eq!(
23194 workspace.panes().len(),
23195 1,
23196 "Should have one pane with one editor"
23197 );
23198 workspace.move_item_to_pane_in_direction(
23199 &MoveItemToPaneInDirection {
23200 direction: SplitDirection::Right,
23201 focus: false,
23202 clone: true,
23203 },
23204 window,
23205 cx,
23206 );
23207 })
23208 .unwrap();
23209 cx.run_until_parked();
23210 workspace
23211 .update(cx, |workspace, _, cx| {
23212 let panes = workspace.panes();
23213 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
23214 for pane in panes {
23215 let editor = pane
23216 .read(cx)
23217 .active_item()
23218 .and_then(|item| item.downcast::<Editor>())
23219 .expect("Should have opened an editor in each split");
23220 let editor_file = editor
23221 .read(cx)
23222 .buffer()
23223 .read(cx)
23224 .as_singleton()
23225 .expect("test deals with singleton buffers")
23226 .read(cx)
23227 .file()
23228 .expect("test buffese should have a file")
23229 .path();
23230 assert_eq!(
23231 editor_file.as_ref(),
23232 Path::new("first.rs"),
23233 "Both editors should be opened for the same file"
23234 )
23235 }
23236 })
23237 .unwrap();
23238
23239 cx.executor().advance_clock(Duration::from_millis(500));
23240 let save = editor.update_in(cx, |editor, window, cx| {
23241 editor.move_to_end(&MoveToEnd, window, cx);
23242 editor.handle_input("dirty", window, cx);
23243 editor.save(
23244 SaveOptions {
23245 format: true,
23246 autosave: true,
23247 },
23248 project.clone(),
23249 window,
23250 cx,
23251 )
23252 });
23253 save.await.unwrap();
23254
23255 color_request_handle.next().await.unwrap();
23256 cx.run_until_parked();
23257 assert_eq!(
23258 3,
23259 requests_made.load(atomic::Ordering::Acquire),
23260 "Should query for colors once per save and once per formatting after save"
23261 );
23262
23263 drop(editor);
23264 let close = workspace
23265 .update(cx, |workspace, window, cx| {
23266 workspace.active_pane().update(cx, |pane, cx| {
23267 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23268 })
23269 })
23270 .unwrap();
23271 close.await.unwrap();
23272 let close = workspace
23273 .update(cx, |workspace, window, cx| {
23274 workspace.active_pane().update(cx, |pane, cx| {
23275 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23276 })
23277 })
23278 .unwrap();
23279 close.await.unwrap();
23280 assert_eq!(
23281 3,
23282 requests_made.load(atomic::Ordering::Acquire),
23283 "After saving and closing all editors, no extra requests should be made"
23284 );
23285 workspace
23286 .update(cx, |workspace, _, cx| {
23287 assert!(
23288 workspace.active_item(cx).is_none(),
23289 "Should close all editors"
23290 )
23291 })
23292 .unwrap();
23293
23294 workspace
23295 .update(cx, |workspace, window, cx| {
23296 workspace.active_pane().update(cx, |pane, cx| {
23297 pane.navigate_backward(window, cx);
23298 })
23299 })
23300 .unwrap();
23301 cx.executor().advance_clock(Duration::from_millis(100));
23302 cx.run_until_parked();
23303 let editor = workspace
23304 .update(cx, |workspace, _, cx| {
23305 workspace
23306 .active_item(cx)
23307 .expect("Should have reopened the editor again after navigating back")
23308 .downcast::<Editor>()
23309 .expect("Should be an editor")
23310 })
23311 .unwrap();
23312 color_request_handle.next().await.unwrap();
23313 assert_eq!(
23314 3,
23315 requests_made.load(atomic::Ordering::Acquire),
23316 "Cache should be reused on buffer close and reopen"
23317 );
23318 editor.update(cx, |editor, cx| {
23319 assert_eq!(
23320 vec![expected_color],
23321 extract_color_inlays(editor, cx),
23322 "Should have an initial inlay"
23323 );
23324 });
23325}
23326
23327#[gpui::test]
23328async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
23329 init_test(cx, |_| {});
23330 let (editor, cx) = cx.add_window_view(Editor::single_line);
23331 editor.update_in(cx, |editor, window, cx| {
23332 editor.set_text("oops\n\nwow\n", window, cx)
23333 });
23334 cx.run_until_parked();
23335 editor.update(cx, |editor, cx| {
23336 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
23337 });
23338 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
23339 cx.run_until_parked();
23340 editor.update(cx, |editor, cx| {
23341 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
23342 });
23343}
23344
23345#[track_caller]
23346fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
23347 editor
23348 .all_inlays(cx)
23349 .into_iter()
23350 .filter_map(|inlay| inlay.get_color())
23351 .map(Rgba::from)
23352 .collect()
23353}