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_convert_to_sentence_case(cx: &mut TestAppContext) {
4729 init_test(cx, |_| {});
4730
4731 let mut cx = EditorTestContext::new(cx).await;
4732
4733 cx.set_state(indoc! {"
4734 «implement-windows-supportˇ»
4735 "});
4736 cx.update_editor(|e, window, cx| {
4737 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
4738 });
4739 cx.assert_editor_state(indoc! {"
4740 «Implement windows supportˇ»
4741 "});
4742}
4743
4744#[gpui::test]
4745async fn test_manipulate_text(cx: &mut TestAppContext) {
4746 init_test(cx, |_| {});
4747
4748 let mut cx = EditorTestContext::new(cx).await;
4749
4750 // Test convert_to_upper_case()
4751 cx.set_state(indoc! {"
4752 «hello worldˇ»
4753 "});
4754 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4755 cx.assert_editor_state(indoc! {"
4756 «HELLO WORLDˇ»
4757 "});
4758
4759 // Test convert_to_lower_case()
4760 cx.set_state(indoc! {"
4761 «HELLO WORLDˇ»
4762 "});
4763 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4764 cx.assert_editor_state(indoc! {"
4765 «hello worldˇ»
4766 "});
4767
4768 // Test multiple line, single selection case
4769 cx.set_state(indoc! {"
4770 «The quick brown
4771 fox jumps over
4772 the lazy dogˇ»
4773 "});
4774 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4775 cx.assert_editor_state(indoc! {"
4776 «The Quick Brown
4777 Fox Jumps Over
4778 The Lazy Dogˇ»
4779 "});
4780
4781 // Test multiple line, single selection case
4782 cx.set_state(indoc! {"
4783 «The quick brown
4784 fox jumps over
4785 the lazy dogˇ»
4786 "});
4787 cx.update_editor(|e, window, cx| {
4788 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4789 });
4790 cx.assert_editor_state(indoc! {"
4791 «TheQuickBrown
4792 FoxJumpsOver
4793 TheLazyDogˇ»
4794 "});
4795
4796 // From here on out, test more complex cases of manipulate_text()
4797
4798 // Test no selection case - should affect words cursors are in
4799 // Cursor at beginning, middle, and end of word
4800 cx.set_state(indoc! {"
4801 ˇhello big beauˇtiful worldˇ
4802 "});
4803 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4804 cx.assert_editor_state(indoc! {"
4805 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4806 "});
4807
4808 // Test multiple selections on a single line and across multiple lines
4809 cx.set_state(indoc! {"
4810 «Theˇ» quick «brown
4811 foxˇ» jumps «overˇ»
4812 the «lazyˇ» dog
4813 "});
4814 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4815 cx.assert_editor_state(indoc! {"
4816 «THEˇ» quick «BROWN
4817 FOXˇ» jumps «OVERˇ»
4818 the «LAZYˇ» dog
4819 "});
4820
4821 // Test case where text length grows
4822 cx.set_state(indoc! {"
4823 «tschüߡ»
4824 "});
4825 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4826 cx.assert_editor_state(indoc! {"
4827 «TSCHÜSSˇ»
4828 "});
4829
4830 // Test to make sure we don't crash when text shrinks
4831 cx.set_state(indoc! {"
4832 aaa_bbbˇ
4833 "});
4834 cx.update_editor(|e, window, cx| {
4835 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4836 });
4837 cx.assert_editor_state(indoc! {"
4838 «aaaBbbˇ»
4839 "});
4840
4841 // Test to make sure we all aware of the fact that each word can grow and shrink
4842 // Final selections should be aware of this fact
4843 cx.set_state(indoc! {"
4844 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4845 "});
4846 cx.update_editor(|e, window, cx| {
4847 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4848 });
4849 cx.assert_editor_state(indoc! {"
4850 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4851 "});
4852
4853 cx.set_state(indoc! {"
4854 «hElLo, WoRld!ˇ»
4855 "});
4856 cx.update_editor(|e, window, cx| {
4857 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4858 });
4859 cx.assert_editor_state(indoc! {"
4860 «HeLlO, wOrLD!ˇ»
4861 "});
4862}
4863
4864#[gpui::test]
4865fn test_duplicate_line(cx: &mut TestAppContext) {
4866 init_test(cx, |_| {});
4867
4868 let editor = cx.add_window(|window, cx| {
4869 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4870 build_editor(buffer, window, cx)
4871 });
4872 _ = editor.update(cx, |editor, window, cx| {
4873 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4874 s.select_display_ranges([
4875 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4876 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4877 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4878 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4879 ])
4880 });
4881 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4882 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4883 assert_eq!(
4884 editor.selections.display_ranges(cx),
4885 vec![
4886 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4887 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4888 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4889 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4890 ]
4891 );
4892 });
4893
4894 let editor = cx.add_window(|window, cx| {
4895 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4896 build_editor(buffer, window, cx)
4897 });
4898 _ = editor.update(cx, |editor, window, cx| {
4899 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4900 s.select_display_ranges([
4901 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4902 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4903 ])
4904 });
4905 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4906 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4907 assert_eq!(
4908 editor.selections.display_ranges(cx),
4909 vec![
4910 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4911 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4912 ]
4913 );
4914 });
4915
4916 // With `move_upwards` the selections stay in place, except for
4917 // the lines inserted above them
4918 let editor = cx.add_window(|window, cx| {
4919 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4920 build_editor(buffer, window, cx)
4921 });
4922 _ = editor.update(cx, |editor, window, cx| {
4923 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4924 s.select_display_ranges([
4925 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4926 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4927 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4928 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4929 ])
4930 });
4931 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4932 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4933 assert_eq!(
4934 editor.selections.display_ranges(cx),
4935 vec![
4936 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4937 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4938 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4939 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4940 ]
4941 );
4942 });
4943
4944 let editor = cx.add_window(|window, cx| {
4945 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4946 build_editor(buffer, window, cx)
4947 });
4948 _ = editor.update(cx, |editor, window, cx| {
4949 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4950 s.select_display_ranges([
4951 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4952 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4953 ])
4954 });
4955 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4956 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4957 assert_eq!(
4958 editor.selections.display_ranges(cx),
4959 vec![
4960 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4961 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4962 ]
4963 );
4964 });
4965
4966 let editor = cx.add_window(|window, cx| {
4967 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4968 build_editor(buffer, window, cx)
4969 });
4970 _ = editor.update(cx, |editor, window, cx| {
4971 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4972 s.select_display_ranges([
4973 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4974 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4975 ])
4976 });
4977 editor.duplicate_selection(&DuplicateSelection, window, cx);
4978 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4979 assert_eq!(
4980 editor.selections.display_ranges(cx),
4981 vec![
4982 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4983 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4984 ]
4985 );
4986 });
4987}
4988
4989#[gpui::test]
4990fn test_move_line_up_down(cx: &mut TestAppContext) {
4991 init_test(cx, |_| {});
4992
4993 let editor = cx.add_window(|window, cx| {
4994 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4995 build_editor(buffer, window, cx)
4996 });
4997 _ = editor.update(cx, |editor, window, cx| {
4998 editor.fold_creases(
4999 vec![
5000 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5001 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5002 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5003 ],
5004 true,
5005 window,
5006 cx,
5007 );
5008 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5009 s.select_display_ranges([
5010 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5011 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5012 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5013 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5014 ])
5015 });
5016 assert_eq!(
5017 editor.display_text(cx),
5018 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5019 );
5020
5021 editor.move_line_up(&MoveLineUp, window, cx);
5022 assert_eq!(
5023 editor.display_text(cx),
5024 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5025 );
5026 assert_eq!(
5027 editor.selections.display_ranges(cx),
5028 vec![
5029 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5030 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5031 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5032 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 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\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5042 );
5043 assert_eq!(
5044 editor.selections.display_ranges(cx),
5045 vec![
5046 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 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_down(&MoveLineDown, window, cx);
5056 assert_eq!(
5057 editor.display_text(cx),
5058 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5059 );
5060 assert_eq!(
5061 editor.selections.display_ranges(cx),
5062 vec![
5063 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5064 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5065 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5066 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5067 ]
5068 );
5069 });
5070
5071 _ = editor.update(cx, |editor, window, cx| {
5072 editor.move_line_up(&MoveLineUp, window, cx);
5073 assert_eq!(
5074 editor.display_text(cx),
5075 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5076 );
5077 assert_eq!(
5078 editor.selections.display_ranges(cx),
5079 vec![
5080 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5081 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5082 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5083 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5084 ]
5085 );
5086 });
5087}
5088
5089#[gpui::test]
5090fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5091 init_test(cx, |_| {});
5092 let editor = cx.add_window(|window, cx| {
5093 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5094 build_editor(buffer, window, cx)
5095 });
5096 _ = editor.update(cx, |editor, window, cx| {
5097 editor.fold_creases(
5098 vec![Crease::simple(
5099 Point::new(6, 4)..Point::new(7, 4),
5100 FoldPlaceholder::test(),
5101 )],
5102 true,
5103 window,
5104 cx,
5105 );
5106 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5107 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5108 });
5109 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5110 editor.move_line_up(&MoveLineUp, window, cx);
5111 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5112 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5113 });
5114}
5115
5116#[gpui::test]
5117fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5118 init_test(cx, |_| {});
5119
5120 let editor = cx.add_window(|window, cx| {
5121 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5122 build_editor(buffer, window, cx)
5123 });
5124 _ = editor.update(cx, |editor, window, cx| {
5125 let snapshot = editor.buffer.read(cx).snapshot(cx);
5126 editor.insert_blocks(
5127 [BlockProperties {
5128 style: BlockStyle::Fixed,
5129 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5130 height: Some(1),
5131 render: Arc::new(|_| div().into_any()),
5132 priority: 0,
5133 }],
5134 Some(Autoscroll::fit()),
5135 cx,
5136 );
5137 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5138 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5139 });
5140 editor.move_line_down(&MoveLineDown, window, cx);
5141 });
5142}
5143
5144#[gpui::test]
5145async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5146 init_test(cx, |_| {});
5147
5148 let mut cx = EditorTestContext::new(cx).await;
5149 cx.set_state(
5150 &"
5151 ˇzero
5152 one
5153 two
5154 three
5155 four
5156 five
5157 "
5158 .unindent(),
5159 );
5160
5161 // Create a four-line block that replaces three lines of text.
5162 cx.update_editor(|editor, window, cx| {
5163 let snapshot = editor.snapshot(window, cx);
5164 let snapshot = &snapshot.buffer_snapshot;
5165 let placement = BlockPlacement::Replace(
5166 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5167 );
5168 editor.insert_blocks(
5169 [BlockProperties {
5170 placement,
5171 height: Some(4),
5172 style: BlockStyle::Sticky,
5173 render: Arc::new(|_| gpui::div().into_any_element()),
5174 priority: 0,
5175 }],
5176 None,
5177 cx,
5178 );
5179 });
5180
5181 // Move down so that the cursor touches the block.
5182 cx.update_editor(|editor, window, cx| {
5183 editor.move_down(&Default::default(), window, cx);
5184 });
5185 cx.assert_editor_state(
5186 &"
5187 zero
5188 «one
5189 two
5190 threeˇ»
5191 four
5192 five
5193 "
5194 .unindent(),
5195 );
5196
5197 // Move down past the block.
5198 cx.update_editor(|editor, window, cx| {
5199 editor.move_down(&Default::default(), window, cx);
5200 });
5201 cx.assert_editor_state(
5202 &"
5203 zero
5204 one
5205 two
5206 three
5207 ˇfour
5208 five
5209 "
5210 .unindent(),
5211 );
5212}
5213
5214#[gpui::test]
5215fn test_transpose(cx: &mut TestAppContext) {
5216 init_test(cx, |_| {});
5217
5218 _ = cx.add_window(|window, cx| {
5219 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5220 editor.set_style(EditorStyle::default(), window, cx);
5221 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5222 s.select_ranges([1..1])
5223 });
5224 editor.transpose(&Default::default(), window, cx);
5225 assert_eq!(editor.text(cx), "bac");
5226 assert_eq!(editor.selections.ranges(cx), [2..2]);
5227
5228 editor.transpose(&Default::default(), window, cx);
5229 assert_eq!(editor.text(cx), "bca");
5230 assert_eq!(editor.selections.ranges(cx), [3..3]);
5231
5232 editor.transpose(&Default::default(), window, cx);
5233 assert_eq!(editor.text(cx), "bac");
5234 assert_eq!(editor.selections.ranges(cx), [3..3]);
5235
5236 editor
5237 });
5238
5239 _ = cx.add_window(|window, cx| {
5240 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5241 editor.set_style(EditorStyle::default(), window, cx);
5242 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5243 s.select_ranges([3..3])
5244 });
5245 editor.transpose(&Default::default(), window, cx);
5246 assert_eq!(editor.text(cx), "acb\nde");
5247 assert_eq!(editor.selections.ranges(cx), [3..3]);
5248
5249 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5250 s.select_ranges([4..4])
5251 });
5252 editor.transpose(&Default::default(), window, cx);
5253 assert_eq!(editor.text(cx), "acbd\ne");
5254 assert_eq!(editor.selections.ranges(cx), [5..5]);
5255
5256 editor.transpose(&Default::default(), window, cx);
5257 assert_eq!(editor.text(cx), "acbde\n");
5258 assert_eq!(editor.selections.ranges(cx), [6..6]);
5259
5260 editor.transpose(&Default::default(), window, cx);
5261 assert_eq!(editor.text(cx), "acbd\ne");
5262 assert_eq!(editor.selections.ranges(cx), [6..6]);
5263
5264 editor
5265 });
5266
5267 _ = cx.add_window(|window, cx| {
5268 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5269 editor.set_style(EditorStyle::default(), window, cx);
5270 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5271 s.select_ranges([1..1, 2..2, 4..4])
5272 });
5273 editor.transpose(&Default::default(), window, cx);
5274 assert_eq!(editor.text(cx), "bacd\ne");
5275 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5276
5277 editor.transpose(&Default::default(), window, cx);
5278 assert_eq!(editor.text(cx), "bcade\n");
5279 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5280
5281 editor.transpose(&Default::default(), window, cx);
5282 assert_eq!(editor.text(cx), "bcda\ne");
5283 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5284
5285 editor.transpose(&Default::default(), window, cx);
5286 assert_eq!(editor.text(cx), "bcade\n");
5287 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5288
5289 editor.transpose(&Default::default(), window, cx);
5290 assert_eq!(editor.text(cx), "bcaed\n");
5291 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5292
5293 editor
5294 });
5295
5296 _ = cx.add_window(|window, cx| {
5297 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5298 editor.set_style(EditorStyle::default(), window, cx);
5299 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5300 s.select_ranges([4..4])
5301 });
5302 editor.transpose(&Default::default(), window, cx);
5303 assert_eq!(editor.text(cx), "🏀🍐✋");
5304 assert_eq!(editor.selections.ranges(cx), [8..8]);
5305
5306 editor.transpose(&Default::default(), window, cx);
5307 assert_eq!(editor.text(cx), "🏀✋🍐");
5308 assert_eq!(editor.selections.ranges(cx), [11..11]);
5309
5310 editor.transpose(&Default::default(), window, cx);
5311 assert_eq!(editor.text(cx), "🏀🍐✋");
5312 assert_eq!(editor.selections.ranges(cx), [11..11]);
5313
5314 editor
5315 });
5316}
5317
5318#[gpui::test]
5319async fn test_rewrap(cx: &mut TestAppContext) {
5320 init_test(cx, |settings| {
5321 settings.languages.0.extend([
5322 (
5323 "Markdown".into(),
5324 LanguageSettingsContent {
5325 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5326 preferred_line_length: Some(40),
5327 ..Default::default()
5328 },
5329 ),
5330 (
5331 "Plain Text".into(),
5332 LanguageSettingsContent {
5333 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5334 preferred_line_length: Some(40),
5335 ..Default::default()
5336 },
5337 ),
5338 (
5339 "C++".into(),
5340 LanguageSettingsContent {
5341 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5342 preferred_line_length: Some(40),
5343 ..Default::default()
5344 },
5345 ),
5346 (
5347 "Python".into(),
5348 LanguageSettingsContent {
5349 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5350 preferred_line_length: Some(40),
5351 ..Default::default()
5352 },
5353 ),
5354 (
5355 "Rust".into(),
5356 LanguageSettingsContent {
5357 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5358 preferred_line_length: Some(40),
5359 ..Default::default()
5360 },
5361 ),
5362 ])
5363 });
5364
5365 let mut cx = EditorTestContext::new(cx).await;
5366
5367 let cpp_language = Arc::new(Language::new(
5368 LanguageConfig {
5369 name: "C++".into(),
5370 line_comments: vec!["// ".into()],
5371 ..LanguageConfig::default()
5372 },
5373 None,
5374 ));
5375 let python_language = Arc::new(Language::new(
5376 LanguageConfig {
5377 name: "Python".into(),
5378 line_comments: vec!["# ".into()],
5379 ..LanguageConfig::default()
5380 },
5381 None,
5382 ));
5383 let markdown_language = Arc::new(Language::new(
5384 LanguageConfig {
5385 name: "Markdown".into(),
5386 rewrap_prefixes: vec![
5387 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5388 regex::Regex::new("[-*+]\\s+").unwrap(),
5389 ],
5390 ..LanguageConfig::default()
5391 },
5392 None,
5393 ));
5394 let rust_language = Arc::new(Language::new(
5395 LanguageConfig {
5396 name: "Rust".into(),
5397 line_comments: vec!["// ".into(), "/// ".into()],
5398 ..LanguageConfig::default()
5399 },
5400 Some(tree_sitter_rust::LANGUAGE.into()),
5401 ));
5402
5403 let plaintext_language = Arc::new(Language::new(
5404 LanguageConfig {
5405 name: "Plain Text".into(),
5406 ..LanguageConfig::default()
5407 },
5408 None,
5409 ));
5410
5411 // Test basic rewrapping of a long line with a cursor
5412 assert_rewrap(
5413 indoc! {"
5414 // ˇThis is a long comment that needs to be wrapped.
5415 "},
5416 indoc! {"
5417 // ˇThis is a long comment that needs to
5418 // be wrapped.
5419 "},
5420 cpp_language.clone(),
5421 &mut cx,
5422 );
5423
5424 // Test rewrapping a full selection
5425 assert_rewrap(
5426 indoc! {"
5427 «// This selected long comment needs to be wrapped.ˇ»"
5428 },
5429 indoc! {"
5430 «// This selected long comment needs to
5431 // be wrapped.ˇ»"
5432 },
5433 cpp_language.clone(),
5434 &mut cx,
5435 );
5436
5437 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5438 assert_rewrap(
5439 indoc! {"
5440 // ˇThis is the first line.
5441 // Thisˇ is the second line.
5442 // This is the thirdˇ line, all part of one paragraph.
5443 "},
5444 indoc! {"
5445 // ˇThis is the first line. Thisˇ is the
5446 // second line. This is the thirdˇ line,
5447 // all part of one paragraph.
5448 "},
5449 cpp_language.clone(),
5450 &mut cx,
5451 );
5452
5453 // Test multiple cursors in different paragraphs trigger separate rewraps
5454 assert_rewrap(
5455 indoc! {"
5456 // ˇThis is the first paragraph, first line.
5457 // ˇThis is the first paragraph, second line.
5458
5459 // ˇThis is the second paragraph, first line.
5460 // ˇThis is the second paragraph, second line.
5461 "},
5462 indoc! {"
5463 // ˇThis is the first paragraph, first
5464 // line. ˇThis is the first paragraph,
5465 // second line.
5466
5467 // ˇThis is the second paragraph, first
5468 // line. ˇThis is the second paragraph,
5469 // second line.
5470 "},
5471 cpp_language.clone(),
5472 &mut cx,
5473 );
5474
5475 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
5476 assert_rewrap(
5477 indoc! {"
5478 «// A regular long long comment to be wrapped.
5479 /// A documentation long comment to be wrapped.ˇ»
5480 "},
5481 indoc! {"
5482 «// A regular long long comment to be
5483 // wrapped.
5484 /// A documentation long comment to be
5485 /// wrapped.ˇ»
5486 "},
5487 rust_language.clone(),
5488 &mut cx,
5489 );
5490
5491 // Test that change in indentation level trigger seperate rewraps
5492 assert_rewrap(
5493 indoc! {"
5494 fn foo() {
5495 «// This is a long comment at the base indent.
5496 // This is a long comment at the next indent.ˇ»
5497 }
5498 "},
5499 indoc! {"
5500 fn foo() {
5501 «// This is a long comment at the
5502 // base indent.
5503 // This is a long comment at the
5504 // next indent.ˇ»
5505 }
5506 "},
5507 rust_language.clone(),
5508 &mut cx,
5509 );
5510
5511 // Test that different comment prefix characters (e.g., '#') are handled correctly
5512 assert_rewrap(
5513 indoc! {"
5514 # ˇThis is a long comment using a pound sign.
5515 "},
5516 indoc! {"
5517 # ˇThis is a long comment using a pound
5518 # sign.
5519 "},
5520 python_language.clone(),
5521 &mut cx,
5522 );
5523
5524 // Test rewrapping only affects comments, not code even when selected
5525 assert_rewrap(
5526 indoc! {"
5527 «/// This doc comment is long and should be wrapped.
5528 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5529 "},
5530 indoc! {"
5531 «/// This doc comment is long and should
5532 /// be wrapped.
5533 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5534 "},
5535 rust_language.clone(),
5536 &mut cx,
5537 );
5538
5539 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
5540 assert_rewrap(
5541 indoc! {"
5542 # Header
5543
5544 A long long long line of markdown text to wrap.ˇ
5545 "},
5546 indoc! {"
5547 # Header
5548
5549 A long long long line of markdown text
5550 to wrap.ˇ
5551 "},
5552 markdown_language.clone(),
5553 &mut cx,
5554 );
5555
5556 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
5557 assert_rewrap(
5558 indoc! {"
5559 «1. This is a numbered list item that is very long and needs to be wrapped properly.
5560 2. This is a numbered list item that is very long and needs to be wrapped properly.
5561 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
5562 "},
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
5572 with the numbered item.ˇ»
5573 "},
5574 markdown_language.clone(),
5575 &mut cx,
5576 );
5577
5578 // Test that rewrapping add indents for rewrapping boundary if not exists already.
5579 assert_rewrap(
5580 indoc! {"
5581 «1. This is a numbered list item that is
5582 very long and needs to be wrapped
5583 properly.
5584 2. This is a numbered list item that is
5585 very long and needs to be wrapped
5586 properly.
5587 - This is an unordered list item that is
5588 also very long and should not merge with
5589 the numbered item.ˇ»
5590 "},
5591 indoc! {"
5592 «1. This is a numbered list item that is
5593 very long and needs to be wrapped
5594 properly.
5595 2. This is a numbered list item that is
5596 very long and needs to be wrapped
5597 properly.
5598 - This is an unordered list item that is
5599 also very long and should not merge
5600 with the numbered item.ˇ»
5601 "},
5602 markdown_language.clone(),
5603 &mut cx,
5604 );
5605
5606 // Test that rewrapping maintain indents even when they already exists.
5607 assert_rewrap(
5608 indoc! {"
5609 «1. This is a numbered list
5610 item that is very long and needs to be wrapped properly.
5611 2. This is a numbered list
5612 item that is very long and needs to be wrapped properly.
5613 - This is an unordered list item that is also very long and
5614 should not merge with the numbered item.ˇ»
5615 "},
5616 indoc! {"
5617 «1. This is a numbered list item that is
5618 very long and needs to be wrapped
5619 properly.
5620 2. This is a numbered list item that is
5621 very long and needs to be wrapped
5622 properly.
5623 - This is an unordered list item that is
5624 also very long and should not merge
5625 with the numbered item.ˇ»
5626 "},
5627 markdown_language.clone(),
5628 &mut cx,
5629 );
5630
5631 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
5632 assert_rewrap(
5633 indoc! {"
5634 ˇThis is a very long line of plain text that will be wrapped.
5635 "},
5636 indoc! {"
5637 ˇThis is a very long line of plain text
5638 that will be wrapped.
5639 "},
5640 plaintext_language.clone(),
5641 &mut cx,
5642 );
5643
5644 // Test that non-commented code acts as a paragraph boundary within a selection
5645 assert_rewrap(
5646 indoc! {"
5647 «// This is the first long comment block to be wrapped.
5648 fn my_func(a: u32);
5649 // This is the second long comment block to be wrapped.ˇ»
5650 "},
5651 indoc! {"
5652 «// This is the first long comment block
5653 // to be wrapped.
5654 fn my_func(a: u32);
5655 // This is the second long comment block
5656 // to be wrapped.ˇ»
5657 "},
5658 rust_language.clone(),
5659 &mut cx,
5660 );
5661
5662 // Test rewrapping multiple selections, including ones with blank lines or tabs
5663 assert_rewrap(
5664 indoc! {"
5665 «ˇThis is a very long line that will be wrapped.
5666
5667 This is another paragraph in the same selection.»
5668
5669 «\tThis is a very long indented line that will be wrapped.ˇ»
5670 "},
5671 indoc! {"
5672 «ˇThis is a very long line that will be
5673 wrapped.
5674
5675 This is another paragraph in the same
5676 selection.»
5677
5678 «\tThis is a very long indented line
5679 \tthat will be wrapped.ˇ»
5680 "},
5681 plaintext_language.clone(),
5682 &mut cx,
5683 );
5684
5685 // Test that an empty comment line acts as a paragraph boundary
5686 assert_rewrap(
5687 indoc! {"
5688 // ˇThis is a long comment that will be wrapped.
5689 //
5690 // And this is another long comment that will also be wrapped.ˇ
5691 "},
5692 indoc! {"
5693 // ˇThis is a long comment that will be
5694 // wrapped.
5695 //
5696 // And this is another long comment that
5697 // will also be wrapped.ˇ
5698 "},
5699 cpp_language,
5700 &mut cx,
5701 );
5702
5703 #[track_caller]
5704 fn assert_rewrap(
5705 unwrapped_text: &str,
5706 wrapped_text: &str,
5707 language: Arc<Language>,
5708 cx: &mut EditorTestContext,
5709 ) {
5710 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5711 cx.set_state(unwrapped_text);
5712 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5713 cx.assert_editor_state(wrapped_text);
5714 }
5715}
5716
5717#[gpui::test]
5718async fn test_hard_wrap(cx: &mut TestAppContext) {
5719 init_test(cx, |_| {});
5720 let mut cx = EditorTestContext::new(cx).await;
5721
5722 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5723 cx.update_editor(|editor, _, cx| {
5724 editor.set_hard_wrap(Some(14), cx);
5725 });
5726
5727 cx.set_state(indoc!(
5728 "
5729 one two three ˇ
5730 "
5731 ));
5732 cx.simulate_input("four");
5733 cx.run_until_parked();
5734
5735 cx.assert_editor_state(indoc!(
5736 "
5737 one two three
5738 fourˇ
5739 "
5740 ));
5741
5742 cx.update_editor(|editor, window, cx| {
5743 editor.newline(&Default::default(), window, cx);
5744 });
5745 cx.run_until_parked();
5746 cx.assert_editor_state(indoc!(
5747 "
5748 one two three
5749 four
5750 ˇ
5751 "
5752 ));
5753
5754 cx.simulate_input("five");
5755 cx.run_until_parked();
5756 cx.assert_editor_state(indoc!(
5757 "
5758 one two three
5759 four
5760 fiveˇ
5761 "
5762 ));
5763
5764 cx.update_editor(|editor, window, cx| {
5765 editor.newline(&Default::default(), window, cx);
5766 });
5767 cx.run_until_parked();
5768 cx.simulate_input("# ");
5769 cx.run_until_parked();
5770 cx.assert_editor_state(indoc!(
5771 "
5772 one two three
5773 four
5774 five
5775 # ˇ
5776 "
5777 ));
5778
5779 cx.update_editor(|editor, window, cx| {
5780 editor.newline(&Default::default(), window, cx);
5781 });
5782 cx.run_until_parked();
5783 cx.assert_editor_state(indoc!(
5784 "
5785 one two three
5786 four
5787 five
5788 #\x20
5789 #ˇ
5790 "
5791 ));
5792
5793 cx.simulate_input(" 6");
5794 cx.run_until_parked();
5795 cx.assert_editor_state(indoc!(
5796 "
5797 one two three
5798 four
5799 five
5800 #
5801 # 6ˇ
5802 "
5803 ));
5804}
5805
5806#[gpui::test]
5807async fn test_clipboard(cx: &mut TestAppContext) {
5808 init_test(cx, |_| {});
5809
5810 let mut cx = EditorTestContext::new(cx).await;
5811
5812 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5813 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5814 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5815
5816 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5817 cx.set_state("two ˇfour ˇsix ˇ");
5818 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5819 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5820
5821 // Paste again but with only two cursors. Since the number of cursors doesn't
5822 // match the number of slices in the clipboard, the entire clipboard text
5823 // is pasted at each cursor.
5824 cx.set_state("ˇtwo one✅ four three six five ˇ");
5825 cx.update_editor(|e, window, cx| {
5826 e.handle_input("( ", window, cx);
5827 e.paste(&Paste, window, cx);
5828 e.handle_input(") ", window, cx);
5829 });
5830 cx.assert_editor_state(
5831 &([
5832 "( one✅ ",
5833 "three ",
5834 "five ) ˇtwo one✅ four three six five ( one✅ ",
5835 "three ",
5836 "five ) ˇ",
5837 ]
5838 .join("\n")),
5839 );
5840
5841 // Cut with three selections, one of which is full-line.
5842 cx.set_state(indoc! {"
5843 1«2ˇ»3
5844 4ˇ567
5845 «8ˇ»9"});
5846 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5847 cx.assert_editor_state(indoc! {"
5848 1ˇ3
5849 ˇ9"});
5850
5851 // Paste with three selections, noticing how the copied selection that was full-line
5852 // gets inserted before the second cursor.
5853 cx.set_state(indoc! {"
5854 1ˇ3
5855 9ˇ
5856 «oˇ»ne"});
5857 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5858 cx.assert_editor_state(indoc! {"
5859 12ˇ3
5860 4567
5861 9ˇ
5862 8ˇne"});
5863
5864 // Copy with a single cursor only, which writes the whole line into the clipboard.
5865 cx.set_state(indoc! {"
5866 The quick brown
5867 fox juˇmps over
5868 the lazy dog"});
5869 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5870 assert_eq!(
5871 cx.read_from_clipboard()
5872 .and_then(|item| item.text().as_deref().map(str::to_string)),
5873 Some("fox jumps over\n".to_string())
5874 );
5875
5876 // Paste with three selections, noticing how the copied full-line selection is inserted
5877 // before the empty selections but replaces the selection that is non-empty.
5878 cx.set_state(indoc! {"
5879 Tˇhe quick brown
5880 «foˇ»x jumps over
5881 tˇhe lazy dog"});
5882 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5883 cx.assert_editor_state(indoc! {"
5884 fox jumps over
5885 Tˇhe quick brown
5886 fox jumps over
5887 ˇx jumps over
5888 fox jumps over
5889 tˇhe lazy dog"});
5890}
5891
5892#[gpui::test]
5893async fn test_copy_trim(cx: &mut TestAppContext) {
5894 init_test(cx, |_| {});
5895
5896 let mut cx = EditorTestContext::new(cx).await;
5897 cx.set_state(
5898 r#" «for selection in selections.iter() {
5899 let mut start = selection.start;
5900 let mut end = selection.end;
5901 let is_entire_line = selection.is_empty();
5902 if is_entire_line {
5903 start = Point::new(start.row, 0);ˇ»
5904 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5905 }
5906 "#,
5907 );
5908 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5909 assert_eq!(
5910 cx.read_from_clipboard()
5911 .and_then(|item| item.text().as_deref().map(str::to_string)),
5912 Some(
5913 "for selection in selections.iter() {
5914 let mut start = selection.start;
5915 let mut end = selection.end;
5916 let is_entire_line = selection.is_empty();
5917 if is_entire_line {
5918 start = Point::new(start.row, 0);"
5919 .to_string()
5920 ),
5921 "Regular copying preserves all indentation selected",
5922 );
5923 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5924 assert_eq!(
5925 cx.read_from_clipboard()
5926 .and_then(|item| item.text().as_deref().map(str::to_string)),
5927 Some(
5928 "for selection in selections.iter() {
5929let mut start = selection.start;
5930let mut end = selection.end;
5931let is_entire_line = selection.is_empty();
5932if is_entire_line {
5933 start = Point::new(start.row, 0);"
5934 .to_string()
5935 ),
5936 "Copying with stripping should strip all leading whitespaces"
5937 );
5938
5939 cx.set_state(
5940 r#" « for selection in selections.iter() {
5941 let mut start = selection.start;
5942 let mut end = selection.end;
5943 let is_entire_line = selection.is_empty();
5944 if is_entire_line {
5945 start = Point::new(start.row, 0);ˇ»
5946 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5947 }
5948 "#,
5949 );
5950 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5951 assert_eq!(
5952 cx.read_from_clipboard()
5953 .and_then(|item| item.text().as_deref().map(str::to_string)),
5954 Some(
5955 " for selection in selections.iter() {
5956 let mut start = selection.start;
5957 let mut end = selection.end;
5958 let is_entire_line = selection.is_empty();
5959 if is_entire_line {
5960 start = Point::new(start.row, 0);"
5961 .to_string()
5962 ),
5963 "Regular copying preserves all indentation selected",
5964 );
5965 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5966 assert_eq!(
5967 cx.read_from_clipboard()
5968 .and_then(|item| item.text().as_deref().map(str::to_string)),
5969 Some(
5970 "for selection in selections.iter() {
5971let mut start = selection.start;
5972let mut end = selection.end;
5973let is_entire_line = selection.is_empty();
5974if is_entire_line {
5975 start = Point::new(start.row, 0);"
5976 .to_string()
5977 ),
5978 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5979 );
5980
5981 cx.set_state(
5982 r#" «ˇ for selection in selections.iter() {
5983 let mut start = selection.start;
5984 let mut end = selection.end;
5985 let is_entire_line = selection.is_empty();
5986 if is_entire_line {
5987 start = Point::new(start.row, 0);»
5988 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5989 }
5990 "#,
5991 );
5992 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5993 assert_eq!(
5994 cx.read_from_clipboard()
5995 .and_then(|item| item.text().as_deref().map(str::to_string)),
5996 Some(
5997 " for selection in selections.iter() {
5998 let mut start = selection.start;
5999 let mut end = selection.end;
6000 let is_entire_line = selection.is_empty();
6001 if is_entire_line {
6002 start = Point::new(start.row, 0);"
6003 .to_string()
6004 ),
6005 "Regular copying for reverse selection works the same",
6006 );
6007 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6008 assert_eq!(
6009 cx.read_from_clipboard()
6010 .and_then(|item| item.text().as_deref().map(str::to_string)),
6011 Some(
6012 "for selection in selections.iter() {
6013let mut start = selection.start;
6014let mut end = selection.end;
6015let is_entire_line = selection.is_empty();
6016if is_entire_line {
6017 start = Point::new(start.row, 0);"
6018 .to_string()
6019 ),
6020 "Copying with stripping for reverse selection works the same"
6021 );
6022
6023 cx.set_state(
6024 r#" for selection «in selections.iter() {
6025 let mut start = selection.start;
6026 let mut end = selection.end;
6027 let is_entire_line = selection.is_empty();
6028 if is_entire_line {
6029 start = Point::new(start.row, 0);ˇ»
6030 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6031 }
6032 "#,
6033 );
6034 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6035 assert_eq!(
6036 cx.read_from_clipboard()
6037 .and_then(|item| item.text().as_deref().map(str::to_string)),
6038 Some(
6039 "in selections.iter() {
6040 let mut start = selection.start;
6041 let mut end = selection.end;
6042 let is_entire_line = selection.is_empty();
6043 if is_entire_line {
6044 start = Point::new(start.row, 0);"
6045 .to_string()
6046 ),
6047 "When selecting past the indent, the copying works as usual",
6048 );
6049 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6050 assert_eq!(
6051 cx.read_from_clipboard()
6052 .and_then(|item| item.text().as_deref().map(str::to_string)),
6053 Some(
6054 "in selections.iter() {
6055 let mut start = selection.start;
6056 let mut end = selection.end;
6057 let is_entire_line = selection.is_empty();
6058 if is_entire_line {
6059 start = Point::new(start.row, 0);"
6060 .to_string()
6061 ),
6062 "When selecting past the indent, nothing is trimmed"
6063 );
6064
6065 cx.set_state(
6066 r#" «for selection in selections.iter() {
6067 let mut start = selection.start;
6068
6069 let mut end = selection.end;
6070 let is_entire_line = selection.is_empty();
6071 if is_entire_line {
6072 start = Point::new(start.row, 0);
6073ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
6074 }
6075 "#,
6076 );
6077 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6078 assert_eq!(
6079 cx.read_from_clipboard()
6080 .and_then(|item| item.text().as_deref().map(str::to_string)),
6081 Some(
6082 "for selection in selections.iter() {
6083let mut start = selection.start;
6084
6085let mut end = selection.end;
6086let is_entire_line = selection.is_empty();
6087if is_entire_line {
6088 start = Point::new(start.row, 0);
6089"
6090 .to_string()
6091 ),
6092 "Copying with stripping should ignore empty lines"
6093 );
6094}
6095
6096#[gpui::test]
6097async fn test_paste_multiline(cx: &mut TestAppContext) {
6098 init_test(cx, |_| {});
6099
6100 let mut cx = EditorTestContext::new(cx).await;
6101 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6102
6103 // Cut an indented block, without the leading whitespace.
6104 cx.set_state(indoc! {"
6105 const a: B = (
6106 c(),
6107 «d(
6108 e,
6109 f
6110 )ˇ»
6111 );
6112 "});
6113 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6114 cx.assert_editor_state(indoc! {"
6115 const a: B = (
6116 c(),
6117 ˇ
6118 );
6119 "});
6120
6121 // Paste it at the same position.
6122 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6123 cx.assert_editor_state(indoc! {"
6124 const a: B = (
6125 c(),
6126 d(
6127 e,
6128 f
6129 )ˇ
6130 );
6131 "});
6132
6133 // Paste it at a line with a lower indent level.
6134 cx.set_state(indoc! {"
6135 ˇ
6136 const a: B = (
6137 c(),
6138 );
6139 "});
6140 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6141 cx.assert_editor_state(indoc! {"
6142 d(
6143 e,
6144 f
6145 )ˇ
6146 const a: B = (
6147 c(),
6148 );
6149 "});
6150
6151 // Cut an indented block, with the leading whitespace.
6152 cx.set_state(indoc! {"
6153 const a: B = (
6154 c(),
6155 « d(
6156 e,
6157 f
6158 )
6159 ˇ»);
6160 "});
6161 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6162 cx.assert_editor_state(indoc! {"
6163 const a: B = (
6164 c(),
6165 ˇ);
6166 "});
6167
6168 // Paste it at the same position.
6169 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6170 cx.assert_editor_state(indoc! {"
6171 const a: B = (
6172 c(),
6173 d(
6174 e,
6175 f
6176 )
6177 ˇ);
6178 "});
6179
6180 // Paste it at a line with a higher indent level.
6181 cx.set_state(indoc! {"
6182 const a: B = (
6183 c(),
6184 d(
6185 e,
6186 fˇ
6187 )
6188 );
6189 "});
6190 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6191 cx.assert_editor_state(indoc! {"
6192 const a: B = (
6193 c(),
6194 d(
6195 e,
6196 f d(
6197 e,
6198 f
6199 )
6200 ˇ
6201 )
6202 );
6203 "});
6204
6205 // Copy an indented block, starting mid-line
6206 cx.set_state(indoc! {"
6207 const a: B = (
6208 c(),
6209 somethin«g(
6210 e,
6211 f
6212 )ˇ»
6213 );
6214 "});
6215 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6216
6217 // Paste it on a line with a lower indent level
6218 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
6219 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6220 cx.assert_editor_state(indoc! {"
6221 const a: B = (
6222 c(),
6223 something(
6224 e,
6225 f
6226 )
6227 );
6228 g(
6229 e,
6230 f
6231 )ˇ"});
6232}
6233
6234#[gpui::test]
6235async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
6236 init_test(cx, |_| {});
6237
6238 cx.write_to_clipboard(ClipboardItem::new_string(
6239 " d(\n e\n );\n".into(),
6240 ));
6241
6242 let mut cx = EditorTestContext::new(cx).await;
6243 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6244
6245 cx.set_state(indoc! {"
6246 fn a() {
6247 b();
6248 if c() {
6249 ˇ
6250 }
6251 }
6252 "});
6253
6254 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6255 cx.assert_editor_state(indoc! {"
6256 fn a() {
6257 b();
6258 if c() {
6259 d(
6260 e
6261 );
6262 ˇ
6263 }
6264 }
6265 "});
6266
6267 cx.set_state(indoc! {"
6268 fn a() {
6269 b();
6270 ˇ
6271 }
6272 "});
6273
6274 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6275 cx.assert_editor_state(indoc! {"
6276 fn a() {
6277 b();
6278 d(
6279 e
6280 );
6281 ˇ
6282 }
6283 "});
6284}
6285
6286#[gpui::test]
6287fn test_select_all(cx: &mut TestAppContext) {
6288 init_test(cx, |_| {});
6289
6290 let editor = cx.add_window(|window, cx| {
6291 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
6292 build_editor(buffer, window, cx)
6293 });
6294 _ = editor.update(cx, |editor, window, cx| {
6295 editor.select_all(&SelectAll, window, cx);
6296 assert_eq!(
6297 editor.selections.display_ranges(cx),
6298 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
6299 );
6300 });
6301}
6302
6303#[gpui::test]
6304fn test_select_line(cx: &mut TestAppContext) {
6305 init_test(cx, |_| {});
6306
6307 let editor = cx.add_window(|window, cx| {
6308 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
6309 build_editor(buffer, window, cx)
6310 });
6311 _ = editor.update(cx, |editor, window, cx| {
6312 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6313 s.select_display_ranges([
6314 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6315 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6316 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6317 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
6318 ])
6319 });
6320 editor.select_line(&SelectLine, window, cx);
6321 assert_eq!(
6322 editor.selections.display_ranges(cx),
6323 vec![
6324 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
6325 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
6326 ]
6327 );
6328 });
6329
6330 _ = editor.update(cx, |editor, window, cx| {
6331 editor.select_line(&SelectLine, window, cx);
6332 assert_eq!(
6333 editor.selections.display_ranges(cx),
6334 vec![
6335 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
6336 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
6337 ]
6338 );
6339 });
6340
6341 _ = editor.update(cx, |editor, window, cx| {
6342 editor.select_line(&SelectLine, window, cx);
6343 assert_eq!(
6344 editor.selections.display_ranges(cx),
6345 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
6346 );
6347 });
6348}
6349
6350#[gpui::test]
6351async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
6352 init_test(cx, |_| {});
6353 let mut cx = EditorTestContext::new(cx).await;
6354
6355 #[track_caller]
6356 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
6357 cx.set_state(initial_state);
6358 cx.update_editor(|e, window, cx| {
6359 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
6360 });
6361 cx.assert_editor_state(expected_state);
6362 }
6363
6364 // Selection starts and ends at the middle of lines, left-to-right
6365 test(
6366 &mut cx,
6367 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
6368 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6369 );
6370 // Same thing, right-to-left
6371 test(
6372 &mut cx,
6373 "aa\nb«b\ncc\ndd\neˇ»e\nff",
6374 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6375 );
6376
6377 // Whole buffer, left-to-right, last line *doesn't* end with newline
6378 test(
6379 &mut cx,
6380 "«ˇaa\nbb\ncc\ndd\nee\nff»",
6381 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6382 );
6383 // Same thing, right-to-left
6384 test(
6385 &mut cx,
6386 "«aa\nbb\ncc\ndd\nee\nffˇ»",
6387 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6388 );
6389
6390 // Whole buffer, left-to-right, last line ends with newline
6391 test(
6392 &mut cx,
6393 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
6394 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6395 );
6396 // Same thing, right-to-left
6397 test(
6398 &mut cx,
6399 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
6400 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6401 );
6402
6403 // Starts at the end of a line, ends at the start of another
6404 test(
6405 &mut cx,
6406 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
6407 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
6408 );
6409}
6410
6411#[gpui::test]
6412async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
6413 init_test(cx, |_| {});
6414
6415 let editor = cx.add_window(|window, cx| {
6416 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
6417 build_editor(buffer, window, cx)
6418 });
6419
6420 // setup
6421 _ = editor.update(cx, |editor, window, cx| {
6422 editor.fold_creases(
6423 vec![
6424 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
6425 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
6426 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
6427 ],
6428 true,
6429 window,
6430 cx,
6431 );
6432 assert_eq!(
6433 editor.display_text(cx),
6434 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6435 );
6436 });
6437
6438 _ = editor.update(cx, |editor, window, cx| {
6439 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6440 s.select_display_ranges([
6441 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6442 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6443 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6444 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
6445 ])
6446 });
6447 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6448 assert_eq!(
6449 editor.display_text(cx),
6450 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6451 );
6452 });
6453 EditorTestContext::for_editor(editor, cx)
6454 .await
6455 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
6456
6457 _ = editor.update(cx, |editor, window, cx| {
6458 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6459 s.select_display_ranges([
6460 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
6461 ])
6462 });
6463 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6464 assert_eq!(
6465 editor.display_text(cx),
6466 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
6467 );
6468 assert_eq!(
6469 editor.selections.display_ranges(cx),
6470 [
6471 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
6472 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
6473 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
6474 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
6475 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
6476 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
6477 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
6478 ]
6479 );
6480 });
6481 EditorTestContext::for_editor(editor, cx)
6482 .await
6483 .assert_editor_state(
6484 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
6485 );
6486}
6487
6488#[gpui::test]
6489async fn test_add_selection_above_below(cx: &mut TestAppContext) {
6490 init_test(cx, |_| {});
6491
6492 let mut cx = EditorTestContext::new(cx).await;
6493
6494 cx.set_state(indoc!(
6495 r#"abc
6496 defˇghi
6497
6498 jk
6499 nlmo
6500 "#
6501 ));
6502
6503 cx.update_editor(|editor, window, cx| {
6504 editor.add_selection_above(&Default::default(), window, cx);
6505 });
6506
6507 cx.assert_editor_state(indoc!(
6508 r#"abcˇ
6509 defˇghi
6510
6511 jk
6512 nlmo
6513 "#
6514 ));
6515
6516 cx.update_editor(|editor, window, cx| {
6517 editor.add_selection_above(&Default::default(), window, cx);
6518 });
6519
6520 cx.assert_editor_state(indoc!(
6521 r#"abcˇ
6522 defˇghi
6523
6524 jk
6525 nlmo
6526 "#
6527 ));
6528
6529 cx.update_editor(|editor, window, cx| {
6530 editor.add_selection_below(&Default::default(), window, cx);
6531 });
6532
6533 cx.assert_editor_state(indoc!(
6534 r#"abc
6535 defˇghi
6536
6537 jk
6538 nlmo
6539 "#
6540 ));
6541
6542 cx.update_editor(|editor, window, cx| {
6543 editor.undo_selection(&Default::default(), window, cx);
6544 });
6545
6546 cx.assert_editor_state(indoc!(
6547 r#"abcˇ
6548 defˇghi
6549
6550 jk
6551 nlmo
6552 "#
6553 ));
6554
6555 cx.update_editor(|editor, window, cx| {
6556 editor.redo_selection(&Default::default(), window, cx);
6557 });
6558
6559 cx.assert_editor_state(indoc!(
6560 r#"abc
6561 defˇghi
6562
6563 jk
6564 nlmo
6565 "#
6566 ));
6567
6568 cx.update_editor(|editor, window, cx| {
6569 editor.add_selection_below(&Default::default(), window, cx);
6570 });
6571
6572 cx.assert_editor_state(indoc!(
6573 r#"abc
6574 defˇghi
6575 ˇ
6576 jk
6577 nlmo
6578 "#
6579 ));
6580
6581 cx.update_editor(|editor, window, cx| {
6582 editor.add_selection_below(&Default::default(), window, cx);
6583 });
6584
6585 cx.assert_editor_state(indoc!(
6586 r#"abc
6587 defˇghi
6588 ˇ
6589 jkˇ
6590 nlmo
6591 "#
6592 ));
6593
6594 cx.update_editor(|editor, window, cx| {
6595 editor.add_selection_below(&Default::default(), window, cx);
6596 });
6597
6598 cx.assert_editor_state(indoc!(
6599 r#"abc
6600 defˇghi
6601 ˇ
6602 jkˇ
6603 nlmˇo
6604 "#
6605 ));
6606
6607 cx.update_editor(|editor, window, cx| {
6608 editor.add_selection_below(&Default::default(), window, cx);
6609 });
6610
6611 cx.assert_editor_state(indoc!(
6612 r#"abc
6613 defˇghi
6614 ˇ
6615 jkˇ
6616 nlmˇo
6617 ˇ"#
6618 ));
6619
6620 // change selections
6621 cx.set_state(indoc!(
6622 r#"abc
6623 def«ˇg»hi
6624
6625 jk
6626 nlmo
6627 "#
6628 ));
6629
6630 cx.update_editor(|editor, window, cx| {
6631 editor.add_selection_below(&Default::default(), window, cx);
6632 });
6633
6634 cx.assert_editor_state(indoc!(
6635 r#"abc
6636 def«ˇg»hi
6637
6638 jk
6639 nlm«ˇo»
6640 "#
6641 ));
6642
6643 cx.update_editor(|editor, window, cx| {
6644 editor.add_selection_below(&Default::default(), window, cx);
6645 });
6646
6647 cx.assert_editor_state(indoc!(
6648 r#"abc
6649 def«ˇg»hi
6650
6651 jk
6652 nlm«ˇo»
6653 "#
6654 ));
6655
6656 cx.update_editor(|editor, window, cx| {
6657 editor.add_selection_above(&Default::default(), window, cx);
6658 });
6659
6660 cx.assert_editor_state(indoc!(
6661 r#"abc
6662 def«ˇg»hi
6663
6664 jk
6665 nlmo
6666 "#
6667 ));
6668
6669 cx.update_editor(|editor, window, cx| {
6670 editor.add_selection_above(&Default::default(), window, cx);
6671 });
6672
6673 cx.assert_editor_state(indoc!(
6674 r#"abc
6675 def«ˇg»hi
6676
6677 jk
6678 nlmo
6679 "#
6680 ));
6681
6682 // Change selections again
6683 cx.set_state(indoc!(
6684 r#"a«bc
6685 defgˇ»hi
6686
6687 jk
6688 nlmo
6689 "#
6690 ));
6691
6692 cx.update_editor(|editor, window, cx| {
6693 editor.add_selection_below(&Default::default(), window, cx);
6694 });
6695
6696 cx.assert_editor_state(indoc!(
6697 r#"a«bcˇ»
6698 d«efgˇ»hi
6699
6700 j«kˇ»
6701 nlmo
6702 "#
6703 ));
6704
6705 cx.update_editor(|editor, window, cx| {
6706 editor.add_selection_below(&Default::default(), window, cx);
6707 });
6708 cx.assert_editor_state(indoc!(
6709 r#"a«bcˇ»
6710 d«efgˇ»hi
6711
6712 j«kˇ»
6713 n«lmoˇ»
6714 "#
6715 ));
6716 cx.update_editor(|editor, window, cx| {
6717 editor.add_selection_above(&Default::default(), window, cx);
6718 });
6719
6720 cx.assert_editor_state(indoc!(
6721 r#"a«bcˇ»
6722 d«efgˇ»hi
6723
6724 j«kˇ»
6725 nlmo
6726 "#
6727 ));
6728
6729 // Change selections again
6730 cx.set_state(indoc!(
6731 r#"abc
6732 d«ˇefghi
6733
6734 jk
6735 nlm»o
6736 "#
6737 ));
6738
6739 cx.update_editor(|editor, window, cx| {
6740 editor.add_selection_above(&Default::default(), window, cx);
6741 });
6742
6743 cx.assert_editor_state(indoc!(
6744 r#"a«ˇbc»
6745 d«ˇef»ghi
6746
6747 j«ˇk»
6748 n«ˇlm»o
6749 "#
6750 ));
6751
6752 cx.update_editor(|editor, window, cx| {
6753 editor.add_selection_below(&Default::default(), window, cx);
6754 });
6755
6756 cx.assert_editor_state(indoc!(
6757 r#"abc
6758 d«ˇef»ghi
6759
6760 j«ˇk»
6761 n«ˇlm»o
6762 "#
6763 ));
6764}
6765
6766#[gpui::test]
6767async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
6768 init_test(cx, |_| {});
6769 let mut cx = EditorTestContext::new(cx).await;
6770
6771 cx.set_state(indoc!(
6772 r#"line onˇe
6773 liˇne two
6774 line three
6775 line four"#
6776 ));
6777
6778 cx.update_editor(|editor, window, cx| {
6779 editor.add_selection_below(&Default::default(), window, cx);
6780 });
6781
6782 // test multiple cursors expand in the same direction
6783 cx.assert_editor_state(indoc!(
6784 r#"line onˇe
6785 liˇne twˇo
6786 liˇne three
6787 line four"#
6788 ));
6789
6790 cx.update_editor(|editor, window, cx| {
6791 editor.add_selection_below(&Default::default(), window, cx);
6792 });
6793
6794 cx.update_editor(|editor, window, cx| {
6795 editor.add_selection_below(&Default::default(), window, cx);
6796 });
6797
6798 // test multiple cursors expand below overflow
6799 cx.assert_editor_state(indoc!(
6800 r#"line onˇe
6801 liˇne twˇo
6802 liˇne thˇree
6803 liˇne foˇur"#
6804 ));
6805
6806 cx.update_editor(|editor, window, cx| {
6807 editor.add_selection_above(&Default::default(), window, cx);
6808 });
6809
6810 // test multiple cursors retrieves back correctly
6811 cx.assert_editor_state(indoc!(
6812 r#"line onˇe
6813 liˇne twˇo
6814 liˇne thˇree
6815 line four"#
6816 ));
6817
6818 cx.update_editor(|editor, window, cx| {
6819 editor.add_selection_above(&Default::default(), window, cx);
6820 });
6821
6822 cx.update_editor(|editor, window, cx| {
6823 editor.add_selection_above(&Default::default(), window, cx);
6824 });
6825
6826 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
6827 cx.assert_editor_state(indoc!(
6828 r#"liˇne onˇe
6829 liˇne two
6830 line three
6831 line four"#
6832 ));
6833
6834 cx.update_editor(|editor, window, cx| {
6835 editor.undo_selection(&Default::default(), window, cx);
6836 });
6837
6838 // test undo
6839 cx.assert_editor_state(indoc!(
6840 r#"line onˇe
6841 liˇne twˇo
6842 line three
6843 line four"#
6844 ));
6845
6846 cx.update_editor(|editor, window, cx| {
6847 editor.redo_selection(&Default::default(), window, cx);
6848 });
6849
6850 // test redo
6851 cx.assert_editor_state(indoc!(
6852 r#"liˇne onˇe
6853 liˇne two
6854 line three
6855 line four"#
6856 ));
6857
6858 cx.set_state(indoc!(
6859 r#"abcd
6860 ef«ghˇ»
6861 ijkl
6862 «mˇ»nop"#
6863 ));
6864
6865 cx.update_editor(|editor, window, cx| {
6866 editor.add_selection_above(&Default::default(), window, cx);
6867 });
6868
6869 // test multiple selections expand in the same direction
6870 cx.assert_editor_state(indoc!(
6871 r#"ab«cdˇ»
6872 ef«ghˇ»
6873 «iˇ»jkl
6874 «mˇ»nop"#
6875 ));
6876
6877 cx.update_editor(|editor, window, cx| {
6878 editor.add_selection_above(&Default::default(), window, cx);
6879 });
6880
6881 // test multiple selection upward overflow
6882 cx.assert_editor_state(indoc!(
6883 r#"ab«cdˇ»
6884 «eˇ»f«ghˇ»
6885 «iˇ»jkl
6886 «mˇ»nop"#
6887 ));
6888
6889 cx.update_editor(|editor, window, cx| {
6890 editor.add_selection_below(&Default::default(), window, cx);
6891 });
6892
6893 // test multiple selection retrieves back correctly
6894 cx.assert_editor_state(indoc!(
6895 r#"abcd
6896 ef«ghˇ»
6897 «iˇ»jkl
6898 «mˇ»nop"#
6899 ));
6900
6901 cx.update_editor(|editor, window, cx| {
6902 editor.add_selection_below(&Default::default(), window, cx);
6903 });
6904
6905 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
6906 cx.assert_editor_state(indoc!(
6907 r#"abcd
6908 ef«ghˇ»
6909 ij«klˇ»
6910 «mˇ»nop"#
6911 ));
6912
6913 cx.update_editor(|editor, window, cx| {
6914 editor.undo_selection(&Default::default(), window, cx);
6915 });
6916
6917 // test undo
6918 cx.assert_editor_state(indoc!(
6919 r#"abcd
6920 ef«ghˇ»
6921 «iˇ»jkl
6922 «mˇ»nop"#
6923 ));
6924
6925 cx.update_editor(|editor, window, cx| {
6926 editor.redo_selection(&Default::default(), window, cx);
6927 });
6928
6929 // test redo
6930 cx.assert_editor_state(indoc!(
6931 r#"abcd
6932 ef«ghˇ»
6933 ij«klˇ»
6934 «mˇ»nop"#
6935 ));
6936}
6937
6938#[gpui::test]
6939async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
6940 init_test(cx, |_| {});
6941 let mut cx = EditorTestContext::new(cx).await;
6942
6943 cx.set_state(indoc!(
6944 r#"line onˇe
6945 liˇne two
6946 line three
6947 line four"#
6948 ));
6949
6950 cx.update_editor(|editor, window, cx| {
6951 editor.add_selection_below(&Default::default(), window, cx);
6952 editor.add_selection_below(&Default::default(), window, cx);
6953 editor.add_selection_below(&Default::default(), window, cx);
6954 });
6955
6956 // initial state with two multi cursor groups
6957 cx.assert_editor_state(indoc!(
6958 r#"line onˇe
6959 liˇne twˇo
6960 liˇne thˇree
6961 liˇne foˇur"#
6962 ));
6963
6964 // add single cursor in middle - simulate opt click
6965 cx.update_editor(|editor, window, cx| {
6966 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
6967 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6968 editor.end_selection(window, cx);
6969 });
6970
6971 cx.assert_editor_state(indoc!(
6972 r#"line onˇe
6973 liˇne twˇo
6974 liˇneˇ thˇree
6975 liˇne foˇur"#
6976 ));
6977
6978 cx.update_editor(|editor, window, cx| {
6979 editor.add_selection_above(&Default::default(), window, cx);
6980 });
6981
6982 // test new added selection expands above and existing selection shrinks
6983 cx.assert_editor_state(indoc!(
6984 r#"line onˇe
6985 liˇneˇ twˇo
6986 liˇneˇ thˇree
6987 line four"#
6988 ));
6989
6990 cx.update_editor(|editor, window, cx| {
6991 editor.add_selection_above(&Default::default(), window, cx);
6992 });
6993
6994 // test new added selection expands above and existing selection shrinks
6995 cx.assert_editor_state(indoc!(
6996 r#"lineˇ onˇe
6997 liˇneˇ twˇo
6998 lineˇ three
6999 line four"#
7000 ));
7001
7002 // intial state with two selection groups
7003 cx.set_state(indoc!(
7004 r#"abcd
7005 ef«ghˇ»
7006 ijkl
7007 «mˇ»nop"#
7008 ));
7009
7010 cx.update_editor(|editor, window, cx| {
7011 editor.add_selection_above(&Default::default(), window, cx);
7012 editor.add_selection_above(&Default::default(), window, cx);
7013 });
7014
7015 cx.assert_editor_state(indoc!(
7016 r#"ab«cdˇ»
7017 «eˇ»f«ghˇ»
7018 «iˇ»jkl
7019 «mˇ»nop"#
7020 ));
7021
7022 // add single selection in middle - simulate opt drag
7023 cx.update_editor(|editor, window, cx| {
7024 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
7025 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7026 editor.update_selection(
7027 DisplayPoint::new(DisplayRow(2), 4),
7028 0,
7029 gpui::Point::<f32>::default(),
7030 window,
7031 cx,
7032 );
7033 editor.end_selection(window, cx);
7034 });
7035
7036 cx.assert_editor_state(indoc!(
7037 r#"ab«cdˇ»
7038 «eˇ»f«ghˇ»
7039 «iˇ»jk«lˇ»
7040 «mˇ»nop"#
7041 ));
7042
7043 cx.update_editor(|editor, window, cx| {
7044 editor.add_selection_below(&Default::default(), window, cx);
7045 });
7046
7047 // test new added selection expands below, others shrinks from above
7048 cx.assert_editor_state(indoc!(
7049 r#"abcd
7050 ef«ghˇ»
7051 «iˇ»jk«lˇ»
7052 «mˇ»no«pˇ»"#
7053 ));
7054}
7055
7056#[gpui::test]
7057async fn test_select_next(cx: &mut TestAppContext) {
7058 init_test(cx, |_| {});
7059
7060 let mut cx = EditorTestContext::new(cx).await;
7061 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7062
7063 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7064 .unwrap();
7065 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7066
7067 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7068 .unwrap();
7069 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7070
7071 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7072 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7073
7074 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7075 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7076
7077 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7078 .unwrap();
7079 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7080
7081 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7082 .unwrap();
7083 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7084
7085 // Test selection direction should be preserved
7086 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7087
7088 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7089 .unwrap();
7090 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
7091}
7092
7093#[gpui::test]
7094async fn test_select_all_matches(cx: &mut TestAppContext) {
7095 init_test(cx, |_| {});
7096
7097 let mut cx = EditorTestContext::new(cx).await;
7098
7099 // Test caret-only selections
7100 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7101 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7102 .unwrap();
7103 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7104
7105 // Test left-to-right selections
7106 cx.set_state("abc\n«abcˇ»\nabc");
7107 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7108 .unwrap();
7109 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
7110
7111 // Test right-to-left selections
7112 cx.set_state("abc\n«ˇabc»\nabc");
7113 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7114 .unwrap();
7115 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
7116
7117 // Test selecting whitespace with caret selection
7118 cx.set_state("abc\nˇ abc\nabc");
7119 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7120 .unwrap();
7121 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7122
7123 // Test selecting whitespace with left-to-right selection
7124 cx.set_state("abc\n«ˇ »abc\nabc");
7125 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7126 .unwrap();
7127 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
7128
7129 // Test no matches with right-to-left selection
7130 cx.set_state("abc\n« ˇ»abc\nabc");
7131 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7132 .unwrap();
7133 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7134
7135 // Test with a single word and clip_at_line_ends=true (#29823)
7136 cx.set_state("aˇbc");
7137 cx.update_editor(|e, window, cx| {
7138 e.set_clip_at_line_ends(true, cx);
7139 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
7140 e.set_clip_at_line_ends(false, cx);
7141 });
7142 cx.assert_editor_state("«abcˇ»");
7143}
7144
7145#[gpui::test]
7146async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
7147 init_test(cx, |_| {});
7148
7149 let mut cx = EditorTestContext::new(cx).await;
7150
7151 let large_body_1 = "\nd".repeat(200);
7152 let large_body_2 = "\ne".repeat(200);
7153
7154 cx.set_state(&format!(
7155 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
7156 ));
7157 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
7158 let scroll_position = editor.scroll_position(cx);
7159 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
7160 scroll_position
7161 });
7162
7163 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7164 .unwrap();
7165 cx.assert_editor_state(&format!(
7166 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
7167 ));
7168 let scroll_position_after_selection =
7169 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
7170 assert_eq!(
7171 initial_scroll_position, scroll_position_after_selection,
7172 "Scroll position should not change after selecting all matches"
7173 );
7174}
7175
7176#[gpui::test]
7177async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
7178 init_test(cx, |_| {});
7179
7180 let mut cx = EditorLspTestContext::new_rust(
7181 lsp::ServerCapabilities {
7182 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7183 ..Default::default()
7184 },
7185 cx,
7186 )
7187 .await;
7188
7189 cx.set_state(indoc! {"
7190 line 1
7191 line 2
7192 linˇe 3
7193 line 4
7194 line 5
7195 "});
7196
7197 // Make an edit
7198 cx.update_editor(|editor, window, cx| {
7199 editor.handle_input("X", window, cx);
7200 });
7201
7202 // Move cursor to a different position
7203 cx.update_editor(|editor, window, cx| {
7204 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7205 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
7206 });
7207 });
7208
7209 cx.assert_editor_state(indoc! {"
7210 line 1
7211 line 2
7212 linXe 3
7213 line 4
7214 liˇne 5
7215 "});
7216
7217 cx.lsp
7218 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
7219 Ok(Some(vec![lsp::TextEdit::new(
7220 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
7221 "PREFIX ".to_string(),
7222 )]))
7223 });
7224
7225 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
7226 .unwrap()
7227 .await
7228 .unwrap();
7229
7230 cx.assert_editor_state(indoc! {"
7231 PREFIX line 1
7232 line 2
7233 linXe 3
7234 line 4
7235 liˇne 5
7236 "});
7237
7238 // Undo formatting
7239 cx.update_editor(|editor, window, cx| {
7240 editor.undo(&Default::default(), window, cx);
7241 });
7242
7243 // Verify cursor moved back to position after edit
7244 cx.assert_editor_state(indoc! {"
7245 line 1
7246 line 2
7247 linXˇe 3
7248 line 4
7249 line 5
7250 "});
7251}
7252
7253#[gpui::test]
7254async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) {
7255 init_test(cx, |_| {});
7256
7257 let mut cx = EditorTestContext::new(cx).await;
7258
7259 let provider = cx.new(|_| FakeInlineCompletionProvider::default());
7260 cx.update_editor(|editor, window, cx| {
7261 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
7262 });
7263
7264 cx.set_state(indoc! {"
7265 line 1
7266 line 2
7267 linˇe 3
7268 line 4
7269 line 5
7270 line 6
7271 line 7
7272 line 8
7273 line 9
7274 line 10
7275 "});
7276
7277 let snapshot = cx.buffer_snapshot();
7278 let edit_position = snapshot.anchor_after(Point::new(2, 4));
7279
7280 cx.update(|_, cx| {
7281 provider.update(cx, |provider, _| {
7282 provider.set_inline_completion(Some(inline_completion::InlineCompletion {
7283 id: None,
7284 edits: vec![(edit_position..edit_position, "X".into())],
7285 edit_preview: None,
7286 }))
7287 })
7288 });
7289
7290 cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
7291 cx.update_editor(|editor, window, cx| {
7292 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
7293 });
7294
7295 cx.assert_editor_state(indoc! {"
7296 line 1
7297 line 2
7298 lineXˇ 3
7299 line 4
7300 line 5
7301 line 6
7302 line 7
7303 line 8
7304 line 9
7305 line 10
7306 "});
7307
7308 cx.update_editor(|editor, window, cx| {
7309 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7310 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
7311 });
7312 });
7313
7314 cx.assert_editor_state(indoc! {"
7315 line 1
7316 line 2
7317 lineX 3
7318 line 4
7319 line 5
7320 line 6
7321 line 7
7322 line 8
7323 line 9
7324 liˇne 10
7325 "});
7326
7327 cx.update_editor(|editor, window, cx| {
7328 editor.undo(&Default::default(), window, cx);
7329 });
7330
7331 cx.assert_editor_state(indoc! {"
7332 line 1
7333 line 2
7334 lineˇ 3
7335 line 4
7336 line 5
7337 line 6
7338 line 7
7339 line 8
7340 line 9
7341 line 10
7342 "});
7343}
7344
7345#[gpui::test]
7346async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
7347 init_test(cx, |_| {});
7348
7349 let mut cx = EditorTestContext::new(cx).await;
7350 cx.set_state(
7351 r#"let foo = 2;
7352lˇet foo = 2;
7353let fooˇ = 2;
7354let foo = 2;
7355let foo = ˇ2;"#,
7356 );
7357
7358 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7359 .unwrap();
7360 cx.assert_editor_state(
7361 r#"let foo = 2;
7362«letˇ» foo = 2;
7363let «fooˇ» = 2;
7364let foo = 2;
7365let foo = «2ˇ»;"#,
7366 );
7367
7368 // noop for multiple selections with different contents
7369 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7370 .unwrap();
7371 cx.assert_editor_state(
7372 r#"let foo = 2;
7373«letˇ» foo = 2;
7374let «fooˇ» = 2;
7375let foo = 2;
7376let foo = «2ˇ»;"#,
7377 );
7378
7379 // Test last selection direction should be preserved
7380 cx.set_state(
7381 r#"let foo = 2;
7382let foo = 2;
7383let «fooˇ» = 2;
7384let «ˇfoo» = 2;
7385let foo = 2;"#,
7386 );
7387
7388 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7389 .unwrap();
7390 cx.assert_editor_state(
7391 r#"let foo = 2;
7392let foo = 2;
7393let «fooˇ» = 2;
7394let «ˇfoo» = 2;
7395let «ˇfoo» = 2;"#,
7396 );
7397}
7398
7399#[gpui::test]
7400async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
7401 init_test(cx, |_| {});
7402
7403 let mut cx =
7404 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
7405
7406 cx.assert_editor_state(indoc! {"
7407 ˇbbb
7408 ccc
7409
7410 bbb
7411 ccc
7412 "});
7413 cx.dispatch_action(SelectPrevious::default());
7414 cx.assert_editor_state(indoc! {"
7415 «bbbˇ»
7416 ccc
7417
7418 bbb
7419 ccc
7420 "});
7421 cx.dispatch_action(SelectPrevious::default());
7422 cx.assert_editor_state(indoc! {"
7423 «bbbˇ»
7424 ccc
7425
7426 «bbbˇ»
7427 ccc
7428 "});
7429}
7430
7431#[gpui::test]
7432async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
7433 init_test(cx, |_| {});
7434
7435 let mut cx = EditorTestContext::new(cx).await;
7436 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7437
7438 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7439 .unwrap();
7440 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7441
7442 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7443 .unwrap();
7444 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7445
7446 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7447 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7448
7449 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7450 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7451
7452 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7453 .unwrap();
7454 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
7455
7456 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7457 .unwrap();
7458 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7459}
7460
7461#[gpui::test]
7462async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
7463 init_test(cx, |_| {});
7464
7465 let mut cx = EditorTestContext::new(cx).await;
7466 cx.set_state("aˇ");
7467
7468 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7469 .unwrap();
7470 cx.assert_editor_state("«aˇ»");
7471 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7472 .unwrap();
7473 cx.assert_editor_state("«aˇ»");
7474}
7475
7476#[gpui::test]
7477async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
7478 init_test(cx, |_| {});
7479
7480 let mut cx = EditorTestContext::new(cx).await;
7481 cx.set_state(
7482 r#"let foo = 2;
7483lˇet foo = 2;
7484let fooˇ = 2;
7485let foo = 2;
7486let foo = ˇ2;"#,
7487 );
7488
7489 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7490 .unwrap();
7491 cx.assert_editor_state(
7492 r#"let foo = 2;
7493«letˇ» foo = 2;
7494let «fooˇ» = 2;
7495let foo = 2;
7496let foo = «2ˇ»;"#,
7497 );
7498
7499 // noop for multiple selections with different contents
7500 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7501 .unwrap();
7502 cx.assert_editor_state(
7503 r#"let foo = 2;
7504«letˇ» foo = 2;
7505let «fooˇ» = 2;
7506let foo = 2;
7507let foo = «2ˇ»;"#,
7508 );
7509}
7510
7511#[gpui::test]
7512async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
7513 init_test(cx, |_| {});
7514
7515 let mut cx = EditorTestContext::new(cx).await;
7516 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7517
7518 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7519 .unwrap();
7520 // selection direction is preserved
7521 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7522
7523 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7524 .unwrap();
7525 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7526
7527 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7528 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7529
7530 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7531 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7532
7533 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7534 .unwrap();
7535 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
7536
7537 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7538 .unwrap();
7539 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
7540}
7541
7542#[gpui::test]
7543async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
7544 init_test(cx, |_| {});
7545
7546 let language = Arc::new(Language::new(
7547 LanguageConfig::default(),
7548 Some(tree_sitter_rust::LANGUAGE.into()),
7549 ));
7550
7551 let text = r#"
7552 use mod1::mod2::{mod3, mod4};
7553
7554 fn fn_1(param1: bool, param2: &str) {
7555 let var1 = "text";
7556 }
7557 "#
7558 .unindent();
7559
7560 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7561 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7562 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7563
7564 editor
7565 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7566 .await;
7567
7568 editor.update_in(cx, |editor, window, cx| {
7569 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7570 s.select_display_ranges([
7571 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
7572 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
7573 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
7574 ]);
7575 });
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 editor.update(cx, |editor, cx| {
7596 assert_text_with_selections(
7597 editor,
7598 indoc! {r#"
7599 use mod1::mod2::«{mod3, mod4}ˇ»;
7600
7601 «ˇfn fn_1(param1: bool, param2: &str) {
7602 let var1 = "text";
7603 }»
7604 "#},
7605 cx,
7606 );
7607 });
7608
7609 editor.update_in(cx, |editor, window, cx| {
7610 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7611 });
7612 assert_eq!(
7613 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7614 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7615 );
7616
7617 // Trying to expand the selected syntax node one more time has no effect.
7618 editor.update_in(cx, |editor, window, cx| {
7619 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7620 });
7621 assert_eq!(
7622 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7623 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
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, «mod4ˇ»};
7651
7652 fn fn_1«ˇ(param1: bool, param2: &str)» {
7653 let var1 = "«ˇtext»";
7654 }
7655 "#},
7656 cx,
7657 );
7658 });
7659
7660 editor.update_in(cx, |editor, window, cx| {
7661 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7662 });
7663 editor.update(cx, |editor, cx| {
7664 assert_text_with_selections(
7665 editor,
7666 indoc! {r#"
7667 use mod1::mod2::{mod3, mo«ˇ»d4};
7668
7669 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7670 let var1 = "te«ˇ»xt";
7671 }
7672 "#},
7673 cx,
7674 );
7675 });
7676
7677 // Trying to shrink the selected syntax node one more time has no effect.
7678 editor.update_in(cx, |editor, window, cx| {
7679 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7680 });
7681 editor.update_in(cx, |editor, _, cx| {
7682 assert_text_with_selections(
7683 editor,
7684 indoc! {r#"
7685 use mod1::mod2::{mod3, mo«ˇ»d4};
7686
7687 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7688 let var1 = "te«ˇ»xt";
7689 }
7690 "#},
7691 cx,
7692 );
7693 });
7694
7695 // Ensure that we keep expanding the selection if the larger selection starts or ends within
7696 // a fold.
7697 editor.update_in(cx, |editor, window, cx| {
7698 editor.fold_creases(
7699 vec![
7700 Crease::simple(
7701 Point::new(0, 21)..Point::new(0, 24),
7702 FoldPlaceholder::test(),
7703 ),
7704 Crease::simple(
7705 Point::new(3, 20)..Point::new(3, 22),
7706 FoldPlaceholder::test(),
7707 ),
7708 ],
7709 true,
7710 window,
7711 cx,
7712 );
7713 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7714 });
7715 editor.update(cx, |editor, cx| {
7716 assert_text_with_selections(
7717 editor,
7718 indoc! {r#"
7719 use mod1::mod2::«{mod3, mod4}ˇ»;
7720
7721 fn fn_1«ˇ(param1: bool, param2: &str)» {
7722 let var1 = "«ˇtext»";
7723 }
7724 "#},
7725 cx,
7726 );
7727 });
7728}
7729
7730#[gpui::test]
7731async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
7732 init_test(cx, |_| {});
7733
7734 let language = Arc::new(Language::new(
7735 LanguageConfig::default(),
7736 Some(tree_sitter_rust::LANGUAGE.into()),
7737 ));
7738
7739 let text = "let a = 2;";
7740
7741 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7742 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7743 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7744
7745 editor
7746 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7747 .await;
7748
7749 // Test case 1: Cursor at end of word
7750 editor.update_in(cx, |editor, window, cx| {
7751 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7752 s.select_display_ranges([
7753 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
7754 ]);
7755 });
7756 });
7757 editor.update(cx, |editor, cx| {
7758 assert_text_with_selections(editor, "let aˇ = 2;", cx);
7759 });
7760 editor.update_in(cx, |editor, window, cx| {
7761 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7762 });
7763 editor.update(cx, |editor, cx| {
7764 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
7765 });
7766 editor.update_in(cx, |editor, window, cx| {
7767 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7768 });
7769 editor.update(cx, |editor, cx| {
7770 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7771 });
7772
7773 // Test case 2: Cursor at end of statement
7774 editor.update_in(cx, |editor, window, cx| {
7775 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7776 s.select_display_ranges([
7777 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
7778 ]);
7779 });
7780 });
7781 editor.update(cx, |editor, cx| {
7782 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
7783 });
7784 editor.update_in(cx, |editor, window, cx| {
7785 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7786 });
7787 editor.update(cx, |editor, cx| {
7788 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7789 });
7790}
7791
7792#[gpui::test]
7793async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
7794 init_test(cx, |_| {});
7795
7796 let language = Arc::new(Language::new(
7797 LanguageConfig::default(),
7798 Some(tree_sitter_rust::LANGUAGE.into()),
7799 ));
7800
7801 let text = r#"
7802 use mod1::mod2::{mod3, mod4};
7803
7804 fn fn_1(param1: bool, param2: &str) {
7805 let var1 = "hello world";
7806 }
7807 "#
7808 .unindent();
7809
7810 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7811 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7812 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7813
7814 editor
7815 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7816 .await;
7817
7818 // Test 1: Cursor on a letter of a string word
7819 editor.update_in(cx, |editor, window, cx| {
7820 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7821 s.select_display_ranges([
7822 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
7823 ]);
7824 });
7825 });
7826 editor.update_in(cx, |editor, window, cx| {
7827 assert_text_with_selections(
7828 editor,
7829 indoc! {r#"
7830 use mod1::mod2::{mod3, mod4};
7831
7832 fn fn_1(param1: bool, param2: &str) {
7833 let var1 = "hˇello world";
7834 }
7835 "#},
7836 cx,
7837 );
7838 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7839 assert_text_with_selections(
7840 editor,
7841 indoc! {r#"
7842 use mod1::mod2::{mod3, mod4};
7843
7844 fn fn_1(param1: bool, param2: &str) {
7845 let var1 = "«ˇhello» world";
7846 }
7847 "#},
7848 cx,
7849 );
7850 });
7851
7852 // Test 2: Partial selection within a word
7853 editor.update_in(cx, |editor, window, cx| {
7854 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7855 s.select_display_ranges([
7856 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7857 ]);
7858 });
7859 });
7860 editor.update_in(cx, |editor, window, cx| {
7861 assert_text_with_selections(
7862 editor,
7863 indoc! {r#"
7864 use mod1::mod2::{mod3, mod4};
7865
7866 fn fn_1(param1: bool, param2: &str) {
7867 let var1 = "h«elˇ»lo world";
7868 }
7869 "#},
7870 cx,
7871 );
7872 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7873 assert_text_with_selections(
7874 editor,
7875 indoc! {r#"
7876 use mod1::mod2::{mod3, mod4};
7877
7878 fn fn_1(param1: bool, param2: &str) {
7879 let var1 = "«ˇhello» world";
7880 }
7881 "#},
7882 cx,
7883 );
7884 });
7885
7886 // Test 3: Complete word already selected
7887 editor.update_in(cx, |editor, window, cx| {
7888 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7889 s.select_display_ranges([
7890 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7891 ]);
7892 });
7893 });
7894 editor.update_in(cx, |editor, window, cx| {
7895 assert_text_with_selections(
7896 editor,
7897 indoc! {r#"
7898 use mod1::mod2::{mod3, mod4};
7899
7900 fn fn_1(param1: bool, param2: &str) {
7901 let var1 = "«helloˇ» world";
7902 }
7903 "#},
7904 cx,
7905 );
7906 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7907 assert_text_with_selections(
7908 editor,
7909 indoc! {r#"
7910 use mod1::mod2::{mod3, mod4};
7911
7912 fn fn_1(param1: bool, param2: &str) {
7913 let var1 = "«hello worldˇ»";
7914 }
7915 "#},
7916 cx,
7917 );
7918 });
7919
7920 // Test 4: Selection spanning across words
7921 editor.update_in(cx, |editor, window, cx| {
7922 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7923 s.select_display_ranges([
7924 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7925 ]);
7926 });
7927 });
7928 editor.update_in(cx, |editor, window, cx| {
7929 assert_text_with_selections(
7930 editor,
7931 indoc! {r#"
7932 use mod1::mod2::{mod3, mod4};
7933
7934 fn fn_1(param1: bool, param2: &str) {
7935 let var1 = "hel«lo woˇ»rld";
7936 }
7937 "#},
7938 cx,
7939 );
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 // Test 5: Expansion beyond string
7955 editor.update_in(cx, |editor, window, cx| {
7956 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7957 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7958 assert_text_with_selections(
7959 editor,
7960 indoc! {r#"
7961 use mod1::mod2::{mod3, mod4};
7962
7963 fn fn_1(param1: bool, param2: &str) {
7964 «ˇlet var1 = "hello world";»
7965 }
7966 "#},
7967 cx,
7968 );
7969 });
7970}
7971
7972#[gpui::test]
7973async fn test_fold_function_bodies(cx: &mut TestAppContext) {
7974 init_test(cx, |_| {});
7975
7976 let base_text = r#"
7977 impl A {
7978 // this is an uncommitted comment
7979
7980 fn b() {
7981 c();
7982 }
7983
7984 // this is another uncommitted comment
7985
7986 fn d() {
7987 // e
7988 // f
7989 }
7990 }
7991
7992 fn g() {
7993 // h
7994 }
7995 "#
7996 .unindent();
7997
7998 let text = r#"
7999 ˇimpl A {
8000
8001 fn b() {
8002 c();
8003 }
8004
8005 fn d() {
8006 // e
8007 // f
8008 }
8009 }
8010
8011 fn g() {
8012 // h
8013 }
8014 "#
8015 .unindent();
8016
8017 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8018 cx.set_state(&text);
8019 cx.set_head_text(&base_text);
8020 cx.update_editor(|editor, window, cx| {
8021 editor.expand_all_diff_hunks(&Default::default(), window, cx);
8022 });
8023
8024 cx.assert_state_with_diff(
8025 "
8026 ˇimpl A {
8027 - // this is an uncommitted comment
8028
8029 fn b() {
8030 c();
8031 }
8032
8033 - // this is another uncommitted comment
8034 -
8035 fn d() {
8036 // e
8037 // f
8038 }
8039 }
8040
8041 fn g() {
8042 // h
8043 }
8044 "
8045 .unindent(),
8046 );
8047
8048 let expected_display_text = "
8049 impl A {
8050 // this is an uncommitted comment
8051
8052 fn b() {
8053 ⋯
8054 }
8055
8056 // this is another uncommitted comment
8057
8058 fn d() {
8059 ⋯
8060 }
8061 }
8062
8063 fn g() {
8064 ⋯
8065 }
8066 "
8067 .unindent();
8068
8069 cx.update_editor(|editor, window, cx| {
8070 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
8071 assert_eq!(editor.display_text(cx), expected_display_text);
8072 });
8073}
8074
8075#[gpui::test]
8076async fn test_autoindent(cx: &mut TestAppContext) {
8077 init_test(cx, |_| {});
8078
8079 let language = Arc::new(
8080 Language::new(
8081 LanguageConfig {
8082 brackets: BracketPairConfig {
8083 pairs: vec![
8084 BracketPair {
8085 start: "{".to_string(),
8086 end: "}".to_string(),
8087 close: false,
8088 surround: false,
8089 newline: true,
8090 },
8091 BracketPair {
8092 start: "(".to_string(),
8093 end: ")".to_string(),
8094 close: false,
8095 surround: false,
8096 newline: true,
8097 },
8098 ],
8099 ..Default::default()
8100 },
8101 ..Default::default()
8102 },
8103 Some(tree_sitter_rust::LANGUAGE.into()),
8104 )
8105 .with_indents_query(
8106 r#"
8107 (_ "(" ")" @end) @indent
8108 (_ "{" "}" @end) @indent
8109 "#,
8110 )
8111 .unwrap(),
8112 );
8113
8114 let text = "fn a() {}";
8115
8116 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8117 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8118 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8119 editor
8120 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8121 .await;
8122
8123 editor.update_in(cx, |editor, window, cx| {
8124 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8125 s.select_ranges([5..5, 8..8, 9..9])
8126 });
8127 editor.newline(&Newline, window, cx);
8128 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
8129 assert_eq!(
8130 editor.selections.ranges(cx),
8131 &[
8132 Point::new(1, 4)..Point::new(1, 4),
8133 Point::new(3, 4)..Point::new(3, 4),
8134 Point::new(5, 0)..Point::new(5, 0)
8135 ]
8136 );
8137 });
8138}
8139
8140#[gpui::test]
8141async fn test_autoindent_selections(cx: &mut TestAppContext) {
8142 init_test(cx, |_| {});
8143
8144 {
8145 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8146 cx.set_state(indoc! {"
8147 impl A {
8148
8149 fn b() {}
8150
8151 «fn c() {
8152
8153 }ˇ»
8154 }
8155 "});
8156
8157 cx.update_editor(|editor, window, cx| {
8158 editor.autoindent(&Default::default(), window, cx);
8159 });
8160
8161 cx.assert_editor_state(indoc! {"
8162 impl A {
8163
8164 fn b() {}
8165
8166 «fn c() {
8167
8168 }ˇ»
8169 }
8170 "});
8171 }
8172
8173 {
8174 let mut cx = EditorTestContext::new_multibuffer(
8175 cx,
8176 [indoc! { "
8177 impl A {
8178 «
8179 // a
8180 fn b(){}
8181 »
8182 «
8183 }
8184 fn c(){}
8185 »
8186 "}],
8187 );
8188
8189 let buffer = cx.update_editor(|editor, _, cx| {
8190 let buffer = editor.buffer().update(cx, |buffer, _| {
8191 buffer.all_buffers().iter().next().unwrap().clone()
8192 });
8193 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
8194 buffer
8195 });
8196
8197 cx.run_until_parked();
8198 cx.update_editor(|editor, window, cx| {
8199 editor.select_all(&Default::default(), window, cx);
8200 editor.autoindent(&Default::default(), window, cx)
8201 });
8202 cx.run_until_parked();
8203
8204 cx.update(|_, cx| {
8205 assert_eq!(
8206 buffer.read(cx).text(),
8207 indoc! { "
8208 impl A {
8209
8210 // a
8211 fn b(){}
8212
8213
8214 }
8215 fn c(){}
8216
8217 " }
8218 )
8219 });
8220 }
8221}
8222
8223#[gpui::test]
8224async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
8225 init_test(cx, |_| {});
8226
8227 let mut cx = EditorTestContext::new(cx).await;
8228
8229 let language = Arc::new(Language::new(
8230 LanguageConfig {
8231 brackets: BracketPairConfig {
8232 pairs: vec![
8233 BracketPair {
8234 start: "{".to_string(),
8235 end: "}".to_string(),
8236 close: true,
8237 surround: true,
8238 newline: true,
8239 },
8240 BracketPair {
8241 start: "(".to_string(),
8242 end: ")".to_string(),
8243 close: true,
8244 surround: true,
8245 newline: true,
8246 },
8247 BracketPair {
8248 start: "/*".to_string(),
8249 end: " */".to_string(),
8250 close: true,
8251 surround: true,
8252 newline: true,
8253 },
8254 BracketPair {
8255 start: "[".to_string(),
8256 end: "]".to_string(),
8257 close: false,
8258 surround: false,
8259 newline: true,
8260 },
8261 BracketPair {
8262 start: "\"".to_string(),
8263 end: "\"".to_string(),
8264 close: true,
8265 surround: true,
8266 newline: false,
8267 },
8268 BracketPair {
8269 start: "<".to_string(),
8270 end: ">".to_string(),
8271 close: false,
8272 surround: true,
8273 newline: true,
8274 },
8275 ],
8276 ..Default::default()
8277 },
8278 autoclose_before: "})]".to_string(),
8279 ..Default::default()
8280 },
8281 Some(tree_sitter_rust::LANGUAGE.into()),
8282 ));
8283
8284 cx.language_registry().add(language.clone());
8285 cx.update_buffer(|buffer, cx| {
8286 buffer.set_language(Some(language), cx);
8287 });
8288
8289 cx.set_state(
8290 &r#"
8291 🏀ˇ
8292 εˇ
8293 ❤️ˇ
8294 "#
8295 .unindent(),
8296 );
8297
8298 // autoclose multiple nested brackets at multiple cursors
8299 cx.update_editor(|editor, window, cx| {
8300 editor.handle_input("{", window, cx);
8301 editor.handle_input("{", window, cx);
8302 editor.handle_input("{", window, cx);
8303 });
8304 cx.assert_editor_state(
8305 &"
8306 🏀{{{ˇ}}}
8307 ε{{{ˇ}}}
8308 ❤️{{{ˇ}}}
8309 "
8310 .unindent(),
8311 );
8312
8313 // insert a different closing bracket
8314 cx.update_editor(|editor, window, cx| {
8315 editor.handle_input(")", window, cx);
8316 });
8317 cx.assert_editor_state(
8318 &"
8319 🏀{{{)ˇ}}}
8320 ε{{{)ˇ}}}
8321 ❤️{{{)ˇ}}}
8322 "
8323 .unindent(),
8324 );
8325
8326 // skip over the auto-closed brackets when typing a closing bracket
8327 cx.update_editor(|editor, window, cx| {
8328 editor.move_right(&MoveRight, window, cx);
8329 editor.handle_input("}", window, cx);
8330 editor.handle_input("}", window, cx);
8331 editor.handle_input("}", window, cx);
8332 });
8333 cx.assert_editor_state(
8334 &"
8335 🏀{{{)}}}}ˇ
8336 ε{{{)}}}}ˇ
8337 ❤️{{{)}}}}ˇ
8338 "
8339 .unindent(),
8340 );
8341
8342 // autoclose multi-character pairs
8343 cx.set_state(
8344 &"
8345 ˇ
8346 ˇ
8347 "
8348 .unindent(),
8349 );
8350 cx.update_editor(|editor, window, cx| {
8351 editor.handle_input("/", window, cx);
8352 editor.handle_input("*", window, cx);
8353 });
8354 cx.assert_editor_state(
8355 &"
8356 /*ˇ */
8357 /*ˇ */
8358 "
8359 .unindent(),
8360 );
8361
8362 // one cursor autocloses a multi-character pair, one cursor
8363 // does not autoclose.
8364 cx.set_state(
8365 &"
8366 /ˇ
8367 ˇ
8368 "
8369 .unindent(),
8370 );
8371 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
8372 cx.assert_editor_state(
8373 &"
8374 /*ˇ */
8375 *ˇ
8376 "
8377 .unindent(),
8378 );
8379
8380 // Don't autoclose if the next character isn't whitespace and isn't
8381 // listed in the language's "autoclose_before" section.
8382 cx.set_state("ˇa b");
8383 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8384 cx.assert_editor_state("{ˇa b");
8385
8386 // Don't autoclose if `close` is false for the bracket pair
8387 cx.set_state("ˇ");
8388 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
8389 cx.assert_editor_state("[ˇ");
8390
8391 // Surround with brackets if text is selected
8392 cx.set_state("«aˇ» b");
8393 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8394 cx.assert_editor_state("{«aˇ»} b");
8395
8396 // Autoclose when not immediately after a word character
8397 cx.set_state("a ˇ");
8398 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8399 cx.assert_editor_state("a \"ˇ\"");
8400
8401 // Autoclose pair where the start and end characters are the same
8402 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8403 cx.assert_editor_state("a \"\"ˇ");
8404
8405 // Don't autoclose when immediately after a word character
8406 cx.set_state("aˇ");
8407 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8408 cx.assert_editor_state("a\"ˇ");
8409
8410 // Do autoclose when after a non-word character
8411 cx.set_state("{ˇ");
8412 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8413 cx.assert_editor_state("{\"ˇ\"");
8414
8415 // Non identical pairs autoclose regardless of preceding character
8416 cx.set_state("aˇ");
8417 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8418 cx.assert_editor_state("a{ˇ}");
8419
8420 // Don't autoclose pair if autoclose is disabled
8421 cx.set_state("ˇ");
8422 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8423 cx.assert_editor_state("<ˇ");
8424
8425 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
8426 cx.set_state("«aˇ» b");
8427 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8428 cx.assert_editor_state("<«aˇ»> b");
8429}
8430
8431#[gpui::test]
8432async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
8433 init_test(cx, |settings| {
8434 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8435 });
8436
8437 let mut cx = EditorTestContext::new(cx).await;
8438
8439 let language = Arc::new(Language::new(
8440 LanguageConfig {
8441 brackets: BracketPairConfig {
8442 pairs: vec![
8443 BracketPair {
8444 start: "{".to_string(),
8445 end: "}".to_string(),
8446 close: true,
8447 surround: true,
8448 newline: true,
8449 },
8450 BracketPair {
8451 start: "(".to_string(),
8452 end: ")".to_string(),
8453 close: true,
8454 surround: true,
8455 newline: true,
8456 },
8457 BracketPair {
8458 start: "[".to_string(),
8459 end: "]".to_string(),
8460 close: false,
8461 surround: false,
8462 newline: true,
8463 },
8464 ],
8465 ..Default::default()
8466 },
8467 autoclose_before: "})]".to_string(),
8468 ..Default::default()
8469 },
8470 Some(tree_sitter_rust::LANGUAGE.into()),
8471 ));
8472
8473 cx.language_registry().add(language.clone());
8474 cx.update_buffer(|buffer, cx| {
8475 buffer.set_language(Some(language), cx);
8476 });
8477
8478 cx.set_state(
8479 &"
8480 ˇ
8481 ˇ
8482 ˇ
8483 "
8484 .unindent(),
8485 );
8486
8487 // ensure only matching closing brackets are skipped over
8488 cx.update_editor(|editor, window, cx| {
8489 editor.handle_input("}", window, cx);
8490 editor.move_left(&MoveLeft, window, cx);
8491 editor.handle_input(")", window, cx);
8492 editor.move_left(&MoveLeft, window, cx);
8493 });
8494 cx.assert_editor_state(
8495 &"
8496 ˇ)}
8497 ˇ)}
8498 ˇ)}
8499 "
8500 .unindent(),
8501 );
8502
8503 // skip-over closing brackets at multiple cursors
8504 cx.update_editor(|editor, window, cx| {
8505 editor.handle_input(")", window, cx);
8506 editor.handle_input("}", window, cx);
8507 });
8508 cx.assert_editor_state(
8509 &"
8510 )}ˇ
8511 )}ˇ
8512 )}ˇ
8513 "
8514 .unindent(),
8515 );
8516
8517 // ignore non-close brackets
8518 cx.update_editor(|editor, window, cx| {
8519 editor.handle_input("]", window, cx);
8520 editor.move_left(&MoveLeft, window, cx);
8521 editor.handle_input("]", window, cx);
8522 });
8523 cx.assert_editor_state(
8524 &"
8525 )}]ˇ]
8526 )}]ˇ]
8527 )}]ˇ]
8528 "
8529 .unindent(),
8530 );
8531}
8532
8533#[gpui::test]
8534async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8535 init_test(cx, |_| {});
8536
8537 let mut cx = EditorTestContext::new(cx).await;
8538
8539 let html_language = Arc::new(
8540 Language::new(
8541 LanguageConfig {
8542 name: "HTML".into(),
8543 brackets: BracketPairConfig {
8544 pairs: vec![
8545 BracketPair {
8546 start: "<".into(),
8547 end: ">".into(),
8548 close: true,
8549 ..Default::default()
8550 },
8551 BracketPair {
8552 start: "{".into(),
8553 end: "}".into(),
8554 close: true,
8555 ..Default::default()
8556 },
8557 BracketPair {
8558 start: "(".into(),
8559 end: ")".into(),
8560 close: true,
8561 ..Default::default()
8562 },
8563 ],
8564 ..Default::default()
8565 },
8566 autoclose_before: "})]>".into(),
8567 ..Default::default()
8568 },
8569 Some(tree_sitter_html::LANGUAGE.into()),
8570 )
8571 .with_injection_query(
8572 r#"
8573 (script_element
8574 (raw_text) @injection.content
8575 (#set! injection.language "javascript"))
8576 "#,
8577 )
8578 .unwrap(),
8579 );
8580
8581 let javascript_language = Arc::new(Language::new(
8582 LanguageConfig {
8583 name: "JavaScript".into(),
8584 brackets: BracketPairConfig {
8585 pairs: vec![
8586 BracketPair {
8587 start: "/*".into(),
8588 end: " */".into(),
8589 close: true,
8590 ..Default::default()
8591 },
8592 BracketPair {
8593 start: "{".into(),
8594 end: "}".into(),
8595 close: true,
8596 ..Default::default()
8597 },
8598 BracketPair {
8599 start: "(".into(),
8600 end: ")".into(),
8601 close: true,
8602 ..Default::default()
8603 },
8604 ],
8605 ..Default::default()
8606 },
8607 autoclose_before: "})]>".into(),
8608 ..Default::default()
8609 },
8610 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8611 ));
8612
8613 cx.language_registry().add(html_language.clone());
8614 cx.language_registry().add(javascript_language.clone());
8615
8616 cx.update_buffer(|buffer, cx| {
8617 buffer.set_language(Some(html_language), cx);
8618 });
8619
8620 cx.set_state(
8621 &r#"
8622 <body>ˇ
8623 <script>
8624 var x = 1;ˇ
8625 </script>
8626 </body>ˇ
8627 "#
8628 .unindent(),
8629 );
8630
8631 // Precondition: different languages are active at different locations.
8632 cx.update_editor(|editor, window, cx| {
8633 let snapshot = editor.snapshot(window, cx);
8634 let cursors = editor.selections.ranges::<usize>(cx);
8635 let languages = cursors
8636 .iter()
8637 .map(|c| snapshot.language_at(c.start).unwrap().name())
8638 .collect::<Vec<_>>();
8639 assert_eq!(
8640 languages,
8641 &["HTML".into(), "JavaScript".into(), "HTML".into()]
8642 );
8643 });
8644
8645 // Angle brackets autoclose in HTML, but not JavaScript.
8646 cx.update_editor(|editor, window, cx| {
8647 editor.handle_input("<", window, cx);
8648 editor.handle_input("a", window, cx);
8649 });
8650 cx.assert_editor_state(
8651 &r#"
8652 <body><aˇ>
8653 <script>
8654 var x = 1;<aˇ
8655 </script>
8656 </body><aˇ>
8657 "#
8658 .unindent(),
8659 );
8660
8661 // Curly braces and parens autoclose in both HTML and JavaScript.
8662 cx.update_editor(|editor, window, cx| {
8663 editor.handle_input(" b=", window, cx);
8664 editor.handle_input("{", window, cx);
8665 editor.handle_input("c", window, cx);
8666 editor.handle_input("(", window, cx);
8667 });
8668 cx.assert_editor_state(
8669 &r#"
8670 <body><a b={c(ˇ)}>
8671 <script>
8672 var x = 1;<a b={c(ˇ)}
8673 </script>
8674 </body><a b={c(ˇ)}>
8675 "#
8676 .unindent(),
8677 );
8678
8679 // Brackets that were already autoclosed are skipped.
8680 cx.update_editor(|editor, window, cx| {
8681 editor.handle_input(")", window, cx);
8682 editor.handle_input("d", window, cx);
8683 editor.handle_input("}", window, cx);
8684 });
8685 cx.assert_editor_state(
8686 &r#"
8687 <body><a b={c()d}ˇ>
8688 <script>
8689 var x = 1;<a b={c()d}ˇ
8690 </script>
8691 </body><a b={c()d}ˇ>
8692 "#
8693 .unindent(),
8694 );
8695 cx.update_editor(|editor, window, cx| {
8696 editor.handle_input(">", window, cx);
8697 });
8698 cx.assert_editor_state(
8699 &r#"
8700 <body><a b={c()d}>ˇ
8701 <script>
8702 var x = 1;<a b={c()d}>ˇ
8703 </script>
8704 </body><a b={c()d}>ˇ
8705 "#
8706 .unindent(),
8707 );
8708
8709 // Reset
8710 cx.set_state(
8711 &r#"
8712 <body>ˇ
8713 <script>
8714 var x = 1;ˇ
8715 </script>
8716 </body>ˇ
8717 "#
8718 .unindent(),
8719 );
8720
8721 cx.update_editor(|editor, window, cx| {
8722 editor.handle_input("<", window, cx);
8723 });
8724 cx.assert_editor_state(
8725 &r#"
8726 <body><ˇ>
8727 <script>
8728 var x = 1;<ˇ
8729 </script>
8730 </body><ˇ>
8731 "#
8732 .unindent(),
8733 );
8734
8735 // When backspacing, the closing angle brackets are removed.
8736 cx.update_editor(|editor, window, cx| {
8737 editor.backspace(&Backspace, window, cx);
8738 });
8739 cx.assert_editor_state(
8740 &r#"
8741 <body>ˇ
8742 <script>
8743 var x = 1;ˇ
8744 </script>
8745 </body>ˇ
8746 "#
8747 .unindent(),
8748 );
8749
8750 // Block comments autoclose in JavaScript, but not HTML.
8751 cx.update_editor(|editor, window, cx| {
8752 editor.handle_input("/", window, cx);
8753 editor.handle_input("*", window, cx);
8754 });
8755 cx.assert_editor_state(
8756 &r#"
8757 <body>/*ˇ
8758 <script>
8759 var x = 1;/*ˇ */
8760 </script>
8761 </body>/*ˇ
8762 "#
8763 .unindent(),
8764 );
8765}
8766
8767#[gpui::test]
8768async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
8769 init_test(cx, |_| {});
8770
8771 let mut cx = EditorTestContext::new(cx).await;
8772
8773 let rust_language = Arc::new(
8774 Language::new(
8775 LanguageConfig {
8776 name: "Rust".into(),
8777 brackets: serde_json::from_value(json!([
8778 { "start": "{", "end": "}", "close": true, "newline": true },
8779 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
8780 ]))
8781 .unwrap(),
8782 autoclose_before: "})]>".into(),
8783 ..Default::default()
8784 },
8785 Some(tree_sitter_rust::LANGUAGE.into()),
8786 )
8787 .with_override_query("(string_literal) @string")
8788 .unwrap(),
8789 );
8790
8791 cx.language_registry().add(rust_language.clone());
8792 cx.update_buffer(|buffer, cx| {
8793 buffer.set_language(Some(rust_language), cx);
8794 });
8795
8796 cx.set_state(
8797 &r#"
8798 let x = ˇ
8799 "#
8800 .unindent(),
8801 );
8802
8803 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
8804 cx.update_editor(|editor, window, cx| {
8805 editor.handle_input("\"", window, cx);
8806 });
8807 cx.assert_editor_state(
8808 &r#"
8809 let x = "ˇ"
8810 "#
8811 .unindent(),
8812 );
8813
8814 // Inserting another quotation mark. The cursor moves across the existing
8815 // automatically-inserted quotation mark.
8816 cx.update_editor(|editor, window, cx| {
8817 editor.handle_input("\"", window, cx);
8818 });
8819 cx.assert_editor_state(
8820 &r#"
8821 let x = ""ˇ
8822 "#
8823 .unindent(),
8824 );
8825
8826 // Reset
8827 cx.set_state(
8828 &r#"
8829 let x = ˇ
8830 "#
8831 .unindent(),
8832 );
8833
8834 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
8835 cx.update_editor(|editor, window, cx| {
8836 editor.handle_input("\"", window, cx);
8837 editor.handle_input(" ", window, cx);
8838 editor.move_left(&Default::default(), window, cx);
8839 editor.handle_input("\\", window, cx);
8840 editor.handle_input("\"", window, cx);
8841 });
8842 cx.assert_editor_state(
8843 &r#"
8844 let x = "\"ˇ "
8845 "#
8846 .unindent(),
8847 );
8848
8849 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8850 // mark. Nothing is inserted.
8851 cx.update_editor(|editor, window, cx| {
8852 editor.move_right(&Default::default(), window, cx);
8853 editor.handle_input("\"", window, cx);
8854 });
8855 cx.assert_editor_state(
8856 &r#"
8857 let x = "\" "ˇ
8858 "#
8859 .unindent(),
8860 );
8861}
8862
8863#[gpui::test]
8864async fn test_surround_with_pair(cx: &mut TestAppContext) {
8865 init_test(cx, |_| {});
8866
8867 let language = Arc::new(Language::new(
8868 LanguageConfig {
8869 brackets: BracketPairConfig {
8870 pairs: vec![
8871 BracketPair {
8872 start: "{".to_string(),
8873 end: "}".to_string(),
8874 close: true,
8875 surround: true,
8876 newline: true,
8877 },
8878 BracketPair {
8879 start: "/* ".to_string(),
8880 end: "*/".to_string(),
8881 close: true,
8882 surround: true,
8883 ..Default::default()
8884 },
8885 ],
8886 ..Default::default()
8887 },
8888 ..Default::default()
8889 },
8890 Some(tree_sitter_rust::LANGUAGE.into()),
8891 ));
8892
8893 let text = r#"
8894 a
8895 b
8896 c
8897 "#
8898 .unindent();
8899
8900 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8901 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8902 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8903 editor
8904 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8905 .await;
8906
8907 editor.update_in(cx, |editor, window, cx| {
8908 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8909 s.select_display_ranges([
8910 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8911 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8912 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8913 ])
8914 });
8915
8916 editor.handle_input("{", window, cx);
8917 editor.handle_input("{", window, cx);
8918 editor.handle_input("{", window, cx);
8919 assert_eq!(
8920 editor.text(cx),
8921 "
8922 {{{a}}}
8923 {{{b}}}
8924 {{{c}}}
8925 "
8926 .unindent()
8927 );
8928 assert_eq!(
8929 editor.selections.display_ranges(cx),
8930 [
8931 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
8932 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
8933 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
8934 ]
8935 );
8936
8937 editor.undo(&Undo, window, cx);
8938 editor.undo(&Undo, window, cx);
8939 editor.undo(&Undo, window, cx);
8940 assert_eq!(
8941 editor.text(cx),
8942 "
8943 a
8944 b
8945 c
8946 "
8947 .unindent()
8948 );
8949 assert_eq!(
8950 editor.selections.display_ranges(cx),
8951 [
8952 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8953 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8954 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8955 ]
8956 );
8957
8958 // Ensure inserting the first character of a multi-byte bracket pair
8959 // doesn't surround the selections with the bracket.
8960 editor.handle_input("/", window, cx);
8961 assert_eq!(
8962 editor.text(cx),
8963 "
8964 /
8965 /
8966 /
8967 "
8968 .unindent()
8969 );
8970 assert_eq!(
8971 editor.selections.display_ranges(cx),
8972 [
8973 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8974 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8975 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8976 ]
8977 );
8978
8979 editor.undo(&Undo, window, cx);
8980 assert_eq!(
8981 editor.text(cx),
8982 "
8983 a
8984 b
8985 c
8986 "
8987 .unindent()
8988 );
8989 assert_eq!(
8990 editor.selections.display_ranges(cx),
8991 [
8992 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8993 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8994 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8995 ]
8996 );
8997
8998 // Ensure inserting the last character of a multi-byte bracket pair
8999 // doesn't surround the selections with the bracket.
9000 editor.handle_input("*", window, cx);
9001 assert_eq!(
9002 editor.text(cx),
9003 "
9004 *
9005 *
9006 *
9007 "
9008 .unindent()
9009 );
9010 assert_eq!(
9011 editor.selections.display_ranges(cx),
9012 [
9013 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
9014 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
9015 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
9016 ]
9017 );
9018 });
9019}
9020
9021#[gpui::test]
9022async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
9023 init_test(cx, |_| {});
9024
9025 let language = Arc::new(Language::new(
9026 LanguageConfig {
9027 brackets: BracketPairConfig {
9028 pairs: vec![BracketPair {
9029 start: "{".to_string(),
9030 end: "}".to_string(),
9031 close: true,
9032 surround: true,
9033 newline: true,
9034 }],
9035 ..Default::default()
9036 },
9037 autoclose_before: "}".to_string(),
9038 ..Default::default()
9039 },
9040 Some(tree_sitter_rust::LANGUAGE.into()),
9041 ));
9042
9043 let text = r#"
9044 a
9045 b
9046 c
9047 "#
9048 .unindent();
9049
9050 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9051 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9052 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9053 editor
9054 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9055 .await;
9056
9057 editor.update_in(cx, |editor, window, cx| {
9058 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9059 s.select_ranges([
9060 Point::new(0, 1)..Point::new(0, 1),
9061 Point::new(1, 1)..Point::new(1, 1),
9062 Point::new(2, 1)..Point::new(2, 1),
9063 ])
9064 });
9065
9066 editor.handle_input("{", window, cx);
9067 editor.handle_input("{", window, cx);
9068 editor.handle_input("_", window, cx);
9069 assert_eq!(
9070 editor.text(cx),
9071 "
9072 a{{_}}
9073 b{{_}}
9074 c{{_}}
9075 "
9076 .unindent()
9077 );
9078 assert_eq!(
9079 editor.selections.ranges::<Point>(cx),
9080 [
9081 Point::new(0, 4)..Point::new(0, 4),
9082 Point::new(1, 4)..Point::new(1, 4),
9083 Point::new(2, 4)..Point::new(2, 4)
9084 ]
9085 );
9086
9087 editor.backspace(&Default::default(), window, cx);
9088 editor.backspace(&Default::default(), window, cx);
9089 assert_eq!(
9090 editor.text(cx),
9091 "
9092 a{}
9093 b{}
9094 c{}
9095 "
9096 .unindent()
9097 );
9098 assert_eq!(
9099 editor.selections.ranges::<Point>(cx),
9100 [
9101 Point::new(0, 2)..Point::new(0, 2),
9102 Point::new(1, 2)..Point::new(1, 2),
9103 Point::new(2, 2)..Point::new(2, 2)
9104 ]
9105 );
9106
9107 editor.delete_to_previous_word_start(&Default::default(), window, cx);
9108 assert_eq!(
9109 editor.text(cx),
9110 "
9111 a
9112 b
9113 c
9114 "
9115 .unindent()
9116 );
9117 assert_eq!(
9118 editor.selections.ranges::<Point>(cx),
9119 [
9120 Point::new(0, 1)..Point::new(0, 1),
9121 Point::new(1, 1)..Point::new(1, 1),
9122 Point::new(2, 1)..Point::new(2, 1)
9123 ]
9124 );
9125 });
9126}
9127
9128#[gpui::test]
9129async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
9130 init_test(cx, |settings| {
9131 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9132 });
9133
9134 let mut cx = EditorTestContext::new(cx).await;
9135
9136 let language = Arc::new(Language::new(
9137 LanguageConfig {
9138 brackets: BracketPairConfig {
9139 pairs: vec![
9140 BracketPair {
9141 start: "{".to_string(),
9142 end: "}".to_string(),
9143 close: true,
9144 surround: true,
9145 newline: true,
9146 },
9147 BracketPair {
9148 start: "(".to_string(),
9149 end: ")".to_string(),
9150 close: true,
9151 surround: true,
9152 newline: true,
9153 },
9154 BracketPair {
9155 start: "[".to_string(),
9156 end: "]".to_string(),
9157 close: false,
9158 surround: true,
9159 newline: true,
9160 },
9161 ],
9162 ..Default::default()
9163 },
9164 autoclose_before: "})]".to_string(),
9165 ..Default::default()
9166 },
9167 Some(tree_sitter_rust::LANGUAGE.into()),
9168 ));
9169
9170 cx.language_registry().add(language.clone());
9171 cx.update_buffer(|buffer, cx| {
9172 buffer.set_language(Some(language), cx);
9173 });
9174
9175 cx.set_state(
9176 &"
9177 {(ˇ)}
9178 [[ˇ]]
9179 {(ˇ)}
9180 "
9181 .unindent(),
9182 );
9183
9184 cx.update_editor(|editor, window, cx| {
9185 editor.backspace(&Default::default(), window, cx);
9186 editor.backspace(&Default::default(), window, cx);
9187 });
9188
9189 cx.assert_editor_state(
9190 &"
9191 ˇ
9192 ˇ]]
9193 ˇ
9194 "
9195 .unindent(),
9196 );
9197
9198 cx.update_editor(|editor, window, cx| {
9199 editor.handle_input("{", window, cx);
9200 editor.handle_input("{", window, cx);
9201 editor.move_right(&MoveRight, window, cx);
9202 editor.move_right(&MoveRight, window, cx);
9203 editor.move_left(&MoveLeft, window, cx);
9204 editor.move_left(&MoveLeft, window, cx);
9205 editor.backspace(&Default::default(), window, cx);
9206 });
9207
9208 cx.assert_editor_state(
9209 &"
9210 {ˇ}
9211 {ˇ}]]
9212 {ˇ}
9213 "
9214 .unindent(),
9215 );
9216
9217 cx.update_editor(|editor, window, cx| {
9218 editor.backspace(&Default::default(), window, cx);
9219 });
9220
9221 cx.assert_editor_state(
9222 &"
9223 ˇ
9224 ˇ]]
9225 ˇ
9226 "
9227 .unindent(),
9228 );
9229}
9230
9231#[gpui::test]
9232async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
9233 init_test(cx, |_| {});
9234
9235 let language = Arc::new(Language::new(
9236 LanguageConfig::default(),
9237 Some(tree_sitter_rust::LANGUAGE.into()),
9238 ));
9239
9240 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
9241 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9242 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9243 editor
9244 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9245 .await;
9246
9247 editor.update_in(cx, |editor, window, cx| {
9248 editor.set_auto_replace_emoji_shortcode(true);
9249
9250 editor.handle_input("Hello ", window, cx);
9251 editor.handle_input(":wave", window, cx);
9252 assert_eq!(editor.text(cx), "Hello :wave".unindent());
9253
9254 editor.handle_input(":", window, cx);
9255 assert_eq!(editor.text(cx), "Hello 👋".unindent());
9256
9257 editor.handle_input(" :smile", window, cx);
9258 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
9259
9260 editor.handle_input(":", window, cx);
9261 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
9262
9263 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
9264 editor.handle_input(":wave", window, cx);
9265 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
9266
9267 editor.handle_input(":", window, cx);
9268 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
9269
9270 editor.handle_input(":1", window, cx);
9271 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
9272
9273 editor.handle_input(":", window, cx);
9274 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
9275
9276 // Ensure shortcode does not get replaced when it is part of a word
9277 editor.handle_input(" Test:wave", window, cx);
9278 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
9279
9280 editor.handle_input(":", window, cx);
9281 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
9282
9283 editor.set_auto_replace_emoji_shortcode(false);
9284
9285 // Ensure shortcode does not get replaced when auto replace is off
9286 editor.handle_input(" :wave", window, cx);
9287 assert_eq!(
9288 editor.text(cx),
9289 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
9290 );
9291
9292 editor.handle_input(":", window, cx);
9293 assert_eq!(
9294 editor.text(cx),
9295 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
9296 );
9297 });
9298}
9299
9300#[gpui::test]
9301async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
9302 init_test(cx, |_| {});
9303
9304 let (text, insertion_ranges) = marked_text_ranges(
9305 indoc! {"
9306 ˇ
9307 "},
9308 false,
9309 );
9310
9311 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
9312 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9313
9314 _ = editor.update_in(cx, |editor, window, cx| {
9315 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
9316
9317 editor
9318 .insert_snippet(&insertion_ranges, snippet, window, cx)
9319 .unwrap();
9320
9321 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
9322 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
9323 assert_eq!(editor.text(cx), expected_text);
9324 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
9325 }
9326
9327 assert(
9328 editor,
9329 cx,
9330 indoc! {"
9331 type «» =•
9332 "},
9333 );
9334
9335 assert!(editor.context_menu_visible(), "There should be a matches");
9336 });
9337}
9338
9339#[gpui::test]
9340async fn test_snippets(cx: &mut TestAppContext) {
9341 init_test(cx, |_| {});
9342
9343 let mut cx = EditorTestContext::new(cx).await;
9344
9345 cx.set_state(indoc! {"
9346 a.ˇ b
9347 a.ˇ b
9348 a.ˇ b
9349 "});
9350
9351 cx.update_editor(|editor, window, cx| {
9352 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
9353 let insertion_ranges = editor
9354 .selections
9355 .all(cx)
9356 .iter()
9357 .map(|s| s.range().clone())
9358 .collect::<Vec<_>>();
9359 editor
9360 .insert_snippet(&insertion_ranges, snippet, window, cx)
9361 .unwrap();
9362 });
9363
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 // Can't move earlier than the first tab stop
9371 cx.update_editor(|editor, window, cx| {
9372 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9373 });
9374 cx.assert_editor_state(indoc! {"
9375 a.f(«oneˇ», two, «threeˇ») b
9376 a.f(«oneˇ», two, «threeˇ») b
9377 a.f(«oneˇ», two, «threeˇ») b
9378 "});
9379
9380 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9381 cx.assert_editor_state(indoc! {"
9382 a.f(one, «twoˇ», three) b
9383 a.f(one, «twoˇ», three) b
9384 a.f(one, «twoˇ», three) b
9385 "});
9386
9387 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
9388 cx.assert_editor_state(indoc! {"
9389 a.f(«oneˇ», two, «threeˇ») b
9390 a.f(«oneˇ», two, «threeˇ») b
9391 a.f(«oneˇ», two, «threeˇ») b
9392 "});
9393
9394 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9395 cx.assert_editor_state(indoc! {"
9396 a.f(one, «twoˇ», three) b
9397 a.f(one, «twoˇ», three) b
9398 a.f(one, «twoˇ», three) b
9399 "});
9400 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9401 cx.assert_editor_state(indoc! {"
9402 a.f(one, two, three)ˇ b
9403 a.f(one, two, three)ˇ b
9404 a.f(one, two, three)ˇ b
9405 "});
9406
9407 // As soon as the last tab stop is reached, snippet state is gone
9408 cx.update_editor(|editor, window, cx| {
9409 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9410 });
9411 cx.assert_editor_state(indoc! {"
9412 a.f(one, two, three)ˇ b
9413 a.f(one, two, three)ˇ b
9414 a.f(one, two, three)ˇ b
9415 "});
9416}
9417
9418#[gpui::test]
9419async fn test_snippet_indentation(cx: &mut TestAppContext) {
9420 init_test(cx, |_| {});
9421
9422 let mut cx = EditorTestContext::new(cx).await;
9423
9424 cx.update_editor(|editor, window, cx| {
9425 let snippet = Snippet::parse(indoc! {"
9426 /*
9427 * Multiline comment with leading indentation
9428 *
9429 * $1
9430 */
9431 $0"})
9432 .unwrap();
9433 let insertion_ranges = editor
9434 .selections
9435 .all(cx)
9436 .iter()
9437 .map(|s| s.range().clone())
9438 .collect::<Vec<_>>();
9439 editor
9440 .insert_snippet(&insertion_ranges, snippet, window, cx)
9441 .unwrap();
9442 });
9443
9444 cx.assert_editor_state(indoc! {"
9445 /*
9446 * Multiline comment with leading indentation
9447 *
9448 * ˇ
9449 */
9450 "});
9451
9452 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9453 cx.assert_editor_state(indoc! {"
9454 /*
9455 * Multiline comment with leading indentation
9456 *
9457 *•
9458 */
9459 ˇ"});
9460}
9461
9462#[gpui::test]
9463async fn test_document_format_during_save(cx: &mut TestAppContext) {
9464 init_test(cx, |_| {});
9465
9466 let fs = FakeFs::new(cx.executor());
9467 fs.insert_file(path!("/file.rs"), Default::default()).await;
9468
9469 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
9470
9471 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9472 language_registry.add(rust_lang());
9473 let mut fake_servers = language_registry.register_fake_lsp(
9474 "Rust",
9475 FakeLspAdapter {
9476 capabilities: lsp::ServerCapabilities {
9477 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9478 ..Default::default()
9479 },
9480 ..Default::default()
9481 },
9482 );
9483
9484 let buffer = project
9485 .update(cx, |project, cx| {
9486 project.open_local_buffer(path!("/file.rs"), cx)
9487 })
9488 .await
9489 .unwrap();
9490
9491 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9492 let (editor, cx) = cx.add_window_view(|window, cx| {
9493 build_editor_with_project(project.clone(), buffer, window, cx)
9494 });
9495 editor.update_in(cx, |editor, window, cx| {
9496 editor.set_text("one\ntwo\nthree\n", window, cx)
9497 });
9498 assert!(cx.read(|cx| editor.is_dirty(cx)));
9499
9500 cx.executor().start_waiting();
9501 let fake_server = fake_servers.next().await.unwrap();
9502
9503 {
9504 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9505 move |params, _| async move {
9506 assert_eq!(
9507 params.text_document.uri,
9508 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9509 );
9510 assert_eq!(params.options.tab_size, 4);
9511 Ok(Some(vec![lsp::TextEdit::new(
9512 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9513 ", ".to_string(),
9514 )]))
9515 },
9516 );
9517 let save = editor
9518 .update_in(cx, |editor, window, cx| {
9519 editor.save(
9520 SaveOptions {
9521 format: true,
9522 autosave: false,
9523 },
9524 project.clone(),
9525 window,
9526 cx,
9527 )
9528 })
9529 .unwrap();
9530 cx.executor().start_waiting();
9531 save.await;
9532
9533 assert_eq!(
9534 editor.update(cx, |editor, cx| editor.text(cx)),
9535 "one, two\nthree\n"
9536 );
9537 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9538 }
9539
9540 {
9541 editor.update_in(cx, |editor, window, cx| {
9542 editor.set_text("one\ntwo\nthree\n", window, cx)
9543 });
9544 assert!(cx.read(|cx| editor.is_dirty(cx)));
9545
9546 // Ensure we can still save even if formatting hangs.
9547 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9548 move |params, _| async move {
9549 assert_eq!(
9550 params.text_document.uri,
9551 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9552 );
9553 futures::future::pending::<()>().await;
9554 unreachable!()
9555 },
9556 );
9557 let save = editor
9558 .update_in(cx, |editor, window, cx| {
9559 editor.save(
9560 SaveOptions {
9561 format: true,
9562 autosave: false,
9563 },
9564 project.clone(),
9565 window,
9566 cx,
9567 )
9568 })
9569 .unwrap();
9570 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9571 cx.executor().start_waiting();
9572 save.await;
9573 assert_eq!(
9574 editor.update(cx, |editor, cx| editor.text(cx)),
9575 "one\ntwo\nthree\n"
9576 );
9577 }
9578
9579 // Set rust language override and assert overridden tabsize is sent to language server
9580 update_test_language_settings(cx, |settings| {
9581 settings.languages.0.insert(
9582 "Rust".into(),
9583 LanguageSettingsContent {
9584 tab_size: NonZeroU32::new(8),
9585 ..Default::default()
9586 },
9587 );
9588 });
9589
9590 {
9591 editor.update_in(cx, |editor, window, cx| {
9592 editor.set_text("somehting_new\n", window, cx)
9593 });
9594 assert!(cx.read(|cx| editor.is_dirty(cx)));
9595 let _formatting_request_signal = fake_server
9596 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9597 assert_eq!(
9598 params.text_document.uri,
9599 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9600 );
9601 assert_eq!(params.options.tab_size, 8);
9602 Ok(Some(vec![]))
9603 });
9604 let save = editor
9605 .update_in(cx, |editor, window, cx| {
9606 editor.save(
9607 SaveOptions {
9608 format: true,
9609 autosave: false,
9610 },
9611 project.clone(),
9612 window,
9613 cx,
9614 )
9615 })
9616 .unwrap();
9617 cx.executor().start_waiting();
9618 save.await;
9619 }
9620}
9621
9622#[gpui::test]
9623async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
9624 init_test(cx, |settings| {
9625 settings.defaults.ensure_final_newline_on_save = Some(false);
9626 });
9627
9628 let fs = FakeFs::new(cx.executor());
9629 fs.insert_file(path!("/file.txt"), "foo".into()).await;
9630
9631 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
9632
9633 let buffer = project
9634 .update(cx, |project, cx| {
9635 project.open_local_buffer(path!("/file.txt"), cx)
9636 })
9637 .await
9638 .unwrap();
9639
9640 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9641 let (editor, cx) = cx.add_window_view(|window, cx| {
9642 build_editor_with_project(project.clone(), buffer, window, cx)
9643 });
9644 editor.update_in(cx, |editor, window, cx| {
9645 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9646 s.select_ranges([0..0])
9647 });
9648 });
9649 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9650
9651 editor.update_in(cx, |editor, window, cx| {
9652 editor.handle_input("\n", window, cx)
9653 });
9654 cx.run_until_parked();
9655 save(&editor, &project, cx).await;
9656 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9657
9658 editor.update_in(cx, |editor, window, cx| {
9659 editor.undo(&Default::default(), window, cx);
9660 });
9661 save(&editor, &project, cx).await;
9662 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9663
9664 editor.update_in(cx, |editor, window, cx| {
9665 editor.redo(&Default::default(), window, cx);
9666 });
9667 cx.run_until_parked();
9668 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9669
9670 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
9671 let save = editor
9672 .update_in(cx, |editor, window, cx| {
9673 editor.save(
9674 SaveOptions {
9675 format: true,
9676 autosave: false,
9677 },
9678 project.clone(),
9679 window,
9680 cx,
9681 )
9682 })
9683 .unwrap();
9684 cx.executor().start_waiting();
9685 save.await;
9686 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9687 }
9688}
9689
9690#[gpui::test]
9691async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9692 init_test(cx, |_| {});
9693
9694 let cols = 4;
9695 let rows = 10;
9696 let sample_text_1 = sample_text(rows, cols, 'a');
9697 assert_eq!(
9698 sample_text_1,
9699 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9700 );
9701 let sample_text_2 = sample_text(rows, cols, 'l');
9702 assert_eq!(
9703 sample_text_2,
9704 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9705 );
9706 let sample_text_3 = sample_text(rows, cols, 'v');
9707 assert_eq!(
9708 sample_text_3,
9709 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9710 );
9711
9712 let fs = FakeFs::new(cx.executor());
9713 fs.insert_tree(
9714 path!("/a"),
9715 json!({
9716 "main.rs": sample_text_1,
9717 "other.rs": sample_text_2,
9718 "lib.rs": sample_text_3,
9719 }),
9720 )
9721 .await;
9722
9723 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9724 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9725 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9726
9727 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9728 language_registry.add(rust_lang());
9729 let mut fake_servers = language_registry.register_fake_lsp(
9730 "Rust",
9731 FakeLspAdapter {
9732 capabilities: lsp::ServerCapabilities {
9733 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9734 ..Default::default()
9735 },
9736 ..Default::default()
9737 },
9738 );
9739
9740 let worktree = project.update(cx, |project, cx| {
9741 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
9742 assert_eq!(worktrees.len(), 1);
9743 worktrees.pop().unwrap()
9744 });
9745 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9746
9747 let buffer_1 = project
9748 .update(cx, |project, cx| {
9749 project.open_buffer((worktree_id, "main.rs"), cx)
9750 })
9751 .await
9752 .unwrap();
9753 let buffer_2 = project
9754 .update(cx, |project, cx| {
9755 project.open_buffer((worktree_id, "other.rs"), cx)
9756 })
9757 .await
9758 .unwrap();
9759 let buffer_3 = project
9760 .update(cx, |project, cx| {
9761 project.open_buffer((worktree_id, "lib.rs"), cx)
9762 })
9763 .await
9764 .unwrap();
9765
9766 let multi_buffer = cx.new(|cx| {
9767 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9768 multi_buffer.push_excerpts(
9769 buffer_1.clone(),
9770 [
9771 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9772 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9773 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9774 ],
9775 cx,
9776 );
9777 multi_buffer.push_excerpts(
9778 buffer_2.clone(),
9779 [
9780 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9781 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9782 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9783 ],
9784 cx,
9785 );
9786 multi_buffer.push_excerpts(
9787 buffer_3.clone(),
9788 [
9789 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9790 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9791 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9792 ],
9793 cx,
9794 );
9795 multi_buffer
9796 });
9797 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
9798 Editor::new(
9799 EditorMode::full(),
9800 multi_buffer,
9801 Some(project.clone()),
9802 window,
9803 cx,
9804 )
9805 });
9806
9807 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9808 editor.change_selections(
9809 SelectionEffects::scroll(Autoscroll::Next),
9810 window,
9811 cx,
9812 |s| s.select_ranges(Some(1..2)),
9813 );
9814 editor.insert("|one|two|three|", window, cx);
9815 });
9816 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9817 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9818 editor.change_selections(
9819 SelectionEffects::scroll(Autoscroll::Next),
9820 window,
9821 cx,
9822 |s| s.select_ranges(Some(60..70)),
9823 );
9824 editor.insert("|four|five|six|", window, cx);
9825 });
9826 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9827
9828 // First two buffers should be edited, but not the third one.
9829 assert_eq!(
9830 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9831 "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}",
9832 );
9833 buffer_1.update(cx, |buffer, _| {
9834 assert!(buffer.is_dirty());
9835 assert_eq!(
9836 buffer.text(),
9837 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
9838 )
9839 });
9840 buffer_2.update(cx, |buffer, _| {
9841 assert!(buffer.is_dirty());
9842 assert_eq!(
9843 buffer.text(),
9844 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
9845 )
9846 });
9847 buffer_3.update(cx, |buffer, _| {
9848 assert!(!buffer.is_dirty());
9849 assert_eq!(buffer.text(), sample_text_3,)
9850 });
9851 cx.executor().run_until_parked();
9852
9853 cx.executor().start_waiting();
9854 let save = multi_buffer_editor
9855 .update_in(cx, |editor, window, cx| {
9856 editor.save(
9857 SaveOptions {
9858 format: true,
9859 autosave: false,
9860 },
9861 project.clone(),
9862 window,
9863 cx,
9864 )
9865 })
9866 .unwrap();
9867
9868 let fake_server = fake_servers.next().await.unwrap();
9869 fake_server
9870 .server
9871 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
9872 Ok(Some(vec![lsp::TextEdit::new(
9873 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9874 format!("[{} formatted]", params.text_document.uri),
9875 )]))
9876 })
9877 .detach();
9878 save.await;
9879
9880 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
9881 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
9882 assert_eq!(
9883 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9884 uri!(
9885 "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}"
9886 ),
9887 );
9888 buffer_1.update(cx, |buffer, _| {
9889 assert!(!buffer.is_dirty());
9890 assert_eq!(
9891 buffer.text(),
9892 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
9893 )
9894 });
9895 buffer_2.update(cx, |buffer, _| {
9896 assert!(!buffer.is_dirty());
9897 assert_eq!(
9898 buffer.text(),
9899 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
9900 )
9901 });
9902 buffer_3.update(cx, |buffer, _| {
9903 assert!(!buffer.is_dirty());
9904 assert_eq!(buffer.text(), sample_text_3,)
9905 });
9906}
9907
9908#[gpui::test]
9909async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
9910 init_test(cx, |_| {});
9911
9912 let fs = FakeFs::new(cx.executor());
9913 fs.insert_tree(
9914 path!("/dir"),
9915 json!({
9916 "file1.rs": "fn main() { println!(\"hello\"); }",
9917 "file2.rs": "fn test() { println!(\"test\"); }",
9918 "file3.rs": "fn other() { println!(\"other\"); }\n",
9919 }),
9920 )
9921 .await;
9922
9923 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
9924 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9925 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9926
9927 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9928 language_registry.add(rust_lang());
9929
9930 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
9931 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9932
9933 // Open three buffers
9934 let buffer_1 = project
9935 .update(cx, |project, cx| {
9936 project.open_buffer((worktree_id, "file1.rs"), cx)
9937 })
9938 .await
9939 .unwrap();
9940 let buffer_2 = project
9941 .update(cx, |project, cx| {
9942 project.open_buffer((worktree_id, "file2.rs"), cx)
9943 })
9944 .await
9945 .unwrap();
9946 let buffer_3 = project
9947 .update(cx, |project, cx| {
9948 project.open_buffer((worktree_id, "file3.rs"), cx)
9949 })
9950 .await
9951 .unwrap();
9952
9953 // Create a multi-buffer with all three buffers
9954 let multi_buffer = cx.new(|cx| {
9955 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9956 multi_buffer.push_excerpts(
9957 buffer_1.clone(),
9958 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9959 cx,
9960 );
9961 multi_buffer.push_excerpts(
9962 buffer_2.clone(),
9963 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9964 cx,
9965 );
9966 multi_buffer.push_excerpts(
9967 buffer_3.clone(),
9968 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9969 cx,
9970 );
9971 multi_buffer
9972 });
9973
9974 let editor = cx.new_window_entity(|window, cx| {
9975 Editor::new(
9976 EditorMode::full(),
9977 multi_buffer,
9978 Some(project.clone()),
9979 window,
9980 cx,
9981 )
9982 });
9983
9984 // Edit only the first buffer
9985 editor.update_in(cx, |editor, window, cx| {
9986 editor.change_selections(
9987 SelectionEffects::scroll(Autoscroll::Next),
9988 window,
9989 cx,
9990 |s| s.select_ranges(Some(10..10)),
9991 );
9992 editor.insert("// edited", window, cx);
9993 });
9994
9995 // Verify that only buffer 1 is dirty
9996 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
9997 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9998 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9999
10000 // Get write counts after file creation (files were created with initial content)
10001 // We expect each file to have been written once during creation
10002 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
10003 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
10004 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
10005
10006 // Perform autosave
10007 let save_task = editor.update_in(cx, |editor, window, cx| {
10008 editor.save(
10009 SaveOptions {
10010 format: true,
10011 autosave: true,
10012 },
10013 project.clone(),
10014 window,
10015 cx,
10016 )
10017 });
10018 save_task.await.unwrap();
10019
10020 // Only the dirty buffer should have been saved
10021 assert_eq!(
10022 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
10023 1,
10024 "Buffer 1 was dirty, so it should have been written once during autosave"
10025 );
10026 assert_eq!(
10027 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
10028 0,
10029 "Buffer 2 was clean, so it should not have been written during autosave"
10030 );
10031 assert_eq!(
10032 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
10033 0,
10034 "Buffer 3 was clean, so it should not have been written during autosave"
10035 );
10036
10037 // Verify buffer states after autosave
10038 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10039 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10040 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10041
10042 // Now perform a manual save (format = true)
10043 let save_task = editor.update_in(cx, |editor, window, cx| {
10044 editor.save(
10045 SaveOptions {
10046 format: true,
10047 autosave: false,
10048 },
10049 project.clone(),
10050 window,
10051 cx,
10052 )
10053 });
10054 save_task.await.unwrap();
10055
10056 // During manual save, clean buffers don't get written to disk
10057 // They just get did_save called for language server notifications
10058 assert_eq!(
10059 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
10060 1,
10061 "Buffer 1 should only have been written once total (during autosave, not manual save)"
10062 );
10063 assert_eq!(
10064 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
10065 0,
10066 "Buffer 2 should not have been written at all"
10067 );
10068 assert_eq!(
10069 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
10070 0,
10071 "Buffer 3 should not have been written at all"
10072 );
10073}
10074
10075#[gpui::test]
10076async fn test_range_format_during_save(cx: &mut TestAppContext) {
10077 init_test(cx, |_| {});
10078
10079 let fs = FakeFs::new(cx.executor());
10080 fs.insert_file(path!("/file.rs"), Default::default()).await;
10081
10082 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10083
10084 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10085 language_registry.add(rust_lang());
10086 let mut fake_servers = language_registry.register_fake_lsp(
10087 "Rust",
10088 FakeLspAdapter {
10089 capabilities: lsp::ServerCapabilities {
10090 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
10091 ..Default::default()
10092 },
10093 ..Default::default()
10094 },
10095 );
10096
10097 let buffer = project
10098 .update(cx, |project, cx| {
10099 project.open_local_buffer(path!("/file.rs"), cx)
10100 })
10101 .await
10102 .unwrap();
10103
10104 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10105 let (editor, cx) = cx.add_window_view(|window, cx| {
10106 build_editor_with_project(project.clone(), buffer, window, cx)
10107 });
10108 editor.update_in(cx, |editor, window, cx| {
10109 editor.set_text("one\ntwo\nthree\n", window, cx)
10110 });
10111 assert!(cx.read(|cx| editor.is_dirty(cx)));
10112
10113 cx.executor().start_waiting();
10114 let fake_server = fake_servers.next().await.unwrap();
10115
10116 let save = editor
10117 .update_in(cx, |editor, window, cx| {
10118 editor.save(
10119 SaveOptions {
10120 format: true,
10121 autosave: false,
10122 },
10123 project.clone(),
10124 window,
10125 cx,
10126 )
10127 })
10128 .unwrap();
10129 fake_server
10130 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10131 assert_eq!(
10132 params.text_document.uri,
10133 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10134 );
10135 assert_eq!(params.options.tab_size, 4);
10136 Ok(Some(vec![lsp::TextEdit::new(
10137 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10138 ", ".to_string(),
10139 )]))
10140 })
10141 .next()
10142 .await;
10143 cx.executor().start_waiting();
10144 save.await;
10145 assert_eq!(
10146 editor.update(cx, |editor, cx| editor.text(cx)),
10147 "one, two\nthree\n"
10148 );
10149 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10150
10151 editor.update_in(cx, |editor, window, cx| {
10152 editor.set_text("one\ntwo\nthree\n", window, cx)
10153 });
10154 assert!(cx.read(|cx| editor.is_dirty(cx)));
10155
10156 // Ensure we can still save even if formatting hangs.
10157 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
10158 move |params, _| async move {
10159 assert_eq!(
10160 params.text_document.uri,
10161 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10162 );
10163 futures::future::pending::<()>().await;
10164 unreachable!()
10165 },
10166 );
10167 let save = editor
10168 .update_in(cx, |editor, window, cx| {
10169 editor.save(
10170 SaveOptions {
10171 format: true,
10172 autosave: false,
10173 },
10174 project.clone(),
10175 window,
10176 cx,
10177 )
10178 })
10179 .unwrap();
10180 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10181 cx.executor().start_waiting();
10182 save.await;
10183 assert_eq!(
10184 editor.update(cx, |editor, cx| editor.text(cx)),
10185 "one\ntwo\nthree\n"
10186 );
10187 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10188
10189 // For non-dirty buffer, no formatting request should be sent
10190 let save = editor
10191 .update_in(cx, |editor, window, cx| {
10192 editor.save(
10193 SaveOptions {
10194 format: false,
10195 autosave: false,
10196 },
10197 project.clone(),
10198 window,
10199 cx,
10200 )
10201 })
10202 .unwrap();
10203 let _pending_format_request = fake_server
10204 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
10205 panic!("Should not be invoked");
10206 })
10207 .next();
10208 cx.executor().start_waiting();
10209 save.await;
10210
10211 // Set Rust language override and assert overridden tabsize is sent to language server
10212 update_test_language_settings(cx, |settings| {
10213 settings.languages.0.insert(
10214 "Rust".into(),
10215 LanguageSettingsContent {
10216 tab_size: NonZeroU32::new(8),
10217 ..Default::default()
10218 },
10219 );
10220 });
10221
10222 editor.update_in(cx, |editor, window, cx| {
10223 editor.set_text("somehting_new\n", window, cx)
10224 });
10225 assert!(cx.read(|cx| editor.is_dirty(cx)));
10226 let save = editor
10227 .update_in(cx, |editor, window, cx| {
10228 editor.save(
10229 SaveOptions {
10230 format: true,
10231 autosave: false,
10232 },
10233 project.clone(),
10234 window,
10235 cx,
10236 )
10237 })
10238 .unwrap();
10239 fake_server
10240 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10241 assert_eq!(
10242 params.text_document.uri,
10243 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10244 );
10245 assert_eq!(params.options.tab_size, 8);
10246 Ok(Some(Vec::new()))
10247 })
10248 .next()
10249 .await;
10250 save.await;
10251}
10252
10253#[gpui::test]
10254async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
10255 init_test(cx, |settings| {
10256 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
10257 Formatter::LanguageServer { name: None },
10258 )))
10259 });
10260
10261 let fs = FakeFs::new(cx.executor());
10262 fs.insert_file(path!("/file.rs"), Default::default()).await;
10263
10264 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10265
10266 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10267 language_registry.add(Arc::new(Language::new(
10268 LanguageConfig {
10269 name: "Rust".into(),
10270 matcher: LanguageMatcher {
10271 path_suffixes: vec!["rs".to_string()],
10272 ..Default::default()
10273 },
10274 ..LanguageConfig::default()
10275 },
10276 Some(tree_sitter_rust::LANGUAGE.into()),
10277 )));
10278 update_test_language_settings(cx, |settings| {
10279 // Enable Prettier formatting for the same buffer, and ensure
10280 // LSP is called instead of Prettier.
10281 settings.defaults.prettier = Some(PrettierSettings {
10282 allowed: true,
10283 ..PrettierSettings::default()
10284 });
10285 });
10286 let mut fake_servers = language_registry.register_fake_lsp(
10287 "Rust",
10288 FakeLspAdapter {
10289 capabilities: lsp::ServerCapabilities {
10290 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10291 ..Default::default()
10292 },
10293 ..Default::default()
10294 },
10295 );
10296
10297 let buffer = project
10298 .update(cx, |project, cx| {
10299 project.open_local_buffer(path!("/file.rs"), cx)
10300 })
10301 .await
10302 .unwrap();
10303
10304 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10305 let (editor, cx) = cx.add_window_view(|window, cx| {
10306 build_editor_with_project(project.clone(), buffer, window, cx)
10307 });
10308 editor.update_in(cx, |editor, window, cx| {
10309 editor.set_text("one\ntwo\nthree\n", window, cx)
10310 });
10311
10312 cx.executor().start_waiting();
10313 let fake_server = fake_servers.next().await.unwrap();
10314
10315 let format = editor
10316 .update_in(cx, |editor, window, cx| {
10317 editor.perform_format(
10318 project.clone(),
10319 FormatTrigger::Manual,
10320 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10321 window,
10322 cx,
10323 )
10324 })
10325 .unwrap();
10326 fake_server
10327 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10328 assert_eq!(
10329 params.text_document.uri,
10330 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10331 );
10332 assert_eq!(params.options.tab_size, 4);
10333 Ok(Some(vec![lsp::TextEdit::new(
10334 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10335 ", ".to_string(),
10336 )]))
10337 })
10338 .next()
10339 .await;
10340 cx.executor().start_waiting();
10341 format.await;
10342 assert_eq!(
10343 editor.update(cx, |editor, cx| editor.text(cx)),
10344 "one, two\nthree\n"
10345 );
10346
10347 editor.update_in(cx, |editor, window, cx| {
10348 editor.set_text("one\ntwo\nthree\n", window, cx)
10349 });
10350 // Ensure we don't lock if formatting hangs.
10351 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10352 move |params, _| async move {
10353 assert_eq!(
10354 params.text_document.uri,
10355 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10356 );
10357 futures::future::pending::<()>().await;
10358 unreachable!()
10359 },
10360 );
10361 let format = editor
10362 .update_in(cx, |editor, window, cx| {
10363 editor.perform_format(
10364 project,
10365 FormatTrigger::Manual,
10366 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10367 window,
10368 cx,
10369 )
10370 })
10371 .unwrap();
10372 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10373 cx.executor().start_waiting();
10374 format.await;
10375 assert_eq!(
10376 editor.update(cx, |editor, cx| editor.text(cx)),
10377 "one\ntwo\nthree\n"
10378 );
10379}
10380
10381#[gpui::test]
10382async fn test_multiple_formatters(cx: &mut TestAppContext) {
10383 init_test(cx, |settings| {
10384 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
10385 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10386 Formatter::LanguageServer { name: None },
10387 Formatter::CodeActions(
10388 [
10389 ("code-action-1".into(), true),
10390 ("code-action-2".into(), true),
10391 ]
10392 .into_iter()
10393 .collect(),
10394 ),
10395 ])))
10396 });
10397
10398 let fs = FakeFs::new(cx.executor());
10399 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
10400 .await;
10401
10402 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10403 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10404 language_registry.add(rust_lang());
10405
10406 let mut fake_servers = language_registry.register_fake_lsp(
10407 "Rust",
10408 FakeLspAdapter {
10409 capabilities: lsp::ServerCapabilities {
10410 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10411 execute_command_provider: Some(lsp::ExecuteCommandOptions {
10412 commands: vec!["the-command-for-code-action-1".into()],
10413 ..Default::default()
10414 }),
10415 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10416 ..Default::default()
10417 },
10418 ..Default::default()
10419 },
10420 );
10421
10422 let buffer = project
10423 .update(cx, |project, cx| {
10424 project.open_local_buffer(path!("/file.rs"), cx)
10425 })
10426 .await
10427 .unwrap();
10428
10429 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10430 let (editor, cx) = cx.add_window_view(|window, cx| {
10431 build_editor_with_project(project.clone(), buffer, window, cx)
10432 });
10433
10434 cx.executor().start_waiting();
10435
10436 let fake_server = fake_servers.next().await.unwrap();
10437 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10438 move |_params, _| async move {
10439 Ok(Some(vec![lsp::TextEdit::new(
10440 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10441 "applied-formatting\n".to_string(),
10442 )]))
10443 },
10444 );
10445 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10446 move |params, _| async move {
10447 assert_eq!(
10448 params.context.only,
10449 Some(vec!["code-action-1".into(), "code-action-2".into()])
10450 );
10451 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
10452 Ok(Some(vec![
10453 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10454 kind: Some("code-action-1".into()),
10455 edit: Some(lsp::WorkspaceEdit::new(
10456 [(
10457 uri.clone(),
10458 vec![lsp::TextEdit::new(
10459 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10460 "applied-code-action-1-edit\n".to_string(),
10461 )],
10462 )]
10463 .into_iter()
10464 .collect(),
10465 )),
10466 command: Some(lsp::Command {
10467 command: "the-command-for-code-action-1".into(),
10468 ..Default::default()
10469 }),
10470 ..Default::default()
10471 }),
10472 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10473 kind: Some("code-action-2".into()),
10474 edit: Some(lsp::WorkspaceEdit::new(
10475 [(
10476 uri.clone(),
10477 vec![lsp::TextEdit::new(
10478 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10479 "applied-code-action-2-edit\n".to_string(),
10480 )],
10481 )]
10482 .into_iter()
10483 .collect(),
10484 )),
10485 ..Default::default()
10486 }),
10487 ]))
10488 },
10489 );
10490
10491 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
10492 move |params, _| async move { Ok(params) }
10493 });
10494
10495 let command_lock = Arc::new(futures::lock::Mutex::new(()));
10496 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
10497 let fake = fake_server.clone();
10498 let lock = command_lock.clone();
10499 move |params, _| {
10500 assert_eq!(params.command, "the-command-for-code-action-1");
10501 let fake = fake.clone();
10502 let lock = lock.clone();
10503 async move {
10504 lock.lock().await;
10505 fake.server
10506 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
10507 label: None,
10508 edit: lsp::WorkspaceEdit {
10509 changes: Some(
10510 [(
10511 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
10512 vec![lsp::TextEdit {
10513 range: lsp::Range::new(
10514 lsp::Position::new(0, 0),
10515 lsp::Position::new(0, 0),
10516 ),
10517 new_text: "applied-code-action-1-command\n".into(),
10518 }],
10519 )]
10520 .into_iter()
10521 .collect(),
10522 ),
10523 ..Default::default()
10524 },
10525 })
10526 .await
10527 .into_response()
10528 .unwrap();
10529 Ok(Some(json!(null)))
10530 }
10531 }
10532 });
10533
10534 cx.executor().start_waiting();
10535 editor
10536 .update_in(cx, |editor, window, cx| {
10537 editor.perform_format(
10538 project.clone(),
10539 FormatTrigger::Manual,
10540 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10541 window,
10542 cx,
10543 )
10544 })
10545 .unwrap()
10546 .await;
10547 editor.update(cx, |editor, cx| {
10548 assert_eq!(
10549 editor.text(cx),
10550 r#"
10551 applied-code-action-2-edit
10552 applied-code-action-1-command
10553 applied-code-action-1-edit
10554 applied-formatting
10555 one
10556 two
10557 three
10558 "#
10559 .unindent()
10560 );
10561 });
10562
10563 editor.update_in(cx, |editor, window, cx| {
10564 editor.undo(&Default::default(), window, cx);
10565 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10566 });
10567
10568 // Perform a manual edit while waiting for an LSP command
10569 // that's being run as part of a formatting code action.
10570 let lock_guard = command_lock.lock().await;
10571 let format = editor
10572 .update_in(cx, |editor, window, cx| {
10573 editor.perform_format(
10574 project.clone(),
10575 FormatTrigger::Manual,
10576 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10577 window,
10578 cx,
10579 )
10580 })
10581 .unwrap();
10582 cx.run_until_parked();
10583 editor.update(cx, |editor, cx| {
10584 assert_eq!(
10585 editor.text(cx),
10586 r#"
10587 applied-code-action-1-edit
10588 applied-formatting
10589 one
10590 two
10591 three
10592 "#
10593 .unindent()
10594 );
10595
10596 editor.buffer.update(cx, |buffer, cx| {
10597 let ix = buffer.len(cx);
10598 buffer.edit([(ix..ix, "edited\n")], None, cx);
10599 });
10600 });
10601
10602 // Allow the LSP command to proceed. Because the buffer was edited,
10603 // the second code action will not be run.
10604 drop(lock_guard);
10605 format.await;
10606 editor.update_in(cx, |editor, 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 edited
10617 "#
10618 .unindent()
10619 );
10620
10621 // The manual edit is undone first, because it is the last thing the user did
10622 // (even though the command completed afterwards).
10623 editor.undo(&Default::default(), window, cx);
10624 assert_eq!(
10625 editor.text(cx),
10626 r#"
10627 applied-code-action-1-command
10628 applied-code-action-1-edit
10629 applied-formatting
10630 one
10631 two
10632 three
10633 "#
10634 .unindent()
10635 );
10636
10637 // All the formatting (including the command, which completed after the manual edit)
10638 // is undone together.
10639 editor.undo(&Default::default(), window, cx);
10640 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10641 });
10642}
10643
10644#[gpui::test]
10645async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
10646 init_test(cx, |settings| {
10647 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10648 Formatter::LanguageServer { name: None },
10649 ])))
10650 });
10651
10652 let fs = FakeFs::new(cx.executor());
10653 fs.insert_file(path!("/file.ts"), Default::default()).await;
10654
10655 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10656
10657 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10658 language_registry.add(Arc::new(Language::new(
10659 LanguageConfig {
10660 name: "TypeScript".into(),
10661 matcher: LanguageMatcher {
10662 path_suffixes: vec!["ts".to_string()],
10663 ..Default::default()
10664 },
10665 ..LanguageConfig::default()
10666 },
10667 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10668 )));
10669 update_test_language_settings(cx, |settings| {
10670 settings.defaults.prettier = Some(PrettierSettings {
10671 allowed: true,
10672 ..PrettierSettings::default()
10673 });
10674 });
10675 let mut fake_servers = language_registry.register_fake_lsp(
10676 "TypeScript",
10677 FakeLspAdapter {
10678 capabilities: lsp::ServerCapabilities {
10679 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10680 ..Default::default()
10681 },
10682 ..Default::default()
10683 },
10684 );
10685
10686 let buffer = project
10687 .update(cx, |project, cx| {
10688 project.open_local_buffer(path!("/file.ts"), cx)
10689 })
10690 .await
10691 .unwrap();
10692
10693 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10694 let (editor, cx) = cx.add_window_view(|window, cx| {
10695 build_editor_with_project(project.clone(), buffer, window, cx)
10696 });
10697 editor.update_in(cx, |editor, window, cx| {
10698 editor.set_text(
10699 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10700 window,
10701 cx,
10702 )
10703 });
10704
10705 cx.executor().start_waiting();
10706 let fake_server = fake_servers.next().await.unwrap();
10707
10708 let format = editor
10709 .update_in(cx, |editor, window, cx| {
10710 editor.perform_code_action_kind(
10711 project.clone(),
10712 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10713 window,
10714 cx,
10715 )
10716 })
10717 .unwrap();
10718 fake_server
10719 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
10720 assert_eq!(
10721 params.text_document.uri,
10722 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10723 );
10724 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
10725 lsp::CodeAction {
10726 title: "Organize Imports".to_string(),
10727 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
10728 edit: Some(lsp::WorkspaceEdit {
10729 changes: Some(
10730 [(
10731 params.text_document.uri.clone(),
10732 vec![lsp::TextEdit::new(
10733 lsp::Range::new(
10734 lsp::Position::new(1, 0),
10735 lsp::Position::new(2, 0),
10736 ),
10737 "".to_string(),
10738 )],
10739 )]
10740 .into_iter()
10741 .collect(),
10742 ),
10743 ..Default::default()
10744 }),
10745 ..Default::default()
10746 },
10747 )]))
10748 })
10749 .next()
10750 .await;
10751 cx.executor().start_waiting();
10752 format.await;
10753 assert_eq!(
10754 editor.update(cx, |editor, cx| editor.text(cx)),
10755 "import { a } from 'module';\n\nconst x = a;\n"
10756 );
10757
10758 editor.update_in(cx, |editor, window, cx| {
10759 editor.set_text(
10760 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10761 window,
10762 cx,
10763 )
10764 });
10765 // Ensure we don't lock if code action hangs.
10766 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10767 move |params, _| async move {
10768 assert_eq!(
10769 params.text_document.uri,
10770 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10771 );
10772 futures::future::pending::<()>().await;
10773 unreachable!()
10774 },
10775 );
10776 let format = editor
10777 .update_in(cx, |editor, window, cx| {
10778 editor.perform_code_action_kind(
10779 project,
10780 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10781 window,
10782 cx,
10783 )
10784 })
10785 .unwrap();
10786 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
10787 cx.executor().start_waiting();
10788 format.await;
10789 assert_eq!(
10790 editor.update(cx, |editor, cx| editor.text(cx)),
10791 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
10792 );
10793}
10794
10795#[gpui::test]
10796async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
10797 init_test(cx, |_| {});
10798
10799 let mut cx = EditorLspTestContext::new_rust(
10800 lsp::ServerCapabilities {
10801 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10802 ..Default::default()
10803 },
10804 cx,
10805 )
10806 .await;
10807
10808 cx.set_state(indoc! {"
10809 one.twoˇ
10810 "});
10811
10812 // The format request takes a long time. When it completes, it inserts
10813 // a newline and an indent before the `.`
10814 cx.lsp
10815 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
10816 let executor = cx.background_executor().clone();
10817 async move {
10818 executor.timer(Duration::from_millis(100)).await;
10819 Ok(Some(vec![lsp::TextEdit {
10820 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
10821 new_text: "\n ".into(),
10822 }]))
10823 }
10824 });
10825
10826 // Submit a format request.
10827 let format_1 = cx
10828 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10829 .unwrap();
10830 cx.executor().run_until_parked();
10831
10832 // Submit a second format request.
10833 let format_2 = cx
10834 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10835 .unwrap();
10836 cx.executor().run_until_parked();
10837
10838 // Wait for both format requests to complete
10839 cx.executor().advance_clock(Duration::from_millis(200));
10840 cx.executor().start_waiting();
10841 format_1.await.unwrap();
10842 cx.executor().start_waiting();
10843 format_2.await.unwrap();
10844
10845 // The formatting edits only happens once.
10846 cx.assert_editor_state(indoc! {"
10847 one
10848 .twoˇ
10849 "});
10850}
10851
10852#[gpui::test]
10853async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
10854 init_test(cx, |settings| {
10855 settings.defaults.formatter = Some(SelectedFormatter::Auto)
10856 });
10857
10858 let mut cx = EditorLspTestContext::new_rust(
10859 lsp::ServerCapabilities {
10860 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10861 ..Default::default()
10862 },
10863 cx,
10864 )
10865 .await;
10866
10867 // Set up a buffer white some trailing whitespace and no trailing newline.
10868 cx.set_state(
10869 &[
10870 "one ", //
10871 "twoˇ", //
10872 "three ", //
10873 "four", //
10874 ]
10875 .join("\n"),
10876 );
10877
10878 // Submit a format request.
10879 let format = cx
10880 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10881 .unwrap();
10882
10883 // Record which buffer changes have been sent to the language server
10884 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
10885 cx.lsp
10886 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
10887 let buffer_changes = buffer_changes.clone();
10888 move |params, _| {
10889 buffer_changes.lock().extend(
10890 params
10891 .content_changes
10892 .into_iter()
10893 .map(|e| (e.range.unwrap(), e.text)),
10894 );
10895 }
10896 });
10897
10898 // Handle formatting requests to the language server.
10899 cx.lsp
10900 .set_request_handler::<lsp::request::Formatting, _, _>({
10901 let buffer_changes = buffer_changes.clone();
10902 move |_, _| {
10903 // When formatting is requested, trailing whitespace has already been stripped,
10904 // and the trailing newline has already been added.
10905 assert_eq!(
10906 &buffer_changes.lock()[1..],
10907 &[
10908 (
10909 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
10910 "".into()
10911 ),
10912 (
10913 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
10914 "".into()
10915 ),
10916 (
10917 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
10918 "\n".into()
10919 ),
10920 ]
10921 );
10922
10923 // Insert blank lines between each line of the buffer.
10924 async move {
10925 Ok(Some(vec![
10926 lsp::TextEdit {
10927 range: lsp::Range::new(
10928 lsp::Position::new(1, 0),
10929 lsp::Position::new(1, 0),
10930 ),
10931 new_text: "\n".into(),
10932 },
10933 lsp::TextEdit {
10934 range: lsp::Range::new(
10935 lsp::Position::new(2, 0),
10936 lsp::Position::new(2, 0),
10937 ),
10938 new_text: "\n".into(),
10939 },
10940 ]))
10941 }
10942 }
10943 });
10944
10945 // After formatting the buffer, the trailing whitespace is stripped,
10946 // a newline is appended, and the edits provided by the language server
10947 // have been applied.
10948 format.await.unwrap();
10949 cx.assert_editor_state(
10950 &[
10951 "one", //
10952 "", //
10953 "twoˇ", //
10954 "", //
10955 "three", //
10956 "four", //
10957 "", //
10958 ]
10959 .join("\n"),
10960 );
10961
10962 // Undoing the formatting undoes the trailing whitespace removal, the
10963 // trailing newline, and the LSP edits.
10964 cx.update_buffer(|buffer, cx| buffer.undo(cx));
10965 cx.assert_editor_state(
10966 &[
10967 "one ", //
10968 "twoˇ", //
10969 "three ", //
10970 "four", //
10971 ]
10972 .join("\n"),
10973 );
10974}
10975
10976#[gpui::test]
10977async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
10978 cx: &mut TestAppContext,
10979) {
10980 init_test(cx, |_| {});
10981
10982 cx.update(|cx| {
10983 cx.update_global::<SettingsStore, _>(|settings, cx| {
10984 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10985 settings.auto_signature_help = Some(true);
10986 });
10987 });
10988 });
10989
10990 let mut cx = EditorLspTestContext::new_rust(
10991 lsp::ServerCapabilities {
10992 signature_help_provider: Some(lsp::SignatureHelpOptions {
10993 ..Default::default()
10994 }),
10995 ..Default::default()
10996 },
10997 cx,
10998 )
10999 .await;
11000
11001 let language = Language::new(
11002 LanguageConfig {
11003 name: "Rust".into(),
11004 brackets: BracketPairConfig {
11005 pairs: vec![
11006 BracketPair {
11007 start: "{".to_string(),
11008 end: "}".to_string(),
11009 close: true,
11010 surround: true,
11011 newline: true,
11012 },
11013 BracketPair {
11014 start: "(".to_string(),
11015 end: ")".to_string(),
11016 close: true,
11017 surround: true,
11018 newline: true,
11019 },
11020 BracketPair {
11021 start: "/*".to_string(),
11022 end: " */".to_string(),
11023 close: true,
11024 surround: true,
11025 newline: true,
11026 },
11027 BracketPair {
11028 start: "[".to_string(),
11029 end: "]".to_string(),
11030 close: false,
11031 surround: false,
11032 newline: true,
11033 },
11034 BracketPair {
11035 start: "\"".to_string(),
11036 end: "\"".to_string(),
11037 close: true,
11038 surround: true,
11039 newline: false,
11040 },
11041 BracketPair {
11042 start: "<".to_string(),
11043 end: ">".to_string(),
11044 close: false,
11045 surround: true,
11046 newline: true,
11047 },
11048 ],
11049 ..Default::default()
11050 },
11051 autoclose_before: "})]".to_string(),
11052 ..Default::default()
11053 },
11054 Some(tree_sitter_rust::LANGUAGE.into()),
11055 );
11056 let language = Arc::new(language);
11057
11058 cx.language_registry().add(language.clone());
11059 cx.update_buffer(|buffer, cx| {
11060 buffer.set_language(Some(language), cx);
11061 });
11062
11063 cx.set_state(
11064 &r#"
11065 fn main() {
11066 sampleˇ
11067 }
11068 "#
11069 .unindent(),
11070 );
11071
11072 cx.update_editor(|editor, window, cx| {
11073 editor.handle_input("(", window, cx);
11074 });
11075 cx.assert_editor_state(
11076 &"
11077 fn main() {
11078 sample(ˇ)
11079 }
11080 "
11081 .unindent(),
11082 );
11083
11084 let mocked_response = lsp::SignatureHelp {
11085 signatures: vec![lsp::SignatureInformation {
11086 label: "fn sample(param1: u8, param2: u8)".to_string(),
11087 documentation: None,
11088 parameters: Some(vec![
11089 lsp::ParameterInformation {
11090 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11091 documentation: None,
11092 },
11093 lsp::ParameterInformation {
11094 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11095 documentation: None,
11096 },
11097 ]),
11098 active_parameter: None,
11099 }],
11100 active_signature: Some(0),
11101 active_parameter: Some(0),
11102 };
11103 handle_signature_help_request(&mut cx, mocked_response).await;
11104
11105 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11106 .await;
11107
11108 cx.editor(|editor, _, _| {
11109 let signature_help_state = editor.signature_help_state.popover().cloned();
11110 let signature = signature_help_state.unwrap();
11111 assert_eq!(
11112 signature.signatures[signature.current_signature].label,
11113 "fn sample(param1: u8, param2: u8)"
11114 );
11115 });
11116}
11117
11118#[gpui::test]
11119async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
11120 init_test(cx, |_| {});
11121
11122 cx.update(|cx| {
11123 cx.update_global::<SettingsStore, _>(|settings, cx| {
11124 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11125 settings.auto_signature_help = Some(false);
11126 settings.show_signature_help_after_edits = Some(false);
11127 });
11128 });
11129 });
11130
11131 let mut cx = EditorLspTestContext::new_rust(
11132 lsp::ServerCapabilities {
11133 signature_help_provider: Some(lsp::SignatureHelpOptions {
11134 ..Default::default()
11135 }),
11136 ..Default::default()
11137 },
11138 cx,
11139 )
11140 .await;
11141
11142 let language = Language::new(
11143 LanguageConfig {
11144 name: "Rust".into(),
11145 brackets: BracketPairConfig {
11146 pairs: vec![
11147 BracketPair {
11148 start: "{".to_string(),
11149 end: "}".to_string(),
11150 close: true,
11151 surround: true,
11152 newline: true,
11153 },
11154 BracketPair {
11155 start: "(".to_string(),
11156 end: ")".to_string(),
11157 close: true,
11158 surround: true,
11159 newline: true,
11160 },
11161 BracketPair {
11162 start: "/*".to_string(),
11163 end: " */".to_string(),
11164 close: true,
11165 surround: true,
11166 newline: true,
11167 },
11168 BracketPair {
11169 start: "[".to_string(),
11170 end: "]".to_string(),
11171 close: false,
11172 surround: false,
11173 newline: true,
11174 },
11175 BracketPair {
11176 start: "\"".to_string(),
11177 end: "\"".to_string(),
11178 close: true,
11179 surround: true,
11180 newline: false,
11181 },
11182 BracketPair {
11183 start: "<".to_string(),
11184 end: ">".to_string(),
11185 close: false,
11186 surround: true,
11187 newline: true,
11188 },
11189 ],
11190 ..Default::default()
11191 },
11192 autoclose_before: "})]".to_string(),
11193 ..Default::default()
11194 },
11195 Some(tree_sitter_rust::LANGUAGE.into()),
11196 );
11197 let language = Arc::new(language);
11198
11199 cx.language_registry().add(language.clone());
11200 cx.update_buffer(|buffer, cx| {
11201 buffer.set_language(Some(language), cx);
11202 });
11203
11204 // Ensure that signature_help is not called when no signature help is enabled.
11205 cx.set_state(
11206 &r#"
11207 fn main() {
11208 sampleˇ
11209 }
11210 "#
11211 .unindent(),
11212 );
11213 cx.update_editor(|editor, window, cx| {
11214 editor.handle_input("(", window, cx);
11215 });
11216 cx.assert_editor_state(
11217 &"
11218 fn main() {
11219 sample(ˇ)
11220 }
11221 "
11222 .unindent(),
11223 );
11224 cx.editor(|editor, _, _| {
11225 assert!(editor.signature_help_state.task().is_none());
11226 });
11227
11228 let mocked_response = lsp::SignatureHelp {
11229 signatures: vec![lsp::SignatureInformation {
11230 label: "fn sample(param1: u8, param2: u8)".to_string(),
11231 documentation: None,
11232 parameters: Some(vec![
11233 lsp::ParameterInformation {
11234 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11235 documentation: None,
11236 },
11237 lsp::ParameterInformation {
11238 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11239 documentation: None,
11240 },
11241 ]),
11242 active_parameter: None,
11243 }],
11244 active_signature: Some(0),
11245 active_parameter: Some(0),
11246 };
11247
11248 // Ensure that signature_help is called when enabled afte edits
11249 cx.update(|_, cx| {
11250 cx.update_global::<SettingsStore, _>(|settings, cx| {
11251 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11252 settings.auto_signature_help = Some(false);
11253 settings.show_signature_help_after_edits = Some(true);
11254 });
11255 });
11256 });
11257 cx.set_state(
11258 &r#"
11259 fn main() {
11260 sampleˇ
11261 }
11262 "#
11263 .unindent(),
11264 );
11265 cx.update_editor(|editor, window, cx| {
11266 editor.handle_input("(", window, cx);
11267 });
11268 cx.assert_editor_state(
11269 &"
11270 fn main() {
11271 sample(ˇ)
11272 }
11273 "
11274 .unindent(),
11275 );
11276 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11277 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11278 .await;
11279 cx.update_editor(|editor, _, _| {
11280 let signature_help_state = editor.signature_help_state.popover().cloned();
11281 assert!(signature_help_state.is_some());
11282 let signature = signature_help_state.unwrap();
11283 assert_eq!(
11284 signature.signatures[signature.current_signature].label,
11285 "fn sample(param1: u8, param2: u8)"
11286 );
11287 editor.signature_help_state = SignatureHelpState::default();
11288 });
11289
11290 // Ensure that signature_help is called when auto signature help override is enabled
11291 cx.update(|_, cx| {
11292 cx.update_global::<SettingsStore, _>(|settings, cx| {
11293 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11294 settings.auto_signature_help = Some(true);
11295 settings.show_signature_help_after_edits = Some(false);
11296 });
11297 });
11298 });
11299 cx.set_state(
11300 &r#"
11301 fn main() {
11302 sampleˇ
11303 }
11304 "#
11305 .unindent(),
11306 );
11307 cx.update_editor(|editor, window, cx| {
11308 editor.handle_input("(", window, cx);
11309 });
11310 cx.assert_editor_state(
11311 &"
11312 fn main() {
11313 sample(ˇ)
11314 }
11315 "
11316 .unindent(),
11317 );
11318 handle_signature_help_request(&mut cx, mocked_response).await;
11319 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11320 .await;
11321 cx.editor(|editor, _, _| {
11322 let signature_help_state = editor.signature_help_state.popover().cloned();
11323 assert!(signature_help_state.is_some());
11324 let signature = signature_help_state.unwrap();
11325 assert_eq!(
11326 signature.signatures[signature.current_signature].label,
11327 "fn sample(param1: u8, param2: u8)"
11328 );
11329 });
11330}
11331
11332#[gpui::test]
11333async fn test_signature_help(cx: &mut TestAppContext) {
11334 init_test(cx, |_| {});
11335 cx.update(|cx| {
11336 cx.update_global::<SettingsStore, _>(|settings, cx| {
11337 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11338 settings.auto_signature_help = Some(true);
11339 });
11340 });
11341 });
11342
11343 let mut cx = EditorLspTestContext::new_rust(
11344 lsp::ServerCapabilities {
11345 signature_help_provider: Some(lsp::SignatureHelpOptions {
11346 ..Default::default()
11347 }),
11348 ..Default::default()
11349 },
11350 cx,
11351 )
11352 .await;
11353
11354 // A test that directly calls `show_signature_help`
11355 cx.update_editor(|editor, window, cx| {
11356 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11357 });
11358
11359 let mocked_response = lsp::SignatureHelp {
11360 signatures: vec![lsp::SignatureInformation {
11361 label: "fn sample(param1: u8, param2: u8)".to_string(),
11362 documentation: None,
11363 parameters: Some(vec![
11364 lsp::ParameterInformation {
11365 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11366 documentation: None,
11367 },
11368 lsp::ParameterInformation {
11369 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11370 documentation: None,
11371 },
11372 ]),
11373 active_parameter: None,
11374 }],
11375 active_signature: Some(0),
11376 active_parameter: Some(0),
11377 };
11378 handle_signature_help_request(&mut cx, mocked_response).await;
11379
11380 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11381 .await;
11382
11383 cx.editor(|editor, _, _| {
11384 let signature_help_state = editor.signature_help_state.popover().cloned();
11385 assert!(signature_help_state.is_some());
11386 let signature = signature_help_state.unwrap();
11387 assert_eq!(
11388 signature.signatures[signature.current_signature].label,
11389 "fn sample(param1: u8, param2: u8)"
11390 );
11391 });
11392
11393 // When exiting outside from inside the brackets, `signature_help` is closed.
11394 cx.set_state(indoc! {"
11395 fn main() {
11396 sample(ˇ);
11397 }
11398
11399 fn sample(param1: u8, param2: u8) {}
11400 "});
11401
11402 cx.update_editor(|editor, window, cx| {
11403 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11404 s.select_ranges([0..0])
11405 });
11406 });
11407
11408 let mocked_response = lsp::SignatureHelp {
11409 signatures: Vec::new(),
11410 active_signature: None,
11411 active_parameter: None,
11412 };
11413 handle_signature_help_request(&mut cx, mocked_response).await;
11414
11415 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11416 .await;
11417
11418 cx.editor(|editor, _, _| {
11419 assert!(!editor.signature_help_state.is_shown());
11420 });
11421
11422 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
11423 cx.set_state(indoc! {"
11424 fn main() {
11425 sample(ˇ);
11426 }
11427
11428 fn sample(param1: u8, param2: u8) {}
11429 "});
11430
11431 let mocked_response = lsp::SignatureHelp {
11432 signatures: vec![lsp::SignatureInformation {
11433 label: "fn sample(param1: u8, param2: u8)".to_string(),
11434 documentation: None,
11435 parameters: Some(vec![
11436 lsp::ParameterInformation {
11437 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11438 documentation: None,
11439 },
11440 lsp::ParameterInformation {
11441 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11442 documentation: None,
11443 },
11444 ]),
11445 active_parameter: None,
11446 }],
11447 active_signature: Some(0),
11448 active_parameter: Some(0),
11449 };
11450 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11451 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11452 .await;
11453 cx.editor(|editor, _, _| {
11454 assert!(editor.signature_help_state.is_shown());
11455 });
11456
11457 // Restore the popover with more parameter input
11458 cx.set_state(indoc! {"
11459 fn main() {
11460 sample(param1, param2ˇ);
11461 }
11462
11463 fn sample(param1: u8, param2: u8) {}
11464 "});
11465
11466 let mocked_response = lsp::SignatureHelp {
11467 signatures: vec![lsp::SignatureInformation {
11468 label: "fn sample(param1: u8, param2: u8)".to_string(),
11469 documentation: None,
11470 parameters: Some(vec![
11471 lsp::ParameterInformation {
11472 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11473 documentation: None,
11474 },
11475 lsp::ParameterInformation {
11476 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11477 documentation: None,
11478 },
11479 ]),
11480 active_parameter: None,
11481 }],
11482 active_signature: Some(0),
11483 active_parameter: Some(1),
11484 };
11485 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11486 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11487 .await;
11488
11489 // When selecting a range, the popover is gone.
11490 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
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, 25)..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 cx.editor(|editor, _, _| {
11504 assert!(!editor.signature_help_state.is_shown());
11505 });
11506
11507 // When unselecting again, the popover is back if within the brackets.
11508 cx.update_editor(|editor, window, cx| {
11509 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11510 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11511 })
11512 });
11513 cx.assert_editor_state(indoc! {"
11514 fn main() {
11515 sample(param1, ˇparam2);
11516 }
11517
11518 fn sample(param1: u8, param2: u8) {}
11519 "});
11520 handle_signature_help_request(&mut cx, mocked_response).await;
11521 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11522 .await;
11523 cx.editor(|editor, _, _| {
11524 assert!(editor.signature_help_state.is_shown());
11525 });
11526
11527 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
11528 cx.update_editor(|editor, window, cx| {
11529 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11530 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
11531 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11532 })
11533 });
11534 cx.assert_editor_state(indoc! {"
11535 fn main() {
11536 sample(param1, ˇparam2);
11537 }
11538
11539 fn sample(param1: u8, param2: u8) {}
11540 "});
11541
11542 let mocked_response = lsp::SignatureHelp {
11543 signatures: vec![lsp::SignatureInformation {
11544 label: "fn sample(param1: u8, param2: u8)".to_string(),
11545 documentation: None,
11546 parameters: Some(vec![
11547 lsp::ParameterInformation {
11548 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11549 documentation: None,
11550 },
11551 lsp::ParameterInformation {
11552 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11553 documentation: None,
11554 },
11555 ]),
11556 active_parameter: None,
11557 }],
11558 active_signature: Some(0),
11559 active_parameter: Some(1),
11560 };
11561 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11562 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11563 .await;
11564 cx.update_editor(|editor, _, cx| {
11565 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
11566 });
11567 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11568 .await;
11569 cx.update_editor(|editor, window, cx| {
11570 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11571 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11572 })
11573 });
11574 cx.assert_editor_state(indoc! {"
11575 fn main() {
11576 sample(param1, «ˇparam2»);
11577 }
11578
11579 fn sample(param1: u8, param2: u8) {}
11580 "});
11581 cx.update_editor(|editor, window, cx| {
11582 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11583 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11584 })
11585 });
11586 cx.assert_editor_state(indoc! {"
11587 fn main() {
11588 sample(param1, ˇparam2);
11589 }
11590
11591 fn sample(param1: u8, param2: u8) {}
11592 "});
11593 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
11594 .await;
11595}
11596
11597#[gpui::test]
11598async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
11599 init_test(cx, |_| {});
11600
11601 let mut cx = EditorLspTestContext::new_rust(
11602 lsp::ServerCapabilities {
11603 signature_help_provider: Some(lsp::SignatureHelpOptions {
11604 ..Default::default()
11605 }),
11606 ..Default::default()
11607 },
11608 cx,
11609 )
11610 .await;
11611
11612 cx.set_state(indoc! {"
11613 fn main() {
11614 overloadedˇ
11615 }
11616 "});
11617
11618 cx.update_editor(|editor, window, cx| {
11619 editor.handle_input("(", window, cx);
11620 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11621 });
11622
11623 // Mock response with 3 signatures
11624 let mocked_response = lsp::SignatureHelp {
11625 signatures: vec![
11626 lsp::SignatureInformation {
11627 label: "fn overloaded(x: i32)".to_string(),
11628 documentation: None,
11629 parameters: Some(vec![lsp::ParameterInformation {
11630 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11631 documentation: None,
11632 }]),
11633 active_parameter: None,
11634 },
11635 lsp::SignatureInformation {
11636 label: "fn overloaded(x: i32, y: i32)".to_string(),
11637 documentation: None,
11638 parameters: Some(vec![
11639 lsp::ParameterInformation {
11640 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11641 documentation: None,
11642 },
11643 lsp::ParameterInformation {
11644 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11645 documentation: None,
11646 },
11647 ]),
11648 active_parameter: None,
11649 },
11650 lsp::SignatureInformation {
11651 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
11652 documentation: None,
11653 parameters: Some(vec![
11654 lsp::ParameterInformation {
11655 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11656 documentation: None,
11657 },
11658 lsp::ParameterInformation {
11659 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11660 documentation: None,
11661 },
11662 lsp::ParameterInformation {
11663 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
11664 documentation: None,
11665 },
11666 ]),
11667 active_parameter: None,
11668 },
11669 ],
11670 active_signature: Some(1),
11671 active_parameter: Some(0),
11672 };
11673 handle_signature_help_request(&mut cx, mocked_response).await;
11674
11675 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11676 .await;
11677
11678 // Verify we have multiple signatures and the right one is selected
11679 cx.editor(|editor, _, _| {
11680 let popover = editor.signature_help_state.popover().cloned().unwrap();
11681 assert_eq!(popover.signatures.len(), 3);
11682 // active_signature was 1, so that should be the current
11683 assert_eq!(popover.current_signature, 1);
11684 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
11685 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
11686 assert_eq!(
11687 popover.signatures[2].label,
11688 "fn overloaded(x: i32, y: i32, z: i32)"
11689 );
11690 });
11691
11692 // Test navigation functionality
11693 cx.update_editor(|editor, window, cx| {
11694 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11695 });
11696
11697 cx.editor(|editor, _, _| {
11698 let popover = editor.signature_help_state.popover().cloned().unwrap();
11699 assert_eq!(popover.current_signature, 2);
11700 });
11701
11702 // Test wrap around
11703 cx.update_editor(|editor, window, cx| {
11704 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11705 });
11706
11707 cx.editor(|editor, _, _| {
11708 let popover = editor.signature_help_state.popover().cloned().unwrap();
11709 assert_eq!(popover.current_signature, 0);
11710 });
11711
11712 // Test previous navigation
11713 cx.update_editor(|editor, window, cx| {
11714 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
11715 });
11716
11717 cx.editor(|editor, _, _| {
11718 let popover = editor.signature_help_state.popover().cloned().unwrap();
11719 assert_eq!(popover.current_signature, 2);
11720 });
11721}
11722
11723#[gpui::test]
11724async fn test_completion_mode(cx: &mut TestAppContext) {
11725 init_test(cx, |_| {});
11726 let mut cx = EditorLspTestContext::new_rust(
11727 lsp::ServerCapabilities {
11728 completion_provider: Some(lsp::CompletionOptions {
11729 resolve_provider: Some(true),
11730 ..Default::default()
11731 }),
11732 ..Default::default()
11733 },
11734 cx,
11735 )
11736 .await;
11737
11738 struct Run {
11739 run_description: &'static str,
11740 initial_state: String,
11741 buffer_marked_text: String,
11742 completion_label: &'static str,
11743 completion_text: &'static str,
11744 expected_with_insert_mode: String,
11745 expected_with_replace_mode: String,
11746 expected_with_replace_subsequence_mode: String,
11747 expected_with_replace_suffix_mode: String,
11748 }
11749
11750 let runs = [
11751 Run {
11752 run_description: "Start of word matches completion text",
11753 initial_state: "before ediˇ after".into(),
11754 buffer_marked_text: "before <edi|> after".into(),
11755 completion_label: "editor",
11756 completion_text: "editor",
11757 expected_with_insert_mode: "before editorˇ after".into(),
11758 expected_with_replace_mode: "before editorˇ after".into(),
11759 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11760 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11761 },
11762 Run {
11763 run_description: "Accept same text at the middle of the word",
11764 initial_state: "before ediˇtor after".into(),
11765 buffer_marked_text: "before <edi|tor> after".into(),
11766 completion_label: "editor",
11767 completion_text: "editor",
11768 expected_with_insert_mode: "before editorˇtor after".into(),
11769 expected_with_replace_mode: "before editorˇ after".into(),
11770 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11771 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11772 },
11773 Run {
11774 run_description: "End of word matches completion text -- cursor at end",
11775 initial_state: "before torˇ after".into(),
11776 buffer_marked_text: "before <tor|> after".into(),
11777 completion_label: "editor",
11778 completion_text: "editor",
11779 expected_with_insert_mode: "before editorˇ after".into(),
11780 expected_with_replace_mode: "before editorˇ after".into(),
11781 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11782 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11783 },
11784 Run {
11785 run_description: "End of word matches completion text -- cursor at start",
11786 initial_state: "before ˇtor after".into(),
11787 buffer_marked_text: "before <|tor> after".into(),
11788 completion_label: "editor",
11789 completion_text: "editor",
11790 expected_with_insert_mode: "before editorˇtor after".into(),
11791 expected_with_replace_mode: "before editorˇ after".into(),
11792 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11793 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11794 },
11795 Run {
11796 run_description: "Prepend text containing whitespace",
11797 initial_state: "pˇfield: bool".into(),
11798 buffer_marked_text: "<p|field>: bool".into(),
11799 completion_label: "pub ",
11800 completion_text: "pub ",
11801 expected_with_insert_mode: "pub ˇfield: bool".into(),
11802 expected_with_replace_mode: "pub ˇ: bool".into(),
11803 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
11804 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
11805 },
11806 Run {
11807 run_description: "Add element to start of list",
11808 initial_state: "[element_ˇelement_2]".into(),
11809 buffer_marked_text: "[<element_|element_2>]".into(),
11810 completion_label: "element_1",
11811 completion_text: "element_1",
11812 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
11813 expected_with_replace_mode: "[element_1ˇ]".into(),
11814 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
11815 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
11816 },
11817 Run {
11818 run_description: "Add element to start of list -- first and second elements are equal",
11819 initial_state: "[elˇelement]".into(),
11820 buffer_marked_text: "[<el|element>]".into(),
11821 completion_label: "element",
11822 completion_text: "element",
11823 expected_with_insert_mode: "[elementˇelement]".into(),
11824 expected_with_replace_mode: "[elementˇ]".into(),
11825 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
11826 expected_with_replace_suffix_mode: "[elementˇ]".into(),
11827 },
11828 Run {
11829 run_description: "Ends with matching suffix",
11830 initial_state: "SubˇError".into(),
11831 buffer_marked_text: "<Sub|Error>".into(),
11832 completion_label: "SubscriptionError",
11833 completion_text: "SubscriptionError",
11834 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
11835 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11836 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11837 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
11838 },
11839 Run {
11840 run_description: "Suffix is a subsequence -- contiguous",
11841 initial_state: "SubˇErr".into(),
11842 buffer_marked_text: "<Sub|Err>".into(),
11843 completion_label: "SubscriptionError",
11844 completion_text: "SubscriptionError",
11845 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
11846 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11847 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11848 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
11849 },
11850 Run {
11851 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
11852 initial_state: "Suˇscrirr".into(),
11853 buffer_marked_text: "<Su|scrirr>".into(),
11854 completion_label: "SubscriptionError",
11855 completion_text: "SubscriptionError",
11856 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
11857 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11858 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11859 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
11860 },
11861 Run {
11862 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
11863 initial_state: "foo(indˇix)".into(),
11864 buffer_marked_text: "foo(<ind|ix>)".into(),
11865 completion_label: "node_index",
11866 completion_text: "node_index",
11867 expected_with_insert_mode: "foo(node_indexˇix)".into(),
11868 expected_with_replace_mode: "foo(node_indexˇ)".into(),
11869 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
11870 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
11871 },
11872 Run {
11873 run_description: "Replace range ends before cursor - should extend to cursor",
11874 initial_state: "before editˇo after".into(),
11875 buffer_marked_text: "before <{ed}>it|o after".into(),
11876 completion_label: "editor",
11877 completion_text: "editor",
11878 expected_with_insert_mode: "before editorˇo after".into(),
11879 expected_with_replace_mode: "before editorˇo after".into(),
11880 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
11881 expected_with_replace_suffix_mode: "before editorˇo after".into(),
11882 },
11883 Run {
11884 run_description: "Uses label for suffix matching",
11885 initial_state: "before ediˇtor after".into(),
11886 buffer_marked_text: "before <edi|tor> after".into(),
11887 completion_label: "editor",
11888 completion_text: "editor()",
11889 expected_with_insert_mode: "before editor()ˇtor after".into(),
11890 expected_with_replace_mode: "before editor()ˇ after".into(),
11891 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
11892 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
11893 },
11894 Run {
11895 run_description: "Case insensitive subsequence and suffix matching",
11896 initial_state: "before EDiˇtoR after".into(),
11897 buffer_marked_text: "before <EDi|toR> after".into(),
11898 completion_label: "editor",
11899 completion_text: "editor",
11900 expected_with_insert_mode: "before editorˇtoR after".into(),
11901 expected_with_replace_mode: "before editorˇ after".into(),
11902 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11903 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11904 },
11905 ];
11906
11907 for run in runs {
11908 let run_variations = [
11909 (LspInsertMode::Insert, run.expected_with_insert_mode),
11910 (LspInsertMode::Replace, run.expected_with_replace_mode),
11911 (
11912 LspInsertMode::ReplaceSubsequence,
11913 run.expected_with_replace_subsequence_mode,
11914 ),
11915 (
11916 LspInsertMode::ReplaceSuffix,
11917 run.expected_with_replace_suffix_mode,
11918 ),
11919 ];
11920
11921 for (lsp_insert_mode, expected_text) in run_variations {
11922 eprintln!(
11923 "run = {:?}, mode = {lsp_insert_mode:.?}",
11924 run.run_description,
11925 );
11926
11927 update_test_language_settings(&mut cx, |settings| {
11928 settings.defaults.completions = Some(CompletionSettings {
11929 lsp_insert_mode,
11930 words: WordsCompletionMode::Disabled,
11931 lsp: true,
11932 lsp_fetch_timeout_ms: 0,
11933 });
11934 });
11935
11936 cx.set_state(&run.initial_state);
11937 cx.update_editor(|editor, window, cx| {
11938 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11939 });
11940
11941 let counter = Arc::new(AtomicUsize::new(0));
11942 handle_completion_request_with_insert_and_replace(
11943 &mut cx,
11944 &run.buffer_marked_text,
11945 vec![(run.completion_label, run.completion_text)],
11946 counter.clone(),
11947 )
11948 .await;
11949 cx.condition(|editor, _| editor.context_menu_visible())
11950 .await;
11951 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11952
11953 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11954 editor
11955 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11956 .unwrap()
11957 });
11958 cx.assert_editor_state(&expected_text);
11959 handle_resolve_completion_request(&mut cx, None).await;
11960 apply_additional_edits.await.unwrap();
11961 }
11962 }
11963}
11964
11965#[gpui::test]
11966async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
11967 init_test(cx, |_| {});
11968 let mut cx = EditorLspTestContext::new_rust(
11969 lsp::ServerCapabilities {
11970 completion_provider: Some(lsp::CompletionOptions {
11971 resolve_provider: Some(true),
11972 ..Default::default()
11973 }),
11974 ..Default::default()
11975 },
11976 cx,
11977 )
11978 .await;
11979
11980 let initial_state = "SubˇError";
11981 let buffer_marked_text = "<Sub|Error>";
11982 let completion_text = "SubscriptionError";
11983 let expected_with_insert_mode = "SubscriptionErrorˇError";
11984 let expected_with_replace_mode = "SubscriptionErrorˇ";
11985
11986 update_test_language_settings(&mut cx, |settings| {
11987 settings.defaults.completions = Some(CompletionSettings {
11988 words: WordsCompletionMode::Disabled,
11989 // set the opposite here to ensure that the action is overriding the default behavior
11990 lsp_insert_mode: LspInsertMode::Insert,
11991 lsp: true,
11992 lsp_fetch_timeout_ms: 0,
11993 });
11994 });
11995
11996 cx.set_state(initial_state);
11997 cx.update_editor(|editor, window, cx| {
11998 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11999 });
12000
12001 let counter = Arc::new(AtomicUsize::new(0));
12002 handle_completion_request_with_insert_and_replace(
12003 &mut cx,
12004 &buffer_marked_text,
12005 vec![(completion_text, completion_text)],
12006 counter.clone(),
12007 )
12008 .await;
12009 cx.condition(|editor, _| editor.context_menu_visible())
12010 .await;
12011 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12012
12013 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12014 editor
12015 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12016 .unwrap()
12017 });
12018 cx.assert_editor_state(&expected_with_replace_mode);
12019 handle_resolve_completion_request(&mut cx, None).await;
12020 apply_additional_edits.await.unwrap();
12021
12022 update_test_language_settings(&mut cx, |settings| {
12023 settings.defaults.completions = Some(CompletionSettings {
12024 words: WordsCompletionMode::Disabled,
12025 // set the opposite here to ensure that the action is overriding the default behavior
12026 lsp_insert_mode: LspInsertMode::Replace,
12027 lsp: true,
12028 lsp_fetch_timeout_ms: 0,
12029 });
12030 });
12031
12032 cx.set_state(initial_state);
12033 cx.update_editor(|editor, window, cx| {
12034 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12035 });
12036 handle_completion_request_with_insert_and_replace(
12037 &mut cx,
12038 &buffer_marked_text,
12039 vec![(completion_text, completion_text)],
12040 counter.clone(),
12041 )
12042 .await;
12043 cx.condition(|editor, _| editor.context_menu_visible())
12044 .await;
12045 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12046
12047 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12048 editor
12049 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
12050 .unwrap()
12051 });
12052 cx.assert_editor_state(&expected_with_insert_mode);
12053 handle_resolve_completion_request(&mut cx, None).await;
12054 apply_additional_edits.await.unwrap();
12055}
12056
12057#[gpui::test]
12058async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
12059 init_test(cx, |_| {});
12060 let mut cx = EditorLspTestContext::new_rust(
12061 lsp::ServerCapabilities {
12062 completion_provider: Some(lsp::CompletionOptions {
12063 resolve_provider: Some(true),
12064 ..Default::default()
12065 }),
12066 ..Default::default()
12067 },
12068 cx,
12069 )
12070 .await;
12071
12072 // scenario: surrounding text matches completion text
12073 let completion_text = "to_offset";
12074 let initial_state = indoc! {"
12075 1. buf.to_offˇsuffix
12076 2. buf.to_offˇsuf
12077 3. buf.to_offˇfix
12078 4. buf.to_offˇ
12079 5. into_offˇensive
12080 6. ˇsuffix
12081 7. let ˇ //
12082 8. aaˇzz
12083 9. buf.to_off«zzzzzˇ»suffix
12084 10. buf.«ˇzzzzz»suffix
12085 11. to_off«ˇzzzzz»
12086
12087 buf.to_offˇsuffix // newest cursor
12088 "};
12089 let completion_marked_buffer = indoc! {"
12090 1. buf.to_offsuffix
12091 2. buf.to_offsuf
12092 3. buf.to_offfix
12093 4. buf.to_off
12094 5. into_offensive
12095 6. suffix
12096 7. let //
12097 8. aazz
12098 9. buf.to_offzzzzzsuffix
12099 10. buf.zzzzzsuffix
12100 11. to_offzzzzz
12101
12102 buf.<to_off|suffix> // newest cursor
12103 "};
12104 let expected = indoc! {"
12105 1. buf.to_offsetˇ
12106 2. buf.to_offsetˇsuf
12107 3. buf.to_offsetˇfix
12108 4. buf.to_offsetˇ
12109 5. into_offsetˇensive
12110 6. to_offsetˇsuffix
12111 7. let to_offsetˇ //
12112 8. aato_offsetˇzz
12113 9. buf.to_offsetˇ
12114 10. buf.to_offsetˇsuffix
12115 11. to_offsetˇ
12116
12117 buf.to_offsetˇ // newest cursor
12118 "};
12119 cx.set_state(initial_state);
12120 cx.update_editor(|editor, window, cx| {
12121 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12122 });
12123 handle_completion_request_with_insert_and_replace(
12124 &mut cx,
12125 completion_marked_buffer,
12126 vec![(completion_text, completion_text)],
12127 Arc::new(AtomicUsize::new(0)),
12128 )
12129 .await;
12130 cx.condition(|editor, _| editor.context_menu_visible())
12131 .await;
12132 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12133 editor
12134 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12135 .unwrap()
12136 });
12137 cx.assert_editor_state(expected);
12138 handle_resolve_completion_request(&mut cx, None).await;
12139 apply_additional_edits.await.unwrap();
12140
12141 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
12142 let completion_text = "foo_and_bar";
12143 let initial_state = indoc! {"
12144 1. ooanbˇ
12145 2. zooanbˇ
12146 3. ooanbˇz
12147 4. zooanbˇz
12148 5. ooanˇ
12149 6. oanbˇ
12150
12151 ooanbˇ
12152 "};
12153 let completion_marked_buffer = indoc! {"
12154 1. ooanb
12155 2. zooanb
12156 3. ooanbz
12157 4. zooanbz
12158 5. ooan
12159 6. oanb
12160
12161 <ooanb|>
12162 "};
12163 let expected = indoc! {"
12164 1. foo_and_barˇ
12165 2. zfoo_and_barˇ
12166 3. foo_and_barˇz
12167 4. zfoo_and_barˇz
12168 5. ooanfoo_and_barˇ
12169 6. oanbfoo_and_barˇ
12170
12171 foo_and_barˇ
12172 "};
12173 cx.set_state(initial_state);
12174 cx.update_editor(|editor, window, cx| {
12175 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12176 });
12177 handle_completion_request_with_insert_and_replace(
12178 &mut cx,
12179 completion_marked_buffer,
12180 vec![(completion_text, completion_text)],
12181 Arc::new(AtomicUsize::new(0)),
12182 )
12183 .await;
12184 cx.condition(|editor, _| editor.context_menu_visible())
12185 .await;
12186 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12187 editor
12188 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12189 .unwrap()
12190 });
12191 cx.assert_editor_state(expected);
12192 handle_resolve_completion_request(&mut cx, None).await;
12193 apply_additional_edits.await.unwrap();
12194
12195 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
12196 // (expects the same as if it was inserted at the end)
12197 let completion_text = "foo_and_bar";
12198 let initial_state = indoc! {"
12199 1. ooˇanb
12200 2. zooˇanb
12201 3. ooˇanbz
12202 4. zooˇanbz
12203
12204 ooˇanb
12205 "};
12206 let completion_marked_buffer = indoc! {"
12207 1. ooanb
12208 2. zooanb
12209 3. ooanbz
12210 4. zooanbz
12211
12212 <oo|anb>
12213 "};
12214 let expected = indoc! {"
12215 1. foo_and_barˇ
12216 2. zfoo_and_barˇ
12217 3. foo_and_barˇz
12218 4. zfoo_and_barˇz
12219
12220 foo_and_barˇ
12221 "};
12222 cx.set_state(initial_state);
12223 cx.update_editor(|editor, window, cx| {
12224 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12225 });
12226 handle_completion_request_with_insert_and_replace(
12227 &mut cx,
12228 completion_marked_buffer,
12229 vec![(completion_text, completion_text)],
12230 Arc::new(AtomicUsize::new(0)),
12231 )
12232 .await;
12233 cx.condition(|editor, _| editor.context_menu_visible())
12234 .await;
12235 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12236 editor
12237 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12238 .unwrap()
12239 });
12240 cx.assert_editor_state(expected);
12241 handle_resolve_completion_request(&mut cx, None).await;
12242 apply_additional_edits.await.unwrap();
12243}
12244
12245// This used to crash
12246#[gpui::test]
12247async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
12248 init_test(cx, |_| {});
12249
12250 let buffer_text = indoc! {"
12251 fn main() {
12252 10.satu;
12253
12254 //
12255 // separate cursors so they open in different excerpts (manually reproducible)
12256 //
12257
12258 10.satu20;
12259 }
12260 "};
12261 let multibuffer_text_with_selections = indoc! {"
12262 fn main() {
12263 10.satuˇ;
12264
12265 //
12266
12267 //
12268
12269 10.satuˇ20;
12270 }
12271 "};
12272 let expected_multibuffer = indoc! {"
12273 fn main() {
12274 10.saturating_sub()ˇ;
12275
12276 //
12277
12278 //
12279
12280 10.saturating_sub()ˇ;
12281 }
12282 "};
12283
12284 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
12285 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
12286
12287 let fs = FakeFs::new(cx.executor());
12288 fs.insert_tree(
12289 path!("/a"),
12290 json!({
12291 "main.rs": buffer_text,
12292 }),
12293 )
12294 .await;
12295
12296 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12297 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12298 language_registry.add(rust_lang());
12299 let mut fake_servers = language_registry.register_fake_lsp(
12300 "Rust",
12301 FakeLspAdapter {
12302 capabilities: lsp::ServerCapabilities {
12303 completion_provider: Some(lsp::CompletionOptions {
12304 resolve_provider: None,
12305 ..lsp::CompletionOptions::default()
12306 }),
12307 ..lsp::ServerCapabilities::default()
12308 },
12309 ..FakeLspAdapter::default()
12310 },
12311 );
12312 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12313 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12314 let buffer = project
12315 .update(cx, |project, cx| {
12316 project.open_local_buffer(path!("/a/main.rs"), cx)
12317 })
12318 .await
12319 .unwrap();
12320
12321 let multi_buffer = cx.new(|cx| {
12322 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
12323 multi_buffer.push_excerpts(
12324 buffer.clone(),
12325 [ExcerptRange::new(0..first_excerpt_end)],
12326 cx,
12327 );
12328 multi_buffer.push_excerpts(
12329 buffer.clone(),
12330 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
12331 cx,
12332 );
12333 multi_buffer
12334 });
12335
12336 let editor = workspace
12337 .update(cx, |_, window, cx| {
12338 cx.new(|cx| {
12339 Editor::new(
12340 EditorMode::Full {
12341 scale_ui_elements_with_buffer_font_size: false,
12342 show_active_line_background: false,
12343 sized_by_content: false,
12344 },
12345 multi_buffer.clone(),
12346 Some(project.clone()),
12347 window,
12348 cx,
12349 )
12350 })
12351 })
12352 .unwrap();
12353
12354 let pane = workspace
12355 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12356 .unwrap();
12357 pane.update_in(cx, |pane, window, cx| {
12358 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
12359 });
12360
12361 let fake_server = fake_servers.next().await.unwrap();
12362
12363 editor.update_in(cx, |editor, window, cx| {
12364 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12365 s.select_ranges([
12366 Point::new(1, 11)..Point::new(1, 11),
12367 Point::new(7, 11)..Point::new(7, 11),
12368 ])
12369 });
12370
12371 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
12372 });
12373
12374 editor.update_in(cx, |editor, window, cx| {
12375 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12376 });
12377
12378 fake_server
12379 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12380 let completion_item = lsp::CompletionItem {
12381 label: "saturating_sub()".into(),
12382 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
12383 lsp::InsertReplaceEdit {
12384 new_text: "saturating_sub()".to_owned(),
12385 insert: lsp::Range::new(
12386 lsp::Position::new(7, 7),
12387 lsp::Position::new(7, 11),
12388 ),
12389 replace: lsp::Range::new(
12390 lsp::Position::new(7, 7),
12391 lsp::Position::new(7, 13),
12392 ),
12393 },
12394 )),
12395 ..lsp::CompletionItem::default()
12396 };
12397
12398 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
12399 })
12400 .next()
12401 .await
12402 .unwrap();
12403
12404 cx.condition(&editor, |editor, _| editor.context_menu_visible())
12405 .await;
12406
12407 editor
12408 .update_in(cx, |editor, window, cx| {
12409 editor
12410 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12411 .unwrap()
12412 })
12413 .await
12414 .unwrap();
12415
12416 editor.update(cx, |editor, cx| {
12417 assert_text_with_selections(editor, expected_multibuffer, cx);
12418 })
12419}
12420
12421#[gpui::test]
12422async fn test_completion(cx: &mut TestAppContext) {
12423 init_test(cx, |_| {});
12424
12425 let mut cx = EditorLspTestContext::new_rust(
12426 lsp::ServerCapabilities {
12427 completion_provider: Some(lsp::CompletionOptions {
12428 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12429 resolve_provider: Some(true),
12430 ..Default::default()
12431 }),
12432 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12433 ..Default::default()
12434 },
12435 cx,
12436 )
12437 .await;
12438 let counter = Arc::new(AtomicUsize::new(0));
12439
12440 cx.set_state(indoc! {"
12441 oneˇ
12442 two
12443 three
12444 "});
12445 cx.simulate_keystroke(".");
12446 handle_completion_request(
12447 indoc! {"
12448 one.|<>
12449 two
12450 three
12451 "},
12452 vec!["first_completion", "second_completion"],
12453 true,
12454 counter.clone(),
12455 &mut cx,
12456 )
12457 .await;
12458 cx.condition(|editor, _| editor.context_menu_visible())
12459 .await;
12460 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12461
12462 let _handler = handle_signature_help_request(
12463 &mut cx,
12464 lsp::SignatureHelp {
12465 signatures: vec![lsp::SignatureInformation {
12466 label: "test signature".to_string(),
12467 documentation: None,
12468 parameters: Some(vec![lsp::ParameterInformation {
12469 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
12470 documentation: None,
12471 }]),
12472 active_parameter: None,
12473 }],
12474 active_signature: None,
12475 active_parameter: None,
12476 },
12477 );
12478 cx.update_editor(|editor, window, cx| {
12479 assert!(
12480 !editor.signature_help_state.is_shown(),
12481 "No signature help was called for"
12482 );
12483 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12484 });
12485 cx.run_until_parked();
12486 cx.update_editor(|editor, _, _| {
12487 assert!(
12488 !editor.signature_help_state.is_shown(),
12489 "No signature help should be shown when completions menu is open"
12490 );
12491 });
12492
12493 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12494 editor.context_menu_next(&Default::default(), window, cx);
12495 editor
12496 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12497 .unwrap()
12498 });
12499 cx.assert_editor_state(indoc! {"
12500 one.second_completionˇ
12501 two
12502 three
12503 "});
12504
12505 handle_resolve_completion_request(
12506 &mut cx,
12507 Some(vec![
12508 (
12509 //This overlaps with the primary completion edit which is
12510 //misbehavior from the LSP spec, test that we filter it out
12511 indoc! {"
12512 one.second_ˇcompletion
12513 two
12514 threeˇ
12515 "},
12516 "overlapping additional edit",
12517 ),
12518 (
12519 indoc! {"
12520 one.second_completion
12521 two
12522 threeˇ
12523 "},
12524 "\nadditional edit",
12525 ),
12526 ]),
12527 )
12528 .await;
12529 apply_additional_edits.await.unwrap();
12530 cx.assert_editor_state(indoc! {"
12531 one.second_completionˇ
12532 two
12533 three
12534 additional edit
12535 "});
12536
12537 cx.set_state(indoc! {"
12538 one.second_completion
12539 twoˇ
12540 threeˇ
12541 additional edit
12542 "});
12543 cx.simulate_keystroke(" ");
12544 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12545 cx.simulate_keystroke("s");
12546 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12547
12548 cx.assert_editor_state(indoc! {"
12549 one.second_completion
12550 two sˇ
12551 three sˇ
12552 additional edit
12553 "});
12554 handle_completion_request(
12555 indoc! {"
12556 one.second_completion
12557 two s
12558 three <s|>
12559 additional edit
12560 "},
12561 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12562 true,
12563 counter.clone(),
12564 &mut cx,
12565 )
12566 .await;
12567 cx.condition(|editor, _| editor.context_menu_visible())
12568 .await;
12569 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12570
12571 cx.simulate_keystroke("i");
12572
12573 handle_completion_request(
12574 indoc! {"
12575 one.second_completion
12576 two si
12577 three <si|>
12578 additional edit
12579 "},
12580 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12581 true,
12582 counter.clone(),
12583 &mut cx,
12584 )
12585 .await;
12586 cx.condition(|editor, _| editor.context_menu_visible())
12587 .await;
12588 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12589
12590 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12591 editor
12592 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12593 .unwrap()
12594 });
12595 cx.assert_editor_state(indoc! {"
12596 one.second_completion
12597 two sixth_completionˇ
12598 three sixth_completionˇ
12599 additional edit
12600 "});
12601
12602 apply_additional_edits.await.unwrap();
12603
12604 update_test_language_settings(&mut cx, |settings| {
12605 settings.defaults.show_completions_on_input = Some(false);
12606 });
12607 cx.set_state("editorˇ");
12608 cx.simulate_keystroke(".");
12609 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12610 cx.simulate_keystrokes("c l o");
12611 cx.assert_editor_state("editor.cloˇ");
12612 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12613 cx.update_editor(|editor, window, cx| {
12614 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12615 });
12616 handle_completion_request(
12617 "editor.<clo|>",
12618 vec!["close", "clobber"],
12619 true,
12620 counter.clone(),
12621 &mut cx,
12622 )
12623 .await;
12624 cx.condition(|editor, _| editor.context_menu_visible())
12625 .await;
12626 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
12627
12628 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12629 editor
12630 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12631 .unwrap()
12632 });
12633 cx.assert_editor_state("editor.clobberˇ");
12634 handle_resolve_completion_request(&mut cx, None).await;
12635 apply_additional_edits.await.unwrap();
12636}
12637
12638#[gpui::test]
12639async fn test_completion_reuse(cx: &mut TestAppContext) {
12640 init_test(cx, |_| {});
12641
12642 let mut cx = EditorLspTestContext::new_rust(
12643 lsp::ServerCapabilities {
12644 completion_provider: Some(lsp::CompletionOptions {
12645 trigger_characters: Some(vec![".".to_string()]),
12646 ..Default::default()
12647 }),
12648 ..Default::default()
12649 },
12650 cx,
12651 )
12652 .await;
12653
12654 let counter = Arc::new(AtomicUsize::new(0));
12655 cx.set_state("objˇ");
12656 cx.simulate_keystroke(".");
12657
12658 // Initial completion request returns complete results
12659 let is_incomplete = false;
12660 handle_completion_request(
12661 "obj.|<>",
12662 vec!["a", "ab", "abc"],
12663 is_incomplete,
12664 counter.clone(),
12665 &mut cx,
12666 )
12667 .await;
12668 cx.run_until_parked();
12669 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12670 cx.assert_editor_state("obj.ˇ");
12671 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12672
12673 // Type "a" - filters existing completions
12674 cx.simulate_keystroke("a");
12675 cx.run_until_parked();
12676 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12677 cx.assert_editor_state("obj.aˇ");
12678 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12679
12680 // Type "b" - filters existing completions
12681 cx.simulate_keystroke("b");
12682 cx.run_until_parked();
12683 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12684 cx.assert_editor_state("obj.abˇ");
12685 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12686
12687 // Type "c" - filters existing completions
12688 cx.simulate_keystroke("c");
12689 cx.run_until_parked();
12690 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12691 cx.assert_editor_state("obj.abcˇ");
12692 check_displayed_completions(vec!["abc"], &mut cx);
12693
12694 // Backspace to delete "c" - filters existing completions
12695 cx.update_editor(|editor, window, cx| {
12696 editor.backspace(&Backspace, window, cx);
12697 });
12698 cx.run_until_parked();
12699 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12700 cx.assert_editor_state("obj.abˇ");
12701 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12702
12703 // Moving cursor to the left dismisses menu.
12704 cx.update_editor(|editor, window, cx| {
12705 editor.move_left(&MoveLeft, window, cx);
12706 });
12707 cx.run_until_parked();
12708 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12709 cx.assert_editor_state("obj.aˇb");
12710 cx.update_editor(|editor, _, _| {
12711 assert_eq!(editor.context_menu_visible(), false);
12712 });
12713
12714 // Type "b" - new request
12715 cx.simulate_keystroke("b");
12716 let is_incomplete = false;
12717 handle_completion_request(
12718 "obj.<ab|>a",
12719 vec!["ab", "abc"],
12720 is_incomplete,
12721 counter.clone(),
12722 &mut cx,
12723 )
12724 .await;
12725 cx.run_until_parked();
12726 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12727 cx.assert_editor_state("obj.abˇb");
12728 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12729
12730 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
12731 cx.update_editor(|editor, window, cx| {
12732 editor.backspace(&Backspace, window, cx);
12733 });
12734 let is_incomplete = false;
12735 handle_completion_request(
12736 "obj.<a|>b",
12737 vec!["a", "ab", "abc"],
12738 is_incomplete,
12739 counter.clone(),
12740 &mut cx,
12741 )
12742 .await;
12743 cx.run_until_parked();
12744 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12745 cx.assert_editor_state("obj.aˇb");
12746 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12747
12748 // Backspace to delete "a" - dismisses menu.
12749 cx.update_editor(|editor, window, cx| {
12750 editor.backspace(&Backspace, window, cx);
12751 });
12752 cx.run_until_parked();
12753 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12754 cx.assert_editor_state("obj.ˇb");
12755 cx.update_editor(|editor, _, _| {
12756 assert_eq!(editor.context_menu_visible(), false);
12757 });
12758}
12759
12760#[gpui::test]
12761async fn test_word_completion(cx: &mut TestAppContext) {
12762 let lsp_fetch_timeout_ms = 10;
12763 init_test(cx, |language_settings| {
12764 language_settings.defaults.completions = Some(CompletionSettings {
12765 words: WordsCompletionMode::Fallback,
12766 lsp: true,
12767 lsp_fetch_timeout_ms: 10,
12768 lsp_insert_mode: LspInsertMode::Insert,
12769 });
12770 });
12771
12772 let mut cx = EditorLspTestContext::new_rust(
12773 lsp::ServerCapabilities {
12774 completion_provider: Some(lsp::CompletionOptions {
12775 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12776 ..lsp::CompletionOptions::default()
12777 }),
12778 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12779 ..lsp::ServerCapabilities::default()
12780 },
12781 cx,
12782 )
12783 .await;
12784
12785 let throttle_completions = Arc::new(AtomicBool::new(false));
12786
12787 let lsp_throttle_completions = throttle_completions.clone();
12788 let _completion_requests_handler =
12789 cx.lsp
12790 .server
12791 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
12792 let lsp_throttle_completions = lsp_throttle_completions.clone();
12793 let cx = cx.clone();
12794 async move {
12795 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
12796 cx.background_executor()
12797 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
12798 .await;
12799 }
12800 Ok(Some(lsp::CompletionResponse::Array(vec![
12801 lsp::CompletionItem {
12802 label: "first".into(),
12803 ..lsp::CompletionItem::default()
12804 },
12805 lsp::CompletionItem {
12806 label: "last".into(),
12807 ..lsp::CompletionItem::default()
12808 },
12809 ])))
12810 }
12811 });
12812
12813 cx.set_state(indoc! {"
12814 oneˇ
12815 two
12816 three
12817 "});
12818 cx.simulate_keystroke(".");
12819 cx.executor().run_until_parked();
12820 cx.condition(|editor, _| editor.context_menu_visible())
12821 .await;
12822 cx.update_editor(|editor, window, cx| {
12823 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12824 {
12825 assert_eq!(
12826 completion_menu_entries(&menu),
12827 &["first", "last"],
12828 "When LSP server is fast to reply, no fallback word completions are used"
12829 );
12830 } else {
12831 panic!("expected completion menu to be open");
12832 }
12833 editor.cancel(&Cancel, window, cx);
12834 });
12835 cx.executor().run_until_parked();
12836 cx.condition(|editor, _| !editor.context_menu_visible())
12837 .await;
12838
12839 throttle_completions.store(true, atomic::Ordering::Release);
12840 cx.simulate_keystroke(".");
12841 cx.executor()
12842 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
12843 cx.executor().run_until_parked();
12844 cx.condition(|editor, _| editor.context_menu_visible())
12845 .await;
12846 cx.update_editor(|editor, _, _| {
12847 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12848 {
12849 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
12850 "When LSP server is slow, document words can be shown instead, if configured accordingly");
12851 } else {
12852 panic!("expected completion menu to be open");
12853 }
12854 });
12855}
12856
12857#[gpui::test]
12858async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
12859 init_test(cx, |language_settings| {
12860 language_settings.defaults.completions = Some(CompletionSettings {
12861 words: WordsCompletionMode::Enabled,
12862 lsp: true,
12863 lsp_fetch_timeout_ms: 0,
12864 lsp_insert_mode: LspInsertMode::Insert,
12865 });
12866 });
12867
12868 let mut cx = EditorLspTestContext::new_rust(
12869 lsp::ServerCapabilities {
12870 completion_provider: Some(lsp::CompletionOptions {
12871 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12872 ..lsp::CompletionOptions::default()
12873 }),
12874 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12875 ..lsp::ServerCapabilities::default()
12876 },
12877 cx,
12878 )
12879 .await;
12880
12881 let _completion_requests_handler =
12882 cx.lsp
12883 .server
12884 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12885 Ok(Some(lsp::CompletionResponse::Array(vec![
12886 lsp::CompletionItem {
12887 label: "first".into(),
12888 ..lsp::CompletionItem::default()
12889 },
12890 lsp::CompletionItem {
12891 label: "last".into(),
12892 ..lsp::CompletionItem::default()
12893 },
12894 ])))
12895 });
12896
12897 cx.set_state(indoc! {"ˇ
12898 first
12899 last
12900 second
12901 "});
12902 cx.simulate_keystroke(".");
12903 cx.executor().run_until_parked();
12904 cx.condition(|editor, _| editor.context_menu_visible())
12905 .await;
12906 cx.update_editor(|editor, _, _| {
12907 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12908 {
12909 assert_eq!(
12910 completion_menu_entries(&menu),
12911 &["first", "last", "second"],
12912 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
12913 );
12914 } else {
12915 panic!("expected completion menu to be open");
12916 }
12917 });
12918}
12919
12920#[gpui::test]
12921async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
12922 init_test(cx, |language_settings| {
12923 language_settings.defaults.completions = Some(CompletionSettings {
12924 words: WordsCompletionMode::Disabled,
12925 lsp: true,
12926 lsp_fetch_timeout_ms: 0,
12927 lsp_insert_mode: LspInsertMode::Insert,
12928 });
12929 });
12930
12931 let mut cx = EditorLspTestContext::new_rust(
12932 lsp::ServerCapabilities {
12933 completion_provider: Some(lsp::CompletionOptions {
12934 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12935 ..lsp::CompletionOptions::default()
12936 }),
12937 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12938 ..lsp::ServerCapabilities::default()
12939 },
12940 cx,
12941 )
12942 .await;
12943
12944 let _completion_requests_handler =
12945 cx.lsp
12946 .server
12947 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12948 panic!("LSP completions should not be queried when dealing with word completions")
12949 });
12950
12951 cx.set_state(indoc! {"ˇ
12952 first
12953 last
12954 second
12955 "});
12956 cx.update_editor(|editor, window, cx| {
12957 editor.show_word_completions(&ShowWordCompletions, window, cx);
12958 });
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 &["first", "last", "second"],
12968 "`ShowWordCompletions` action should show word completions"
12969 );
12970 } else {
12971 panic!("expected completion menu to be open");
12972 }
12973 });
12974
12975 cx.simulate_keystroke("l");
12976 cx.executor().run_until_parked();
12977 cx.condition(|editor, _| editor.context_menu_visible())
12978 .await;
12979 cx.update_editor(|editor, _, _| {
12980 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12981 {
12982 assert_eq!(
12983 completion_menu_entries(&menu),
12984 &["last"],
12985 "After showing word completions, further editing should filter them and not query the LSP"
12986 );
12987 } else {
12988 panic!("expected completion menu to be open");
12989 }
12990 });
12991}
12992
12993#[gpui::test]
12994async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
12995 init_test(cx, |language_settings| {
12996 language_settings.defaults.completions = Some(CompletionSettings {
12997 words: WordsCompletionMode::Fallback,
12998 lsp: false,
12999 lsp_fetch_timeout_ms: 0,
13000 lsp_insert_mode: LspInsertMode::Insert,
13001 });
13002 });
13003
13004 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13005
13006 cx.set_state(indoc! {"ˇ
13007 0_usize
13008 let
13009 33
13010 4.5f32
13011 "});
13012 cx.update_editor(|editor, window, cx| {
13013 editor.show_completions(&ShowCompletions::default(), window, cx);
13014 });
13015 cx.executor().run_until_parked();
13016 cx.condition(|editor, _| editor.context_menu_visible())
13017 .await;
13018 cx.update_editor(|editor, window, cx| {
13019 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13020 {
13021 assert_eq!(
13022 completion_menu_entries(&menu),
13023 &["let"],
13024 "With no digits in the completion query, no digits should be in the word completions"
13025 );
13026 } else {
13027 panic!("expected completion menu to be open");
13028 }
13029 editor.cancel(&Cancel, window, cx);
13030 });
13031
13032 cx.set_state(indoc! {"3ˇ
13033 0_usize
13034 let
13035 3
13036 33.35f32
13037 "});
13038 cx.update_editor(|editor, window, cx| {
13039 editor.show_completions(&ShowCompletions::default(), window, cx);
13040 });
13041 cx.executor().run_until_parked();
13042 cx.condition(|editor, _| editor.context_menu_visible())
13043 .await;
13044 cx.update_editor(|editor, _, _| {
13045 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13046 {
13047 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
13048 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
13049 } else {
13050 panic!("expected completion menu to be open");
13051 }
13052 });
13053}
13054
13055fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
13056 let position = || lsp::Position {
13057 line: params.text_document_position.position.line,
13058 character: params.text_document_position.position.character,
13059 };
13060 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13061 range: lsp::Range {
13062 start: position(),
13063 end: position(),
13064 },
13065 new_text: text.to_string(),
13066 }))
13067}
13068
13069#[gpui::test]
13070async fn test_multiline_completion(cx: &mut TestAppContext) {
13071 init_test(cx, |_| {});
13072
13073 let fs = FakeFs::new(cx.executor());
13074 fs.insert_tree(
13075 path!("/a"),
13076 json!({
13077 "main.ts": "a",
13078 }),
13079 )
13080 .await;
13081
13082 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13083 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13084 let typescript_language = Arc::new(Language::new(
13085 LanguageConfig {
13086 name: "TypeScript".into(),
13087 matcher: LanguageMatcher {
13088 path_suffixes: vec!["ts".to_string()],
13089 ..LanguageMatcher::default()
13090 },
13091 line_comments: vec!["// ".into()],
13092 ..LanguageConfig::default()
13093 },
13094 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13095 ));
13096 language_registry.add(typescript_language.clone());
13097 let mut fake_servers = language_registry.register_fake_lsp(
13098 "TypeScript",
13099 FakeLspAdapter {
13100 capabilities: lsp::ServerCapabilities {
13101 completion_provider: Some(lsp::CompletionOptions {
13102 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13103 ..lsp::CompletionOptions::default()
13104 }),
13105 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13106 ..lsp::ServerCapabilities::default()
13107 },
13108 // Emulate vtsls label generation
13109 label_for_completion: Some(Box::new(|item, _| {
13110 let text = if let Some(description) = item
13111 .label_details
13112 .as_ref()
13113 .and_then(|label_details| label_details.description.as_ref())
13114 {
13115 format!("{} {}", item.label, description)
13116 } else if let Some(detail) = &item.detail {
13117 format!("{} {}", item.label, detail)
13118 } else {
13119 item.label.clone()
13120 };
13121 let len = text.len();
13122 Some(language::CodeLabel {
13123 text,
13124 runs: Vec::new(),
13125 filter_range: 0..len,
13126 })
13127 })),
13128 ..FakeLspAdapter::default()
13129 },
13130 );
13131 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13132 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13133 let worktree_id = workspace
13134 .update(cx, |workspace, _window, cx| {
13135 workspace.project().update(cx, |project, cx| {
13136 project.worktrees(cx).next().unwrap().read(cx).id()
13137 })
13138 })
13139 .unwrap();
13140 let _buffer = project
13141 .update(cx, |project, cx| {
13142 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
13143 })
13144 .await
13145 .unwrap();
13146 let editor = workspace
13147 .update(cx, |workspace, window, cx| {
13148 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
13149 })
13150 .unwrap()
13151 .await
13152 .unwrap()
13153 .downcast::<Editor>()
13154 .unwrap();
13155 let fake_server = fake_servers.next().await.unwrap();
13156
13157 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
13158 let multiline_label_2 = "a\nb\nc\n";
13159 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
13160 let multiline_description = "d\ne\nf\n";
13161 let multiline_detail_2 = "g\nh\ni\n";
13162
13163 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
13164 move |params, _| async move {
13165 Ok(Some(lsp::CompletionResponse::Array(vec![
13166 lsp::CompletionItem {
13167 label: multiline_label.to_string(),
13168 text_edit: gen_text_edit(¶ms, "new_text_1"),
13169 ..lsp::CompletionItem::default()
13170 },
13171 lsp::CompletionItem {
13172 label: "single line label 1".to_string(),
13173 detail: Some(multiline_detail.to_string()),
13174 text_edit: gen_text_edit(¶ms, "new_text_2"),
13175 ..lsp::CompletionItem::default()
13176 },
13177 lsp::CompletionItem {
13178 label: "single line label 2".to_string(),
13179 label_details: Some(lsp::CompletionItemLabelDetails {
13180 description: Some(multiline_description.to_string()),
13181 detail: None,
13182 }),
13183 text_edit: gen_text_edit(¶ms, "new_text_2"),
13184 ..lsp::CompletionItem::default()
13185 },
13186 lsp::CompletionItem {
13187 label: multiline_label_2.to_string(),
13188 detail: Some(multiline_detail_2.to_string()),
13189 text_edit: gen_text_edit(¶ms, "new_text_3"),
13190 ..lsp::CompletionItem::default()
13191 },
13192 lsp::CompletionItem {
13193 label: "Label with many spaces and \t but without newlines".to_string(),
13194 detail: Some(
13195 "Details with many spaces and \t but without newlines".to_string(),
13196 ),
13197 text_edit: gen_text_edit(¶ms, "new_text_4"),
13198 ..lsp::CompletionItem::default()
13199 },
13200 ])))
13201 },
13202 );
13203
13204 editor.update_in(cx, |editor, window, cx| {
13205 cx.focus_self(window);
13206 editor.move_to_end(&MoveToEnd, window, cx);
13207 editor.handle_input(".", window, cx);
13208 });
13209 cx.run_until_parked();
13210 completion_handle.next().await.unwrap();
13211
13212 editor.update(cx, |editor, _| {
13213 assert!(editor.context_menu_visible());
13214 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13215 {
13216 let completion_labels = menu
13217 .completions
13218 .borrow()
13219 .iter()
13220 .map(|c| c.label.text.clone())
13221 .collect::<Vec<_>>();
13222 assert_eq!(
13223 completion_labels,
13224 &[
13225 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
13226 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
13227 "single line label 2 d e f ",
13228 "a b c g h i ",
13229 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
13230 ],
13231 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
13232 );
13233
13234 for completion in menu
13235 .completions
13236 .borrow()
13237 .iter() {
13238 assert_eq!(
13239 completion.label.filter_range,
13240 0..completion.label.text.len(),
13241 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
13242 );
13243 }
13244 } else {
13245 panic!("expected completion menu to be open");
13246 }
13247 });
13248}
13249
13250#[gpui::test]
13251async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
13252 init_test(cx, |_| {});
13253 let mut cx = EditorLspTestContext::new_rust(
13254 lsp::ServerCapabilities {
13255 completion_provider: Some(lsp::CompletionOptions {
13256 trigger_characters: Some(vec![".".to_string()]),
13257 ..Default::default()
13258 }),
13259 ..Default::default()
13260 },
13261 cx,
13262 )
13263 .await;
13264 cx.lsp
13265 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13266 Ok(Some(lsp::CompletionResponse::Array(vec![
13267 lsp::CompletionItem {
13268 label: "first".into(),
13269 ..Default::default()
13270 },
13271 lsp::CompletionItem {
13272 label: "last".into(),
13273 ..Default::default()
13274 },
13275 ])))
13276 });
13277 cx.set_state("variableˇ");
13278 cx.simulate_keystroke(".");
13279 cx.executor().run_until_parked();
13280
13281 cx.update_editor(|editor, _, _| {
13282 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13283 {
13284 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
13285 } else {
13286 panic!("expected completion menu to be open");
13287 }
13288 });
13289
13290 cx.update_editor(|editor, window, cx| {
13291 editor.move_page_down(&MovePageDown::default(), window, cx);
13292 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13293 {
13294 assert!(
13295 menu.selected_item == 1,
13296 "expected PageDown to select the last item from the context menu"
13297 );
13298 } else {
13299 panic!("expected completion menu to stay open after PageDown");
13300 }
13301 });
13302
13303 cx.update_editor(|editor, window, cx| {
13304 editor.move_page_up(&MovePageUp::default(), window, cx);
13305 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13306 {
13307 assert!(
13308 menu.selected_item == 0,
13309 "expected PageUp to select the first item from the context menu"
13310 );
13311 } else {
13312 panic!("expected completion menu to stay open after PageUp");
13313 }
13314 });
13315}
13316
13317#[gpui::test]
13318async fn test_as_is_completions(cx: &mut TestAppContext) {
13319 init_test(cx, |_| {});
13320 let mut cx = EditorLspTestContext::new_rust(
13321 lsp::ServerCapabilities {
13322 completion_provider: Some(lsp::CompletionOptions {
13323 ..Default::default()
13324 }),
13325 ..Default::default()
13326 },
13327 cx,
13328 )
13329 .await;
13330 cx.lsp
13331 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13332 Ok(Some(lsp::CompletionResponse::Array(vec![
13333 lsp::CompletionItem {
13334 label: "unsafe".into(),
13335 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13336 range: lsp::Range {
13337 start: lsp::Position {
13338 line: 1,
13339 character: 2,
13340 },
13341 end: lsp::Position {
13342 line: 1,
13343 character: 3,
13344 },
13345 },
13346 new_text: "unsafe".to_string(),
13347 })),
13348 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
13349 ..Default::default()
13350 },
13351 ])))
13352 });
13353 cx.set_state("fn a() {}\n nˇ");
13354 cx.executor().run_until_parked();
13355 cx.update_editor(|editor, window, cx| {
13356 editor.show_completions(
13357 &ShowCompletions {
13358 trigger: Some("\n".into()),
13359 },
13360 window,
13361 cx,
13362 );
13363 });
13364 cx.executor().run_until_parked();
13365
13366 cx.update_editor(|editor, window, cx| {
13367 editor.confirm_completion(&Default::default(), window, cx)
13368 });
13369 cx.executor().run_until_parked();
13370 cx.assert_editor_state("fn a() {}\n unsafeˇ");
13371}
13372
13373#[gpui::test]
13374async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
13375 init_test(cx, |_| {});
13376
13377 let mut cx = EditorLspTestContext::new_rust(
13378 lsp::ServerCapabilities {
13379 completion_provider: Some(lsp::CompletionOptions {
13380 trigger_characters: Some(vec![".".to_string()]),
13381 resolve_provider: Some(true),
13382 ..Default::default()
13383 }),
13384 ..Default::default()
13385 },
13386 cx,
13387 )
13388 .await;
13389
13390 cx.set_state("fn main() { let a = 2ˇ; }");
13391 cx.simulate_keystroke(".");
13392 let completion_item = lsp::CompletionItem {
13393 label: "Some".into(),
13394 kind: Some(lsp::CompletionItemKind::SNIPPET),
13395 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13396 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13397 kind: lsp::MarkupKind::Markdown,
13398 value: "```rust\nSome(2)\n```".to_string(),
13399 })),
13400 deprecated: Some(false),
13401 sort_text: Some("Some".to_string()),
13402 filter_text: Some("Some".to_string()),
13403 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13404 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13405 range: lsp::Range {
13406 start: lsp::Position {
13407 line: 0,
13408 character: 22,
13409 },
13410 end: lsp::Position {
13411 line: 0,
13412 character: 22,
13413 },
13414 },
13415 new_text: "Some(2)".to_string(),
13416 })),
13417 additional_text_edits: Some(vec![lsp::TextEdit {
13418 range: lsp::Range {
13419 start: lsp::Position {
13420 line: 0,
13421 character: 20,
13422 },
13423 end: lsp::Position {
13424 line: 0,
13425 character: 22,
13426 },
13427 },
13428 new_text: "".to_string(),
13429 }]),
13430 ..Default::default()
13431 };
13432
13433 let closure_completion_item = completion_item.clone();
13434 let counter = Arc::new(AtomicUsize::new(0));
13435 let counter_clone = counter.clone();
13436 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13437 let task_completion_item = closure_completion_item.clone();
13438 counter_clone.fetch_add(1, atomic::Ordering::Release);
13439 async move {
13440 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13441 is_incomplete: true,
13442 item_defaults: None,
13443 items: vec![task_completion_item],
13444 })))
13445 }
13446 });
13447
13448 cx.condition(|editor, _| editor.context_menu_visible())
13449 .await;
13450 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
13451 assert!(request.next().await.is_some());
13452 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13453
13454 cx.simulate_keystrokes("S o m");
13455 cx.condition(|editor, _| editor.context_menu_visible())
13456 .await;
13457 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
13458 assert!(request.next().await.is_some());
13459 assert!(request.next().await.is_some());
13460 assert!(request.next().await.is_some());
13461 request.close();
13462 assert!(request.next().await.is_none());
13463 assert_eq!(
13464 counter.load(atomic::Ordering::Acquire),
13465 4,
13466 "With the completions menu open, only one LSP request should happen per input"
13467 );
13468}
13469
13470#[gpui::test]
13471async fn test_toggle_comment(cx: &mut TestAppContext) {
13472 init_test(cx, |_| {});
13473 let mut cx = EditorTestContext::new(cx).await;
13474 let language = Arc::new(Language::new(
13475 LanguageConfig {
13476 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13477 ..Default::default()
13478 },
13479 Some(tree_sitter_rust::LANGUAGE.into()),
13480 ));
13481 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13482
13483 // If multiple selections intersect a line, the line is only toggled once.
13484 cx.set_state(indoc! {"
13485 fn a() {
13486 «//b();
13487 ˇ»// «c();
13488 //ˇ» d();
13489 }
13490 "});
13491
13492 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13493
13494 cx.assert_editor_state(indoc! {"
13495 fn a() {
13496 «b();
13497 c();
13498 ˇ» d();
13499 }
13500 "});
13501
13502 // The comment prefix is inserted at the same column for every line in a
13503 // selection.
13504 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13505
13506 cx.assert_editor_state(indoc! {"
13507 fn a() {
13508 // «b();
13509 // c();
13510 ˇ»// d();
13511 }
13512 "});
13513
13514 // If a selection ends at the beginning of a line, that line is not toggled.
13515 cx.set_selections_state(indoc! {"
13516 fn a() {
13517 // b();
13518 «// c();
13519 ˇ» // d();
13520 }
13521 "});
13522
13523 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13524
13525 cx.assert_editor_state(indoc! {"
13526 fn a() {
13527 // b();
13528 «c();
13529 ˇ» // d();
13530 }
13531 "});
13532
13533 // If a selection span a single line and is empty, the line is toggled.
13534 cx.set_state(indoc! {"
13535 fn a() {
13536 a();
13537 b();
13538 ˇ
13539 }
13540 "});
13541
13542 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13543
13544 cx.assert_editor_state(indoc! {"
13545 fn a() {
13546 a();
13547 b();
13548 //•ˇ
13549 }
13550 "});
13551
13552 // If a selection span multiple lines, empty lines are not toggled.
13553 cx.set_state(indoc! {"
13554 fn a() {
13555 «a();
13556
13557 c();ˇ»
13558 }
13559 "});
13560
13561 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13562
13563 cx.assert_editor_state(indoc! {"
13564 fn a() {
13565 // «a();
13566
13567 // c();ˇ»
13568 }
13569 "});
13570
13571 // If a selection includes multiple comment prefixes, all lines are uncommented.
13572 cx.set_state(indoc! {"
13573 fn a() {
13574 «// a();
13575 /// b();
13576 //! c();ˇ»
13577 }
13578 "});
13579
13580 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13581
13582 cx.assert_editor_state(indoc! {"
13583 fn a() {
13584 «a();
13585 b();
13586 c();ˇ»
13587 }
13588 "});
13589}
13590
13591#[gpui::test]
13592async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
13593 init_test(cx, |_| {});
13594 let mut cx = EditorTestContext::new(cx).await;
13595 let language = Arc::new(Language::new(
13596 LanguageConfig {
13597 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13598 ..Default::default()
13599 },
13600 Some(tree_sitter_rust::LANGUAGE.into()),
13601 ));
13602 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13603
13604 let toggle_comments = &ToggleComments {
13605 advance_downwards: false,
13606 ignore_indent: true,
13607 };
13608
13609 // If multiple selections intersect a line, the line is only toggled once.
13610 cx.set_state(indoc! {"
13611 fn a() {
13612 // «b();
13613 // c();
13614 // ˇ» d();
13615 }
13616 "});
13617
13618 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13619
13620 cx.assert_editor_state(indoc! {"
13621 fn a() {
13622 «b();
13623 c();
13624 ˇ» d();
13625 }
13626 "});
13627
13628 // The comment prefix is inserted at the beginning of each line
13629 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13630
13631 cx.assert_editor_state(indoc! {"
13632 fn a() {
13633 // «b();
13634 // c();
13635 // ˇ» d();
13636 }
13637 "});
13638
13639 // If a selection ends at the beginning of a line, that line is not toggled.
13640 cx.set_selections_state(indoc! {"
13641 fn a() {
13642 // b();
13643 // «c();
13644 ˇ»// d();
13645 }
13646 "});
13647
13648 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13649
13650 cx.assert_editor_state(indoc! {"
13651 fn a() {
13652 // b();
13653 «c();
13654 ˇ»// d();
13655 }
13656 "});
13657
13658 // If a selection span a single line and is empty, the line is toggled.
13659 cx.set_state(indoc! {"
13660 fn a() {
13661 a();
13662 b();
13663 ˇ
13664 }
13665 "});
13666
13667 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13668
13669 cx.assert_editor_state(indoc! {"
13670 fn a() {
13671 a();
13672 b();
13673 //ˇ
13674 }
13675 "});
13676
13677 // If a selection span multiple lines, empty lines are not toggled.
13678 cx.set_state(indoc! {"
13679 fn a() {
13680 «a();
13681
13682 c();ˇ»
13683 }
13684 "});
13685
13686 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13687
13688 cx.assert_editor_state(indoc! {"
13689 fn a() {
13690 // «a();
13691
13692 // c();ˇ»
13693 }
13694 "});
13695
13696 // If a selection includes multiple comment prefixes, all lines are uncommented.
13697 cx.set_state(indoc! {"
13698 fn a() {
13699 // «a();
13700 /// b();
13701 //! c();ˇ»
13702 }
13703 "});
13704
13705 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13706
13707 cx.assert_editor_state(indoc! {"
13708 fn a() {
13709 «a();
13710 b();
13711 c();ˇ»
13712 }
13713 "});
13714}
13715
13716#[gpui::test]
13717async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
13718 init_test(cx, |_| {});
13719
13720 let language = Arc::new(Language::new(
13721 LanguageConfig {
13722 line_comments: vec!["// ".into()],
13723 ..Default::default()
13724 },
13725 Some(tree_sitter_rust::LANGUAGE.into()),
13726 ));
13727
13728 let mut cx = EditorTestContext::new(cx).await;
13729
13730 cx.language_registry().add(language.clone());
13731 cx.update_buffer(|buffer, cx| {
13732 buffer.set_language(Some(language), cx);
13733 });
13734
13735 let toggle_comments = &ToggleComments {
13736 advance_downwards: true,
13737 ignore_indent: false,
13738 };
13739
13740 // Single cursor on one line -> advance
13741 // Cursor moves horizontally 3 characters as well on non-blank line
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 // Single selection on one line -> don't advance
13759 cx.set_state(indoc!(
13760 "fn a() {
13761 «dog()ˇ»;
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 -> 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 // dog();
13788 catˇ(ˇ);
13789 }"
13790 ));
13791
13792 // Multiple cursors on one line, with selection -> don't advance
13793 cx.set_state(indoc!(
13794 "fn a() {
13795 ˇdˇog«()ˇ»;
13796 cat();
13797 }"
13798 ));
13799 cx.update_editor(|editor, window, cx| {
13800 editor.toggle_comments(toggle_comments, window, cx);
13801 });
13802 cx.assert_editor_state(indoc!(
13803 "fn a() {
13804 // ˇdˇog«()ˇ»;
13805 cat();
13806 }"
13807 ));
13808
13809 // Single cursor on one line -> advance
13810 // Cursor moves to column 0 on blank line
13811 cx.set_state(indoc!(
13812 "fn a() {
13813 ˇdog();
13814
13815 cat();
13816 }"
13817 ));
13818 cx.update_editor(|editor, window, cx| {
13819 editor.toggle_comments(toggle_comments, window, cx);
13820 });
13821 cx.assert_editor_state(indoc!(
13822 "fn a() {
13823 // dog();
13824 ˇ
13825 cat();
13826 }"
13827 ));
13828
13829 // Single cursor on one line -> advance
13830 // Cursor starts and ends at column 0
13831 cx.set_state(indoc!(
13832 "fn a() {
13833 ˇ dog();
13834 cat();
13835 }"
13836 ));
13837 cx.update_editor(|editor, window, cx| {
13838 editor.toggle_comments(toggle_comments, window, cx);
13839 });
13840 cx.assert_editor_state(indoc!(
13841 "fn a() {
13842 // dog();
13843 ˇ cat();
13844 }"
13845 ));
13846}
13847
13848#[gpui::test]
13849async fn test_toggle_block_comment(cx: &mut TestAppContext) {
13850 init_test(cx, |_| {});
13851
13852 let mut cx = EditorTestContext::new(cx).await;
13853
13854 let html_language = Arc::new(
13855 Language::new(
13856 LanguageConfig {
13857 name: "HTML".into(),
13858 block_comment: Some(BlockCommentConfig {
13859 start: "<!-- ".into(),
13860 prefix: "".into(),
13861 end: " -->".into(),
13862 tab_size: 0,
13863 }),
13864 ..Default::default()
13865 },
13866 Some(tree_sitter_html::LANGUAGE.into()),
13867 )
13868 .with_injection_query(
13869 r#"
13870 (script_element
13871 (raw_text) @injection.content
13872 (#set! injection.language "javascript"))
13873 "#,
13874 )
13875 .unwrap(),
13876 );
13877
13878 let javascript_language = Arc::new(Language::new(
13879 LanguageConfig {
13880 name: "JavaScript".into(),
13881 line_comments: vec!["// ".into()],
13882 ..Default::default()
13883 },
13884 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13885 ));
13886
13887 cx.language_registry().add(html_language.clone());
13888 cx.language_registry().add(javascript_language.clone());
13889 cx.update_buffer(|buffer, cx| {
13890 buffer.set_language(Some(html_language), cx);
13891 });
13892
13893 // Toggle comments for empty selections
13894 cx.set_state(
13895 &r#"
13896 <p>A</p>ˇ
13897 <p>B</p>ˇ
13898 <p>C</p>ˇ
13899 "#
13900 .unindent(),
13901 );
13902 cx.update_editor(|editor, window, cx| {
13903 editor.toggle_comments(&ToggleComments::default(), window, cx)
13904 });
13905 cx.assert_editor_state(
13906 &r#"
13907 <!-- <p>A</p>ˇ -->
13908 <!-- <p>B</p>ˇ -->
13909 <!-- <p>C</p>ˇ -->
13910 "#
13911 .unindent(),
13912 );
13913 cx.update_editor(|editor, window, cx| {
13914 editor.toggle_comments(&ToggleComments::default(), window, cx)
13915 });
13916 cx.assert_editor_state(
13917 &r#"
13918 <p>A</p>ˇ
13919 <p>B</p>ˇ
13920 <p>C</p>ˇ
13921 "#
13922 .unindent(),
13923 );
13924
13925 // Toggle comments for mixture of empty and non-empty selections, where
13926 // multiple selections occupy a given line.
13927 cx.set_state(
13928 &r#"
13929 <p>A«</p>
13930 <p>ˇ»B</p>ˇ
13931 <p>C«</p>
13932 <p>ˇ»D</p>ˇ
13933 "#
13934 .unindent(),
13935 );
13936
13937 cx.update_editor(|editor, window, cx| {
13938 editor.toggle_comments(&ToggleComments::default(), window, cx)
13939 });
13940 cx.assert_editor_state(
13941 &r#"
13942 <!-- <p>A«</p>
13943 <p>ˇ»B</p>ˇ -->
13944 <!-- <p>C«</p>
13945 <p>ˇ»D</p>ˇ -->
13946 "#
13947 .unindent(),
13948 );
13949 cx.update_editor(|editor, window, cx| {
13950 editor.toggle_comments(&ToggleComments::default(), window, cx)
13951 });
13952 cx.assert_editor_state(
13953 &r#"
13954 <p>A«</p>
13955 <p>ˇ»B</p>ˇ
13956 <p>C«</p>
13957 <p>ˇ»D</p>ˇ
13958 "#
13959 .unindent(),
13960 );
13961
13962 // Toggle comments when different languages are active for different
13963 // selections.
13964 cx.set_state(
13965 &r#"
13966 ˇ<script>
13967 ˇvar x = new Y();
13968 ˇ</script>
13969 "#
13970 .unindent(),
13971 );
13972 cx.executor().run_until_parked();
13973 cx.update_editor(|editor, window, cx| {
13974 editor.toggle_comments(&ToggleComments::default(), window, cx)
13975 });
13976 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
13977 // Uncommenting and commenting from this position brings in even more wrong artifacts.
13978 cx.assert_editor_state(
13979 &r#"
13980 <!-- ˇ<script> -->
13981 // ˇvar x = new Y();
13982 <!-- ˇ</script> -->
13983 "#
13984 .unindent(),
13985 );
13986}
13987
13988#[gpui::test]
13989fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
13990 init_test(cx, |_| {});
13991
13992 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13993 let multibuffer = cx.new(|cx| {
13994 let mut multibuffer = MultiBuffer::new(ReadWrite);
13995 multibuffer.push_excerpts(
13996 buffer.clone(),
13997 [
13998 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
13999 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
14000 ],
14001 cx,
14002 );
14003 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
14004 multibuffer
14005 });
14006
14007 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
14008 editor.update_in(cx, |editor, window, cx| {
14009 assert_eq!(editor.text(cx), "aaaa\nbbbb");
14010 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14011 s.select_ranges([
14012 Point::new(0, 0)..Point::new(0, 0),
14013 Point::new(1, 0)..Point::new(1, 0),
14014 ])
14015 });
14016
14017 editor.handle_input("X", window, cx);
14018 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
14019 assert_eq!(
14020 editor.selections.ranges(cx),
14021 [
14022 Point::new(0, 1)..Point::new(0, 1),
14023 Point::new(1, 1)..Point::new(1, 1),
14024 ]
14025 );
14026
14027 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
14028 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14029 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
14030 });
14031 editor.backspace(&Default::default(), window, cx);
14032 assert_eq!(editor.text(cx), "Xa\nbbb");
14033 assert_eq!(
14034 editor.selections.ranges(cx),
14035 [Point::new(1, 0)..Point::new(1, 0)]
14036 );
14037
14038 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14039 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
14040 });
14041 editor.backspace(&Default::default(), window, cx);
14042 assert_eq!(editor.text(cx), "X\nbb");
14043 assert_eq!(
14044 editor.selections.ranges(cx),
14045 [Point::new(0, 1)..Point::new(0, 1)]
14046 );
14047 });
14048}
14049
14050#[gpui::test]
14051fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
14052 init_test(cx, |_| {});
14053
14054 let markers = vec![('[', ']').into(), ('(', ')').into()];
14055 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
14056 indoc! {"
14057 [aaaa
14058 (bbbb]
14059 cccc)",
14060 },
14061 markers.clone(),
14062 );
14063 let excerpt_ranges = markers.into_iter().map(|marker| {
14064 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
14065 ExcerptRange::new(context.clone())
14066 });
14067 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
14068 let multibuffer = cx.new(|cx| {
14069 let mut multibuffer = MultiBuffer::new(ReadWrite);
14070 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
14071 multibuffer
14072 });
14073
14074 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
14075 editor.update_in(cx, |editor, window, cx| {
14076 let (expected_text, selection_ranges) = marked_text_ranges(
14077 indoc! {"
14078 aaaa
14079 bˇbbb
14080 bˇbbˇb
14081 cccc"
14082 },
14083 true,
14084 );
14085 assert_eq!(editor.text(cx), expected_text);
14086 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14087 s.select_ranges(selection_ranges)
14088 });
14089
14090 editor.handle_input("X", window, cx);
14091
14092 let (expected_text, expected_selections) = marked_text_ranges(
14093 indoc! {"
14094 aaaa
14095 bXˇbbXb
14096 bXˇbbXˇ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 editor.newline(&Newline, window, cx);
14105 let (expected_text, expected_selections) = marked_text_ranges(
14106 indoc! {"
14107 aaaa
14108 bX
14109 ˇbbX
14110 b
14111 bX
14112 ˇbbX
14113 ˇb
14114 cccc"
14115 },
14116 false,
14117 );
14118 assert_eq!(editor.text(cx), expected_text);
14119 assert_eq!(editor.selections.ranges(cx), expected_selections);
14120 });
14121}
14122
14123#[gpui::test]
14124fn test_refresh_selections(cx: &mut TestAppContext) {
14125 init_test(cx, |_| {});
14126
14127 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14128 let mut excerpt1_id = None;
14129 let multibuffer = cx.new(|cx| {
14130 let mut multibuffer = MultiBuffer::new(ReadWrite);
14131 excerpt1_id = multibuffer
14132 .push_excerpts(
14133 buffer.clone(),
14134 [
14135 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14136 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14137 ],
14138 cx,
14139 )
14140 .into_iter()
14141 .next();
14142 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14143 multibuffer
14144 });
14145
14146 let editor = cx.add_window(|window, cx| {
14147 let mut editor = build_editor(multibuffer.clone(), window, cx);
14148 let snapshot = editor.snapshot(window, cx);
14149 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14150 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
14151 });
14152 editor.begin_selection(
14153 Point::new(2, 1).to_display_point(&snapshot),
14154 true,
14155 1,
14156 window,
14157 cx,
14158 );
14159 assert_eq!(
14160 editor.selections.ranges(cx),
14161 [
14162 Point::new(1, 3)..Point::new(1, 3),
14163 Point::new(2, 1)..Point::new(2, 1),
14164 ]
14165 );
14166 editor
14167 });
14168
14169 // Refreshing selections is a no-op when excerpts haven't changed.
14170 _ = editor.update(cx, |editor, window, cx| {
14171 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14172 assert_eq!(
14173 editor.selections.ranges(cx),
14174 [
14175 Point::new(1, 3)..Point::new(1, 3),
14176 Point::new(2, 1)..Point::new(2, 1),
14177 ]
14178 );
14179 });
14180
14181 multibuffer.update(cx, |multibuffer, cx| {
14182 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14183 });
14184 _ = editor.update(cx, |editor, window, cx| {
14185 // Removing an excerpt causes the first selection to become degenerate.
14186 assert_eq!(
14187 editor.selections.ranges(cx),
14188 [
14189 Point::new(0, 0)..Point::new(0, 0),
14190 Point::new(0, 1)..Point::new(0, 1)
14191 ]
14192 );
14193
14194 // Refreshing selections will relocate the first selection to the original buffer
14195 // location.
14196 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14197 assert_eq!(
14198 editor.selections.ranges(cx),
14199 [
14200 Point::new(0, 1)..Point::new(0, 1),
14201 Point::new(0, 3)..Point::new(0, 3)
14202 ]
14203 );
14204 assert!(editor.selections.pending_anchor().is_some());
14205 });
14206}
14207
14208#[gpui::test]
14209fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
14210 init_test(cx, |_| {});
14211
14212 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14213 let mut excerpt1_id = None;
14214 let multibuffer = cx.new(|cx| {
14215 let mut multibuffer = MultiBuffer::new(ReadWrite);
14216 excerpt1_id = multibuffer
14217 .push_excerpts(
14218 buffer.clone(),
14219 [
14220 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14221 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14222 ],
14223 cx,
14224 )
14225 .into_iter()
14226 .next();
14227 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14228 multibuffer
14229 });
14230
14231 let editor = cx.add_window(|window, cx| {
14232 let mut editor = build_editor(multibuffer.clone(), window, cx);
14233 let snapshot = editor.snapshot(window, cx);
14234 editor.begin_selection(
14235 Point::new(1, 3).to_display_point(&snapshot),
14236 false,
14237 1,
14238 window,
14239 cx,
14240 );
14241 assert_eq!(
14242 editor.selections.ranges(cx),
14243 [Point::new(1, 3)..Point::new(1, 3)]
14244 );
14245 editor
14246 });
14247
14248 multibuffer.update(cx, |multibuffer, cx| {
14249 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14250 });
14251 _ = editor.update(cx, |editor, window, cx| {
14252 assert_eq!(
14253 editor.selections.ranges(cx),
14254 [Point::new(0, 0)..Point::new(0, 0)]
14255 );
14256
14257 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
14258 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14259 assert_eq!(
14260 editor.selections.ranges(cx),
14261 [Point::new(0, 3)..Point::new(0, 3)]
14262 );
14263 assert!(editor.selections.pending_anchor().is_some());
14264 });
14265}
14266
14267#[gpui::test]
14268async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
14269 init_test(cx, |_| {});
14270
14271 let language = Arc::new(
14272 Language::new(
14273 LanguageConfig {
14274 brackets: BracketPairConfig {
14275 pairs: vec![
14276 BracketPair {
14277 start: "{".to_string(),
14278 end: "}".to_string(),
14279 close: true,
14280 surround: true,
14281 newline: true,
14282 },
14283 BracketPair {
14284 start: "/* ".to_string(),
14285 end: " */".to_string(),
14286 close: true,
14287 surround: true,
14288 newline: true,
14289 },
14290 ],
14291 ..Default::default()
14292 },
14293 ..Default::default()
14294 },
14295 Some(tree_sitter_rust::LANGUAGE.into()),
14296 )
14297 .with_indents_query("")
14298 .unwrap(),
14299 );
14300
14301 let text = concat!(
14302 "{ }\n", //
14303 " x\n", //
14304 " /* */\n", //
14305 "x\n", //
14306 "{{} }\n", //
14307 );
14308
14309 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
14310 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14311 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14312 editor
14313 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
14314 .await;
14315
14316 editor.update_in(cx, |editor, window, cx| {
14317 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14318 s.select_display_ranges([
14319 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
14320 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
14321 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
14322 ])
14323 });
14324 editor.newline(&Newline, window, cx);
14325
14326 assert_eq!(
14327 editor.buffer().read(cx).read(cx).text(),
14328 concat!(
14329 "{ \n", // Suppress rustfmt
14330 "\n", //
14331 "}\n", //
14332 " x\n", //
14333 " /* \n", //
14334 " \n", //
14335 " */\n", //
14336 "x\n", //
14337 "{{} \n", //
14338 "}\n", //
14339 )
14340 );
14341 });
14342}
14343
14344#[gpui::test]
14345fn test_highlighted_ranges(cx: &mut TestAppContext) {
14346 init_test(cx, |_| {});
14347
14348 let editor = cx.add_window(|window, cx| {
14349 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
14350 build_editor(buffer.clone(), window, cx)
14351 });
14352
14353 _ = editor.update(cx, |editor, window, cx| {
14354 struct Type1;
14355 struct Type2;
14356
14357 let buffer = editor.buffer.read(cx).snapshot(cx);
14358
14359 let anchor_range =
14360 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
14361
14362 editor.highlight_background::<Type1>(
14363 &[
14364 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
14365 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
14366 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
14367 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
14368 ],
14369 |_| Hsla::red(),
14370 cx,
14371 );
14372 editor.highlight_background::<Type2>(
14373 &[
14374 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
14375 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
14376 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
14377 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
14378 ],
14379 |_| Hsla::green(),
14380 cx,
14381 );
14382
14383 let snapshot = editor.snapshot(window, cx);
14384 let mut highlighted_ranges = editor.background_highlights_in_range(
14385 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
14386 &snapshot,
14387 cx.theme(),
14388 );
14389 // Enforce a consistent ordering based on color without relying on the ordering of the
14390 // highlight's `TypeId` which is non-executor.
14391 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
14392 assert_eq!(
14393 highlighted_ranges,
14394 &[
14395 (
14396 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
14397 Hsla::red(),
14398 ),
14399 (
14400 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14401 Hsla::red(),
14402 ),
14403 (
14404 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
14405 Hsla::green(),
14406 ),
14407 (
14408 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
14409 Hsla::green(),
14410 ),
14411 ]
14412 );
14413 assert_eq!(
14414 editor.background_highlights_in_range(
14415 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
14416 &snapshot,
14417 cx.theme(),
14418 ),
14419 &[(
14420 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14421 Hsla::red(),
14422 )]
14423 );
14424 });
14425}
14426
14427#[gpui::test]
14428async fn test_following(cx: &mut TestAppContext) {
14429 init_test(cx, |_| {});
14430
14431 let fs = FakeFs::new(cx.executor());
14432 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14433
14434 let buffer = project.update(cx, |project, cx| {
14435 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
14436 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
14437 });
14438 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
14439 let follower = cx.update(|cx| {
14440 cx.open_window(
14441 WindowOptions {
14442 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
14443 gpui::Point::new(px(0.), px(0.)),
14444 gpui::Point::new(px(10.), px(80.)),
14445 ))),
14446 ..Default::default()
14447 },
14448 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
14449 )
14450 .unwrap()
14451 });
14452
14453 let is_still_following = Rc::new(RefCell::new(true));
14454 let follower_edit_event_count = Rc::new(RefCell::new(0));
14455 let pending_update = Rc::new(RefCell::new(None));
14456 let leader_entity = leader.root(cx).unwrap();
14457 let follower_entity = follower.root(cx).unwrap();
14458 _ = follower.update(cx, {
14459 let update = pending_update.clone();
14460 let is_still_following = is_still_following.clone();
14461 let follower_edit_event_count = follower_edit_event_count.clone();
14462 |_, window, cx| {
14463 cx.subscribe_in(
14464 &leader_entity,
14465 window,
14466 move |_, leader, event, window, cx| {
14467 leader.read(cx).add_event_to_update_proto(
14468 event,
14469 &mut update.borrow_mut(),
14470 window,
14471 cx,
14472 );
14473 },
14474 )
14475 .detach();
14476
14477 cx.subscribe_in(
14478 &follower_entity,
14479 window,
14480 move |_, _, event: &EditorEvent, _window, _cx| {
14481 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
14482 *is_still_following.borrow_mut() = false;
14483 }
14484
14485 if let EditorEvent::BufferEdited = event {
14486 *follower_edit_event_count.borrow_mut() += 1;
14487 }
14488 },
14489 )
14490 .detach();
14491 }
14492 });
14493
14494 // Update the selections only
14495 _ = leader.update(cx, |leader, window, cx| {
14496 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14497 s.select_ranges([1..1])
14498 });
14499 });
14500 follower
14501 .update(cx, |follower, window, cx| {
14502 follower.apply_update_proto(
14503 &project,
14504 pending_update.borrow_mut().take().unwrap(),
14505 window,
14506 cx,
14507 )
14508 })
14509 .unwrap()
14510 .await
14511 .unwrap();
14512 _ = follower.update(cx, |follower, _, cx| {
14513 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
14514 });
14515 assert!(*is_still_following.borrow());
14516 assert_eq!(*follower_edit_event_count.borrow(), 0);
14517
14518 // Update the scroll position only
14519 _ = leader.update(cx, |leader, window, cx| {
14520 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14521 });
14522 follower
14523 .update(cx, |follower, window, cx| {
14524 follower.apply_update_proto(
14525 &project,
14526 pending_update.borrow_mut().take().unwrap(),
14527 window,
14528 cx,
14529 )
14530 })
14531 .unwrap()
14532 .await
14533 .unwrap();
14534 assert_eq!(
14535 follower
14536 .update(cx, |follower, _, cx| follower.scroll_position(cx))
14537 .unwrap(),
14538 gpui::Point::new(1.5, 3.5)
14539 );
14540 assert!(*is_still_following.borrow());
14541 assert_eq!(*follower_edit_event_count.borrow(), 0);
14542
14543 // Update the selections and scroll position. The follower's scroll position is updated
14544 // via autoscroll, not via the leader's exact scroll position.
14545 _ = leader.update(cx, |leader, window, cx| {
14546 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14547 s.select_ranges([0..0])
14548 });
14549 leader.request_autoscroll(Autoscroll::newest(), cx);
14550 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14551 });
14552 follower
14553 .update(cx, |follower, window, cx| {
14554 follower.apply_update_proto(
14555 &project,
14556 pending_update.borrow_mut().take().unwrap(),
14557 window,
14558 cx,
14559 )
14560 })
14561 .unwrap()
14562 .await
14563 .unwrap();
14564 _ = follower.update(cx, |follower, _, cx| {
14565 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
14566 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
14567 });
14568 assert!(*is_still_following.borrow());
14569
14570 // Creating a pending selection that precedes another selection
14571 _ = leader.update(cx, |leader, window, cx| {
14572 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14573 s.select_ranges([1..1])
14574 });
14575 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
14576 });
14577 follower
14578 .update(cx, |follower, window, cx| {
14579 follower.apply_update_proto(
14580 &project,
14581 pending_update.borrow_mut().take().unwrap(),
14582 window,
14583 cx,
14584 )
14585 })
14586 .unwrap()
14587 .await
14588 .unwrap();
14589 _ = follower.update(cx, |follower, _, cx| {
14590 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
14591 });
14592 assert!(*is_still_following.borrow());
14593
14594 // Extend the pending selection so that it surrounds another selection
14595 _ = leader.update(cx, |leader, window, cx| {
14596 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
14597 });
14598 follower
14599 .update(cx, |follower, window, cx| {
14600 follower.apply_update_proto(
14601 &project,
14602 pending_update.borrow_mut().take().unwrap(),
14603 window,
14604 cx,
14605 )
14606 })
14607 .unwrap()
14608 .await
14609 .unwrap();
14610 _ = follower.update(cx, |follower, _, cx| {
14611 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
14612 });
14613
14614 // Scrolling locally breaks the follow
14615 _ = follower.update(cx, |follower, window, cx| {
14616 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
14617 follower.set_scroll_anchor(
14618 ScrollAnchor {
14619 anchor: top_anchor,
14620 offset: gpui::Point::new(0.0, 0.5),
14621 },
14622 window,
14623 cx,
14624 );
14625 });
14626 assert!(!(*is_still_following.borrow()));
14627}
14628
14629#[gpui::test]
14630async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
14631 init_test(cx, |_| {});
14632
14633 let fs = FakeFs::new(cx.executor());
14634 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14635 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14636 let pane = workspace
14637 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14638 .unwrap();
14639
14640 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14641
14642 let leader = pane.update_in(cx, |_, window, cx| {
14643 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
14644 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
14645 });
14646
14647 // Start following the editor when it has no excerpts.
14648 let mut state_message =
14649 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14650 let workspace_entity = workspace.root(cx).unwrap();
14651 let follower_1 = cx
14652 .update_window(*workspace.deref(), |_, window, cx| {
14653 Editor::from_state_proto(
14654 workspace_entity,
14655 ViewId {
14656 creator: CollaboratorId::PeerId(PeerId::default()),
14657 id: 0,
14658 },
14659 &mut state_message,
14660 window,
14661 cx,
14662 )
14663 })
14664 .unwrap()
14665 .unwrap()
14666 .await
14667 .unwrap();
14668
14669 let update_message = Rc::new(RefCell::new(None));
14670 follower_1.update_in(cx, {
14671 let update = update_message.clone();
14672 |_, window, cx| {
14673 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
14674 leader.read(cx).add_event_to_update_proto(
14675 event,
14676 &mut update.borrow_mut(),
14677 window,
14678 cx,
14679 );
14680 })
14681 .detach();
14682 }
14683 });
14684
14685 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
14686 (
14687 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
14688 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
14689 )
14690 });
14691
14692 // Insert some excerpts.
14693 leader.update(cx, |leader, cx| {
14694 leader.buffer.update(cx, |multibuffer, cx| {
14695 multibuffer.set_excerpts_for_path(
14696 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
14697 buffer_1.clone(),
14698 vec![
14699 Point::row_range(0..3),
14700 Point::row_range(1..6),
14701 Point::row_range(12..15),
14702 ],
14703 0,
14704 cx,
14705 );
14706 multibuffer.set_excerpts_for_path(
14707 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
14708 buffer_2.clone(),
14709 vec![Point::row_range(0..6), Point::row_range(8..12)],
14710 0,
14711 cx,
14712 );
14713 });
14714 });
14715
14716 // Apply the update of adding the excerpts.
14717 follower_1
14718 .update_in(cx, |follower, window, cx| {
14719 follower.apply_update_proto(
14720 &project,
14721 update_message.borrow().clone().unwrap(),
14722 window,
14723 cx,
14724 )
14725 })
14726 .await
14727 .unwrap();
14728 assert_eq!(
14729 follower_1.update(cx, |editor, cx| editor.text(cx)),
14730 leader.update(cx, |editor, cx| editor.text(cx))
14731 );
14732 update_message.borrow_mut().take();
14733
14734 // Start following separately after it already has excerpts.
14735 let mut state_message =
14736 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14737 let workspace_entity = workspace.root(cx).unwrap();
14738 let follower_2 = cx
14739 .update_window(*workspace.deref(), |_, window, cx| {
14740 Editor::from_state_proto(
14741 workspace_entity,
14742 ViewId {
14743 creator: CollaboratorId::PeerId(PeerId::default()),
14744 id: 0,
14745 },
14746 &mut state_message,
14747 window,
14748 cx,
14749 )
14750 })
14751 .unwrap()
14752 .unwrap()
14753 .await
14754 .unwrap();
14755 assert_eq!(
14756 follower_2.update(cx, |editor, cx| editor.text(cx)),
14757 leader.update(cx, |editor, cx| editor.text(cx))
14758 );
14759
14760 // Remove some excerpts.
14761 leader.update(cx, |leader, cx| {
14762 leader.buffer.update(cx, |multibuffer, cx| {
14763 let excerpt_ids = multibuffer.excerpt_ids();
14764 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
14765 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
14766 });
14767 });
14768
14769 // Apply the update of removing the excerpts.
14770 follower_1
14771 .update_in(cx, |follower, window, cx| {
14772 follower.apply_update_proto(
14773 &project,
14774 update_message.borrow().clone().unwrap(),
14775 window,
14776 cx,
14777 )
14778 })
14779 .await
14780 .unwrap();
14781 follower_2
14782 .update_in(cx, |follower, window, cx| {
14783 follower.apply_update_proto(
14784 &project,
14785 update_message.borrow().clone().unwrap(),
14786 window,
14787 cx,
14788 )
14789 })
14790 .await
14791 .unwrap();
14792 update_message.borrow_mut().take();
14793 assert_eq!(
14794 follower_1.update(cx, |editor, cx| editor.text(cx)),
14795 leader.update(cx, |editor, cx| editor.text(cx))
14796 );
14797}
14798
14799#[gpui::test]
14800async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14801 init_test(cx, |_| {});
14802
14803 let mut cx = EditorTestContext::new(cx).await;
14804 let lsp_store =
14805 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
14806
14807 cx.set_state(indoc! {"
14808 ˇfn func(abc def: i32) -> u32 {
14809 }
14810 "});
14811
14812 cx.update(|_, cx| {
14813 lsp_store.update(cx, |lsp_store, cx| {
14814 lsp_store
14815 .update_diagnostics(
14816 LanguageServerId(0),
14817 lsp::PublishDiagnosticsParams {
14818 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
14819 version: None,
14820 diagnostics: vec![
14821 lsp::Diagnostic {
14822 range: lsp::Range::new(
14823 lsp::Position::new(0, 11),
14824 lsp::Position::new(0, 12),
14825 ),
14826 severity: Some(lsp::DiagnosticSeverity::ERROR),
14827 ..Default::default()
14828 },
14829 lsp::Diagnostic {
14830 range: lsp::Range::new(
14831 lsp::Position::new(0, 12),
14832 lsp::Position::new(0, 15),
14833 ),
14834 severity: Some(lsp::DiagnosticSeverity::ERROR),
14835 ..Default::default()
14836 },
14837 lsp::Diagnostic {
14838 range: lsp::Range::new(
14839 lsp::Position::new(0, 25),
14840 lsp::Position::new(0, 28),
14841 ),
14842 severity: Some(lsp::DiagnosticSeverity::ERROR),
14843 ..Default::default()
14844 },
14845 ],
14846 },
14847 None,
14848 DiagnosticSourceKind::Pushed,
14849 &[],
14850 cx,
14851 )
14852 .unwrap()
14853 });
14854 });
14855
14856 executor.run_until_parked();
14857
14858 cx.update_editor(|editor, window, cx| {
14859 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14860 });
14861
14862 cx.assert_editor_state(indoc! {"
14863 fn func(abc def: i32) -> ˇu32 {
14864 }
14865 "});
14866
14867 cx.update_editor(|editor, window, cx| {
14868 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14869 });
14870
14871 cx.assert_editor_state(indoc! {"
14872 fn func(abc ˇdef: i32) -> u32 {
14873 }
14874 "});
14875
14876 cx.update_editor(|editor, window, cx| {
14877 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14878 });
14879
14880 cx.assert_editor_state(indoc! {"
14881 fn func(abcˇ def: i32) -> u32 {
14882 }
14883 "});
14884
14885 cx.update_editor(|editor, window, cx| {
14886 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14887 });
14888
14889 cx.assert_editor_state(indoc! {"
14890 fn func(abc def: i32) -> ˇu32 {
14891 }
14892 "});
14893}
14894
14895#[gpui::test]
14896async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14897 init_test(cx, |_| {});
14898
14899 let mut cx = EditorTestContext::new(cx).await;
14900
14901 let diff_base = r#"
14902 use some::mod;
14903
14904 const A: u32 = 42;
14905
14906 fn main() {
14907 println!("hello");
14908
14909 println!("world");
14910 }
14911 "#
14912 .unindent();
14913
14914 // Edits are modified, removed, modified, added
14915 cx.set_state(
14916 &r#"
14917 use some::modified;
14918
14919 ˇ
14920 fn main() {
14921 println!("hello there");
14922
14923 println!("around the");
14924 println!("world");
14925 }
14926 "#
14927 .unindent(),
14928 );
14929
14930 cx.set_head_text(&diff_base);
14931 executor.run_until_parked();
14932
14933 cx.update_editor(|editor, window, cx| {
14934 //Wrap around the bottom of the buffer
14935 for _ in 0..3 {
14936 editor.go_to_next_hunk(&GoToHunk, window, cx);
14937 }
14938 });
14939
14940 cx.assert_editor_state(
14941 &r#"
14942 ˇuse some::modified;
14943
14944
14945 fn main() {
14946 println!("hello there");
14947
14948 println!("around the");
14949 println!("world");
14950 }
14951 "#
14952 .unindent(),
14953 );
14954
14955 cx.update_editor(|editor, window, cx| {
14956 //Wrap around the top of the buffer
14957 for _ in 0..2 {
14958 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14959 }
14960 });
14961
14962 cx.assert_editor_state(
14963 &r#"
14964 use some::modified;
14965
14966
14967 fn main() {
14968 ˇ println!("hello there");
14969
14970 println!("around the");
14971 println!("world");
14972 }
14973 "#
14974 .unindent(),
14975 );
14976
14977 cx.update_editor(|editor, window, cx| {
14978 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14979 });
14980
14981 cx.assert_editor_state(
14982 &r#"
14983 use some::modified;
14984
14985 ˇ
14986 fn main() {
14987 println!("hello there");
14988
14989 println!("around the");
14990 println!("world");
14991 }
14992 "#
14993 .unindent(),
14994 );
14995
14996 cx.update_editor(|editor, window, cx| {
14997 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14998 });
14999
15000 cx.assert_editor_state(
15001 &r#"
15002 ˇuse some::modified;
15003
15004
15005 fn main() {
15006 println!("hello there");
15007
15008 println!("around the");
15009 println!("world");
15010 }
15011 "#
15012 .unindent(),
15013 );
15014
15015 cx.update_editor(|editor, window, cx| {
15016 for _ in 0..2 {
15017 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15018 }
15019 });
15020
15021 cx.assert_editor_state(
15022 &r#"
15023 use some::modified;
15024
15025
15026 fn main() {
15027 ˇ println!("hello there");
15028
15029 println!("around the");
15030 println!("world");
15031 }
15032 "#
15033 .unindent(),
15034 );
15035
15036 cx.update_editor(|editor, window, cx| {
15037 editor.fold(&Fold, window, cx);
15038 });
15039
15040 cx.update_editor(|editor, window, cx| {
15041 editor.go_to_next_hunk(&GoToHunk, window, cx);
15042 });
15043
15044 cx.assert_editor_state(
15045 &r#"
15046 ˇuse some::modified;
15047
15048
15049 fn main() {
15050 println!("hello there");
15051
15052 println!("around the");
15053 println!("world");
15054 }
15055 "#
15056 .unindent(),
15057 );
15058}
15059
15060#[test]
15061fn test_split_words() {
15062 fn split(text: &str) -> Vec<&str> {
15063 split_words(text).collect()
15064 }
15065
15066 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
15067 assert_eq!(split("hello_world"), &["hello_", "world"]);
15068 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
15069 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
15070 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
15071 assert_eq!(split("helloworld"), &["helloworld"]);
15072
15073 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
15074}
15075
15076#[gpui::test]
15077async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
15078 init_test(cx, |_| {});
15079
15080 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
15081 let mut assert = |before, after| {
15082 let _state_context = cx.set_state(before);
15083 cx.run_until_parked();
15084 cx.update_editor(|editor, window, cx| {
15085 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
15086 });
15087 cx.run_until_parked();
15088 cx.assert_editor_state(after);
15089 };
15090
15091 // Outside bracket jumps to outside of matching bracket
15092 assert("console.logˇ(var);", "console.log(var)ˇ;");
15093 assert("console.log(var)ˇ;", "console.logˇ(var);");
15094
15095 // Inside bracket jumps to inside of matching bracket
15096 assert("console.log(ˇvar);", "console.log(varˇ);");
15097 assert("console.log(varˇ);", "console.log(ˇvar);");
15098
15099 // When outside a bracket and inside, favor jumping to the inside bracket
15100 assert(
15101 "console.log('foo', [1, 2, 3]ˇ);",
15102 "console.log(ˇ'foo', [1, 2, 3]);",
15103 );
15104 assert(
15105 "console.log(ˇ'foo', [1, 2, 3]);",
15106 "console.log('foo', [1, 2, 3]ˇ);",
15107 );
15108
15109 // Bias forward if two options are equally likely
15110 assert(
15111 "let result = curried_fun()ˇ();",
15112 "let result = curried_fun()()ˇ;",
15113 );
15114
15115 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
15116 assert(
15117 indoc! {"
15118 function test() {
15119 console.log('test')ˇ
15120 }"},
15121 indoc! {"
15122 function test() {
15123 console.logˇ('test')
15124 }"},
15125 );
15126}
15127
15128#[gpui::test]
15129async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
15130 init_test(cx, |_| {});
15131
15132 let fs = FakeFs::new(cx.executor());
15133 fs.insert_tree(
15134 path!("/a"),
15135 json!({
15136 "main.rs": "fn main() { let a = 5; }",
15137 "other.rs": "// Test file",
15138 }),
15139 )
15140 .await;
15141 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15142
15143 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15144 language_registry.add(Arc::new(Language::new(
15145 LanguageConfig {
15146 name: "Rust".into(),
15147 matcher: LanguageMatcher {
15148 path_suffixes: vec!["rs".to_string()],
15149 ..Default::default()
15150 },
15151 brackets: BracketPairConfig {
15152 pairs: vec![BracketPair {
15153 start: "{".to_string(),
15154 end: "}".to_string(),
15155 close: true,
15156 surround: true,
15157 newline: true,
15158 }],
15159 disabled_scopes_by_bracket_ix: Vec::new(),
15160 },
15161 ..Default::default()
15162 },
15163 Some(tree_sitter_rust::LANGUAGE.into()),
15164 )));
15165 let mut fake_servers = language_registry.register_fake_lsp(
15166 "Rust",
15167 FakeLspAdapter {
15168 capabilities: lsp::ServerCapabilities {
15169 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15170 first_trigger_character: "{".to_string(),
15171 more_trigger_character: None,
15172 }),
15173 ..Default::default()
15174 },
15175 ..Default::default()
15176 },
15177 );
15178
15179 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15180
15181 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15182
15183 let worktree_id = workspace
15184 .update(cx, |workspace, _, cx| {
15185 workspace.project().update(cx, |project, cx| {
15186 project.worktrees(cx).next().unwrap().read(cx).id()
15187 })
15188 })
15189 .unwrap();
15190
15191 let buffer = project
15192 .update(cx, |project, cx| {
15193 project.open_local_buffer(path!("/a/main.rs"), cx)
15194 })
15195 .await
15196 .unwrap();
15197 let editor_handle = workspace
15198 .update(cx, |workspace, window, cx| {
15199 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
15200 })
15201 .unwrap()
15202 .await
15203 .unwrap()
15204 .downcast::<Editor>()
15205 .unwrap();
15206
15207 cx.executor().start_waiting();
15208 let fake_server = fake_servers.next().await.unwrap();
15209
15210 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
15211 |params, _| async move {
15212 assert_eq!(
15213 params.text_document_position.text_document.uri,
15214 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
15215 );
15216 assert_eq!(
15217 params.text_document_position.position,
15218 lsp::Position::new(0, 21),
15219 );
15220
15221 Ok(Some(vec![lsp::TextEdit {
15222 new_text: "]".to_string(),
15223 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15224 }]))
15225 },
15226 );
15227
15228 editor_handle.update_in(cx, |editor, window, cx| {
15229 window.focus(&editor.focus_handle(cx));
15230 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15231 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
15232 });
15233 editor.handle_input("{", window, cx);
15234 });
15235
15236 cx.executor().run_until_parked();
15237
15238 buffer.update(cx, |buffer, _| {
15239 assert_eq!(
15240 buffer.text(),
15241 "fn main() { let a = {5}; }",
15242 "No extra braces from on type formatting should appear in the buffer"
15243 )
15244 });
15245}
15246
15247#[gpui::test(iterations = 20, seeds(31))]
15248async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
15249 init_test(cx, |_| {});
15250
15251 let mut cx = EditorLspTestContext::new_rust(
15252 lsp::ServerCapabilities {
15253 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15254 first_trigger_character: ".".to_string(),
15255 more_trigger_character: None,
15256 }),
15257 ..Default::default()
15258 },
15259 cx,
15260 )
15261 .await;
15262
15263 cx.update_buffer(|buffer, _| {
15264 // This causes autoindent to be async.
15265 buffer.set_sync_parse_timeout(Duration::ZERO)
15266 });
15267
15268 cx.set_state("fn c() {\n d()ˇ\n}\n");
15269 cx.simulate_keystroke("\n");
15270 cx.run_until_parked();
15271
15272 let buffer_cloned =
15273 cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap().clone());
15274 let mut request =
15275 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
15276 let buffer_cloned = buffer_cloned.clone();
15277 async move {
15278 buffer_cloned.update(&mut cx, |buffer, _| {
15279 assert_eq!(
15280 buffer.text(),
15281 "fn c() {\n d()\n .\n}\n",
15282 "OnTypeFormatting should triggered after autoindent applied"
15283 )
15284 })?;
15285
15286 Ok(Some(vec![]))
15287 }
15288 });
15289
15290 cx.simulate_keystroke(".");
15291 cx.run_until_parked();
15292
15293 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
15294 assert!(request.next().await.is_some());
15295 request.close();
15296 assert!(request.next().await.is_none());
15297}
15298
15299#[gpui::test]
15300async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
15301 init_test(cx, |_| {});
15302
15303 let fs = FakeFs::new(cx.executor());
15304 fs.insert_tree(
15305 path!("/a"),
15306 json!({
15307 "main.rs": "fn main() { let a = 5; }",
15308 "other.rs": "// Test file",
15309 }),
15310 )
15311 .await;
15312
15313 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15314
15315 let server_restarts = Arc::new(AtomicUsize::new(0));
15316 let closure_restarts = Arc::clone(&server_restarts);
15317 let language_server_name = "test language server";
15318 let language_name: LanguageName = "Rust".into();
15319
15320 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15321 language_registry.add(Arc::new(Language::new(
15322 LanguageConfig {
15323 name: language_name.clone(),
15324 matcher: LanguageMatcher {
15325 path_suffixes: vec!["rs".to_string()],
15326 ..Default::default()
15327 },
15328 ..Default::default()
15329 },
15330 Some(tree_sitter_rust::LANGUAGE.into()),
15331 )));
15332 let mut fake_servers = language_registry.register_fake_lsp(
15333 "Rust",
15334 FakeLspAdapter {
15335 name: language_server_name,
15336 initialization_options: Some(json!({
15337 "testOptionValue": true
15338 })),
15339 initializer: Some(Box::new(move |fake_server| {
15340 let task_restarts = Arc::clone(&closure_restarts);
15341 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
15342 task_restarts.fetch_add(1, atomic::Ordering::Release);
15343 futures::future::ready(Ok(()))
15344 });
15345 })),
15346 ..Default::default()
15347 },
15348 );
15349
15350 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15351 let _buffer = project
15352 .update(cx, |project, cx| {
15353 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
15354 })
15355 .await
15356 .unwrap();
15357 let _fake_server = fake_servers.next().await.unwrap();
15358 update_test_language_settings(cx, |language_settings| {
15359 language_settings.languages.0.insert(
15360 language_name.clone(),
15361 LanguageSettingsContent {
15362 tab_size: NonZeroU32::new(8),
15363 ..Default::default()
15364 },
15365 );
15366 });
15367 cx.executor().run_until_parked();
15368 assert_eq!(
15369 server_restarts.load(atomic::Ordering::Acquire),
15370 0,
15371 "Should not restart LSP server on an unrelated change"
15372 );
15373
15374 update_test_project_settings(cx, |project_settings| {
15375 project_settings.lsp.insert(
15376 "Some other server name".into(),
15377 LspSettings {
15378 binary: None,
15379 settings: None,
15380 initialization_options: Some(json!({
15381 "some other init value": false
15382 })),
15383 enable_lsp_tasks: false,
15384 },
15385 );
15386 });
15387 cx.executor().run_until_parked();
15388 assert_eq!(
15389 server_restarts.load(atomic::Ordering::Acquire),
15390 0,
15391 "Should not restart LSP server on an unrelated LSP settings change"
15392 );
15393
15394 update_test_project_settings(cx, |project_settings| {
15395 project_settings.lsp.insert(
15396 language_server_name.into(),
15397 LspSettings {
15398 binary: None,
15399 settings: None,
15400 initialization_options: Some(json!({
15401 "anotherInitValue": false
15402 })),
15403 enable_lsp_tasks: false,
15404 },
15405 );
15406 });
15407 cx.executor().run_until_parked();
15408 assert_eq!(
15409 server_restarts.load(atomic::Ordering::Acquire),
15410 1,
15411 "Should restart LSP server on a related LSP settings change"
15412 );
15413
15414 update_test_project_settings(cx, |project_settings| {
15415 project_settings.lsp.insert(
15416 language_server_name.into(),
15417 LspSettings {
15418 binary: None,
15419 settings: None,
15420 initialization_options: Some(json!({
15421 "anotherInitValue": false
15422 })),
15423 enable_lsp_tasks: false,
15424 },
15425 );
15426 });
15427 cx.executor().run_until_parked();
15428 assert_eq!(
15429 server_restarts.load(atomic::Ordering::Acquire),
15430 1,
15431 "Should not restart LSP server on a related LSP settings change that is the same"
15432 );
15433
15434 update_test_project_settings(cx, |project_settings| {
15435 project_settings.lsp.insert(
15436 language_server_name.into(),
15437 LspSettings {
15438 binary: None,
15439 settings: None,
15440 initialization_options: None,
15441 enable_lsp_tasks: false,
15442 },
15443 );
15444 });
15445 cx.executor().run_until_parked();
15446 assert_eq!(
15447 server_restarts.load(atomic::Ordering::Acquire),
15448 2,
15449 "Should restart LSP server on another related LSP settings change"
15450 );
15451}
15452
15453#[gpui::test]
15454async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
15455 init_test(cx, |_| {});
15456
15457 let mut cx = EditorLspTestContext::new_rust(
15458 lsp::ServerCapabilities {
15459 completion_provider: Some(lsp::CompletionOptions {
15460 trigger_characters: Some(vec![".".to_string()]),
15461 resolve_provider: Some(true),
15462 ..Default::default()
15463 }),
15464 ..Default::default()
15465 },
15466 cx,
15467 )
15468 .await;
15469
15470 cx.set_state("fn main() { let a = 2ˇ; }");
15471 cx.simulate_keystroke(".");
15472 let completion_item = lsp::CompletionItem {
15473 label: "some".into(),
15474 kind: Some(lsp::CompletionItemKind::SNIPPET),
15475 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15476 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15477 kind: lsp::MarkupKind::Markdown,
15478 value: "```rust\nSome(2)\n```".to_string(),
15479 })),
15480 deprecated: Some(false),
15481 sort_text: Some("fffffff2".to_string()),
15482 filter_text: Some("some".to_string()),
15483 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15484 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15485 range: lsp::Range {
15486 start: lsp::Position {
15487 line: 0,
15488 character: 22,
15489 },
15490 end: lsp::Position {
15491 line: 0,
15492 character: 22,
15493 },
15494 },
15495 new_text: "Some(2)".to_string(),
15496 })),
15497 additional_text_edits: Some(vec![lsp::TextEdit {
15498 range: lsp::Range {
15499 start: lsp::Position {
15500 line: 0,
15501 character: 20,
15502 },
15503 end: lsp::Position {
15504 line: 0,
15505 character: 22,
15506 },
15507 },
15508 new_text: "".to_string(),
15509 }]),
15510 ..Default::default()
15511 };
15512
15513 let closure_completion_item = completion_item.clone();
15514 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15515 let task_completion_item = closure_completion_item.clone();
15516 async move {
15517 Ok(Some(lsp::CompletionResponse::Array(vec![
15518 task_completion_item,
15519 ])))
15520 }
15521 });
15522
15523 request.next().await;
15524
15525 cx.condition(|editor, _| editor.context_menu_visible())
15526 .await;
15527 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15528 editor
15529 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15530 .unwrap()
15531 });
15532 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
15533
15534 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
15535 let task_completion_item = completion_item.clone();
15536 async move { Ok(task_completion_item) }
15537 })
15538 .next()
15539 .await
15540 .unwrap();
15541 apply_additional_edits.await.unwrap();
15542 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
15543}
15544
15545#[gpui::test]
15546async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
15547 init_test(cx, |_| {});
15548
15549 let mut cx = EditorLspTestContext::new_rust(
15550 lsp::ServerCapabilities {
15551 completion_provider: Some(lsp::CompletionOptions {
15552 trigger_characters: Some(vec![".".to_string()]),
15553 resolve_provider: Some(true),
15554 ..Default::default()
15555 }),
15556 ..Default::default()
15557 },
15558 cx,
15559 )
15560 .await;
15561
15562 cx.set_state("fn main() { let a = 2ˇ; }");
15563 cx.simulate_keystroke(".");
15564
15565 let item1 = lsp::CompletionItem {
15566 label: "method id()".to_string(),
15567 filter_text: Some("id".to_string()),
15568 detail: None,
15569 documentation: None,
15570 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15571 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15572 new_text: ".id".to_string(),
15573 })),
15574 ..lsp::CompletionItem::default()
15575 };
15576
15577 let item2 = lsp::CompletionItem {
15578 label: "other".to_string(),
15579 filter_text: Some("other".to_string()),
15580 detail: None,
15581 documentation: None,
15582 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15583 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15584 new_text: ".other".to_string(),
15585 })),
15586 ..lsp::CompletionItem::default()
15587 };
15588
15589 let item1 = item1.clone();
15590 cx.set_request_handler::<lsp::request::Completion, _, _>({
15591 let item1 = item1.clone();
15592 move |_, _, _| {
15593 let item1 = item1.clone();
15594 let item2 = item2.clone();
15595 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
15596 }
15597 })
15598 .next()
15599 .await;
15600
15601 cx.condition(|editor, _| editor.context_menu_visible())
15602 .await;
15603 cx.update_editor(|editor, _, _| {
15604 let context_menu = editor.context_menu.borrow_mut();
15605 let context_menu = context_menu
15606 .as_ref()
15607 .expect("Should have the context menu deployed");
15608 match context_menu {
15609 CodeContextMenu::Completions(completions_menu) => {
15610 let completions = completions_menu.completions.borrow_mut();
15611 assert_eq!(
15612 completions
15613 .iter()
15614 .map(|completion| &completion.label.text)
15615 .collect::<Vec<_>>(),
15616 vec!["method id()", "other"]
15617 )
15618 }
15619 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15620 }
15621 });
15622
15623 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
15624 let item1 = item1.clone();
15625 move |_, item_to_resolve, _| {
15626 let item1 = item1.clone();
15627 async move {
15628 if item1 == item_to_resolve {
15629 Ok(lsp::CompletionItem {
15630 label: "method id()".to_string(),
15631 filter_text: Some("id".to_string()),
15632 detail: Some("Now resolved!".to_string()),
15633 documentation: Some(lsp::Documentation::String("Docs".to_string())),
15634 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15635 range: lsp::Range::new(
15636 lsp::Position::new(0, 22),
15637 lsp::Position::new(0, 22),
15638 ),
15639 new_text: ".id".to_string(),
15640 })),
15641 ..lsp::CompletionItem::default()
15642 })
15643 } else {
15644 Ok(item_to_resolve)
15645 }
15646 }
15647 }
15648 })
15649 .next()
15650 .await
15651 .unwrap();
15652 cx.run_until_parked();
15653
15654 cx.update_editor(|editor, window, cx| {
15655 editor.context_menu_next(&Default::default(), window, cx);
15656 });
15657
15658 cx.update_editor(|editor, _, _| {
15659 let context_menu = editor.context_menu.borrow_mut();
15660 let context_menu = context_menu
15661 .as_ref()
15662 .expect("Should have the context menu deployed");
15663 match context_menu {
15664 CodeContextMenu::Completions(completions_menu) => {
15665 let completions = completions_menu.completions.borrow_mut();
15666 assert_eq!(
15667 completions
15668 .iter()
15669 .map(|completion| &completion.label.text)
15670 .collect::<Vec<_>>(),
15671 vec!["method id() Now resolved!", "other"],
15672 "Should update first completion label, but not second as the filter text did not match."
15673 );
15674 }
15675 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15676 }
15677 });
15678}
15679
15680#[gpui::test]
15681async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
15682 init_test(cx, |_| {});
15683 let mut cx = EditorLspTestContext::new_rust(
15684 lsp::ServerCapabilities {
15685 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
15686 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
15687 completion_provider: Some(lsp::CompletionOptions {
15688 resolve_provider: Some(true),
15689 ..Default::default()
15690 }),
15691 ..Default::default()
15692 },
15693 cx,
15694 )
15695 .await;
15696 cx.set_state(indoc! {"
15697 struct TestStruct {
15698 field: i32
15699 }
15700
15701 fn mainˇ() {
15702 let unused_var = 42;
15703 let test_struct = TestStruct { field: 42 };
15704 }
15705 "});
15706 let symbol_range = cx.lsp_range(indoc! {"
15707 struct TestStruct {
15708 field: i32
15709 }
15710
15711 «fn main»() {
15712 let unused_var = 42;
15713 let test_struct = TestStruct { field: 42 };
15714 }
15715 "});
15716 let mut hover_requests =
15717 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
15718 Ok(Some(lsp::Hover {
15719 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
15720 kind: lsp::MarkupKind::Markdown,
15721 value: "Function documentation".to_string(),
15722 }),
15723 range: Some(symbol_range),
15724 }))
15725 });
15726
15727 // Case 1: Test that code action menu hide hover popover
15728 cx.dispatch_action(Hover);
15729 hover_requests.next().await;
15730 cx.condition(|editor, _| editor.hover_state.visible()).await;
15731 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
15732 move |_, _, _| async move {
15733 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
15734 lsp::CodeAction {
15735 title: "Remove unused variable".to_string(),
15736 kind: Some(CodeActionKind::QUICKFIX),
15737 edit: Some(lsp::WorkspaceEdit {
15738 changes: Some(
15739 [(
15740 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
15741 vec![lsp::TextEdit {
15742 range: lsp::Range::new(
15743 lsp::Position::new(5, 4),
15744 lsp::Position::new(5, 27),
15745 ),
15746 new_text: "".to_string(),
15747 }],
15748 )]
15749 .into_iter()
15750 .collect(),
15751 ),
15752 ..Default::default()
15753 }),
15754 ..Default::default()
15755 },
15756 )]))
15757 },
15758 );
15759 cx.update_editor(|editor, window, cx| {
15760 editor.toggle_code_actions(
15761 &ToggleCodeActions {
15762 deployed_from: None,
15763 quick_launch: false,
15764 },
15765 window,
15766 cx,
15767 );
15768 });
15769 code_action_requests.next().await;
15770 cx.run_until_parked();
15771 cx.condition(|editor, _| editor.context_menu_visible())
15772 .await;
15773 cx.update_editor(|editor, _, _| {
15774 assert!(
15775 !editor.hover_state.visible(),
15776 "Hover popover should be hidden when code action menu is shown"
15777 );
15778 // Hide code actions
15779 editor.context_menu.take();
15780 });
15781
15782 // Case 2: Test that code completions hide hover popover
15783 cx.dispatch_action(Hover);
15784 hover_requests.next().await;
15785 cx.condition(|editor, _| editor.hover_state.visible()).await;
15786 let counter = Arc::new(AtomicUsize::new(0));
15787 let mut completion_requests =
15788 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15789 let counter = counter.clone();
15790 async move {
15791 counter.fetch_add(1, atomic::Ordering::Release);
15792 Ok(Some(lsp::CompletionResponse::Array(vec![
15793 lsp::CompletionItem {
15794 label: "main".into(),
15795 kind: Some(lsp::CompletionItemKind::FUNCTION),
15796 detail: Some("() -> ()".to_string()),
15797 ..Default::default()
15798 },
15799 lsp::CompletionItem {
15800 label: "TestStruct".into(),
15801 kind: Some(lsp::CompletionItemKind::STRUCT),
15802 detail: Some("struct TestStruct".to_string()),
15803 ..Default::default()
15804 },
15805 ])))
15806 }
15807 });
15808 cx.update_editor(|editor, window, cx| {
15809 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15810 });
15811 completion_requests.next().await;
15812 cx.condition(|editor, _| editor.context_menu_visible())
15813 .await;
15814 cx.update_editor(|editor, _, _| {
15815 assert!(
15816 !editor.hover_state.visible(),
15817 "Hover popover should be hidden when completion menu is shown"
15818 );
15819 });
15820}
15821
15822#[gpui::test]
15823async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
15824 init_test(cx, |_| {});
15825
15826 let mut cx = EditorLspTestContext::new_rust(
15827 lsp::ServerCapabilities {
15828 completion_provider: Some(lsp::CompletionOptions {
15829 trigger_characters: Some(vec![".".to_string()]),
15830 resolve_provider: Some(true),
15831 ..Default::default()
15832 }),
15833 ..Default::default()
15834 },
15835 cx,
15836 )
15837 .await;
15838
15839 cx.set_state("fn main() { let a = 2ˇ; }");
15840 cx.simulate_keystroke(".");
15841
15842 let unresolved_item_1 = lsp::CompletionItem {
15843 label: "id".to_string(),
15844 filter_text: Some("id".to_string()),
15845 detail: None,
15846 documentation: None,
15847 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15848 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15849 new_text: ".id".to_string(),
15850 })),
15851 ..lsp::CompletionItem::default()
15852 };
15853 let resolved_item_1 = lsp::CompletionItem {
15854 additional_text_edits: Some(vec![lsp::TextEdit {
15855 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15856 new_text: "!!".to_string(),
15857 }]),
15858 ..unresolved_item_1.clone()
15859 };
15860 let unresolved_item_2 = lsp::CompletionItem {
15861 label: "other".to_string(),
15862 filter_text: Some("other".to_string()),
15863 detail: None,
15864 documentation: None,
15865 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15866 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15867 new_text: ".other".to_string(),
15868 })),
15869 ..lsp::CompletionItem::default()
15870 };
15871 let resolved_item_2 = lsp::CompletionItem {
15872 additional_text_edits: Some(vec![lsp::TextEdit {
15873 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15874 new_text: "??".to_string(),
15875 }]),
15876 ..unresolved_item_2.clone()
15877 };
15878
15879 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
15880 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
15881 cx.lsp
15882 .server
15883 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15884 let unresolved_item_1 = unresolved_item_1.clone();
15885 let resolved_item_1 = resolved_item_1.clone();
15886 let unresolved_item_2 = unresolved_item_2.clone();
15887 let resolved_item_2 = resolved_item_2.clone();
15888 let resolve_requests_1 = resolve_requests_1.clone();
15889 let resolve_requests_2 = resolve_requests_2.clone();
15890 move |unresolved_request, _| {
15891 let unresolved_item_1 = unresolved_item_1.clone();
15892 let resolved_item_1 = resolved_item_1.clone();
15893 let unresolved_item_2 = unresolved_item_2.clone();
15894 let resolved_item_2 = resolved_item_2.clone();
15895 let resolve_requests_1 = resolve_requests_1.clone();
15896 let resolve_requests_2 = resolve_requests_2.clone();
15897 async move {
15898 if unresolved_request == unresolved_item_1 {
15899 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
15900 Ok(resolved_item_1.clone())
15901 } else if unresolved_request == unresolved_item_2 {
15902 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
15903 Ok(resolved_item_2.clone())
15904 } else {
15905 panic!("Unexpected completion item {unresolved_request:?}")
15906 }
15907 }
15908 }
15909 })
15910 .detach();
15911
15912 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15913 let unresolved_item_1 = unresolved_item_1.clone();
15914 let unresolved_item_2 = unresolved_item_2.clone();
15915 async move {
15916 Ok(Some(lsp::CompletionResponse::Array(vec![
15917 unresolved_item_1,
15918 unresolved_item_2,
15919 ])))
15920 }
15921 })
15922 .next()
15923 .await;
15924
15925 cx.condition(|editor, _| editor.context_menu_visible())
15926 .await;
15927 cx.update_editor(|editor, _, _| {
15928 let context_menu = editor.context_menu.borrow_mut();
15929 let context_menu = context_menu
15930 .as_ref()
15931 .expect("Should have the context menu deployed");
15932 match context_menu {
15933 CodeContextMenu::Completions(completions_menu) => {
15934 let completions = completions_menu.completions.borrow_mut();
15935 assert_eq!(
15936 completions
15937 .iter()
15938 .map(|completion| &completion.label.text)
15939 .collect::<Vec<_>>(),
15940 vec!["id", "other"]
15941 )
15942 }
15943 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15944 }
15945 });
15946 cx.run_until_parked();
15947
15948 cx.update_editor(|editor, window, cx| {
15949 editor.context_menu_next(&ContextMenuNext, window, cx);
15950 });
15951 cx.run_until_parked();
15952 cx.update_editor(|editor, window, cx| {
15953 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15954 });
15955 cx.run_until_parked();
15956 cx.update_editor(|editor, window, cx| {
15957 editor.context_menu_next(&ContextMenuNext, window, cx);
15958 });
15959 cx.run_until_parked();
15960 cx.update_editor(|editor, window, cx| {
15961 editor
15962 .compose_completion(&ComposeCompletion::default(), window, cx)
15963 .expect("No task returned")
15964 })
15965 .await
15966 .expect("Completion failed");
15967 cx.run_until_parked();
15968
15969 cx.update_editor(|editor, _, cx| {
15970 assert_eq!(
15971 resolve_requests_1.load(atomic::Ordering::Acquire),
15972 1,
15973 "Should always resolve once despite multiple selections"
15974 );
15975 assert_eq!(
15976 resolve_requests_2.load(atomic::Ordering::Acquire),
15977 1,
15978 "Should always resolve once after multiple selections and applying the completion"
15979 );
15980 assert_eq!(
15981 editor.text(cx),
15982 "fn main() { let a = ??.other; }",
15983 "Should use resolved data when applying the completion"
15984 );
15985 });
15986}
15987
15988#[gpui::test]
15989async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
15990 init_test(cx, |_| {});
15991
15992 let item_0 = lsp::CompletionItem {
15993 label: "abs".into(),
15994 insert_text: Some("abs".into()),
15995 data: Some(json!({ "very": "special"})),
15996 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
15997 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
15998 lsp::InsertReplaceEdit {
15999 new_text: "abs".to_string(),
16000 insert: lsp::Range::default(),
16001 replace: lsp::Range::default(),
16002 },
16003 )),
16004 ..lsp::CompletionItem::default()
16005 };
16006 let items = iter::once(item_0.clone())
16007 .chain((11..51).map(|i| lsp::CompletionItem {
16008 label: format!("item_{}", i),
16009 insert_text: Some(format!("item_{}", i)),
16010 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
16011 ..lsp::CompletionItem::default()
16012 }))
16013 .collect::<Vec<_>>();
16014
16015 let default_commit_characters = vec!["?".to_string()];
16016 let default_data = json!({ "default": "data"});
16017 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
16018 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
16019 let default_edit_range = lsp::Range {
16020 start: lsp::Position {
16021 line: 0,
16022 character: 5,
16023 },
16024 end: lsp::Position {
16025 line: 0,
16026 character: 5,
16027 },
16028 };
16029
16030 let mut cx = EditorLspTestContext::new_rust(
16031 lsp::ServerCapabilities {
16032 completion_provider: Some(lsp::CompletionOptions {
16033 trigger_characters: Some(vec![".".to_string()]),
16034 resolve_provider: Some(true),
16035 ..Default::default()
16036 }),
16037 ..Default::default()
16038 },
16039 cx,
16040 )
16041 .await;
16042
16043 cx.set_state("fn main() { let a = 2ˇ; }");
16044 cx.simulate_keystroke(".");
16045
16046 let completion_data = default_data.clone();
16047 let completion_characters = default_commit_characters.clone();
16048 let completion_items = items.clone();
16049 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16050 let default_data = completion_data.clone();
16051 let default_commit_characters = completion_characters.clone();
16052 let items = completion_items.clone();
16053 async move {
16054 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16055 items,
16056 item_defaults: Some(lsp::CompletionListItemDefaults {
16057 data: Some(default_data.clone()),
16058 commit_characters: Some(default_commit_characters.clone()),
16059 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
16060 default_edit_range,
16061 )),
16062 insert_text_format: Some(default_insert_text_format),
16063 insert_text_mode: Some(default_insert_text_mode),
16064 }),
16065 ..lsp::CompletionList::default()
16066 })))
16067 }
16068 })
16069 .next()
16070 .await;
16071
16072 let resolved_items = Arc::new(Mutex::new(Vec::new()));
16073 cx.lsp
16074 .server
16075 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
16076 let closure_resolved_items = resolved_items.clone();
16077 move |item_to_resolve, _| {
16078 let closure_resolved_items = closure_resolved_items.clone();
16079 async move {
16080 closure_resolved_items.lock().push(item_to_resolve.clone());
16081 Ok(item_to_resolve)
16082 }
16083 }
16084 })
16085 .detach();
16086
16087 cx.condition(|editor, _| editor.context_menu_visible())
16088 .await;
16089 cx.run_until_parked();
16090 cx.update_editor(|editor, _, _| {
16091 let menu = editor.context_menu.borrow_mut();
16092 match menu.as_ref().expect("should have the completions menu") {
16093 CodeContextMenu::Completions(completions_menu) => {
16094 assert_eq!(
16095 completions_menu
16096 .entries
16097 .borrow()
16098 .iter()
16099 .map(|mat| mat.string.clone())
16100 .collect::<Vec<String>>(),
16101 items
16102 .iter()
16103 .map(|completion| completion.label.clone())
16104 .collect::<Vec<String>>()
16105 );
16106 }
16107 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
16108 }
16109 });
16110 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
16111 // with 4 from the end.
16112 assert_eq!(
16113 *resolved_items.lock(),
16114 [&items[0..16], &items[items.len() - 4..items.len()]]
16115 .concat()
16116 .iter()
16117 .cloned()
16118 .map(|mut item| {
16119 if item.data.is_none() {
16120 item.data = Some(default_data.clone());
16121 }
16122 item
16123 })
16124 .collect::<Vec<lsp::CompletionItem>>(),
16125 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
16126 );
16127 resolved_items.lock().clear();
16128
16129 cx.update_editor(|editor, window, cx| {
16130 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
16131 });
16132 cx.run_until_parked();
16133 // Completions that have already been resolved are skipped.
16134 assert_eq!(
16135 *resolved_items.lock(),
16136 items[items.len() - 17..items.len() - 4]
16137 .iter()
16138 .cloned()
16139 .map(|mut item| {
16140 if item.data.is_none() {
16141 item.data = Some(default_data.clone());
16142 }
16143 item
16144 })
16145 .collect::<Vec<lsp::CompletionItem>>()
16146 );
16147 resolved_items.lock().clear();
16148}
16149
16150#[gpui::test]
16151async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
16152 init_test(cx, |_| {});
16153
16154 let mut cx = EditorLspTestContext::new(
16155 Language::new(
16156 LanguageConfig {
16157 matcher: LanguageMatcher {
16158 path_suffixes: vec!["jsx".into()],
16159 ..Default::default()
16160 },
16161 overrides: [(
16162 "element".into(),
16163 LanguageConfigOverride {
16164 completion_query_characters: Override::Set(['-'].into_iter().collect()),
16165 ..Default::default()
16166 },
16167 )]
16168 .into_iter()
16169 .collect(),
16170 ..Default::default()
16171 },
16172 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16173 )
16174 .with_override_query("(jsx_self_closing_element) @element")
16175 .unwrap(),
16176 lsp::ServerCapabilities {
16177 completion_provider: Some(lsp::CompletionOptions {
16178 trigger_characters: Some(vec![":".to_string()]),
16179 ..Default::default()
16180 }),
16181 ..Default::default()
16182 },
16183 cx,
16184 )
16185 .await;
16186
16187 cx.lsp
16188 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16189 Ok(Some(lsp::CompletionResponse::Array(vec![
16190 lsp::CompletionItem {
16191 label: "bg-blue".into(),
16192 ..Default::default()
16193 },
16194 lsp::CompletionItem {
16195 label: "bg-red".into(),
16196 ..Default::default()
16197 },
16198 lsp::CompletionItem {
16199 label: "bg-yellow".into(),
16200 ..Default::default()
16201 },
16202 ])))
16203 });
16204
16205 cx.set_state(r#"<p class="bgˇ" />"#);
16206
16207 // Trigger completion when typing a dash, because the dash is an extra
16208 // word character in the 'element' scope, which contains the cursor.
16209 cx.simulate_keystroke("-");
16210 cx.executor().run_until_parked();
16211 cx.update_editor(|editor, _, _| {
16212 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16213 {
16214 assert_eq!(
16215 completion_menu_entries(&menu),
16216 &["bg-blue", "bg-red", "bg-yellow"]
16217 );
16218 } else {
16219 panic!("expected completion menu to be open");
16220 }
16221 });
16222
16223 cx.simulate_keystroke("l");
16224 cx.executor().run_until_parked();
16225 cx.update_editor(|editor, _, _| {
16226 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16227 {
16228 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
16229 } else {
16230 panic!("expected completion menu to be open");
16231 }
16232 });
16233
16234 // When filtering completions, consider the character after the '-' to
16235 // be the start of a subword.
16236 cx.set_state(r#"<p class="yelˇ" />"#);
16237 cx.simulate_keystroke("l");
16238 cx.executor().run_until_parked();
16239 cx.update_editor(|editor, _, _| {
16240 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16241 {
16242 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
16243 } else {
16244 panic!("expected completion menu to be open");
16245 }
16246 });
16247}
16248
16249fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
16250 let entries = menu.entries.borrow();
16251 entries.iter().map(|mat| mat.string.clone()).collect()
16252}
16253
16254#[gpui::test]
16255async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
16256 init_test(cx, |settings| {
16257 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
16258 Formatter::Prettier,
16259 )))
16260 });
16261
16262 let fs = FakeFs::new(cx.executor());
16263 fs.insert_file(path!("/file.ts"), Default::default()).await;
16264
16265 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
16266 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16267
16268 language_registry.add(Arc::new(Language::new(
16269 LanguageConfig {
16270 name: "TypeScript".into(),
16271 matcher: LanguageMatcher {
16272 path_suffixes: vec!["ts".to_string()],
16273 ..Default::default()
16274 },
16275 ..Default::default()
16276 },
16277 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
16278 )));
16279 update_test_language_settings(cx, |settings| {
16280 settings.defaults.prettier = Some(PrettierSettings {
16281 allowed: true,
16282 ..PrettierSettings::default()
16283 });
16284 });
16285
16286 let test_plugin = "test_plugin";
16287 let _ = language_registry.register_fake_lsp(
16288 "TypeScript",
16289 FakeLspAdapter {
16290 prettier_plugins: vec![test_plugin],
16291 ..Default::default()
16292 },
16293 );
16294
16295 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
16296 let buffer = project
16297 .update(cx, |project, cx| {
16298 project.open_local_buffer(path!("/file.ts"), cx)
16299 })
16300 .await
16301 .unwrap();
16302
16303 let buffer_text = "one\ntwo\nthree\n";
16304 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16305 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16306 editor.update_in(cx, |editor, window, cx| {
16307 editor.set_text(buffer_text, window, cx)
16308 });
16309
16310 editor
16311 .update_in(cx, |editor, window, cx| {
16312 editor.perform_format(
16313 project.clone(),
16314 FormatTrigger::Manual,
16315 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16316 window,
16317 cx,
16318 )
16319 })
16320 .unwrap()
16321 .await;
16322 assert_eq!(
16323 editor.update(cx, |editor, cx| editor.text(cx)),
16324 buffer_text.to_string() + prettier_format_suffix,
16325 "Test prettier formatting was not applied to the original buffer text",
16326 );
16327
16328 update_test_language_settings(cx, |settings| {
16329 settings.defaults.formatter = Some(SelectedFormatter::Auto)
16330 });
16331 let format = editor.update_in(cx, |editor, window, cx| {
16332 editor.perform_format(
16333 project.clone(),
16334 FormatTrigger::Manual,
16335 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16336 window,
16337 cx,
16338 )
16339 });
16340 format.await.unwrap();
16341 assert_eq!(
16342 editor.update(cx, |editor, cx| editor.text(cx)),
16343 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
16344 "Autoformatting (via test prettier) was not applied to the original buffer text",
16345 );
16346}
16347
16348#[gpui::test]
16349async fn test_addition_reverts(cx: &mut TestAppContext) {
16350 init_test(cx, |_| {});
16351 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16352 let base_text = indoc! {r#"
16353 struct Row;
16354 struct Row1;
16355 struct Row2;
16356
16357 struct Row4;
16358 struct Row5;
16359 struct Row6;
16360
16361 struct Row8;
16362 struct Row9;
16363 struct Row10;"#};
16364
16365 // When addition hunks are not adjacent to carets, no hunk revert is performed
16366 assert_hunk_revert(
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 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16384 indoc! {r#"struct Row;
16385 struct Row1;
16386 struct Row1.1;
16387 struct Row1.2;
16388 struct Row2;ˇ
16389
16390 struct Row4;
16391 struct Row5;
16392 struct Row6;
16393
16394 struct Row8;
16395 ˇstruct Row9;
16396 struct Row9.1;
16397 struct Row9.2;
16398 struct Row9.3;
16399 struct Row10;"#},
16400 base_text,
16401 &mut cx,
16402 );
16403 // Same for selections
16404 assert_hunk_revert(
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 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16422 indoc! {r#"struct Row;
16423 struct Row1;
16424 struct Row2;
16425 struct Row2.1;
16426 struct Row2.2;
16427 «ˇ
16428 struct Row4;
16429 struct» Row5;
16430 «struct Row6;
16431 ˇ»
16432 struct Row9.1;
16433 struct Row9.2;
16434 struct Row9.3;
16435 struct Row8;
16436 struct Row9;
16437 struct Row10;"#},
16438 base_text,
16439 &mut cx,
16440 );
16441
16442 // When carets and selections intersect the addition hunks, those are reverted.
16443 // Adjacent carets got merged.
16444 assert_hunk_revert(
16445 indoc! {r#"struct Row;
16446 ˇ// something on the top
16447 struct Row1;
16448 struct Row2;
16449 struct Roˇw3.1;
16450 struct Row2.2;
16451 struct Row2.3;ˇ
16452
16453 struct Row4;
16454 struct ˇRow5.1;
16455 struct Row5.2;
16456 struct «Rowˇ»5.3;
16457 struct Row5;
16458 struct Row6;
16459 ˇ
16460 struct Row9.1;
16461 struct «Rowˇ»9.2;
16462 struct «ˇRow»9.3;
16463 struct Row8;
16464 struct Row9;
16465 «ˇ// something on bottom»
16466 struct Row10;"#},
16467 vec![
16468 DiffHunkStatusKind::Added,
16469 DiffHunkStatusKind::Added,
16470 DiffHunkStatusKind::Added,
16471 DiffHunkStatusKind::Added,
16472 DiffHunkStatusKind::Added,
16473 ],
16474 indoc! {r#"struct Row;
16475 ˇstruct Row1;
16476 struct Row2;
16477 ˇ
16478 struct Row4;
16479 ˇstruct Row5;
16480 struct Row6;
16481 ˇ
16482 ˇstruct Row8;
16483 struct Row9;
16484 ˇstruct Row10;"#},
16485 base_text,
16486 &mut cx,
16487 );
16488}
16489
16490#[gpui::test]
16491async fn test_modification_reverts(cx: &mut TestAppContext) {
16492 init_test(cx, |_| {});
16493 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16494 let base_text = indoc! {r#"
16495 struct Row;
16496 struct Row1;
16497 struct Row2;
16498
16499 struct Row4;
16500 struct Row5;
16501 struct Row6;
16502
16503 struct Row8;
16504 struct Row9;
16505 struct Row10;"#};
16506
16507 // Modification hunks behave the same as the addition ones.
16508 assert_hunk_revert(
16509 indoc! {r#"struct Row;
16510 struct Row1;
16511 struct Row33;
16512 ˇ
16513 struct Row4;
16514 struct Row5;
16515 struct Row6;
16516 ˇ
16517 struct Row99;
16518 struct Row9;
16519 struct Row10;"#},
16520 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16521 indoc! {r#"struct Row;
16522 struct Row1;
16523 struct Row33;
16524 ˇ
16525 struct Row4;
16526 struct Row5;
16527 struct Row6;
16528 ˇ
16529 struct Row99;
16530 struct Row9;
16531 struct Row10;"#},
16532 base_text,
16533 &mut cx,
16534 );
16535 assert_hunk_revert(
16536 indoc! {r#"struct Row;
16537 struct Row1;
16538 struct Row33;
16539 «ˇ
16540 struct Row4;
16541 struct» Row5;
16542 «struct Row6;
16543 ˇ»
16544 struct Row99;
16545 struct Row9;
16546 struct Row10;"#},
16547 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16548 indoc! {r#"struct Row;
16549 struct Row1;
16550 struct Row33;
16551 «ˇ
16552 struct Row4;
16553 struct» Row5;
16554 «struct Row6;
16555 ˇ»
16556 struct Row99;
16557 struct Row9;
16558 struct Row10;"#},
16559 base_text,
16560 &mut cx,
16561 );
16562
16563 assert_hunk_revert(
16564 indoc! {r#"ˇstruct Row1.1;
16565 struct Row1;
16566 «ˇstr»uct Row22;
16567
16568 struct ˇRow44;
16569 struct Row5;
16570 struct «Rˇ»ow66;ˇ
16571
16572 «struˇ»ct Row88;
16573 struct Row9;
16574 struct Row1011;ˇ"#},
16575 vec![
16576 DiffHunkStatusKind::Modified,
16577 DiffHunkStatusKind::Modified,
16578 DiffHunkStatusKind::Modified,
16579 DiffHunkStatusKind::Modified,
16580 DiffHunkStatusKind::Modified,
16581 DiffHunkStatusKind::Modified,
16582 ],
16583 indoc! {r#"struct Row;
16584 ˇstruct Row1;
16585 struct Row2;
16586 ˇ
16587 struct Row4;
16588 ˇstruct Row5;
16589 struct Row6;
16590 ˇ
16591 struct Row8;
16592 ˇstruct Row9;
16593 struct Row10;ˇ"#},
16594 base_text,
16595 &mut cx,
16596 );
16597}
16598
16599#[gpui::test]
16600async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
16601 init_test(cx, |_| {});
16602 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16603 let base_text = indoc! {r#"
16604 one
16605
16606 two
16607 three
16608 "#};
16609
16610 cx.set_head_text(base_text);
16611 cx.set_state("\nˇ\n");
16612 cx.executor().run_until_parked();
16613 cx.update_editor(|editor, _window, cx| {
16614 editor.expand_selected_diff_hunks(cx);
16615 });
16616 cx.executor().run_until_parked();
16617 cx.update_editor(|editor, window, cx| {
16618 editor.backspace(&Default::default(), window, cx);
16619 });
16620 cx.run_until_parked();
16621 cx.assert_state_with_diff(
16622 indoc! {r#"
16623
16624 - two
16625 - threeˇ
16626 +
16627 "#}
16628 .to_string(),
16629 );
16630}
16631
16632#[gpui::test]
16633async fn test_deletion_reverts(cx: &mut TestAppContext) {
16634 init_test(cx, |_| {});
16635 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16636 let base_text = indoc! {r#"struct Row;
16637struct Row1;
16638struct Row2;
16639
16640struct Row4;
16641struct Row5;
16642struct Row6;
16643
16644struct Row8;
16645struct Row9;
16646struct Row10;"#};
16647
16648 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
16649 assert_hunk_revert(
16650 indoc! {r#"struct Row;
16651 struct Row2;
16652
16653 ˇstruct Row4;
16654 struct Row5;
16655 struct Row6;
16656 ˇ
16657 struct Row8;
16658 struct Row10;"#},
16659 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16660 indoc! {r#"struct Row;
16661 struct Row2;
16662
16663 ˇstruct Row4;
16664 struct Row5;
16665 struct Row6;
16666 ˇ
16667 struct Row8;
16668 struct Row10;"#},
16669 base_text,
16670 &mut cx,
16671 );
16672 assert_hunk_revert(
16673 indoc! {r#"struct Row;
16674 struct Row2;
16675
16676 «ˇstruct Row4;
16677 struct» Row5;
16678 «struct Row6;
16679 ˇ»
16680 struct Row8;
16681 struct Row10;"#},
16682 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16683 indoc! {r#"struct Row;
16684 struct Row2;
16685
16686 «ˇstruct Row4;
16687 struct» Row5;
16688 «struct Row6;
16689 ˇ»
16690 struct Row8;
16691 struct Row10;"#},
16692 base_text,
16693 &mut cx,
16694 );
16695
16696 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
16697 assert_hunk_revert(
16698 indoc! {r#"struct Row;
16699 ˇstruct Row2;
16700
16701 struct Row4;
16702 struct Row5;
16703 struct Row6;
16704
16705 struct Row8;ˇ
16706 struct Row10;"#},
16707 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16708 indoc! {r#"struct Row;
16709 struct Row1;
16710 ˇstruct Row2;
16711
16712 struct Row4;
16713 struct Row5;
16714 struct Row6;
16715
16716 struct Row8;ˇ
16717 struct Row9;
16718 struct Row10;"#},
16719 base_text,
16720 &mut cx,
16721 );
16722 assert_hunk_revert(
16723 indoc! {r#"struct Row;
16724 struct Row2«ˇ;
16725 struct Row4;
16726 struct» Row5;
16727 «struct Row6;
16728
16729 struct Row8;ˇ»
16730 struct Row10;"#},
16731 vec![
16732 DiffHunkStatusKind::Deleted,
16733 DiffHunkStatusKind::Deleted,
16734 DiffHunkStatusKind::Deleted,
16735 ],
16736 indoc! {r#"struct Row;
16737 struct Row1;
16738 struct Row2«ˇ;
16739
16740 struct Row4;
16741 struct» Row5;
16742 «struct Row6;
16743
16744 struct Row8;ˇ»
16745 struct Row9;
16746 struct Row10;"#},
16747 base_text,
16748 &mut cx,
16749 );
16750}
16751
16752#[gpui::test]
16753async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
16754 init_test(cx, |_| {});
16755
16756 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
16757 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
16758 let base_text_3 =
16759 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
16760
16761 let text_1 = edit_first_char_of_every_line(base_text_1);
16762 let text_2 = edit_first_char_of_every_line(base_text_2);
16763 let text_3 = edit_first_char_of_every_line(base_text_3);
16764
16765 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
16766 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
16767 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
16768
16769 let multibuffer = cx.new(|cx| {
16770 let mut multibuffer = MultiBuffer::new(ReadWrite);
16771 multibuffer.push_excerpts(
16772 buffer_1.clone(),
16773 [
16774 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16775 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16776 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16777 ],
16778 cx,
16779 );
16780 multibuffer.push_excerpts(
16781 buffer_2.clone(),
16782 [
16783 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16784 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16785 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16786 ],
16787 cx,
16788 );
16789 multibuffer.push_excerpts(
16790 buffer_3.clone(),
16791 [
16792 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16793 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16794 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16795 ],
16796 cx,
16797 );
16798 multibuffer
16799 });
16800
16801 let fs = FakeFs::new(cx.executor());
16802 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
16803 let (editor, cx) = cx
16804 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
16805 editor.update_in(cx, |editor, _window, cx| {
16806 for (buffer, diff_base) in [
16807 (buffer_1.clone(), base_text_1),
16808 (buffer_2.clone(), base_text_2),
16809 (buffer_3.clone(), base_text_3),
16810 ] {
16811 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16812 editor
16813 .buffer
16814 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16815 }
16816 });
16817 cx.executor().run_until_parked();
16818
16819 editor.update_in(cx, |editor, window, cx| {
16820 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}");
16821 editor.select_all(&SelectAll, window, cx);
16822 editor.git_restore(&Default::default(), window, cx);
16823 });
16824 cx.executor().run_until_parked();
16825
16826 // When all ranges are selected, all buffer hunks are reverted.
16827 editor.update(cx, |editor, cx| {
16828 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");
16829 });
16830 buffer_1.update(cx, |buffer, _| {
16831 assert_eq!(buffer.text(), base_text_1);
16832 });
16833 buffer_2.update(cx, |buffer, _| {
16834 assert_eq!(buffer.text(), base_text_2);
16835 });
16836 buffer_3.update(cx, |buffer, _| {
16837 assert_eq!(buffer.text(), base_text_3);
16838 });
16839
16840 editor.update_in(cx, |editor, window, cx| {
16841 editor.undo(&Default::default(), window, cx);
16842 });
16843
16844 editor.update_in(cx, |editor, window, cx| {
16845 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16846 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
16847 });
16848 editor.git_restore(&Default::default(), window, cx);
16849 });
16850
16851 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
16852 // but not affect buffer_2 and its related excerpts.
16853 editor.update(cx, |editor, cx| {
16854 assert_eq!(
16855 editor.text(cx),
16856 "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}"
16857 );
16858 });
16859 buffer_1.update(cx, |buffer, _| {
16860 assert_eq!(buffer.text(), base_text_1);
16861 });
16862 buffer_2.update(cx, |buffer, _| {
16863 assert_eq!(
16864 buffer.text(),
16865 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
16866 );
16867 });
16868 buffer_3.update(cx, |buffer, _| {
16869 assert_eq!(
16870 buffer.text(),
16871 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
16872 );
16873 });
16874
16875 fn edit_first_char_of_every_line(text: &str) -> String {
16876 text.split('\n')
16877 .map(|line| format!("X{}", &line[1..]))
16878 .collect::<Vec<_>>()
16879 .join("\n")
16880 }
16881}
16882
16883#[gpui::test]
16884async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
16885 init_test(cx, |_| {});
16886
16887 let cols = 4;
16888 let rows = 10;
16889 let sample_text_1 = sample_text(rows, cols, 'a');
16890 assert_eq!(
16891 sample_text_1,
16892 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
16893 );
16894 let sample_text_2 = sample_text(rows, cols, 'l');
16895 assert_eq!(
16896 sample_text_2,
16897 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
16898 );
16899 let sample_text_3 = sample_text(rows, cols, 'v');
16900 assert_eq!(
16901 sample_text_3,
16902 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
16903 );
16904
16905 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
16906 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
16907 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
16908
16909 let multi_buffer = cx.new(|cx| {
16910 let mut multibuffer = MultiBuffer::new(ReadWrite);
16911 multibuffer.push_excerpts(
16912 buffer_1.clone(),
16913 [
16914 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16915 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16916 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16917 ],
16918 cx,
16919 );
16920 multibuffer.push_excerpts(
16921 buffer_2.clone(),
16922 [
16923 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16924 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16925 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16926 ],
16927 cx,
16928 );
16929 multibuffer.push_excerpts(
16930 buffer_3.clone(),
16931 [
16932 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16933 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16934 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16935 ],
16936 cx,
16937 );
16938 multibuffer
16939 });
16940
16941 let fs = FakeFs::new(cx.executor());
16942 fs.insert_tree(
16943 "/a",
16944 json!({
16945 "main.rs": sample_text_1,
16946 "other.rs": sample_text_2,
16947 "lib.rs": sample_text_3,
16948 }),
16949 )
16950 .await;
16951 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16952 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16953 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16954 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16955 Editor::new(
16956 EditorMode::full(),
16957 multi_buffer,
16958 Some(project.clone()),
16959 window,
16960 cx,
16961 )
16962 });
16963 let multibuffer_item_id = workspace
16964 .update(cx, |workspace, window, cx| {
16965 assert!(
16966 workspace.active_item(cx).is_none(),
16967 "active item should be None before the first item is added"
16968 );
16969 workspace.add_item_to_active_pane(
16970 Box::new(multi_buffer_editor.clone()),
16971 None,
16972 true,
16973 window,
16974 cx,
16975 );
16976 let active_item = workspace
16977 .active_item(cx)
16978 .expect("should have an active item after adding the multi buffer");
16979 assert!(
16980 !active_item.is_singleton(cx),
16981 "A multi buffer was expected to active after adding"
16982 );
16983 active_item.item_id()
16984 })
16985 .unwrap();
16986 cx.executor().run_until_parked();
16987
16988 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16989 editor.change_selections(
16990 SelectionEffects::scroll(Autoscroll::Next),
16991 window,
16992 cx,
16993 |s| s.select_ranges(Some(1..2)),
16994 );
16995 editor.open_excerpts(&OpenExcerpts, window, cx);
16996 });
16997 cx.executor().run_until_parked();
16998 let first_item_id = workspace
16999 .update(cx, |workspace, window, cx| {
17000 let active_item = workspace
17001 .active_item(cx)
17002 .expect("should have an active item after navigating into the 1st buffer");
17003 let first_item_id = active_item.item_id();
17004 assert_ne!(
17005 first_item_id, multibuffer_item_id,
17006 "Should navigate into the 1st buffer and activate it"
17007 );
17008 assert!(
17009 active_item.is_singleton(cx),
17010 "New active item should be a singleton buffer"
17011 );
17012 assert_eq!(
17013 active_item
17014 .act_as::<Editor>(cx)
17015 .expect("should have navigated into an editor for the 1st buffer")
17016 .read(cx)
17017 .text(cx),
17018 sample_text_1
17019 );
17020
17021 workspace
17022 .go_back(workspace.active_pane().downgrade(), window, cx)
17023 .detach_and_log_err(cx);
17024
17025 first_item_id
17026 })
17027 .unwrap();
17028 cx.executor().run_until_parked();
17029 workspace
17030 .update(cx, |workspace, _, cx| {
17031 let active_item = workspace
17032 .active_item(cx)
17033 .expect("should have an active item after navigating back");
17034 assert_eq!(
17035 active_item.item_id(),
17036 multibuffer_item_id,
17037 "Should navigate back to the multi buffer"
17038 );
17039 assert!(!active_item.is_singleton(cx));
17040 })
17041 .unwrap();
17042
17043 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17044 editor.change_selections(
17045 SelectionEffects::scroll(Autoscroll::Next),
17046 window,
17047 cx,
17048 |s| s.select_ranges(Some(39..40)),
17049 );
17050 editor.open_excerpts(&OpenExcerpts, window, cx);
17051 });
17052 cx.executor().run_until_parked();
17053 let second_item_id = workspace
17054 .update(cx, |workspace, window, cx| {
17055 let active_item = workspace
17056 .active_item(cx)
17057 .expect("should have an active item after navigating into the 2nd buffer");
17058 let second_item_id = active_item.item_id();
17059 assert_ne!(
17060 second_item_id, multibuffer_item_id,
17061 "Should navigate away from the multibuffer"
17062 );
17063 assert_ne!(
17064 second_item_id, first_item_id,
17065 "Should navigate into the 2nd buffer and activate it"
17066 );
17067 assert!(
17068 active_item.is_singleton(cx),
17069 "New active item should be a singleton buffer"
17070 );
17071 assert_eq!(
17072 active_item
17073 .act_as::<Editor>(cx)
17074 .expect("should have navigated into an editor")
17075 .read(cx)
17076 .text(cx),
17077 sample_text_2
17078 );
17079
17080 workspace
17081 .go_back(workspace.active_pane().downgrade(), window, cx)
17082 .detach_and_log_err(cx);
17083
17084 second_item_id
17085 })
17086 .unwrap();
17087 cx.executor().run_until_parked();
17088 workspace
17089 .update(cx, |workspace, _, cx| {
17090 let active_item = workspace
17091 .active_item(cx)
17092 .expect("should have an active item after navigating back from the 2nd buffer");
17093 assert_eq!(
17094 active_item.item_id(),
17095 multibuffer_item_id,
17096 "Should navigate back from the 2nd buffer to the multi buffer"
17097 );
17098 assert!(!active_item.is_singleton(cx));
17099 })
17100 .unwrap();
17101
17102 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17103 editor.change_selections(
17104 SelectionEffects::scroll(Autoscroll::Next),
17105 window,
17106 cx,
17107 |s| s.select_ranges(Some(70..70)),
17108 );
17109 editor.open_excerpts(&OpenExcerpts, window, cx);
17110 });
17111 cx.executor().run_until_parked();
17112 workspace
17113 .update(cx, |workspace, window, cx| {
17114 let active_item = workspace
17115 .active_item(cx)
17116 .expect("should have an active item after navigating into the 3rd buffer");
17117 let third_item_id = active_item.item_id();
17118 assert_ne!(
17119 third_item_id, multibuffer_item_id,
17120 "Should navigate into the 3rd buffer and activate it"
17121 );
17122 assert_ne!(third_item_id, first_item_id);
17123 assert_ne!(third_item_id, second_item_id);
17124 assert!(
17125 active_item.is_singleton(cx),
17126 "New active item should be a singleton buffer"
17127 );
17128 assert_eq!(
17129 active_item
17130 .act_as::<Editor>(cx)
17131 .expect("should have navigated into an editor")
17132 .read(cx)
17133 .text(cx),
17134 sample_text_3
17135 );
17136
17137 workspace
17138 .go_back(workspace.active_pane().downgrade(), window, cx)
17139 .detach_and_log_err(cx);
17140 })
17141 .unwrap();
17142 cx.executor().run_until_parked();
17143 workspace
17144 .update(cx, |workspace, _, cx| {
17145 let active_item = workspace
17146 .active_item(cx)
17147 .expect("should have an active item after navigating back from the 3rd buffer");
17148 assert_eq!(
17149 active_item.item_id(),
17150 multibuffer_item_id,
17151 "Should navigate back from the 3rd buffer to the multi buffer"
17152 );
17153 assert!(!active_item.is_singleton(cx));
17154 })
17155 .unwrap();
17156}
17157
17158#[gpui::test]
17159async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17160 init_test(cx, |_| {});
17161
17162 let mut cx = EditorTestContext::new(cx).await;
17163
17164 let diff_base = r#"
17165 use some::mod;
17166
17167 const A: u32 = 42;
17168
17169 fn main() {
17170 println!("hello");
17171
17172 println!("world");
17173 }
17174 "#
17175 .unindent();
17176
17177 cx.set_state(
17178 &r#"
17179 use some::modified;
17180
17181 ˇ
17182 fn main() {
17183 println!("hello there");
17184
17185 println!("around the");
17186 println!("world");
17187 }
17188 "#
17189 .unindent(),
17190 );
17191
17192 cx.set_head_text(&diff_base);
17193 executor.run_until_parked();
17194
17195 cx.update_editor(|editor, window, cx| {
17196 editor.go_to_next_hunk(&GoToHunk, window, cx);
17197 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17198 });
17199 executor.run_until_parked();
17200 cx.assert_state_with_diff(
17201 r#"
17202 use some::modified;
17203
17204
17205 fn main() {
17206 - println!("hello");
17207 + ˇ println!("hello there");
17208
17209 println!("around the");
17210 println!("world");
17211 }
17212 "#
17213 .unindent(),
17214 );
17215
17216 cx.update_editor(|editor, window, cx| {
17217 for _ in 0..2 {
17218 editor.go_to_next_hunk(&GoToHunk, window, cx);
17219 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17220 }
17221 });
17222 executor.run_until_parked();
17223 cx.assert_state_with_diff(
17224 r#"
17225 - use some::mod;
17226 + ˇuse some::modified;
17227
17228
17229 fn main() {
17230 - println!("hello");
17231 + println!("hello there");
17232
17233 + println!("around the");
17234 println!("world");
17235 }
17236 "#
17237 .unindent(),
17238 );
17239
17240 cx.update_editor(|editor, window, cx| {
17241 editor.go_to_next_hunk(&GoToHunk, window, cx);
17242 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17243 });
17244 executor.run_until_parked();
17245 cx.assert_state_with_diff(
17246 r#"
17247 - use some::mod;
17248 + use some::modified;
17249
17250 - const A: u32 = 42;
17251 ˇ
17252 fn main() {
17253 - println!("hello");
17254 + println!("hello there");
17255
17256 + println!("around the");
17257 println!("world");
17258 }
17259 "#
17260 .unindent(),
17261 );
17262
17263 cx.update_editor(|editor, window, cx| {
17264 editor.cancel(&Cancel, window, cx);
17265 });
17266
17267 cx.assert_state_with_diff(
17268 r#"
17269 use some::modified;
17270
17271 ˇ
17272 fn main() {
17273 println!("hello there");
17274
17275 println!("around the");
17276 println!("world");
17277 }
17278 "#
17279 .unindent(),
17280 );
17281}
17282
17283#[gpui::test]
17284async fn test_diff_base_change_with_expanded_diff_hunks(
17285 executor: BackgroundExecutor,
17286 cx: &mut TestAppContext,
17287) {
17288 init_test(cx, |_| {});
17289
17290 let mut cx = EditorTestContext::new(cx).await;
17291
17292 let diff_base = r#"
17293 use some::mod1;
17294 use some::mod2;
17295
17296 const A: u32 = 42;
17297 const B: u32 = 42;
17298 const C: u32 = 42;
17299
17300 fn main() {
17301 println!("hello");
17302
17303 println!("world");
17304 }
17305 "#
17306 .unindent();
17307
17308 cx.set_state(
17309 &r#"
17310 use some::mod2;
17311
17312 const A: u32 = 42;
17313 const C: u32 = 42;
17314
17315 fn main(ˇ) {
17316 //println!("hello");
17317
17318 println!("world");
17319 //
17320 //
17321 }
17322 "#
17323 .unindent(),
17324 );
17325
17326 cx.set_head_text(&diff_base);
17327 executor.run_until_parked();
17328
17329 cx.update_editor(|editor, window, cx| {
17330 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17331 });
17332 executor.run_until_parked();
17333 cx.assert_state_with_diff(
17334 r#"
17335 - use some::mod1;
17336 use some::mod2;
17337
17338 const A: u32 = 42;
17339 - const B: u32 = 42;
17340 const C: u32 = 42;
17341
17342 fn main(ˇ) {
17343 - println!("hello");
17344 + //println!("hello");
17345
17346 println!("world");
17347 + //
17348 + //
17349 }
17350 "#
17351 .unindent(),
17352 );
17353
17354 cx.set_head_text("new diff base!");
17355 executor.run_until_parked();
17356 cx.assert_state_with_diff(
17357 r#"
17358 - new diff base!
17359 + use some::mod2;
17360 +
17361 + const A: u32 = 42;
17362 + const C: u32 = 42;
17363 +
17364 + fn main(ˇ) {
17365 + //println!("hello");
17366 +
17367 + println!("world");
17368 + //
17369 + //
17370 + }
17371 "#
17372 .unindent(),
17373 );
17374}
17375
17376#[gpui::test]
17377async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
17378 init_test(cx, |_| {});
17379
17380 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17381 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17382 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17383 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17384 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
17385 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
17386
17387 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
17388 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
17389 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
17390
17391 let multi_buffer = cx.new(|cx| {
17392 let mut multibuffer = MultiBuffer::new(ReadWrite);
17393 multibuffer.push_excerpts(
17394 buffer_1.clone(),
17395 [
17396 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17397 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17398 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17399 ],
17400 cx,
17401 );
17402 multibuffer.push_excerpts(
17403 buffer_2.clone(),
17404 [
17405 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17406 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17407 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17408 ],
17409 cx,
17410 );
17411 multibuffer.push_excerpts(
17412 buffer_3.clone(),
17413 [
17414 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17415 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17416 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17417 ],
17418 cx,
17419 );
17420 multibuffer
17421 });
17422
17423 let editor =
17424 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17425 editor
17426 .update(cx, |editor, _window, cx| {
17427 for (buffer, diff_base) in [
17428 (buffer_1.clone(), file_1_old),
17429 (buffer_2.clone(), file_2_old),
17430 (buffer_3.clone(), file_3_old),
17431 ] {
17432 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
17433 editor
17434 .buffer
17435 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
17436 }
17437 })
17438 .unwrap();
17439
17440 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17441 cx.run_until_parked();
17442
17443 cx.assert_editor_state(
17444 &"
17445 ˇaaa
17446 ccc
17447 ddd
17448
17449 ggg
17450 hhh
17451
17452
17453 lll
17454 mmm
17455 NNN
17456
17457 qqq
17458 rrr
17459
17460 uuu
17461 111
17462 222
17463 333
17464
17465 666
17466 777
17467
17468 000
17469 !!!"
17470 .unindent(),
17471 );
17472
17473 cx.update_editor(|editor, window, cx| {
17474 editor.select_all(&SelectAll, window, cx);
17475 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17476 });
17477 cx.executor().run_until_parked();
17478
17479 cx.assert_state_with_diff(
17480 "
17481 «aaa
17482 - bbb
17483 ccc
17484 ddd
17485
17486 ggg
17487 hhh
17488
17489
17490 lll
17491 mmm
17492 - nnn
17493 + NNN
17494
17495 qqq
17496 rrr
17497
17498 uuu
17499 111
17500 222
17501 333
17502
17503 + 666
17504 777
17505
17506 000
17507 !!!ˇ»"
17508 .unindent(),
17509 );
17510}
17511
17512#[gpui::test]
17513async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
17514 init_test(cx, |_| {});
17515
17516 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
17517 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
17518
17519 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
17520 let multi_buffer = cx.new(|cx| {
17521 let mut multibuffer = MultiBuffer::new(ReadWrite);
17522 multibuffer.push_excerpts(
17523 buffer.clone(),
17524 [
17525 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
17526 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
17527 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
17528 ],
17529 cx,
17530 );
17531 multibuffer
17532 });
17533
17534 let editor =
17535 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17536 editor
17537 .update(cx, |editor, _window, cx| {
17538 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
17539 editor
17540 .buffer
17541 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
17542 })
17543 .unwrap();
17544
17545 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17546 cx.run_until_parked();
17547
17548 cx.update_editor(|editor, window, cx| {
17549 editor.expand_all_diff_hunks(&Default::default(), window, cx)
17550 });
17551 cx.executor().run_until_parked();
17552
17553 // When the start of a hunk coincides with the start of its excerpt,
17554 // the hunk is expanded. When the start of a a hunk is earlier than
17555 // the start of its excerpt, the hunk is not expanded.
17556 cx.assert_state_with_diff(
17557 "
17558 ˇaaa
17559 - bbb
17560 + BBB
17561
17562 - ddd
17563 - eee
17564 + DDD
17565 + EEE
17566 fff
17567
17568 iii
17569 "
17570 .unindent(),
17571 );
17572}
17573
17574#[gpui::test]
17575async fn test_edits_around_expanded_insertion_hunks(
17576 executor: BackgroundExecutor,
17577 cx: &mut TestAppContext,
17578) {
17579 init_test(cx, |_| {});
17580
17581 let mut cx = EditorTestContext::new(cx).await;
17582
17583 let diff_base = r#"
17584 use some::mod1;
17585 use some::mod2;
17586
17587 const A: u32 = 42;
17588
17589 fn main() {
17590 println!("hello");
17591
17592 println!("world");
17593 }
17594 "#
17595 .unindent();
17596 executor.run_until_parked();
17597 cx.set_state(
17598 &r#"
17599 use some::mod1;
17600 use some::mod2;
17601
17602 const A: u32 = 42;
17603 const B: u32 = 42;
17604 const C: u32 = 42;
17605 ˇ
17606
17607 fn main() {
17608 println!("hello");
17609
17610 println!("world");
17611 }
17612 "#
17613 .unindent(),
17614 );
17615
17616 cx.set_head_text(&diff_base);
17617 executor.run_until_parked();
17618
17619 cx.update_editor(|editor, window, cx| {
17620 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17621 });
17622 executor.run_until_parked();
17623
17624 cx.assert_state_with_diff(
17625 r#"
17626 use some::mod1;
17627 use some::mod2;
17628
17629 const A: u32 = 42;
17630 + const B: u32 = 42;
17631 + const C: u32 = 42;
17632 + ˇ
17633
17634 fn main() {
17635 println!("hello");
17636
17637 println!("world");
17638 }
17639 "#
17640 .unindent(),
17641 );
17642
17643 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
17644 executor.run_until_parked();
17645
17646 cx.assert_state_with_diff(
17647 r#"
17648 use some::mod1;
17649 use some::mod2;
17650
17651 const A: u32 = 42;
17652 + const B: u32 = 42;
17653 + const C: u32 = 42;
17654 + const D: u32 = 42;
17655 + ˇ
17656
17657 fn main() {
17658 println!("hello");
17659
17660 println!("world");
17661 }
17662 "#
17663 .unindent(),
17664 );
17665
17666 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
17667 executor.run_until_parked();
17668
17669 cx.assert_state_with_diff(
17670 r#"
17671 use some::mod1;
17672 use some::mod2;
17673
17674 const A: u32 = 42;
17675 + const B: u32 = 42;
17676 + const C: u32 = 42;
17677 + const D: u32 = 42;
17678 + const E: u32 = 42;
17679 + ˇ
17680
17681 fn main() {
17682 println!("hello");
17683
17684 println!("world");
17685 }
17686 "#
17687 .unindent(),
17688 );
17689
17690 cx.update_editor(|editor, window, cx| {
17691 editor.delete_line(&DeleteLine, window, cx);
17692 });
17693 executor.run_until_parked();
17694
17695 cx.assert_state_with_diff(
17696 r#"
17697 use some::mod1;
17698 use some::mod2;
17699
17700 const A: u32 = 42;
17701 + const B: u32 = 42;
17702 + const C: u32 = 42;
17703 + const D: u32 = 42;
17704 + const E: u32 = 42;
17705 ˇ
17706 fn main() {
17707 println!("hello");
17708
17709 println!("world");
17710 }
17711 "#
17712 .unindent(),
17713 );
17714
17715 cx.update_editor(|editor, window, cx| {
17716 editor.move_up(&MoveUp, window, cx);
17717 editor.delete_line(&DeleteLine, window, cx);
17718 editor.move_up(&MoveUp, window, cx);
17719 editor.delete_line(&DeleteLine, window, cx);
17720 editor.move_up(&MoveUp, window, cx);
17721 editor.delete_line(&DeleteLine, window, cx);
17722 });
17723 executor.run_until_parked();
17724 cx.assert_state_with_diff(
17725 r#"
17726 use some::mod1;
17727 use some::mod2;
17728
17729 const A: u32 = 42;
17730 + const B: u32 = 42;
17731 ˇ
17732 fn main() {
17733 println!("hello");
17734
17735 println!("world");
17736 }
17737 "#
17738 .unindent(),
17739 );
17740
17741 cx.update_editor(|editor, window, cx| {
17742 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
17743 editor.delete_line(&DeleteLine, window, cx);
17744 });
17745 executor.run_until_parked();
17746 cx.assert_state_with_diff(
17747 r#"
17748 ˇ
17749 fn main() {
17750 println!("hello");
17751
17752 println!("world");
17753 }
17754 "#
17755 .unindent(),
17756 );
17757}
17758
17759#[gpui::test]
17760async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
17761 init_test(cx, |_| {});
17762
17763 let mut cx = EditorTestContext::new(cx).await;
17764 cx.set_head_text(indoc! { "
17765 one
17766 two
17767 three
17768 four
17769 five
17770 "
17771 });
17772 cx.set_state(indoc! { "
17773 one
17774 ˇthree
17775 five
17776 "});
17777 cx.run_until_parked();
17778 cx.update_editor(|editor, window, cx| {
17779 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17780 });
17781 cx.assert_state_with_diff(
17782 indoc! { "
17783 one
17784 - two
17785 ˇthree
17786 - four
17787 five
17788 "}
17789 .to_string(),
17790 );
17791 cx.update_editor(|editor, window, cx| {
17792 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17793 });
17794
17795 cx.assert_state_with_diff(
17796 indoc! { "
17797 one
17798 ˇthree
17799 five
17800 "}
17801 .to_string(),
17802 );
17803
17804 cx.set_state(indoc! { "
17805 one
17806 ˇTWO
17807 three
17808 four
17809 five
17810 "});
17811 cx.run_until_parked();
17812 cx.update_editor(|editor, window, cx| {
17813 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17814 });
17815
17816 cx.assert_state_with_diff(
17817 indoc! { "
17818 one
17819 - two
17820 + ˇTWO
17821 three
17822 four
17823 five
17824 "}
17825 .to_string(),
17826 );
17827 cx.update_editor(|editor, window, cx| {
17828 editor.move_up(&Default::default(), window, cx);
17829 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17830 });
17831 cx.assert_state_with_diff(
17832 indoc! { "
17833 one
17834 ˇTWO
17835 three
17836 four
17837 five
17838 "}
17839 .to_string(),
17840 );
17841}
17842
17843#[gpui::test]
17844async fn test_edits_around_expanded_deletion_hunks(
17845 executor: BackgroundExecutor,
17846 cx: &mut TestAppContext,
17847) {
17848 init_test(cx, |_| {});
17849
17850 let mut cx = EditorTestContext::new(cx).await;
17851
17852 let diff_base = r#"
17853 use some::mod1;
17854 use some::mod2;
17855
17856 const A: u32 = 42;
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 executor.run_until_parked();
17869 cx.set_state(
17870 &r#"
17871 use some::mod1;
17872 use some::mod2;
17873
17874 ˇconst B: u32 = 42;
17875 const C: u32 = 42;
17876
17877
17878 fn main() {
17879 println!("hello");
17880
17881 println!("world");
17882 }
17883 "#
17884 .unindent(),
17885 );
17886
17887 cx.set_head_text(&diff_base);
17888 executor.run_until_parked();
17889
17890 cx.update_editor(|editor, window, cx| {
17891 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17892 });
17893 executor.run_until_parked();
17894
17895 cx.assert_state_with_diff(
17896 r#"
17897 use some::mod1;
17898 use some::mod2;
17899
17900 - const A: u32 = 42;
17901 ˇconst B: u32 = 42;
17902 const C: u32 = 42;
17903
17904
17905 fn main() {
17906 println!("hello");
17907
17908 println!("world");
17909 }
17910 "#
17911 .unindent(),
17912 );
17913
17914 cx.update_editor(|editor, window, cx| {
17915 editor.delete_line(&DeleteLine, window, cx);
17916 });
17917 executor.run_until_parked();
17918 cx.assert_state_with_diff(
17919 r#"
17920 use some::mod1;
17921 use some::mod2;
17922
17923 - const A: u32 = 42;
17924 - const B: u32 = 42;
17925 ˇconst C: u32 = 42;
17926
17927
17928 fn main() {
17929 println!("hello");
17930
17931 println!("world");
17932 }
17933 "#
17934 .unindent(),
17935 );
17936
17937 cx.update_editor(|editor, window, cx| {
17938 editor.delete_line(&DeleteLine, window, cx);
17939 });
17940 executor.run_until_parked();
17941 cx.assert_state_with_diff(
17942 r#"
17943 use some::mod1;
17944 use some::mod2;
17945
17946 - const A: u32 = 42;
17947 - const B: u32 = 42;
17948 - const C: u32 = 42;
17949 ˇ
17950
17951 fn main() {
17952 println!("hello");
17953
17954 println!("world");
17955 }
17956 "#
17957 .unindent(),
17958 );
17959
17960 cx.update_editor(|editor, window, cx| {
17961 editor.handle_input("replacement", window, cx);
17962 });
17963 executor.run_until_parked();
17964 cx.assert_state_with_diff(
17965 r#"
17966 use some::mod1;
17967 use some::mod2;
17968
17969 - const A: u32 = 42;
17970 - const B: u32 = 42;
17971 - const C: u32 = 42;
17972 -
17973 + replacementˇ
17974
17975 fn main() {
17976 println!("hello");
17977
17978 println!("world");
17979 }
17980 "#
17981 .unindent(),
17982 );
17983}
17984
17985#[gpui::test]
17986async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17987 init_test(cx, |_| {});
17988
17989 let mut cx = EditorTestContext::new(cx).await;
17990
17991 let base_text = r#"
17992 one
17993 two
17994 three
17995 four
17996 five
17997 "#
17998 .unindent();
17999 executor.run_until_parked();
18000 cx.set_state(
18001 &r#"
18002 one
18003 two
18004 fˇour
18005 five
18006 "#
18007 .unindent(),
18008 );
18009
18010 cx.set_head_text(&base_text);
18011 executor.run_until_parked();
18012
18013 cx.update_editor(|editor, window, cx| {
18014 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18015 });
18016 executor.run_until_parked();
18017
18018 cx.assert_state_with_diff(
18019 r#"
18020 one
18021 two
18022 - three
18023 fˇour
18024 five
18025 "#
18026 .unindent(),
18027 );
18028
18029 cx.update_editor(|editor, window, cx| {
18030 editor.backspace(&Backspace, window, cx);
18031 editor.backspace(&Backspace, window, cx);
18032 });
18033 executor.run_until_parked();
18034 cx.assert_state_with_diff(
18035 r#"
18036 one
18037 two
18038 - threeˇ
18039 - four
18040 + our
18041 five
18042 "#
18043 .unindent(),
18044 );
18045}
18046
18047#[gpui::test]
18048async fn test_edit_after_expanded_modification_hunk(
18049 executor: BackgroundExecutor,
18050 cx: &mut TestAppContext,
18051) {
18052 init_test(cx, |_| {});
18053
18054 let mut cx = EditorTestContext::new(cx).await;
18055
18056 let diff_base = r#"
18057 use some::mod1;
18058 use some::mod2;
18059
18060 const A: u32 = 42;
18061 const B: u32 = 42;
18062 const C: u32 = 42;
18063 const D: u32 = 42;
18064
18065
18066 fn main() {
18067 println!("hello");
18068
18069 println!("world");
18070 }"#
18071 .unindent();
18072
18073 cx.set_state(
18074 &r#"
18075 use some::mod1;
18076 use some::mod2;
18077
18078 const A: u32 = 42;
18079 const B: u32 = 42;
18080 const C: u32 = 43ˇ
18081 const D: u32 = 42;
18082
18083
18084 fn main() {
18085 println!("hello");
18086
18087 println!("world");
18088 }"#
18089 .unindent(),
18090 );
18091
18092 cx.set_head_text(&diff_base);
18093 executor.run_until_parked();
18094 cx.update_editor(|editor, window, cx| {
18095 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18096 });
18097 executor.run_until_parked();
18098
18099 cx.assert_state_with_diff(
18100 r#"
18101 use some::mod1;
18102 use some::mod2;
18103
18104 const A: u32 = 42;
18105 const B: u32 = 42;
18106 - const C: u32 = 42;
18107 + const C: u32 = 43ˇ
18108 const D: u32 = 42;
18109
18110
18111 fn main() {
18112 println!("hello");
18113
18114 println!("world");
18115 }"#
18116 .unindent(),
18117 );
18118
18119 cx.update_editor(|editor, window, cx| {
18120 editor.handle_input("\nnew_line\n", window, cx);
18121 });
18122 executor.run_until_parked();
18123
18124 cx.assert_state_with_diff(
18125 r#"
18126 use some::mod1;
18127 use some::mod2;
18128
18129 const A: u32 = 42;
18130 const B: u32 = 42;
18131 - const C: u32 = 42;
18132 + const C: u32 = 43
18133 + new_line
18134 + ˇ
18135 const D: u32 = 42;
18136
18137
18138 fn main() {
18139 println!("hello");
18140
18141 println!("world");
18142 }"#
18143 .unindent(),
18144 );
18145}
18146
18147#[gpui::test]
18148async fn test_stage_and_unstage_added_file_hunk(
18149 executor: BackgroundExecutor,
18150 cx: &mut TestAppContext,
18151) {
18152 init_test(cx, |_| {});
18153
18154 let mut cx = EditorTestContext::new(cx).await;
18155 cx.update_editor(|editor, _, cx| {
18156 editor.set_expand_all_diff_hunks(cx);
18157 });
18158
18159 let working_copy = r#"
18160 ˇfn main() {
18161 println!("hello, world!");
18162 }
18163 "#
18164 .unindent();
18165
18166 cx.set_state(&working_copy);
18167 executor.run_until_parked();
18168
18169 cx.assert_state_with_diff(
18170 r#"
18171 + ˇfn main() {
18172 + println!("hello, world!");
18173 + }
18174 "#
18175 .unindent(),
18176 );
18177 cx.assert_index_text(None);
18178
18179 cx.update_editor(|editor, window, cx| {
18180 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18181 });
18182 executor.run_until_parked();
18183 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
18184 cx.assert_state_with_diff(
18185 r#"
18186 + ˇfn main() {
18187 + println!("hello, world!");
18188 + }
18189 "#
18190 .unindent(),
18191 );
18192
18193 cx.update_editor(|editor, window, cx| {
18194 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18195 });
18196 executor.run_until_parked();
18197 cx.assert_index_text(None);
18198}
18199
18200async fn setup_indent_guides_editor(
18201 text: &str,
18202 cx: &mut TestAppContext,
18203) -> (BufferId, EditorTestContext) {
18204 init_test(cx, |_| {});
18205
18206 let mut cx = EditorTestContext::new(cx).await;
18207
18208 let buffer_id = cx.update_editor(|editor, window, cx| {
18209 editor.set_text(text, window, cx);
18210 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
18211
18212 buffer_ids[0]
18213 });
18214
18215 (buffer_id, cx)
18216}
18217
18218fn assert_indent_guides(
18219 range: Range<u32>,
18220 expected: Vec<IndentGuide>,
18221 active_indices: Option<Vec<usize>>,
18222 cx: &mut EditorTestContext,
18223) {
18224 let indent_guides = cx.update_editor(|editor, window, cx| {
18225 let snapshot = editor.snapshot(window, cx).display_snapshot;
18226 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
18227 editor,
18228 MultiBufferRow(range.start)..MultiBufferRow(range.end),
18229 true,
18230 &snapshot,
18231 cx,
18232 );
18233
18234 indent_guides.sort_by(|a, b| {
18235 a.depth.cmp(&b.depth).then(
18236 a.start_row
18237 .cmp(&b.start_row)
18238 .then(a.end_row.cmp(&b.end_row)),
18239 )
18240 });
18241 indent_guides
18242 });
18243
18244 if let Some(expected) = active_indices {
18245 let active_indices = cx.update_editor(|editor, window, cx| {
18246 let snapshot = editor.snapshot(window, cx).display_snapshot;
18247 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
18248 });
18249
18250 assert_eq!(
18251 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
18252 expected,
18253 "Active indent guide indices do not match"
18254 );
18255 }
18256
18257 assert_eq!(indent_guides, expected, "Indent guides do not match");
18258}
18259
18260fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
18261 IndentGuide {
18262 buffer_id,
18263 start_row: MultiBufferRow(start_row),
18264 end_row: MultiBufferRow(end_row),
18265 depth,
18266 tab_size: 4,
18267 settings: IndentGuideSettings {
18268 enabled: true,
18269 line_width: 1,
18270 active_line_width: 1,
18271 ..Default::default()
18272 },
18273 }
18274}
18275
18276#[gpui::test]
18277async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
18278 let (buffer_id, mut cx) = setup_indent_guides_editor(
18279 &"
18280 fn main() {
18281 let a = 1;
18282 }"
18283 .unindent(),
18284 cx,
18285 )
18286 .await;
18287
18288 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18289}
18290
18291#[gpui::test]
18292async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
18293 let (buffer_id, mut cx) = setup_indent_guides_editor(
18294 &"
18295 fn main() {
18296 let a = 1;
18297 let b = 2;
18298 }"
18299 .unindent(),
18300 cx,
18301 )
18302 .await;
18303
18304 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
18305}
18306
18307#[gpui::test]
18308async fn test_indent_guide_nested(cx: &mut TestAppContext) {
18309 let (buffer_id, mut cx) = setup_indent_guides_editor(
18310 &"
18311 fn main() {
18312 let a = 1;
18313 if a == 3 {
18314 let b = 2;
18315 } else {
18316 let c = 3;
18317 }
18318 }"
18319 .unindent(),
18320 cx,
18321 )
18322 .await;
18323
18324 assert_indent_guides(
18325 0..8,
18326 vec![
18327 indent_guide(buffer_id, 1, 6, 0),
18328 indent_guide(buffer_id, 3, 3, 1),
18329 indent_guide(buffer_id, 5, 5, 1),
18330 ],
18331 None,
18332 &mut cx,
18333 );
18334}
18335
18336#[gpui::test]
18337async fn test_indent_guide_tab(cx: &mut TestAppContext) {
18338 let (buffer_id, mut cx) = setup_indent_guides_editor(
18339 &"
18340 fn main() {
18341 let a = 1;
18342 let b = 2;
18343 let c = 3;
18344 }"
18345 .unindent(),
18346 cx,
18347 )
18348 .await;
18349
18350 assert_indent_guides(
18351 0..5,
18352 vec![
18353 indent_guide(buffer_id, 1, 3, 0),
18354 indent_guide(buffer_id, 2, 2, 1),
18355 ],
18356 None,
18357 &mut cx,
18358 );
18359}
18360
18361#[gpui::test]
18362async fn test_indent_guide_continues_on_empty_line(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 .unindent(),
18371 cx,
18372 )
18373 .await;
18374
18375 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
18376}
18377
18378#[gpui::test]
18379async fn test_indent_guide_complex(cx: &mut TestAppContext) {
18380 let (buffer_id, mut cx) = setup_indent_guides_editor(
18381 &"
18382 fn main() {
18383 let a = 1;
18384
18385 let c = 3;
18386
18387 if a == 3 {
18388 let b = 2;
18389 } else {
18390 let c = 3;
18391 }
18392 }"
18393 .unindent(),
18394 cx,
18395 )
18396 .await;
18397
18398 assert_indent_guides(
18399 0..11,
18400 vec![
18401 indent_guide(buffer_id, 1, 9, 0),
18402 indent_guide(buffer_id, 6, 6, 1),
18403 indent_guide(buffer_id, 8, 8, 1),
18404 ],
18405 None,
18406 &mut cx,
18407 );
18408}
18409
18410#[gpui::test]
18411async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
18412 let (buffer_id, mut cx) = setup_indent_guides_editor(
18413 &"
18414 fn main() {
18415 let a = 1;
18416
18417 let c = 3;
18418
18419 if a == 3 {
18420 let b = 2;
18421 } else {
18422 let c = 3;
18423 }
18424 }"
18425 .unindent(),
18426 cx,
18427 )
18428 .await;
18429
18430 assert_indent_guides(
18431 1..11,
18432 vec![
18433 indent_guide(buffer_id, 1, 9, 0),
18434 indent_guide(buffer_id, 6, 6, 1),
18435 indent_guide(buffer_id, 8, 8, 1),
18436 ],
18437 None,
18438 &mut cx,
18439 );
18440}
18441
18442#[gpui::test]
18443async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
18444 let (buffer_id, mut cx) = setup_indent_guides_editor(
18445 &"
18446 fn main() {
18447 let a = 1;
18448
18449 let c = 3;
18450
18451 if a == 3 {
18452 let b = 2;
18453 } else {
18454 let c = 3;
18455 }
18456 }"
18457 .unindent(),
18458 cx,
18459 )
18460 .await;
18461
18462 assert_indent_guides(
18463 1..10,
18464 vec![
18465 indent_guide(buffer_id, 1, 9, 0),
18466 indent_guide(buffer_id, 6, 6, 1),
18467 indent_guide(buffer_id, 8, 8, 1),
18468 ],
18469 None,
18470 &mut cx,
18471 );
18472}
18473
18474#[gpui::test]
18475async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
18476 let (buffer_id, mut cx) = setup_indent_guides_editor(
18477 &"
18478 fn main() {
18479 if a {
18480 b(
18481 c,
18482 d,
18483 )
18484 } else {
18485 e(
18486 f
18487 )
18488 }
18489 }"
18490 .unindent(),
18491 cx,
18492 )
18493 .await;
18494
18495 assert_indent_guides(
18496 0..11,
18497 vec![
18498 indent_guide(buffer_id, 1, 10, 0),
18499 indent_guide(buffer_id, 2, 5, 1),
18500 indent_guide(buffer_id, 7, 9, 1),
18501 indent_guide(buffer_id, 3, 4, 2),
18502 indent_guide(buffer_id, 8, 8, 2),
18503 ],
18504 None,
18505 &mut cx,
18506 );
18507
18508 cx.update_editor(|editor, window, cx| {
18509 editor.fold_at(MultiBufferRow(2), window, cx);
18510 assert_eq!(
18511 editor.display_text(cx),
18512 "
18513 fn main() {
18514 if a {
18515 b(⋯
18516 )
18517 } else {
18518 e(
18519 f
18520 )
18521 }
18522 }"
18523 .unindent()
18524 );
18525 });
18526
18527 assert_indent_guides(
18528 0..11,
18529 vec![
18530 indent_guide(buffer_id, 1, 10, 0),
18531 indent_guide(buffer_id, 2, 5, 1),
18532 indent_guide(buffer_id, 7, 9, 1),
18533 indent_guide(buffer_id, 8, 8, 2),
18534 ],
18535 None,
18536 &mut cx,
18537 );
18538}
18539
18540#[gpui::test]
18541async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
18542 let (buffer_id, mut cx) = setup_indent_guides_editor(
18543 &"
18544 block1
18545 block2
18546 block3
18547 block4
18548 block2
18549 block1
18550 block1"
18551 .unindent(),
18552 cx,
18553 )
18554 .await;
18555
18556 assert_indent_guides(
18557 1..10,
18558 vec![
18559 indent_guide(buffer_id, 1, 4, 0),
18560 indent_guide(buffer_id, 2, 3, 1),
18561 indent_guide(buffer_id, 3, 3, 2),
18562 ],
18563 None,
18564 &mut cx,
18565 );
18566}
18567
18568#[gpui::test]
18569async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
18570 let (buffer_id, mut cx) = setup_indent_guides_editor(
18571 &"
18572 block1
18573 block2
18574 block3
18575
18576 block1
18577 block1"
18578 .unindent(),
18579 cx,
18580 )
18581 .await;
18582
18583 assert_indent_guides(
18584 0..6,
18585 vec![
18586 indent_guide(buffer_id, 1, 2, 0),
18587 indent_guide(buffer_id, 2, 2, 1),
18588 ],
18589 None,
18590 &mut cx,
18591 );
18592}
18593
18594#[gpui::test]
18595async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
18596 let (buffer_id, mut cx) = setup_indent_guides_editor(
18597 &"
18598 function component() {
18599 \treturn (
18600 \t\t\t
18601 \t\t<div>
18602 \t\t\t<abc></abc>
18603 \t\t</div>
18604 \t)
18605 }"
18606 .unindent(),
18607 cx,
18608 )
18609 .await;
18610
18611 assert_indent_guides(
18612 0..8,
18613 vec![
18614 indent_guide(buffer_id, 1, 6, 0),
18615 indent_guide(buffer_id, 2, 5, 1),
18616 indent_guide(buffer_id, 4, 4, 2),
18617 ],
18618 None,
18619 &mut cx,
18620 );
18621}
18622
18623#[gpui::test]
18624async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
18625 let (buffer_id, mut cx) = setup_indent_guides_editor(
18626 &"
18627 function component() {
18628 \treturn (
18629 \t
18630 \t\t<div>
18631 \t\t\t<abc></abc>
18632 \t\t</div>
18633 \t)
18634 }"
18635 .unindent(),
18636 cx,
18637 )
18638 .await;
18639
18640 assert_indent_guides(
18641 0..8,
18642 vec![
18643 indent_guide(buffer_id, 1, 6, 0),
18644 indent_guide(buffer_id, 2, 5, 1),
18645 indent_guide(buffer_id, 4, 4, 2),
18646 ],
18647 None,
18648 &mut cx,
18649 );
18650}
18651
18652#[gpui::test]
18653async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
18654 let (buffer_id, mut cx) = setup_indent_guides_editor(
18655 &"
18656 block1
18657
18658
18659
18660 block2
18661 "
18662 .unindent(),
18663 cx,
18664 )
18665 .await;
18666
18667 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18668}
18669
18670#[gpui::test]
18671async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
18672 let (buffer_id, mut cx) = setup_indent_guides_editor(
18673 &"
18674 def a:
18675 \tb = 3
18676 \tif True:
18677 \t\tc = 4
18678 \t\td = 5
18679 \tprint(b)
18680 "
18681 .unindent(),
18682 cx,
18683 )
18684 .await;
18685
18686 assert_indent_guides(
18687 0..6,
18688 vec![
18689 indent_guide(buffer_id, 1, 5, 0),
18690 indent_guide(buffer_id, 3, 4, 1),
18691 ],
18692 None,
18693 &mut cx,
18694 );
18695}
18696
18697#[gpui::test]
18698async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
18699 let (buffer_id, mut cx) = setup_indent_guides_editor(
18700 &"
18701 fn main() {
18702 let a = 1;
18703 }"
18704 .unindent(),
18705 cx,
18706 )
18707 .await;
18708
18709 cx.update_editor(|editor, window, cx| {
18710 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18711 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18712 });
18713 });
18714
18715 assert_indent_guides(
18716 0..3,
18717 vec![indent_guide(buffer_id, 1, 1, 0)],
18718 Some(vec![0]),
18719 &mut cx,
18720 );
18721}
18722
18723#[gpui::test]
18724async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
18725 let (buffer_id, mut cx) = setup_indent_guides_editor(
18726 &"
18727 fn main() {
18728 if 1 == 2 {
18729 let a = 1;
18730 }
18731 }"
18732 .unindent(),
18733 cx,
18734 )
18735 .await;
18736
18737 cx.update_editor(|editor, window, cx| {
18738 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18739 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18740 });
18741 });
18742
18743 assert_indent_guides(
18744 0..4,
18745 vec![
18746 indent_guide(buffer_id, 1, 3, 0),
18747 indent_guide(buffer_id, 2, 2, 1),
18748 ],
18749 Some(vec![1]),
18750 &mut cx,
18751 );
18752
18753 cx.update_editor(|editor, window, cx| {
18754 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18755 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18756 });
18757 });
18758
18759 assert_indent_guides(
18760 0..4,
18761 vec![
18762 indent_guide(buffer_id, 1, 3, 0),
18763 indent_guide(buffer_id, 2, 2, 1),
18764 ],
18765 Some(vec![1]),
18766 &mut cx,
18767 );
18768
18769 cx.update_editor(|editor, window, cx| {
18770 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18771 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
18772 });
18773 });
18774
18775 assert_indent_guides(
18776 0..4,
18777 vec![
18778 indent_guide(buffer_id, 1, 3, 0),
18779 indent_guide(buffer_id, 2, 2, 1),
18780 ],
18781 Some(vec![0]),
18782 &mut cx,
18783 );
18784}
18785
18786#[gpui::test]
18787async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
18788 let (buffer_id, mut cx) = setup_indent_guides_editor(
18789 &"
18790 fn main() {
18791 let a = 1;
18792
18793 let b = 2;
18794 }"
18795 .unindent(),
18796 cx,
18797 )
18798 .await;
18799
18800 cx.update_editor(|editor, window, cx| {
18801 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18802 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18803 });
18804 });
18805
18806 assert_indent_guides(
18807 0..5,
18808 vec![indent_guide(buffer_id, 1, 3, 0)],
18809 Some(vec![0]),
18810 &mut cx,
18811 );
18812}
18813
18814#[gpui::test]
18815async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
18816 let (buffer_id, mut cx) = setup_indent_guides_editor(
18817 &"
18818 def m:
18819 a = 1
18820 pass"
18821 .unindent(),
18822 cx,
18823 )
18824 .await;
18825
18826 cx.update_editor(|editor, window, cx| {
18827 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18828 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18829 });
18830 });
18831
18832 assert_indent_guides(
18833 0..3,
18834 vec![indent_guide(buffer_id, 1, 2, 0)],
18835 Some(vec![0]),
18836 &mut cx,
18837 );
18838}
18839
18840#[gpui::test]
18841async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
18842 init_test(cx, |_| {});
18843 let mut cx = EditorTestContext::new(cx).await;
18844 let text = indoc! {
18845 "
18846 impl A {
18847 fn b() {
18848 0;
18849 3;
18850 5;
18851 6;
18852 7;
18853 }
18854 }
18855 "
18856 };
18857 let base_text = indoc! {
18858 "
18859 impl A {
18860 fn b() {
18861 0;
18862 1;
18863 2;
18864 3;
18865 4;
18866 }
18867 fn c() {
18868 5;
18869 6;
18870 7;
18871 }
18872 }
18873 "
18874 };
18875
18876 cx.update_editor(|editor, window, cx| {
18877 editor.set_text(text, window, cx);
18878
18879 editor.buffer().update(cx, |multibuffer, cx| {
18880 let buffer = multibuffer.as_singleton().unwrap();
18881 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
18882
18883 multibuffer.set_all_diff_hunks_expanded(cx);
18884 multibuffer.add_diff(diff, cx);
18885
18886 buffer.read(cx).remote_id()
18887 })
18888 });
18889 cx.run_until_parked();
18890
18891 cx.assert_state_with_diff(
18892 indoc! { "
18893 impl A {
18894 fn b() {
18895 0;
18896 - 1;
18897 - 2;
18898 3;
18899 - 4;
18900 - }
18901 - fn c() {
18902 5;
18903 6;
18904 7;
18905 }
18906 }
18907 ˇ"
18908 }
18909 .to_string(),
18910 );
18911
18912 let mut actual_guides = cx.update_editor(|editor, window, cx| {
18913 editor
18914 .snapshot(window, cx)
18915 .buffer_snapshot
18916 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
18917 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
18918 .collect::<Vec<_>>()
18919 });
18920 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
18921 assert_eq!(
18922 actual_guides,
18923 vec![
18924 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
18925 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
18926 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
18927 ]
18928 );
18929}
18930
18931#[gpui::test]
18932async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18933 init_test(cx, |_| {});
18934 let mut cx = EditorTestContext::new(cx).await;
18935
18936 let diff_base = r#"
18937 a
18938 b
18939 c
18940 "#
18941 .unindent();
18942
18943 cx.set_state(
18944 &r#"
18945 ˇA
18946 b
18947 C
18948 "#
18949 .unindent(),
18950 );
18951 cx.set_head_text(&diff_base);
18952 cx.update_editor(|editor, window, cx| {
18953 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18954 });
18955 executor.run_until_parked();
18956
18957 let both_hunks_expanded = r#"
18958 - a
18959 + ˇA
18960 b
18961 - c
18962 + C
18963 "#
18964 .unindent();
18965
18966 cx.assert_state_with_diff(both_hunks_expanded.clone());
18967
18968 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18969 let snapshot = editor.snapshot(window, cx);
18970 let hunks = editor
18971 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18972 .collect::<Vec<_>>();
18973 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18974 let buffer_id = hunks[0].buffer_id;
18975 hunks
18976 .into_iter()
18977 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18978 .collect::<Vec<_>>()
18979 });
18980 assert_eq!(hunk_ranges.len(), 2);
18981
18982 cx.update_editor(|editor, _, cx| {
18983 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18984 });
18985 executor.run_until_parked();
18986
18987 let second_hunk_expanded = r#"
18988 ˇA
18989 b
18990 - c
18991 + C
18992 "#
18993 .unindent();
18994
18995 cx.assert_state_with_diff(second_hunk_expanded);
18996
18997 cx.update_editor(|editor, _, cx| {
18998 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18999 });
19000 executor.run_until_parked();
19001
19002 cx.assert_state_with_diff(both_hunks_expanded.clone());
19003
19004 cx.update_editor(|editor, _, cx| {
19005 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19006 });
19007 executor.run_until_parked();
19008
19009 let first_hunk_expanded = r#"
19010 - a
19011 + ˇA
19012 b
19013 C
19014 "#
19015 .unindent();
19016
19017 cx.assert_state_with_diff(first_hunk_expanded);
19018
19019 cx.update_editor(|editor, _, cx| {
19020 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19021 });
19022 executor.run_until_parked();
19023
19024 cx.assert_state_with_diff(both_hunks_expanded);
19025
19026 cx.set_state(
19027 &r#"
19028 ˇA
19029 b
19030 "#
19031 .unindent(),
19032 );
19033 cx.run_until_parked();
19034
19035 // TODO this cursor position seems bad
19036 cx.assert_state_with_diff(
19037 r#"
19038 - ˇa
19039 + A
19040 b
19041 "#
19042 .unindent(),
19043 );
19044
19045 cx.update_editor(|editor, window, cx| {
19046 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19047 });
19048
19049 cx.assert_state_with_diff(
19050 r#"
19051 - ˇa
19052 + A
19053 b
19054 - c
19055 "#
19056 .unindent(),
19057 );
19058
19059 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19060 let snapshot = editor.snapshot(window, cx);
19061 let hunks = editor
19062 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19063 .collect::<Vec<_>>();
19064 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19065 let buffer_id = hunks[0].buffer_id;
19066 hunks
19067 .into_iter()
19068 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19069 .collect::<Vec<_>>()
19070 });
19071 assert_eq!(hunk_ranges.len(), 2);
19072
19073 cx.update_editor(|editor, _, cx| {
19074 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19075 });
19076 executor.run_until_parked();
19077
19078 cx.assert_state_with_diff(
19079 r#"
19080 - ˇa
19081 + A
19082 b
19083 "#
19084 .unindent(),
19085 );
19086}
19087
19088#[gpui::test]
19089async fn test_toggle_deletion_hunk_at_start_of_file(
19090 executor: BackgroundExecutor,
19091 cx: &mut TestAppContext,
19092) {
19093 init_test(cx, |_| {});
19094 let mut cx = EditorTestContext::new(cx).await;
19095
19096 let diff_base = r#"
19097 a
19098 b
19099 c
19100 "#
19101 .unindent();
19102
19103 cx.set_state(
19104 &r#"
19105 ˇb
19106 c
19107 "#
19108 .unindent(),
19109 );
19110 cx.set_head_text(&diff_base);
19111 cx.update_editor(|editor, window, cx| {
19112 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19113 });
19114 executor.run_until_parked();
19115
19116 let hunk_expanded = r#"
19117 - a
19118 ˇb
19119 c
19120 "#
19121 .unindent();
19122
19123 cx.assert_state_with_diff(hunk_expanded.clone());
19124
19125 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19126 let snapshot = editor.snapshot(window, cx);
19127 let hunks = editor
19128 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19129 .collect::<Vec<_>>();
19130 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19131 let buffer_id = hunks[0].buffer_id;
19132 hunks
19133 .into_iter()
19134 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19135 .collect::<Vec<_>>()
19136 });
19137 assert_eq!(hunk_ranges.len(), 1);
19138
19139 cx.update_editor(|editor, _, cx| {
19140 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19141 });
19142 executor.run_until_parked();
19143
19144 let hunk_collapsed = r#"
19145 ˇb
19146 c
19147 "#
19148 .unindent();
19149
19150 cx.assert_state_with_diff(hunk_collapsed);
19151
19152 cx.update_editor(|editor, _, cx| {
19153 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19154 });
19155 executor.run_until_parked();
19156
19157 cx.assert_state_with_diff(hunk_expanded.clone());
19158}
19159
19160#[gpui::test]
19161async fn test_display_diff_hunks(cx: &mut TestAppContext) {
19162 init_test(cx, |_| {});
19163
19164 let fs = FakeFs::new(cx.executor());
19165 fs.insert_tree(
19166 path!("/test"),
19167 json!({
19168 ".git": {},
19169 "file-1": "ONE\n",
19170 "file-2": "TWO\n",
19171 "file-3": "THREE\n",
19172 }),
19173 )
19174 .await;
19175
19176 fs.set_head_for_repo(
19177 path!("/test/.git").as_ref(),
19178 &[
19179 ("file-1".into(), "one\n".into()),
19180 ("file-2".into(), "two\n".into()),
19181 ("file-3".into(), "three\n".into()),
19182 ],
19183 "deadbeef",
19184 );
19185
19186 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
19187 let mut buffers = vec![];
19188 for i in 1..=3 {
19189 let buffer = project
19190 .update(cx, |project, cx| {
19191 let path = format!(path!("/test/file-{}"), i);
19192 project.open_local_buffer(path, cx)
19193 })
19194 .await
19195 .unwrap();
19196 buffers.push(buffer);
19197 }
19198
19199 let multibuffer = cx.new(|cx| {
19200 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
19201 multibuffer.set_all_diff_hunks_expanded(cx);
19202 for buffer in &buffers {
19203 let snapshot = buffer.read(cx).snapshot();
19204 multibuffer.set_excerpts_for_path(
19205 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
19206 buffer.clone(),
19207 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
19208 DEFAULT_MULTIBUFFER_CONTEXT,
19209 cx,
19210 );
19211 }
19212 multibuffer
19213 });
19214
19215 let editor = cx.add_window(|window, cx| {
19216 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
19217 });
19218 cx.run_until_parked();
19219
19220 let snapshot = editor
19221 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19222 .unwrap();
19223 let hunks = snapshot
19224 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
19225 .map(|hunk| match hunk {
19226 DisplayDiffHunk::Unfolded {
19227 display_row_range, ..
19228 } => display_row_range,
19229 DisplayDiffHunk::Folded { .. } => unreachable!(),
19230 })
19231 .collect::<Vec<_>>();
19232 assert_eq!(
19233 hunks,
19234 [
19235 DisplayRow(2)..DisplayRow(4),
19236 DisplayRow(7)..DisplayRow(9),
19237 DisplayRow(12)..DisplayRow(14),
19238 ]
19239 );
19240}
19241
19242#[gpui::test]
19243async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
19244 init_test(cx, |_| {});
19245
19246 let mut cx = EditorTestContext::new(cx).await;
19247 cx.set_head_text(indoc! { "
19248 one
19249 two
19250 three
19251 four
19252 five
19253 "
19254 });
19255 cx.set_index_text(indoc! { "
19256 one
19257 two
19258 three
19259 four
19260 five
19261 "
19262 });
19263 cx.set_state(indoc! {"
19264 one
19265 TWO
19266 ˇTHREE
19267 FOUR
19268 five
19269 "});
19270 cx.run_until_parked();
19271 cx.update_editor(|editor, window, cx| {
19272 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19273 });
19274 cx.run_until_parked();
19275 cx.assert_index_text(Some(indoc! {"
19276 one
19277 TWO
19278 THREE
19279 FOUR
19280 five
19281 "}));
19282 cx.set_state(indoc! { "
19283 one
19284 TWO
19285 ˇTHREE-HUNDRED
19286 FOUR
19287 five
19288 "});
19289 cx.run_until_parked();
19290 cx.update_editor(|editor, window, cx| {
19291 let snapshot = editor.snapshot(window, cx);
19292 let hunks = editor
19293 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19294 .collect::<Vec<_>>();
19295 assert_eq!(hunks.len(), 1);
19296 assert_eq!(
19297 hunks[0].status(),
19298 DiffHunkStatus {
19299 kind: DiffHunkStatusKind::Modified,
19300 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
19301 }
19302 );
19303
19304 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19305 });
19306 cx.run_until_parked();
19307 cx.assert_index_text(Some(indoc! {"
19308 one
19309 TWO
19310 THREE-HUNDRED
19311 FOUR
19312 five
19313 "}));
19314}
19315
19316#[gpui::test]
19317fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
19318 init_test(cx, |_| {});
19319
19320 let editor = cx.add_window(|window, cx| {
19321 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
19322 build_editor(buffer, window, cx)
19323 });
19324
19325 let render_args = Arc::new(Mutex::new(None));
19326 let snapshot = editor
19327 .update(cx, |editor, window, cx| {
19328 let snapshot = editor.buffer().read(cx).snapshot(cx);
19329 let range =
19330 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
19331
19332 struct RenderArgs {
19333 row: MultiBufferRow,
19334 folded: bool,
19335 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
19336 }
19337
19338 let crease = Crease::inline(
19339 range,
19340 FoldPlaceholder::test(),
19341 {
19342 let toggle_callback = render_args.clone();
19343 move |row, folded, callback, _window, _cx| {
19344 *toggle_callback.lock() = Some(RenderArgs {
19345 row,
19346 folded,
19347 callback,
19348 });
19349 div()
19350 }
19351 },
19352 |_row, _folded, _window, _cx| div(),
19353 );
19354
19355 editor.insert_creases(Some(crease), cx);
19356 let snapshot = editor.snapshot(window, cx);
19357 let _div = snapshot.render_crease_toggle(
19358 MultiBufferRow(1),
19359 false,
19360 cx.entity().clone(),
19361 window,
19362 cx,
19363 );
19364 snapshot
19365 })
19366 .unwrap();
19367
19368 let render_args = render_args.lock().take().unwrap();
19369 assert_eq!(render_args.row, MultiBufferRow(1));
19370 assert!(!render_args.folded);
19371 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19372
19373 cx.update_window(*editor, |_, window, cx| {
19374 (render_args.callback)(true, window, cx)
19375 })
19376 .unwrap();
19377 let snapshot = editor
19378 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19379 .unwrap();
19380 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
19381
19382 cx.update_window(*editor, |_, window, cx| {
19383 (render_args.callback)(false, window, cx)
19384 })
19385 .unwrap();
19386 let snapshot = editor
19387 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19388 .unwrap();
19389 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19390}
19391
19392#[gpui::test]
19393async fn test_input_text(cx: &mut TestAppContext) {
19394 init_test(cx, |_| {});
19395 let mut cx = EditorTestContext::new(cx).await;
19396
19397 cx.set_state(
19398 &r#"ˇone
19399 two
19400
19401 three
19402 fourˇ
19403 five
19404
19405 siˇx"#
19406 .unindent(),
19407 );
19408
19409 cx.dispatch_action(HandleInput(String::new()));
19410 cx.assert_editor_state(
19411 &r#"ˇone
19412 two
19413
19414 three
19415 fourˇ
19416 five
19417
19418 siˇx"#
19419 .unindent(),
19420 );
19421
19422 cx.dispatch_action(HandleInput("AAAA".to_string()));
19423 cx.assert_editor_state(
19424 &r#"AAAAˇone
19425 two
19426
19427 three
19428 fourAAAAˇ
19429 five
19430
19431 siAAAAˇx"#
19432 .unindent(),
19433 );
19434}
19435
19436#[gpui::test]
19437async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
19438 init_test(cx, |_| {});
19439
19440 let mut cx = EditorTestContext::new(cx).await;
19441 cx.set_state(
19442 r#"let foo = 1;
19443let foo = 2;
19444let foo = 3;
19445let fooˇ = 4;
19446let foo = 5;
19447let foo = 6;
19448let foo = 7;
19449let foo = 8;
19450let foo = 9;
19451let foo = 10;
19452let foo = 11;
19453let foo = 12;
19454let foo = 13;
19455let foo = 14;
19456let foo = 15;"#,
19457 );
19458
19459 cx.update_editor(|e, window, cx| {
19460 assert_eq!(
19461 e.next_scroll_position,
19462 NextScrollCursorCenterTopBottom::Center,
19463 "Default next scroll direction is center",
19464 );
19465
19466 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19467 assert_eq!(
19468 e.next_scroll_position,
19469 NextScrollCursorCenterTopBottom::Top,
19470 "After center, next scroll direction should be top",
19471 );
19472
19473 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19474 assert_eq!(
19475 e.next_scroll_position,
19476 NextScrollCursorCenterTopBottom::Bottom,
19477 "After top, next scroll direction should be bottom",
19478 );
19479
19480 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19481 assert_eq!(
19482 e.next_scroll_position,
19483 NextScrollCursorCenterTopBottom::Center,
19484 "After bottom, scrolling should start over",
19485 );
19486
19487 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19488 assert_eq!(
19489 e.next_scroll_position,
19490 NextScrollCursorCenterTopBottom::Top,
19491 "Scrolling continues if retriggered fast enough"
19492 );
19493 });
19494
19495 cx.executor()
19496 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
19497 cx.executor().run_until_parked();
19498 cx.update_editor(|e, _, _| {
19499 assert_eq!(
19500 e.next_scroll_position,
19501 NextScrollCursorCenterTopBottom::Center,
19502 "If scrolling is not triggered fast enough, it should reset"
19503 );
19504 });
19505}
19506
19507#[gpui::test]
19508async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
19509 init_test(cx, |_| {});
19510 let mut cx = EditorLspTestContext::new_rust(
19511 lsp::ServerCapabilities {
19512 definition_provider: Some(lsp::OneOf::Left(true)),
19513 references_provider: Some(lsp::OneOf::Left(true)),
19514 ..lsp::ServerCapabilities::default()
19515 },
19516 cx,
19517 )
19518 .await;
19519
19520 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
19521 let go_to_definition = cx
19522 .lsp
19523 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19524 move |params, _| async move {
19525 if empty_go_to_definition {
19526 Ok(None)
19527 } else {
19528 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
19529 uri: params.text_document_position_params.text_document.uri,
19530 range: lsp::Range::new(
19531 lsp::Position::new(4, 3),
19532 lsp::Position::new(4, 6),
19533 ),
19534 })))
19535 }
19536 },
19537 );
19538 let references = cx
19539 .lsp
19540 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
19541 Ok(Some(vec![lsp::Location {
19542 uri: params.text_document_position.text_document.uri,
19543 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
19544 }]))
19545 });
19546 (go_to_definition, references)
19547 };
19548
19549 cx.set_state(
19550 &r#"fn one() {
19551 let mut a = ˇtwo();
19552 }
19553
19554 fn two() {}"#
19555 .unindent(),
19556 );
19557 set_up_lsp_handlers(false, &mut cx);
19558 let navigated = cx
19559 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19560 .await
19561 .expect("Failed to navigate to definition");
19562 assert_eq!(
19563 navigated,
19564 Navigated::Yes,
19565 "Should have navigated to definition from the GetDefinition response"
19566 );
19567 cx.assert_editor_state(
19568 &r#"fn one() {
19569 let mut a = two();
19570 }
19571
19572 fn «twoˇ»() {}"#
19573 .unindent(),
19574 );
19575
19576 let editors = cx.update_workspace(|workspace, _, cx| {
19577 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19578 });
19579 cx.update_editor(|_, _, test_editor_cx| {
19580 assert_eq!(
19581 editors.len(),
19582 1,
19583 "Initially, only one, test, editor should be open in the workspace"
19584 );
19585 assert_eq!(
19586 test_editor_cx.entity(),
19587 editors.last().expect("Asserted len is 1").clone()
19588 );
19589 });
19590
19591 set_up_lsp_handlers(true, &mut cx);
19592 let navigated = cx
19593 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19594 .await
19595 .expect("Failed to navigate to lookup references");
19596 assert_eq!(
19597 navigated,
19598 Navigated::Yes,
19599 "Should have navigated to references as a fallback after empty GoToDefinition response"
19600 );
19601 // We should not change the selections in the existing file,
19602 // if opening another milti buffer with the references
19603 cx.assert_editor_state(
19604 &r#"fn one() {
19605 let mut a = two();
19606 }
19607
19608 fn «twoˇ»() {}"#
19609 .unindent(),
19610 );
19611 let editors = cx.update_workspace(|workspace, _, cx| {
19612 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19613 });
19614 cx.update_editor(|_, _, test_editor_cx| {
19615 assert_eq!(
19616 editors.len(),
19617 2,
19618 "After falling back to references search, we open a new editor with the results"
19619 );
19620 let references_fallback_text = editors
19621 .into_iter()
19622 .find(|new_editor| *new_editor != test_editor_cx.entity())
19623 .expect("Should have one non-test editor now")
19624 .read(test_editor_cx)
19625 .text(test_editor_cx);
19626 assert_eq!(
19627 references_fallback_text, "fn one() {\n let mut a = two();\n}",
19628 "Should use the range from the references response and not the GoToDefinition one"
19629 );
19630 });
19631}
19632
19633#[gpui::test]
19634async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
19635 init_test(cx, |_| {});
19636 cx.update(|cx| {
19637 let mut editor_settings = EditorSettings::get_global(cx).clone();
19638 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
19639 EditorSettings::override_global(editor_settings, cx);
19640 });
19641 let mut cx = EditorLspTestContext::new_rust(
19642 lsp::ServerCapabilities {
19643 definition_provider: Some(lsp::OneOf::Left(true)),
19644 references_provider: Some(lsp::OneOf::Left(true)),
19645 ..lsp::ServerCapabilities::default()
19646 },
19647 cx,
19648 )
19649 .await;
19650 let original_state = r#"fn one() {
19651 let mut a = ˇtwo();
19652 }
19653
19654 fn two() {}"#
19655 .unindent();
19656 cx.set_state(&original_state);
19657
19658 let mut go_to_definition = cx
19659 .lsp
19660 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19661 move |_, _| async move { Ok(None) },
19662 );
19663 let _references = cx
19664 .lsp
19665 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
19666 panic!("Should not call for references with no go to definition fallback")
19667 });
19668
19669 let navigated = cx
19670 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19671 .await
19672 .expect("Failed to navigate to lookup references");
19673 go_to_definition
19674 .next()
19675 .await
19676 .expect("Should have called the go_to_definition handler");
19677
19678 assert_eq!(
19679 navigated,
19680 Navigated::No,
19681 "Should have navigated to references as a fallback after empty GoToDefinition response"
19682 );
19683 cx.assert_editor_state(&original_state);
19684 let editors = cx.update_workspace(|workspace, _, cx| {
19685 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19686 });
19687 cx.update_editor(|_, _, _| {
19688 assert_eq!(
19689 editors.len(),
19690 1,
19691 "After unsuccessful fallback, no other editor should have been opened"
19692 );
19693 });
19694}
19695
19696#[gpui::test]
19697async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
19698 init_test(cx, |_| {});
19699
19700 let language = Arc::new(Language::new(
19701 LanguageConfig::default(),
19702 Some(tree_sitter_rust::LANGUAGE.into()),
19703 ));
19704
19705 let text = r#"
19706 #[cfg(test)]
19707 mod tests() {
19708 #[test]
19709 fn runnable_1() {
19710 let a = 1;
19711 }
19712
19713 #[test]
19714 fn runnable_2() {
19715 let a = 1;
19716 let b = 2;
19717 }
19718 }
19719 "#
19720 .unindent();
19721
19722 let fs = FakeFs::new(cx.executor());
19723 fs.insert_file("/file.rs", Default::default()).await;
19724
19725 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19726 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19727 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19728 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
19729 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
19730
19731 let editor = cx.new_window_entity(|window, cx| {
19732 Editor::new(
19733 EditorMode::full(),
19734 multi_buffer,
19735 Some(project.clone()),
19736 window,
19737 cx,
19738 )
19739 });
19740
19741 editor.update_in(cx, |editor, window, cx| {
19742 let snapshot = editor.buffer().read(cx).snapshot(cx);
19743 editor.tasks.insert(
19744 (buffer.read(cx).remote_id(), 3),
19745 RunnableTasks {
19746 templates: vec![],
19747 offset: snapshot.anchor_before(43),
19748 column: 0,
19749 extra_variables: HashMap::default(),
19750 context_range: BufferOffset(43)..BufferOffset(85),
19751 },
19752 );
19753 editor.tasks.insert(
19754 (buffer.read(cx).remote_id(), 8),
19755 RunnableTasks {
19756 templates: vec![],
19757 offset: snapshot.anchor_before(86),
19758 column: 0,
19759 extra_variables: HashMap::default(),
19760 context_range: BufferOffset(86)..BufferOffset(191),
19761 },
19762 );
19763
19764 // Test finding task when cursor is inside function body
19765 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19766 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
19767 });
19768 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19769 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
19770
19771 // Test finding task when cursor is on function name
19772 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19773 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
19774 });
19775 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19776 assert_eq!(row, 8, "Should find task when cursor is on function name");
19777 });
19778}
19779
19780#[gpui::test]
19781async fn test_folding_buffers(cx: &mut TestAppContext) {
19782 init_test(cx, |_| {});
19783
19784 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19785 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
19786 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
19787
19788 let fs = FakeFs::new(cx.executor());
19789 fs.insert_tree(
19790 path!("/a"),
19791 json!({
19792 "first.rs": sample_text_1,
19793 "second.rs": sample_text_2,
19794 "third.rs": sample_text_3,
19795 }),
19796 )
19797 .await;
19798 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19799 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19800 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19801 let worktree = project.update(cx, |project, cx| {
19802 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19803 assert_eq!(worktrees.len(), 1);
19804 worktrees.pop().unwrap()
19805 });
19806 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19807
19808 let buffer_1 = project
19809 .update(cx, |project, cx| {
19810 project.open_buffer((worktree_id, "first.rs"), cx)
19811 })
19812 .await
19813 .unwrap();
19814 let buffer_2 = project
19815 .update(cx, |project, cx| {
19816 project.open_buffer((worktree_id, "second.rs"), cx)
19817 })
19818 .await
19819 .unwrap();
19820 let buffer_3 = project
19821 .update(cx, |project, cx| {
19822 project.open_buffer((worktree_id, "third.rs"), cx)
19823 })
19824 .await
19825 .unwrap();
19826
19827 let multi_buffer = cx.new(|cx| {
19828 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19829 multi_buffer.push_excerpts(
19830 buffer_1.clone(),
19831 [
19832 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19833 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19834 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19835 ],
19836 cx,
19837 );
19838 multi_buffer.push_excerpts(
19839 buffer_2.clone(),
19840 [
19841 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19842 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19843 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19844 ],
19845 cx,
19846 );
19847 multi_buffer.push_excerpts(
19848 buffer_3.clone(),
19849 [
19850 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19851 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19852 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19853 ],
19854 cx,
19855 );
19856 multi_buffer
19857 });
19858 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19859 Editor::new(
19860 EditorMode::full(),
19861 multi_buffer.clone(),
19862 Some(project.clone()),
19863 window,
19864 cx,
19865 )
19866 });
19867
19868 assert_eq!(
19869 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19870 "\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",
19871 );
19872
19873 multi_buffer_editor.update(cx, |editor, cx| {
19874 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19875 });
19876 assert_eq!(
19877 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19878 "\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",
19879 "After folding the first buffer, its text should not be displayed"
19880 );
19881
19882 multi_buffer_editor.update(cx, |editor, cx| {
19883 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19884 });
19885 assert_eq!(
19886 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19887 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
19888 "After folding the second buffer, its text should not be displayed"
19889 );
19890
19891 multi_buffer_editor.update(cx, |editor, cx| {
19892 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19893 });
19894 assert_eq!(
19895 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19896 "\n\n\n\n\n",
19897 "After folding the third buffer, its text should not be displayed"
19898 );
19899
19900 // Emulate selection inside the fold logic, that should work
19901 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19902 editor
19903 .snapshot(window, cx)
19904 .next_line_boundary(Point::new(0, 4));
19905 });
19906
19907 multi_buffer_editor.update(cx, |editor, cx| {
19908 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19909 });
19910 assert_eq!(
19911 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19912 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19913 "After unfolding the second buffer, its text should be displayed"
19914 );
19915
19916 // Typing inside of buffer 1 causes that buffer to be unfolded.
19917 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19918 assert_eq!(
19919 multi_buffer
19920 .read(cx)
19921 .snapshot(cx)
19922 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
19923 .collect::<String>(),
19924 "bbbb"
19925 );
19926 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
19927 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
19928 });
19929 editor.handle_input("B", window, cx);
19930 });
19931
19932 assert_eq!(
19933 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19934 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19935 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
19936 );
19937
19938 multi_buffer_editor.update(cx, |editor, cx| {
19939 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19940 });
19941 assert_eq!(
19942 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19943 "\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",
19944 "After unfolding the all buffers, all original text should be displayed"
19945 );
19946}
19947
19948#[gpui::test]
19949async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
19950 init_test(cx, |_| {});
19951
19952 let sample_text_1 = "1111\n2222\n3333".to_string();
19953 let sample_text_2 = "4444\n5555\n6666".to_string();
19954 let sample_text_3 = "7777\n8888\n9999".to_string();
19955
19956 let fs = FakeFs::new(cx.executor());
19957 fs.insert_tree(
19958 path!("/a"),
19959 json!({
19960 "first.rs": sample_text_1,
19961 "second.rs": sample_text_2,
19962 "third.rs": sample_text_3,
19963 }),
19964 )
19965 .await;
19966 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19967 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19968 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19969 let worktree = project.update(cx, |project, cx| {
19970 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19971 assert_eq!(worktrees.len(), 1);
19972 worktrees.pop().unwrap()
19973 });
19974 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19975
19976 let buffer_1 = project
19977 .update(cx, |project, cx| {
19978 project.open_buffer((worktree_id, "first.rs"), cx)
19979 })
19980 .await
19981 .unwrap();
19982 let buffer_2 = project
19983 .update(cx, |project, cx| {
19984 project.open_buffer((worktree_id, "second.rs"), cx)
19985 })
19986 .await
19987 .unwrap();
19988 let buffer_3 = project
19989 .update(cx, |project, cx| {
19990 project.open_buffer((worktree_id, "third.rs"), cx)
19991 })
19992 .await
19993 .unwrap();
19994
19995 let multi_buffer = cx.new(|cx| {
19996 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19997 multi_buffer.push_excerpts(
19998 buffer_1.clone(),
19999 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20000 cx,
20001 );
20002 multi_buffer.push_excerpts(
20003 buffer_2.clone(),
20004 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20005 cx,
20006 );
20007 multi_buffer.push_excerpts(
20008 buffer_3.clone(),
20009 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20010 cx,
20011 );
20012 multi_buffer
20013 });
20014
20015 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20016 Editor::new(
20017 EditorMode::full(),
20018 multi_buffer,
20019 Some(project.clone()),
20020 window,
20021 cx,
20022 )
20023 });
20024
20025 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
20026 assert_eq!(
20027 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20028 full_text,
20029 );
20030
20031 multi_buffer_editor.update(cx, |editor, cx| {
20032 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
20033 });
20034 assert_eq!(
20035 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20036 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
20037 "After folding the first buffer, its text should not be displayed"
20038 );
20039
20040 multi_buffer_editor.update(cx, |editor, cx| {
20041 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
20042 });
20043
20044 assert_eq!(
20045 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20046 "\n\n\n\n\n\n7777\n8888\n9999",
20047 "After folding the second buffer, its text should not be displayed"
20048 );
20049
20050 multi_buffer_editor.update(cx, |editor, cx| {
20051 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
20052 });
20053 assert_eq!(
20054 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20055 "\n\n\n\n\n",
20056 "After folding the third buffer, its text should not be displayed"
20057 );
20058
20059 multi_buffer_editor.update(cx, |editor, cx| {
20060 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
20061 });
20062 assert_eq!(
20063 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20064 "\n\n\n\n4444\n5555\n6666\n\n",
20065 "After unfolding the second buffer, its text should be displayed"
20066 );
20067
20068 multi_buffer_editor.update(cx, |editor, cx| {
20069 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
20070 });
20071 assert_eq!(
20072 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20073 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
20074 "After unfolding the first buffer, its text should be displayed"
20075 );
20076
20077 multi_buffer_editor.update(cx, |editor, cx| {
20078 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
20079 });
20080 assert_eq!(
20081 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20082 full_text,
20083 "After unfolding all buffers, all original text should be displayed"
20084 );
20085}
20086
20087#[gpui::test]
20088async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
20089 init_test(cx, |_| {});
20090
20091 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
20092
20093 let fs = FakeFs::new(cx.executor());
20094 fs.insert_tree(
20095 path!("/a"),
20096 json!({
20097 "main.rs": sample_text,
20098 }),
20099 )
20100 .await;
20101 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20102 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20103 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20104 let worktree = project.update(cx, |project, cx| {
20105 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20106 assert_eq!(worktrees.len(), 1);
20107 worktrees.pop().unwrap()
20108 });
20109 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20110
20111 let buffer_1 = project
20112 .update(cx, |project, cx| {
20113 project.open_buffer((worktree_id, "main.rs"), cx)
20114 })
20115 .await
20116 .unwrap();
20117
20118 let multi_buffer = cx.new(|cx| {
20119 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20120 multi_buffer.push_excerpts(
20121 buffer_1.clone(),
20122 [ExcerptRange::new(
20123 Point::new(0, 0)
20124 ..Point::new(
20125 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
20126 0,
20127 ),
20128 )],
20129 cx,
20130 );
20131 multi_buffer
20132 });
20133 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20134 Editor::new(
20135 EditorMode::full(),
20136 multi_buffer,
20137 Some(project.clone()),
20138 window,
20139 cx,
20140 )
20141 });
20142
20143 let selection_range = Point::new(1, 0)..Point::new(2, 0);
20144 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20145 enum TestHighlight {}
20146 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
20147 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
20148 editor.highlight_text::<TestHighlight>(
20149 vec![highlight_range.clone()],
20150 HighlightStyle::color(Hsla::green()),
20151 cx,
20152 );
20153 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20154 s.select_ranges(Some(highlight_range))
20155 });
20156 });
20157
20158 let full_text = format!("\n\n{sample_text}");
20159 assert_eq!(
20160 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20161 full_text,
20162 );
20163}
20164
20165#[gpui::test]
20166async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
20167 init_test(cx, |_| {});
20168 cx.update(|cx| {
20169 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
20170 "keymaps/default-linux.json",
20171 cx,
20172 )
20173 .unwrap();
20174 cx.bind_keys(default_key_bindings);
20175 });
20176
20177 let (editor, cx) = cx.add_window_view(|window, cx| {
20178 let multi_buffer = MultiBuffer::build_multi(
20179 [
20180 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
20181 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
20182 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
20183 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
20184 ],
20185 cx,
20186 );
20187 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
20188
20189 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
20190 // fold all but the second buffer, so that we test navigating between two
20191 // adjacent folded buffers, as well as folded buffers at the start and
20192 // end the multibuffer
20193 editor.fold_buffer(buffer_ids[0], cx);
20194 editor.fold_buffer(buffer_ids[2], cx);
20195 editor.fold_buffer(buffer_ids[3], cx);
20196
20197 editor
20198 });
20199 cx.simulate_resize(size(px(1000.), px(1000.)));
20200
20201 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
20202 cx.assert_excerpts_with_selections(indoc! {"
20203 [EXCERPT]
20204 ˇ[FOLDED]
20205 [EXCERPT]
20206 a1
20207 b1
20208 [EXCERPT]
20209 [FOLDED]
20210 [EXCERPT]
20211 [FOLDED]
20212 "
20213 });
20214 cx.simulate_keystroke("down");
20215 cx.assert_excerpts_with_selections(indoc! {"
20216 [EXCERPT]
20217 [FOLDED]
20218 [EXCERPT]
20219 ˇa1
20220 b1
20221 [EXCERPT]
20222 [FOLDED]
20223 [EXCERPT]
20224 [FOLDED]
20225 "
20226 });
20227 cx.simulate_keystroke("down");
20228 cx.assert_excerpts_with_selections(indoc! {"
20229 [EXCERPT]
20230 [FOLDED]
20231 [EXCERPT]
20232 a1
20233 ˇb1
20234 [EXCERPT]
20235 [FOLDED]
20236 [EXCERPT]
20237 [FOLDED]
20238 "
20239 });
20240 cx.simulate_keystroke("down");
20241 cx.assert_excerpts_with_selections(indoc! {"
20242 [EXCERPT]
20243 [FOLDED]
20244 [EXCERPT]
20245 a1
20246 b1
20247 ˇ[EXCERPT]
20248 [FOLDED]
20249 [EXCERPT]
20250 [FOLDED]
20251 "
20252 });
20253 cx.simulate_keystroke("down");
20254 cx.assert_excerpts_with_selections(indoc! {"
20255 [EXCERPT]
20256 [FOLDED]
20257 [EXCERPT]
20258 a1
20259 b1
20260 [EXCERPT]
20261 ˇ[FOLDED]
20262 [EXCERPT]
20263 [FOLDED]
20264 "
20265 });
20266 for _ in 0..5 {
20267 cx.simulate_keystroke("down");
20268 cx.assert_excerpts_with_selections(indoc! {"
20269 [EXCERPT]
20270 [FOLDED]
20271 [EXCERPT]
20272 a1
20273 b1
20274 [EXCERPT]
20275 [FOLDED]
20276 [EXCERPT]
20277 ˇ[FOLDED]
20278 "
20279 });
20280 }
20281
20282 cx.simulate_keystroke("up");
20283 cx.assert_excerpts_with_selections(indoc! {"
20284 [EXCERPT]
20285 [FOLDED]
20286 [EXCERPT]
20287 a1
20288 b1
20289 [EXCERPT]
20290 ˇ[FOLDED]
20291 [EXCERPT]
20292 [FOLDED]
20293 "
20294 });
20295 cx.simulate_keystroke("up");
20296 cx.assert_excerpts_with_selections(indoc! {"
20297 [EXCERPT]
20298 [FOLDED]
20299 [EXCERPT]
20300 a1
20301 b1
20302 ˇ[EXCERPT]
20303 [FOLDED]
20304 [EXCERPT]
20305 [FOLDED]
20306 "
20307 });
20308 cx.simulate_keystroke("up");
20309 cx.assert_excerpts_with_selections(indoc! {"
20310 [EXCERPT]
20311 [FOLDED]
20312 [EXCERPT]
20313 a1
20314 ˇb1
20315 [EXCERPT]
20316 [FOLDED]
20317 [EXCERPT]
20318 [FOLDED]
20319 "
20320 });
20321 cx.simulate_keystroke("up");
20322 cx.assert_excerpts_with_selections(indoc! {"
20323 [EXCERPT]
20324 [FOLDED]
20325 [EXCERPT]
20326 ˇa1
20327 b1
20328 [EXCERPT]
20329 [FOLDED]
20330 [EXCERPT]
20331 [FOLDED]
20332 "
20333 });
20334 for _ in 0..5 {
20335 cx.simulate_keystroke("up");
20336 cx.assert_excerpts_with_selections(indoc! {"
20337 [EXCERPT]
20338 ˇ[FOLDED]
20339 [EXCERPT]
20340 a1
20341 b1
20342 [EXCERPT]
20343 [FOLDED]
20344 [EXCERPT]
20345 [FOLDED]
20346 "
20347 });
20348 }
20349}
20350
20351#[gpui::test]
20352async fn test_inline_completion_text(cx: &mut TestAppContext) {
20353 init_test(cx, |_| {});
20354
20355 // Simple insertion
20356 assert_highlighted_edits(
20357 "Hello, world!",
20358 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
20359 true,
20360 cx,
20361 |highlighted_edits, cx| {
20362 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
20363 assert_eq!(highlighted_edits.highlights.len(), 1);
20364 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
20365 assert_eq!(
20366 highlighted_edits.highlights[0].1.background_color,
20367 Some(cx.theme().status().created_background)
20368 );
20369 },
20370 )
20371 .await;
20372
20373 // Replacement
20374 assert_highlighted_edits(
20375 "This is a test.",
20376 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
20377 false,
20378 cx,
20379 |highlighted_edits, cx| {
20380 assert_eq!(highlighted_edits.text, "That is a test.");
20381 assert_eq!(highlighted_edits.highlights.len(), 1);
20382 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
20383 assert_eq!(
20384 highlighted_edits.highlights[0].1.background_color,
20385 Some(cx.theme().status().created_background)
20386 );
20387 },
20388 )
20389 .await;
20390
20391 // Multiple edits
20392 assert_highlighted_edits(
20393 "Hello, world!",
20394 vec![
20395 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
20396 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
20397 ],
20398 false,
20399 cx,
20400 |highlighted_edits, cx| {
20401 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
20402 assert_eq!(highlighted_edits.highlights.len(), 2);
20403 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
20404 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
20405 assert_eq!(
20406 highlighted_edits.highlights[0].1.background_color,
20407 Some(cx.theme().status().created_background)
20408 );
20409 assert_eq!(
20410 highlighted_edits.highlights[1].1.background_color,
20411 Some(cx.theme().status().created_background)
20412 );
20413 },
20414 )
20415 .await;
20416
20417 // Multiple lines with edits
20418 assert_highlighted_edits(
20419 "First line\nSecond line\nThird line\nFourth line",
20420 vec![
20421 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
20422 (
20423 Point::new(2, 0)..Point::new(2, 10),
20424 "New third line".to_string(),
20425 ),
20426 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
20427 ],
20428 false,
20429 cx,
20430 |highlighted_edits, cx| {
20431 assert_eq!(
20432 highlighted_edits.text,
20433 "Second modified\nNew third line\nFourth updated line"
20434 );
20435 assert_eq!(highlighted_edits.highlights.len(), 3);
20436 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
20437 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
20438 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
20439 for highlight in &highlighted_edits.highlights {
20440 assert_eq!(
20441 highlight.1.background_color,
20442 Some(cx.theme().status().created_background)
20443 );
20444 }
20445 },
20446 )
20447 .await;
20448}
20449
20450#[gpui::test]
20451async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
20452 init_test(cx, |_| {});
20453
20454 // Deletion
20455 assert_highlighted_edits(
20456 "Hello, world!",
20457 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
20458 true,
20459 cx,
20460 |highlighted_edits, cx| {
20461 assert_eq!(highlighted_edits.text, "Hello, world!");
20462 assert_eq!(highlighted_edits.highlights.len(), 1);
20463 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
20464 assert_eq!(
20465 highlighted_edits.highlights[0].1.background_color,
20466 Some(cx.theme().status().deleted_background)
20467 );
20468 },
20469 )
20470 .await;
20471
20472 // Insertion
20473 assert_highlighted_edits(
20474 "Hello, world!",
20475 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
20476 true,
20477 cx,
20478 |highlighted_edits, cx| {
20479 assert_eq!(highlighted_edits.highlights.len(), 1);
20480 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
20481 assert_eq!(
20482 highlighted_edits.highlights[0].1.background_color,
20483 Some(cx.theme().status().created_background)
20484 );
20485 },
20486 )
20487 .await;
20488}
20489
20490async fn assert_highlighted_edits(
20491 text: &str,
20492 edits: Vec<(Range<Point>, String)>,
20493 include_deletions: bool,
20494 cx: &mut TestAppContext,
20495 assertion_fn: impl Fn(HighlightedText, &App),
20496) {
20497 let window = cx.add_window(|window, cx| {
20498 let buffer = MultiBuffer::build_simple(text, cx);
20499 Editor::new(EditorMode::full(), buffer, None, window, cx)
20500 });
20501 let cx = &mut VisualTestContext::from_window(*window, cx);
20502
20503 let (buffer, snapshot) = window
20504 .update(cx, |editor, _window, cx| {
20505 (
20506 editor.buffer().clone(),
20507 editor.buffer().read(cx).snapshot(cx),
20508 )
20509 })
20510 .unwrap();
20511
20512 let edits = edits
20513 .into_iter()
20514 .map(|(range, edit)| {
20515 (
20516 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
20517 edit,
20518 )
20519 })
20520 .collect::<Vec<_>>();
20521
20522 let text_anchor_edits = edits
20523 .clone()
20524 .into_iter()
20525 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
20526 .collect::<Vec<_>>();
20527
20528 let edit_preview = window
20529 .update(cx, |_, _window, cx| {
20530 buffer
20531 .read(cx)
20532 .as_singleton()
20533 .unwrap()
20534 .read(cx)
20535 .preview_edits(text_anchor_edits.into(), cx)
20536 })
20537 .unwrap()
20538 .await;
20539
20540 cx.update(|_window, cx| {
20541 let highlighted_edits = inline_completion_edit_text(
20542 &snapshot.as_singleton().unwrap().2,
20543 &edits,
20544 &edit_preview,
20545 include_deletions,
20546 cx,
20547 );
20548 assertion_fn(highlighted_edits, cx)
20549 });
20550}
20551
20552#[track_caller]
20553fn assert_breakpoint(
20554 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
20555 path: &Arc<Path>,
20556 expected: Vec<(u32, Breakpoint)>,
20557) {
20558 if expected.len() == 0usize {
20559 assert!(!breakpoints.contains_key(path), "{}", path.display());
20560 } else {
20561 let mut breakpoint = breakpoints
20562 .get(path)
20563 .unwrap()
20564 .into_iter()
20565 .map(|breakpoint| {
20566 (
20567 breakpoint.row,
20568 Breakpoint {
20569 message: breakpoint.message.clone(),
20570 state: breakpoint.state,
20571 condition: breakpoint.condition.clone(),
20572 hit_condition: breakpoint.hit_condition.clone(),
20573 },
20574 )
20575 })
20576 .collect::<Vec<_>>();
20577
20578 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
20579
20580 assert_eq!(expected, breakpoint);
20581 }
20582}
20583
20584fn add_log_breakpoint_at_cursor(
20585 editor: &mut Editor,
20586 log_message: &str,
20587 window: &mut Window,
20588 cx: &mut Context<Editor>,
20589) {
20590 let (anchor, bp) = editor
20591 .breakpoints_at_cursors(window, cx)
20592 .first()
20593 .and_then(|(anchor, bp)| {
20594 if let Some(bp) = bp {
20595 Some((*anchor, bp.clone()))
20596 } else {
20597 None
20598 }
20599 })
20600 .unwrap_or_else(|| {
20601 let cursor_position: Point = editor.selections.newest(cx).head();
20602
20603 let breakpoint_position = editor
20604 .snapshot(window, cx)
20605 .display_snapshot
20606 .buffer_snapshot
20607 .anchor_before(Point::new(cursor_position.row, 0));
20608
20609 (breakpoint_position, Breakpoint::new_log(&log_message))
20610 });
20611
20612 editor.edit_breakpoint_at_anchor(
20613 anchor,
20614 bp,
20615 BreakpointEditAction::EditLogMessage(log_message.into()),
20616 cx,
20617 );
20618}
20619
20620#[gpui::test]
20621async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
20622 init_test(cx, |_| {});
20623
20624 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20625 let fs = FakeFs::new(cx.executor());
20626 fs.insert_tree(
20627 path!("/a"),
20628 json!({
20629 "main.rs": sample_text,
20630 }),
20631 )
20632 .await;
20633 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20634 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20635 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20636
20637 let fs = FakeFs::new(cx.executor());
20638 fs.insert_tree(
20639 path!("/a"),
20640 json!({
20641 "main.rs": sample_text,
20642 }),
20643 )
20644 .await;
20645 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20646 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20647 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20648 let worktree_id = workspace
20649 .update(cx, |workspace, _window, cx| {
20650 workspace.project().update(cx, |project, cx| {
20651 project.worktrees(cx).next().unwrap().read(cx).id()
20652 })
20653 })
20654 .unwrap();
20655
20656 let buffer = project
20657 .update(cx, |project, cx| {
20658 project.open_buffer((worktree_id, "main.rs"), cx)
20659 })
20660 .await
20661 .unwrap();
20662
20663 let (editor, cx) = cx.add_window_view(|window, cx| {
20664 Editor::new(
20665 EditorMode::full(),
20666 MultiBuffer::build_from_buffer(buffer, cx),
20667 Some(project.clone()),
20668 window,
20669 cx,
20670 )
20671 });
20672
20673 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20674 let abs_path = project.read_with(cx, |project, cx| {
20675 project
20676 .absolute_path(&project_path, cx)
20677 .map(|path_buf| Arc::from(path_buf.to_owned()))
20678 .unwrap()
20679 });
20680
20681 // assert we can add breakpoint on the first line
20682 editor.update_in(cx, |editor, window, cx| {
20683 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20684 editor.move_to_end(&MoveToEnd, window, cx);
20685 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20686 });
20687
20688 let breakpoints = editor.update(cx, |editor, cx| {
20689 editor
20690 .breakpoint_store()
20691 .as_ref()
20692 .unwrap()
20693 .read(cx)
20694 .all_source_breakpoints(cx)
20695 .clone()
20696 });
20697
20698 assert_eq!(1, breakpoints.len());
20699 assert_breakpoint(
20700 &breakpoints,
20701 &abs_path,
20702 vec![
20703 (0, Breakpoint::new_standard()),
20704 (3, Breakpoint::new_standard()),
20705 ],
20706 );
20707
20708 editor.update_in(cx, |editor, window, cx| {
20709 editor.move_to_beginning(&MoveToBeginning, window, cx);
20710 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20711 });
20712
20713 let breakpoints = editor.update(cx, |editor, cx| {
20714 editor
20715 .breakpoint_store()
20716 .as_ref()
20717 .unwrap()
20718 .read(cx)
20719 .all_source_breakpoints(cx)
20720 .clone()
20721 });
20722
20723 assert_eq!(1, breakpoints.len());
20724 assert_breakpoint(
20725 &breakpoints,
20726 &abs_path,
20727 vec![(3, Breakpoint::new_standard())],
20728 );
20729
20730 editor.update_in(cx, |editor, window, cx| {
20731 editor.move_to_end(&MoveToEnd, window, cx);
20732 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20733 });
20734
20735 let breakpoints = editor.update(cx, |editor, cx| {
20736 editor
20737 .breakpoint_store()
20738 .as_ref()
20739 .unwrap()
20740 .read(cx)
20741 .all_source_breakpoints(cx)
20742 .clone()
20743 });
20744
20745 assert_eq!(0, breakpoints.len());
20746 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20747}
20748
20749#[gpui::test]
20750async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
20751 init_test(cx, |_| {});
20752
20753 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20754
20755 let fs = FakeFs::new(cx.executor());
20756 fs.insert_tree(
20757 path!("/a"),
20758 json!({
20759 "main.rs": sample_text,
20760 }),
20761 )
20762 .await;
20763 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20764 let (workspace, cx) =
20765 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20766
20767 let worktree_id = workspace.update(cx, |workspace, cx| {
20768 workspace.project().update(cx, |project, cx| {
20769 project.worktrees(cx).next().unwrap().read(cx).id()
20770 })
20771 });
20772
20773 let buffer = project
20774 .update(cx, |project, cx| {
20775 project.open_buffer((worktree_id, "main.rs"), cx)
20776 })
20777 .await
20778 .unwrap();
20779
20780 let (editor, cx) = cx.add_window_view(|window, cx| {
20781 Editor::new(
20782 EditorMode::full(),
20783 MultiBuffer::build_from_buffer(buffer, cx),
20784 Some(project.clone()),
20785 window,
20786 cx,
20787 )
20788 });
20789
20790 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20791 let abs_path = project.read_with(cx, |project, cx| {
20792 project
20793 .absolute_path(&project_path, cx)
20794 .map(|path_buf| Arc::from(path_buf.to_owned()))
20795 .unwrap()
20796 });
20797
20798 editor.update_in(cx, |editor, window, cx| {
20799 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20800 });
20801
20802 let breakpoints = editor.update(cx, |editor, cx| {
20803 editor
20804 .breakpoint_store()
20805 .as_ref()
20806 .unwrap()
20807 .read(cx)
20808 .all_source_breakpoints(cx)
20809 .clone()
20810 });
20811
20812 assert_breakpoint(
20813 &breakpoints,
20814 &abs_path,
20815 vec![(0, Breakpoint::new_log("hello world"))],
20816 );
20817
20818 // Removing a log message from a log breakpoint should remove it
20819 editor.update_in(cx, |editor, window, cx| {
20820 add_log_breakpoint_at_cursor(editor, "", window, cx);
20821 });
20822
20823 let breakpoints = editor.update(cx, |editor, cx| {
20824 editor
20825 .breakpoint_store()
20826 .as_ref()
20827 .unwrap()
20828 .read(cx)
20829 .all_source_breakpoints(cx)
20830 .clone()
20831 });
20832
20833 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20834
20835 editor.update_in(cx, |editor, window, cx| {
20836 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20837 editor.move_to_end(&MoveToEnd, window, cx);
20838 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20839 // Not adding a log message to a standard breakpoint shouldn't remove it
20840 add_log_breakpoint_at_cursor(editor, "", window, cx);
20841 });
20842
20843 let breakpoints = editor.update(cx, |editor, cx| {
20844 editor
20845 .breakpoint_store()
20846 .as_ref()
20847 .unwrap()
20848 .read(cx)
20849 .all_source_breakpoints(cx)
20850 .clone()
20851 });
20852
20853 assert_breakpoint(
20854 &breakpoints,
20855 &abs_path,
20856 vec![
20857 (0, Breakpoint::new_standard()),
20858 (3, Breakpoint::new_standard()),
20859 ],
20860 );
20861
20862 editor.update_in(cx, |editor, window, cx| {
20863 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20864 });
20865
20866 let breakpoints = editor.update(cx, |editor, cx| {
20867 editor
20868 .breakpoint_store()
20869 .as_ref()
20870 .unwrap()
20871 .read(cx)
20872 .all_source_breakpoints(cx)
20873 .clone()
20874 });
20875
20876 assert_breakpoint(
20877 &breakpoints,
20878 &abs_path,
20879 vec![
20880 (0, Breakpoint::new_standard()),
20881 (3, Breakpoint::new_log("hello world")),
20882 ],
20883 );
20884
20885 editor.update_in(cx, |editor, window, cx| {
20886 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
20887 });
20888
20889 let breakpoints = editor.update(cx, |editor, cx| {
20890 editor
20891 .breakpoint_store()
20892 .as_ref()
20893 .unwrap()
20894 .read(cx)
20895 .all_source_breakpoints(cx)
20896 .clone()
20897 });
20898
20899 assert_breakpoint(
20900 &breakpoints,
20901 &abs_path,
20902 vec![
20903 (0, Breakpoint::new_standard()),
20904 (3, Breakpoint::new_log("hello Earth!!")),
20905 ],
20906 );
20907}
20908
20909/// This also tests that Editor::breakpoint_at_cursor_head is working properly
20910/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
20911/// or when breakpoints were placed out of order. This tests for a regression too
20912#[gpui::test]
20913async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
20914 init_test(cx, |_| {});
20915
20916 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20917 let fs = FakeFs::new(cx.executor());
20918 fs.insert_tree(
20919 path!("/a"),
20920 json!({
20921 "main.rs": sample_text,
20922 }),
20923 )
20924 .await;
20925 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20926 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20927 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20928
20929 let fs = FakeFs::new(cx.executor());
20930 fs.insert_tree(
20931 path!("/a"),
20932 json!({
20933 "main.rs": sample_text,
20934 }),
20935 )
20936 .await;
20937 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20938 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20939 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20940 let worktree_id = workspace
20941 .update(cx, |workspace, _window, cx| {
20942 workspace.project().update(cx, |project, cx| {
20943 project.worktrees(cx).next().unwrap().read(cx).id()
20944 })
20945 })
20946 .unwrap();
20947
20948 let buffer = project
20949 .update(cx, |project, cx| {
20950 project.open_buffer((worktree_id, "main.rs"), cx)
20951 })
20952 .await
20953 .unwrap();
20954
20955 let (editor, cx) = cx.add_window_view(|window, cx| {
20956 Editor::new(
20957 EditorMode::full(),
20958 MultiBuffer::build_from_buffer(buffer, cx),
20959 Some(project.clone()),
20960 window,
20961 cx,
20962 )
20963 });
20964
20965 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20966 let abs_path = project.read_with(cx, |project, cx| {
20967 project
20968 .absolute_path(&project_path, cx)
20969 .map(|path_buf| Arc::from(path_buf.to_owned()))
20970 .unwrap()
20971 });
20972
20973 // assert we can add breakpoint on the first line
20974 editor.update_in(cx, |editor, window, cx| {
20975 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20976 editor.move_to_end(&MoveToEnd, window, cx);
20977 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20978 editor.move_up(&MoveUp, window, cx);
20979 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20980 });
20981
20982 let breakpoints = editor.update(cx, |editor, cx| {
20983 editor
20984 .breakpoint_store()
20985 .as_ref()
20986 .unwrap()
20987 .read(cx)
20988 .all_source_breakpoints(cx)
20989 .clone()
20990 });
20991
20992 assert_eq!(1, breakpoints.len());
20993 assert_breakpoint(
20994 &breakpoints,
20995 &abs_path,
20996 vec![
20997 (0, Breakpoint::new_standard()),
20998 (2, Breakpoint::new_standard()),
20999 (3, Breakpoint::new_standard()),
21000 ],
21001 );
21002
21003 editor.update_in(cx, |editor, window, cx| {
21004 editor.move_to_beginning(&MoveToBeginning, window, cx);
21005 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21006 editor.move_to_end(&MoveToEnd, window, cx);
21007 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21008 // Disabling a breakpoint that doesn't exist should do nothing
21009 editor.move_up(&MoveUp, window, cx);
21010 editor.move_up(&MoveUp, window, cx);
21011 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21012 });
21013
21014 let breakpoints = editor.update(cx, |editor, cx| {
21015 editor
21016 .breakpoint_store()
21017 .as_ref()
21018 .unwrap()
21019 .read(cx)
21020 .all_source_breakpoints(cx)
21021 .clone()
21022 });
21023
21024 let disable_breakpoint = {
21025 let mut bp = Breakpoint::new_standard();
21026 bp.state = BreakpointState::Disabled;
21027 bp
21028 };
21029
21030 assert_eq!(1, breakpoints.len());
21031 assert_breakpoint(
21032 &breakpoints,
21033 &abs_path,
21034 vec![
21035 (0, disable_breakpoint.clone()),
21036 (2, Breakpoint::new_standard()),
21037 (3, disable_breakpoint.clone()),
21038 ],
21039 );
21040
21041 editor.update_in(cx, |editor, window, cx| {
21042 editor.move_to_beginning(&MoveToBeginning, window, cx);
21043 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
21044 editor.move_to_end(&MoveToEnd, window, cx);
21045 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
21046 editor.move_up(&MoveUp, window, cx);
21047 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21048 });
21049
21050 let breakpoints = editor.update(cx, |editor, cx| {
21051 editor
21052 .breakpoint_store()
21053 .as_ref()
21054 .unwrap()
21055 .read(cx)
21056 .all_source_breakpoints(cx)
21057 .clone()
21058 });
21059
21060 assert_eq!(1, breakpoints.len());
21061 assert_breakpoint(
21062 &breakpoints,
21063 &abs_path,
21064 vec![
21065 (0, Breakpoint::new_standard()),
21066 (2, disable_breakpoint),
21067 (3, Breakpoint::new_standard()),
21068 ],
21069 );
21070}
21071
21072#[gpui::test]
21073async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
21074 init_test(cx, |_| {});
21075 let capabilities = lsp::ServerCapabilities {
21076 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
21077 prepare_provider: Some(true),
21078 work_done_progress_options: Default::default(),
21079 })),
21080 ..Default::default()
21081 };
21082 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21083
21084 cx.set_state(indoc! {"
21085 struct Fˇoo {}
21086 "});
21087
21088 cx.update_editor(|editor, _, cx| {
21089 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21090 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21091 editor.highlight_background::<DocumentHighlightRead>(
21092 &[highlight_range],
21093 |theme| theme.colors().editor_document_highlight_read_background,
21094 cx,
21095 );
21096 });
21097
21098 let mut prepare_rename_handler = cx
21099 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
21100 move |_, _, _| async move {
21101 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
21102 start: lsp::Position {
21103 line: 0,
21104 character: 7,
21105 },
21106 end: lsp::Position {
21107 line: 0,
21108 character: 10,
21109 },
21110 })))
21111 },
21112 );
21113 let prepare_rename_task = cx
21114 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21115 .expect("Prepare rename was not started");
21116 prepare_rename_handler.next().await.unwrap();
21117 prepare_rename_task.await.expect("Prepare rename failed");
21118
21119 let mut rename_handler =
21120 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21121 let edit = lsp::TextEdit {
21122 range: lsp::Range {
21123 start: lsp::Position {
21124 line: 0,
21125 character: 7,
21126 },
21127 end: lsp::Position {
21128 line: 0,
21129 character: 10,
21130 },
21131 },
21132 new_text: "FooRenamed".to_string(),
21133 };
21134 Ok(Some(lsp::WorkspaceEdit::new(
21135 // Specify the same edit twice
21136 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
21137 )))
21138 });
21139 let rename_task = cx
21140 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21141 .expect("Confirm rename was not started");
21142 rename_handler.next().await.unwrap();
21143 rename_task.await.expect("Confirm rename failed");
21144 cx.run_until_parked();
21145
21146 // Despite two edits, only one is actually applied as those are identical
21147 cx.assert_editor_state(indoc! {"
21148 struct FooRenamedˇ {}
21149 "});
21150}
21151
21152#[gpui::test]
21153async fn test_rename_without_prepare(cx: &mut TestAppContext) {
21154 init_test(cx, |_| {});
21155 // These capabilities indicate that the server does not support prepare rename.
21156 let capabilities = lsp::ServerCapabilities {
21157 rename_provider: Some(lsp::OneOf::Left(true)),
21158 ..Default::default()
21159 };
21160 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21161
21162 cx.set_state(indoc! {"
21163 struct Fˇoo {}
21164 "});
21165
21166 cx.update_editor(|editor, _window, cx| {
21167 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21168 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21169 editor.highlight_background::<DocumentHighlightRead>(
21170 &[highlight_range],
21171 |theme| theme.colors().editor_document_highlight_read_background,
21172 cx,
21173 );
21174 });
21175
21176 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21177 .expect("Prepare rename was not started")
21178 .await
21179 .expect("Prepare rename failed");
21180
21181 let mut rename_handler =
21182 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21183 let edit = lsp::TextEdit {
21184 range: lsp::Range {
21185 start: lsp::Position {
21186 line: 0,
21187 character: 7,
21188 },
21189 end: lsp::Position {
21190 line: 0,
21191 character: 10,
21192 },
21193 },
21194 new_text: "FooRenamed".to_string(),
21195 };
21196 Ok(Some(lsp::WorkspaceEdit::new(
21197 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
21198 )))
21199 });
21200 let rename_task = cx
21201 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21202 .expect("Confirm rename was not started");
21203 rename_handler.next().await.unwrap();
21204 rename_task.await.expect("Confirm rename failed");
21205 cx.run_until_parked();
21206
21207 // Correct range is renamed, as `surrounding_word` is used to find it.
21208 cx.assert_editor_state(indoc! {"
21209 struct FooRenamedˇ {}
21210 "});
21211}
21212
21213#[gpui::test]
21214async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
21215 init_test(cx, |_| {});
21216 let mut cx = EditorTestContext::new(cx).await;
21217
21218 let language = Arc::new(
21219 Language::new(
21220 LanguageConfig::default(),
21221 Some(tree_sitter_html::LANGUAGE.into()),
21222 )
21223 .with_brackets_query(
21224 r#"
21225 ("<" @open "/>" @close)
21226 ("</" @open ">" @close)
21227 ("<" @open ">" @close)
21228 ("\"" @open "\"" @close)
21229 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
21230 "#,
21231 )
21232 .unwrap(),
21233 );
21234 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21235
21236 cx.set_state(indoc! {"
21237 <span>ˇ</span>
21238 "});
21239 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21240 cx.assert_editor_state(indoc! {"
21241 <span>
21242 ˇ
21243 </span>
21244 "});
21245
21246 cx.set_state(indoc! {"
21247 <span><span></span>ˇ</span>
21248 "});
21249 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21250 cx.assert_editor_state(indoc! {"
21251 <span><span></span>
21252 ˇ</span>
21253 "});
21254
21255 cx.set_state(indoc! {"
21256 <span>ˇ
21257 </span>
21258 "});
21259 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21260 cx.assert_editor_state(indoc! {"
21261 <span>
21262 ˇ
21263 </span>
21264 "});
21265}
21266
21267#[gpui::test(iterations = 10)]
21268async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
21269 init_test(cx, |_| {});
21270
21271 let fs = FakeFs::new(cx.executor());
21272 fs.insert_tree(
21273 path!("/dir"),
21274 json!({
21275 "a.ts": "a",
21276 }),
21277 )
21278 .await;
21279
21280 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
21281 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21282 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21283
21284 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21285 language_registry.add(Arc::new(Language::new(
21286 LanguageConfig {
21287 name: "TypeScript".into(),
21288 matcher: LanguageMatcher {
21289 path_suffixes: vec!["ts".to_string()],
21290 ..Default::default()
21291 },
21292 ..Default::default()
21293 },
21294 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
21295 )));
21296 let mut fake_language_servers = language_registry.register_fake_lsp(
21297 "TypeScript",
21298 FakeLspAdapter {
21299 capabilities: lsp::ServerCapabilities {
21300 code_lens_provider: Some(lsp::CodeLensOptions {
21301 resolve_provider: Some(true),
21302 }),
21303 execute_command_provider: Some(lsp::ExecuteCommandOptions {
21304 commands: vec!["_the/command".to_string()],
21305 ..lsp::ExecuteCommandOptions::default()
21306 }),
21307 ..lsp::ServerCapabilities::default()
21308 },
21309 ..FakeLspAdapter::default()
21310 },
21311 );
21312
21313 let (buffer, _handle) = project
21314 .update(cx, |p, cx| {
21315 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
21316 })
21317 .await
21318 .unwrap();
21319 cx.executor().run_until_parked();
21320
21321 let fake_server = fake_language_servers.next().await.unwrap();
21322
21323 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
21324 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
21325 drop(buffer_snapshot);
21326 let actions = cx
21327 .update_window(*workspace, |_, window, cx| {
21328 project.code_actions(&buffer, anchor..anchor, window, cx)
21329 })
21330 .unwrap();
21331
21332 fake_server
21333 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21334 Ok(Some(vec![
21335 lsp::CodeLens {
21336 range: lsp::Range::default(),
21337 command: Some(lsp::Command {
21338 title: "Code lens command".to_owned(),
21339 command: "_the/command".to_owned(),
21340 arguments: None,
21341 }),
21342 data: None,
21343 },
21344 lsp::CodeLens {
21345 range: lsp::Range::default(),
21346 command: Some(lsp::Command {
21347 title: "Command not in capabilities".to_owned(),
21348 command: "not in capabilities".to_owned(),
21349 arguments: None,
21350 }),
21351 data: None,
21352 },
21353 lsp::CodeLens {
21354 range: lsp::Range {
21355 start: lsp::Position {
21356 line: 1,
21357 character: 1,
21358 },
21359 end: lsp::Position {
21360 line: 1,
21361 character: 1,
21362 },
21363 },
21364 command: Some(lsp::Command {
21365 title: "Command not in range".to_owned(),
21366 command: "_the/command".to_owned(),
21367 arguments: None,
21368 }),
21369 data: None,
21370 },
21371 ]))
21372 })
21373 .next()
21374 .await;
21375
21376 let actions = actions.await.unwrap();
21377 assert_eq!(
21378 actions.len(),
21379 1,
21380 "Should have only one valid action for the 0..0 range"
21381 );
21382 let action = actions[0].clone();
21383 let apply = project.update(cx, |project, cx| {
21384 project.apply_code_action(buffer.clone(), action, true, cx)
21385 });
21386
21387 // Resolving the code action does not populate its edits. In absence of
21388 // edits, we must execute the given command.
21389 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
21390 |mut lens, _| async move {
21391 let lens_command = lens.command.as_mut().expect("should have a command");
21392 assert_eq!(lens_command.title, "Code lens command");
21393 lens_command.arguments = Some(vec![json!("the-argument")]);
21394 Ok(lens)
21395 },
21396 );
21397
21398 // While executing the command, the language server sends the editor
21399 // a `workspaceEdit` request.
21400 fake_server
21401 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
21402 let fake = fake_server.clone();
21403 move |params, _| {
21404 assert_eq!(params.command, "_the/command");
21405 let fake = fake.clone();
21406 async move {
21407 fake.server
21408 .request::<lsp::request::ApplyWorkspaceEdit>(
21409 lsp::ApplyWorkspaceEditParams {
21410 label: None,
21411 edit: lsp::WorkspaceEdit {
21412 changes: Some(
21413 [(
21414 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
21415 vec![lsp::TextEdit {
21416 range: lsp::Range::new(
21417 lsp::Position::new(0, 0),
21418 lsp::Position::new(0, 0),
21419 ),
21420 new_text: "X".into(),
21421 }],
21422 )]
21423 .into_iter()
21424 .collect(),
21425 ),
21426 ..Default::default()
21427 },
21428 },
21429 )
21430 .await
21431 .into_response()
21432 .unwrap();
21433 Ok(Some(json!(null)))
21434 }
21435 }
21436 })
21437 .next()
21438 .await;
21439
21440 // Applying the code lens command returns a project transaction containing the edits
21441 // sent by the language server in its `workspaceEdit` request.
21442 let transaction = apply.await.unwrap();
21443 assert!(transaction.0.contains_key(&buffer));
21444 buffer.update(cx, |buffer, cx| {
21445 assert_eq!(buffer.text(), "Xa");
21446 buffer.undo(cx);
21447 assert_eq!(buffer.text(), "a");
21448 });
21449}
21450
21451#[gpui::test]
21452async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
21453 init_test(cx, |_| {});
21454
21455 let fs = FakeFs::new(cx.executor());
21456 let main_text = r#"fn main() {
21457println!("1");
21458println!("2");
21459println!("3");
21460println!("4");
21461println!("5");
21462}"#;
21463 let lib_text = "mod foo {}";
21464 fs.insert_tree(
21465 path!("/a"),
21466 json!({
21467 "lib.rs": lib_text,
21468 "main.rs": main_text,
21469 }),
21470 )
21471 .await;
21472
21473 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21474 let (workspace, cx) =
21475 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21476 let worktree_id = workspace.update(cx, |workspace, cx| {
21477 workspace.project().update(cx, |project, cx| {
21478 project.worktrees(cx).next().unwrap().read(cx).id()
21479 })
21480 });
21481
21482 let expected_ranges = vec![
21483 Point::new(0, 0)..Point::new(0, 0),
21484 Point::new(1, 0)..Point::new(1, 1),
21485 Point::new(2, 0)..Point::new(2, 2),
21486 Point::new(3, 0)..Point::new(3, 3),
21487 ];
21488
21489 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21490 let editor_1 = workspace
21491 .update_in(cx, |workspace, window, cx| {
21492 workspace.open_path(
21493 (worktree_id, "main.rs"),
21494 Some(pane_1.downgrade()),
21495 true,
21496 window,
21497 cx,
21498 )
21499 })
21500 .unwrap()
21501 .await
21502 .downcast::<Editor>()
21503 .unwrap();
21504 pane_1.update(cx, |pane, cx| {
21505 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21506 open_editor.update(cx, |editor, cx| {
21507 assert_eq!(
21508 editor.display_text(cx),
21509 main_text,
21510 "Original main.rs text on initial open",
21511 );
21512 assert_eq!(
21513 editor
21514 .selections
21515 .all::<Point>(cx)
21516 .into_iter()
21517 .map(|s| s.range())
21518 .collect::<Vec<_>>(),
21519 vec![Point::zero()..Point::zero()],
21520 "Default selections on initial open",
21521 );
21522 })
21523 });
21524 editor_1.update_in(cx, |editor, window, cx| {
21525 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21526 s.select_ranges(expected_ranges.clone());
21527 });
21528 });
21529
21530 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
21531 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
21532 });
21533 let editor_2 = workspace
21534 .update_in(cx, |workspace, window, cx| {
21535 workspace.open_path(
21536 (worktree_id, "main.rs"),
21537 Some(pane_2.downgrade()),
21538 true,
21539 window,
21540 cx,
21541 )
21542 })
21543 .unwrap()
21544 .await
21545 .downcast::<Editor>()
21546 .unwrap();
21547 pane_2.update(cx, |pane, cx| {
21548 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21549 open_editor.update(cx, |editor, cx| {
21550 assert_eq!(
21551 editor.display_text(cx),
21552 main_text,
21553 "Original main.rs text on initial open in another panel",
21554 );
21555 assert_eq!(
21556 editor
21557 .selections
21558 .all::<Point>(cx)
21559 .into_iter()
21560 .map(|s| s.range())
21561 .collect::<Vec<_>>(),
21562 vec![Point::zero()..Point::zero()],
21563 "Default selections on initial open in another panel",
21564 );
21565 })
21566 });
21567
21568 editor_2.update_in(cx, |editor, window, cx| {
21569 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
21570 });
21571
21572 let _other_editor_1 = workspace
21573 .update_in(cx, |workspace, window, cx| {
21574 workspace.open_path(
21575 (worktree_id, "lib.rs"),
21576 Some(pane_1.downgrade()),
21577 true,
21578 window,
21579 cx,
21580 )
21581 })
21582 .unwrap()
21583 .await
21584 .downcast::<Editor>()
21585 .unwrap();
21586 pane_1
21587 .update_in(cx, |pane, window, cx| {
21588 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
21589 })
21590 .await
21591 .unwrap();
21592 drop(editor_1);
21593 pane_1.update(cx, |pane, cx| {
21594 pane.active_item()
21595 .unwrap()
21596 .downcast::<Editor>()
21597 .unwrap()
21598 .update(cx, |editor, cx| {
21599 assert_eq!(
21600 editor.display_text(cx),
21601 lib_text,
21602 "Other file should be open and active",
21603 );
21604 });
21605 assert_eq!(pane.items().count(), 1, "No other editors should be open");
21606 });
21607
21608 let _other_editor_2 = workspace
21609 .update_in(cx, |workspace, window, cx| {
21610 workspace.open_path(
21611 (worktree_id, "lib.rs"),
21612 Some(pane_2.downgrade()),
21613 true,
21614 window,
21615 cx,
21616 )
21617 })
21618 .unwrap()
21619 .await
21620 .downcast::<Editor>()
21621 .unwrap();
21622 pane_2
21623 .update_in(cx, |pane, window, cx| {
21624 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
21625 })
21626 .await
21627 .unwrap();
21628 drop(editor_2);
21629 pane_2.update(cx, |pane, cx| {
21630 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21631 open_editor.update(cx, |editor, cx| {
21632 assert_eq!(
21633 editor.display_text(cx),
21634 lib_text,
21635 "Other file should be open and active in another panel too",
21636 );
21637 });
21638 assert_eq!(
21639 pane.items().count(),
21640 1,
21641 "No other editors should be open in another pane",
21642 );
21643 });
21644
21645 let _editor_1_reopened = workspace
21646 .update_in(cx, |workspace, window, cx| {
21647 workspace.open_path(
21648 (worktree_id, "main.rs"),
21649 Some(pane_1.downgrade()),
21650 true,
21651 window,
21652 cx,
21653 )
21654 })
21655 .unwrap()
21656 .await
21657 .downcast::<Editor>()
21658 .unwrap();
21659 let _editor_2_reopened = workspace
21660 .update_in(cx, |workspace, window, cx| {
21661 workspace.open_path(
21662 (worktree_id, "main.rs"),
21663 Some(pane_2.downgrade()),
21664 true,
21665 window,
21666 cx,
21667 )
21668 })
21669 .unwrap()
21670 .await
21671 .downcast::<Editor>()
21672 .unwrap();
21673 pane_1.update(cx, |pane, cx| {
21674 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21675 open_editor.update(cx, |editor, cx| {
21676 assert_eq!(
21677 editor.display_text(cx),
21678 main_text,
21679 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
21680 );
21681 assert_eq!(
21682 editor
21683 .selections
21684 .all::<Point>(cx)
21685 .into_iter()
21686 .map(|s| s.range())
21687 .collect::<Vec<_>>(),
21688 expected_ranges,
21689 "Previous editor in the 1st panel had selections and should get them restored on reopen",
21690 );
21691 })
21692 });
21693 pane_2.update(cx, |pane, cx| {
21694 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21695 open_editor.update(cx, |editor, cx| {
21696 assert_eq!(
21697 editor.display_text(cx),
21698 r#"fn main() {
21699⋯rintln!("1");
21700⋯intln!("2");
21701⋯ntln!("3");
21702println!("4");
21703println!("5");
21704}"#,
21705 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
21706 );
21707 assert_eq!(
21708 editor
21709 .selections
21710 .all::<Point>(cx)
21711 .into_iter()
21712 .map(|s| s.range())
21713 .collect::<Vec<_>>(),
21714 vec![Point::zero()..Point::zero()],
21715 "Previous editor in the 2nd pane had no selections changed hence should restore none",
21716 );
21717 })
21718 });
21719}
21720
21721#[gpui::test]
21722async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
21723 init_test(cx, |_| {});
21724
21725 let fs = FakeFs::new(cx.executor());
21726 let main_text = r#"fn main() {
21727println!("1");
21728println!("2");
21729println!("3");
21730println!("4");
21731println!("5");
21732}"#;
21733 let lib_text = "mod foo {}";
21734 fs.insert_tree(
21735 path!("/a"),
21736 json!({
21737 "lib.rs": lib_text,
21738 "main.rs": main_text,
21739 }),
21740 )
21741 .await;
21742
21743 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21744 let (workspace, cx) =
21745 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21746 let worktree_id = workspace.update(cx, |workspace, cx| {
21747 workspace.project().update(cx, |project, cx| {
21748 project.worktrees(cx).next().unwrap().read(cx).id()
21749 })
21750 });
21751
21752 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21753 let editor = workspace
21754 .update_in(cx, |workspace, window, cx| {
21755 workspace.open_path(
21756 (worktree_id, "main.rs"),
21757 Some(pane.downgrade()),
21758 true,
21759 window,
21760 cx,
21761 )
21762 })
21763 .unwrap()
21764 .await
21765 .downcast::<Editor>()
21766 .unwrap();
21767 pane.update(cx, |pane, cx| {
21768 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21769 open_editor.update(cx, |editor, cx| {
21770 assert_eq!(
21771 editor.display_text(cx),
21772 main_text,
21773 "Original main.rs text on initial open",
21774 );
21775 })
21776 });
21777 editor.update_in(cx, |editor, window, cx| {
21778 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
21779 });
21780
21781 cx.update_global(|store: &mut SettingsStore, cx| {
21782 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21783 s.restore_on_file_reopen = Some(false);
21784 });
21785 });
21786 editor.update_in(cx, |editor, window, cx| {
21787 editor.fold_ranges(
21788 vec![
21789 Point::new(1, 0)..Point::new(1, 1),
21790 Point::new(2, 0)..Point::new(2, 2),
21791 Point::new(3, 0)..Point::new(3, 3),
21792 ],
21793 false,
21794 window,
21795 cx,
21796 );
21797 });
21798 pane.update_in(cx, |pane, window, cx| {
21799 pane.close_all_items(&CloseAllItems::default(), window, cx)
21800 })
21801 .await
21802 .unwrap();
21803 pane.update(cx, |pane, _| {
21804 assert!(pane.active_item().is_none());
21805 });
21806 cx.update_global(|store: &mut SettingsStore, cx| {
21807 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21808 s.restore_on_file_reopen = Some(true);
21809 });
21810 });
21811
21812 let _editor_reopened = workspace
21813 .update_in(cx, |workspace, window, cx| {
21814 workspace.open_path(
21815 (worktree_id, "main.rs"),
21816 Some(pane.downgrade()),
21817 true,
21818 window,
21819 cx,
21820 )
21821 })
21822 .unwrap()
21823 .await
21824 .downcast::<Editor>()
21825 .unwrap();
21826 pane.update(cx, |pane, cx| {
21827 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21828 open_editor.update(cx, |editor, cx| {
21829 assert_eq!(
21830 editor.display_text(cx),
21831 main_text,
21832 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
21833 );
21834 })
21835 });
21836}
21837
21838#[gpui::test]
21839async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
21840 struct EmptyModalView {
21841 focus_handle: gpui::FocusHandle,
21842 }
21843 impl EventEmitter<DismissEvent> for EmptyModalView {}
21844 impl Render for EmptyModalView {
21845 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
21846 div()
21847 }
21848 }
21849 impl Focusable for EmptyModalView {
21850 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
21851 self.focus_handle.clone()
21852 }
21853 }
21854 impl workspace::ModalView for EmptyModalView {}
21855 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
21856 EmptyModalView {
21857 focus_handle: cx.focus_handle(),
21858 }
21859 }
21860
21861 init_test(cx, |_| {});
21862
21863 let fs = FakeFs::new(cx.executor());
21864 let project = Project::test(fs, [], cx).await;
21865 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21866 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
21867 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21868 let editor = cx.new_window_entity(|window, cx| {
21869 Editor::new(
21870 EditorMode::full(),
21871 buffer,
21872 Some(project.clone()),
21873 window,
21874 cx,
21875 )
21876 });
21877 workspace
21878 .update(cx, |workspace, window, cx| {
21879 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
21880 })
21881 .unwrap();
21882 editor.update_in(cx, |editor, window, cx| {
21883 editor.open_context_menu(&OpenContextMenu, window, cx);
21884 assert!(editor.mouse_context_menu.is_some());
21885 });
21886 workspace
21887 .update(cx, |workspace, window, cx| {
21888 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
21889 })
21890 .unwrap();
21891 cx.read(|cx| {
21892 assert!(editor.read(cx).mouse_context_menu.is_none());
21893 });
21894}
21895
21896#[gpui::test]
21897async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
21898 init_test(cx, |_| {});
21899
21900 let fs = FakeFs::new(cx.executor());
21901 fs.insert_file(path!("/file.html"), Default::default())
21902 .await;
21903
21904 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
21905
21906 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21907 let html_language = Arc::new(Language::new(
21908 LanguageConfig {
21909 name: "HTML".into(),
21910 matcher: LanguageMatcher {
21911 path_suffixes: vec!["html".to_string()],
21912 ..LanguageMatcher::default()
21913 },
21914 brackets: BracketPairConfig {
21915 pairs: vec![BracketPair {
21916 start: "<".into(),
21917 end: ">".into(),
21918 close: true,
21919 ..Default::default()
21920 }],
21921 ..Default::default()
21922 },
21923 ..Default::default()
21924 },
21925 Some(tree_sitter_html::LANGUAGE.into()),
21926 ));
21927 language_registry.add(html_language);
21928 let mut fake_servers = language_registry.register_fake_lsp(
21929 "HTML",
21930 FakeLspAdapter {
21931 capabilities: lsp::ServerCapabilities {
21932 completion_provider: Some(lsp::CompletionOptions {
21933 resolve_provider: Some(true),
21934 ..Default::default()
21935 }),
21936 ..Default::default()
21937 },
21938 ..Default::default()
21939 },
21940 );
21941
21942 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21943 let cx = &mut VisualTestContext::from_window(*workspace, cx);
21944
21945 let worktree_id = workspace
21946 .update(cx, |workspace, _window, cx| {
21947 workspace.project().update(cx, |project, cx| {
21948 project.worktrees(cx).next().unwrap().read(cx).id()
21949 })
21950 })
21951 .unwrap();
21952 project
21953 .update(cx, |project, cx| {
21954 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
21955 })
21956 .await
21957 .unwrap();
21958 let editor = workspace
21959 .update(cx, |workspace, window, cx| {
21960 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
21961 })
21962 .unwrap()
21963 .await
21964 .unwrap()
21965 .downcast::<Editor>()
21966 .unwrap();
21967
21968 let fake_server = fake_servers.next().await.unwrap();
21969 editor.update_in(cx, |editor, window, cx| {
21970 editor.set_text("<ad></ad>", window, cx);
21971 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21972 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
21973 });
21974 let Some((buffer, _)) = editor
21975 .buffer
21976 .read(cx)
21977 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
21978 else {
21979 panic!("Failed to get buffer for selection position");
21980 };
21981 let buffer = buffer.read(cx);
21982 let buffer_id = buffer.remote_id();
21983 let opening_range =
21984 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
21985 let closing_range =
21986 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
21987 let mut linked_ranges = HashMap::default();
21988 linked_ranges.insert(
21989 buffer_id,
21990 vec![(opening_range.clone(), vec![closing_range.clone()])],
21991 );
21992 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
21993 });
21994 let mut completion_handle =
21995 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
21996 Ok(Some(lsp::CompletionResponse::Array(vec![
21997 lsp::CompletionItem {
21998 label: "head".to_string(),
21999 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22000 lsp::InsertReplaceEdit {
22001 new_text: "head".to_string(),
22002 insert: lsp::Range::new(
22003 lsp::Position::new(0, 1),
22004 lsp::Position::new(0, 3),
22005 ),
22006 replace: lsp::Range::new(
22007 lsp::Position::new(0, 1),
22008 lsp::Position::new(0, 3),
22009 ),
22010 },
22011 )),
22012 ..Default::default()
22013 },
22014 ])))
22015 });
22016 editor.update_in(cx, |editor, window, cx| {
22017 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
22018 });
22019 cx.run_until_parked();
22020 completion_handle.next().await.unwrap();
22021 editor.update(cx, |editor, _| {
22022 assert!(
22023 editor.context_menu_visible(),
22024 "Completion menu should be visible"
22025 );
22026 });
22027 editor.update_in(cx, |editor, window, cx| {
22028 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
22029 });
22030 cx.executor().run_until_parked();
22031 editor.update(cx, |editor, cx| {
22032 assert_eq!(editor.text(cx), "<head></head>");
22033 });
22034}
22035
22036#[gpui::test]
22037async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
22038 init_test(cx, |_| {});
22039
22040 let fs = FakeFs::new(cx.executor());
22041 fs.insert_tree(
22042 path!("/root"),
22043 json!({
22044 "a": {
22045 "main.rs": "fn main() {}",
22046 },
22047 "foo": {
22048 "bar": {
22049 "external_file.rs": "pub mod external {}",
22050 }
22051 }
22052 }),
22053 )
22054 .await;
22055
22056 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
22057 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22058 language_registry.add(rust_lang());
22059 let _fake_servers = language_registry.register_fake_lsp(
22060 "Rust",
22061 FakeLspAdapter {
22062 ..FakeLspAdapter::default()
22063 },
22064 );
22065 let (workspace, cx) =
22066 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22067 let worktree_id = workspace.update(cx, |workspace, cx| {
22068 workspace.project().update(cx, |project, cx| {
22069 project.worktrees(cx).next().unwrap().read(cx).id()
22070 })
22071 });
22072
22073 let assert_language_servers_count =
22074 |expected: usize, context: &str, cx: &mut VisualTestContext| {
22075 project.update(cx, |project, cx| {
22076 let current = project
22077 .lsp_store()
22078 .read(cx)
22079 .as_local()
22080 .unwrap()
22081 .language_servers
22082 .len();
22083 assert_eq!(expected, current, "{context}");
22084 });
22085 };
22086
22087 assert_language_servers_count(
22088 0,
22089 "No servers should be running before any file is open",
22090 cx,
22091 );
22092 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22093 let main_editor = workspace
22094 .update_in(cx, |workspace, window, cx| {
22095 workspace.open_path(
22096 (worktree_id, "main.rs"),
22097 Some(pane.downgrade()),
22098 true,
22099 window,
22100 cx,
22101 )
22102 })
22103 .unwrap()
22104 .await
22105 .downcast::<Editor>()
22106 .unwrap();
22107 pane.update(cx, |pane, cx| {
22108 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22109 open_editor.update(cx, |editor, cx| {
22110 assert_eq!(
22111 editor.display_text(cx),
22112 "fn main() {}",
22113 "Original main.rs text on initial open",
22114 );
22115 });
22116 assert_eq!(open_editor, main_editor);
22117 });
22118 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
22119
22120 let external_editor = workspace
22121 .update_in(cx, |workspace, window, cx| {
22122 workspace.open_abs_path(
22123 PathBuf::from("/root/foo/bar/external_file.rs"),
22124 OpenOptions::default(),
22125 window,
22126 cx,
22127 )
22128 })
22129 .await
22130 .expect("opening external file")
22131 .downcast::<Editor>()
22132 .expect("downcasted external file's open element to editor");
22133 pane.update(cx, |pane, cx| {
22134 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22135 open_editor.update(cx, |editor, cx| {
22136 assert_eq!(
22137 editor.display_text(cx),
22138 "pub mod external {}",
22139 "External file is open now",
22140 );
22141 });
22142 assert_eq!(open_editor, external_editor);
22143 });
22144 assert_language_servers_count(
22145 1,
22146 "Second, external, *.rs file should join the existing server",
22147 cx,
22148 );
22149
22150 pane.update_in(cx, |pane, window, cx| {
22151 pane.close_active_item(&CloseActiveItem::default(), window, cx)
22152 })
22153 .await
22154 .unwrap();
22155 pane.update_in(cx, |pane, window, cx| {
22156 pane.navigate_backward(window, cx);
22157 });
22158 cx.run_until_parked();
22159 pane.update(cx, |pane, cx| {
22160 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22161 open_editor.update(cx, |editor, cx| {
22162 assert_eq!(
22163 editor.display_text(cx),
22164 "pub mod external {}",
22165 "External file is open now",
22166 );
22167 });
22168 });
22169 assert_language_servers_count(
22170 1,
22171 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
22172 cx,
22173 );
22174
22175 cx.update(|_, cx| {
22176 workspace::reload(&workspace::Reload::default(), cx);
22177 });
22178 assert_language_servers_count(
22179 1,
22180 "After reloading the worktree with local and external files opened, only one project should be started",
22181 cx,
22182 );
22183}
22184
22185#[gpui::test]
22186async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
22187 init_test(cx, |_| {});
22188
22189 let mut cx = EditorTestContext::new(cx).await;
22190 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22191 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22192
22193 // test cursor move to start of each line on tab
22194 // for `if`, `elif`, `else`, `while`, `with` and `for`
22195 cx.set_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 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22212 cx.assert_editor_state(indoc! {"
22213 def main():
22214 ˇfor item in items:
22215 ˇwhile item.active:
22216 ˇif item.value > 10:
22217 ˇcontinue
22218 ˇelif item.value < 0:
22219 ˇbreak
22220 ˇelse:
22221 ˇwith item.context() as ctx:
22222 ˇyield count
22223 ˇelse:
22224 ˇlog('while else')
22225 ˇelse:
22226 ˇlog('for else')
22227 "});
22228 // test relative indent is preserved when tab
22229 // for `if`, `elif`, `else`, `while`, `with` and `for`
22230 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22231 cx.assert_editor_state(indoc! {"
22232 def main():
22233 ˇfor item in items:
22234 ˇwhile item.active:
22235 ˇif item.value > 10:
22236 ˇcontinue
22237 ˇelif item.value < 0:
22238 ˇbreak
22239 ˇelse:
22240 ˇwith item.context() as ctx:
22241 ˇyield count
22242 ˇelse:
22243 ˇlog('while else')
22244 ˇelse:
22245 ˇlog('for else')
22246 "});
22247
22248 // test cursor move to start of each line on tab
22249 // for `try`, `except`, `else`, `finally`, `match` and `def`
22250 cx.set_state(indoc! {"
22251 def main():
22252 ˇ try:
22253 ˇ fetch()
22254 ˇ except ValueError:
22255 ˇ handle_error()
22256 ˇ else:
22257 ˇ match value:
22258 ˇ case _:
22259 ˇ finally:
22260 ˇ def status():
22261 ˇ return 0
22262 "});
22263 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22264 cx.assert_editor_state(indoc! {"
22265 def main():
22266 ˇtry:
22267 ˇfetch()
22268 ˇexcept ValueError:
22269 ˇhandle_error()
22270 ˇelse:
22271 ˇmatch value:
22272 ˇcase _:
22273 ˇfinally:
22274 ˇdef status():
22275 ˇreturn 0
22276 "});
22277 // test relative indent is preserved when tab
22278 // for `try`, `except`, `else`, `finally`, `match` and `def`
22279 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22280 cx.assert_editor_state(indoc! {"
22281 def main():
22282 ˇtry:
22283 ˇfetch()
22284 ˇexcept ValueError:
22285 ˇhandle_error()
22286 ˇelse:
22287 ˇmatch value:
22288 ˇcase _:
22289 ˇfinally:
22290 ˇdef status():
22291 ˇreturn 0
22292 "});
22293}
22294
22295#[gpui::test]
22296async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
22297 init_test(cx, |_| {});
22298
22299 let mut cx = EditorTestContext::new(cx).await;
22300 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22301 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22302
22303 // test `else` auto outdents when typed inside `if` block
22304 cx.set_state(indoc! {"
22305 def main():
22306 if i == 2:
22307 return
22308 ˇ
22309 "});
22310 cx.update_editor(|editor, window, cx| {
22311 editor.handle_input("else:", window, cx);
22312 });
22313 cx.assert_editor_state(indoc! {"
22314 def main():
22315 if i == 2:
22316 return
22317 else:ˇ
22318 "});
22319
22320 // test `except` auto outdents when typed inside `try` block
22321 cx.set_state(indoc! {"
22322 def main():
22323 try:
22324 i = 2
22325 ˇ
22326 "});
22327 cx.update_editor(|editor, window, cx| {
22328 editor.handle_input("except:", window, cx);
22329 });
22330 cx.assert_editor_state(indoc! {"
22331 def main():
22332 try:
22333 i = 2
22334 except:ˇ
22335 "});
22336
22337 // test `else` auto outdents when typed inside `except` block
22338 cx.set_state(indoc! {"
22339 def main():
22340 try:
22341 i = 2
22342 except:
22343 j = 2
22344 ˇ
22345 "});
22346 cx.update_editor(|editor, window, cx| {
22347 editor.handle_input("else:", window, cx);
22348 });
22349 cx.assert_editor_state(indoc! {"
22350 def main():
22351 try:
22352 i = 2
22353 except:
22354 j = 2
22355 else:ˇ
22356 "});
22357
22358 // test `finally` auto outdents when typed inside `else` block
22359 cx.set_state(indoc! {"
22360 def main():
22361 try:
22362 i = 2
22363 except:
22364 j = 2
22365 else:
22366 k = 2
22367 ˇ
22368 "});
22369 cx.update_editor(|editor, window, cx| {
22370 editor.handle_input("finally:", window, cx);
22371 });
22372 cx.assert_editor_state(indoc! {"
22373 def main():
22374 try:
22375 i = 2
22376 except:
22377 j = 2
22378 else:
22379 k = 2
22380 finally:ˇ
22381 "});
22382
22383 // test `else` does not outdents when typed inside `except` block right after for block
22384 cx.set_state(indoc! {"
22385 def main():
22386 try:
22387 i = 2
22388 except:
22389 for i in range(n):
22390 pass
22391 ˇ
22392 "});
22393 cx.update_editor(|editor, window, cx| {
22394 editor.handle_input("else:", window, cx);
22395 });
22396 cx.assert_editor_state(indoc! {"
22397 def main():
22398 try:
22399 i = 2
22400 except:
22401 for i in range(n):
22402 pass
22403 else:ˇ
22404 "});
22405
22406 // test `finally` auto outdents when typed inside `else` block right after for block
22407 cx.set_state(indoc! {"
22408 def main():
22409 try:
22410 i = 2
22411 except:
22412 j = 2
22413 else:
22414 for i in range(n):
22415 pass
22416 ˇ
22417 "});
22418 cx.update_editor(|editor, window, cx| {
22419 editor.handle_input("finally:", window, cx);
22420 });
22421 cx.assert_editor_state(indoc! {"
22422 def main():
22423 try:
22424 i = 2
22425 except:
22426 j = 2
22427 else:
22428 for i in range(n):
22429 pass
22430 finally:ˇ
22431 "});
22432
22433 // test `except` outdents to inner "try" block
22434 cx.set_state(indoc! {"
22435 def main():
22436 try:
22437 i = 2
22438 if i == 2:
22439 try:
22440 i = 3
22441 ˇ
22442 "});
22443 cx.update_editor(|editor, window, cx| {
22444 editor.handle_input("except:", window, cx);
22445 });
22446 cx.assert_editor_state(indoc! {"
22447 def main():
22448 try:
22449 i = 2
22450 if i == 2:
22451 try:
22452 i = 3
22453 except:ˇ
22454 "});
22455
22456 // test `except` outdents to outer "try" block
22457 cx.set_state(indoc! {"
22458 def main():
22459 try:
22460 i = 2
22461 if i == 2:
22462 try:
22463 i = 3
22464 ˇ
22465 "});
22466 cx.update_editor(|editor, window, cx| {
22467 editor.handle_input("except:", window, cx);
22468 });
22469 cx.assert_editor_state(indoc! {"
22470 def main():
22471 try:
22472 i = 2
22473 if i == 2:
22474 try:
22475 i = 3
22476 except:ˇ
22477 "});
22478
22479 // test `else` stays at correct indent when typed after `for` block
22480 cx.set_state(indoc! {"
22481 def main():
22482 for i in range(10):
22483 if i == 3:
22484 break
22485 ˇ
22486 "});
22487 cx.update_editor(|editor, window, cx| {
22488 editor.handle_input("else:", window, cx);
22489 });
22490 cx.assert_editor_state(indoc! {"
22491 def main():
22492 for i in range(10):
22493 if i == 3:
22494 break
22495 else:ˇ
22496 "});
22497
22498 // test does not outdent on typing after line with square brackets
22499 cx.set_state(indoc! {"
22500 def f() -> list[str]:
22501 ˇ
22502 "});
22503 cx.update_editor(|editor, window, cx| {
22504 editor.handle_input("a", window, cx);
22505 });
22506 cx.assert_editor_state(indoc! {"
22507 def f() -> list[str]:
22508 aˇ
22509 "});
22510
22511 // test does not outdent on typing : after case keyword
22512 cx.set_state(indoc! {"
22513 match 1:
22514 caseˇ
22515 "});
22516 cx.update_editor(|editor, window, cx| {
22517 editor.handle_input(":", window, cx);
22518 });
22519 cx.assert_editor_state(indoc! {"
22520 match 1:
22521 case:ˇ
22522 "});
22523}
22524
22525#[gpui::test]
22526async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
22527 init_test(cx, |_| {});
22528 update_test_language_settings(cx, |settings| {
22529 settings.defaults.extend_comment_on_newline = Some(false);
22530 });
22531 let mut cx = EditorTestContext::new(cx).await;
22532 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22533 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22534
22535 // test correct indent after newline on comment
22536 cx.set_state(indoc! {"
22537 # COMMENT:ˇ
22538 "});
22539 cx.update_editor(|editor, window, cx| {
22540 editor.newline(&Newline, window, cx);
22541 });
22542 cx.assert_editor_state(indoc! {"
22543 # COMMENT:
22544 ˇ
22545 "});
22546
22547 // test correct indent after newline in brackets
22548 cx.set_state(indoc! {"
22549 {ˇ}
22550 "});
22551 cx.update_editor(|editor, window, cx| {
22552 editor.newline(&Newline, window, cx);
22553 });
22554 cx.run_until_parked();
22555 cx.assert_editor_state(indoc! {"
22556 {
22557 ˇ
22558 }
22559 "});
22560
22561 cx.set_state(indoc! {"
22562 (ˇ)
22563 "});
22564 cx.update_editor(|editor, window, cx| {
22565 editor.newline(&Newline, window, cx);
22566 });
22567 cx.run_until_parked();
22568 cx.assert_editor_state(indoc! {"
22569 (
22570 ˇ
22571 )
22572 "});
22573
22574 // do not indent after empty lists or dictionaries
22575 cx.set_state(indoc! {"
22576 a = []ˇ
22577 "});
22578 cx.update_editor(|editor, window, cx| {
22579 editor.newline(&Newline, window, cx);
22580 });
22581 cx.run_until_parked();
22582 cx.assert_editor_state(indoc! {"
22583 a = []
22584 ˇ
22585 "});
22586}
22587
22588#[gpui::test]
22589async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
22590 init_test(cx, |_| {});
22591
22592 let mut cx = EditorTestContext::new(cx).await;
22593 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
22594 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22595
22596 // test cursor move to start of each line on tab
22597 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
22598 cx.set_state(indoc! {"
22599 function main() {
22600 ˇ for item in $items; do
22601 ˇ while [ -n \"$item\" ]; do
22602 ˇ if [ \"$value\" -gt 10 ]; then
22603 ˇ continue
22604 ˇ elif [ \"$value\" -lt 0 ]; then
22605 ˇ break
22606 ˇ else
22607 ˇ echo \"$item\"
22608 ˇ fi
22609 ˇ done
22610 ˇ done
22611 ˇ}
22612 "});
22613 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22614 cx.assert_editor_state(indoc! {"
22615 function main() {
22616 ˇfor item in $items; do
22617 ˇwhile [ -n \"$item\" ]; do
22618 ˇif [ \"$value\" -gt 10 ]; then
22619 ˇcontinue
22620 ˇelif [ \"$value\" -lt 0 ]; then
22621 ˇbreak
22622 ˇelse
22623 ˇecho \"$item\"
22624 ˇfi
22625 ˇdone
22626 ˇdone
22627 ˇ}
22628 "});
22629 // test relative indent is preserved when tab
22630 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22631 cx.assert_editor_state(indoc! {"
22632 function main() {
22633 ˇfor item in $items; do
22634 ˇwhile [ -n \"$item\" ]; do
22635 ˇif [ \"$value\" -gt 10 ]; then
22636 ˇcontinue
22637 ˇelif [ \"$value\" -lt 0 ]; then
22638 ˇbreak
22639 ˇelse
22640 ˇecho \"$item\"
22641 ˇfi
22642 ˇdone
22643 ˇdone
22644 ˇ}
22645 "});
22646
22647 // test cursor move to start of each line on tab
22648 // for `case` statement with patterns
22649 cx.set_state(indoc! {"
22650 function handle() {
22651 ˇ case \"$1\" in
22652 ˇ start)
22653 ˇ echo \"a\"
22654 ˇ ;;
22655 ˇ stop)
22656 ˇ echo \"b\"
22657 ˇ ;;
22658 ˇ *)
22659 ˇ echo \"c\"
22660 ˇ ;;
22661 ˇ esac
22662 ˇ}
22663 "});
22664 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22665 cx.assert_editor_state(indoc! {"
22666 function handle() {
22667 ˇcase \"$1\" in
22668 ˇstart)
22669 ˇecho \"a\"
22670 ˇ;;
22671 ˇstop)
22672 ˇecho \"b\"
22673 ˇ;;
22674 ˇ*)
22675 ˇecho \"c\"
22676 ˇ;;
22677 ˇesac
22678 ˇ}
22679 "});
22680}
22681
22682#[gpui::test]
22683async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
22684 init_test(cx, |_| {});
22685
22686 let mut cx = EditorTestContext::new(cx).await;
22687 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
22688 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22689
22690 // test indents on comment insert
22691 cx.set_state(indoc! {"
22692 function main() {
22693 ˇ for item in $items; do
22694 ˇ while [ -n \"$item\" ]; do
22695 ˇ if [ \"$value\" -gt 10 ]; then
22696 ˇ continue
22697 ˇ elif [ \"$value\" -lt 0 ]; then
22698 ˇ break
22699 ˇ else
22700 ˇ echo \"$item\"
22701 ˇ fi
22702 ˇ done
22703 ˇ done
22704 ˇ}
22705 "});
22706 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
22707 cx.assert_editor_state(indoc! {"
22708 function main() {
22709 #ˇ for item in $items; do
22710 #ˇ while [ -n \"$item\" ]; do
22711 #ˇ if [ \"$value\" -gt 10 ]; then
22712 #ˇ continue
22713 #ˇ elif [ \"$value\" -lt 0 ]; then
22714 #ˇ break
22715 #ˇ else
22716 #ˇ echo \"$item\"
22717 #ˇ fi
22718 #ˇ done
22719 #ˇ done
22720 #ˇ}
22721 "});
22722}
22723
22724#[gpui::test]
22725async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
22726 init_test(cx, |_| {});
22727
22728 let mut cx = EditorTestContext::new(cx).await;
22729 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
22730 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22731
22732 // test `else` auto outdents when typed inside `if` block
22733 cx.set_state(indoc! {"
22734 if [ \"$1\" = \"test\" ]; then
22735 echo \"foo bar\"
22736 ˇ
22737 "});
22738 cx.update_editor(|editor, window, cx| {
22739 editor.handle_input("else", window, cx);
22740 });
22741 cx.assert_editor_state(indoc! {"
22742 if [ \"$1\" = \"test\" ]; then
22743 echo \"foo bar\"
22744 elseˇ
22745 "});
22746
22747 // test `elif` auto outdents when typed inside `if` block
22748 cx.set_state(indoc! {"
22749 if [ \"$1\" = \"test\" ]; then
22750 echo \"foo bar\"
22751 ˇ
22752 "});
22753 cx.update_editor(|editor, window, cx| {
22754 editor.handle_input("elif", window, cx);
22755 });
22756 cx.assert_editor_state(indoc! {"
22757 if [ \"$1\" = \"test\" ]; then
22758 echo \"foo bar\"
22759 elifˇ
22760 "});
22761
22762 // test `fi` auto outdents when typed inside `else` block
22763 cx.set_state(indoc! {"
22764 if [ \"$1\" = \"test\" ]; then
22765 echo \"foo bar\"
22766 else
22767 echo \"bar baz\"
22768 ˇ
22769 "});
22770 cx.update_editor(|editor, window, cx| {
22771 editor.handle_input("fi", window, cx);
22772 });
22773 cx.assert_editor_state(indoc! {"
22774 if [ \"$1\" = \"test\" ]; then
22775 echo \"foo bar\"
22776 else
22777 echo \"bar baz\"
22778 fiˇ
22779 "});
22780
22781 // test `done` auto outdents when typed inside `while` block
22782 cx.set_state(indoc! {"
22783 while read line; do
22784 echo \"$line\"
22785 ˇ
22786 "});
22787 cx.update_editor(|editor, window, cx| {
22788 editor.handle_input("done", window, cx);
22789 });
22790 cx.assert_editor_state(indoc! {"
22791 while read line; do
22792 echo \"$line\"
22793 doneˇ
22794 "});
22795
22796 // test `done` auto outdents when typed inside `for` block
22797 cx.set_state(indoc! {"
22798 for file in *.txt; do
22799 cat \"$file\"
22800 ˇ
22801 "});
22802 cx.update_editor(|editor, window, cx| {
22803 editor.handle_input("done", window, cx);
22804 });
22805 cx.assert_editor_state(indoc! {"
22806 for file in *.txt; do
22807 cat \"$file\"
22808 doneˇ
22809 "});
22810
22811 // test `esac` auto outdents when typed inside `case` block
22812 cx.set_state(indoc! {"
22813 case \"$1\" in
22814 start)
22815 echo \"foo bar\"
22816 ;;
22817 stop)
22818 echo \"bar baz\"
22819 ;;
22820 ˇ
22821 "});
22822 cx.update_editor(|editor, window, cx| {
22823 editor.handle_input("esac", window, cx);
22824 });
22825 cx.assert_editor_state(indoc! {"
22826 case \"$1\" in
22827 start)
22828 echo \"foo bar\"
22829 ;;
22830 stop)
22831 echo \"bar baz\"
22832 ;;
22833 esacˇ
22834 "});
22835
22836 // test `*)` auto outdents when typed inside `case` block
22837 cx.set_state(indoc! {"
22838 case \"$1\" in
22839 start)
22840 echo \"foo bar\"
22841 ;;
22842 ˇ
22843 "});
22844 cx.update_editor(|editor, window, cx| {
22845 editor.handle_input("*)", window, cx);
22846 });
22847 cx.assert_editor_state(indoc! {"
22848 case \"$1\" in
22849 start)
22850 echo \"foo bar\"
22851 ;;
22852 *)ˇ
22853 "});
22854
22855 // test `fi` outdents to correct level with nested if blocks
22856 cx.set_state(indoc! {"
22857 if [ \"$1\" = \"test\" ]; then
22858 echo \"outer if\"
22859 if [ \"$2\" = \"debug\" ]; then
22860 echo \"inner if\"
22861 ˇ
22862 "});
22863 cx.update_editor(|editor, window, cx| {
22864 editor.handle_input("fi", window, cx);
22865 });
22866 cx.assert_editor_state(indoc! {"
22867 if [ \"$1\" = \"test\" ]; then
22868 echo \"outer if\"
22869 if [ \"$2\" = \"debug\" ]; then
22870 echo \"inner if\"
22871 fiˇ
22872 "});
22873}
22874
22875#[gpui::test]
22876async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
22877 init_test(cx, |_| {});
22878 update_test_language_settings(cx, |settings| {
22879 settings.defaults.extend_comment_on_newline = Some(false);
22880 });
22881 let mut cx = EditorTestContext::new(cx).await;
22882 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
22883 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22884
22885 // test correct indent after newline on comment
22886 cx.set_state(indoc! {"
22887 # COMMENT:ˇ
22888 "});
22889 cx.update_editor(|editor, window, cx| {
22890 editor.newline(&Newline, window, cx);
22891 });
22892 cx.assert_editor_state(indoc! {"
22893 # COMMENT:
22894 ˇ
22895 "});
22896
22897 // test correct indent after newline after `then`
22898 cx.set_state(indoc! {"
22899
22900 if [ \"$1\" = \"test\" ]; thenˇ
22901 "});
22902 cx.update_editor(|editor, window, cx| {
22903 editor.newline(&Newline, window, cx);
22904 });
22905 cx.run_until_parked();
22906 cx.assert_editor_state(indoc! {"
22907
22908 if [ \"$1\" = \"test\" ]; then
22909 ˇ
22910 "});
22911
22912 // test correct indent after newline after `else`
22913 cx.set_state(indoc! {"
22914 if [ \"$1\" = \"test\" ]; then
22915 elseˇ
22916 "});
22917 cx.update_editor(|editor, window, cx| {
22918 editor.newline(&Newline, window, cx);
22919 });
22920 cx.run_until_parked();
22921 cx.assert_editor_state(indoc! {"
22922 if [ \"$1\" = \"test\" ]; then
22923 else
22924 ˇ
22925 "});
22926
22927 // test correct indent after newline after `elif`
22928 cx.set_state(indoc! {"
22929 if [ \"$1\" = \"test\" ]; then
22930 elifˇ
22931 "});
22932 cx.update_editor(|editor, window, cx| {
22933 editor.newline(&Newline, window, cx);
22934 });
22935 cx.run_until_parked();
22936 cx.assert_editor_state(indoc! {"
22937 if [ \"$1\" = \"test\" ]; then
22938 elif
22939 ˇ
22940 "});
22941
22942 // test correct indent after newline after `do`
22943 cx.set_state(indoc! {"
22944 for file in *.txt; doˇ
22945 "});
22946 cx.update_editor(|editor, window, cx| {
22947 editor.newline(&Newline, window, cx);
22948 });
22949 cx.run_until_parked();
22950 cx.assert_editor_state(indoc! {"
22951 for file in *.txt; do
22952 ˇ
22953 "});
22954
22955 // test correct indent after newline after case pattern
22956 cx.set_state(indoc! {"
22957 case \"$1\" in
22958 start)ˇ
22959 "});
22960 cx.update_editor(|editor, window, cx| {
22961 editor.newline(&Newline, window, cx);
22962 });
22963 cx.run_until_parked();
22964 cx.assert_editor_state(indoc! {"
22965 case \"$1\" in
22966 start)
22967 ˇ
22968 "});
22969
22970 // test correct indent after newline after case pattern
22971 cx.set_state(indoc! {"
22972 case \"$1\" in
22973 start)
22974 ;;
22975 *)ˇ
22976 "});
22977 cx.update_editor(|editor, window, cx| {
22978 editor.newline(&Newline, window, cx);
22979 });
22980 cx.run_until_parked();
22981 cx.assert_editor_state(indoc! {"
22982 case \"$1\" in
22983 start)
22984 ;;
22985 *)
22986 ˇ
22987 "});
22988
22989 // test correct indent after newline after function opening brace
22990 cx.set_state(indoc! {"
22991 function test() {ˇ}
22992 "});
22993 cx.update_editor(|editor, window, cx| {
22994 editor.newline(&Newline, window, cx);
22995 });
22996 cx.run_until_parked();
22997 cx.assert_editor_state(indoc! {"
22998 function test() {
22999 ˇ
23000 }
23001 "});
23002
23003 // test no extra indent after semicolon on same line
23004 cx.set_state(indoc! {"
23005 echo \"test\";ˇ
23006 "});
23007 cx.update_editor(|editor, window, cx| {
23008 editor.newline(&Newline, window, cx);
23009 });
23010 cx.run_until_parked();
23011 cx.assert_editor_state(indoc! {"
23012 echo \"test\";
23013 ˇ
23014 "});
23015}
23016
23017fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
23018 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
23019 point..point
23020}
23021
23022#[track_caller]
23023fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
23024 let (text, ranges) = marked_text_ranges(marked_text, true);
23025 assert_eq!(editor.text(cx), text);
23026 assert_eq!(
23027 editor.selections.ranges(cx),
23028 ranges,
23029 "Assert selections are {}",
23030 marked_text
23031 );
23032}
23033
23034pub fn handle_signature_help_request(
23035 cx: &mut EditorLspTestContext,
23036 mocked_response: lsp::SignatureHelp,
23037) -> impl Future<Output = ()> + use<> {
23038 let mut request =
23039 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
23040 let mocked_response = mocked_response.clone();
23041 async move { Ok(Some(mocked_response)) }
23042 });
23043
23044 async move {
23045 request.next().await;
23046 }
23047}
23048
23049#[track_caller]
23050pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
23051 cx.update_editor(|editor, _, _| {
23052 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
23053 let entries = menu.entries.borrow();
23054 let entries = entries
23055 .iter()
23056 .map(|entry| entry.string.as_str())
23057 .collect::<Vec<_>>();
23058 assert_eq!(entries, expected);
23059 } else {
23060 panic!("Expected completions menu");
23061 }
23062 });
23063}
23064
23065/// Handle completion request passing a marked string specifying where the completion
23066/// should be triggered from using '|' character, what range should be replaced, and what completions
23067/// should be returned using '<' and '>' to delimit the range.
23068///
23069/// Also see `handle_completion_request_with_insert_and_replace`.
23070#[track_caller]
23071pub fn handle_completion_request(
23072 marked_string: &str,
23073 completions: Vec<&'static str>,
23074 is_incomplete: bool,
23075 counter: Arc<AtomicUsize>,
23076 cx: &mut EditorLspTestContext,
23077) -> impl Future<Output = ()> {
23078 let complete_from_marker: TextRangeMarker = '|'.into();
23079 let replace_range_marker: TextRangeMarker = ('<', '>').into();
23080 let (_, mut marked_ranges) = marked_text_ranges_by(
23081 marked_string,
23082 vec![complete_from_marker.clone(), replace_range_marker.clone()],
23083 );
23084
23085 let complete_from_position =
23086 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
23087 let replace_range =
23088 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
23089
23090 let mut request =
23091 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
23092 let completions = completions.clone();
23093 counter.fetch_add(1, atomic::Ordering::Release);
23094 async move {
23095 assert_eq!(params.text_document_position.text_document.uri, url.clone());
23096 assert_eq!(
23097 params.text_document_position.position,
23098 complete_from_position
23099 );
23100 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
23101 is_incomplete: is_incomplete,
23102 item_defaults: None,
23103 items: completions
23104 .iter()
23105 .map(|completion_text| lsp::CompletionItem {
23106 label: completion_text.to_string(),
23107 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
23108 range: replace_range,
23109 new_text: completion_text.to_string(),
23110 })),
23111 ..Default::default()
23112 })
23113 .collect(),
23114 })))
23115 }
23116 });
23117
23118 async move {
23119 request.next().await;
23120 }
23121}
23122
23123/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
23124/// given instead, which also contains an `insert` range.
23125///
23126/// This function uses markers to define ranges:
23127/// - `|` marks the cursor position
23128/// - `<>` marks the replace range
23129/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
23130pub fn handle_completion_request_with_insert_and_replace(
23131 cx: &mut EditorLspTestContext,
23132 marked_string: &str,
23133 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
23134 counter: Arc<AtomicUsize>,
23135) -> impl Future<Output = ()> {
23136 let complete_from_marker: TextRangeMarker = '|'.into();
23137 let replace_range_marker: TextRangeMarker = ('<', '>').into();
23138 let insert_range_marker: TextRangeMarker = ('{', '}').into();
23139
23140 let (_, mut marked_ranges) = marked_text_ranges_by(
23141 marked_string,
23142 vec![
23143 complete_from_marker.clone(),
23144 replace_range_marker.clone(),
23145 insert_range_marker.clone(),
23146 ],
23147 );
23148
23149 let complete_from_position =
23150 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
23151 let replace_range =
23152 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
23153
23154 let insert_range = match marked_ranges.remove(&insert_range_marker) {
23155 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
23156 _ => lsp::Range {
23157 start: replace_range.start,
23158 end: complete_from_position,
23159 },
23160 };
23161
23162 let mut request =
23163 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
23164 let completions = completions.clone();
23165 counter.fetch_add(1, atomic::Ordering::Release);
23166 async move {
23167 assert_eq!(params.text_document_position.text_document.uri, url.clone());
23168 assert_eq!(
23169 params.text_document_position.position, complete_from_position,
23170 "marker `|` position doesn't match",
23171 );
23172 Ok(Some(lsp::CompletionResponse::Array(
23173 completions
23174 .iter()
23175 .map(|(label, new_text)| lsp::CompletionItem {
23176 label: label.to_string(),
23177 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
23178 lsp::InsertReplaceEdit {
23179 insert: insert_range,
23180 replace: replace_range,
23181 new_text: new_text.to_string(),
23182 },
23183 )),
23184 ..Default::default()
23185 })
23186 .collect(),
23187 )))
23188 }
23189 });
23190
23191 async move {
23192 request.next().await;
23193 }
23194}
23195
23196fn handle_resolve_completion_request(
23197 cx: &mut EditorLspTestContext,
23198 edits: Option<Vec<(&'static str, &'static str)>>,
23199) -> impl Future<Output = ()> {
23200 let edits = edits.map(|edits| {
23201 edits
23202 .iter()
23203 .map(|(marked_string, new_text)| {
23204 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
23205 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
23206 lsp::TextEdit::new(replace_range, new_text.to_string())
23207 })
23208 .collect::<Vec<_>>()
23209 });
23210
23211 let mut request =
23212 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
23213 let edits = edits.clone();
23214 async move {
23215 Ok(lsp::CompletionItem {
23216 additional_text_edits: edits,
23217 ..Default::default()
23218 })
23219 }
23220 });
23221
23222 async move {
23223 request.next().await;
23224 }
23225}
23226
23227pub(crate) fn update_test_language_settings(
23228 cx: &mut TestAppContext,
23229 f: impl Fn(&mut AllLanguageSettingsContent),
23230) {
23231 cx.update(|cx| {
23232 SettingsStore::update_global(cx, |store, cx| {
23233 store.update_user_settings::<AllLanguageSettings>(cx, f);
23234 });
23235 });
23236}
23237
23238pub(crate) fn update_test_project_settings(
23239 cx: &mut TestAppContext,
23240 f: impl Fn(&mut ProjectSettings),
23241) {
23242 cx.update(|cx| {
23243 SettingsStore::update_global(cx, |store, cx| {
23244 store.update_user_settings::<ProjectSettings>(cx, f);
23245 });
23246 });
23247}
23248
23249pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
23250 cx.update(|cx| {
23251 assets::Assets.load_test_fonts(cx);
23252 let store = SettingsStore::test(cx);
23253 cx.set_global(store);
23254 theme::init(theme::LoadThemes::JustBase, cx);
23255 release_channel::init(SemanticVersion::default(), cx);
23256 client::init_settings(cx);
23257 language::init(cx);
23258 Project::init_settings(cx);
23259 workspace::init_settings(cx);
23260 crate::init(cx);
23261 });
23262 zlog::init_test();
23263 update_test_language_settings(cx, f);
23264}
23265
23266#[track_caller]
23267fn assert_hunk_revert(
23268 not_reverted_text_with_selections: &str,
23269 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
23270 expected_reverted_text_with_selections: &str,
23271 base_text: &str,
23272 cx: &mut EditorLspTestContext,
23273) {
23274 cx.set_state(not_reverted_text_with_selections);
23275 cx.set_head_text(base_text);
23276 cx.executor().run_until_parked();
23277
23278 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
23279 let snapshot = editor.snapshot(window, cx);
23280 let reverted_hunk_statuses = snapshot
23281 .buffer_snapshot
23282 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
23283 .map(|hunk| hunk.status().kind)
23284 .collect::<Vec<_>>();
23285
23286 editor.git_restore(&Default::default(), window, cx);
23287 reverted_hunk_statuses
23288 });
23289 cx.executor().run_until_parked();
23290 cx.assert_editor_state(expected_reverted_text_with_selections);
23291 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
23292}
23293
23294#[gpui::test(iterations = 10)]
23295async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
23296 init_test(cx, |_| {});
23297
23298 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
23299 let counter = diagnostic_requests.clone();
23300
23301 let fs = FakeFs::new(cx.executor());
23302 fs.insert_tree(
23303 path!("/a"),
23304 json!({
23305 "first.rs": "fn main() { let a = 5; }",
23306 "second.rs": "// Test file",
23307 }),
23308 )
23309 .await;
23310
23311 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23312 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23313 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23314
23315 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23316 language_registry.add(rust_lang());
23317 let mut fake_servers = language_registry.register_fake_lsp(
23318 "Rust",
23319 FakeLspAdapter {
23320 capabilities: lsp::ServerCapabilities {
23321 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
23322 lsp::DiagnosticOptions {
23323 identifier: None,
23324 inter_file_dependencies: true,
23325 workspace_diagnostics: true,
23326 work_done_progress_options: Default::default(),
23327 },
23328 )),
23329 ..Default::default()
23330 },
23331 ..Default::default()
23332 },
23333 );
23334
23335 let editor = workspace
23336 .update(cx, |workspace, window, cx| {
23337 workspace.open_abs_path(
23338 PathBuf::from(path!("/a/first.rs")),
23339 OpenOptions::default(),
23340 window,
23341 cx,
23342 )
23343 })
23344 .unwrap()
23345 .await
23346 .unwrap()
23347 .downcast::<Editor>()
23348 .unwrap();
23349 let fake_server = fake_servers.next().await.unwrap();
23350 let server_id = fake_server.server.server_id();
23351 let mut first_request = fake_server
23352 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
23353 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
23354 let result_id = Some(new_result_id.to_string());
23355 assert_eq!(
23356 params.text_document.uri,
23357 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
23358 );
23359 async move {
23360 Ok(lsp::DocumentDiagnosticReportResult::Report(
23361 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
23362 related_documents: None,
23363 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
23364 items: Vec::new(),
23365 result_id,
23366 },
23367 }),
23368 ))
23369 }
23370 });
23371
23372 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
23373 project.update(cx, |project, cx| {
23374 let buffer_id = editor
23375 .read(cx)
23376 .buffer()
23377 .read(cx)
23378 .as_singleton()
23379 .expect("created a singleton buffer")
23380 .read(cx)
23381 .remote_id();
23382 let buffer_result_id = project
23383 .lsp_store()
23384 .read(cx)
23385 .result_id(server_id, buffer_id, cx);
23386 assert_eq!(expected, buffer_result_id);
23387 });
23388 };
23389
23390 ensure_result_id(None, cx);
23391 cx.executor().advance_clock(Duration::from_millis(60));
23392 cx.executor().run_until_parked();
23393 assert_eq!(
23394 diagnostic_requests.load(atomic::Ordering::Acquire),
23395 1,
23396 "Opening file should trigger diagnostic request"
23397 );
23398 first_request
23399 .next()
23400 .await
23401 .expect("should have sent the first diagnostics pull request");
23402 ensure_result_id(Some("1".to_string()), cx);
23403
23404 // Editing should trigger diagnostics
23405 editor.update_in(cx, |editor, window, cx| {
23406 editor.handle_input("2", window, cx)
23407 });
23408 cx.executor().advance_clock(Duration::from_millis(60));
23409 cx.executor().run_until_parked();
23410 assert_eq!(
23411 diagnostic_requests.load(atomic::Ordering::Acquire),
23412 2,
23413 "Editing should trigger diagnostic request"
23414 );
23415 ensure_result_id(Some("2".to_string()), cx);
23416
23417 // Moving cursor should not trigger diagnostic request
23418 editor.update_in(cx, |editor, window, cx| {
23419 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23420 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
23421 });
23422 });
23423 cx.executor().advance_clock(Duration::from_millis(60));
23424 cx.executor().run_until_parked();
23425 assert_eq!(
23426 diagnostic_requests.load(atomic::Ordering::Acquire),
23427 2,
23428 "Cursor movement should not trigger diagnostic request"
23429 );
23430 ensure_result_id(Some("2".to_string()), cx);
23431 // Multiple rapid edits should be debounced
23432 for _ in 0..5 {
23433 editor.update_in(cx, |editor, window, cx| {
23434 editor.handle_input("x", window, cx)
23435 });
23436 }
23437 cx.executor().advance_clock(Duration::from_millis(60));
23438 cx.executor().run_until_parked();
23439
23440 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
23441 assert!(
23442 final_requests <= 4,
23443 "Multiple rapid edits should be debounced (got {final_requests} requests)",
23444 );
23445 ensure_result_id(Some(final_requests.to_string()), cx);
23446}
23447
23448#[gpui::test]
23449async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
23450 // Regression test for issue #11671
23451 // Previously, adding a cursor after moving multiple cursors would reset
23452 // the cursor count instead of adding to the existing cursors.
23453 init_test(cx, |_| {});
23454 let mut cx = EditorTestContext::new(cx).await;
23455
23456 // Create a simple buffer with cursor at start
23457 cx.set_state(indoc! {"
23458 ˇaaaa
23459 bbbb
23460 cccc
23461 dddd
23462 eeee
23463 ffff
23464 gggg
23465 hhhh"});
23466
23467 // Add 2 cursors below (so we have 3 total)
23468 cx.update_editor(|editor, window, cx| {
23469 editor.add_selection_below(&Default::default(), window, cx);
23470 editor.add_selection_below(&Default::default(), window, cx);
23471 });
23472
23473 // Verify we have 3 cursors
23474 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
23475 assert_eq!(
23476 initial_count, 3,
23477 "Should have 3 cursors after adding 2 below"
23478 );
23479
23480 // Move down one line
23481 cx.update_editor(|editor, window, cx| {
23482 editor.move_down(&MoveDown, window, cx);
23483 });
23484
23485 // Add another cursor below
23486 cx.update_editor(|editor, window, cx| {
23487 editor.add_selection_below(&Default::default(), window, cx);
23488 });
23489
23490 // Should now have 4 cursors (3 original + 1 new)
23491 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
23492 assert_eq!(
23493 final_count, 4,
23494 "Should have 4 cursors after moving and adding another"
23495 );
23496}
23497
23498#[gpui::test(iterations = 10)]
23499async fn test_document_colors(cx: &mut TestAppContext) {
23500 let expected_color = Rgba {
23501 r: 0.33,
23502 g: 0.33,
23503 b: 0.33,
23504 a: 0.33,
23505 };
23506
23507 init_test(cx, |_| {});
23508
23509 let fs = FakeFs::new(cx.executor());
23510 fs.insert_tree(
23511 path!("/a"),
23512 json!({
23513 "first.rs": "fn main() { let a = 5; }",
23514 }),
23515 )
23516 .await;
23517
23518 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23519 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23520 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23521
23522 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23523 language_registry.add(rust_lang());
23524 let mut fake_servers = language_registry.register_fake_lsp(
23525 "Rust",
23526 FakeLspAdapter {
23527 capabilities: lsp::ServerCapabilities {
23528 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
23529 ..lsp::ServerCapabilities::default()
23530 },
23531 name: "rust-analyzer",
23532 ..FakeLspAdapter::default()
23533 },
23534 );
23535 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
23536 "Rust",
23537 FakeLspAdapter {
23538 capabilities: lsp::ServerCapabilities {
23539 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
23540 ..lsp::ServerCapabilities::default()
23541 },
23542 name: "not-rust-analyzer",
23543 ..FakeLspAdapter::default()
23544 },
23545 );
23546
23547 let editor = workspace
23548 .update(cx, |workspace, window, cx| {
23549 workspace.open_abs_path(
23550 PathBuf::from(path!("/a/first.rs")),
23551 OpenOptions::default(),
23552 window,
23553 cx,
23554 )
23555 })
23556 .unwrap()
23557 .await
23558 .unwrap()
23559 .downcast::<Editor>()
23560 .unwrap();
23561 let fake_language_server = fake_servers.next().await.unwrap();
23562 let fake_language_server_without_capabilities =
23563 fake_servers_without_capabilities.next().await.unwrap();
23564 let requests_made = Arc::new(AtomicUsize::new(0));
23565 let closure_requests_made = Arc::clone(&requests_made);
23566 let mut color_request_handle = fake_language_server
23567 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
23568 let requests_made = Arc::clone(&closure_requests_made);
23569 async move {
23570 assert_eq!(
23571 params.text_document.uri,
23572 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
23573 );
23574 requests_made.fetch_add(1, atomic::Ordering::Release);
23575 Ok(vec![
23576 lsp::ColorInformation {
23577 range: lsp::Range {
23578 start: lsp::Position {
23579 line: 0,
23580 character: 0,
23581 },
23582 end: lsp::Position {
23583 line: 0,
23584 character: 1,
23585 },
23586 },
23587 color: lsp::Color {
23588 red: 0.33,
23589 green: 0.33,
23590 blue: 0.33,
23591 alpha: 0.33,
23592 },
23593 },
23594 lsp::ColorInformation {
23595 range: lsp::Range {
23596 start: lsp::Position {
23597 line: 0,
23598 character: 0,
23599 },
23600 end: lsp::Position {
23601 line: 0,
23602 character: 1,
23603 },
23604 },
23605 color: lsp::Color {
23606 red: 0.33,
23607 green: 0.33,
23608 blue: 0.33,
23609 alpha: 0.33,
23610 },
23611 },
23612 ])
23613 }
23614 });
23615
23616 let _handle = fake_language_server_without_capabilities
23617 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
23618 panic!("Should not be called");
23619 });
23620 cx.executor().advance_clock(Duration::from_millis(100));
23621 color_request_handle.next().await.unwrap();
23622 cx.run_until_parked();
23623 assert_eq!(
23624 1,
23625 requests_made.load(atomic::Ordering::Acquire),
23626 "Should query for colors once per editor open"
23627 );
23628 editor.update_in(cx, |editor, _, cx| {
23629 assert_eq!(
23630 vec![expected_color],
23631 extract_color_inlays(editor, cx),
23632 "Should have an initial inlay"
23633 );
23634 });
23635
23636 // opening another file in a split should not influence the LSP query counter
23637 workspace
23638 .update(cx, |workspace, window, cx| {
23639 assert_eq!(
23640 workspace.panes().len(),
23641 1,
23642 "Should have one pane with one editor"
23643 );
23644 workspace.move_item_to_pane_in_direction(
23645 &MoveItemToPaneInDirection {
23646 direction: SplitDirection::Right,
23647 focus: false,
23648 clone: true,
23649 },
23650 window,
23651 cx,
23652 );
23653 })
23654 .unwrap();
23655 cx.run_until_parked();
23656 workspace
23657 .update(cx, |workspace, _, cx| {
23658 let panes = workspace.panes();
23659 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
23660 for pane in panes {
23661 let editor = pane
23662 .read(cx)
23663 .active_item()
23664 .and_then(|item| item.downcast::<Editor>())
23665 .expect("Should have opened an editor in each split");
23666 let editor_file = editor
23667 .read(cx)
23668 .buffer()
23669 .read(cx)
23670 .as_singleton()
23671 .expect("test deals with singleton buffers")
23672 .read(cx)
23673 .file()
23674 .expect("test buffese should have a file")
23675 .path();
23676 assert_eq!(
23677 editor_file.as_ref(),
23678 Path::new("first.rs"),
23679 "Both editors should be opened for the same file"
23680 )
23681 }
23682 })
23683 .unwrap();
23684
23685 cx.executor().advance_clock(Duration::from_millis(500));
23686 let save = editor.update_in(cx, |editor, window, cx| {
23687 editor.move_to_end(&MoveToEnd, window, cx);
23688 editor.handle_input("dirty", window, cx);
23689 editor.save(
23690 SaveOptions {
23691 format: true,
23692 autosave: true,
23693 },
23694 project.clone(),
23695 window,
23696 cx,
23697 )
23698 });
23699 save.await.unwrap();
23700
23701 color_request_handle.next().await.unwrap();
23702 cx.run_until_parked();
23703 assert_eq!(
23704 3,
23705 requests_made.load(atomic::Ordering::Acquire),
23706 "Should query for colors once per save and once per formatting after save"
23707 );
23708
23709 drop(editor);
23710 let close = workspace
23711 .update(cx, |workspace, window, cx| {
23712 workspace.active_pane().update(cx, |pane, cx| {
23713 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23714 })
23715 })
23716 .unwrap();
23717 close.await.unwrap();
23718 let close = workspace
23719 .update(cx, |workspace, window, cx| {
23720 workspace.active_pane().update(cx, |pane, cx| {
23721 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23722 })
23723 })
23724 .unwrap();
23725 close.await.unwrap();
23726 assert_eq!(
23727 3,
23728 requests_made.load(atomic::Ordering::Acquire),
23729 "After saving and closing all editors, no extra requests should be made"
23730 );
23731 workspace
23732 .update(cx, |workspace, _, cx| {
23733 assert!(
23734 workspace.active_item(cx).is_none(),
23735 "Should close all editors"
23736 )
23737 })
23738 .unwrap();
23739
23740 workspace
23741 .update(cx, |workspace, window, cx| {
23742 workspace.active_pane().update(cx, |pane, cx| {
23743 pane.navigate_backward(window, cx);
23744 })
23745 })
23746 .unwrap();
23747 cx.executor().advance_clock(Duration::from_millis(100));
23748 cx.run_until_parked();
23749 let editor = workspace
23750 .update(cx, |workspace, _, cx| {
23751 workspace
23752 .active_item(cx)
23753 .expect("Should have reopened the editor again after navigating back")
23754 .downcast::<Editor>()
23755 .expect("Should be an editor")
23756 })
23757 .unwrap();
23758 color_request_handle.next().await.unwrap();
23759 assert_eq!(
23760 3,
23761 requests_made.load(atomic::Ordering::Acquire),
23762 "Cache should be reused on buffer close and reopen"
23763 );
23764 editor.update(cx, |editor, cx| {
23765 assert_eq!(
23766 vec![expected_color],
23767 extract_color_inlays(editor, cx),
23768 "Should have an initial inlay"
23769 );
23770 });
23771}
23772
23773#[gpui::test]
23774async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
23775 init_test(cx, |_| {});
23776 let (editor, cx) = cx.add_window_view(Editor::single_line);
23777 editor.update_in(cx, |editor, window, cx| {
23778 editor.set_text("oops\n\nwow\n", window, cx)
23779 });
23780 cx.run_until_parked();
23781 editor.update(cx, |editor, cx| {
23782 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
23783 });
23784 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
23785 cx.run_until_parked();
23786 editor.update(cx, |editor, cx| {
23787 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
23788 });
23789}
23790
23791#[track_caller]
23792fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
23793 editor
23794 .all_inlays(cx)
23795 .into_iter()
23796 .filter_map(|inlay| inlay.get_color())
23797 .map(Rgba::from)
23798 .collect()
23799}