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: Some(language::DocumentationConfig {
2879 start: "/**".into(),
2880 end: "*/".into(),
2881 prefix: "* ".into(),
2882 tab_size: NonZeroU32::new(1).unwrap(),
2883 }),
2884
2885 ..LanguageConfig::default()
2886 },
2887 Some(tree_sitter_rust::LANGUAGE.into()),
2888 )
2889 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
2890 .unwrap(),
2891 );
2892
2893 {
2894 let mut cx = EditorTestContext::new(cx).await;
2895 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2896 cx.set_state(indoc! {"
2897 /**ˇ
2898 "});
2899
2900 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2901 cx.assert_editor_state(indoc! {"
2902 /**
2903 * ˇ
2904 "});
2905 // Ensure that if cursor is before the comment start,
2906 // we do not actually insert a comment prefix.
2907 cx.set_state(indoc! {"
2908 ˇ/**
2909 "});
2910 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2911 cx.assert_editor_state(indoc! {"
2912
2913 ˇ/**
2914 "});
2915 // Ensure that if cursor is between it doesn't add comment prefix.
2916 cx.set_state(indoc! {"
2917 /*ˇ*
2918 "});
2919 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2920 cx.assert_editor_state(indoc! {"
2921 /*
2922 ˇ*
2923 "});
2924 // Ensure that if suffix exists on same line after cursor it adds new line.
2925 cx.set_state(indoc! {"
2926 /**ˇ*/
2927 "});
2928 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2929 cx.assert_editor_state(indoc! {"
2930 /**
2931 * ˇ
2932 */
2933 "});
2934 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2935 cx.set_state(indoc! {"
2936 /**ˇ */
2937 "});
2938 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2939 cx.assert_editor_state(indoc! {"
2940 /**
2941 * ˇ
2942 */
2943 "});
2944 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2945 cx.set_state(indoc! {"
2946 /** ˇ*/
2947 "});
2948 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2949 cx.assert_editor_state(
2950 indoc! {"
2951 /**s
2952 * ˇ
2953 */
2954 "}
2955 .replace("s", " ") // s is used as space placeholder to prevent format on save
2956 .as_str(),
2957 );
2958 // Ensure that delimiter space is preserved when newline on already
2959 // spaced delimiter.
2960 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2961 cx.assert_editor_state(
2962 indoc! {"
2963 /**s
2964 *s
2965 * ˇ
2966 */
2967 "}
2968 .replace("s", " ") // s is used as space placeholder to prevent format on save
2969 .as_str(),
2970 );
2971 // Ensure that delimiter space is preserved when space is not
2972 // on existing delimiter.
2973 cx.set_state(indoc! {"
2974 /**
2975 *ˇ
2976 */
2977 "});
2978 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2979 cx.assert_editor_state(indoc! {"
2980 /**
2981 *
2982 * ˇ
2983 */
2984 "});
2985 // Ensure that if suffix exists on same line after cursor it
2986 // doesn't add extra new line if prefix is not on same line.
2987 cx.set_state(indoc! {"
2988 /**
2989 ˇ*/
2990 "});
2991 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2992 cx.assert_editor_state(indoc! {"
2993 /**
2994
2995 ˇ*/
2996 "});
2997 // Ensure that it detects suffix after existing prefix.
2998 cx.set_state(indoc! {"
2999 /**ˇ/
3000 "});
3001 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3002 cx.assert_editor_state(indoc! {"
3003 /**
3004 ˇ/
3005 "});
3006 // Ensure that if suffix exists on same line before
3007 // cursor it does not add comment prefix.
3008 cx.set_state(indoc! {"
3009 /** */ˇ
3010 "});
3011 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3012 cx.assert_editor_state(indoc! {"
3013 /** */
3014 ˇ
3015 "});
3016 // Ensure that if suffix exists on same line before
3017 // cursor it does not add comment prefix.
3018 cx.set_state(indoc! {"
3019 /**
3020 *
3021 */ˇ
3022 "});
3023 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3024 cx.assert_editor_state(indoc! {"
3025 /**
3026 *
3027 */
3028 ˇ
3029 "});
3030
3031 // Ensure that inline comment followed by code
3032 // doesn't add comment prefix on newline
3033 cx.set_state(indoc! {"
3034 /** */ textˇ
3035 "});
3036 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3037 cx.assert_editor_state(indoc! {"
3038 /** */ text
3039 ˇ
3040 "});
3041
3042 // Ensure that text after comment end tag
3043 // doesn't add comment prefix on newline
3044 cx.set_state(indoc! {"
3045 /**
3046 *
3047 */ˇtext
3048 "});
3049 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3050 cx.assert_editor_state(indoc! {"
3051 /**
3052 *
3053 */
3054 ˇtext
3055 "});
3056
3057 // Ensure if not comment block it doesn't
3058 // add comment prefix on newline
3059 cx.set_state(indoc! {"
3060 * textˇ
3061 "});
3062 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3063 cx.assert_editor_state(indoc! {"
3064 * text
3065 ˇ
3066 "});
3067 }
3068 // Ensure that comment continuations can be disabled.
3069 update_test_language_settings(cx, |settings| {
3070 settings.defaults.extend_comment_on_newline = Some(false);
3071 });
3072 let mut cx = EditorTestContext::new(cx).await;
3073 cx.set_state(indoc! {"
3074 /**ˇ
3075 "});
3076 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3077 cx.assert_editor_state(indoc! {"
3078 /**
3079 ˇ
3080 "});
3081}
3082
3083#[gpui::test]
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(("--[[".into(), "]]".into())),
3093 ..LanguageConfig::default()
3094 },
3095 None,
3096 ));
3097
3098 let mut cx = EditorTestContext::new(cx).await;
3099 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3100
3101 // Line with line comment should extend
3102 cx.set_state(indoc! {"
3103 --ˇ
3104 "});
3105 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3106 cx.assert_editor_state(indoc! {"
3107 --
3108 --ˇ
3109 "});
3110
3111 // Line with block comment that matches line comment should not extend
3112 cx.set_state(indoc! {"
3113 --[[ˇ
3114 "});
3115 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3116 cx.assert_editor_state(indoc! {"
3117 --[[
3118 ˇ
3119 "});
3120}
3121
3122#[gpui::test]
3123fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3124 init_test(cx, |_| {});
3125
3126 let editor = cx.add_window(|window, cx| {
3127 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3128 let mut editor = build_editor(buffer.clone(), window, cx);
3129 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3130 s.select_ranges([3..4, 11..12, 19..20])
3131 });
3132 editor
3133 });
3134
3135 _ = editor.update(cx, |editor, window, cx| {
3136 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3137 editor.buffer.update(cx, |buffer, cx| {
3138 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3139 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3140 });
3141 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3142
3143 editor.insert("Z", window, cx);
3144 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3145
3146 // The selections are moved after the inserted characters
3147 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3148 });
3149}
3150
3151#[gpui::test]
3152async fn test_tab(cx: &mut TestAppContext) {
3153 init_test(cx, |settings| {
3154 settings.defaults.tab_size = NonZeroU32::new(3)
3155 });
3156
3157 let mut cx = EditorTestContext::new(cx).await;
3158 cx.set_state(indoc! {"
3159 ˇabˇc
3160 ˇ🏀ˇ🏀ˇefg
3161 dˇ
3162 "});
3163 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3164 cx.assert_editor_state(indoc! {"
3165 ˇab ˇc
3166 ˇ🏀 ˇ🏀 ˇefg
3167 d ˇ
3168 "});
3169
3170 cx.set_state(indoc! {"
3171 a
3172 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3173 "});
3174 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3175 cx.assert_editor_state(indoc! {"
3176 a
3177 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3178 "});
3179}
3180
3181#[gpui::test]
3182async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3183 init_test(cx, |_| {});
3184
3185 let mut cx = EditorTestContext::new(cx).await;
3186 let language = Arc::new(
3187 Language::new(
3188 LanguageConfig::default(),
3189 Some(tree_sitter_rust::LANGUAGE.into()),
3190 )
3191 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3192 .unwrap(),
3193 );
3194 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3195
3196 // test when all cursors are not at suggested indent
3197 // then simply move to their suggested indent location
3198 cx.set_state(indoc! {"
3199 const a: B = (
3200 c(
3201 ˇ
3202 ˇ )
3203 );
3204 "});
3205 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3206 cx.assert_editor_state(indoc! {"
3207 const a: B = (
3208 c(
3209 ˇ
3210 ˇ)
3211 );
3212 "});
3213
3214 // test cursor already at suggested indent not moving when
3215 // other cursors are yet to reach their suggested indents
3216 cx.set_state(indoc! {"
3217 ˇ
3218 const a: B = (
3219 c(
3220 d(
3221 ˇ
3222 )
3223 ˇ
3224 ˇ )
3225 );
3226 "});
3227 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3228 cx.assert_editor_state(indoc! {"
3229 ˇ
3230 const a: B = (
3231 c(
3232 d(
3233 ˇ
3234 )
3235 ˇ
3236 ˇ)
3237 );
3238 "});
3239 // test when all cursors are at suggested indent then tab is inserted
3240 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3241 cx.assert_editor_state(indoc! {"
3242 ˇ
3243 const a: B = (
3244 c(
3245 d(
3246 ˇ
3247 )
3248 ˇ
3249 ˇ)
3250 );
3251 "});
3252
3253 // test when current indent is less than suggested indent,
3254 // we adjust line to match suggested indent and move cursor to it
3255 //
3256 // when no other cursor is at word boundary, all of them should move
3257 cx.set_state(indoc! {"
3258 const a: B = (
3259 c(
3260 d(
3261 ˇ
3262 ˇ )
3263 ˇ )
3264 );
3265 "});
3266 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3267 cx.assert_editor_state(indoc! {"
3268 const a: B = (
3269 c(
3270 d(
3271 ˇ
3272 ˇ)
3273 ˇ)
3274 );
3275 "});
3276
3277 // test when current indent is less than suggested indent,
3278 // we adjust line to match suggested indent and move cursor to it
3279 //
3280 // when some other cursor is at word boundary, it should not move
3281 cx.set_state(indoc! {"
3282 const a: B = (
3283 c(
3284 d(
3285 ˇ
3286 ˇ )
3287 ˇ)
3288 );
3289 "});
3290 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3291 cx.assert_editor_state(indoc! {"
3292 const a: B = (
3293 c(
3294 d(
3295 ˇ
3296 ˇ)
3297 ˇ)
3298 );
3299 "});
3300
3301 // test when current indent is more than suggested indent,
3302 // we just move cursor to current indent instead of suggested indent
3303 //
3304 // when no other cursor is at word boundary, all of them should move
3305 cx.set_state(indoc! {"
3306 const a: B = (
3307 c(
3308 d(
3309 ˇ
3310 ˇ )
3311 ˇ )
3312 );
3313 "});
3314 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3315 cx.assert_editor_state(indoc! {"
3316 const a: B = (
3317 c(
3318 d(
3319 ˇ
3320 ˇ)
3321 ˇ)
3322 );
3323 "});
3324 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3325 cx.assert_editor_state(indoc! {"
3326 const a: B = (
3327 c(
3328 d(
3329 ˇ
3330 ˇ)
3331 ˇ)
3332 );
3333 "});
3334
3335 // test when current indent is more than suggested indent,
3336 // we just move cursor to current indent instead of suggested indent
3337 //
3338 // when some other cursor is at word boundary, it doesn't move
3339 cx.set_state(indoc! {"
3340 const a: B = (
3341 c(
3342 d(
3343 ˇ
3344 ˇ )
3345 ˇ)
3346 );
3347 "});
3348 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3349 cx.assert_editor_state(indoc! {"
3350 const a: B = (
3351 c(
3352 d(
3353 ˇ
3354 ˇ)
3355 ˇ)
3356 );
3357 "});
3358
3359 // handle auto-indent when there are multiple cursors on the same line
3360 cx.set_state(indoc! {"
3361 const a: B = (
3362 c(
3363 ˇ ˇ
3364 ˇ )
3365 );
3366 "});
3367 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3368 cx.assert_editor_state(indoc! {"
3369 const a: B = (
3370 c(
3371 ˇ
3372 ˇ)
3373 );
3374 "});
3375}
3376
3377#[gpui::test]
3378async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3379 init_test(cx, |settings| {
3380 settings.defaults.tab_size = NonZeroU32::new(3)
3381 });
3382
3383 let mut cx = EditorTestContext::new(cx).await;
3384 cx.set_state(indoc! {"
3385 ˇ
3386 \t ˇ
3387 \t ˇ
3388 \t ˇ
3389 \t \t\t \t \t\t \t\t \t \t ˇ
3390 "});
3391
3392 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3393 cx.assert_editor_state(indoc! {"
3394 ˇ
3395 \t ˇ
3396 \t ˇ
3397 \t ˇ
3398 \t \t\t \t \t\t \t\t \t \t ˇ
3399 "});
3400}
3401
3402#[gpui::test]
3403async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3404 init_test(cx, |settings| {
3405 settings.defaults.tab_size = NonZeroU32::new(4)
3406 });
3407
3408 let language = Arc::new(
3409 Language::new(
3410 LanguageConfig::default(),
3411 Some(tree_sitter_rust::LANGUAGE.into()),
3412 )
3413 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3414 .unwrap(),
3415 );
3416
3417 let mut cx = EditorTestContext::new(cx).await;
3418 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3419 cx.set_state(indoc! {"
3420 fn a() {
3421 if b {
3422 \t ˇc
3423 }
3424 }
3425 "});
3426
3427 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3428 cx.assert_editor_state(indoc! {"
3429 fn a() {
3430 if b {
3431 ˇc
3432 }
3433 }
3434 "});
3435}
3436
3437#[gpui::test]
3438async fn test_indent_outdent(cx: &mut TestAppContext) {
3439 init_test(cx, |settings| {
3440 settings.defaults.tab_size = NonZeroU32::new(4);
3441 });
3442
3443 let mut cx = EditorTestContext::new(cx).await;
3444
3445 cx.set_state(indoc! {"
3446 «oneˇ» «twoˇ»
3447 three
3448 four
3449 "});
3450 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3451 cx.assert_editor_state(indoc! {"
3452 «oneˇ» «twoˇ»
3453 three
3454 four
3455 "});
3456
3457 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3458 cx.assert_editor_state(indoc! {"
3459 «oneˇ» «twoˇ»
3460 three
3461 four
3462 "});
3463
3464 // select across line ending
3465 cx.set_state(indoc! {"
3466 one two
3467 t«hree
3468 ˇ» four
3469 "});
3470 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3471 cx.assert_editor_state(indoc! {"
3472 one two
3473 t«hree
3474 ˇ» four
3475 "});
3476
3477 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3478 cx.assert_editor_state(indoc! {"
3479 one two
3480 t«hree
3481 ˇ» four
3482 "});
3483
3484 // Ensure that indenting/outdenting works when the cursor is at column 0.
3485 cx.set_state(indoc! {"
3486 one two
3487 ˇthree
3488 four
3489 "});
3490 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3491 cx.assert_editor_state(indoc! {"
3492 one two
3493 ˇthree
3494 four
3495 "});
3496
3497 cx.set_state(indoc! {"
3498 one two
3499 ˇ three
3500 four
3501 "});
3502 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3503 cx.assert_editor_state(indoc! {"
3504 one two
3505 ˇthree
3506 four
3507 "});
3508}
3509
3510#[gpui::test]
3511async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3512 // This is a regression test for issue #33761
3513 init_test(cx, |_| {});
3514
3515 let mut cx = EditorTestContext::new(cx).await;
3516 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3517 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3518
3519 cx.set_state(
3520 r#"ˇ# ingress:
3521ˇ# api:
3522ˇ# enabled: false
3523ˇ# pathType: Prefix
3524ˇ# console:
3525ˇ# enabled: false
3526ˇ# pathType: Prefix
3527"#,
3528 );
3529
3530 // Press tab to indent all lines
3531 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3532
3533 cx.assert_editor_state(
3534 r#" ˇ# ingress:
3535 ˇ# api:
3536 ˇ# enabled: false
3537 ˇ# pathType: Prefix
3538 ˇ# console:
3539 ˇ# enabled: false
3540 ˇ# pathType: Prefix
3541"#,
3542 );
3543}
3544
3545#[gpui::test]
3546async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3547 // This is a test to make sure our fix for issue #33761 didn't break anything
3548 init_test(cx, |_| {});
3549
3550 let mut cx = EditorTestContext::new(cx).await;
3551 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3552 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3553
3554 cx.set_state(
3555 r#"ˇingress:
3556ˇ api:
3557ˇ enabled: false
3558ˇ pathType: Prefix
3559"#,
3560 );
3561
3562 // Press tab to indent all lines
3563 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3564
3565 cx.assert_editor_state(
3566 r#"ˇingress:
3567 ˇapi:
3568 ˇenabled: false
3569 ˇpathType: Prefix
3570"#,
3571 );
3572}
3573
3574#[gpui::test]
3575async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3576 init_test(cx, |settings| {
3577 settings.defaults.hard_tabs = Some(true);
3578 });
3579
3580 let mut cx = EditorTestContext::new(cx).await;
3581
3582 // select two ranges on one line
3583 cx.set_state(indoc! {"
3584 «oneˇ» «twoˇ»
3585 three
3586 four
3587 "});
3588 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3589 cx.assert_editor_state(indoc! {"
3590 \t«oneˇ» «twoˇ»
3591 three
3592 four
3593 "});
3594 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3595 cx.assert_editor_state(indoc! {"
3596 \t\t«oneˇ» «twoˇ»
3597 three
3598 four
3599 "});
3600 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3601 cx.assert_editor_state(indoc! {"
3602 \t«oneˇ» «twoˇ»
3603 three
3604 four
3605 "});
3606 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3607 cx.assert_editor_state(indoc! {"
3608 «oneˇ» «twoˇ»
3609 three
3610 four
3611 "});
3612
3613 // select across a line ending
3614 cx.set_state(indoc! {"
3615 one two
3616 t«hree
3617 ˇ»four
3618 "});
3619 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3620 cx.assert_editor_state(indoc! {"
3621 one two
3622 \tt«hree
3623 ˇ»four
3624 "});
3625 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3626 cx.assert_editor_state(indoc! {"
3627 one two
3628 \t\tt«hree
3629 ˇ»four
3630 "});
3631 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3632 cx.assert_editor_state(indoc! {"
3633 one two
3634 \tt«hree
3635 ˇ»four
3636 "});
3637 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3638 cx.assert_editor_state(indoc! {"
3639 one two
3640 t«hree
3641 ˇ»four
3642 "});
3643
3644 // Ensure that indenting/outdenting works when the cursor is at column 0.
3645 cx.set_state(indoc! {"
3646 one two
3647 ˇthree
3648 four
3649 "});
3650 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3651 cx.assert_editor_state(indoc! {"
3652 one two
3653 ˇthree
3654 four
3655 "});
3656 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3657 cx.assert_editor_state(indoc! {"
3658 one two
3659 \tˇthree
3660 four
3661 "});
3662 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3663 cx.assert_editor_state(indoc! {"
3664 one two
3665 ˇthree
3666 four
3667 "});
3668}
3669
3670#[gpui::test]
3671fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3672 init_test(cx, |settings| {
3673 settings.languages.0.extend([
3674 (
3675 "TOML".into(),
3676 LanguageSettingsContent {
3677 tab_size: NonZeroU32::new(2),
3678 ..Default::default()
3679 },
3680 ),
3681 (
3682 "Rust".into(),
3683 LanguageSettingsContent {
3684 tab_size: NonZeroU32::new(4),
3685 ..Default::default()
3686 },
3687 ),
3688 ]);
3689 });
3690
3691 let toml_language = Arc::new(Language::new(
3692 LanguageConfig {
3693 name: "TOML".into(),
3694 ..Default::default()
3695 },
3696 None,
3697 ));
3698 let rust_language = Arc::new(Language::new(
3699 LanguageConfig {
3700 name: "Rust".into(),
3701 ..Default::default()
3702 },
3703 None,
3704 ));
3705
3706 let toml_buffer =
3707 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3708 let rust_buffer =
3709 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3710 let multibuffer = cx.new(|cx| {
3711 let mut multibuffer = MultiBuffer::new(ReadWrite);
3712 multibuffer.push_excerpts(
3713 toml_buffer.clone(),
3714 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3715 cx,
3716 );
3717 multibuffer.push_excerpts(
3718 rust_buffer.clone(),
3719 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3720 cx,
3721 );
3722 multibuffer
3723 });
3724
3725 cx.add_window(|window, cx| {
3726 let mut editor = build_editor(multibuffer, window, cx);
3727
3728 assert_eq!(
3729 editor.text(cx),
3730 indoc! {"
3731 a = 1
3732 b = 2
3733
3734 const c: usize = 3;
3735 "}
3736 );
3737
3738 select_ranges(
3739 &mut editor,
3740 indoc! {"
3741 «aˇ» = 1
3742 b = 2
3743
3744 «const c:ˇ» usize = 3;
3745 "},
3746 window,
3747 cx,
3748 );
3749
3750 editor.tab(&Tab, window, cx);
3751 assert_text_with_selections(
3752 &mut editor,
3753 indoc! {"
3754 «aˇ» = 1
3755 b = 2
3756
3757 «const c:ˇ» usize = 3;
3758 "},
3759 cx,
3760 );
3761 editor.backtab(&Backtab, window, cx);
3762 assert_text_with_selections(
3763 &mut editor,
3764 indoc! {"
3765 «aˇ» = 1
3766 b = 2
3767
3768 «const c:ˇ» usize = 3;
3769 "},
3770 cx,
3771 );
3772
3773 editor
3774 });
3775}
3776
3777#[gpui::test]
3778async fn test_backspace(cx: &mut TestAppContext) {
3779 init_test(cx, |_| {});
3780
3781 let mut cx = EditorTestContext::new(cx).await;
3782
3783 // Basic backspace
3784 cx.set_state(indoc! {"
3785 onˇe two three
3786 fou«rˇ» five six
3787 seven «ˇeight nine
3788 »ten
3789 "});
3790 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3791 cx.assert_editor_state(indoc! {"
3792 oˇe two three
3793 fouˇ five six
3794 seven ˇten
3795 "});
3796
3797 // Test backspace inside and around indents
3798 cx.set_state(indoc! {"
3799 zero
3800 ˇone
3801 ˇtwo
3802 ˇ ˇ ˇ three
3803 ˇ ˇ four
3804 "});
3805 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3806 cx.assert_editor_state(indoc! {"
3807 zero
3808 ˇone
3809 ˇtwo
3810 ˇ threeˇ four
3811 "});
3812}
3813
3814#[gpui::test]
3815async fn test_delete(cx: &mut TestAppContext) {
3816 init_test(cx, |_| {});
3817
3818 let mut cx = EditorTestContext::new(cx).await;
3819 cx.set_state(indoc! {"
3820 onˇe two three
3821 fou«rˇ» five six
3822 seven «ˇeight nine
3823 »ten
3824 "});
3825 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3826 cx.assert_editor_state(indoc! {"
3827 onˇ two three
3828 fouˇ five six
3829 seven ˇten
3830 "});
3831}
3832
3833#[gpui::test]
3834fn test_delete_line(cx: &mut TestAppContext) {
3835 init_test(cx, |_| {});
3836
3837 let editor = cx.add_window(|window, cx| {
3838 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3839 build_editor(buffer, window, cx)
3840 });
3841 _ = editor.update(cx, |editor, window, cx| {
3842 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3843 s.select_display_ranges([
3844 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3845 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3846 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3847 ])
3848 });
3849 editor.delete_line(&DeleteLine, window, cx);
3850 assert_eq!(editor.display_text(cx), "ghi");
3851 assert_eq!(
3852 editor.selections.display_ranges(cx),
3853 vec![
3854 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3855 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3856 ]
3857 );
3858 });
3859
3860 let editor = cx.add_window(|window, cx| {
3861 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3862 build_editor(buffer, window, cx)
3863 });
3864 _ = editor.update(cx, |editor, window, cx| {
3865 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3866 s.select_display_ranges([
3867 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3868 ])
3869 });
3870 editor.delete_line(&DeleteLine, window, cx);
3871 assert_eq!(editor.display_text(cx), "ghi\n");
3872 assert_eq!(
3873 editor.selections.display_ranges(cx),
3874 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3875 );
3876 });
3877}
3878
3879#[gpui::test]
3880fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3881 init_test(cx, |_| {});
3882
3883 cx.add_window(|window, cx| {
3884 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3885 let mut editor = build_editor(buffer.clone(), window, cx);
3886 let buffer = buffer.read(cx).as_singleton().unwrap();
3887
3888 assert_eq!(
3889 editor.selections.ranges::<Point>(cx),
3890 &[Point::new(0, 0)..Point::new(0, 0)]
3891 );
3892
3893 // When on single line, replace newline at end by space
3894 editor.join_lines(&JoinLines, window, cx);
3895 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3896 assert_eq!(
3897 editor.selections.ranges::<Point>(cx),
3898 &[Point::new(0, 3)..Point::new(0, 3)]
3899 );
3900
3901 // When multiple lines are selected, remove newlines that are spanned by the selection
3902 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3903 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3904 });
3905 editor.join_lines(&JoinLines, window, cx);
3906 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3907 assert_eq!(
3908 editor.selections.ranges::<Point>(cx),
3909 &[Point::new(0, 11)..Point::new(0, 11)]
3910 );
3911
3912 // Undo should be transactional
3913 editor.undo(&Undo, window, cx);
3914 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3915 assert_eq!(
3916 editor.selections.ranges::<Point>(cx),
3917 &[Point::new(0, 5)..Point::new(2, 2)]
3918 );
3919
3920 // When joining an empty line don't insert a space
3921 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3922 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3923 });
3924 editor.join_lines(&JoinLines, window, cx);
3925 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3926 assert_eq!(
3927 editor.selections.ranges::<Point>(cx),
3928 [Point::new(2, 3)..Point::new(2, 3)]
3929 );
3930
3931 // We can remove trailing newlines
3932 editor.join_lines(&JoinLines, window, cx);
3933 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3934 assert_eq!(
3935 editor.selections.ranges::<Point>(cx),
3936 [Point::new(2, 3)..Point::new(2, 3)]
3937 );
3938
3939 // We don't blow up on the last line
3940 editor.join_lines(&JoinLines, window, cx);
3941 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3942 assert_eq!(
3943 editor.selections.ranges::<Point>(cx),
3944 [Point::new(2, 3)..Point::new(2, 3)]
3945 );
3946
3947 // reset to test indentation
3948 editor.buffer.update(cx, |buffer, cx| {
3949 buffer.edit(
3950 [
3951 (Point::new(1, 0)..Point::new(1, 2), " "),
3952 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3953 ],
3954 None,
3955 cx,
3956 )
3957 });
3958
3959 // We remove any leading spaces
3960 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3961 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3962 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3963 });
3964 editor.join_lines(&JoinLines, window, cx);
3965 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3966
3967 // We don't insert a space for a line containing only spaces
3968 editor.join_lines(&JoinLines, window, cx);
3969 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3970
3971 // We ignore any leading tabs
3972 editor.join_lines(&JoinLines, window, cx);
3973 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3974
3975 editor
3976 });
3977}
3978
3979#[gpui::test]
3980fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3981 init_test(cx, |_| {});
3982
3983 cx.add_window(|window, cx| {
3984 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3985 let mut editor = build_editor(buffer.clone(), window, cx);
3986 let buffer = buffer.read(cx).as_singleton().unwrap();
3987
3988 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3989 s.select_ranges([
3990 Point::new(0, 2)..Point::new(1, 1),
3991 Point::new(1, 2)..Point::new(1, 2),
3992 Point::new(3, 1)..Point::new(3, 2),
3993 ])
3994 });
3995
3996 editor.join_lines(&JoinLines, window, cx);
3997 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3998
3999 assert_eq!(
4000 editor.selections.ranges::<Point>(cx),
4001 [
4002 Point::new(0, 7)..Point::new(0, 7),
4003 Point::new(1, 3)..Point::new(1, 3)
4004 ]
4005 );
4006 editor
4007 });
4008}
4009
4010#[gpui::test]
4011async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4012 init_test(cx, |_| {});
4013
4014 let mut cx = EditorTestContext::new(cx).await;
4015
4016 let diff_base = r#"
4017 Line 0
4018 Line 1
4019 Line 2
4020 Line 3
4021 "#
4022 .unindent();
4023
4024 cx.set_state(
4025 &r#"
4026 ˇLine 0
4027 Line 1
4028 Line 2
4029 Line 3
4030 "#
4031 .unindent(),
4032 );
4033
4034 cx.set_head_text(&diff_base);
4035 executor.run_until_parked();
4036
4037 // Join lines
4038 cx.update_editor(|editor, window, cx| {
4039 editor.join_lines(&JoinLines, window, cx);
4040 });
4041 executor.run_until_parked();
4042
4043 cx.assert_editor_state(
4044 &r#"
4045 Line 0ˇ Line 1
4046 Line 2
4047 Line 3
4048 "#
4049 .unindent(),
4050 );
4051 // Join again
4052 cx.update_editor(|editor, window, cx| {
4053 editor.join_lines(&JoinLines, window, cx);
4054 });
4055 executor.run_until_parked();
4056
4057 cx.assert_editor_state(
4058 &r#"
4059 Line 0 Line 1ˇ Line 2
4060 Line 3
4061 "#
4062 .unindent(),
4063 );
4064}
4065
4066#[gpui::test]
4067async fn test_custom_newlines_cause_no_false_positive_diffs(
4068 executor: BackgroundExecutor,
4069 cx: &mut TestAppContext,
4070) {
4071 init_test(cx, |_| {});
4072 let mut cx = EditorTestContext::new(cx).await;
4073 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4074 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4075 executor.run_until_parked();
4076
4077 cx.update_editor(|editor, window, cx| {
4078 let snapshot = editor.snapshot(window, cx);
4079 assert_eq!(
4080 snapshot
4081 .buffer_snapshot
4082 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
4083 .collect::<Vec<_>>(),
4084 Vec::new(),
4085 "Should not have any diffs for files with custom newlines"
4086 );
4087 });
4088}
4089
4090#[gpui::test]
4091async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4092 init_test(cx, |_| {});
4093
4094 let mut cx = EditorTestContext::new(cx).await;
4095
4096 // Test sort_lines_case_insensitive()
4097 cx.set_state(indoc! {"
4098 «z
4099 y
4100 x
4101 Z
4102 Y
4103 Xˇ»
4104 "});
4105 cx.update_editor(|e, window, cx| {
4106 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4107 });
4108 cx.assert_editor_state(indoc! {"
4109 «x
4110 X
4111 y
4112 Y
4113 z
4114 Zˇ»
4115 "});
4116
4117 // Test sort_lines_by_length()
4118 //
4119 // Demonstrates:
4120 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4121 // - sort is stable
4122 cx.set_state(indoc! {"
4123 «123
4124 æ
4125 12
4126 ∞
4127 1
4128 æˇ»
4129 "});
4130 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4131 cx.assert_editor_state(indoc! {"
4132 «æ
4133 ∞
4134 1
4135 æ
4136 12
4137 123ˇ»
4138 "});
4139
4140 // Test reverse_lines()
4141 cx.set_state(indoc! {"
4142 «5
4143 4
4144 3
4145 2
4146 1ˇ»
4147 "});
4148 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4149 cx.assert_editor_state(indoc! {"
4150 «1
4151 2
4152 3
4153 4
4154 5ˇ»
4155 "});
4156
4157 // Skip testing shuffle_line()
4158
4159 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4160 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4161
4162 // Don't manipulate when cursor is on single line, but expand the selection
4163 cx.set_state(indoc! {"
4164 ddˇdd
4165 ccc
4166 bb
4167 a
4168 "});
4169 cx.update_editor(|e, window, cx| {
4170 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4171 });
4172 cx.assert_editor_state(indoc! {"
4173 «ddddˇ»
4174 ccc
4175 bb
4176 a
4177 "});
4178
4179 // Basic manipulate case
4180 // Start selection moves to column 0
4181 // End of selection shrinks to fit shorter line
4182 cx.set_state(indoc! {"
4183 dd«d
4184 ccc
4185 bb
4186 aaaaaˇ»
4187 "});
4188 cx.update_editor(|e, window, cx| {
4189 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4190 });
4191 cx.assert_editor_state(indoc! {"
4192 «aaaaa
4193 bb
4194 ccc
4195 dddˇ»
4196 "});
4197
4198 // Manipulate case with newlines
4199 cx.set_state(indoc! {"
4200 dd«d
4201 ccc
4202
4203 bb
4204 aaaaa
4205
4206 ˇ»
4207 "});
4208 cx.update_editor(|e, window, cx| {
4209 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4210 });
4211 cx.assert_editor_state(indoc! {"
4212 «
4213
4214 aaaaa
4215 bb
4216 ccc
4217 dddˇ»
4218
4219 "});
4220
4221 // Adding new line
4222 cx.set_state(indoc! {"
4223 aa«a
4224 bbˇ»b
4225 "});
4226 cx.update_editor(|e, window, cx| {
4227 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4228 });
4229 cx.assert_editor_state(indoc! {"
4230 «aaa
4231 bbb
4232 added_lineˇ»
4233 "});
4234
4235 // Removing line
4236 cx.set_state(indoc! {"
4237 aa«a
4238 bbbˇ»
4239 "});
4240 cx.update_editor(|e, window, cx| {
4241 e.manipulate_immutable_lines(window, cx, |lines| {
4242 lines.pop();
4243 })
4244 });
4245 cx.assert_editor_state(indoc! {"
4246 «aaaˇ»
4247 "});
4248
4249 // Removing all lines
4250 cx.set_state(indoc! {"
4251 aa«a
4252 bbbˇ»
4253 "});
4254 cx.update_editor(|e, window, cx| {
4255 e.manipulate_immutable_lines(window, cx, |lines| {
4256 lines.drain(..);
4257 })
4258 });
4259 cx.assert_editor_state(indoc! {"
4260 ˇ
4261 "});
4262}
4263
4264#[gpui::test]
4265async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4266 init_test(cx, |_| {});
4267
4268 let mut cx = EditorTestContext::new(cx).await;
4269
4270 // Consider continuous selection as single selection
4271 cx.set_state(indoc! {"
4272 Aaa«aa
4273 cˇ»c«c
4274 bb
4275 aaaˇ»aa
4276 "});
4277 cx.update_editor(|e, window, cx| {
4278 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4279 });
4280 cx.assert_editor_state(indoc! {"
4281 «Aaaaa
4282 ccc
4283 bb
4284 aaaaaˇ»
4285 "});
4286
4287 cx.set_state(indoc! {"
4288 Aaa«aa
4289 cˇ»c«c
4290 bb
4291 aaaˇ»aa
4292 "});
4293 cx.update_editor(|e, window, cx| {
4294 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4295 });
4296 cx.assert_editor_state(indoc! {"
4297 «Aaaaa
4298 ccc
4299 bbˇ»
4300 "});
4301
4302 // Consider non continuous selection as distinct dedup operations
4303 cx.set_state(indoc! {"
4304 «aaaaa
4305 bb
4306 aaaaa
4307 aaaaaˇ»
4308
4309 aaa«aaˇ»
4310 "});
4311 cx.update_editor(|e, window, cx| {
4312 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4313 });
4314 cx.assert_editor_state(indoc! {"
4315 «aaaaa
4316 bbˇ»
4317
4318 «aaaaaˇ»
4319 "});
4320}
4321
4322#[gpui::test]
4323async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4324 init_test(cx, |_| {});
4325
4326 let mut cx = EditorTestContext::new(cx).await;
4327
4328 cx.set_state(indoc! {"
4329 «Aaa
4330 aAa
4331 Aaaˇ»
4332 "});
4333 cx.update_editor(|e, window, cx| {
4334 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4335 });
4336 cx.assert_editor_state(indoc! {"
4337 «Aaa
4338 aAaˇ»
4339 "});
4340
4341 cx.set_state(indoc! {"
4342 «Aaa
4343 aAa
4344 aaAˇ»
4345 "});
4346 cx.update_editor(|e, window, cx| {
4347 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4348 });
4349 cx.assert_editor_state(indoc! {"
4350 «Aaaˇ»
4351 "});
4352}
4353
4354#[gpui::test]
4355async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4356 init_test(cx, |_| {});
4357
4358 let mut cx = EditorTestContext::new(cx).await;
4359
4360 // Manipulate with multiple selections on a single line
4361 cx.set_state(indoc! {"
4362 dd«dd
4363 cˇ»c«c
4364 bb
4365 aaaˇ»aa
4366 "});
4367 cx.update_editor(|e, window, cx| {
4368 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4369 });
4370 cx.assert_editor_state(indoc! {"
4371 «aaaaa
4372 bb
4373 ccc
4374 ddddˇ»
4375 "});
4376
4377 // Manipulate with multiple disjoin selections
4378 cx.set_state(indoc! {"
4379 5«
4380 4
4381 3
4382 2
4383 1ˇ»
4384
4385 dd«dd
4386 ccc
4387 bb
4388 aaaˇ»aa
4389 "});
4390 cx.update_editor(|e, window, cx| {
4391 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4392 });
4393 cx.assert_editor_state(indoc! {"
4394 «1
4395 2
4396 3
4397 4
4398 5ˇ»
4399
4400 «aaaaa
4401 bb
4402 ccc
4403 ddddˇ»
4404 "});
4405
4406 // Adding lines on each selection
4407 cx.set_state(indoc! {"
4408 2«
4409 1ˇ»
4410
4411 bb«bb
4412 aaaˇ»aa
4413 "});
4414 cx.update_editor(|e, window, cx| {
4415 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4416 });
4417 cx.assert_editor_state(indoc! {"
4418 «2
4419 1
4420 added lineˇ»
4421
4422 «bbbb
4423 aaaaa
4424 added lineˇ»
4425 "});
4426
4427 // Removing lines on each selection
4428 cx.set_state(indoc! {"
4429 2«
4430 1ˇ»
4431
4432 bb«bb
4433 aaaˇ»aa
4434 "});
4435 cx.update_editor(|e, window, cx| {
4436 e.manipulate_immutable_lines(window, cx, |lines| {
4437 lines.pop();
4438 })
4439 });
4440 cx.assert_editor_state(indoc! {"
4441 «2ˇ»
4442
4443 «bbbbˇ»
4444 "});
4445}
4446
4447#[gpui::test]
4448async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4449 init_test(cx, |settings| {
4450 settings.defaults.tab_size = NonZeroU32::new(3)
4451 });
4452
4453 let mut cx = EditorTestContext::new(cx).await;
4454
4455 // MULTI SELECTION
4456 // Ln.1 "«" tests empty lines
4457 // Ln.9 tests just leading whitespace
4458 cx.set_state(indoc! {"
4459 «
4460 abc // No indentationˇ»
4461 «\tabc // 1 tabˇ»
4462 \t\tabc « ˇ» // 2 tabs
4463 \t ab«c // Tab followed by space
4464 \tabc // Space followed by tab (3 spaces should be the result)
4465 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4466 abˇ»ˇc ˇ ˇ // Already space indented«
4467 \t
4468 \tabc\tdef // Only the leading tab is manipulatedˇ»
4469 "});
4470 cx.update_editor(|e, window, cx| {
4471 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4472 });
4473 cx.assert_editor_state(
4474 indoc! {"
4475 «
4476 abc // No indentation
4477 abc // 1 tab
4478 abc // 2 tabs
4479 abc // Tab followed by space
4480 abc // Space followed by tab (3 spaces should be the result)
4481 abc // Mixed indentation (tab conversion depends on the column)
4482 abc // Already space indented
4483 ·
4484 abc\tdef // Only the leading tab is manipulatedˇ»
4485 "}
4486 .replace("·", "")
4487 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4488 );
4489
4490 // Test on just a few lines, the others should remain unchanged
4491 // Only lines (3, 5, 10, 11) should change
4492 cx.set_state(
4493 indoc! {"
4494 ·
4495 abc // No indentation
4496 \tabcˇ // 1 tab
4497 \t\tabc // 2 tabs
4498 \t abcˇ // Tab followed by space
4499 \tabc // Space followed by tab (3 spaces should be the result)
4500 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4501 abc // Already space indented
4502 «\t
4503 \tabc\tdef // Only the leading tab is manipulatedˇ»
4504 "}
4505 .replace("·", "")
4506 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4507 );
4508 cx.update_editor(|e, window, cx| {
4509 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4510 });
4511 cx.assert_editor_state(
4512 indoc! {"
4513 ·
4514 abc // No indentation
4515 « abc // 1 tabˇ»
4516 \t\tabc // 2 tabs
4517 « abc // Tab followed by spaceˇ»
4518 \tabc // Space followed by tab (3 spaces should be the result)
4519 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4520 abc // Already space indented
4521 « ·
4522 abc\tdef // Only the leading tab is manipulatedˇ»
4523 "}
4524 .replace("·", "")
4525 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4526 );
4527
4528 // SINGLE SELECTION
4529 // Ln.1 "«" tests empty lines
4530 // Ln.9 tests just leading whitespace
4531 cx.set_state(indoc! {"
4532 «
4533 abc // No indentation
4534 \tabc // 1 tab
4535 \t\tabc // 2 tabs
4536 \t abc // Tab followed by space
4537 \tabc // Space followed by tab (3 spaces should be the result)
4538 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4539 abc // Already space indented
4540 \t
4541 \tabc\tdef // Only the leading tab is manipulatedˇ»
4542 "});
4543 cx.update_editor(|e, window, cx| {
4544 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4545 });
4546 cx.assert_editor_state(
4547 indoc! {"
4548 «
4549 abc // No indentation
4550 abc // 1 tab
4551 abc // 2 tabs
4552 abc // Tab followed by space
4553 abc // Space followed by tab (3 spaces should be the result)
4554 abc // Mixed indentation (tab conversion depends on the column)
4555 abc // Already space indented
4556 ·
4557 abc\tdef // Only the leading tab is manipulatedˇ»
4558 "}
4559 .replace("·", "")
4560 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4561 );
4562}
4563
4564#[gpui::test]
4565async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
4566 init_test(cx, |settings| {
4567 settings.defaults.tab_size = NonZeroU32::new(3)
4568 });
4569
4570 let mut cx = EditorTestContext::new(cx).await;
4571
4572 // MULTI SELECTION
4573 // Ln.1 "«" tests empty lines
4574 // Ln.11 tests just leading whitespace
4575 cx.set_state(indoc! {"
4576 «
4577 abˇ»ˇc // No indentation
4578 abc ˇ ˇ // 1 space (< 3 so dont convert)
4579 abc « // 2 spaces (< 3 so dont convert)
4580 abc // 3 spaces (convert)
4581 abc ˇ» // 5 spaces (1 tab + 2 spaces)
4582 «\tˇ»\t«\tˇ»abc // Already tab indented
4583 «\t abc // Tab followed by space
4584 \tabc // Space followed by tab (should be consumed due to tab)
4585 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4586 \tˇ» «\t
4587 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
4588 "});
4589 cx.update_editor(|e, window, cx| {
4590 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4591 });
4592 cx.assert_editor_state(indoc! {"
4593 «
4594 abc // No indentation
4595 abc // 1 space (< 3 so dont convert)
4596 abc // 2 spaces (< 3 so dont convert)
4597 \tabc // 3 spaces (convert)
4598 \t abc // 5 spaces (1 tab + 2 spaces)
4599 \t\t\tabc // Already tab indented
4600 \t abc // Tab followed by space
4601 \tabc // Space followed by tab (should be consumed due to tab)
4602 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4603 \t\t\t
4604 \tabc \t // Only the leading spaces should be convertedˇ»
4605 "});
4606
4607 // Test on just a few lines, the other should remain unchanged
4608 // Only lines (4, 8, 11, 12) should change
4609 cx.set_state(
4610 indoc! {"
4611 ·
4612 abc // No indentation
4613 abc // 1 space (< 3 so dont convert)
4614 abc // 2 spaces (< 3 so dont convert)
4615 « abc // 3 spaces (convert)ˇ»
4616 abc // 5 spaces (1 tab + 2 spaces)
4617 \t\t\tabc // Already tab indented
4618 \t abc // Tab followed by space
4619 \tabc ˇ // Space followed by tab (should be consumed due to tab)
4620 \t\t \tabc // Mixed indentation
4621 \t \t \t \tabc // Mixed indentation
4622 \t \tˇ
4623 « abc \t // Only the leading spaces should be convertedˇ»
4624 "}
4625 .replace("·", "")
4626 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4627 );
4628 cx.update_editor(|e, window, cx| {
4629 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4630 });
4631 cx.assert_editor_state(
4632 indoc! {"
4633 ·
4634 abc // No indentation
4635 abc // 1 space (< 3 so dont convert)
4636 abc // 2 spaces (< 3 so dont convert)
4637 «\tabc // 3 spaces (convert)ˇ»
4638 abc // 5 spaces (1 tab + 2 spaces)
4639 \t\t\tabc // Already tab indented
4640 \t abc // Tab followed by space
4641 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
4642 \t\t \tabc // Mixed indentation
4643 \t \t \t \tabc // Mixed indentation
4644 «\t\t\t
4645 \tabc \t // Only the leading spaces should be convertedˇ»
4646 "}
4647 .replace("·", "")
4648 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4649 );
4650
4651 // SINGLE SELECTION
4652 // Ln.1 "«" tests empty lines
4653 // Ln.11 tests just leading whitespace
4654 cx.set_state(indoc! {"
4655 «
4656 abc // No indentation
4657 abc // 1 space (< 3 so dont convert)
4658 abc // 2 spaces (< 3 so dont convert)
4659 abc // 3 spaces (convert)
4660 abc // 5 spaces (1 tab + 2 spaces)
4661 \t\t\tabc // Already tab indented
4662 \t abc // Tab followed by space
4663 \tabc // Space followed by tab (should be consumed due to tab)
4664 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4665 \t \t
4666 abc \t // Only the leading spaces should be convertedˇ»
4667 "});
4668 cx.update_editor(|e, window, cx| {
4669 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4670 });
4671 cx.assert_editor_state(indoc! {"
4672 «
4673 abc // No indentation
4674 abc // 1 space (< 3 so dont convert)
4675 abc // 2 spaces (< 3 so dont convert)
4676 \tabc // 3 spaces (convert)
4677 \t abc // 5 spaces (1 tab + 2 spaces)
4678 \t\t\tabc // Already tab indented
4679 \t abc // Tab followed by space
4680 \tabc // Space followed by tab (should be consumed due to tab)
4681 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4682 \t\t\t
4683 \tabc \t // Only the leading spaces should be convertedˇ»
4684 "});
4685}
4686
4687#[gpui::test]
4688async fn test_toggle_case(cx: &mut TestAppContext) {
4689 init_test(cx, |_| {});
4690
4691 let mut cx = EditorTestContext::new(cx).await;
4692
4693 // If all lower case -> upper case
4694 cx.set_state(indoc! {"
4695 «hello worldˇ»
4696 "});
4697 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4698 cx.assert_editor_state(indoc! {"
4699 «HELLO WORLDˇ»
4700 "});
4701
4702 // If all upper case -> lower case
4703 cx.set_state(indoc! {"
4704 «HELLO WORLDˇ»
4705 "});
4706 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4707 cx.assert_editor_state(indoc! {"
4708 «hello worldˇ»
4709 "});
4710
4711 // If any upper case characters are identified -> lower case
4712 // This matches JetBrains IDEs
4713 cx.set_state(indoc! {"
4714 «hEllo worldˇ»
4715 "});
4716 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4717 cx.assert_editor_state(indoc! {"
4718 «hello worldˇ»
4719 "});
4720}
4721
4722#[gpui::test]
4723async fn test_manipulate_text(cx: &mut TestAppContext) {
4724 init_test(cx, |_| {});
4725
4726 let mut cx = EditorTestContext::new(cx).await;
4727
4728 // Test convert_to_upper_case()
4729 cx.set_state(indoc! {"
4730 «hello worldˇ»
4731 "});
4732 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4733 cx.assert_editor_state(indoc! {"
4734 «HELLO WORLDˇ»
4735 "});
4736
4737 // Test convert_to_lower_case()
4738 cx.set_state(indoc! {"
4739 «HELLO WORLDˇ»
4740 "});
4741 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4742 cx.assert_editor_state(indoc! {"
4743 «hello worldˇ»
4744 "});
4745
4746 // Test multiple line, single selection case
4747 cx.set_state(indoc! {"
4748 «The quick brown
4749 fox jumps over
4750 the lazy dogˇ»
4751 "});
4752 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4753 cx.assert_editor_state(indoc! {"
4754 «The Quick Brown
4755 Fox Jumps Over
4756 The Lazy Dogˇ»
4757 "});
4758
4759 // Test multiple line, single selection case
4760 cx.set_state(indoc! {"
4761 «The quick brown
4762 fox jumps over
4763 the lazy dogˇ»
4764 "});
4765 cx.update_editor(|e, window, cx| {
4766 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4767 });
4768 cx.assert_editor_state(indoc! {"
4769 «TheQuickBrown
4770 FoxJumpsOver
4771 TheLazyDogˇ»
4772 "});
4773
4774 // From here on out, test more complex cases of manipulate_text()
4775
4776 // Test no selection case - should affect words cursors are in
4777 // Cursor at beginning, middle, and end of word
4778 cx.set_state(indoc! {"
4779 ˇhello big beauˇtiful worldˇ
4780 "});
4781 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4782 cx.assert_editor_state(indoc! {"
4783 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4784 "});
4785
4786 // Test multiple selections on a single line and across multiple lines
4787 cx.set_state(indoc! {"
4788 «Theˇ» quick «brown
4789 foxˇ» jumps «overˇ»
4790 the «lazyˇ» dog
4791 "});
4792 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4793 cx.assert_editor_state(indoc! {"
4794 «THEˇ» quick «BROWN
4795 FOXˇ» jumps «OVERˇ»
4796 the «LAZYˇ» dog
4797 "});
4798
4799 // Test case where text length grows
4800 cx.set_state(indoc! {"
4801 «tschüߡ»
4802 "});
4803 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4804 cx.assert_editor_state(indoc! {"
4805 «TSCHÜSSˇ»
4806 "});
4807
4808 // Test to make sure we don't crash when text shrinks
4809 cx.set_state(indoc! {"
4810 aaa_bbbˇ
4811 "});
4812 cx.update_editor(|e, window, cx| {
4813 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4814 });
4815 cx.assert_editor_state(indoc! {"
4816 «aaaBbbˇ»
4817 "});
4818
4819 // Test to make sure we all aware of the fact that each word can grow and shrink
4820 // Final selections should be aware of this fact
4821 cx.set_state(indoc! {"
4822 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4823 "});
4824 cx.update_editor(|e, window, cx| {
4825 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4826 });
4827 cx.assert_editor_state(indoc! {"
4828 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4829 "});
4830
4831 cx.set_state(indoc! {"
4832 «hElLo, WoRld!ˇ»
4833 "});
4834 cx.update_editor(|e, window, cx| {
4835 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4836 });
4837 cx.assert_editor_state(indoc! {"
4838 «HeLlO, wOrLD!ˇ»
4839 "});
4840}
4841
4842#[gpui::test]
4843fn test_duplicate_line(cx: &mut TestAppContext) {
4844 init_test(cx, |_| {});
4845
4846 let editor = cx.add_window(|window, cx| {
4847 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4848 build_editor(buffer, window, cx)
4849 });
4850 _ = editor.update(cx, |editor, window, cx| {
4851 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4852 s.select_display_ranges([
4853 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4854 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4855 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4856 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4857 ])
4858 });
4859 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4860 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4861 assert_eq!(
4862 editor.selections.display_ranges(cx),
4863 vec![
4864 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4865 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4866 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4867 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4868 ]
4869 );
4870 });
4871
4872 let editor = cx.add_window(|window, cx| {
4873 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4874 build_editor(buffer, window, cx)
4875 });
4876 _ = editor.update(cx, |editor, window, cx| {
4877 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4878 s.select_display_ranges([
4879 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4880 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4881 ])
4882 });
4883 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4884 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4885 assert_eq!(
4886 editor.selections.display_ranges(cx),
4887 vec![
4888 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4889 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4890 ]
4891 );
4892 });
4893
4894 // With `move_upwards` the selections stay in place, except for
4895 // the lines inserted above them
4896 let editor = cx.add_window(|window, cx| {
4897 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4898 build_editor(buffer, window, cx)
4899 });
4900 _ = editor.update(cx, |editor, window, cx| {
4901 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4902 s.select_display_ranges([
4903 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4904 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4905 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4906 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4907 ])
4908 });
4909 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4910 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4911 assert_eq!(
4912 editor.selections.display_ranges(cx),
4913 vec![
4914 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4915 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4916 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4917 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4918 ]
4919 );
4920 });
4921
4922 let editor = cx.add_window(|window, cx| {
4923 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4924 build_editor(buffer, window, cx)
4925 });
4926 _ = editor.update(cx, |editor, window, cx| {
4927 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4928 s.select_display_ranges([
4929 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4930 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4931 ])
4932 });
4933 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4934 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4935 assert_eq!(
4936 editor.selections.display_ranges(cx),
4937 vec![
4938 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4939 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
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_selection(&DuplicateSelection, window, cx);
4956 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\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(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4962 ]
4963 );
4964 });
4965}
4966
4967#[gpui::test]
4968fn test_move_line_up_down(cx: &mut TestAppContext) {
4969 init_test(cx, |_| {});
4970
4971 let editor = cx.add_window(|window, cx| {
4972 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4973 build_editor(buffer, window, cx)
4974 });
4975 _ = editor.update(cx, |editor, window, cx| {
4976 editor.fold_creases(
4977 vec![
4978 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4979 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4980 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4981 ],
4982 true,
4983 window,
4984 cx,
4985 );
4986 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4987 s.select_display_ranges([
4988 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4989 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4990 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4991 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4992 ])
4993 });
4994 assert_eq!(
4995 editor.display_text(cx),
4996 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4997 );
4998
4999 editor.move_line_up(&MoveLineUp, window, cx);
5000 assert_eq!(
5001 editor.display_text(cx),
5002 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5003 );
5004 assert_eq!(
5005 editor.selections.display_ranges(cx),
5006 vec![
5007 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5008 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5009 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5010 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5011 ]
5012 );
5013 });
5014
5015 _ = editor.update(cx, |editor, window, cx| {
5016 editor.move_line_down(&MoveLineDown, window, cx);
5017 assert_eq!(
5018 editor.display_text(cx),
5019 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5020 );
5021 assert_eq!(
5022 editor.selections.display_ranges(cx),
5023 vec![
5024 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5025 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5026 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5027 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5028 ]
5029 );
5030 });
5031
5032 _ = editor.update(cx, |editor, window, cx| {
5033 editor.move_line_down(&MoveLineDown, window, cx);
5034 assert_eq!(
5035 editor.display_text(cx),
5036 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5037 );
5038 assert_eq!(
5039 editor.selections.display_ranges(cx),
5040 vec![
5041 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5042 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5043 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5044 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5045 ]
5046 );
5047 });
5048
5049 _ = editor.update(cx, |editor, window, cx| {
5050 editor.move_line_up(&MoveLineUp, window, cx);
5051 assert_eq!(
5052 editor.display_text(cx),
5053 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5054 );
5055 assert_eq!(
5056 editor.selections.display_ranges(cx),
5057 vec![
5058 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5059 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5060 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5061 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5062 ]
5063 );
5064 });
5065}
5066
5067#[gpui::test]
5068fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5069 init_test(cx, |_| {});
5070
5071 let editor = cx.add_window(|window, cx| {
5072 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5073 build_editor(buffer, window, cx)
5074 });
5075 _ = editor.update(cx, |editor, window, cx| {
5076 let snapshot = editor.buffer.read(cx).snapshot(cx);
5077 editor.insert_blocks(
5078 [BlockProperties {
5079 style: BlockStyle::Fixed,
5080 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5081 height: Some(1),
5082 render: Arc::new(|_| div().into_any()),
5083 priority: 0,
5084 }],
5085 Some(Autoscroll::fit()),
5086 cx,
5087 );
5088 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5089 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5090 });
5091 editor.move_line_down(&MoveLineDown, window, cx);
5092 });
5093}
5094
5095#[gpui::test]
5096async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5097 init_test(cx, |_| {});
5098
5099 let mut cx = EditorTestContext::new(cx).await;
5100 cx.set_state(
5101 &"
5102 ˇzero
5103 one
5104 two
5105 three
5106 four
5107 five
5108 "
5109 .unindent(),
5110 );
5111
5112 // Create a four-line block that replaces three lines of text.
5113 cx.update_editor(|editor, window, cx| {
5114 let snapshot = editor.snapshot(window, cx);
5115 let snapshot = &snapshot.buffer_snapshot;
5116 let placement = BlockPlacement::Replace(
5117 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5118 );
5119 editor.insert_blocks(
5120 [BlockProperties {
5121 placement,
5122 height: Some(4),
5123 style: BlockStyle::Sticky,
5124 render: Arc::new(|_| gpui::div().into_any_element()),
5125 priority: 0,
5126 }],
5127 None,
5128 cx,
5129 );
5130 });
5131
5132 // Move down so that the cursor touches the block.
5133 cx.update_editor(|editor, window, cx| {
5134 editor.move_down(&Default::default(), window, cx);
5135 });
5136 cx.assert_editor_state(
5137 &"
5138 zero
5139 «one
5140 two
5141 threeˇ»
5142 four
5143 five
5144 "
5145 .unindent(),
5146 );
5147
5148 // Move down past the block.
5149 cx.update_editor(|editor, window, cx| {
5150 editor.move_down(&Default::default(), window, cx);
5151 });
5152 cx.assert_editor_state(
5153 &"
5154 zero
5155 one
5156 two
5157 three
5158 ˇfour
5159 five
5160 "
5161 .unindent(),
5162 );
5163}
5164
5165#[gpui::test]
5166fn test_transpose(cx: &mut TestAppContext) {
5167 init_test(cx, |_| {});
5168
5169 _ = cx.add_window(|window, cx| {
5170 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5171 editor.set_style(EditorStyle::default(), window, cx);
5172 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5173 s.select_ranges([1..1])
5174 });
5175 editor.transpose(&Default::default(), window, cx);
5176 assert_eq!(editor.text(cx), "bac");
5177 assert_eq!(editor.selections.ranges(cx), [2..2]);
5178
5179 editor.transpose(&Default::default(), window, cx);
5180 assert_eq!(editor.text(cx), "bca");
5181 assert_eq!(editor.selections.ranges(cx), [3..3]);
5182
5183 editor.transpose(&Default::default(), window, cx);
5184 assert_eq!(editor.text(cx), "bac");
5185 assert_eq!(editor.selections.ranges(cx), [3..3]);
5186
5187 editor
5188 });
5189
5190 _ = cx.add_window(|window, cx| {
5191 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5192 editor.set_style(EditorStyle::default(), window, cx);
5193 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5194 s.select_ranges([3..3])
5195 });
5196 editor.transpose(&Default::default(), window, cx);
5197 assert_eq!(editor.text(cx), "acb\nde");
5198 assert_eq!(editor.selections.ranges(cx), [3..3]);
5199
5200 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5201 s.select_ranges([4..4])
5202 });
5203 editor.transpose(&Default::default(), window, cx);
5204 assert_eq!(editor.text(cx), "acbd\ne");
5205 assert_eq!(editor.selections.ranges(cx), [5..5]);
5206
5207 editor.transpose(&Default::default(), window, cx);
5208 assert_eq!(editor.text(cx), "acbde\n");
5209 assert_eq!(editor.selections.ranges(cx), [6..6]);
5210
5211 editor.transpose(&Default::default(), window, cx);
5212 assert_eq!(editor.text(cx), "acbd\ne");
5213 assert_eq!(editor.selections.ranges(cx), [6..6]);
5214
5215 editor
5216 });
5217
5218 _ = cx.add_window(|window, cx| {
5219 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", 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, 2..2, 4..4])
5223 });
5224 editor.transpose(&Default::default(), window, cx);
5225 assert_eq!(editor.text(cx), "bacd\ne");
5226 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5227
5228 editor.transpose(&Default::default(), window, cx);
5229 assert_eq!(editor.text(cx), "bcade\n");
5230 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5231
5232 editor.transpose(&Default::default(), window, cx);
5233 assert_eq!(editor.text(cx), "bcda\ne");
5234 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5235
5236 editor.transpose(&Default::default(), window, cx);
5237 assert_eq!(editor.text(cx), "bcade\n");
5238 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5239
5240 editor.transpose(&Default::default(), window, cx);
5241 assert_eq!(editor.text(cx), "bcaed\n");
5242 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5243
5244 editor
5245 });
5246
5247 _ = cx.add_window(|window, cx| {
5248 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5249 editor.set_style(EditorStyle::default(), window, cx);
5250 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5251 s.select_ranges([4..4])
5252 });
5253 editor.transpose(&Default::default(), window, cx);
5254 assert_eq!(editor.text(cx), "🏀🍐✋");
5255 assert_eq!(editor.selections.ranges(cx), [8..8]);
5256
5257 editor.transpose(&Default::default(), window, cx);
5258 assert_eq!(editor.text(cx), "🏀✋🍐");
5259 assert_eq!(editor.selections.ranges(cx), [11..11]);
5260
5261 editor.transpose(&Default::default(), window, cx);
5262 assert_eq!(editor.text(cx), "🏀🍐✋");
5263 assert_eq!(editor.selections.ranges(cx), [11..11]);
5264
5265 editor
5266 });
5267}
5268
5269#[gpui::test]
5270async fn test_rewrap(cx: &mut TestAppContext) {
5271 init_test(cx, |settings| {
5272 settings.languages.0.extend([
5273 (
5274 "Markdown".into(),
5275 LanguageSettingsContent {
5276 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5277 preferred_line_length: Some(40),
5278 ..Default::default()
5279 },
5280 ),
5281 (
5282 "Plain Text".into(),
5283 LanguageSettingsContent {
5284 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5285 preferred_line_length: Some(40),
5286 ..Default::default()
5287 },
5288 ),
5289 (
5290 "C++".into(),
5291 LanguageSettingsContent {
5292 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5293 preferred_line_length: Some(40),
5294 ..Default::default()
5295 },
5296 ),
5297 (
5298 "Python".into(),
5299 LanguageSettingsContent {
5300 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5301 preferred_line_length: Some(40),
5302 ..Default::default()
5303 },
5304 ),
5305 (
5306 "Rust".into(),
5307 LanguageSettingsContent {
5308 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5309 preferred_line_length: Some(40),
5310 ..Default::default()
5311 },
5312 ),
5313 ])
5314 });
5315
5316 let mut cx = EditorTestContext::new(cx).await;
5317
5318 let cpp_language = Arc::new(Language::new(
5319 LanguageConfig {
5320 name: "C++".into(),
5321 line_comments: vec!["// ".into()],
5322 ..LanguageConfig::default()
5323 },
5324 None,
5325 ));
5326 let python_language = Arc::new(Language::new(
5327 LanguageConfig {
5328 name: "Python".into(),
5329 line_comments: vec!["# ".into()],
5330 ..LanguageConfig::default()
5331 },
5332 None,
5333 ));
5334 let markdown_language = Arc::new(Language::new(
5335 LanguageConfig {
5336 name: "Markdown".into(),
5337 rewrap_prefixes: vec![
5338 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5339 regex::Regex::new("[-*+]\\s+").unwrap(),
5340 ],
5341 ..LanguageConfig::default()
5342 },
5343 None,
5344 ));
5345 let rust_language = Arc::new(Language::new(
5346 LanguageConfig {
5347 name: "Rust".into(),
5348 line_comments: vec!["// ".into(), "/// ".into()],
5349 ..LanguageConfig::default()
5350 },
5351 Some(tree_sitter_rust::LANGUAGE.into()),
5352 ));
5353
5354 let plaintext_language = Arc::new(Language::new(
5355 LanguageConfig {
5356 name: "Plain Text".into(),
5357 ..LanguageConfig::default()
5358 },
5359 None,
5360 ));
5361
5362 // Test basic rewrapping of a long line with a cursor
5363 assert_rewrap(
5364 indoc! {"
5365 // ˇThis is a long comment that needs to be wrapped.
5366 "},
5367 indoc! {"
5368 // ˇThis is a long comment that needs to
5369 // be wrapped.
5370 "},
5371 cpp_language.clone(),
5372 &mut cx,
5373 );
5374
5375 // Test rewrapping a full selection
5376 assert_rewrap(
5377 indoc! {"
5378 «// This selected long comment needs to be wrapped.ˇ»"
5379 },
5380 indoc! {"
5381 «// This selected long comment needs to
5382 // be wrapped.ˇ»"
5383 },
5384 cpp_language.clone(),
5385 &mut cx,
5386 );
5387
5388 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5389 assert_rewrap(
5390 indoc! {"
5391 // ˇThis is the first line.
5392 // Thisˇ is the second line.
5393 // This is the thirdˇ line, all part of one paragraph.
5394 "},
5395 indoc! {"
5396 // ˇThis is the first line. Thisˇ is the
5397 // second line. This is the thirdˇ line,
5398 // all part of one paragraph.
5399 "},
5400 cpp_language.clone(),
5401 &mut cx,
5402 );
5403
5404 // Test multiple cursors in different paragraphs trigger separate rewraps
5405 assert_rewrap(
5406 indoc! {"
5407 // ˇThis is the first paragraph, first line.
5408 // ˇThis is the first paragraph, second line.
5409
5410 // ˇThis is the second paragraph, first line.
5411 // ˇThis is the second paragraph, second line.
5412 "},
5413 indoc! {"
5414 // ˇThis is the first paragraph, first
5415 // line. ˇThis is the first paragraph,
5416 // second line.
5417
5418 // ˇThis is the second paragraph, first
5419 // line. ˇThis is the second paragraph,
5420 // second line.
5421 "},
5422 cpp_language.clone(),
5423 &mut cx,
5424 );
5425
5426 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
5427 assert_rewrap(
5428 indoc! {"
5429 «// A regular long long comment to be wrapped.
5430 /// A documentation long comment to be wrapped.ˇ»
5431 "},
5432 indoc! {"
5433 «// A regular long long comment to be
5434 // wrapped.
5435 /// A documentation long comment to be
5436 /// wrapped.ˇ»
5437 "},
5438 rust_language.clone(),
5439 &mut cx,
5440 );
5441
5442 // Test that change in indentation level trigger seperate rewraps
5443 assert_rewrap(
5444 indoc! {"
5445 fn foo() {
5446 «// This is a long comment at the base indent.
5447 // This is a long comment at the next indent.ˇ»
5448 }
5449 "},
5450 indoc! {"
5451 fn foo() {
5452 «// This is a long comment at the
5453 // base indent.
5454 // This is a long comment at the
5455 // next indent.ˇ»
5456 }
5457 "},
5458 rust_language.clone(),
5459 &mut cx,
5460 );
5461
5462 // Test that different comment prefix characters (e.g., '#') are handled correctly
5463 assert_rewrap(
5464 indoc! {"
5465 # ˇThis is a long comment using a pound sign.
5466 "},
5467 indoc! {"
5468 # ˇThis is a long comment using a pound
5469 # sign.
5470 "},
5471 python_language.clone(),
5472 &mut cx,
5473 );
5474
5475 // Test rewrapping only affects comments, not code even when selected
5476 assert_rewrap(
5477 indoc! {"
5478 «/// This doc comment is long and should be wrapped.
5479 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5480 "},
5481 indoc! {"
5482 «/// This doc comment is long and should
5483 /// be wrapped.
5484 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5485 "},
5486 rust_language.clone(),
5487 &mut cx,
5488 );
5489
5490 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
5491 assert_rewrap(
5492 indoc! {"
5493 # Header
5494
5495 A long long long line of markdown text to wrap.ˇ
5496 "},
5497 indoc! {"
5498 # Header
5499
5500 A long long long line of markdown text
5501 to wrap.ˇ
5502 "},
5503 markdown_language.clone(),
5504 &mut cx,
5505 );
5506
5507 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
5508 assert_rewrap(
5509 indoc! {"
5510 «1. This is a numbered list item that is very long and needs to be wrapped properly.
5511 2. This is a numbered list item that is very long and needs to be wrapped properly.
5512 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
5513 "},
5514 indoc! {"
5515 «1. This is a numbered list item that is
5516 very long and needs to be wrapped
5517 properly.
5518 2. This is a numbered list item that is
5519 very long and needs to be wrapped
5520 properly.
5521 - This is an unordered list item that is
5522 also very long and should not merge
5523 with the numbered item.ˇ»
5524 "},
5525 markdown_language.clone(),
5526 &mut cx,
5527 );
5528
5529 // Test that rewrapping add indents for rewrapping boundary if not exists already.
5530 assert_rewrap(
5531 indoc! {"
5532 «1. This is a numbered list item that is
5533 very long and needs to be wrapped
5534 properly.
5535 2. This is a numbered list item that is
5536 very long and needs to be wrapped
5537 properly.
5538 - This is an unordered list item that is
5539 also very long and should not merge with
5540 the numbered item.ˇ»
5541 "},
5542 indoc! {"
5543 «1. This is a numbered list item that is
5544 very long and needs to be wrapped
5545 properly.
5546 2. This is a numbered list item that is
5547 very long and needs to be wrapped
5548 properly.
5549 - This is an unordered list item that is
5550 also very long and should not merge
5551 with the numbered item.ˇ»
5552 "},
5553 markdown_language.clone(),
5554 &mut cx,
5555 );
5556
5557 // Test that rewrapping maintain indents even when they already exists.
5558 assert_rewrap(
5559 indoc! {"
5560 «1. This is a numbered list
5561 item that is very long and needs to be wrapped properly.
5562 2. This is a numbered list
5563 item that is very long and needs to be wrapped properly.
5564 - This is an unordered list item that is also very long and
5565 should not merge with the numbered item.ˇ»
5566 "},
5567 indoc! {"
5568 «1. This is a numbered list item that is
5569 very long and needs to be wrapped
5570 properly.
5571 2. This is a numbered list item that is
5572 very long and needs to be wrapped
5573 properly.
5574 - This is an unordered list item that is
5575 also very long and should not merge
5576 with the numbered item.ˇ»
5577 "},
5578 markdown_language.clone(),
5579 &mut cx,
5580 );
5581
5582 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
5583 assert_rewrap(
5584 indoc! {"
5585 ˇThis is a very long line of plain text that will be wrapped.
5586 "},
5587 indoc! {"
5588 ˇThis is a very long line of plain text
5589 that will be wrapped.
5590 "},
5591 plaintext_language.clone(),
5592 &mut cx,
5593 );
5594
5595 // Test that non-commented code acts as a paragraph boundary within a selection
5596 assert_rewrap(
5597 indoc! {"
5598 «// This is the first long comment block to be wrapped.
5599 fn my_func(a: u32);
5600 // This is the second long comment block to be wrapped.ˇ»
5601 "},
5602 indoc! {"
5603 «// This is the first long comment block
5604 // to be wrapped.
5605 fn my_func(a: u32);
5606 // This is the second long comment block
5607 // to be wrapped.ˇ»
5608 "},
5609 rust_language.clone(),
5610 &mut cx,
5611 );
5612
5613 // Test rewrapping multiple selections, including ones with blank lines or tabs
5614 assert_rewrap(
5615 indoc! {"
5616 «ˇThis is a very long line that will be wrapped.
5617
5618 This is another paragraph in the same selection.»
5619
5620 «\tThis is a very long indented line that will be wrapped.ˇ»
5621 "},
5622 indoc! {"
5623 «ˇThis is a very long line that will be
5624 wrapped.
5625
5626 This is another paragraph in the same
5627 selection.»
5628
5629 «\tThis is a very long indented line
5630 \tthat will be wrapped.ˇ»
5631 "},
5632 plaintext_language.clone(),
5633 &mut cx,
5634 );
5635
5636 // Test that an empty comment line acts as a paragraph boundary
5637 assert_rewrap(
5638 indoc! {"
5639 // ˇThis is a long comment that will be wrapped.
5640 //
5641 // And this is another long comment that will also be wrapped.ˇ
5642 "},
5643 indoc! {"
5644 // ˇThis is a long comment that will be
5645 // wrapped.
5646 //
5647 // And this is another long comment that
5648 // will also be wrapped.ˇ
5649 "},
5650 cpp_language,
5651 &mut cx,
5652 );
5653
5654 #[track_caller]
5655 fn assert_rewrap(
5656 unwrapped_text: &str,
5657 wrapped_text: &str,
5658 language: Arc<Language>,
5659 cx: &mut EditorTestContext,
5660 ) {
5661 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5662 cx.set_state(unwrapped_text);
5663 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5664 cx.assert_editor_state(wrapped_text);
5665 }
5666}
5667
5668#[gpui::test]
5669async fn test_hard_wrap(cx: &mut TestAppContext) {
5670 init_test(cx, |_| {});
5671 let mut cx = EditorTestContext::new(cx).await;
5672
5673 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5674 cx.update_editor(|editor, _, cx| {
5675 editor.set_hard_wrap(Some(14), cx);
5676 });
5677
5678 cx.set_state(indoc!(
5679 "
5680 one two three ˇ
5681 "
5682 ));
5683 cx.simulate_input("four");
5684 cx.run_until_parked();
5685
5686 cx.assert_editor_state(indoc!(
5687 "
5688 one two three
5689 fourˇ
5690 "
5691 ));
5692
5693 cx.update_editor(|editor, window, cx| {
5694 editor.newline(&Default::default(), window, cx);
5695 });
5696 cx.run_until_parked();
5697 cx.assert_editor_state(indoc!(
5698 "
5699 one two three
5700 four
5701 ˇ
5702 "
5703 ));
5704
5705 cx.simulate_input("five");
5706 cx.run_until_parked();
5707 cx.assert_editor_state(indoc!(
5708 "
5709 one two three
5710 four
5711 fiveˇ
5712 "
5713 ));
5714
5715 cx.update_editor(|editor, window, cx| {
5716 editor.newline(&Default::default(), window, cx);
5717 });
5718 cx.run_until_parked();
5719 cx.simulate_input("# ");
5720 cx.run_until_parked();
5721 cx.assert_editor_state(indoc!(
5722 "
5723 one two three
5724 four
5725 five
5726 # ˇ
5727 "
5728 ));
5729
5730 cx.update_editor(|editor, window, cx| {
5731 editor.newline(&Default::default(), window, cx);
5732 });
5733 cx.run_until_parked();
5734 cx.assert_editor_state(indoc!(
5735 "
5736 one two three
5737 four
5738 five
5739 #\x20
5740 #ˇ
5741 "
5742 ));
5743
5744 cx.simulate_input(" 6");
5745 cx.run_until_parked();
5746 cx.assert_editor_state(indoc!(
5747 "
5748 one two three
5749 four
5750 five
5751 #
5752 # 6ˇ
5753 "
5754 ));
5755}
5756
5757#[gpui::test]
5758async fn test_clipboard(cx: &mut TestAppContext) {
5759 init_test(cx, |_| {});
5760
5761 let mut cx = EditorTestContext::new(cx).await;
5762
5763 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5764 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5765 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5766
5767 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5768 cx.set_state("two ˇfour ˇsix ˇ");
5769 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5770 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5771
5772 // Paste again but with only two cursors. Since the number of cursors doesn't
5773 // match the number of slices in the clipboard, the entire clipboard text
5774 // is pasted at each cursor.
5775 cx.set_state("ˇtwo one✅ four three six five ˇ");
5776 cx.update_editor(|e, window, cx| {
5777 e.handle_input("( ", window, cx);
5778 e.paste(&Paste, window, cx);
5779 e.handle_input(") ", window, cx);
5780 });
5781 cx.assert_editor_state(
5782 &([
5783 "( one✅ ",
5784 "three ",
5785 "five ) ˇtwo one✅ four three six five ( one✅ ",
5786 "three ",
5787 "five ) ˇ",
5788 ]
5789 .join("\n")),
5790 );
5791
5792 // Cut with three selections, one of which is full-line.
5793 cx.set_state(indoc! {"
5794 1«2ˇ»3
5795 4ˇ567
5796 «8ˇ»9"});
5797 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5798 cx.assert_editor_state(indoc! {"
5799 1ˇ3
5800 ˇ9"});
5801
5802 // Paste with three selections, noticing how the copied selection that was full-line
5803 // gets inserted before the second cursor.
5804 cx.set_state(indoc! {"
5805 1ˇ3
5806 9ˇ
5807 «oˇ»ne"});
5808 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5809 cx.assert_editor_state(indoc! {"
5810 12ˇ3
5811 4567
5812 9ˇ
5813 8ˇne"});
5814
5815 // Copy with a single cursor only, which writes the whole line into the clipboard.
5816 cx.set_state(indoc! {"
5817 The quick brown
5818 fox juˇmps over
5819 the lazy dog"});
5820 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5821 assert_eq!(
5822 cx.read_from_clipboard()
5823 .and_then(|item| item.text().as_deref().map(str::to_string)),
5824 Some("fox jumps over\n".to_string())
5825 );
5826
5827 // Paste with three selections, noticing how the copied full-line selection is inserted
5828 // before the empty selections but replaces the selection that is non-empty.
5829 cx.set_state(indoc! {"
5830 Tˇhe quick brown
5831 «foˇ»x jumps over
5832 tˇhe lazy dog"});
5833 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5834 cx.assert_editor_state(indoc! {"
5835 fox jumps over
5836 Tˇhe quick brown
5837 fox jumps over
5838 ˇx jumps over
5839 fox jumps over
5840 tˇhe lazy dog"});
5841}
5842
5843#[gpui::test]
5844async fn test_copy_trim(cx: &mut TestAppContext) {
5845 init_test(cx, |_| {});
5846
5847 let mut cx = EditorTestContext::new(cx).await;
5848 cx.set_state(
5849 r#" «for selection in selections.iter() {
5850 let mut start = selection.start;
5851 let mut end = selection.end;
5852 let is_entire_line = selection.is_empty();
5853 if is_entire_line {
5854 start = Point::new(start.row, 0);ˇ»
5855 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5856 }
5857 "#,
5858 );
5859 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5860 assert_eq!(
5861 cx.read_from_clipboard()
5862 .and_then(|item| item.text().as_deref().map(str::to_string)),
5863 Some(
5864 "for selection in selections.iter() {
5865 let mut start = selection.start;
5866 let mut end = selection.end;
5867 let is_entire_line = selection.is_empty();
5868 if is_entire_line {
5869 start = Point::new(start.row, 0);"
5870 .to_string()
5871 ),
5872 "Regular copying preserves all indentation selected",
5873 );
5874 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5875 assert_eq!(
5876 cx.read_from_clipboard()
5877 .and_then(|item| item.text().as_deref().map(str::to_string)),
5878 Some(
5879 "for selection in selections.iter() {
5880let mut start = selection.start;
5881let mut end = selection.end;
5882let is_entire_line = selection.is_empty();
5883if is_entire_line {
5884 start = Point::new(start.row, 0);"
5885 .to_string()
5886 ),
5887 "Copying with stripping should strip all leading whitespaces"
5888 );
5889
5890 cx.set_state(
5891 r#" « for selection in selections.iter() {
5892 let mut start = selection.start;
5893 let mut end = selection.end;
5894 let is_entire_line = selection.is_empty();
5895 if is_entire_line {
5896 start = Point::new(start.row, 0);ˇ»
5897 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5898 }
5899 "#,
5900 );
5901 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5902 assert_eq!(
5903 cx.read_from_clipboard()
5904 .and_then(|item| item.text().as_deref().map(str::to_string)),
5905 Some(
5906 " for selection in selections.iter() {
5907 let mut start = selection.start;
5908 let mut end = selection.end;
5909 let is_entire_line = selection.is_empty();
5910 if is_entire_line {
5911 start = Point::new(start.row, 0);"
5912 .to_string()
5913 ),
5914 "Regular copying preserves all indentation selected",
5915 );
5916 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5917 assert_eq!(
5918 cx.read_from_clipboard()
5919 .and_then(|item| item.text().as_deref().map(str::to_string)),
5920 Some(
5921 "for selection in selections.iter() {
5922let mut start = selection.start;
5923let mut end = selection.end;
5924let is_entire_line = selection.is_empty();
5925if is_entire_line {
5926 start = Point::new(start.row, 0);"
5927 .to_string()
5928 ),
5929 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5930 );
5931
5932 cx.set_state(
5933 r#" «ˇ for selection in selections.iter() {
5934 let mut start = selection.start;
5935 let mut end = selection.end;
5936 let is_entire_line = selection.is_empty();
5937 if is_entire_line {
5938 start = Point::new(start.row, 0);»
5939 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5940 }
5941 "#,
5942 );
5943 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5944 assert_eq!(
5945 cx.read_from_clipboard()
5946 .and_then(|item| item.text().as_deref().map(str::to_string)),
5947 Some(
5948 " for selection in selections.iter() {
5949 let mut start = selection.start;
5950 let mut end = selection.end;
5951 let is_entire_line = selection.is_empty();
5952 if is_entire_line {
5953 start = Point::new(start.row, 0);"
5954 .to_string()
5955 ),
5956 "Regular copying for reverse selection works the same",
5957 );
5958 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5959 assert_eq!(
5960 cx.read_from_clipboard()
5961 .and_then(|item| item.text().as_deref().map(str::to_string)),
5962 Some(
5963 "for selection in selections.iter() {
5964let mut start = selection.start;
5965let mut end = selection.end;
5966let is_entire_line = selection.is_empty();
5967if is_entire_line {
5968 start = Point::new(start.row, 0);"
5969 .to_string()
5970 ),
5971 "Copying with stripping for reverse selection works the same"
5972 );
5973
5974 cx.set_state(
5975 r#" for selection «in selections.iter() {
5976 let mut start = selection.start;
5977 let mut end = selection.end;
5978 let is_entire_line = selection.is_empty();
5979 if is_entire_line {
5980 start = Point::new(start.row, 0);ˇ»
5981 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5982 }
5983 "#,
5984 );
5985 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5986 assert_eq!(
5987 cx.read_from_clipboard()
5988 .and_then(|item| item.text().as_deref().map(str::to_string)),
5989 Some(
5990 "in selections.iter() {
5991 let mut start = selection.start;
5992 let mut end = selection.end;
5993 let is_entire_line = selection.is_empty();
5994 if is_entire_line {
5995 start = Point::new(start.row, 0);"
5996 .to_string()
5997 ),
5998 "When selecting past the indent, the copying works as usual",
5999 );
6000 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6001 assert_eq!(
6002 cx.read_from_clipboard()
6003 .and_then(|item| item.text().as_deref().map(str::to_string)),
6004 Some(
6005 "in selections.iter() {
6006 let mut start = selection.start;
6007 let mut end = selection.end;
6008 let is_entire_line = selection.is_empty();
6009 if is_entire_line {
6010 start = Point::new(start.row, 0);"
6011 .to_string()
6012 ),
6013 "When selecting past the indent, nothing is trimmed"
6014 );
6015
6016 cx.set_state(
6017 r#" «for selection in selections.iter() {
6018 let mut start = selection.start;
6019
6020 let mut end = selection.end;
6021 let is_entire_line = selection.is_empty();
6022 if is_entire_line {
6023 start = Point::new(start.row, 0);
6024ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
6025 }
6026 "#,
6027 );
6028 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6029 assert_eq!(
6030 cx.read_from_clipboard()
6031 .and_then(|item| item.text().as_deref().map(str::to_string)),
6032 Some(
6033 "for selection in selections.iter() {
6034let mut start = selection.start;
6035
6036let mut end = selection.end;
6037let is_entire_line = selection.is_empty();
6038if is_entire_line {
6039 start = Point::new(start.row, 0);
6040"
6041 .to_string()
6042 ),
6043 "Copying with stripping should ignore empty lines"
6044 );
6045}
6046
6047#[gpui::test]
6048async fn test_paste_multiline(cx: &mut TestAppContext) {
6049 init_test(cx, |_| {});
6050
6051 let mut cx = EditorTestContext::new(cx).await;
6052 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6053
6054 // Cut an indented block, without the leading whitespace.
6055 cx.set_state(indoc! {"
6056 const a: B = (
6057 c(),
6058 «d(
6059 e,
6060 f
6061 )ˇ»
6062 );
6063 "});
6064 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6065 cx.assert_editor_state(indoc! {"
6066 const a: B = (
6067 c(),
6068 ˇ
6069 );
6070 "});
6071
6072 // Paste it at the same position.
6073 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6074 cx.assert_editor_state(indoc! {"
6075 const a: B = (
6076 c(),
6077 d(
6078 e,
6079 f
6080 )ˇ
6081 );
6082 "});
6083
6084 // Paste it at a line with a lower indent level.
6085 cx.set_state(indoc! {"
6086 ˇ
6087 const a: B = (
6088 c(),
6089 );
6090 "});
6091 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6092 cx.assert_editor_state(indoc! {"
6093 d(
6094 e,
6095 f
6096 )ˇ
6097 const a: B = (
6098 c(),
6099 );
6100 "});
6101
6102 // Cut an indented block, with the leading whitespace.
6103 cx.set_state(indoc! {"
6104 const a: B = (
6105 c(),
6106 « d(
6107 e,
6108 f
6109 )
6110 ˇ»);
6111 "});
6112 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6113 cx.assert_editor_state(indoc! {"
6114 const a: B = (
6115 c(),
6116 ˇ);
6117 "});
6118
6119 // Paste it at the same position.
6120 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6121 cx.assert_editor_state(indoc! {"
6122 const a: B = (
6123 c(),
6124 d(
6125 e,
6126 f
6127 )
6128 ˇ);
6129 "});
6130
6131 // Paste it at a line with a higher indent level.
6132 cx.set_state(indoc! {"
6133 const a: B = (
6134 c(),
6135 d(
6136 e,
6137 fˇ
6138 )
6139 );
6140 "});
6141 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6142 cx.assert_editor_state(indoc! {"
6143 const a: B = (
6144 c(),
6145 d(
6146 e,
6147 f d(
6148 e,
6149 f
6150 )
6151 ˇ
6152 )
6153 );
6154 "});
6155
6156 // Copy an indented block, starting mid-line
6157 cx.set_state(indoc! {"
6158 const a: B = (
6159 c(),
6160 somethin«g(
6161 e,
6162 f
6163 )ˇ»
6164 );
6165 "});
6166 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6167
6168 // Paste it on a line with a lower indent level
6169 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
6170 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6171 cx.assert_editor_state(indoc! {"
6172 const a: B = (
6173 c(),
6174 something(
6175 e,
6176 f
6177 )
6178 );
6179 g(
6180 e,
6181 f
6182 )ˇ"});
6183}
6184
6185#[gpui::test]
6186async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
6187 init_test(cx, |_| {});
6188
6189 cx.write_to_clipboard(ClipboardItem::new_string(
6190 " d(\n e\n );\n".into(),
6191 ));
6192
6193 let mut cx = EditorTestContext::new(cx).await;
6194 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6195
6196 cx.set_state(indoc! {"
6197 fn a() {
6198 b();
6199 if c() {
6200 ˇ
6201 }
6202 }
6203 "});
6204
6205 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6206 cx.assert_editor_state(indoc! {"
6207 fn a() {
6208 b();
6209 if c() {
6210 d(
6211 e
6212 );
6213 ˇ
6214 }
6215 }
6216 "});
6217
6218 cx.set_state(indoc! {"
6219 fn a() {
6220 b();
6221 ˇ
6222 }
6223 "});
6224
6225 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6226 cx.assert_editor_state(indoc! {"
6227 fn a() {
6228 b();
6229 d(
6230 e
6231 );
6232 ˇ
6233 }
6234 "});
6235}
6236
6237#[gpui::test]
6238fn test_select_all(cx: &mut TestAppContext) {
6239 init_test(cx, |_| {});
6240
6241 let editor = cx.add_window(|window, cx| {
6242 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
6243 build_editor(buffer, window, cx)
6244 });
6245 _ = editor.update(cx, |editor, window, cx| {
6246 editor.select_all(&SelectAll, window, cx);
6247 assert_eq!(
6248 editor.selections.display_ranges(cx),
6249 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
6250 );
6251 });
6252}
6253
6254#[gpui::test]
6255fn test_select_line(cx: &mut TestAppContext) {
6256 init_test(cx, |_| {});
6257
6258 let editor = cx.add_window(|window, cx| {
6259 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
6260 build_editor(buffer, window, cx)
6261 });
6262 _ = editor.update(cx, |editor, window, cx| {
6263 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6264 s.select_display_ranges([
6265 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6266 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6267 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6268 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
6269 ])
6270 });
6271 editor.select_line(&SelectLine, window, cx);
6272 assert_eq!(
6273 editor.selections.display_ranges(cx),
6274 vec![
6275 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
6276 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
6277 ]
6278 );
6279 });
6280
6281 _ = editor.update(cx, |editor, window, cx| {
6282 editor.select_line(&SelectLine, window, cx);
6283 assert_eq!(
6284 editor.selections.display_ranges(cx),
6285 vec![
6286 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
6287 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
6288 ]
6289 );
6290 });
6291
6292 _ = editor.update(cx, |editor, window, cx| {
6293 editor.select_line(&SelectLine, window, cx);
6294 assert_eq!(
6295 editor.selections.display_ranges(cx),
6296 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
6297 );
6298 });
6299}
6300
6301#[gpui::test]
6302async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
6303 init_test(cx, |_| {});
6304 let mut cx = EditorTestContext::new(cx).await;
6305
6306 #[track_caller]
6307 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
6308 cx.set_state(initial_state);
6309 cx.update_editor(|e, window, cx| {
6310 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
6311 });
6312 cx.assert_editor_state(expected_state);
6313 }
6314
6315 // Selection starts and ends at the middle of lines, left-to-right
6316 test(
6317 &mut cx,
6318 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
6319 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6320 );
6321 // Same thing, right-to-left
6322 test(
6323 &mut cx,
6324 "aa\nb«b\ncc\ndd\neˇ»e\nff",
6325 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6326 );
6327
6328 // Whole buffer, left-to-right, last line *doesn't* end with newline
6329 test(
6330 &mut cx,
6331 "«ˇaa\nbb\ncc\ndd\nee\nff»",
6332 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6333 );
6334 // Same thing, right-to-left
6335 test(
6336 &mut cx,
6337 "«aa\nbb\ncc\ndd\nee\nffˇ»",
6338 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6339 );
6340
6341 // Whole buffer, left-to-right, last line ends with newline
6342 test(
6343 &mut cx,
6344 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
6345 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6346 );
6347 // Same thing, right-to-left
6348 test(
6349 &mut cx,
6350 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
6351 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6352 );
6353
6354 // Starts at the end of a line, ends at the start of another
6355 test(
6356 &mut cx,
6357 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
6358 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
6359 );
6360}
6361
6362#[gpui::test]
6363async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
6364 init_test(cx, |_| {});
6365
6366 let editor = cx.add_window(|window, cx| {
6367 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
6368 build_editor(buffer, window, cx)
6369 });
6370
6371 // setup
6372 _ = editor.update(cx, |editor, window, cx| {
6373 editor.fold_creases(
6374 vec![
6375 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
6376 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
6377 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
6378 ],
6379 true,
6380 window,
6381 cx,
6382 );
6383 assert_eq!(
6384 editor.display_text(cx),
6385 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6386 );
6387 });
6388
6389 _ = editor.update(cx, |editor, window, cx| {
6390 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6391 s.select_display_ranges([
6392 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6393 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6394 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6395 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
6396 ])
6397 });
6398 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6399 assert_eq!(
6400 editor.display_text(cx),
6401 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6402 );
6403 });
6404 EditorTestContext::for_editor(editor, cx)
6405 .await
6406 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
6407
6408 _ = editor.update(cx, |editor, window, cx| {
6409 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6410 s.select_display_ranges([
6411 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
6412 ])
6413 });
6414 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6415 assert_eq!(
6416 editor.display_text(cx),
6417 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
6418 );
6419 assert_eq!(
6420 editor.selections.display_ranges(cx),
6421 [
6422 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
6423 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
6424 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
6425 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
6426 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
6427 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
6428 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
6429 ]
6430 );
6431 });
6432 EditorTestContext::for_editor(editor, cx)
6433 .await
6434 .assert_editor_state(
6435 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
6436 );
6437}
6438
6439#[gpui::test]
6440async fn test_add_selection_above_below(cx: &mut TestAppContext) {
6441 init_test(cx, |_| {});
6442
6443 let mut cx = EditorTestContext::new(cx).await;
6444
6445 cx.set_state(indoc!(
6446 r#"abc
6447 defˇghi
6448
6449 jk
6450 nlmo
6451 "#
6452 ));
6453
6454 cx.update_editor(|editor, window, cx| {
6455 editor.add_selection_above(&Default::default(), window, cx);
6456 });
6457
6458 cx.assert_editor_state(indoc!(
6459 r#"abcˇ
6460 defˇghi
6461
6462 jk
6463 nlmo
6464 "#
6465 ));
6466
6467 cx.update_editor(|editor, window, cx| {
6468 editor.add_selection_above(&Default::default(), window, cx);
6469 });
6470
6471 cx.assert_editor_state(indoc!(
6472 r#"abcˇ
6473 defˇghi
6474
6475 jk
6476 nlmo
6477 "#
6478 ));
6479
6480 cx.update_editor(|editor, window, cx| {
6481 editor.add_selection_below(&Default::default(), window, cx);
6482 });
6483
6484 cx.assert_editor_state(indoc!(
6485 r#"abc
6486 defˇghi
6487
6488 jk
6489 nlmo
6490 "#
6491 ));
6492
6493 cx.update_editor(|editor, window, cx| {
6494 editor.undo_selection(&Default::default(), window, cx);
6495 });
6496
6497 cx.assert_editor_state(indoc!(
6498 r#"abcˇ
6499 defˇghi
6500
6501 jk
6502 nlmo
6503 "#
6504 ));
6505
6506 cx.update_editor(|editor, window, cx| {
6507 editor.redo_selection(&Default::default(), window, cx);
6508 });
6509
6510 cx.assert_editor_state(indoc!(
6511 r#"abc
6512 defˇghi
6513
6514 jk
6515 nlmo
6516 "#
6517 ));
6518
6519 cx.update_editor(|editor, window, cx| {
6520 editor.add_selection_below(&Default::default(), window, cx);
6521 });
6522
6523 cx.assert_editor_state(indoc!(
6524 r#"abc
6525 defˇghi
6526 ˇ
6527 jk
6528 nlmo
6529 "#
6530 ));
6531
6532 cx.update_editor(|editor, window, cx| {
6533 editor.add_selection_below(&Default::default(), window, cx);
6534 });
6535
6536 cx.assert_editor_state(indoc!(
6537 r#"abc
6538 defˇghi
6539 ˇ
6540 jkˇ
6541 nlmo
6542 "#
6543 ));
6544
6545 cx.update_editor(|editor, window, cx| {
6546 editor.add_selection_below(&Default::default(), window, cx);
6547 });
6548
6549 cx.assert_editor_state(indoc!(
6550 r#"abc
6551 defˇghi
6552 ˇ
6553 jkˇ
6554 nlmˇo
6555 "#
6556 ));
6557
6558 cx.update_editor(|editor, window, cx| {
6559 editor.add_selection_below(&Default::default(), window, cx);
6560 });
6561
6562 cx.assert_editor_state(indoc!(
6563 r#"abc
6564 defˇghi
6565 ˇ
6566 jkˇ
6567 nlmˇo
6568 ˇ"#
6569 ));
6570
6571 // change selections
6572 cx.set_state(indoc!(
6573 r#"abc
6574 def«ˇg»hi
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«ˇg»hi
6588
6589 jk
6590 nlm«ˇo»
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«ˇg»hi
6601
6602 jk
6603 nlm«ˇo»
6604 "#
6605 ));
6606
6607 cx.update_editor(|editor, window, cx| {
6608 editor.add_selection_above(&Default::default(), window, cx);
6609 });
6610
6611 cx.assert_editor_state(indoc!(
6612 r#"abc
6613 def«ˇg»hi
6614
6615 jk
6616 nlmo
6617 "#
6618 ));
6619
6620 cx.update_editor(|editor, window, cx| {
6621 editor.add_selection_above(&Default::default(), window, cx);
6622 });
6623
6624 cx.assert_editor_state(indoc!(
6625 r#"abc
6626 def«ˇg»hi
6627
6628 jk
6629 nlmo
6630 "#
6631 ));
6632
6633 // Change selections again
6634 cx.set_state(indoc!(
6635 r#"a«bc
6636 defgˇ»hi
6637
6638 jk
6639 nlmo
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#"a«bcˇ»
6649 d«efgˇ»hi
6650
6651 j«kˇ»
6652 nlmo
6653 "#
6654 ));
6655
6656 cx.update_editor(|editor, window, cx| {
6657 editor.add_selection_below(&Default::default(), window, cx);
6658 });
6659 cx.assert_editor_state(indoc!(
6660 r#"a«bcˇ»
6661 d«efgˇ»hi
6662
6663 j«kˇ»
6664 n«lmoˇ»
6665 "#
6666 ));
6667 cx.update_editor(|editor, window, cx| {
6668 editor.add_selection_above(&Default::default(), window, cx);
6669 });
6670
6671 cx.assert_editor_state(indoc!(
6672 r#"a«bcˇ»
6673 d«efgˇ»hi
6674
6675 j«kˇ»
6676 nlmo
6677 "#
6678 ));
6679
6680 // Change selections again
6681 cx.set_state(indoc!(
6682 r#"abc
6683 d«ˇefghi
6684
6685 jk
6686 nlm»o
6687 "#
6688 ));
6689
6690 cx.update_editor(|editor, window, cx| {
6691 editor.add_selection_above(&Default::default(), window, cx);
6692 });
6693
6694 cx.assert_editor_state(indoc!(
6695 r#"a«ˇbc»
6696 d«ˇef»ghi
6697
6698 j«ˇk»
6699 n«ˇlm»o
6700 "#
6701 ));
6702
6703 cx.update_editor(|editor, window, cx| {
6704 editor.add_selection_below(&Default::default(), window, cx);
6705 });
6706
6707 cx.assert_editor_state(indoc!(
6708 r#"abc
6709 d«ˇef»ghi
6710
6711 j«ˇk»
6712 n«ˇlm»o
6713 "#
6714 ));
6715}
6716
6717#[gpui::test]
6718async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
6719 init_test(cx, |_| {});
6720 let mut cx = EditorTestContext::new(cx).await;
6721
6722 cx.set_state(indoc!(
6723 r#"line onˇe
6724 liˇne two
6725 line three
6726 line four"#
6727 ));
6728
6729 cx.update_editor(|editor, window, cx| {
6730 editor.add_selection_below(&Default::default(), window, cx);
6731 });
6732
6733 // test multiple cursors expand in the same direction
6734 cx.assert_editor_state(indoc!(
6735 r#"line onˇe
6736 liˇne twˇo
6737 liˇne three
6738 line four"#
6739 ));
6740
6741 cx.update_editor(|editor, window, cx| {
6742 editor.add_selection_below(&Default::default(), window, cx);
6743 });
6744
6745 cx.update_editor(|editor, window, cx| {
6746 editor.add_selection_below(&Default::default(), window, cx);
6747 });
6748
6749 // test multiple cursors expand below overflow
6750 cx.assert_editor_state(indoc!(
6751 r#"line onˇe
6752 liˇne twˇo
6753 liˇne thˇree
6754 liˇne foˇur"#
6755 ));
6756
6757 cx.update_editor(|editor, window, cx| {
6758 editor.add_selection_above(&Default::default(), window, cx);
6759 });
6760
6761 // test multiple cursors retrieves back correctly
6762 cx.assert_editor_state(indoc!(
6763 r#"line onˇe
6764 liˇne twˇo
6765 liˇne thˇree
6766 line four"#
6767 ));
6768
6769 cx.update_editor(|editor, window, cx| {
6770 editor.add_selection_above(&Default::default(), window, cx);
6771 });
6772
6773 cx.update_editor(|editor, window, cx| {
6774 editor.add_selection_above(&Default::default(), window, cx);
6775 });
6776
6777 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
6778 cx.assert_editor_state(indoc!(
6779 r#"liˇne onˇe
6780 liˇne two
6781 line three
6782 line four"#
6783 ));
6784
6785 cx.update_editor(|editor, window, cx| {
6786 editor.undo_selection(&Default::default(), window, cx);
6787 });
6788
6789 // test undo
6790 cx.assert_editor_state(indoc!(
6791 r#"line onˇe
6792 liˇne twˇo
6793 line three
6794 line four"#
6795 ));
6796
6797 cx.update_editor(|editor, window, cx| {
6798 editor.redo_selection(&Default::default(), window, cx);
6799 });
6800
6801 // test redo
6802 cx.assert_editor_state(indoc!(
6803 r#"liˇne onˇe
6804 liˇne two
6805 line three
6806 line four"#
6807 ));
6808
6809 cx.set_state(indoc!(
6810 r#"abcd
6811 ef«ghˇ»
6812 ijkl
6813 «mˇ»nop"#
6814 ));
6815
6816 cx.update_editor(|editor, window, cx| {
6817 editor.add_selection_above(&Default::default(), window, cx);
6818 });
6819
6820 // test multiple selections expand in the same direction
6821 cx.assert_editor_state(indoc!(
6822 r#"ab«cdˇ»
6823 ef«ghˇ»
6824 «iˇ»jkl
6825 «mˇ»nop"#
6826 ));
6827
6828 cx.update_editor(|editor, window, cx| {
6829 editor.add_selection_above(&Default::default(), window, cx);
6830 });
6831
6832 // test multiple selection upward overflow
6833 cx.assert_editor_state(indoc!(
6834 r#"ab«cdˇ»
6835 «eˇ»f«ghˇ»
6836 «iˇ»jkl
6837 «mˇ»nop"#
6838 ));
6839
6840 cx.update_editor(|editor, window, cx| {
6841 editor.add_selection_below(&Default::default(), window, cx);
6842 });
6843
6844 // test multiple selection retrieves back correctly
6845 cx.assert_editor_state(indoc!(
6846 r#"abcd
6847 ef«ghˇ»
6848 «iˇ»jkl
6849 «mˇ»nop"#
6850 ));
6851
6852 cx.update_editor(|editor, window, cx| {
6853 editor.add_selection_below(&Default::default(), window, cx);
6854 });
6855
6856 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
6857 cx.assert_editor_state(indoc!(
6858 r#"abcd
6859 ef«ghˇ»
6860 ij«klˇ»
6861 «mˇ»nop"#
6862 ));
6863
6864 cx.update_editor(|editor, window, cx| {
6865 editor.undo_selection(&Default::default(), window, cx);
6866 });
6867
6868 // test undo
6869 cx.assert_editor_state(indoc!(
6870 r#"abcd
6871 ef«ghˇ»
6872 «iˇ»jkl
6873 «mˇ»nop"#
6874 ));
6875
6876 cx.update_editor(|editor, window, cx| {
6877 editor.redo_selection(&Default::default(), window, cx);
6878 });
6879
6880 // test redo
6881 cx.assert_editor_state(indoc!(
6882 r#"abcd
6883 ef«ghˇ»
6884 ij«klˇ»
6885 «mˇ»nop"#
6886 ));
6887}
6888
6889#[gpui::test]
6890async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
6891 init_test(cx, |_| {});
6892 let mut cx = EditorTestContext::new(cx).await;
6893
6894 cx.set_state(indoc!(
6895 r#"line onˇe
6896 liˇne two
6897 line three
6898 line four"#
6899 ));
6900
6901 cx.update_editor(|editor, window, cx| {
6902 editor.add_selection_below(&Default::default(), window, cx);
6903 editor.add_selection_below(&Default::default(), window, cx);
6904 editor.add_selection_below(&Default::default(), window, cx);
6905 });
6906
6907 // initial state with two multi cursor groups
6908 cx.assert_editor_state(indoc!(
6909 r#"line onˇe
6910 liˇne twˇo
6911 liˇne thˇree
6912 liˇne foˇur"#
6913 ));
6914
6915 // add single cursor in middle - simulate opt click
6916 cx.update_editor(|editor, window, cx| {
6917 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
6918 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6919 editor.end_selection(window, cx);
6920 });
6921
6922 cx.assert_editor_state(indoc!(
6923 r#"line onˇe
6924 liˇne twˇo
6925 liˇneˇ thˇree
6926 liˇne foˇur"#
6927 ));
6928
6929 cx.update_editor(|editor, window, cx| {
6930 editor.add_selection_above(&Default::default(), window, cx);
6931 });
6932
6933 // test new added selection expands above and existing selection shrinks
6934 cx.assert_editor_state(indoc!(
6935 r#"line onˇe
6936 liˇneˇ twˇo
6937 liˇneˇ thˇree
6938 line four"#
6939 ));
6940
6941 cx.update_editor(|editor, window, cx| {
6942 editor.add_selection_above(&Default::default(), window, cx);
6943 });
6944
6945 // test new added selection expands above and existing selection shrinks
6946 cx.assert_editor_state(indoc!(
6947 r#"lineˇ onˇe
6948 liˇneˇ twˇo
6949 lineˇ three
6950 line four"#
6951 ));
6952
6953 // intial state with two selection groups
6954 cx.set_state(indoc!(
6955 r#"abcd
6956 ef«ghˇ»
6957 ijkl
6958 «mˇ»nop"#
6959 ));
6960
6961 cx.update_editor(|editor, window, cx| {
6962 editor.add_selection_above(&Default::default(), window, cx);
6963 editor.add_selection_above(&Default::default(), window, cx);
6964 });
6965
6966 cx.assert_editor_state(indoc!(
6967 r#"ab«cdˇ»
6968 «eˇ»f«ghˇ»
6969 «iˇ»jkl
6970 «mˇ»nop"#
6971 ));
6972
6973 // add single selection in middle - simulate opt drag
6974 cx.update_editor(|editor, window, cx| {
6975 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
6976 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6977 editor.update_selection(
6978 DisplayPoint::new(DisplayRow(2), 4),
6979 0,
6980 gpui::Point::<f32>::default(),
6981 window,
6982 cx,
6983 );
6984 editor.end_selection(window, cx);
6985 });
6986
6987 cx.assert_editor_state(indoc!(
6988 r#"ab«cdˇ»
6989 «eˇ»f«ghˇ»
6990 «iˇ»jk«lˇ»
6991 «mˇ»nop"#
6992 ));
6993
6994 cx.update_editor(|editor, window, cx| {
6995 editor.add_selection_below(&Default::default(), window, cx);
6996 });
6997
6998 // test new added selection expands below, others shrinks from above
6999 cx.assert_editor_state(indoc!(
7000 r#"abcd
7001 ef«ghˇ»
7002 «iˇ»jk«lˇ»
7003 «mˇ»no«pˇ»"#
7004 ));
7005}
7006
7007#[gpui::test]
7008async fn test_select_next(cx: &mut TestAppContext) {
7009 init_test(cx, |_| {});
7010
7011 let mut cx = EditorTestContext::new(cx).await;
7012 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7013
7014 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7015 .unwrap();
7016 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7017
7018 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7019 .unwrap();
7020 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7021
7022 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7023 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7024
7025 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7026 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7027
7028 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7029 .unwrap();
7030 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7031
7032 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7033 .unwrap();
7034 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7035
7036 // Test selection direction should be preserved
7037 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7038
7039 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7040 .unwrap();
7041 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
7042}
7043
7044#[gpui::test]
7045async fn test_select_all_matches(cx: &mut TestAppContext) {
7046 init_test(cx, |_| {});
7047
7048 let mut cx = EditorTestContext::new(cx).await;
7049
7050 // Test caret-only selections
7051 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7052 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7053 .unwrap();
7054 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7055
7056 // Test left-to-right selections
7057 cx.set_state("abc\n«abcˇ»\nabc");
7058 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7059 .unwrap();
7060 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
7061
7062 // Test right-to-left selections
7063 cx.set_state("abc\n«ˇabc»\nabc");
7064 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7065 .unwrap();
7066 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
7067
7068 // Test selecting whitespace with caret selection
7069 cx.set_state("abc\nˇ abc\nabc");
7070 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7071 .unwrap();
7072 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7073
7074 // Test selecting whitespace with left-to-right selection
7075 cx.set_state("abc\n«ˇ »abc\nabc");
7076 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7077 .unwrap();
7078 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
7079
7080 // Test no matches with right-to-left selection
7081 cx.set_state("abc\n« ˇ»abc\nabc");
7082 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7083 .unwrap();
7084 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7085
7086 // Test with a single word and clip_at_line_ends=true (#29823)
7087 cx.set_state("aˇbc");
7088 cx.update_editor(|e, window, cx| {
7089 e.set_clip_at_line_ends(true, cx);
7090 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
7091 e.set_clip_at_line_ends(false, cx);
7092 });
7093 cx.assert_editor_state("«abcˇ»");
7094}
7095
7096#[gpui::test]
7097async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
7098 init_test(cx, |_| {});
7099
7100 let mut cx = EditorTestContext::new(cx).await;
7101
7102 let large_body_1 = "\nd".repeat(200);
7103 let large_body_2 = "\ne".repeat(200);
7104
7105 cx.set_state(&format!(
7106 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
7107 ));
7108 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
7109 let scroll_position = editor.scroll_position(cx);
7110 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
7111 scroll_position
7112 });
7113
7114 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7115 .unwrap();
7116 cx.assert_editor_state(&format!(
7117 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
7118 ));
7119 let scroll_position_after_selection =
7120 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
7121 assert_eq!(
7122 initial_scroll_position, scroll_position_after_selection,
7123 "Scroll position should not change after selecting all matches"
7124 );
7125}
7126
7127#[gpui::test]
7128async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
7129 init_test(cx, |_| {});
7130
7131 let mut cx = EditorLspTestContext::new_rust(
7132 lsp::ServerCapabilities {
7133 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7134 ..Default::default()
7135 },
7136 cx,
7137 )
7138 .await;
7139
7140 cx.set_state(indoc! {"
7141 line 1
7142 line 2
7143 linˇe 3
7144 line 4
7145 line 5
7146 "});
7147
7148 // Make an edit
7149 cx.update_editor(|editor, window, cx| {
7150 editor.handle_input("X", window, cx);
7151 });
7152
7153 // Move cursor to a different position
7154 cx.update_editor(|editor, window, cx| {
7155 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7156 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
7157 });
7158 });
7159
7160 cx.assert_editor_state(indoc! {"
7161 line 1
7162 line 2
7163 linXe 3
7164 line 4
7165 liˇne 5
7166 "});
7167
7168 cx.lsp
7169 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
7170 Ok(Some(vec![lsp::TextEdit::new(
7171 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
7172 "PREFIX ".to_string(),
7173 )]))
7174 });
7175
7176 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
7177 .unwrap()
7178 .await
7179 .unwrap();
7180
7181 cx.assert_editor_state(indoc! {"
7182 PREFIX line 1
7183 line 2
7184 linXe 3
7185 line 4
7186 liˇne 5
7187 "});
7188
7189 // Undo formatting
7190 cx.update_editor(|editor, window, cx| {
7191 editor.undo(&Default::default(), window, cx);
7192 });
7193
7194 // Verify cursor moved back to position after edit
7195 cx.assert_editor_state(indoc! {"
7196 line 1
7197 line 2
7198 linXˇe 3
7199 line 4
7200 line 5
7201 "});
7202}
7203
7204#[gpui::test]
7205async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) {
7206 init_test(cx, |_| {});
7207
7208 let mut cx = EditorTestContext::new(cx).await;
7209
7210 let provider = cx.new(|_| FakeInlineCompletionProvider::default());
7211 cx.update_editor(|editor, window, cx| {
7212 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
7213 });
7214
7215 cx.set_state(indoc! {"
7216 line 1
7217 line 2
7218 linˇe 3
7219 line 4
7220 line 5
7221 line 6
7222 line 7
7223 line 8
7224 line 9
7225 line 10
7226 "});
7227
7228 let snapshot = cx.buffer_snapshot();
7229 let edit_position = snapshot.anchor_after(Point::new(2, 4));
7230
7231 cx.update(|_, cx| {
7232 provider.update(cx, |provider, _| {
7233 provider.set_inline_completion(Some(inline_completion::InlineCompletion {
7234 id: None,
7235 edits: vec![(edit_position..edit_position, "X".into())],
7236 edit_preview: None,
7237 }))
7238 })
7239 });
7240
7241 cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
7242 cx.update_editor(|editor, window, cx| {
7243 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
7244 });
7245
7246 cx.assert_editor_state(indoc! {"
7247 line 1
7248 line 2
7249 lineXˇ 3
7250 line 4
7251 line 5
7252 line 6
7253 line 7
7254 line 8
7255 line 9
7256 line 10
7257 "});
7258
7259 cx.update_editor(|editor, window, cx| {
7260 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7261 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
7262 });
7263 });
7264
7265 cx.assert_editor_state(indoc! {"
7266 line 1
7267 line 2
7268 lineX 3
7269 line 4
7270 line 5
7271 line 6
7272 line 7
7273 line 8
7274 line 9
7275 liˇne 10
7276 "});
7277
7278 cx.update_editor(|editor, window, cx| {
7279 editor.undo(&Default::default(), window, cx);
7280 });
7281
7282 cx.assert_editor_state(indoc! {"
7283 line 1
7284 line 2
7285 lineˇ 3
7286 line 4
7287 line 5
7288 line 6
7289 line 7
7290 line 8
7291 line 9
7292 line 10
7293 "});
7294}
7295
7296#[gpui::test]
7297async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
7298 init_test(cx, |_| {});
7299
7300 let mut cx = EditorTestContext::new(cx).await;
7301 cx.set_state(
7302 r#"let foo = 2;
7303lˇet foo = 2;
7304let fooˇ = 2;
7305let foo = 2;
7306let foo = ˇ2;"#,
7307 );
7308
7309 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7310 .unwrap();
7311 cx.assert_editor_state(
7312 r#"let foo = 2;
7313«letˇ» foo = 2;
7314let «fooˇ» = 2;
7315let foo = 2;
7316let foo = «2ˇ»;"#,
7317 );
7318
7319 // noop for multiple selections with different contents
7320 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7321 .unwrap();
7322 cx.assert_editor_state(
7323 r#"let foo = 2;
7324«letˇ» foo = 2;
7325let «fooˇ» = 2;
7326let foo = 2;
7327let foo = «2ˇ»;"#,
7328 );
7329
7330 // Test last selection direction should be preserved
7331 cx.set_state(
7332 r#"let foo = 2;
7333let foo = 2;
7334let «fooˇ» = 2;
7335let «ˇfoo» = 2;
7336let foo = 2;"#,
7337 );
7338
7339 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7340 .unwrap();
7341 cx.assert_editor_state(
7342 r#"let foo = 2;
7343let foo = 2;
7344let «fooˇ» = 2;
7345let «ˇfoo» = 2;
7346let «ˇfoo» = 2;"#,
7347 );
7348}
7349
7350#[gpui::test]
7351async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
7352 init_test(cx, |_| {});
7353
7354 let mut cx =
7355 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
7356
7357 cx.assert_editor_state(indoc! {"
7358 ˇbbb
7359 ccc
7360
7361 bbb
7362 ccc
7363 "});
7364 cx.dispatch_action(SelectPrevious::default());
7365 cx.assert_editor_state(indoc! {"
7366 «bbbˇ»
7367 ccc
7368
7369 bbb
7370 ccc
7371 "});
7372 cx.dispatch_action(SelectPrevious::default());
7373 cx.assert_editor_state(indoc! {"
7374 «bbbˇ»
7375 ccc
7376
7377 «bbbˇ»
7378 ccc
7379 "});
7380}
7381
7382#[gpui::test]
7383async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
7384 init_test(cx, |_| {});
7385
7386 let mut cx = EditorTestContext::new(cx).await;
7387 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7388
7389 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7390 .unwrap();
7391 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7392
7393 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7394 .unwrap();
7395 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7396
7397 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7398 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7399
7400 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7401 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7402
7403 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7404 .unwrap();
7405 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
7406
7407 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7408 .unwrap();
7409 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7410}
7411
7412#[gpui::test]
7413async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
7414 init_test(cx, |_| {});
7415
7416 let mut cx = EditorTestContext::new(cx).await;
7417 cx.set_state("aˇ");
7418
7419 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7420 .unwrap();
7421 cx.assert_editor_state("«aˇ»");
7422 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7423 .unwrap();
7424 cx.assert_editor_state("«aˇ»");
7425}
7426
7427#[gpui::test]
7428async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
7429 init_test(cx, |_| {});
7430
7431 let mut cx = EditorTestContext::new(cx).await;
7432 cx.set_state(
7433 r#"let foo = 2;
7434lˇet foo = 2;
7435let fooˇ = 2;
7436let foo = 2;
7437let foo = ˇ2;"#,
7438 );
7439
7440 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7441 .unwrap();
7442 cx.assert_editor_state(
7443 r#"let foo = 2;
7444«letˇ» foo = 2;
7445let «fooˇ» = 2;
7446let foo = 2;
7447let foo = «2ˇ»;"#,
7448 );
7449
7450 // noop for multiple selections with different contents
7451 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7452 .unwrap();
7453 cx.assert_editor_state(
7454 r#"let foo = 2;
7455«letˇ» foo = 2;
7456let «fooˇ» = 2;
7457let foo = 2;
7458let foo = «2ˇ»;"#,
7459 );
7460}
7461
7462#[gpui::test]
7463async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
7464 init_test(cx, |_| {});
7465
7466 let mut cx = EditorTestContext::new(cx).await;
7467 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7468
7469 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7470 .unwrap();
7471 // selection direction is preserved
7472 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7473
7474 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7475 .unwrap();
7476 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7477
7478 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7479 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7480
7481 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7482 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7483
7484 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7485 .unwrap();
7486 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
7487
7488 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7489 .unwrap();
7490 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
7491}
7492
7493#[gpui::test]
7494async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
7495 init_test(cx, |_| {});
7496
7497 let language = Arc::new(Language::new(
7498 LanguageConfig::default(),
7499 Some(tree_sitter_rust::LANGUAGE.into()),
7500 ));
7501
7502 let text = r#"
7503 use mod1::mod2::{mod3, mod4};
7504
7505 fn fn_1(param1: bool, param2: &str) {
7506 let var1 = "text";
7507 }
7508 "#
7509 .unindent();
7510
7511 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7512 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7513 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7514
7515 editor
7516 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7517 .await;
7518
7519 editor.update_in(cx, |editor, window, cx| {
7520 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7521 s.select_display_ranges([
7522 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
7523 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
7524 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
7525 ]);
7526 });
7527 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7528 });
7529 editor.update(cx, |editor, cx| {
7530 assert_text_with_selections(
7531 editor,
7532 indoc! {r#"
7533 use mod1::mod2::{mod3, «mod4ˇ»};
7534
7535 fn fn_1«ˇ(param1: bool, param2: &str)» {
7536 let var1 = "«ˇtext»";
7537 }
7538 "#},
7539 cx,
7540 );
7541 });
7542
7543 editor.update_in(cx, |editor, window, cx| {
7544 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7545 });
7546 editor.update(cx, |editor, cx| {
7547 assert_text_with_selections(
7548 editor,
7549 indoc! {r#"
7550 use mod1::mod2::«{mod3, mod4}ˇ»;
7551
7552 «ˇfn fn_1(param1: bool, param2: &str) {
7553 let var1 = "text";
7554 }»
7555 "#},
7556 cx,
7557 );
7558 });
7559
7560 editor.update_in(cx, |editor, window, cx| {
7561 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7562 });
7563 assert_eq!(
7564 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7565 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7566 );
7567
7568 // Trying to expand the selected syntax node one more time has no effect.
7569 editor.update_in(cx, |editor, window, cx| {
7570 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7571 });
7572 assert_eq!(
7573 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7574 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7575 );
7576
7577 editor.update_in(cx, |editor, window, cx| {
7578 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7579 });
7580 editor.update(cx, |editor, cx| {
7581 assert_text_with_selections(
7582 editor,
7583 indoc! {r#"
7584 use mod1::mod2::«{mod3, mod4}ˇ»;
7585
7586 «ˇfn fn_1(param1: bool, param2: &str) {
7587 let var1 = "text";
7588 }»
7589 "#},
7590 cx,
7591 );
7592 });
7593
7594 editor.update_in(cx, |editor, window, cx| {
7595 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7596 });
7597 editor.update(cx, |editor, cx| {
7598 assert_text_with_selections(
7599 editor,
7600 indoc! {r#"
7601 use mod1::mod2::{mod3, «mod4ˇ»};
7602
7603 fn fn_1«ˇ(param1: bool, param2: &str)» {
7604 let var1 = "«ˇtext»";
7605 }
7606 "#},
7607 cx,
7608 );
7609 });
7610
7611 editor.update_in(cx, |editor, window, cx| {
7612 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7613 });
7614 editor.update(cx, |editor, cx| {
7615 assert_text_with_selections(
7616 editor,
7617 indoc! {r#"
7618 use mod1::mod2::{mod3, mo«ˇ»d4};
7619
7620 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7621 let var1 = "te«ˇ»xt";
7622 }
7623 "#},
7624 cx,
7625 );
7626 });
7627
7628 // Trying to shrink the selected syntax node one more time has no effect.
7629 editor.update_in(cx, |editor, window, cx| {
7630 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7631 });
7632 editor.update_in(cx, |editor, _, cx| {
7633 assert_text_with_selections(
7634 editor,
7635 indoc! {r#"
7636 use mod1::mod2::{mod3, mo«ˇ»d4};
7637
7638 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7639 let var1 = "te«ˇ»xt";
7640 }
7641 "#},
7642 cx,
7643 );
7644 });
7645
7646 // Ensure that we keep expanding the selection if the larger selection starts or ends within
7647 // a fold.
7648 editor.update_in(cx, |editor, window, cx| {
7649 editor.fold_creases(
7650 vec![
7651 Crease::simple(
7652 Point::new(0, 21)..Point::new(0, 24),
7653 FoldPlaceholder::test(),
7654 ),
7655 Crease::simple(
7656 Point::new(3, 20)..Point::new(3, 22),
7657 FoldPlaceholder::test(),
7658 ),
7659 ],
7660 true,
7661 window,
7662 cx,
7663 );
7664 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7665 });
7666 editor.update(cx, |editor, cx| {
7667 assert_text_with_selections(
7668 editor,
7669 indoc! {r#"
7670 use mod1::mod2::«{mod3, mod4}ˇ»;
7671
7672 fn fn_1«ˇ(param1: bool, param2: &str)» {
7673 let var1 = "«ˇtext»";
7674 }
7675 "#},
7676 cx,
7677 );
7678 });
7679}
7680
7681#[gpui::test]
7682async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
7683 init_test(cx, |_| {});
7684
7685 let language = Arc::new(Language::new(
7686 LanguageConfig::default(),
7687 Some(tree_sitter_rust::LANGUAGE.into()),
7688 ));
7689
7690 let text = "let a = 2;";
7691
7692 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7693 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7694 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7695
7696 editor
7697 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7698 .await;
7699
7700 // Test case 1: Cursor at end of word
7701 editor.update_in(cx, |editor, window, cx| {
7702 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7703 s.select_display_ranges([
7704 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
7705 ]);
7706 });
7707 });
7708 editor.update(cx, |editor, cx| {
7709 assert_text_with_selections(editor, "let aˇ = 2;", cx);
7710 });
7711 editor.update_in(cx, |editor, window, cx| {
7712 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7713 });
7714 editor.update(cx, |editor, cx| {
7715 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
7716 });
7717 editor.update_in(cx, |editor, window, cx| {
7718 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7719 });
7720 editor.update(cx, |editor, cx| {
7721 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7722 });
7723
7724 // Test case 2: Cursor at end of statement
7725 editor.update_in(cx, |editor, window, cx| {
7726 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7727 s.select_display_ranges([
7728 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
7729 ]);
7730 });
7731 });
7732 editor.update(cx, |editor, cx| {
7733 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
7734 });
7735 editor.update_in(cx, |editor, window, cx| {
7736 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7737 });
7738 editor.update(cx, |editor, cx| {
7739 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7740 });
7741}
7742
7743#[gpui::test]
7744async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
7745 init_test(cx, |_| {});
7746
7747 let language = Arc::new(Language::new(
7748 LanguageConfig::default(),
7749 Some(tree_sitter_rust::LANGUAGE.into()),
7750 ));
7751
7752 let text = r#"
7753 use mod1::mod2::{mod3, mod4};
7754
7755 fn fn_1(param1: bool, param2: &str) {
7756 let var1 = "hello world";
7757 }
7758 "#
7759 .unindent();
7760
7761 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7762 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7763 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7764
7765 editor
7766 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7767 .await;
7768
7769 // Test 1: Cursor on a letter of a string word
7770 editor.update_in(cx, |editor, window, cx| {
7771 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7772 s.select_display_ranges([
7773 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
7774 ]);
7775 });
7776 });
7777 editor.update_in(cx, |editor, window, cx| {
7778 assert_text_with_selections(
7779 editor,
7780 indoc! {r#"
7781 use mod1::mod2::{mod3, mod4};
7782
7783 fn fn_1(param1: bool, param2: &str) {
7784 let var1 = "hˇello world";
7785 }
7786 "#},
7787 cx,
7788 );
7789 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7790 assert_text_with_selections(
7791 editor,
7792 indoc! {r#"
7793 use mod1::mod2::{mod3, mod4};
7794
7795 fn fn_1(param1: bool, param2: &str) {
7796 let var1 = "«ˇhello» world";
7797 }
7798 "#},
7799 cx,
7800 );
7801 });
7802
7803 // Test 2: Partial selection within a word
7804 editor.update_in(cx, |editor, window, cx| {
7805 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7806 s.select_display_ranges([
7807 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7808 ]);
7809 });
7810 });
7811 editor.update_in(cx, |editor, window, cx| {
7812 assert_text_with_selections(
7813 editor,
7814 indoc! {r#"
7815 use mod1::mod2::{mod3, mod4};
7816
7817 fn fn_1(param1: bool, param2: &str) {
7818 let var1 = "h«elˇ»lo world";
7819 }
7820 "#},
7821 cx,
7822 );
7823 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7824 assert_text_with_selections(
7825 editor,
7826 indoc! {r#"
7827 use mod1::mod2::{mod3, mod4};
7828
7829 fn fn_1(param1: bool, param2: &str) {
7830 let var1 = "«ˇhello» world";
7831 }
7832 "#},
7833 cx,
7834 );
7835 });
7836
7837 // Test 3: Complete word already selected
7838 editor.update_in(cx, |editor, window, cx| {
7839 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7840 s.select_display_ranges([
7841 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7842 ]);
7843 });
7844 });
7845 editor.update_in(cx, |editor, window, cx| {
7846 assert_text_with_selections(
7847 editor,
7848 indoc! {r#"
7849 use mod1::mod2::{mod3, mod4};
7850
7851 fn fn_1(param1: bool, param2: &str) {
7852 let var1 = "«helloˇ» world";
7853 }
7854 "#},
7855 cx,
7856 );
7857 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7858 assert_text_with_selections(
7859 editor,
7860 indoc! {r#"
7861 use mod1::mod2::{mod3, mod4};
7862
7863 fn fn_1(param1: bool, param2: &str) {
7864 let var1 = "«hello worldˇ»";
7865 }
7866 "#},
7867 cx,
7868 );
7869 });
7870
7871 // Test 4: Selection spanning across words
7872 editor.update_in(cx, |editor, window, cx| {
7873 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7874 s.select_display_ranges([
7875 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7876 ]);
7877 });
7878 });
7879 editor.update_in(cx, |editor, window, cx| {
7880 assert_text_with_selections(
7881 editor,
7882 indoc! {r#"
7883 use mod1::mod2::{mod3, mod4};
7884
7885 fn fn_1(param1: bool, param2: &str) {
7886 let var1 = "hel«lo woˇ»rld";
7887 }
7888 "#},
7889 cx,
7890 );
7891 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7892 assert_text_with_selections(
7893 editor,
7894 indoc! {r#"
7895 use mod1::mod2::{mod3, mod4};
7896
7897 fn fn_1(param1: bool, param2: &str) {
7898 let var1 = "«ˇhello world»";
7899 }
7900 "#},
7901 cx,
7902 );
7903 });
7904
7905 // Test 5: Expansion beyond string
7906 editor.update_in(cx, |editor, window, cx| {
7907 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7908 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7909 assert_text_with_selections(
7910 editor,
7911 indoc! {r#"
7912 use mod1::mod2::{mod3, mod4};
7913
7914 fn fn_1(param1: bool, param2: &str) {
7915 «ˇlet var1 = "hello world";»
7916 }
7917 "#},
7918 cx,
7919 );
7920 });
7921}
7922
7923#[gpui::test]
7924async fn test_fold_function_bodies(cx: &mut TestAppContext) {
7925 init_test(cx, |_| {});
7926
7927 let base_text = r#"
7928 impl A {
7929 // this is an uncommitted comment
7930
7931 fn b() {
7932 c();
7933 }
7934
7935 // this is another uncommitted comment
7936
7937 fn d() {
7938 // e
7939 // f
7940 }
7941 }
7942
7943 fn g() {
7944 // h
7945 }
7946 "#
7947 .unindent();
7948
7949 let text = r#"
7950 ˇimpl A {
7951
7952 fn b() {
7953 c();
7954 }
7955
7956 fn d() {
7957 // e
7958 // f
7959 }
7960 }
7961
7962 fn g() {
7963 // h
7964 }
7965 "#
7966 .unindent();
7967
7968 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7969 cx.set_state(&text);
7970 cx.set_head_text(&base_text);
7971 cx.update_editor(|editor, window, cx| {
7972 editor.expand_all_diff_hunks(&Default::default(), window, cx);
7973 });
7974
7975 cx.assert_state_with_diff(
7976 "
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
7999 let expected_display_text = "
8000 impl A {
8001 // this is an uncommitted comment
8002
8003 fn b() {
8004 ⋯
8005 }
8006
8007 // this is another uncommitted comment
8008
8009 fn d() {
8010 ⋯
8011 }
8012 }
8013
8014 fn g() {
8015 ⋯
8016 }
8017 "
8018 .unindent();
8019
8020 cx.update_editor(|editor, window, cx| {
8021 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
8022 assert_eq!(editor.display_text(cx), expected_display_text);
8023 });
8024}
8025
8026#[gpui::test]
8027async fn test_autoindent(cx: &mut TestAppContext) {
8028 init_test(cx, |_| {});
8029
8030 let language = Arc::new(
8031 Language::new(
8032 LanguageConfig {
8033 brackets: BracketPairConfig {
8034 pairs: vec![
8035 BracketPair {
8036 start: "{".to_string(),
8037 end: "}".to_string(),
8038 close: false,
8039 surround: false,
8040 newline: true,
8041 },
8042 BracketPair {
8043 start: "(".to_string(),
8044 end: ")".to_string(),
8045 close: false,
8046 surround: false,
8047 newline: true,
8048 },
8049 ],
8050 ..Default::default()
8051 },
8052 ..Default::default()
8053 },
8054 Some(tree_sitter_rust::LANGUAGE.into()),
8055 )
8056 .with_indents_query(
8057 r#"
8058 (_ "(" ")" @end) @indent
8059 (_ "{" "}" @end) @indent
8060 "#,
8061 )
8062 .unwrap(),
8063 );
8064
8065 let text = "fn a() {}";
8066
8067 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8068 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8069 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8070 editor
8071 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8072 .await;
8073
8074 editor.update_in(cx, |editor, window, cx| {
8075 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8076 s.select_ranges([5..5, 8..8, 9..9])
8077 });
8078 editor.newline(&Newline, window, cx);
8079 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
8080 assert_eq!(
8081 editor.selections.ranges(cx),
8082 &[
8083 Point::new(1, 4)..Point::new(1, 4),
8084 Point::new(3, 4)..Point::new(3, 4),
8085 Point::new(5, 0)..Point::new(5, 0)
8086 ]
8087 );
8088 });
8089}
8090
8091#[gpui::test]
8092async fn test_autoindent_selections(cx: &mut TestAppContext) {
8093 init_test(cx, |_| {});
8094
8095 {
8096 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8097 cx.set_state(indoc! {"
8098 impl A {
8099
8100 fn b() {}
8101
8102 «fn c() {
8103
8104 }ˇ»
8105 }
8106 "});
8107
8108 cx.update_editor(|editor, window, cx| {
8109 editor.autoindent(&Default::default(), window, cx);
8110 });
8111
8112 cx.assert_editor_state(indoc! {"
8113 impl A {
8114
8115 fn b() {}
8116
8117 «fn c() {
8118
8119 }ˇ»
8120 }
8121 "});
8122 }
8123
8124 {
8125 let mut cx = EditorTestContext::new_multibuffer(
8126 cx,
8127 [indoc! { "
8128 impl A {
8129 «
8130 // a
8131 fn b(){}
8132 »
8133 «
8134 }
8135 fn c(){}
8136 »
8137 "}],
8138 );
8139
8140 let buffer = cx.update_editor(|editor, _, cx| {
8141 let buffer = editor.buffer().update(cx, |buffer, _| {
8142 buffer.all_buffers().iter().next().unwrap().clone()
8143 });
8144 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
8145 buffer
8146 });
8147
8148 cx.run_until_parked();
8149 cx.update_editor(|editor, window, cx| {
8150 editor.select_all(&Default::default(), window, cx);
8151 editor.autoindent(&Default::default(), window, cx)
8152 });
8153 cx.run_until_parked();
8154
8155 cx.update(|_, cx| {
8156 assert_eq!(
8157 buffer.read(cx).text(),
8158 indoc! { "
8159 impl A {
8160
8161 // a
8162 fn b(){}
8163
8164
8165 }
8166 fn c(){}
8167
8168 " }
8169 )
8170 });
8171 }
8172}
8173
8174#[gpui::test]
8175async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
8176 init_test(cx, |_| {});
8177
8178 let mut cx = EditorTestContext::new(cx).await;
8179
8180 let language = Arc::new(Language::new(
8181 LanguageConfig {
8182 brackets: BracketPairConfig {
8183 pairs: vec![
8184 BracketPair {
8185 start: "{".to_string(),
8186 end: "}".to_string(),
8187 close: true,
8188 surround: true,
8189 newline: true,
8190 },
8191 BracketPair {
8192 start: "(".to_string(),
8193 end: ")".to_string(),
8194 close: true,
8195 surround: true,
8196 newline: true,
8197 },
8198 BracketPair {
8199 start: "/*".to_string(),
8200 end: " */".to_string(),
8201 close: true,
8202 surround: true,
8203 newline: true,
8204 },
8205 BracketPair {
8206 start: "[".to_string(),
8207 end: "]".to_string(),
8208 close: false,
8209 surround: false,
8210 newline: true,
8211 },
8212 BracketPair {
8213 start: "\"".to_string(),
8214 end: "\"".to_string(),
8215 close: true,
8216 surround: true,
8217 newline: false,
8218 },
8219 BracketPair {
8220 start: "<".to_string(),
8221 end: ">".to_string(),
8222 close: false,
8223 surround: true,
8224 newline: true,
8225 },
8226 ],
8227 ..Default::default()
8228 },
8229 autoclose_before: "})]".to_string(),
8230 ..Default::default()
8231 },
8232 Some(tree_sitter_rust::LANGUAGE.into()),
8233 ));
8234
8235 cx.language_registry().add(language.clone());
8236 cx.update_buffer(|buffer, cx| {
8237 buffer.set_language(Some(language), cx);
8238 });
8239
8240 cx.set_state(
8241 &r#"
8242 🏀ˇ
8243 εˇ
8244 ❤️ˇ
8245 "#
8246 .unindent(),
8247 );
8248
8249 // autoclose multiple nested brackets at multiple cursors
8250 cx.update_editor(|editor, window, cx| {
8251 editor.handle_input("{", window, cx);
8252 editor.handle_input("{", window, cx);
8253 editor.handle_input("{", window, cx);
8254 });
8255 cx.assert_editor_state(
8256 &"
8257 🏀{{{ˇ}}}
8258 ε{{{ˇ}}}
8259 ❤️{{{ˇ}}}
8260 "
8261 .unindent(),
8262 );
8263
8264 // insert a different closing bracket
8265 cx.update_editor(|editor, window, cx| {
8266 editor.handle_input(")", window, cx);
8267 });
8268 cx.assert_editor_state(
8269 &"
8270 🏀{{{)ˇ}}}
8271 ε{{{)ˇ}}}
8272 ❤️{{{)ˇ}}}
8273 "
8274 .unindent(),
8275 );
8276
8277 // skip over the auto-closed brackets when typing a closing bracket
8278 cx.update_editor(|editor, window, cx| {
8279 editor.move_right(&MoveRight, window, cx);
8280 editor.handle_input("}", window, cx);
8281 editor.handle_input("}", window, cx);
8282 editor.handle_input("}", window, cx);
8283 });
8284 cx.assert_editor_state(
8285 &"
8286 🏀{{{)}}}}ˇ
8287 ε{{{)}}}}ˇ
8288 ❤️{{{)}}}}ˇ
8289 "
8290 .unindent(),
8291 );
8292
8293 // autoclose multi-character pairs
8294 cx.set_state(
8295 &"
8296 ˇ
8297 ˇ
8298 "
8299 .unindent(),
8300 );
8301 cx.update_editor(|editor, window, cx| {
8302 editor.handle_input("/", window, cx);
8303 editor.handle_input("*", window, cx);
8304 });
8305 cx.assert_editor_state(
8306 &"
8307 /*ˇ */
8308 /*ˇ */
8309 "
8310 .unindent(),
8311 );
8312
8313 // one cursor autocloses a multi-character pair, one cursor
8314 // does not autoclose.
8315 cx.set_state(
8316 &"
8317 /ˇ
8318 ˇ
8319 "
8320 .unindent(),
8321 );
8322 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
8323 cx.assert_editor_state(
8324 &"
8325 /*ˇ */
8326 *ˇ
8327 "
8328 .unindent(),
8329 );
8330
8331 // Don't autoclose if the next character isn't whitespace and isn't
8332 // listed in the language's "autoclose_before" section.
8333 cx.set_state("ˇa b");
8334 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8335 cx.assert_editor_state("{ˇa b");
8336
8337 // Don't autoclose if `close` is false for the bracket pair
8338 cx.set_state("ˇ");
8339 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
8340 cx.assert_editor_state("[ˇ");
8341
8342 // Surround with brackets if text is selected
8343 cx.set_state("«aˇ» b");
8344 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8345 cx.assert_editor_state("{«aˇ»} b");
8346
8347 // Autoclose when not immediately after a word character
8348 cx.set_state("a ˇ");
8349 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8350 cx.assert_editor_state("a \"ˇ\"");
8351
8352 // Autoclose pair where the start and end characters are the same
8353 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8354 cx.assert_editor_state("a \"\"ˇ");
8355
8356 // Don't autoclose when immediately after a word character
8357 cx.set_state("aˇ");
8358 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8359 cx.assert_editor_state("a\"ˇ");
8360
8361 // Do autoclose when after a non-word character
8362 cx.set_state("{ˇ");
8363 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8364 cx.assert_editor_state("{\"ˇ\"");
8365
8366 // Non identical pairs autoclose regardless of preceding character
8367 cx.set_state("aˇ");
8368 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8369 cx.assert_editor_state("a{ˇ}");
8370
8371 // Don't autoclose pair if autoclose is disabled
8372 cx.set_state("ˇ");
8373 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8374 cx.assert_editor_state("<ˇ");
8375
8376 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
8377 cx.set_state("«aˇ» b");
8378 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8379 cx.assert_editor_state("<«aˇ»> b");
8380}
8381
8382#[gpui::test]
8383async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
8384 init_test(cx, |settings| {
8385 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8386 });
8387
8388 let mut cx = EditorTestContext::new(cx).await;
8389
8390 let language = Arc::new(Language::new(
8391 LanguageConfig {
8392 brackets: BracketPairConfig {
8393 pairs: vec![
8394 BracketPair {
8395 start: "{".to_string(),
8396 end: "}".to_string(),
8397 close: true,
8398 surround: true,
8399 newline: true,
8400 },
8401 BracketPair {
8402 start: "(".to_string(),
8403 end: ")".to_string(),
8404 close: true,
8405 surround: true,
8406 newline: true,
8407 },
8408 BracketPair {
8409 start: "[".to_string(),
8410 end: "]".to_string(),
8411 close: false,
8412 surround: false,
8413 newline: true,
8414 },
8415 ],
8416 ..Default::default()
8417 },
8418 autoclose_before: "})]".to_string(),
8419 ..Default::default()
8420 },
8421 Some(tree_sitter_rust::LANGUAGE.into()),
8422 ));
8423
8424 cx.language_registry().add(language.clone());
8425 cx.update_buffer(|buffer, cx| {
8426 buffer.set_language(Some(language), cx);
8427 });
8428
8429 cx.set_state(
8430 &"
8431 ˇ
8432 ˇ
8433 ˇ
8434 "
8435 .unindent(),
8436 );
8437
8438 // ensure only matching closing brackets are skipped over
8439 cx.update_editor(|editor, window, cx| {
8440 editor.handle_input("}", window, cx);
8441 editor.move_left(&MoveLeft, window, cx);
8442 editor.handle_input(")", window, cx);
8443 editor.move_left(&MoveLeft, window, cx);
8444 });
8445 cx.assert_editor_state(
8446 &"
8447 ˇ)}
8448 ˇ)}
8449 ˇ)}
8450 "
8451 .unindent(),
8452 );
8453
8454 // skip-over closing brackets at multiple cursors
8455 cx.update_editor(|editor, window, cx| {
8456 editor.handle_input(")", window, cx);
8457 editor.handle_input("}", window, cx);
8458 });
8459 cx.assert_editor_state(
8460 &"
8461 )}ˇ
8462 )}ˇ
8463 )}ˇ
8464 "
8465 .unindent(),
8466 );
8467
8468 // ignore non-close brackets
8469 cx.update_editor(|editor, window, cx| {
8470 editor.handle_input("]", window, cx);
8471 editor.move_left(&MoveLeft, window, cx);
8472 editor.handle_input("]", window, cx);
8473 });
8474 cx.assert_editor_state(
8475 &"
8476 )}]ˇ]
8477 )}]ˇ]
8478 )}]ˇ]
8479 "
8480 .unindent(),
8481 );
8482}
8483
8484#[gpui::test]
8485async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8486 init_test(cx, |_| {});
8487
8488 let mut cx = EditorTestContext::new(cx).await;
8489
8490 let html_language = Arc::new(
8491 Language::new(
8492 LanguageConfig {
8493 name: "HTML".into(),
8494 brackets: BracketPairConfig {
8495 pairs: vec![
8496 BracketPair {
8497 start: "<".into(),
8498 end: ">".into(),
8499 close: true,
8500 ..Default::default()
8501 },
8502 BracketPair {
8503 start: "{".into(),
8504 end: "}".into(),
8505 close: true,
8506 ..Default::default()
8507 },
8508 BracketPair {
8509 start: "(".into(),
8510 end: ")".into(),
8511 close: true,
8512 ..Default::default()
8513 },
8514 ],
8515 ..Default::default()
8516 },
8517 autoclose_before: "})]>".into(),
8518 ..Default::default()
8519 },
8520 Some(tree_sitter_html::LANGUAGE.into()),
8521 )
8522 .with_injection_query(
8523 r#"
8524 (script_element
8525 (raw_text) @injection.content
8526 (#set! injection.language "javascript"))
8527 "#,
8528 )
8529 .unwrap(),
8530 );
8531
8532 let javascript_language = Arc::new(Language::new(
8533 LanguageConfig {
8534 name: "JavaScript".into(),
8535 brackets: BracketPairConfig {
8536 pairs: vec![
8537 BracketPair {
8538 start: "/*".into(),
8539 end: " */".into(),
8540 close: true,
8541 ..Default::default()
8542 },
8543 BracketPair {
8544 start: "{".into(),
8545 end: "}".into(),
8546 close: true,
8547 ..Default::default()
8548 },
8549 BracketPair {
8550 start: "(".into(),
8551 end: ")".into(),
8552 close: true,
8553 ..Default::default()
8554 },
8555 ],
8556 ..Default::default()
8557 },
8558 autoclose_before: "})]>".into(),
8559 ..Default::default()
8560 },
8561 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8562 ));
8563
8564 cx.language_registry().add(html_language.clone());
8565 cx.language_registry().add(javascript_language.clone());
8566
8567 cx.update_buffer(|buffer, cx| {
8568 buffer.set_language(Some(html_language), cx);
8569 });
8570
8571 cx.set_state(
8572 &r#"
8573 <body>ˇ
8574 <script>
8575 var x = 1;ˇ
8576 </script>
8577 </body>ˇ
8578 "#
8579 .unindent(),
8580 );
8581
8582 // Precondition: different languages are active at different locations.
8583 cx.update_editor(|editor, window, cx| {
8584 let snapshot = editor.snapshot(window, cx);
8585 let cursors = editor.selections.ranges::<usize>(cx);
8586 let languages = cursors
8587 .iter()
8588 .map(|c| snapshot.language_at(c.start).unwrap().name())
8589 .collect::<Vec<_>>();
8590 assert_eq!(
8591 languages,
8592 &["HTML".into(), "JavaScript".into(), "HTML".into()]
8593 );
8594 });
8595
8596 // Angle brackets autoclose in HTML, but not JavaScript.
8597 cx.update_editor(|editor, window, cx| {
8598 editor.handle_input("<", window, cx);
8599 editor.handle_input("a", window, cx);
8600 });
8601 cx.assert_editor_state(
8602 &r#"
8603 <body><aˇ>
8604 <script>
8605 var x = 1;<aˇ
8606 </script>
8607 </body><aˇ>
8608 "#
8609 .unindent(),
8610 );
8611
8612 // Curly braces and parens autoclose in both HTML and JavaScript.
8613 cx.update_editor(|editor, window, cx| {
8614 editor.handle_input(" b=", window, cx);
8615 editor.handle_input("{", window, cx);
8616 editor.handle_input("c", window, cx);
8617 editor.handle_input("(", window, cx);
8618 });
8619 cx.assert_editor_state(
8620 &r#"
8621 <body><a b={c(ˇ)}>
8622 <script>
8623 var x = 1;<a b={c(ˇ)}
8624 </script>
8625 </body><a b={c(ˇ)}>
8626 "#
8627 .unindent(),
8628 );
8629
8630 // Brackets that were already autoclosed are skipped.
8631 cx.update_editor(|editor, window, cx| {
8632 editor.handle_input(")", window, cx);
8633 editor.handle_input("d", window, cx);
8634 editor.handle_input("}", window, cx);
8635 });
8636 cx.assert_editor_state(
8637 &r#"
8638 <body><a b={c()d}ˇ>
8639 <script>
8640 var x = 1;<a b={c()d}ˇ
8641 </script>
8642 </body><a b={c()d}ˇ>
8643 "#
8644 .unindent(),
8645 );
8646 cx.update_editor(|editor, window, cx| {
8647 editor.handle_input(">", window, cx);
8648 });
8649 cx.assert_editor_state(
8650 &r#"
8651 <body><a b={c()d}>ˇ
8652 <script>
8653 var x = 1;<a b={c()d}>ˇ
8654 </script>
8655 </body><a b={c()d}>ˇ
8656 "#
8657 .unindent(),
8658 );
8659
8660 // Reset
8661 cx.set_state(
8662 &r#"
8663 <body>ˇ
8664 <script>
8665 var x = 1;ˇ
8666 </script>
8667 </body>ˇ
8668 "#
8669 .unindent(),
8670 );
8671
8672 cx.update_editor(|editor, window, cx| {
8673 editor.handle_input("<", window, cx);
8674 });
8675 cx.assert_editor_state(
8676 &r#"
8677 <body><ˇ>
8678 <script>
8679 var x = 1;<ˇ
8680 </script>
8681 </body><ˇ>
8682 "#
8683 .unindent(),
8684 );
8685
8686 // When backspacing, the closing angle brackets are removed.
8687 cx.update_editor(|editor, window, cx| {
8688 editor.backspace(&Backspace, window, cx);
8689 });
8690 cx.assert_editor_state(
8691 &r#"
8692 <body>ˇ
8693 <script>
8694 var x = 1;ˇ
8695 </script>
8696 </body>ˇ
8697 "#
8698 .unindent(),
8699 );
8700
8701 // Block comments autoclose in JavaScript, but not HTML.
8702 cx.update_editor(|editor, window, cx| {
8703 editor.handle_input("/", window, cx);
8704 editor.handle_input("*", window, cx);
8705 });
8706 cx.assert_editor_state(
8707 &r#"
8708 <body>/*ˇ
8709 <script>
8710 var x = 1;/*ˇ */
8711 </script>
8712 </body>/*ˇ
8713 "#
8714 .unindent(),
8715 );
8716}
8717
8718#[gpui::test]
8719async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
8720 init_test(cx, |_| {});
8721
8722 let mut cx = EditorTestContext::new(cx).await;
8723
8724 let rust_language = Arc::new(
8725 Language::new(
8726 LanguageConfig {
8727 name: "Rust".into(),
8728 brackets: serde_json::from_value(json!([
8729 { "start": "{", "end": "}", "close": true, "newline": true },
8730 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
8731 ]))
8732 .unwrap(),
8733 autoclose_before: "})]>".into(),
8734 ..Default::default()
8735 },
8736 Some(tree_sitter_rust::LANGUAGE.into()),
8737 )
8738 .with_override_query("(string_literal) @string")
8739 .unwrap(),
8740 );
8741
8742 cx.language_registry().add(rust_language.clone());
8743 cx.update_buffer(|buffer, cx| {
8744 buffer.set_language(Some(rust_language), cx);
8745 });
8746
8747 cx.set_state(
8748 &r#"
8749 let x = ˇ
8750 "#
8751 .unindent(),
8752 );
8753
8754 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
8755 cx.update_editor(|editor, window, cx| {
8756 editor.handle_input("\"", window, cx);
8757 });
8758 cx.assert_editor_state(
8759 &r#"
8760 let x = "ˇ"
8761 "#
8762 .unindent(),
8763 );
8764
8765 // Inserting another quotation mark. The cursor moves across the existing
8766 // automatically-inserted quotation mark.
8767 cx.update_editor(|editor, window, cx| {
8768 editor.handle_input("\"", window, cx);
8769 });
8770 cx.assert_editor_state(
8771 &r#"
8772 let x = ""ˇ
8773 "#
8774 .unindent(),
8775 );
8776
8777 // Reset
8778 cx.set_state(
8779 &r#"
8780 let x = ˇ
8781 "#
8782 .unindent(),
8783 );
8784
8785 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
8786 cx.update_editor(|editor, window, cx| {
8787 editor.handle_input("\"", window, cx);
8788 editor.handle_input(" ", window, cx);
8789 editor.move_left(&Default::default(), window, cx);
8790 editor.handle_input("\\", window, cx);
8791 editor.handle_input("\"", window, cx);
8792 });
8793 cx.assert_editor_state(
8794 &r#"
8795 let x = "\"ˇ "
8796 "#
8797 .unindent(),
8798 );
8799
8800 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8801 // mark. Nothing is inserted.
8802 cx.update_editor(|editor, window, cx| {
8803 editor.move_right(&Default::default(), window, cx);
8804 editor.handle_input("\"", window, cx);
8805 });
8806 cx.assert_editor_state(
8807 &r#"
8808 let x = "\" "ˇ
8809 "#
8810 .unindent(),
8811 );
8812}
8813
8814#[gpui::test]
8815async fn test_surround_with_pair(cx: &mut TestAppContext) {
8816 init_test(cx, |_| {});
8817
8818 let language = Arc::new(Language::new(
8819 LanguageConfig {
8820 brackets: BracketPairConfig {
8821 pairs: vec![
8822 BracketPair {
8823 start: "{".to_string(),
8824 end: "}".to_string(),
8825 close: true,
8826 surround: true,
8827 newline: true,
8828 },
8829 BracketPair {
8830 start: "/* ".to_string(),
8831 end: "*/".to_string(),
8832 close: true,
8833 surround: true,
8834 ..Default::default()
8835 },
8836 ],
8837 ..Default::default()
8838 },
8839 ..Default::default()
8840 },
8841 Some(tree_sitter_rust::LANGUAGE.into()),
8842 ));
8843
8844 let text = r#"
8845 a
8846 b
8847 c
8848 "#
8849 .unindent();
8850
8851 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8852 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8853 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8854 editor
8855 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8856 .await;
8857
8858 editor.update_in(cx, |editor, window, cx| {
8859 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8860 s.select_display_ranges([
8861 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8862 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8863 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8864 ])
8865 });
8866
8867 editor.handle_input("{", window, cx);
8868 editor.handle_input("{", window, cx);
8869 editor.handle_input("{", window, cx);
8870 assert_eq!(
8871 editor.text(cx),
8872 "
8873 {{{a}}}
8874 {{{b}}}
8875 {{{c}}}
8876 "
8877 .unindent()
8878 );
8879 assert_eq!(
8880 editor.selections.display_ranges(cx),
8881 [
8882 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
8883 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
8884 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
8885 ]
8886 );
8887
8888 editor.undo(&Undo, window, cx);
8889 editor.undo(&Undo, window, cx);
8890 editor.undo(&Undo, window, cx);
8891 assert_eq!(
8892 editor.text(cx),
8893 "
8894 a
8895 b
8896 c
8897 "
8898 .unindent()
8899 );
8900 assert_eq!(
8901 editor.selections.display_ranges(cx),
8902 [
8903 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8904 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8905 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8906 ]
8907 );
8908
8909 // Ensure inserting the first character of a multi-byte bracket pair
8910 // doesn't surround the selections with the bracket.
8911 editor.handle_input("/", window, cx);
8912 assert_eq!(
8913 editor.text(cx),
8914 "
8915 /
8916 /
8917 /
8918 "
8919 .unindent()
8920 );
8921 assert_eq!(
8922 editor.selections.display_ranges(cx),
8923 [
8924 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8925 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8926 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8927 ]
8928 );
8929
8930 editor.undo(&Undo, window, cx);
8931 assert_eq!(
8932 editor.text(cx),
8933 "
8934 a
8935 b
8936 c
8937 "
8938 .unindent()
8939 );
8940 assert_eq!(
8941 editor.selections.display_ranges(cx),
8942 [
8943 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8944 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8945 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8946 ]
8947 );
8948
8949 // Ensure inserting the last character of a multi-byte bracket pair
8950 // doesn't surround the selections with the bracket.
8951 editor.handle_input("*", window, cx);
8952 assert_eq!(
8953 editor.text(cx),
8954 "
8955 *
8956 *
8957 *
8958 "
8959 .unindent()
8960 );
8961 assert_eq!(
8962 editor.selections.display_ranges(cx),
8963 [
8964 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8965 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8966 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8967 ]
8968 );
8969 });
8970}
8971
8972#[gpui::test]
8973async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
8974 init_test(cx, |_| {});
8975
8976 let language = Arc::new(Language::new(
8977 LanguageConfig {
8978 brackets: BracketPairConfig {
8979 pairs: vec![BracketPair {
8980 start: "{".to_string(),
8981 end: "}".to_string(),
8982 close: true,
8983 surround: true,
8984 newline: true,
8985 }],
8986 ..Default::default()
8987 },
8988 autoclose_before: "}".to_string(),
8989 ..Default::default()
8990 },
8991 Some(tree_sitter_rust::LANGUAGE.into()),
8992 ));
8993
8994 let text = r#"
8995 a
8996 b
8997 c
8998 "#
8999 .unindent();
9000
9001 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9002 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9003 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9004 editor
9005 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9006 .await;
9007
9008 editor.update_in(cx, |editor, window, cx| {
9009 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9010 s.select_ranges([
9011 Point::new(0, 1)..Point::new(0, 1),
9012 Point::new(1, 1)..Point::new(1, 1),
9013 Point::new(2, 1)..Point::new(2, 1),
9014 ])
9015 });
9016
9017 editor.handle_input("{", window, cx);
9018 editor.handle_input("{", window, cx);
9019 editor.handle_input("_", window, cx);
9020 assert_eq!(
9021 editor.text(cx),
9022 "
9023 a{{_}}
9024 b{{_}}
9025 c{{_}}
9026 "
9027 .unindent()
9028 );
9029 assert_eq!(
9030 editor.selections.ranges::<Point>(cx),
9031 [
9032 Point::new(0, 4)..Point::new(0, 4),
9033 Point::new(1, 4)..Point::new(1, 4),
9034 Point::new(2, 4)..Point::new(2, 4)
9035 ]
9036 );
9037
9038 editor.backspace(&Default::default(), window, cx);
9039 editor.backspace(&Default::default(), window, cx);
9040 assert_eq!(
9041 editor.text(cx),
9042 "
9043 a{}
9044 b{}
9045 c{}
9046 "
9047 .unindent()
9048 );
9049 assert_eq!(
9050 editor.selections.ranges::<Point>(cx),
9051 [
9052 Point::new(0, 2)..Point::new(0, 2),
9053 Point::new(1, 2)..Point::new(1, 2),
9054 Point::new(2, 2)..Point::new(2, 2)
9055 ]
9056 );
9057
9058 editor.delete_to_previous_word_start(&Default::default(), window, cx);
9059 assert_eq!(
9060 editor.text(cx),
9061 "
9062 a
9063 b
9064 c
9065 "
9066 .unindent()
9067 );
9068 assert_eq!(
9069 editor.selections.ranges::<Point>(cx),
9070 [
9071 Point::new(0, 1)..Point::new(0, 1),
9072 Point::new(1, 1)..Point::new(1, 1),
9073 Point::new(2, 1)..Point::new(2, 1)
9074 ]
9075 );
9076 });
9077}
9078
9079#[gpui::test]
9080async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
9081 init_test(cx, |settings| {
9082 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9083 });
9084
9085 let mut cx = EditorTestContext::new(cx).await;
9086
9087 let language = Arc::new(Language::new(
9088 LanguageConfig {
9089 brackets: BracketPairConfig {
9090 pairs: vec![
9091 BracketPair {
9092 start: "{".to_string(),
9093 end: "}".to_string(),
9094 close: true,
9095 surround: true,
9096 newline: true,
9097 },
9098 BracketPair {
9099 start: "(".to_string(),
9100 end: ")".to_string(),
9101 close: true,
9102 surround: true,
9103 newline: true,
9104 },
9105 BracketPair {
9106 start: "[".to_string(),
9107 end: "]".to_string(),
9108 close: false,
9109 surround: true,
9110 newline: true,
9111 },
9112 ],
9113 ..Default::default()
9114 },
9115 autoclose_before: "})]".to_string(),
9116 ..Default::default()
9117 },
9118 Some(tree_sitter_rust::LANGUAGE.into()),
9119 ));
9120
9121 cx.language_registry().add(language.clone());
9122 cx.update_buffer(|buffer, cx| {
9123 buffer.set_language(Some(language), cx);
9124 });
9125
9126 cx.set_state(
9127 &"
9128 {(ˇ)}
9129 [[ˇ]]
9130 {(ˇ)}
9131 "
9132 .unindent(),
9133 );
9134
9135 cx.update_editor(|editor, window, cx| {
9136 editor.backspace(&Default::default(), window, cx);
9137 editor.backspace(&Default::default(), window, cx);
9138 });
9139
9140 cx.assert_editor_state(
9141 &"
9142 ˇ
9143 ˇ]]
9144 ˇ
9145 "
9146 .unindent(),
9147 );
9148
9149 cx.update_editor(|editor, window, cx| {
9150 editor.handle_input("{", window, cx);
9151 editor.handle_input("{", window, cx);
9152 editor.move_right(&MoveRight, window, cx);
9153 editor.move_right(&MoveRight, window, cx);
9154 editor.move_left(&MoveLeft, window, cx);
9155 editor.move_left(&MoveLeft, window, cx);
9156 editor.backspace(&Default::default(), window, cx);
9157 });
9158
9159 cx.assert_editor_state(
9160 &"
9161 {ˇ}
9162 {ˇ}]]
9163 {ˇ}
9164 "
9165 .unindent(),
9166 );
9167
9168 cx.update_editor(|editor, window, cx| {
9169 editor.backspace(&Default::default(), window, cx);
9170 });
9171
9172 cx.assert_editor_state(
9173 &"
9174 ˇ
9175 ˇ]]
9176 ˇ
9177 "
9178 .unindent(),
9179 );
9180}
9181
9182#[gpui::test]
9183async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
9184 init_test(cx, |_| {});
9185
9186 let language = Arc::new(Language::new(
9187 LanguageConfig::default(),
9188 Some(tree_sitter_rust::LANGUAGE.into()),
9189 ));
9190
9191 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
9192 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9193 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9194 editor
9195 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9196 .await;
9197
9198 editor.update_in(cx, |editor, window, cx| {
9199 editor.set_auto_replace_emoji_shortcode(true);
9200
9201 editor.handle_input("Hello ", window, cx);
9202 editor.handle_input(":wave", window, cx);
9203 assert_eq!(editor.text(cx), "Hello :wave".unindent());
9204
9205 editor.handle_input(":", window, cx);
9206 assert_eq!(editor.text(cx), "Hello 👋".unindent());
9207
9208 editor.handle_input(" :smile", window, cx);
9209 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
9210
9211 editor.handle_input(":", window, cx);
9212 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
9213
9214 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
9215 editor.handle_input(":wave", window, cx);
9216 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
9217
9218 editor.handle_input(":", window, cx);
9219 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
9220
9221 editor.handle_input(":1", window, cx);
9222 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
9223
9224 editor.handle_input(":", window, cx);
9225 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
9226
9227 // Ensure shortcode does not get replaced when it is part of a word
9228 editor.handle_input(" Test:wave", window, cx);
9229 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
9230
9231 editor.handle_input(":", window, cx);
9232 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
9233
9234 editor.set_auto_replace_emoji_shortcode(false);
9235
9236 // Ensure shortcode does not get replaced when auto replace is off
9237 editor.handle_input(" :wave", window, cx);
9238 assert_eq!(
9239 editor.text(cx),
9240 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
9241 );
9242
9243 editor.handle_input(":", window, cx);
9244 assert_eq!(
9245 editor.text(cx),
9246 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
9247 );
9248 });
9249}
9250
9251#[gpui::test]
9252async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
9253 init_test(cx, |_| {});
9254
9255 let (text, insertion_ranges) = marked_text_ranges(
9256 indoc! {"
9257 ˇ
9258 "},
9259 false,
9260 );
9261
9262 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
9263 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9264
9265 _ = editor.update_in(cx, |editor, window, cx| {
9266 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
9267
9268 editor
9269 .insert_snippet(&insertion_ranges, snippet, window, cx)
9270 .unwrap();
9271
9272 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
9273 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
9274 assert_eq!(editor.text(cx), expected_text);
9275 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
9276 }
9277
9278 assert(
9279 editor,
9280 cx,
9281 indoc! {"
9282 type «» =•
9283 "},
9284 );
9285
9286 assert!(editor.context_menu_visible(), "There should be a matches");
9287 });
9288}
9289
9290#[gpui::test]
9291async fn test_snippets(cx: &mut TestAppContext) {
9292 init_test(cx, |_| {});
9293
9294 let mut cx = EditorTestContext::new(cx).await;
9295
9296 cx.set_state(indoc! {"
9297 a.ˇ b
9298 a.ˇ b
9299 a.ˇ b
9300 "});
9301
9302 cx.update_editor(|editor, window, cx| {
9303 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
9304 let insertion_ranges = editor
9305 .selections
9306 .all(cx)
9307 .iter()
9308 .map(|s| s.range().clone())
9309 .collect::<Vec<_>>();
9310 editor
9311 .insert_snippet(&insertion_ranges, snippet, window, cx)
9312 .unwrap();
9313 });
9314
9315 cx.assert_editor_state(indoc! {"
9316 a.f(«oneˇ», two, «threeˇ») b
9317 a.f(«oneˇ», two, «threeˇ») b
9318 a.f(«oneˇ», two, «threeˇ») b
9319 "});
9320
9321 // Can't move earlier than the first tab stop
9322 cx.update_editor(|editor, window, cx| {
9323 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9324 });
9325 cx.assert_editor_state(indoc! {"
9326 a.f(«oneˇ», two, «threeˇ») b
9327 a.f(«oneˇ», two, «threeˇ») b
9328 a.f(«oneˇ», two, «threeˇ») b
9329 "});
9330
9331 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9332 cx.assert_editor_state(indoc! {"
9333 a.f(one, «twoˇ», three) b
9334 a.f(one, «twoˇ», three) b
9335 a.f(one, «twoˇ», three) b
9336 "});
9337
9338 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
9339 cx.assert_editor_state(indoc! {"
9340 a.f(«oneˇ», two, «threeˇ») b
9341 a.f(«oneˇ», two, «threeˇ») b
9342 a.f(«oneˇ», two, «threeˇ») b
9343 "});
9344
9345 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9346 cx.assert_editor_state(indoc! {"
9347 a.f(one, «twoˇ», three) b
9348 a.f(one, «twoˇ», three) b
9349 a.f(one, «twoˇ», three) b
9350 "});
9351 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9352 cx.assert_editor_state(indoc! {"
9353 a.f(one, two, three)ˇ b
9354 a.f(one, two, three)ˇ b
9355 a.f(one, two, three)ˇ b
9356 "});
9357
9358 // As soon as the last tab stop is reached, snippet state is gone
9359 cx.update_editor(|editor, window, cx| {
9360 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9361 });
9362 cx.assert_editor_state(indoc! {"
9363 a.f(one, two, three)ˇ b
9364 a.f(one, two, three)ˇ b
9365 a.f(one, two, three)ˇ b
9366 "});
9367}
9368
9369#[gpui::test]
9370async fn test_snippet_indentation(cx: &mut TestAppContext) {
9371 init_test(cx, |_| {});
9372
9373 let mut cx = EditorTestContext::new(cx).await;
9374
9375 cx.update_editor(|editor, window, cx| {
9376 let snippet = Snippet::parse(indoc! {"
9377 /*
9378 * Multiline comment with leading indentation
9379 *
9380 * $1
9381 */
9382 $0"})
9383 .unwrap();
9384 let insertion_ranges = editor
9385 .selections
9386 .all(cx)
9387 .iter()
9388 .map(|s| s.range().clone())
9389 .collect::<Vec<_>>();
9390 editor
9391 .insert_snippet(&insertion_ranges, snippet, window, cx)
9392 .unwrap();
9393 });
9394
9395 cx.assert_editor_state(indoc! {"
9396 /*
9397 * Multiline comment with leading indentation
9398 *
9399 * ˇ
9400 */
9401 "});
9402
9403 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9404 cx.assert_editor_state(indoc! {"
9405 /*
9406 * Multiline comment with leading indentation
9407 *
9408 *•
9409 */
9410 ˇ"});
9411}
9412
9413#[gpui::test]
9414async fn test_document_format_during_save(cx: &mut TestAppContext) {
9415 init_test(cx, |_| {});
9416
9417 let fs = FakeFs::new(cx.executor());
9418 fs.insert_file(path!("/file.rs"), Default::default()).await;
9419
9420 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
9421
9422 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9423 language_registry.add(rust_lang());
9424 let mut fake_servers = language_registry.register_fake_lsp(
9425 "Rust",
9426 FakeLspAdapter {
9427 capabilities: lsp::ServerCapabilities {
9428 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9429 ..Default::default()
9430 },
9431 ..Default::default()
9432 },
9433 );
9434
9435 let buffer = project
9436 .update(cx, |project, cx| {
9437 project.open_local_buffer(path!("/file.rs"), cx)
9438 })
9439 .await
9440 .unwrap();
9441
9442 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9443 let (editor, cx) = cx.add_window_view(|window, cx| {
9444 build_editor_with_project(project.clone(), buffer, window, cx)
9445 });
9446 editor.update_in(cx, |editor, window, cx| {
9447 editor.set_text("one\ntwo\nthree\n", window, cx)
9448 });
9449 assert!(cx.read(|cx| editor.is_dirty(cx)));
9450
9451 cx.executor().start_waiting();
9452 let fake_server = fake_servers.next().await.unwrap();
9453
9454 {
9455 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9456 move |params, _| async move {
9457 assert_eq!(
9458 params.text_document.uri,
9459 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9460 );
9461 assert_eq!(params.options.tab_size, 4);
9462 Ok(Some(vec![lsp::TextEdit::new(
9463 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9464 ", ".to_string(),
9465 )]))
9466 },
9467 );
9468 let save = editor
9469 .update_in(cx, |editor, window, cx| {
9470 editor.save(
9471 SaveOptions {
9472 format: true,
9473 autosave: false,
9474 },
9475 project.clone(),
9476 window,
9477 cx,
9478 )
9479 })
9480 .unwrap();
9481 cx.executor().start_waiting();
9482 save.await;
9483
9484 assert_eq!(
9485 editor.update(cx, |editor, cx| editor.text(cx)),
9486 "one, two\nthree\n"
9487 );
9488 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9489 }
9490
9491 {
9492 editor.update_in(cx, |editor, window, cx| {
9493 editor.set_text("one\ntwo\nthree\n", window, cx)
9494 });
9495 assert!(cx.read(|cx| editor.is_dirty(cx)));
9496
9497 // Ensure we can still save even if formatting hangs.
9498 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9499 move |params, _| async move {
9500 assert_eq!(
9501 params.text_document.uri,
9502 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9503 );
9504 futures::future::pending::<()>().await;
9505 unreachable!()
9506 },
9507 );
9508 let save = editor
9509 .update_in(cx, |editor, window, cx| {
9510 editor.save(
9511 SaveOptions {
9512 format: true,
9513 autosave: false,
9514 },
9515 project.clone(),
9516 window,
9517 cx,
9518 )
9519 })
9520 .unwrap();
9521 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9522 cx.executor().start_waiting();
9523 save.await;
9524 assert_eq!(
9525 editor.update(cx, |editor, cx| editor.text(cx)),
9526 "one\ntwo\nthree\n"
9527 );
9528 }
9529
9530 // Set rust language override and assert overridden tabsize is sent to language server
9531 update_test_language_settings(cx, |settings| {
9532 settings.languages.0.insert(
9533 "Rust".into(),
9534 LanguageSettingsContent {
9535 tab_size: NonZeroU32::new(8),
9536 ..Default::default()
9537 },
9538 );
9539 });
9540
9541 {
9542 editor.update_in(cx, |editor, window, cx| {
9543 editor.set_text("somehting_new\n", window, cx)
9544 });
9545 assert!(cx.read(|cx| editor.is_dirty(cx)));
9546 let _formatting_request_signal = fake_server
9547 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9548 assert_eq!(
9549 params.text_document.uri,
9550 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9551 );
9552 assert_eq!(params.options.tab_size, 8);
9553 Ok(Some(vec![]))
9554 });
9555 let save = editor
9556 .update_in(cx, |editor, window, cx| {
9557 editor.save(
9558 SaveOptions {
9559 format: true,
9560 autosave: false,
9561 },
9562 project.clone(),
9563 window,
9564 cx,
9565 )
9566 })
9567 .unwrap();
9568 cx.executor().start_waiting();
9569 save.await;
9570 }
9571}
9572
9573#[gpui::test]
9574async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
9575 init_test(cx, |settings| {
9576 settings.defaults.ensure_final_newline_on_save = Some(false);
9577 });
9578
9579 let fs = FakeFs::new(cx.executor());
9580 fs.insert_file(path!("/file.txt"), "foo".into()).await;
9581
9582 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
9583
9584 let buffer = project
9585 .update(cx, |project, cx| {
9586 project.open_local_buffer(path!("/file.txt"), cx)
9587 })
9588 .await
9589 .unwrap();
9590
9591 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9592 let (editor, cx) = cx.add_window_view(|window, cx| {
9593 build_editor_with_project(project.clone(), buffer, window, cx)
9594 });
9595 editor.update_in(cx, |editor, window, cx| {
9596 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9597 s.select_ranges([0..0])
9598 });
9599 });
9600 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9601
9602 editor.update_in(cx, |editor, window, cx| {
9603 editor.handle_input("\n", window, cx)
9604 });
9605 cx.run_until_parked();
9606 save(&editor, &project, cx).await;
9607 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9608
9609 editor.update_in(cx, |editor, window, cx| {
9610 editor.undo(&Default::default(), window, cx);
9611 });
9612 save(&editor, &project, cx).await;
9613 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9614
9615 editor.update_in(cx, |editor, window, cx| {
9616 editor.redo(&Default::default(), window, cx);
9617 });
9618 cx.run_until_parked();
9619 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9620
9621 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
9622 let save = editor
9623 .update_in(cx, |editor, window, cx| {
9624 editor.save(
9625 SaveOptions {
9626 format: true,
9627 autosave: false,
9628 },
9629 project.clone(),
9630 window,
9631 cx,
9632 )
9633 })
9634 .unwrap();
9635 cx.executor().start_waiting();
9636 save.await;
9637 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9638 }
9639}
9640
9641#[gpui::test]
9642async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9643 init_test(cx, |_| {});
9644
9645 let cols = 4;
9646 let rows = 10;
9647 let sample_text_1 = sample_text(rows, cols, 'a');
9648 assert_eq!(
9649 sample_text_1,
9650 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9651 );
9652 let sample_text_2 = sample_text(rows, cols, 'l');
9653 assert_eq!(
9654 sample_text_2,
9655 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9656 );
9657 let sample_text_3 = sample_text(rows, cols, 'v');
9658 assert_eq!(
9659 sample_text_3,
9660 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9661 );
9662
9663 let fs = FakeFs::new(cx.executor());
9664 fs.insert_tree(
9665 path!("/a"),
9666 json!({
9667 "main.rs": sample_text_1,
9668 "other.rs": sample_text_2,
9669 "lib.rs": sample_text_3,
9670 }),
9671 )
9672 .await;
9673
9674 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9675 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9676 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9677
9678 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9679 language_registry.add(rust_lang());
9680 let mut fake_servers = language_registry.register_fake_lsp(
9681 "Rust",
9682 FakeLspAdapter {
9683 capabilities: lsp::ServerCapabilities {
9684 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9685 ..Default::default()
9686 },
9687 ..Default::default()
9688 },
9689 );
9690
9691 let worktree = project.update(cx, |project, cx| {
9692 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
9693 assert_eq!(worktrees.len(), 1);
9694 worktrees.pop().unwrap()
9695 });
9696 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9697
9698 let buffer_1 = project
9699 .update(cx, |project, cx| {
9700 project.open_buffer((worktree_id, "main.rs"), cx)
9701 })
9702 .await
9703 .unwrap();
9704 let buffer_2 = project
9705 .update(cx, |project, cx| {
9706 project.open_buffer((worktree_id, "other.rs"), cx)
9707 })
9708 .await
9709 .unwrap();
9710 let buffer_3 = project
9711 .update(cx, |project, cx| {
9712 project.open_buffer((worktree_id, "lib.rs"), cx)
9713 })
9714 .await
9715 .unwrap();
9716
9717 let multi_buffer = cx.new(|cx| {
9718 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9719 multi_buffer.push_excerpts(
9720 buffer_1.clone(),
9721 [
9722 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9723 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9724 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9725 ],
9726 cx,
9727 );
9728 multi_buffer.push_excerpts(
9729 buffer_2.clone(),
9730 [
9731 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9732 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9733 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9734 ],
9735 cx,
9736 );
9737 multi_buffer.push_excerpts(
9738 buffer_3.clone(),
9739 [
9740 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9741 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9742 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9743 ],
9744 cx,
9745 );
9746 multi_buffer
9747 });
9748 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
9749 Editor::new(
9750 EditorMode::full(),
9751 multi_buffer,
9752 Some(project.clone()),
9753 window,
9754 cx,
9755 )
9756 });
9757
9758 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9759 editor.change_selections(
9760 SelectionEffects::scroll(Autoscroll::Next),
9761 window,
9762 cx,
9763 |s| s.select_ranges(Some(1..2)),
9764 );
9765 editor.insert("|one|two|three|", window, cx);
9766 });
9767 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9768 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9769 editor.change_selections(
9770 SelectionEffects::scroll(Autoscroll::Next),
9771 window,
9772 cx,
9773 |s| s.select_ranges(Some(60..70)),
9774 );
9775 editor.insert("|four|five|six|", window, cx);
9776 });
9777 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9778
9779 // First two buffers should be edited, but not the third one.
9780 assert_eq!(
9781 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9782 "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}",
9783 );
9784 buffer_1.update(cx, |buffer, _| {
9785 assert!(buffer.is_dirty());
9786 assert_eq!(
9787 buffer.text(),
9788 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
9789 )
9790 });
9791 buffer_2.update(cx, |buffer, _| {
9792 assert!(buffer.is_dirty());
9793 assert_eq!(
9794 buffer.text(),
9795 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
9796 )
9797 });
9798 buffer_3.update(cx, |buffer, _| {
9799 assert!(!buffer.is_dirty());
9800 assert_eq!(buffer.text(), sample_text_3,)
9801 });
9802 cx.executor().run_until_parked();
9803
9804 cx.executor().start_waiting();
9805 let save = multi_buffer_editor
9806 .update_in(cx, |editor, window, cx| {
9807 editor.save(
9808 SaveOptions {
9809 format: true,
9810 autosave: false,
9811 },
9812 project.clone(),
9813 window,
9814 cx,
9815 )
9816 })
9817 .unwrap();
9818
9819 let fake_server = fake_servers.next().await.unwrap();
9820 fake_server
9821 .server
9822 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
9823 Ok(Some(vec![lsp::TextEdit::new(
9824 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9825 format!("[{} formatted]", params.text_document.uri),
9826 )]))
9827 })
9828 .detach();
9829 save.await;
9830
9831 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
9832 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
9833 assert_eq!(
9834 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9835 uri!(
9836 "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}"
9837 ),
9838 );
9839 buffer_1.update(cx, |buffer, _| {
9840 assert!(!buffer.is_dirty());
9841 assert_eq!(
9842 buffer.text(),
9843 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
9844 )
9845 });
9846 buffer_2.update(cx, |buffer, _| {
9847 assert!(!buffer.is_dirty());
9848 assert_eq!(
9849 buffer.text(),
9850 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
9851 )
9852 });
9853 buffer_3.update(cx, |buffer, _| {
9854 assert!(!buffer.is_dirty());
9855 assert_eq!(buffer.text(), sample_text_3,)
9856 });
9857}
9858
9859#[gpui::test]
9860async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
9861 init_test(cx, |_| {});
9862
9863 let fs = FakeFs::new(cx.executor());
9864 fs.insert_tree(
9865 path!("/dir"),
9866 json!({
9867 "file1.rs": "fn main() { println!(\"hello\"); }",
9868 "file2.rs": "fn test() { println!(\"test\"); }",
9869 "file3.rs": "fn other() { println!(\"other\"); }\n",
9870 }),
9871 )
9872 .await;
9873
9874 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
9875 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9876 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9877
9878 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9879 language_registry.add(rust_lang());
9880
9881 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
9882 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9883
9884 // Open three buffers
9885 let buffer_1 = project
9886 .update(cx, |project, cx| {
9887 project.open_buffer((worktree_id, "file1.rs"), cx)
9888 })
9889 .await
9890 .unwrap();
9891 let buffer_2 = project
9892 .update(cx, |project, cx| {
9893 project.open_buffer((worktree_id, "file2.rs"), cx)
9894 })
9895 .await
9896 .unwrap();
9897 let buffer_3 = project
9898 .update(cx, |project, cx| {
9899 project.open_buffer((worktree_id, "file3.rs"), cx)
9900 })
9901 .await
9902 .unwrap();
9903
9904 // Create a multi-buffer with all three buffers
9905 let multi_buffer = cx.new(|cx| {
9906 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9907 multi_buffer.push_excerpts(
9908 buffer_1.clone(),
9909 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9910 cx,
9911 );
9912 multi_buffer.push_excerpts(
9913 buffer_2.clone(),
9914 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9915 cx,
9916 );
9917 multi_buffer.push_excerpts(
9918 buffer_3.clone(),
9919 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9920 cx,
9921 );
9922 multi_buffer
9923 });
9924
9925 let editor = cx.new_window_entity(|window, cx| {
9926 Editor::new(
9927 EditorMode::full(),
9928 multi_buffer,
9929 Some(project.clone()),
9930 window,
9931 cx,
9932 )
9933 });
9934
9935 // Edit only the first buffer
9936 editor.update_in(cx, |editor, window, cx| {
9937 editor.change_selections(
9938 SelectionEffects::scroll(Autoscroll::Next),
9939 window,
9940 cx,
9941 |s| s.select_ranges(Some(10..10)),
9942 );
9943 editor.insert("// edited", window, cx);
9944 });
9945
9946 // Verify that only buffer 1 is dirty
9947 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
9948 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9949 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9950
9951 // Get write counts after file creation (files were created with initial content)
9952 // We expect each file to have been written once during creation
9953 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
9954 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
9955 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
9956
9957 // Perform autosave
9958 let save_task = editor.update_in(cx, |editor, window, cx| {
9959 editor.save(
9960 SaveOptions {
9961 format: true,
9962 autosave: true,
9963 },
9964 project.clone(),
9965 window,
9966 cx,
9967 )
9968 });
9969 save_task.await.unwrap();
9970
9971 // Only the dirty buffer should have been saved
9972 assert_eq!(
9973 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9974 1,
9975 "Buffer 1 was dirty, so it should have been written once during autosave"
9976 );
9977 assert_eq!(
9978 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9979 0,
9980 "Buffer 2 was clean, so it should not have been written during autosave"
9981 );
9982 assert_eq!(
9983 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9984 0,
9985 "Buffer 3 was clean, so it should not have been written during autosave"
9986 );
9987
9988 // Verify buffer states after autosave
9989 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9990 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9991 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9992
9993 // Now perform a manual save (format = true)
9994 let save_task = editor.update_in(cx, |editor, window, cx| {
9995 editor.save(
9996 SaveOptions {
9997 format: true,
9998 autosave: false,
9999 },
10000 project.clone(),
10001 window,
10002 cx,
10003 )
10004 });
10005 save_task.await.unwrap();
10006
10007 // During manual save, clean buffers don't get written to disk
10008 // They just get did_save called for language server notifications
10009 assert_eq!(
10010 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
10011 1,
10012 "Buffer 1 should only have been written once total (during autosave, not manual save)"
10013 );
10014 assert_eq!(
10015 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
10016 0,
10017 "Buffer 2 should not have been written at all"
10018 );
10019 assert_eq!(
10020 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
10021 0,
10022 "Buffer 3 should not have been written at all"
10023 );
10024}
10025
10026#[gpui::test]
10027async fn test_range_format_during_save(cx: &mut TestAppContext) {
10028 init_test(cx, |_| {});
10029
10030 let fs = FakeFs::new(cx.executor());
10031 fs.insert_file(path!("/file.rs"), Default::default()).await;
10032
10033 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10034
10035 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10036 language_registry.add(rust_lang());
10037 let mut fake_servers = language_registry.register_fake_lsp(
10038 "Rust",
10039 FakeLspAdapter {
10040 capabilities: lsp::ServerCapabilities {
10041 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
10042 ..Default::default()
10043 },
10044 ..Default::default()
10045 },
10046 );
10047
10048 let buffer = project
10049 .update(cx, |project, cx| {
10050 project.open_local_buffer(path!("/file.rs"), cx)
10051 })
10052 .await
10053 .unwrap();
10054
10055 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10056 let (editor, cx) = cx.add_window_view(|window, cx| {
10057 build_editor_with_project(project.clone(), buffer, window, cx)
10058 });
10059 editor.update_in(cx, |editor, window, cx| {
10060 editor.set_text("one\ntwo\nthree\n", window, cx)
10061 });
10062 assert!(cx.read(|cx| editor.is_dirty(cx)));
10063
10064 cx.executor().start_waiting();
10065 let fake_server = fake_servers.next().await.unwrap();
10066
10067 let save = editor
10068 .update_in(cx, |editor, window, cx| {
10069 editor.save(
10070 SaveOptions {
10071 format: true,
10072 autosave: false,
10073 },
10074 project.clone(),
10075 window,
10076 cx,
10077 )
10078 })
10079 .unwrap();
10080 fake_server
10081 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10082 assert_eq!(
10083 params.text_document.uri,
10084 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10085 );
10086 assert_eq!(params.options.tab_size, 4);
10087 Ok(Some(vec![lsp::TextEdit::new(
10088 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10089 ", ".to_string(),
10090 )]))
10091 })
10092 .next()
10093 .await;
10094 cx.executor().start_waiting();
10095 save.await;
10096 assert_eq!(
10097 editor.update(cx, |editor, cx| editor.text(cx)),
10098 "one, two\nthree\n"
10099 );
10100 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10101
10102 editor.update_in(cx, |editor, window, cx| {
10103 editor.set_text("one\ntwo\nthree\n", window, cx)
10104 });
10105 assert!(cx.read(|cx| editor.is_dirty(cx)));
10106
10107 // Ensure we can still save even if formatting hangs.
10108 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
10109 move |params, _| async move {
10110 assert_eq!(
10111 params.text_document.uri,
10112 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10113 );
10114 futures::future::pending::<()>().await;
10115 unreachable!()
10116 },
10117 );
10118 let save = editor
10119 .update_in(cx, |editor, window, cx| {
10120 editor.save(
10121 SaveOptions {
10122 format: true,
10123 autosave: false,
10124 },
10125 project.clone(),
10126 window,
10127 cx,
10128 )
10129 })
10130 .unwrap();
10131 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10132 cx.executor().start_waiting();
10133 save.await;
10134 assert_eq!(
10135 editor.update(cx, |editor, cx| editor.text(cx)),
10136 "one\ntwo\nthree\n"
10137 );
10138 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10139
10140 // For non-dirty buffer, no formatting request should be sent
10141 let save = editor
10142 .update_in(cx, |editor, window, cx| {
10143 editor.save(
10144 SaveOptions {
10145 format: false,
10146 autosave: false,
10147 },
10148 project.clone(),
10149 window,
10150 cx,
10151 )
10152 })
10153 .unwrap();
10154 let _pending_format_request = fake_server
10155 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
10156 panic!("Should not be invoked");
10157 })
10158 .next();
10159 cx.executor().start_waiting();
10160 save.await;
10161
10162 // Set Rust language override and assert overridden tabsize is sent to language server
10163 update_test_language_settings(cx, |settings| {
10164 settings.languages.0.insert(
10165 "Rust".into(),
10166 LanguageSettingsContent {
10167 tab_size: NonZeroU32::new(8),
10168 ..Default::default()
10169 },
10170 );
10171 });
10172
10173 editor.update_in(cx, |editor, window, cx| {
10174 editor.set_text("somehting_new\n", window, cx)
10175 });
10176 assert!(cx.read(|cx| editor.is_dirty(cx)));
10177 let save = editor
10178 .update_in(cx, |editor, window, cx| {
10179 editor.save(
10180 SaveOptions {
10181 format: true,
10182 autosave: false,
10183 },
10184 project.clone(),
10185 window,
10186 cx,
10187 )
10188 })
10189 .unwrap();
10190 fake_server
10191 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10192 assert_eq!(
10193 params.text_document.uri,
10194 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10195 );
10196 assert_eq!(params.options.tab_size, 8);
10197 Ok(Some(Vec::new()))
10198 })
10199 .next()
10200 .await;
10201 save.await;
10202}
10203
10204#[gpui::test]
10205async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
10206 init_test(cx, |settings| {
10207 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
10208 Formatter::LanguageServer { name: None },
10209 )))
10210 });
10211
10212 let fs = FakeFs::new(cx.executor());
10213 fs.insert_file(path!("/file.rs"), Default::default()).await;
10214
10215 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10216
10217 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10218 language_registry.add(Arc::new(Language::new(
10219 LanguageConfig {
10220 name: "Rust".into(),
10221 matcher: LanguageMatcher {
10222 path_suffixes: vec!["rs".to_string()],
10223 ..Default::default()
10224 },
10225 ..LanguageConfig::default()
10226 },
10227 Some(tree_sitter_rust::LANGUAGE.into()),
10228 )));
10229 update_test_language_settings(cx, |settings| {
10230 // Enable Prettier formatting for the same buffer, and ensure
10231 // LSP is called instead of Prettier.
10232 settings.defaults.prettier = Some(PrettierSettings {
10233 allowed: true,
10234 ..PrettierSettings::default()
10235 });
10236 });
10237 let mut fake_servers = language_registry.register_fake_lsp(
10238 "Rust",
10239 FakeLspAdapter {
10240 capabilities: lsp::ServerCapabilities {
10241 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10242 ..Default::default()
10243 },
10244 ..Default::default()
10245 },
10246 );
10247
10248 let buffer = project
10249 .update(cx, |project, cx| {
10250 project.open_local_buffer(path!("/file.rs"), cx)
10251 })
10252 .await
10253 .unwrap();
10254
10255 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10256 let (editor, cx) = cx.add_window_view(|window, cx| {
10257 build_editor_with_project(project.clone(), buffer, window, cx)
10258 });
10259 editor.update_in(cx, |editor, window, cx| {
10260 editor.set_text("one\ntwo\nthree\n", window, cx)
10261 });
10262
10263 cx.executor().start_waiting();
10264 let fake_server = fake_servers.next().await.unwrap();
10265
10266 let format = editor
10267 .update_in(cx, |editor, window, cx| {
10268 editor.perform_format(
10269 project.clone(),
10270 FormatTrigger::Manual,
10271 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10272 window,
10273 cx,
10274 )
10275 })
10276 .unwrap();
10277 fake_server
10278 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10279 assert_eq!(
10280 params.text_document.uri,
10281 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10282 );
10283 assert_eq!(params.options.tab_size, 4);
10284 Ok(Some(vec![lsp::TextEdit::new(
10285 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10286 ", ".to_string(),
10287 )]))
10288 })
10289 .next()
10290 .await;
10291 cx.executor().start_waiting();
10292 format.await;
10293 assert_eq!(
10294 editor.update(cx, |editor, cx| editor.text(cx)),
10295 "one, two\nthree\n"
10296 );
10297
10298 editor.update_in(cx, |editor, window, cx| {
10299 editor.set_text("one\ntwo\nthree\n", window, cx)
10300 });
10301 // Ensure we don't lock if formatting hangs.
10302 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10303 move |params, _| async move {
10304 assert_eq!(
10305 params.text_document.uri,
10306 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10307 );
10308 futures::future::pending::<()>().await;
10309 unreachable!()
10310 },
10311 );
10312 let format = editor
10313 .update_in(cx, |editor, window, cx| {
10314 editor.perform_format(
10315 project,
10316 FormatTrigger::Manual,
10317 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10318 window,
10319 cx,
10320 )
10321 })
10322 .unwrap();
10323 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10324 cx.executor().start_waiting();
10325 format.await;
10326 assert_eq!(
10327 editor.update(cx, |editor, cx| editor.text(cx)),
10328 "one\ntwo\nthree\n"
10329 );
10330}
10331
10332#[gpui::test]
10333async fn test_multiple_formatters(cx: &mut TestAppContext) {
10334 init_test(cx, |settings| {
10335 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
10336 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10337 Formatter::LanguageServer { name: None },
10338 Formatter::CodeActions(
10339 [
10340 ("code-action-1".into(), true),
10341 ("code-action-2".into(), true),
10342 ]
10343 .into_iter()
10344 .collect(),
10345 ),
10346 ])))
10347 });
10348
10349 let fs = FakeFs::new(cx.executor());
10350 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
10351 .await;
10352
10353 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10354 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10355 language_registry.add(rust_lang());
10356
10357 let mut fake_servers = language_registry.register_fake_lsp(
10358 "Rust",
10359 FakeLspAdapter {
10360 capabilities: lsp::ServerCapabilities {
10361 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10362 execute_command_provider: Some(lsp::ExecuteCommandOptions {
10363 commands: vec!["the-command-for-code-action-1".into()],
10364 ..Default::default()
10365 }),
10366 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10367 ..Default::default()
10368 },
10369 ..Default::default()
10370 },
10371 );
10372
10373 let buffer = project
10374 .update(cx, |project, cx| {
10375 project.open_local_buffer(path!("/file.rs"), cx)
10376 })
10377 .await
10378 .unwrap();
10379
10380 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10381 let (editor, cx) = cx.add_window_view(|window, cx| {
10382 build_editor_with_project(project.clone(), buffer, window, cx)
10383 });
10384
10385 cx.executor().start_waiting();
10386
10387 let fake_server = fake_servers.next().await.unwrap();
10388 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10389 move |_params, _| async move {
10390 Ok(Some(vec![lsp::TextEdit::new(
10391 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10392 "applied-formatting\n".to_string(),
10393 )]))
10394 },
10395 );
10396 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10397 move |params, _| async move {
10398 assert_eq!(
10399 params.context.only,
10400 Some(vec!["code-action-1".into(), "code-action-2".into()])
10401 );
10402 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
10403 Ok(Some(vec![
10404 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10405 kind: Some("code-action-1".into()),
10406 edit: Some(lsp::WorkspaceEdit::new(
10407 [(
10408 uri.clone(),
10409 vec![lsp::TextEdit::new(
10410 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10411 "applied-code-action-1-edit\n".to_string(),
10412 )],
10413 )]
10414 .into_iter()
10415 .collect(),
10416 )),
10417 command: Some(lsp::Command {
10418 command: "the-command-for-code-action-1".into(),
10419 ..Default::default()
10420 }),
10421 ..Default::default()
10422 }),
10423 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10424 kind: Some("code-action-2".into()),
10425 edit: Some(lsp::WorkspaceEdit::new(
10426 [(
10427 uri.clone(),
10428 vec![lsp::TextEdit::new(
10429 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10430 "applied-code-action-2-edit\n".to_string(),
10431 )],
10432 )]
10433 .into_iter()
10434 .collect(),
10435 )),
10436 ..Default::default()
10437 }),
10438 ]))
10439 },
10440 );
10441
10442 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
10443 move |params, _| async move { Ok(params) }
10444 });
10445
10446 let command_lock = Arc::new(futures::lock::Mutex::new(()));
10447 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
10448 let fake = fake_server.clone();
10449 let lock = command_lock.clone();
10450 move |params, _| {
10451 assert_eq!(params.command, "the-command-for-code-action-1");
10452 let fake = fake.clone();
10453 let lock = lock.clone();
10454 async move {
10455 lock.lock().await;
10456 fake.server
10457 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
10458 label: None,
10459 edit: lsp::WorkspaceEdit {
10460 changes: Some(
10461 [(
10462 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
10463 vec![lsp::TextEdit {
10464 range: lsp::Range::new(
10465 lsp::Position::new(0, 0),
10466 lsp::Position::new(0, 0),
10467 ),
10468 new_text: "applied-code-action-1-command\n".into(),
10469 }],
10470 )]
10471 .into_iter()
10472 .collect(),
10473 ),
10474 ..Default::default()
10475 },
10476 })
10477 .await
10478 .into_response()
10479 .unwrap();
10480 Ok(Some(json!(null)))
10481 }
10482 }
10483 });
10484
10485 cx.executor().start_waiting();
10486 editor
10487 .update_in(cx, |editor, window, cx| {
10488 editor.perform_format(
10489 project.clone(),
10490 FormatTrigger::Manual,
10491 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10492 window,
10493 cx,
10494 )
10495 })
10496 .unwrap()
10497 .await;
10498 editor.update(cx, |editor, cx| {
10499 assert_eq!(
10500 editor.text(cx),
10501 r#"
10502 applied-code-action-2-edit
10503 applied-code-action-1-command
10504 applied-code-action-1-edit
10505 applied-formatting
10506 one
10507 two
10508 three
10509 "#
10510 .unindent()
10511 );
10512 });
10513
10514 editor.update_in(cx, |editor, window, cx| {
10515 editor.undo(&Default::default(), window, cx);
10516 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10517 });
10518
10519 // Perform a manual edit while waiting for an LSP command
10520 // that's being run as part of a formatting code action.
10521 let lock_guard = command_lock.lock().await;
10522 let format = editor
10523 .update_in(cx, |editor, window, cx| {
10524 editor.perform_format(
10525 project.clone(),
10526 FormatTrigger::Manual,
10527 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10528 window,
10529 cx,
10530 )
10531 })
10532 .unwrap();
10533 cx.run_until_parked();
10534 editor.update(cx, |editor, cx| {
10535 assert_eq!(
10536 editor.text(cx),
10537 r#"
10538 applied-code-action-1-edit
10539 applied-formatting
10540 one
10541 two
10542 three
10543 "#
10544 .unindent()
10545 );
10546
10547 editor.buffer.update(cx, |buffer, cx| {
10548 let ix = buffer.len(cx);
10549 buffer.edit([(ix..ix, "edited\n")], None, cx);
10550 });
10551 });
10552
10553 // Allow the LSP command to proceed. Because the buffer was edited,
10554 // the second code action will not be run.
10555 drop(lock_guard);
10556 format.await;
10557 editor.update_in(cx, |editor, window, cx| {
10558 assert_eq!(
10559 editor.text(cx),
10560 r#"
10561 applied-code-action-1-command
10562 applied-code-action-1-edit
10563 applied-formatting
10564 one
10565 two
10566 three
10567 edited
10568 "#
10569 .unindent()
10570 );
10571
10572 // The manual edit is undone first, because it is the last thing the user did
10573 // (even though the command completed afterwards).
10574 editor.undo(&Default::default(), window, cx);
10575 assert_eq!(
10576 editor.text(cx),
10577 r#"
10578 applied-code-action-1-command
10579 applied-code-action-1-edit
10580 applied-formatting
10581 one
10582 two
10583 three
10584 "#
10585 .unindent()
10586 );
10587
10588 // All the formatting (including the command, which completed after the manual edit)
10589 // is undone together.
10590 editor.undo(&Default::default(), window, cx);
10591 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10592 });
10593}
10594
10595#[gpui::test]
10596async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
10597 init_test(cx, |settings| {
10598 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10599 Formatter::LanguageServer { name: None },
10600 ])))
10601 });
10602
10603 let fs = FakeFs::new(cx.executor());
10604 fs.insert_file(path!("/file.ts"), Default::default()).await;
10605
10606 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10607
10608 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10609 language_registry.add(Arc::new(Language::new(
10610 LanguageConfig {
10611 name: "TypeScript".into(),
10612 matcher: LanguageMatcher {
10613 path_suffixes: vec!["ts".to_string()],
10614 ..Default::default()
10615 },
10616 ..LanguageConfig::default()
10617 },
10618 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10619 )));
10620 update_test_language_settings(cx, |settings| {
10621 settings.defaults.prettier = Some(PrettierSettings {
10622 allowed: true,
10623 ..PrettierSettings::default()
10624 });
10625 });
10626 let mut fake_servers = language_registry.register_fake_lsp(
10627 "TypeScript",
10628 FakeLspAdapter {
10629 capabilities: lsp::ServerCapabilities {
10630 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10631 ..Default::default()
10632 },
10633 ..Default::default()
10634 },
10635 );
10636
10637 let buffer = project
10638 .update(cx, |project, cx| {
10639 project.open_local_buffer(path!("/file.ts"), cx)
10640 })
10641 .await
10642 .unwrap();
10643
10644 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10645 let (editor, cx) = cx.add_window_view(|window, cx| {
10646 build_editor_with_project(project.clone(), buffer, window, cx)
10647 });
10648 editor.update_in(cx, |editor, window, cx| {
10649 editor.set_text(
10650 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10651 window,
10652 cx,
10653 )
10654 });
10655
10656 cx.executor().start_waiting();
10657 let fake_server = fake_servers.next().await.unwrap();
10658
10659 let format = editor
10660 .update_in(cx, |editor, window, cx| {
10661 editor.perform_code_action_kind(
10662 project.clone(),
10663 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10664 window,
10665 cx,
10666 )
10667 })
10668 .unwrap();
10669 fake_server
10670 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
10671 assert_eq!(
10672 params.text_document.uri,
10673 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10674 );
10675 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
10676 lsp::CodeAction {
10677 title: "Organize Imports".to_string(),
10678 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
10679 edit: Some(lsp::WorkspaceEdit {
10680 changes: Some(
10681 [(
10682 params.text_document.uri.clone(),
10683 vec![lsp::TextEdit::new(
10684 lsp::Range::new(
10685 lsp::Position::new(1, 0),
10686 lsp::Position::new(2, 0),
10687 ),
10688 "".to_string(),
10689 )],
10690 )]
10691 .into_iter()
10692 .collect(),
10693 ),
10694 ..Default::default()
10695 }),
10696 ..Default::default()
10697 },
10698 )]))
10699 })
10700 .next()
10701 .await;
10702 cx.executor().start_waiting();
10703 format.await;
10704 assert_eq!(
10705 editor.update(cx, |editor, cx| editor.text(cx)),
10706 "import { a } from 'module';\n\nconst x = a;\n"
10707 );
10708
10709 editor.update_in(cx, |editor, window, cx| {
10710 editor.set_text(
10711 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10712 window,
10713 cx,
10714 )
10715 });
10716 // Ensure we don't lock if code action hangs.
10717 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10718 move |params, _| async move {
10719 assert_eq!(
10720 params.text_document.uri,
10721 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10722 );
10723 futures::future::pending::<()>().await;
10724 unreachable!()
10725 },
10726 );
10727 let format = editor
10728 .update_in(cx, |editor, window, cx| {
10729 editor.perform_code_action_kind(
10730 project,
10731 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10732 window,
10733 cx,
10734 )
10735 })
10736 .unwrap();
10737 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
10738 cx.executor().start_waiting();
10739 format.await;
10740 assert_eq!(
10741 editor.update(cx, |editor, cx| editor.text(cx)),
10742 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
10743 );
10744}
10745
10746#[gpui::test]
10747async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
10748 init_test(cx, |_| {});
10749
10750 let mut cx = EditorLspTestContext::new_rust(
10751 lsp::ServerCapabilities {
10752 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10753 ..Default::default()
10754 },
10755 cx,
10756 )
10757 .await;
10758
10759 cx.set_state(indoc! {"
10760 one.twoˇ
10761 "});
10762
10763 // The format request takes a long time. When it completes, it inserts
10764 // a newline and an indent before the `.`
10765 cx.lsp
10766 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
10767 let executor = cx.background_executor().clone();
10768 async move {
10769 executor.timer(Duration::from_millis(100)).await;
10770 Ok(Some(vec![lsp::TextEdit {
10771 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
10772 new_text: "\n ".into(),
10773 }]))
10774 }
10775 });
10776
10777 // Submit a format request.
10778 let format_1 = cx
10779 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10780 .unwrap();
10781 cx.executor().run_until_parked();
10782
10783 // Submit a second format request.
10784 let format_2 = cx
10785 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10786 .unwrap();
10787 cx.executor().run_until_parked();
10788
10789 // Wait for both format requests to complete
10790 cx.executor().advance_clock(Duration::from_millis(200));
10791 cx.executor().start_waiting();
10792 format_1.await.unwrap();
10793 cx.executor().start_waiting();
10794 format_2.await.unwrap();
10795
10796 // The formatting edits only happens once.
10797 cx.assert_editor_state(indoc! {"
10798 one
10799 .twoˇ
10800 "});
10801}
10802
10803#[gpui::test]
10804async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
10805 init_test(cx, |settings| {
10806 settings.defaults.formatter = Some(SelectedFormatter::Auto)
10807 });
10808
10809 let mut cx = EditorLspTestContext::new_rust(
10810 lsp::ServerCapabilities {
10811 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10812 ..Default::default()
10813 },
10814 cx,
10815 )
10816 .await;
10817
10818 // Set up a buffer white some trailing whitespace and no trailing newline.
10819 cx.set_state(
10820 &[
10821 "one ", //
10822 "twoˇ", //
10823 "three ", //
10824 "four", //
10825 ]
10826 .join("\n"),
10827 );
10828
10829 // Submit a format request.
10830 let format = cx
10831 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10832 .unwrap();
10833
10834 // Record which buffer changes have been sent to the language server
10835 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
10836 cx.lsp
10837 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
10838 let buffer_changes = buffer_changes.clone();
10839 move |params, _| {
10840 buffer_changes.lock().extend(
10841 params
10842 .content_changes
10843 .into_iter()
10844 .map(|e| (e.range.unwrap(), e.text)),
10845 );
10846 }
10847 });
10848
10849 // Handle formatting requests to the language server.
10850 cx.lsp
10851 .set_request_handler::<lsp::request::Formatting, _, _>({
10852 let buffer_changes = buffer_changes.clone();
10853 move |_, _| {
10854 // When formatting is requested, trailing whitespace has already been stripped,
10855 // and the trailing newline has already been added.
10856 assert_eq!(
10857 &buffer_changes.lock()[1..],
10858 &[
10859 (
10860 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
10861 "".into()
10862 ),
10863 (
10864 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
10865 "".into()
10866 ),
10867 (
10868 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
10869 "\n".into()
10870 ),
10871 ]
10872 );
10873
10874 // Insert blank lines between each line of the buffer.
10875 async move {
10876 Ok(Some(vec![
10877 lsp::TextEdit {
10878 range: lsp::Range::new(
10879 lsp::Position::new(1, 0),
10880 lsp::Position::new(1, 0),
10881 ),
10882 new_text: "\n".into(),
10883 },
10884 lsp::TextEdit {
10885 range: lsp::Range::new(
10886 lsp::Position::new(2, 0),
10887 lsp::Position::new(2, 0),
10888 ),
10889 new_text: "\n".into(),
10890 },
10891 ]))
10892 }
10893 }
10894 });
10895
10896 // After formatting the buffer, the trailing whitespace is stripped,
10897 // a newline is appended, and the edits provided by the language server
10898 // have been applied.
10899 format.await.unwrap();
10900 cx.assert_editor_state(
10901 &[
10902 "one", //
10903 "", //
10904 "twoˇ", //
10905 "", //
10906 "three", //
10907 "four", //
10908 "", //
10909 ]
10910 .join("\n"),
10911 );
10912
10913 // Undoing the formatting undoes the trailing whitespace removal, the
10914 // trailing newline, and the LSP edits.
10915 cx.update_buffer(|buffer, cx| buffer.undo(cx));
10916 cx.assert_editor_state(
10917 &[
10918 "one ", //
10919 "twoˇ", //
10920 "three ", //
10921 "four", //
10922 ]
10923 .join("\n"),
10924 );
10925}
10926
10927#[gpui::test]
10928async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
10929 cx: &mut TestAppContext,
10930) {
10931 init_test(cx, |_| {});
10932
10933 cx.update(|cx| {
10934 cx.update_global::<SettingsStore, _>(|settings, cx| {
10935 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10936 settings.auto_signature_help = Some(true);
10937 });
10938 });
10939 });
10940
10941 let mut cx = EditorLspTestContext::new_rust(
10942 lsp::ServerCapabilities {
10943 signature_help_provider: Some(lsp::SignatureHelpOptions {
10944 ..Default::default()
10945 }),
10946 ..Default::default()
10947 },
10948 cx,
10949 )
10950 .await;
10951
10952 let language = Language::new(
10953 LanguageConfig {
10954 name: "Rust".into(),
10955 brackets: BracketPairConfig {
10956 pairs: vec![
10957 BracketPair {
10958 start: "{".to_string(),
10959 end: "}".to_string(),
10960 close: true,
10961 surround: true,
10962 newline: true,
10963 },
10964 BracketPair {
10965 start: "(".to_string(),
10966 end: ")".to_string(),
10967 close: true,
10968 surround: true,
10969 newline: true,
10970 },
10971 BracketPair {
10972 start: "/*".to_string(),
10973 end: " */".to_string(),
10974 close: true,
10975 surround: true,
10976 newline: true,
10977 },
10978 BracketPair {
10979 start: "[".to_string(),
10980 end: "]".to_string(),
10981 close: false,
10982 surround: false,
10983 newline: true,
10984 },
10985 BracketPair {
10986 start: "\"".to_string(),
10987 end: "\"".to_string(),
10988 close: true,
10989 surround: true,
10990 newline: false,
10991 },
10992 BracketPair {
10993 start: "<".to_string(),
10994 end: ">".to_string(),
10995 close: false,
10996 surround: true,
10997 newline: true,
10998 },
10999 ],
11000 ..Default::default()
11001 },
11002 autoclose_before: "})]".to_string(),
11003 ..Default::default()
11004 },
11005 Some(tree_sitter_rust::LANGUAGE.into()),
11006 );
11007 let language = Arc::new(language);
11008
11009 cx.language_registry().add(language.clone());
11010 cx.update_buffer(|buffer, cx| {
11011 buffer.set_language(Some(language), cx);
11012 });
11013
11014 cx.set_state(
11015 &r#"
11016 fn main() {
11017 sampleˇ
11018 }
11019 "#
11020 .unindent(),
11021 );
11022
11023 cx.update_editor(|editor, window, cx| {
11024 editor.handle_input("(", window, cx);
11025 });
11026 cx.assert_editor_state(
11027 &"
11028 fn main() {
11029 sample(ˇ)
11030 }
11031 "
11032 .unindent(),
11033 );
11034
11035 let mocked_response = lsp::SignatureHelp {
11036 signatures: vec![lsp::SignatureInformation {
11037 label: "fn sample(param1: u8, param2: u8)".to_string(),
11038 documentation: None,
11039 parameters: Some(vec![
11040 lsp::ParameterInformation {
11041 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11042 documentation: None,
11043 },
11044 lsp::ParameterInformation {
11045 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11046 documentation: None,
11047 },
11048 ]),
11049 active_parameter: None,
11050 }],
11051 active_signature: Some(0),
11052 active_parameter: Some(0),
11053 };
11054 handle_signature_help_request(&mut cx, mocked_response).await;
11055
11056 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11057 .await;
11058
11059 cx.editor(|editor, _, _| {
11060 let signature_help_state = editor.signature_help_state.popover().cloned();
11061 let signature = signature_help_state.unwrap();
11062 assert_eq!(
11063 signature.signatures[signature.current_signature].label,
11064 "fn sample(param1: u8, param2: u8)"
11065 );
11066 });
11067}
11068
11069#[gpui::test]
11070async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
11071 init_test(cx, |_| {});
11072
11073 cx.update(|cx| {
11074 cx.update_global::<SettingsStore, _>(|settings, cx| {
11075 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11076 settings.auto_signature_help = Some(false);
11077 settings.show_signature_help_after_edits = Some(false);
11078 });
11079 });
11080 });
11081
11082 let mut cx = EditorLspTestContext::new_rust(
11083 lsp::ServerCapabilities {
11084 signature_help_provider: Some(lsp::SignatureHelpOptions {
11085 ..Default::default()
11086 }),
11087 ..Default::default()
11088 },
11089 cx,
11090 )
11091 .await;
11092
11093 let language = Language::new(
11094 LanguageConfig {
11095 name: "Rust".into(),
11096 brackets: BracketPairConfig {
11097 pairs: vec![
11098 BracketPair {
11099 start: "{".to_string(),
11100 end: "}".to_string(),
11101 close: true,
11102 surround: true,
11103 newline: true,
11104 },
11105 BracketPair {
11106 start: "(".to_string(),
11107 end: ")".to_string(),
11108 close: true,
11109 surround: true,
11110 newline: true,
11111 },
11112 BracketPair {
11113 start: "/*".to_string(),
11114 end: " */".to_string(),
11115 close: true,
11116 surround: true,
11117 newline: true,
11118 },
11119 BracketPair {
11120 start: "[".to_string(),
11121 end: "]".to_string(),
11122 close: false,
11123 surround: false,
11124 newline: true,
11125 },
11126 BracketPair {
11127 start: "\"".to_string(),
11128 end: "\"".to_string(),
11129 close: true,
11130 surround: true,
11131 newline: false,
11132 },
11133 BracketPair {
11134 start: "<".to_string(),
11135 end: ">".to_string(),
11136 close: false,
11137 surround: true,
11138 newline: true,
11139 },
11140 ],
11141 ..Default::default()
11142 },
11143 autoclose_before: "})]".to_string(),
11144 ..Default::default()
11145 },
11146 Some(tree_sitter_rust::LANGUAGE.into()),
11147 );
11148 let language = Arc::new(language);
11149
11150 cx.language_registry().add(language.clone());
11151 cx.update_buffer(|buffer, cx| {
11152 buffer.set_language(Some(language), cx);
11153 });
11154
11155 // Ensure that signature_help is not called when no signature help is enabled.
11156 cx.set_state(
11157 &r#"
11158 fn main() {
11159 sampleˇ
11160 }
11161 "#
11162 .unindent(),
11163 );
11164 cx.update_editor(|editor, window, cx| {
11165 editor.handle_input("(", window, cx);
11166 });
11167 cx.assert_editor_state(
11168 &"
11169 fn main() {
11170 sample(ˇ)
11171 }
11172 "
11173 .unindent(),
11174 );
11175 cx.editor(|editor, _, _| {
11176 assert!(editor.signature_help_state.task().is_none());
11177 });
11178
11179 let mocked_response = lsp::SignatureHelp {
11180 signatures: vec![lsp::SignatureInformation {
11181 label: "fn sample(param1: u8, param2: u8)".to_string(),
11182 documentation: None,
11183 parameters: Some(vec![
11184 lsp::ParameterInformation {
11185 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11186 documentation: None,
11187 },
11188 lsp::ParameterInformation {
11189 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11190 documentation: None,
11191 },
11192 ]),
11193 active_parameter: None,
11194 }],
11195 active_signature: Some(0),
11196 active_parameter: Some(0),
11197 };
11198
11199 // Ensure that signature_help is called when enabled afte edits
11200 cx.update(|_, cx| {
11201 cx.update_global::<SettingsStore, _>(|settings, cx| {
11202 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11203 settings.auto_signature_help = Some(false);
11204 settings.show_signature_help_after_edits = Some(true);
11205 });
11206 });
11207 });
11208 cx.set_state(
11209 &r#"
11210 fn main() {
11211 sampleˇ
11212 }
11213 "#
11214 .unindent(),
11215 );
11216 cx.update_editor(|editor, window, cx| {
11217 editor.handle_input("(", window, cx);
11218 });
11219 cx.assert_editor_state(
11220 &"
11221 fn main() {
11222 sample(ˇ)
11223 }
11224 "
11225 .unindent(),
11226 );
11227 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11228 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11229 .await;
11230 cx.update_editor(|editor, _, _| {
11231 let signature_help_state = editor.signature_help_state.popover().cloned();
11232 assert!(signature_help_state.is_some());
11233 let signature = signature_help_state.unwrap();
11234 assert_eq!(
11235 signature.signatures[signature.current_signature].label,
11236 "fn sample(param1: u8, param2: u8)"
11237 );
11238 editor.signature_help_state = SignatureHelpState::default();
11239 });
11240
11241 // Ensure that signature_help is called when auto signature help override is enabled
11242 cx.update(|_, cx| {
11243 cx.update_global::<SettingsStore, _>(|settings, cx| {
11244 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11245 settings.auto_signature_help = Some(true);
11246 settings.show_signature_help_after_edits = Some(false);
11247 });
11248 });
11249 });
11250 cx.set_state(
11251 &r#"
11252 fn main() {
11253 sampleˇ
11254 }
11255 "#
11256 .unindent(),
11257 );
11258 cx.update_editor(|editor, window, cx| {
11259 editor.handle_input("(", window, cx);
11260 });
11261 cx.assert_editor_state(
11262 &"
11263 fn main() {
11264 sample(ˇ)
11265 }
11266 "
11267 .unindent(),
11268 );
11269 handle_signature_help_request(&mut cx, mocked_response).await;
11270 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11271 .await;
11272 cx.editor(|editor, _, _| {
11273 let signature_help_state = editor.signature_help_state.popover().cloned();
11274 assert!(signature_help_state.is_some());
11275 let signature = signature_help_state.unwrap();
11276 assert_eq!(
11277 signature.signatures[signature.current_signature].label,
11278 "fn sample(param1: u8, param2: u8)"
11279 );
11280 });
11281}
11282
11283#[gpui::test]
11284async fn test_signature_help(cx: &mut TestAppContext) {
11285 init_test(cx, |_| {});
11286 cx.update(|cx| {
11287 cx.update_global::<SettingsStore, _>(|settings, cx| {
11288 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11289 settings.auto_signature_help = Some(true);
11290 });
11291 });
11292 });
11293
11294 let mut cx = EditorLspTestContext::new_rust(
11295 lsp::ServerCapabilities {
11296 signature_help_provider: Some(lsp::SignatureHelpOptions {
11297 ..Default::default()
11298 }),
11299 ..Default::default()
11300 },
11301 cx,
11302 )
11303 .await;
11304
11305 // A test that directly calls `show_signature_help`
11306 cx.update_editor(|editor, window, cx| {
11307 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11308 });
11309
11310 let mocked_response = lsp::SignatureHelp {
11311 signatures: vec![lsp::SignatureInformation {
11312 label: "fn sample(param1: u8, param2: u8)".to_string(),
11313 documentation: None,
11314 parameters: Some(vec![
11315 lsp::ParameterInformation {
11316 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11317 documentation: None,
11318 },
11319 lsp::ParameterInformation {
11320 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11321 documentation: None,
11322 },
11323 ]),
11324 active_parameter: None,
11325 }],
11326 active_signature: Some(0),
11327 active_parameter: Some(0),
11328 };
11329 handle_signature_help_request(&mut cx, mocked_response).await;
11330
11331 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11332 .await;
11333
11334 cx.editor(|editor, _, _| {
11335 let signature_help_state = editor.signature_help_state.popover().cloned();
11336 assert!(signature_help_state.is_some());
11337 let signature = signature_help_state.unwrap();
11338 assert_eq!(
11339 signature.signatures[signature.current_signature].label,
11340 "fn sample(param1: u8, param2: u8)"
11341 );
11342 });
11343
11344 // When exiting outside from inside the brackets, `signature_help` is closed.
11345 cx.set_state(indoc! {"
11346 fn main() {
11347 sample(ˇ);
11348 }
11349
11350 fn sample(param1: u8, param2: u8) {}
11351 "});
11352
11353 cx.update_editor(|editor, window, cx| {
11354 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11355 s.select_ranges([0..0])
11356 });
11357 });
11358
11359 let mocked_response = lsp::SignatureHelp {
11360 signatures: Vec::new(),
11361 active_signature: None,
11362 active_parameter: None,
11363 };
11364 handle_signature_help_request(&mut cx, mocked_response).await;
11365
11366 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11367 .await;
11368
11369 cx.editor(|editor, _, _| {
11370 assert!(!editor.signature_help_state.is_shown());
11371 });
11372
11373 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
11374 cx.set_state(indoc! {"
11375 fn main() {
11376 sample(ˇ);
11377 }
11378
11379 fn sample(param1: u8, param2: u8) {}
11380 "});
11381
11382 let mocked_response = lsp::SignatureHelp {
11383 signatures: vec![lsp::SignatureInformation {
11384 label: "fn sample(param1: u8, param2: u8)".to_string(),
11385 documentation: None,
11386 parameters: Some(vec![
11387 lsp::ParameterInformation {
11388 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11389 documentation: None,
11390 },
11391 lsp::ParameterInformation {
11392 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11393 documentation: None,
11394 },
11395 ]),
11396 active_parameter: None,
11397 }],
11398 active_signature: Some(0),
11399 active_parameter: Some(0),
11400 };
11401 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11402 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11403 .await;
11404 cx.editor(|editor, _, _| {
11405 assert!(editor.signature_help_state.is_shown());
11406 });
11407
11408 // Restore the popover with more parameter input
11409 cx.set_state(indoc! {"
11410 fn main() {
11411 sample(param1, param2ˇ);
11412 }
11413
11414 fn sample(param1: u8, param2: u8) {}
11415 "});
11416
11417 let mocked_response = lsp::SignatureHelp {
11418 signatures: vec![lsp::SignatureInformation {
11419 label: "fn sample(param1: u8, param2: u8)".to_string(),
11420 documentation: None,
11421 parameters: Some(vec![
11422 lsp::ParameterInformation {
11423 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11424 documentation: None,
11425 },
11426 lsp::ParameterInformation {
11427 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11428 documentation: None,
11429 },
11430 ]),
11431 active_parameter: None,
11432 }],
11433 active_signature: Some(0),
11434 active_parameter: Some(1),
11435 };
11436 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11437 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11438 .await;
11439
11440 // When selecting a range, the popover is gone.
11441 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
11442 cx.update_editor(|editor, window, cx| {
11443 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11444 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11445 })
11446 });
11447 cx.assert_editor_state(indoc! {"
11448 fn main() {
11449 sample(param1, «ˇparam2»);
11450 }
11451
11452 fn sample(param1: u8, param2: u8) {}
11453 "});
11454 cx.editor(|editor, _, _| {
11455 assert!(!editor.signature_help_state.is_shown());
11456 });
11457
11458 // When unselecting again, the popover is back if within the brackets.
11459 cx.update_editor(|editor, window, cx| {
11460 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11461 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11462 })
11463 });
11464 cx.assert_editor_state(indoc! {"
11465 fn main() {
11466 sample(param1, ˇparam2);
11467 }
11468
11469 fn sample(param1: u8, param2: u8) {}
11470 "});
11471 handle_signature_help_request(&mut cx, mocked_response).await;
11472 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11473 .await;
11474 cx.editor(|editor, _, _| {
11475 assert!(editor.signature_help_state.is_shown());
11476 });
11477
11478 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
11479 cx.update_editor(|editor, window, cx| {
11480 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11481 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
11482 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11483 })
11484 });
11485 cx.assert_editor_state(indoc! {"
11486 fn main() {
11487 sample(param1, ˇparam2);
11488 }
11489
11490 fn sample(param1: u8, param2: u8) {}
11491 "});
11492
11493 let mocked_response = lsp::SignatureHelp {
11494 signatures: vec![lsp::SignatureInformation {
11495 label: "fn sample(param1: u8, param2: u8)".to_string(),
11496 documentation: None,
11497 parameters: Some(vec![
11498 lsp::ParameterInformation {
11499 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11500 documentation: None,
11501 },
11502 lsp::ParameterInformation {
11503 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11504 documentation: None,
11505 },
11506 ]),
11507 active_parameter: None,
11508 }],
11509 active_signature: Some(0),
11510 active_parameter: Some(1),
11511 };
11512 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11513 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11514 .await;
11515 cx.update_editor(|editor, _, cx| {
11516 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
11517 });
11518 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11519 .await;
11520 cx.update_editor(|editor, window, cx| {
11521 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11522 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11523 })
11524 });
11525 cx.assert_editor_state(indoc! {"
11526 fn main() {
11527 sample(param1, «ˇparam2»);
11528 }
11529
11530 fn sample(param1: u8, param2: u8) {}
11531 "});
11532 cx.update_editor(|editor, window, cx| {
11533 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11534 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11535 })
11536 });
11537 cx.assert_editor_state(indoc! {"
11538 fn main() {
11539 sample(param1, ˇparam2);
11540 }
11541
11542 fn sample(param1: u8, param2: u8) {}
11543 "});
11544 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
11545 .await;
11546}
11547
11548#[gpui::test]
11549async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
11550 init_test(cx, |_| {});
11551
11552 let mut cx = EditorLspTestContext::new_rust(
11553 lsp::ServerCapabilities {
11554 signature_help_provider: Some(lsp::SignatureHelpOptions {
11555 ..Default::default()
11556 }),
11557 ..Default::default()
11558 },
11559 cx,
11560 )
11561 .await;
11562
11563 cx.set_state(indoc! {"
11564 fn main() {
11565 overloadedˇ
11566 }
11567 "});
11568
11569 cx.update_editor(|editor, window, cx| {
11570 editor.handle_input("(", window, cx);
11571 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11572 });
11573
11574 // Mock response with 3 signatures
11575 let mocked_response = lsp::SignatureHelp {
11576 signatures: vec![
11577 lsp::SignatureInformation {
11578 label: "fn overloaded(x: i32)".to_string(),
11579 documentation: None,
11580 parameters: Some(vec![lsp::ParameterInformation {
11581 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11582 documentation: None,
11583 }]),
11584 active_parameter: None,
11585 },
11586 lsp::SignatureInformation {
11587 label: "fn overloaded(x: i32, y: i32)".to_string(),
11588 documentation: None,
11589 parameters: Some(vec![
11590 lsp::ParameterInformation {
11591 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11592 documentation: None,
11593 },
11594 lsp::ParameterInformation {
11595 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11596 documentation: None,
11597 },
11598 ]),
11599 active_parameter: None,
11600 },
11601 lsp::SignatureInformation {
11602 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
11603 documentation: None,
11604 parameters: Some(vec![
11605 lsp::ParameterInformation {
11606 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11607 documentation: None,
11608 },
11609 lsp::ParameterInformation {
11610 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11611 documentation: None,
11612 },
11613 lsp::ParameterInformation {
11614 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
11615 documentation: None,
11616 },
11617 ]),
11618 active_parameter: None,
11619 },
11620 ],
11621 active_signature: Some(1),
11622 active_parameter: Some(0),
11623 };
11624 handle_signature_help_request(&mut cx, mocked_response).await;
11625
11626 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11627 .await;
11628
11629 // Verify we have multiple signatures and the right one is selected
11630 cx.editor(|editor, _, _| {
11631 let popover = editor.signature_help_state.popover().cloned().unwrap();
11632 assert_eq!(popover.signatures.len(), 3);
11633 // active_signature was 1, so that should be the current
11634 assert_eq!(popover.current_signature, 1);
11635 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
11636 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
11637 assert_eq!(
11638 popover.signatures[2].label,
11639 "fn overloaded(x: i32, y: i32, z: i32)"
11640 );
11641 });
11642
11643 // Test navigation functionality
11644 cx.update_editor(|editor, window, cx| {
11645 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11646 });
11647
11648 cx.editor(|editor, _, _| {
11649 let popover = editor.signature_help_state.popover().cloned().unwrap();
11650 assert_eq!(popover.current_signature, 2);
11651 });
11652
11653 // Test wrap around
11654 cx.update_editor(|editor, window, cx| {
11655 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11656 });
11657
11658 cx.editor(|editor, _, _| {
11659 let popover = editor.signature_help_state.popover().cloned().unwrap();
11660 assert_eq!(popover.current_signature, 0);
11661 });
11662
11663 // Test previous navigation
11664 cx.update_editor(|editor, window, cx| {
11665 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
11666 });
11667
11668 cx.editor(|editor, _, _| {
11669 let popover = editor.signature_help_state.popover().cloned().unwrap();
11670 assert_eq!(popover.current_signature, 2);
11671 });
11672}
11673
11674#[gpui::test]
11675async fn test_completion_mode(cx: &mut TestAppContext) {
11676 init_test(cx, |_| {});
11677 let mut cx = EditorLspTestContext::new_rust(
11678 lsp::ServerCapabilities {
11679 completion_provider: Some(lsp::CompletionOptions {
11680 resolve_provider: Some(true),
11681 ..Default::default()
11682 }),
11683 ..Default::default()
11684 },
11685 cx,
11686 )
11687 .await;
11688
11689 struct Run {
11690 run_description: &'static str,
11691 initial_state: String,
11692 buffer_marked_text: String,
11693 completion_label: &'static str,
11694 completion_text: &'static str,
11695 expected_with_insert_mode: String,
11696 expected_with_replace_mode: String,
11697 expected_with_replace_subsequence_mode: String,
11698 expected_with_replace_suffix_mode: String,
11699 }
11700
11701 let runs = [
11702 Run {
11703 run_description: "Start of word matches completion text",
11704 initial_state: "before ediˇ after".into(),
11705 buffer_marked_text: "before <edi|> after".into(),
11706 completion_label: "editor",
11707 completion_text: "editor",
11708 expected_with_insert_mode: "before editorˇ after".into(),
11709 expected_with_replace_mode: "before editorˇ after".into(),
11710 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11711 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11712 },
11713 Run {
11714 run_description: "Accept same text at the middle of the word",
11715 initial_state: "before ediˇtor after".into(),
11716 buffer_marked_text: "before <edi|tor> after".into(),
11717 completion_label: "editor",
11718 completion_text: "editor",
11719 expected_with_insert_mode: "before editorˇtor after".into(),
11720 expected_with_replace_mode: "before editorˇ after".into(),
11721 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11722 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11723 },
11724 Run {
11725 run_description: "End of word matches completion text -- cursor at end",
11726 initial_state: "before torˇ after".into(),
11727 buffer_marked_text: "before <tor|> after".into(),
11728 completion_label: "editor",
11729 completion_text: "editor",
11730 expected_with_insert_mode: "before editorˇ after".into(),
11731 expected_with_replace_mode: "before editorˇ after".into(),
11732 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11733 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11734 },
11735 Run {
11736 run_description: "End of word matches completion text -- cursor at start",
11737 initial_state: "before ˇtor after".into(),
11738 buffer_marked_text: "before <|tor> after".into(),
11739 completion_label: "editor",
11740 completion_text: "editor",
11741 expected_with_insert_mode: "before editorˇtor after".into(),
11742 expected_with_replace_mode: "before editorˇ after".into(),
11743 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11744 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11745 },
11746 Run {
11747 run_description: "Prepend text containing whitespace",
11748 initial_state: "pˇfield: bool".into(),
11749 buffer_marked_text: "<p|field>: bool".into(),
11750 completion_label: "pub ",
11751 completion_text: "pub ",
11752 expected_with_insert_mode: "pub ˇfield: bool".into(),
11753 expected_with_replace_mode: "pub ˇ: bool".into(),
11754 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
11755 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
11756 },
11757 Run {
11758 run_description: "Add element to start of list",
11759 initial_state: "[element_ˇelement_2]".into(),
11760 buffer_marked_text: "[<element_|element_2>]".into(),
11761 completion_label: "element_1",
11762 completion_text: "element_1",
11763 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
11764 expected_with_replace_mode: "[element_1ˇ]".into(),
11765 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
11766 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
11767 },
11768 Run {
11769 run_description: "Add element to start of list -- first and second elements are equal",
11770 initial_state: "[elˇelement]".into(),
11771 buffer_marked_text: "[<el|element>]".into(),
11772 completion_label: "element",
11773 completion_text: "element",
11774 expected_with_insert_mode: "[elementˇelement]".into(),
11775 expected_with_replace_mode: "[elementˇ]".into(),
11776 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
11777 expected_with_replace_suffix_mode: "[elementˇ]".into(),
11778 },
11779 Run {
11780 run_description: "Ends with matching suffix",
11781 initial_state: "SubˇError".into(),
11782 buffer_marked_text: "<Sub|Error>".into(),
11783 completion_label: "SubscriptionError",
11784 completion_text: "SubscriptionError",
11785 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
11786 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11787 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11788 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
11789 },
11790 Run {
11791 run_description: "Suffix is a subsequence -- contiguous",
11792 initial_state: "SubˇErr".into(),
11793 buffer_marked_text: "<Sub|Err>".into(),
11794 completion_label: "SubscriptionError",
11795 completion_text: "SubscriptionError",
11796 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
11797 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11798 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11799 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
11800 },
11801 Run {
11802 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
11803 initial_state: "Suˇscrirr".into(),
11804 buffer_marked_text: "<Su|scrirr>".into(),
11805 completion_label: "SubscriptionError",
11806 completion_text: "SubscriptionError",
11807 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
11808 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11809 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11810 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
11811 },
11812 Run {
11813 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
11814 initial_state: "foo(indˇix)".into(),
11815 buffer_marked_text: "foo(<ind|ix>)".into(),
11816 completion_label: "node_index",
11817 completion_text: "node_index",
11818 expected_with_insert_mode: "foo(node_indexˇix)".into(),
11819 expected_with_replace_mode: "foo(node_indexˇ)".into(),
11820 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
11821 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
11822 },
11823 Run {
11824 run_description: "Replace range ends before cursor - should extend to cursor",
11825 initial_state: "before editˇo after".into(),
11826 buffer_marked_text: "before <{ed}>it|o after".into(),
11827 completion_label: "editor",
11828 completion_text: "editor",
11829 expected_with_insert_mode: "before editorˇo after".into(),
11830 expected_with_replace_mode: "before editorˇo after".into(),
11831 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
11832 expected_with_replace_suffix_mode: "before editorˇo after".into(),
11833 },
11834 Run {
11835 run_description: "Uses label for suffix matching",
11836 initial_state: "before ediˇtor after".into(),
11837 buffer_marked_text: "before <edi|tor> after".into(),
11838 completion_label: "editor",
11839 completion_text: "editor()",
11840 expected_with_insert_mode: "before editor()ˇtor after".into(),
11841 expected_with_replace_mode: "before editor()ˇ after".into(),
11842 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
11843 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
11844 },
11845 Run {
11846 run_description: "Case insensitive subsequence and suffix matching",
11847 initial_state: "before EDiˇtoR after".into(),
11848 buffer_marked_text: "before <EDi|toR> after".into(),
11849 completion_label: "editor",
11850 completion_text: "editor",
11851 expected_with_insert_mode: "before editorˇtoR after".into(),
11852 expected_with_replace_mode: "before editorˇ after".into(),
11853 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11854 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11855 },
11856 ];
11857
11858 for run in runs {
11859 let run_variations = [
11860 (LspInsertMode::Insert, run.expected_with_insert_mode),
11861 (LspInsertMode::Replace, run.expected_with_replace_mode),
11862 (
11863 LspInsertMode::ReplaceSubsequence,
11864 run.expected_with_replace_subsequence_mode,
11865 ),
11866 (
11867 LspInsertMode::ReplaceSuffix,
11868 run.expected_with_replace_suffix_mode,
11869 ),
11870 ];
11871
11872 for (lsp_insert_mode, expected_text) in run_variations {
11873 eprintln!(
11874 "run = {:?}, mode = {lsp_insert_mode:.?}",
11875 run.run_description,
11876 );
11877
11878 update_test_language_settings(&mut cx, |settings| {
11879 settings.defaults.completions = Some(CompletionSettings {
11880 lsp_insert_mode,
11881 words: WordsCompletionMode::Disabled,
11882 lsp: true,
11883 lsp_fetch_timeout_ms: 0,
11884 });
11885 });
11886
11887 cx.set_state(&run.initial_state);
11888 cx.update_editor(|editor, window, cx| {
11889 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11890 });
11891
11892 let counter = Arc::new(AtomicUsize::new(0));
11893 handle_completion_request_with_insert_and_replace(
11894 &mut cx,
11895 &run.buffer_marked_text,
11896 vec![(run.completion_label, run.completion_text)],
11897 counter.clone(),
11898 )
11899 .await;
11900 cx.condition(|editor, _| editor.context_menu_visible())
11901 .await;
11902 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11903
11904 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11905 editor
11906 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11907 .unwrap()
11908 });
11909 cx.assert_editor_state(&expected_text);
11910 handle_resolve_completion_request(&mut cx, None).await;
11911 apply_additional_edits.await.unwrap();
11912 }
11913 }
11914}
11915
11916#[gpui::test]
11917async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
11918 init_test(cx, |_| {});
11919 let mut cx = EditorLspTestContext::new_rust(
11920 lsp::ServerCapabilities {
11921 completion_provider: Some(lsp::CompletionOptions {
11922 resolve_provider: Some(true),
11923 ..Default::default()
11924 }),
11925 ..Default::default()
11926 },
11927 cx,
11928 )
11929 .await;
11930
11931 let initial_state = "SubˇError";
11932 let buffer_marked_text = "<Sub|Error>";
11933 let completion_text = "SubscriptionError";
11934 let expected_with_insert_mode = "SubscriptionErrorˇError";
11935 let expected_with_replace_mode = "SubscriptionErrorˇ";
11936
11937 update_test_language_settings(&mut cx, |settings| {
11938 settings.defaults.completions = Some(CompletionSettings {
11939 words: WordsCompletionMode::Disabled,
11940 // set the opposite here to ensure that the action is overriding the default behavior
11941 lsp_insert_mode: LspInsertMode::Insert,
11942 lsp: true,
11943 lsp_fetch_timeout_ms: 0,
11944 });
11945 });
11946
11947 cx.set_state(initial_state);
11948 cx.update_editor(|editor, window, cx| {
11949 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11950 });
11951
11952 let counter = Arc::new(AtomicUsize::new(0));
11953 handle_completion_request_with_insert_and_replace(
11954 &mut cx,
11955 &buffer_marked_text,
11956 vec![(completion_text, completion_text)],
11957 counter.clone(),
11958 )
11959 .await;
11960 cx.condition(|editor, _| editor.context_menu_visible())
11961 .await;
11962 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11963
11964 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11965 editor
11966 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11967 .unwrap()
11968 });
11969 cx.assert_editor_state(&expected_with_replace_mode);
11970 handle_resolve_completion_request(&mut cx, None).await;
11971 apply_additional_edits.await.unwrap();
11972
11973 update_test_language_settings(&mut cx, |settings| {
11974 settings.defaults.completions = Some(CompletionSettings {
11975 words: WordsCompletionMode::Disabled,
11976 // set the opposite here to ensure that the action is overriding the default behavior
11977 lsp_insert_mode: LspInsertMode::Replace,
11978 lsp: true,
11979 lsp_fetch_timeout_ms: 0,
11980 });
11981 });
11982
11983 cx.set_state(initial_state);
11984 cx.update_editor(|editor, window, cx| {
11985 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11986 });
11987 handle_completion_request_with_insert_and_replace(
11988 &mut cx,
11989 &buffer_marked_text,
11990 vec![(completion_text, completion_text)],
11991 counter.clone(),
11992 )
11993 .await;
11994 cx.condition(|editor, _| editor.context_menu_visible())
11995 .await;
11996 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11997
11998 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11999 editor
12000 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
12001 .unwrap()
12002 });
12003 cx.assert_editor_state(&expected_with_insert_mode);
12004 handle_resolve_completion_request(&mut cx, None).await;
12005 apply_additional_edits.await.unwrap();
12006}
12007
12008#[gpui::test]
12009async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
12010 init_test(cx, |_| {});
12011 let mut cx = EditorLspTestContext::new_rust(
12012 lsp::ServerCapabilities {
12013 completion_provider: Some(lsp::CompletionOptions {
12014 resolve_provider: Some(true),
12015 ..Default::default()
12016 }),
12017 ..Default::default()
12018 },
12019 cx,
12020 )
12021 .await;
12022
12023 // scenario: surrounding text matches completion text
12024 let completion_text = "to_offset";
12025 let initial_state = indoc! {"
12026 1. buf.to_offˇsuffix
12027 2. buf.to_offˇsuf
12028 3. buf.to_offˇfix
12029 4. buf.to_offˇ
12030 5. into_offˇensive
12031 6. ˇsuffix
12032 7. let ˇ //
12033 8. aaˇzz
12034 9. buf.to_off«zzzzzˇ»suffix
12035 10. buf.«ˇzzzzz»suffix
12036 11. to_off«ˇzzzzz»
12037
12038 buf.to_offˇsuffix // newest cursor
12039 "};
12040 let completion_marked_buffer = indoc! {"
12041 1. buf.to_offsuffix
12042 2. buf.to_offsuf
12043 3. buf.to_offfix
12044 4. buf.to_off
12045 5. into_offensive
12046 6. suffix
12047 7. let //
12048 8. aazz
12049 9. buf.to_offzzzzzsuffix
12050 10. buf.zzzzzsuffix
12051 11. to_offzzzzz
12052
12053 buf.<to_off|suffix> // newest cursor
12054 "};
12055 let expected = indoc! {"
12056 1. buf.to_offsetˇ
12057 2. buf.to_offsetˇsuf
12058 3. buf.to_offsetˇfix
12059 4. buf.to_offsetˇ
12060 5. into_offsetˇensive
12061 6. to_offsetˇsuffix
12062 7. let to_offsetˇ //
12063 8. aato_offsetˇzz
12064 9. buf.to_offsetˇ
12065 10. buf.to_offsetˇsuffix
12066 11. to_offsetˇ
12067
12068 buf.to_offsetˇ // newest cursor
12069 "};
12070 cx.set_state(initial_state);
12071 cx.update_editor(|editor, window, cx| {
12072 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12073 });
12074 handle_completion_request_with_insert_and_replace(
12075 &mut cx,
12076 completion_marked_buffer,
12077 vec![(completion_text, completion_text)],
12078 Arc::new(AtomicUsize::new(0)),
12079 )
12080 .await;
12081 cx.condition(|editor, _| editor.context_menu_visible())
12082 .await;
12083 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12084 editor
12085 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12086 .unwrap()
12087 });
12088 cx.assert_editor_state(expected);
12089 handle_resolve_completion_request(&mut cx, None).await;
12090 apply_additional_edits.await.unwrap();
12091
12092 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
12093 let completion_text = "foo_and_bar";
12094 let initial_state = indoc! {"
12095 1. ooanbˇ
12096 2. zooanbˇ
12097 3. ooanbˇz
12098 4. zooanbˇz
12099 5. ooanˇ
12100 6. oanbˇ
12101
12102 ooanbˇ
12103 "};
12104 let completion_marked_buffer = indoc! {"
12105 1. ooanb
12106 2. zooanb
12107 3. ooanbz
12108 4. zooanbz
12109 5. ooan
12110 6. oanb
12111
12112 <ooanb|>
12113 "};
12114 let expected = indoc! {"
12115 1. foo_and_barˇ
12116 2. zfoo_and_barˇ
12117 3. foo_and_barˇz
12118 4. zfoo_and_barˇz
12119 5. ooanfoo_and_barˇ
12120 6. oanbfoo_and_barˇ
12121
12122 foo_and_barˇ
12123 "};
12124 cx.set_state(initial_state);
12125 cx.update_editor(|editor, window, cx| {
12126 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12127 });
12128 handle_completion_request_with_insert_and_replace(
12129 &mut cx,
12130 completion_marked_buffer,
12131 vec![(completion_text, completion_text)],
12132 Arc::new(AtomicUsize::new(0)),
12133 )
12134 .await;
12135 cx.condition(|editor, _| editor.context_menu_visible())
12136 .await;
12137 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12138 editor
12139 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12140 .unwrap()
12141 });
12142 cx.assert_editor_state(expected);
12143 handle_resolve_completion_request(&mut cx, None).await;
12144 apply_additional_edits.await.unwrap();
12145
12146 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
12147 // (expects the same as if it was inserted at the end)
12148 let completion_text = "foo_and_bar";
12149 let initial_state = indoc! {"
12150 1. ooˇanb
12151 2. zooˇanb
12152 3. ooˇanbz
12153 4. zooˇanbz
12154
12155 ooˇanb
12156 "};
12157 let completion_marked_buffer = indoc! {"
12158 1. ooanb
12159 2. zooanb
12160 3. ooanbz
12161 4. zooanbz
12162
12163 <oo|anb>
12164 "};
12165 let expected = indoc! {"
12166 1. foo_and_barˇ
12167 2. zfoo_and_barˇ
12168 3. foo_and_barˇz
12169 4. zfoo_and_barˇz
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
12196// This used to crash
12197#[gpui::test]
12198async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
12199 init_test(cx, |_| {});
12200
12201 let buffer_text = indoc! {"
12202 fn main() {
12203 10.satu;
12204
12205 //
12206 // separate cursors so they open in different excerpts (manually reproducible)
12207 //
12208
12209 10.satu20;
12210 }
12211 "};
12212 let multibuffer_text_with_selections = indoc! {"
12213 fn main() {
12214 10.satuˇ;
12215
12216 //
12217
12218 //
12219
12220 10.satuˇ20;
12221 }
12222 "};
12223 let expected_multibuffer = indoc! {"
12224 fn main() {
12225 10.saturating_sub()ˇ;
12226
12227 //
12228
12229 //
12230
12231 10.saturating_sub()ˇ;
12232 }
12233 "};
12234
12235 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
12236 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
12237
12238 let fs = FakeFs::new(cx.executor());
12239 fs.insert_tree(
12240 path!("/a"),
12241 json!({
12242 "main.rs": buffer_text,
12243 }),
12244 )
12245 .await;
12246
12247 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12248 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12249 language_registry.add(rust_lang());
12250 let mut fake_servers = language_registry.register_fake_lsp(
12251 "Rust",
12252 FakeLspAdapter {
12253 capabilities: lsp::ServerCapabilities {
12254 completion_provider: Some(lsp::CompletionOptions {
12255 resolve_provider: None,
12256 ..lsp::CompletionOptions::default()
12257 }),
12258 ..lsp::ServerCapabilities::default()
12259 },
12260 ..FakeLspAdapter::default()
12261 },
12262 );
12263 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12264 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12265 let buffer = project
12266 .update(cx, |project, cx| {
12267 project.open_local_buffer(path!("/a/main.rs"), cx)
12268 })
12269 .await
12270 .unwrap();
12271
12272 let multi_buffer = cx.new(|cx| {
12273 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
12274 multi_buffer.push_excerpts(
12275 buffer.clone(),
12276 [ExcerptRange::new(0..first_excerpt_end)],
12277 cx,
12278 );
12279 multi_buffer.push_excerpts(
12280 buffer.clone(),
12281 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
12282 cx,
12283 );
12284 multi_buffer
12285 });
12286
12287 let editor = workspace
12288 .update(cx, |_, window, cx| {
12289 cx.new(|cx| {
12290 Editor::new(
12291 EditorMode::Full {
12292 scale_ui_elements_with_buffer_font_size: false,
12293 show_active_line_background: false,
12294 sized_by_content: false,
12295 },
12296 multi_buffer.clone(),
12297 Some(project.clone()),
12298 window,
12299 cx,
12300 )
12301 })
12302 })
12303 .unwrap();
12304
12305 let pane = workspace
12306 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12307 .unwrap();
12308 pane.update_in(cx, |pane, window, cx| {
12309 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
12310 });
12311
12312 let fake_server = fake_servers.next().await.unwrap();
12313
12314 editor.update_in(cx, |editor, window, cx| {
12315 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12316 s.select_ranges([
12317 Point::new(1, 11)..Point::new(1, 11),
12318 Point::new(7, 11)..Point::new(7, 11),
12319 ])
12320 });
12321
12322 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
12323 });
12324
12325 editor.update_in(cx, |editor, window, cx| {
12326 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12327 });
12328
12329 fake_server
12330 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12331 let completion_item = lsp::CompletionItem {
12332 label: "saturating_sub()".into(),
12333 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
12334 lsp::InsertReplaceEdit {
12335 new_text: "saturating_sub()".to_owned(),
12336 insert: lsp::Range::new(
12337 lsp::Position::new(7, 7),
12338 lsp::Position::new(7, 11),
12339 ),
12340 replace: lsp::Range::new(
12341 lsp::Position::new(7, 7),
12342 lsp::Position::new(7, 13),
12343 ),
12344 },
12345 )),
12346 ..lsp::CompletionItem::default()
12347 };
12348
12349 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
12350 })
12351 .next()
12352 .await
12353 .unwrap();
12354
12355 cx.condition(&editor, |editor, _| editor.context_menu_visible())
12356 .await;
12357
12358 editor
12359 .update_in(cx, |editor, window, cx| {
12360 editor
12361 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12362 .unwrap()
12363 })
12364 .await
12365 .unwrap();
12366
12367 editor.update(cx, |editor, cx| {
12368 assert_text_with_selections(editor, expected_multibuffer, cx);
12369 })
12370}
12371
12372#[gpui::test]
12373async fn test_completion(cx: &mut TestAppContext) {
12374 init_test(cx, |_| {});
12375
12376 let mut cx = EditorLspTestContext::new_rust(
12377 lsp::ServerCapabilities {
12378 completion_provider: Some(lsp::CompletionOptions {
12379 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12380 resolve_provider: Some(true),
12381 ..Default::default()
12382 }),
12383 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12384 ..Default::default()
12385 },
12386 cx,
12387 )
12388 .await;
12389 let counter = Arc::new(AtomicUsize::new(0));
12390
12391 cx.set_state(indoc! {"
12392 oneˇ
12393 two
12394 three
12395 "});
12396 cx.simulate_keystroke(".");
12397 handle_completion_request(
12398 indoc! {"
12399 one.|<>
12400 two
12401 three
12402 "},
12403 vec!["first_completion", "second_completion"],
12404 true,
12405 counter.clone(),
12406 &mut cx,
12407 )
12408 .await;
12409 cx.condition(|editor, _| editor.context_menu_visible())
12410 .await;
12411 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12412
12413 let _handler = handle_signature_help_request(
12414 &mut cx,
12415 lsp::SignatureHelp {
12416 signatures: vec![lsp::SignatureInformation {
12417 label: "test signature".to_string(),
12418 documentation: None,
12419 parameters: Some(vec![lsp::ParameterInformation {
12420 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
12421 documentation: None,
12422 }]),
12423 active_parameter: None,
12424 }],
12425 active_signature: None,
12426 active_parameter: None,
12427 },
12428 );
12429 cx.update_editor(|editor, window, cx| {
12430 assert!(
12431 !editor.signature_help_state.is_shown(),
12432 "No signature help was called for"
12433 );
12434 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12435 });
12436 cx.run_until_parked();
12437 cx.update_editor(|editor, _, _| {
12438 assert!(
12439 !editor.signature_help_state.is_shown(),
12440 "No signature help should be shown when completions menu is open"
12441 );
12442 });
12443
12444 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12445 editor.context_menu_next(&Default::default(), window, cx);
12446 editor
12447 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12448 .unwrap()
12449 });
12450 cx.assert_editor_state(indoc! {"
12451 one.second_completionˇ
12452 two
12453 three
12454 "});
12455
12456 handle_resolve_completion_request(
12457 &mut cx,
12458 Some(vec![
12459 (
12460 //This overlaps with the primary completion edit which is
12461 //misbehavior from the LSP spec, test that we filter it out
12462 indoc! {"
12463 one.second_ˇcompletion
12464 two
12465 threeˇ
12466 "},
12467 "overlapping additional edit",
12468 ),
12469 (
12470 indoc! {"
12471 one.second_completion
12472 two
12473 threeˇ
12474 "},
12475 "\nadditional edit",
12476 ),
12477 ]),
12478 )
12479 .await;
12480 apply_additional_edits.await.unwrap();
12481 cx.assert_editor_state(indoc! {"
12482 one.second_completionˇ
12483 two
12484 three
12485 additional edit
12486 "});
12487
12488 cx.set_state(indoc! {"
12489 one.second_completion
12490 twoˇ
12491 threeˇ
12492 additional edit
12493 "});
12494 cx.simulate_keystroke(" ");
12495 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12496 cx.simulate_keystroke("s");
12497 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12498
12499 cx.assert_editor_state(indoc! {"
12500 one.second_completion
12501 two sˇ
12502 three sˇ
12503 additional edit
12504 "});
12505 handle_completion_request(
12506 indoc! {"
12507 one.second_completion
12508 two s
12509 three <s|>
12510 additional edit
12511 "},
12512 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12513 true,
12514 counter.clone(),
12515 &mut cx,
12516 )
12517 .await;
12518 cx.condition(|editor, _| editor.context_menu_visible())
12519 .await;
12520 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12521
12522 cx.simulate_keystroke("i");
12523
12524 handle_completion_request(
12525 indoc! {"
12526 one.second_completion
12527 two si
12528 three <si|>
12529 additional edit
12530 "},
12531 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12532 true,
12533 counter.clone(),
12534 &mut cx,
12535 )
12536 .await;
12537 cx.condition(|editor, _| editor.context_menu_visible())
12538 .await;
12539 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12540
12541 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12542 editor
12543 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12544 .unwrap()
12545 });
12546 cx.assert_editor_state(indoc! {"
12547 one.second_completion
12548 two sixth_completionˇ
12549 three sixth_completionˇ
12550 additional edit
12551 "});
12552
12553 apply_additional_edits.await.unwrap();
12554
12555 update_test_language_settings(&mut cx, |settings| {
12556 settings.defaults.show_completions_on_input = Some(false);
12557 });
12558 cx.set_state("editorˇ");
12559 cx.simulate_keystroke(".");
12560 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12561 cx.simulate_keystrokes("c l o");
12562 cx.assert_editor_state("editor.cloˇ");
12563 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12564 cx.update_editor(|editor, window, cx| {
12565 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12566 });
12567 handle_completion_request(
12568 "editor.<clo|>",
12569 vec!["close", "clobber"],
12570 true,
12571 counter.clone(),
12572 &mut cx,
12573 )
12574 .await;
12575 cx.condition(|editor, _| editor.context_menu_visible())
12576 .await;
12577 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
12578
12579 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12580 editor
12581 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12582 .unwrap()
12583 });
12584 cx.assert_editor_state("editor.clobberˇ");
12585 handle_resolve_completion_request(&mut cx, None).await;
12586 apply_additional_edits.await.unwrap();
12587}
12588
12589#[gpui::test]
12590async fn test_completion_reuse(cx: &mut TestAppContext) {
12591 init_test(cx, |_| {});
12592
12593 let mut cx = EditorLspTestContext::new_rust(
12594 lsp::ServerCapabilities {
12595 completion_provider: Some(lsp::CompletionOptions {
12596 trigger_characters: Some(vec![".".to_string()]),
12597 ..Default::default()
12598 }),
12599 ..Default::default()
12600 },
12601 cx,
12602 )
12603 .await;
12604
12605 let counter = Arc::new(AtomicUsize::new(0));
12606 cx.set_state("objˇ");
12607 cx.simulate_keystroke(".");
12608
12609 // Initial completion request returns complete results
12610 let is_incomplete = false;
12611 handle_completion_request(
12612 "obj.|<>",
12613 vec!["a", "ab", "abc"],
12614 is_incomplete,
12615 counter.clone(),
12616 &mut cx,
12617 )
12618 .await;
12619 cx.run_until_parked();
12620 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12621 cx.assert_editor_state("obj.ˇ");
12622 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12623
12624 // Type "a" - filters existing completions
12625 cx.simulate_keystroke("a");
12626 cx.run_until_parked();
12627 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12628 cx.assert_editor_state("obj.aˇ");
12629 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12630
12631 // Type "b" - filters existing completions
12632 cx.simulate_keystroke("b");
12633 cx.run_until_parked();
12634 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12635 cx.assert_editor_state("obj.abˇ");
12636 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12637
12638 // Type "c" - filters existing completions
12639 cx.simulate_keystroke("c");
12640 cx.run_until_parked();
12641 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12642 cx.assert_editor_state("obj.abcˇ");
12643 check_displayed_completions(vec!["abc"], &mut cx);
12644
12645 // Backspace to delete "c" - filters existing completions
12646 cx.update_editor(|editor, window, cx| {
12647 editor.backspace(&Backspace, window, cx);
12648 });
12649 cx.run_until_parked();
12650 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12651 cx.assert_editor_state("obj.abˇ");
12652 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12653
12654 // Moving cursor to the left dismisses menu.
12655 cx.update_editor(|editor, window, cx| {
12656 editor.move_left(&MoveLeft, window, cx);
12657 });
12658 cx.run_until_parked();
12659 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12660 cx.assert_editor_state("obj.aˇb");
12661 cx.update_editor(|editor, _, _| {
12662 assert_eq!(editor.context_menu_visible(), false);
12663 });
12664
12665 // Type "b" - new request
12666 cx.simulate_keystroke("b");
12667 let is_incomplete = false;
12668 handle_completion_request(
12669 "obj.<ab|>a",
12670 vec!["ab", "abc"],
12671 is_incomplete,
12672 counter.clone(),
12673 &mut cx,
12674 )
12675 .await;
12676 cx.run_until_parked();
12677 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12678 cx.assert_editor_state("obj.abˇb");
12679 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12680
12681 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
12682 cx.update_editor(|editor, window, cx| {
12683 editor.backspace(&Backspace, window, cx);
12684 });
12685 let is_incomplete = false;
12686 handle_completion_request(
12687 "obj.<a|>b",
12688 vec!["a", "ab", "abc"],
12689 is_incomplete,
12690 counter.clone(),
12691 &mut cx,
12692 )
12693 .await;
12694 cx.run_until_parked();
12695 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12696 cx.assert_editor_state("obj.aˇb");
12697 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12698
12699 // Backspace to delete "a" - dismisses menu.
12700 cx.update_editor(|editor, window, cx| {
12701 editor.backspace(&Backspace, window, cx);
12702 });
12703 cx.run_until_parked();
12704 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12705 cx.assert_editor_state("obj.ˇb");
12706 cx.update_editor(|editor, _, _| {
12707 assert_eq!(editor.context_menu_visible(), false);
12708 });
12709}
12710
12711#[gpui::test]
12712async fn test_word_completion(cx: &mut TestAppContext) {
12713 let lsp_fetch_timeout_ms = 10;
12714 init_test(cx, |language_settings| {
12715 language_settings.defaults.completions = Some(CompletionSettings {
12716 words: WordsCompletionMode::Fallback,
12717 lsp: true,
12718 lsp_fetch_timeout_ms: 10,
12719 lsp_insert_mode: LspInsertMode::Insert,
12720 });
12721 });
12722
12723 let mut cx = EditorLspTestContext::new_rust(
12724 lsp::ServerCapabilities {
12725 completion_provider: Some(lsp::CompletionOptions {
12726 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12727 ..lsp::CompletionOptions::default()
12728 }),
12729 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12730 ..lsp::ServerCapabilities::default()
12731 },
12732 cx,
12733 )
12734 .await;
12735
12736 let throttle_completions = Arc::new(AtomicBool::new(false));
12737
12738 let lsp_throttle_completions = throttle_completions.clone();
12739 let _completion_requests_handler =
12740 cx.lsp
12741 .server
12742 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
12743 let lsp_throttle_completions = lsp_throttle_completions.clone();
12744 let cx = cx.clone();
12745 async move {
12746 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
12747 cx.background_executor()
12748 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
12749 .await;
12750 }
12751 Ok(Some(lsp::CompletionResponse::Array(vec![
12752 lsp::CompletionItem {
12753 label: "first".into(),
12754 ..lsp::CompletionItem::default()
12755 },
12756 lsp::CompletionItem {
12757 label: "last".into(),
12758 ..lsp::CompletionItem::default()
12759 },
12760 ])))
12761 }
12762 });
12763
12764 cx.set_state(indoc! {"
12765 oneˇ
12766 two
12767 three
12768 "});
12769 cx.simulate_keystroke(".");
12770 cx.executor().run_until_parked();
12771 cx.condition(|editor, _| editor.context_menu_visible())
12772 .await;
12773 cx.update_editor(|editor, window, cx| {
12774 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12775 {
12776 assert_eq!(
12777 completion_menu_entries(&menu),
12778 &["first", "last"],
12779 "When LSP server is fast to reply, no fallback word completions are used"
12780 );
12781 } else {
12782 panic!("expected completion menu to be open");
12783 }
12784 editor.cancel(&Cancel, window, cx);
12785 });
12786 cx.executor().run_until_parked();
12787 cx.condition(|editor, _| !editor.context_menu_visible())
12788 .await;
12789
12790 throttle_completions.store(true, atomic::Ordering::Release);
12791 cx.simulate_keystroke(".");
12792 cx.executor()
12793 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
12794 cx.executor().run_until_parked();
12795 cx.condition(|editor, _| editor.context_menu_visible())
12796 .await;
12797 cx.update_editor(|editor, _, _| {
12798 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12799 {
12800 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
12801 "When LSP server is slow, document words can be shown instead, if configured accordingly");
12802 } else {
12803 panic!("expected completion menu to be open");
12804 }
12805 });
12806}
12807
12808#[gpui::test]
12809async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
12810 init_test(cx, |language_settings| {
12811 language_settings.defaults.completions = Some(CompletionSettings {
12812 words: WordsCompletionMode::Enabled,
12813 lsp: true,
12814 lsp_fetch_timeout_ms: 0,
12815 lsp_insert_mode: LspInsertMode::Insert,
12816 });
12817 });
12818
12819 let mut cx = EditorLspTestContext::new_rust(
12820 lsp::ServerCapabilities {
12821 completion_provider: Some(lsp::CompletionOptions {
12822 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12823 ..lsp::CompletionOptions::default()
12824 }),
12825 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12826 ..lsp::ServerCapabilities::default()
12827 },
12828 cx,
12829 )
12830 .await;
12831
12832 let _completion_requests_handler =
12833 cx.lsp
12834 .server
12835 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12836 Ok(Some(lsp::CompletionResponse::Array(vec![
12837 lsp::CompletionItem {
12838 label: "first".into(),
12839 ..lsp::CompletionItem::default()
12840 },
12841 lsp::CompletionItem {
12842 label: "last".into(),
12843 ..lsp::CompletionItem::default()
12844 },
12845 ])))
12846 });
12847
12848 cx.set_state(indoc! {"ˇ
12849 first
12850 last
12851 second
12852 "});
12853 cx.simulate_keystroke(".");
12854 cx.executor().run_until_parked();
12855 cx.condition(|editor, _| editor.context_menu_visible())
12856 .await;
12857 cx.update_editor(|editor, _, _| {
12858 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12859 {
12860 assert_eq!(
12861 completion_menu_entries(&menu),
12862 &["first", "last", "second"],
12863 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
12864 );
12865 } else {
12866 panic!("expected completion menu to be open");
12867 }
12868 });
12869}
12870
12871#[gpui::test]
12872async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
12873 init_test(cx, |language_settings| {
12874 language_settings.defaults.completions = Some(CompletionSettings {
12875 words: WordsCompletionMode::Disabled,
12876 lsp: true,
12877 lsp_fetch_timeout_ms: 0,
12878 lsp_insert_mode: LspInsertMode::Insert,
12879 });
12880 });
12881
12882 let mut cx = EditorLspTestContext::new_rust(
12883 lsp::ServerCapabilities {
12884 completion_provider: Some(lsp::CompletionOptions {
12885 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12886 ..lsp::CompletionOptions::default()
12887 }),
12888 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12889 ..lsp::ServerCapabilities::default()
12890 },
12891 cx,
12892 )
12893 .await;
12894
12895 let _completion_requests_handler =
12896 cx.lsp
12897 .server
12898 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12899 panic!("LSP completions should not be queried when dealing with word completions")
12900 });
12901
12902 cx.set_state(indoc! {"ˇ
12903 first
12904 last
12905 second
12906 "});
12907 cx.update_editor(|editor, window, cx| {
12908 editor.show_word_completions(&ShowWordCompletions, window, cx);
12909 });
12910 cx.executor().run_until_parked();
12911 cx.condition(|editor, _| editor.context_menu_visible())
12912 .await;
12913 cx.update_editor(|editor, _, _| {
12914 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12915 {
12916 assert_eq!(
12917 completion_menu_entries(&menu),
12918 &["first", "last", "second"],
12919 "`ShowWordCompletions` action should show word completions"
12920 );
12921 } else {
12922 panic!("expected completion menu to be open");
12923 }
12924 });
12925
12926 cx.simulate_keystroke("l");
12927 cx.executor().run_until_parked();
12928 cx.condition(|editor, _| editor.context_menu_visible())
12929 .await;
12930 cx.update_editor(|editor, _, _| {
12931 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12932 {
12933 assert_eq!(
12934 completion_menu_entries(&menu),
12935 &["last"],
12936 "After showing word completions, further editing should filter them and not query the LSP"
12937 );
12938 } else {
12939 panic!("expected completion menu to be open");
12940 }
12941 });
12942}
12943
12944#[gpui::test]
12945async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
12946 init_test(cx, |language_settings| {
12947 language_settings.defaults.completions = Some(CompletionSettings {
12948 words: WordsCompletionMode::Fallback,
12949 lsp: false,
12950 lsp_fetch_timeout_ms: 0,
12951 lsp_insert_mode: LspInsertMode::Insert,
12952 });
12953 });
12954
12955 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12956
12957 cx.set_state(indoc! {"ˇ
12958 0_usize
12959 let
12960 33
12961 4.5f32
12962 "});
12963 cx.update_editor(|editor, window, cx| {
12964 editor.show_completions(&ShowCompletions::default(), window, cx);
12965 });
12966 cx.executor().run_until_parked();
12967 cx.condition(|editor, _| editor.context_menu_visible())
12968 .await;
12969 cx.update_editor(|editor, window, cx| {
12970 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12971 {
12972 assert_eq!(
12973 completion_menu_entries(&menu),
12974 &["let"],
12975 "With no digits in the completion query, no digits should be in the word completions"
12976 );
12977 } else {
12978 panic!("expected completion menu to be open");
12979 }
12980 editor.cancel(&Cancel, window, cx);
12981 });
12982
12983 cx.set_state(indoc! {"3ˇ
12984 0_usize
12985 let
12986 3
12987 33.35f32
12988 "});
12989 cx.update_editor(|editor, window, cx| {
12990 editor.show_completions(&ShowCompletions::default(), window, cx);
12991 });
12992 cx.executor().run_until_parked();
12993 cx.condition(|editor, _| editor.context_menu_visible())
12994 .await;
12995 cx.update_editor(|editor, _, _| {
12996 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12997 {
12998 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
12999 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
13000 } else {
13001 panic!("expected completion menu to be open");
13002 }
13003 });
13004}
13005
13006fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
13007 let position = || lsp::Position {
13008 line: params.text_document_position.position.line,
13009 character: params.text_document_position.position.character,
13010 };
13011 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13012 range: lsp::Range {
13013 start: position(),
13014 end: position(),
13015 },
13016 new_text: text.to_string(),
13017 }))
13018}
13019
13020#[gpui::test]
13021async fn test_multiline_completion(cx: &mut TestAppContext) {
13022 init_test(cx, |_| {});
13023
13024 let fs = FakeFs::new(cx.executor());
13025 fs.insert_tree(
13026 path!("/a"),
13027 json!({
13028 "main.ts": "a",
13029 }),
13030 )
13031 .await;
13032
13033 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13034 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13035 let typescript_language = Arc::new(Language::new(
13036 LanguageConfig {
13037 name: "TypeScript".into(),
13038 matcher: LanguageMatcher {
13039 path_suffixes: vec!["ts".to_string()],
13040 ..LanguageMatcher::default()
13041 },
13042 line_comments: vec!["// ".into()],
13043 ..LanguageConfig::default()
13044 },
13045 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13046 ));
13047 language_registry.add(typescript_language.clone());
13048 let mut fake_servers = language_registry.register_fake_lsp(
13049 "TypeScript",
13050 FakeLspAdapter {
13051 capabilities: lsp::ServerCapabilities {
13052 completion_provider: Some(lsp::CompletionOptions {
13053 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13054 ..lsp::CompletionOptions::default()
13055 }),
13056 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13057 ..lsp::ServerCapabilities::default()
13058 },
13059 // Emulate vtsls label generation
13060 label_for_completion: Some(Box::new(|item, _| {
13061 let text = if let Some(description) = item
13062 .label_details
13063 .as_ref()
13064 .and_then(|label_details| label_details.description.as_ref())
13065 {
13066 format!("{} {}", item.label, description)
13067 } else if let Some(detail) = &item.detail {
13068 format!("{} {}", item.label, detail)
13069 } else {
13070 item.label.clone()
13071 };
13072 let len = text.len();
13073 Some(language::CodeLabel {
13074 text,
13075 runs: Vec::new(),
13076 filter_range: 0..len,
13077 })
13078 })),
13079 ..FakeLspAdapter::default()
13080 },
13081 );
13082 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13083 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13084 let worktree_id = workspace
13085 .update(cx, |workspace, _window, cx| {
13086 workspace.project().update(cx, |project, cx| {
13087 project.worktrees(cx).next().unwrap().read(cx).id()
13088 })
13089 })
13090 .unwrap();
13091 let _buffer = project
13092 .update(cx, |project, cx| {
13093 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
13094 })
13095 .await
13096 .unwrap();
13097 let editor = workspace
13098 .update(cx, |workspace, window, cx| {
13099 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
13100 })
13101 .unwrap()
13102 .await
13103 .unwrap()
13104 .downcast::<Editor>()
13105 .unwrap();
13106 let fake_server = fake_servers.next().await.unwrap();
13107
13108 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
13109 let multiline_label_2 = "a\nb\nc\n";
13110 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
13111 let multiline_description = "d\ne\nf\n";
13112 let multiline_detail_2 = "g\nh\ni\n";
13113
13114 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
13115 move |params, _| async move {
13116 Ok(Some(lsp::CompletionResponse::Array(vec![
13117 lsp::CompletionItem {
13118 label: multiline_label.to_string(),
13119 text_edit: gen_text_edit(¶ms, "new_text_1"),
13120 ..lsp::CompletionItem::default()
13121 },
13122 lsp::CompletionItem {
13123 label: "single line label 1".to_string(),
13124 detail: Some(multiline_detail.to_string()),
13125 text_edit: gen_text_edit(¶ms, "new_text_2"),
13126 ..lsp::CompletionItem::default()
13127 },
13128 lsp::CompletionItem {
13129 label: "single line label 2".to_string(),
13130 label_details: Some(lsp::CompletionItemLabelDetails {
13131 description: Some(multiline_description.to_string()),
13132 detail: None,
13133 }),
13134 text_edit: gen_text_edit(¶ms, "new_text_2"),
13135 ..lsp::CompletionItem::default()
13136 },
13137 lsp::CompletionItem {
13138 label: multiline_label_2.to_string(),
13139 detail: Some(multiline_detail_2.to_string()),
13140 text_edit: gen_text_edit(¶ms, "new_text_3"),
13141 ..lsp::CompletionItem::default()
13142 },
13143 lsp::CompletionItem {
13144 label: "Label with many spaces and \t but without newlines".to_string(),
13145 detail: Some(
13146 "Details with many spaces and \t but without newlines".to_string(),
13147 ),
13148 text_edit: gen_text_edit(¶ms, "new_text_4"),
13149 ..lsp::CompletionItem::default()
13150 },
13151 ])))
13152 },
13153 );
13154
13155 editor.update_in(cx, |editor, window, cx| {
13156 cx.focus_self(window);
13157 editor.move_to_end(&MoveToEnd, window, cx);
13158 editor.handle_input(".", window, cx);
13159 });
13160 cx.run_until_parked();
13161 completion_handle.next().await.unwrap();
13162
13163 editor.update(cx, |editor, _| {
13164 assert!(editor.context_menu_visible());
13165 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13166 {
13167 let completion_labels = menu
13168 .completions
13169 .borrow()
13170 .iter()
13171 .map(|c| c.label.text.clone())
13172 .collect::<Vec<_>>();
13173 assert_eq!(
13174 completion_labels,
13175 &[
13176 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
13177 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
13178 "single line label 2 d e f ",
13179 "a b c g h i ",
13180 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
13181 ],
13182 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
13183 );
13184
13185 for completion in menu
13186 .completions
13187 .borrow()
13188 .iter() {
13189 assert_eq!(
13190 completion.label.filter_range,
13191 0..completion.label.text.len(),
13192 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
13193 );
13194 }
13195 } else {
13196 panic!("expected completion menu to be open");
13197 }
13198 });
13199}
13200
13201#[gpui::test]
13202async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
13203 init_test(cx, |_| {});
13204 let mut cx = EditorLspTestContext::new_rust(
13205 lsp::ServerCapabilities {
13206 completion_provider: Some(lsp::CompletionOptions {
13207 trigger_characters: Some(vec![".".to_string()]),
13208 ..Default::default()
13209 }),
13210 ..Default::default()
13211 },
13212 cx,
13213 )
13214 .await;
13215 cx.lsp
13216 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13217 Ok(Some(lsp::CompletionResponse::Array(vec![
13218 lsp::CompletionItem {
13219 label: "first".into(),
13220 ..Default::default()
13221 },
13222 lsp::CompletionItem {
13223 label: "last".into(),
13224 ..Default::default()
13225 },
13226 ])))
13227 });
13228 cx.set_state("variableˇ");
13229 cx.simulate_keystroke(".");
13230 cx.executor().run_until_parked();
13231
13232 cx.update_editor(|editor, _, _| {
13233 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13234 {
13235 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
13236 } else {
13237 panic!("expected completion menu to be open");
13238 }
13239 });
13240
13241 cx.update_editor(|editor, window, cx| {
13242 editor.move_page_down(&MovePageDown::default(), window, cx);
13243 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13244 {
13245 assert!(
13246 menu.selected_item == 1,
13247 "expected PageDown to select the last item from the context menu"
13248 );
13249 } else {
13250 panic!("expected completion menu to stay open after PageDown");
13251 }
13252 });
13253
13254 cx.update_editor(|editor, window, cx| {
13255 editor.move_page_up(&MovePageUp::default(), window, cx);
13256 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13257 {
13258 assert!(
13259 menu.selected_item == 0,
13260 "expected PageUp to select the first item from the context menu"
13261 );
13262 } else {
13263 panic!("expected completion menu to stay open after PageUp");
13264 }
13265 });
13266}
13267
13268#[gpui::test]
13269async fn test_as_is_completions(cx: &mut TestAppContext) {
13270 init_test(cx, |_| {});
13271 let mut cx = EditorLspTestContext::new_rust(
13272 lsp::ServerCapabilities {
13273 completion_provider: Some(lsp::CompletionOptions {
13274 ..Default::default()
13275 }),
13276 ..Default::default()
13277 },
13278 cx,
13279 )
13280 .await;
13281 cx.lsp
13282 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13283 Ok(Some(lsp::CompletionResponse::Array(vec![
13284 lsp::CompletionItem {
13285 label: "unsafe".into(),
13286 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13287 range: lsp::Range {
13288 start: lsp::Position {
13289 line: 1,
13290 character: 2,
13291 },
13292 end: lsp::Position {
13293 line: 1,
13294 character: 3,
13295 },
13296 },
13297 new_text: "unsafe".to_string(),
13298 })),
13299 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
13300 ..Default::default()
13301 },
13302 ])))
13303 });
13304 cx.set_state("fn a() {}\n nˇ");
13305 cx.executor().run_until_parked();
13306 cx.update_editor(|editor, window, cx| {
13307 editor.show_completions(
13308 &ShowCompletions {
13309 trigger: Some("\n".into()),
13310 },
13311 window,
13312 cx,
13313 );
13314 });
13315 cx.executor().run_until_parked();
13316
13317 cx.update_editor(|editor, window, cx| {
13318 editor.confirm_completion(&Default::default(), window, cx)
13319 });
13320 cx.executor().run_until_parked();
13321 cx.assert_editor_state("fn a() {}\n unsafeˇ");
13322}
13323
13324#[gpui::test]
13325async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
13326 init_test(cx, |_| {});
13327
13328 let mut cx = EditorLspTestContext::new_rust(
13329 lsp::ServerCapabilities {
13330 completion_provider: Some(lsp::CompletionOptions {
13331 trigger_characters: Some(vec![".".to_string()]),
13332 resolve_provider: Some(true),
13333 ..Default::default()
13334 }),
13335 ..Default::default()
13336 },
13337 cx,
13338 )
13339 .await;
13340
13341 cx.set_state("fn main() { let a = 2ˇ; }");
13342 cx.simulate_keystroke(".");
13343 let completion_item = lsp::CompletionItem {
13344 label: "Some".into(),
13345 kind: Some(lsp::CompletionItemKind::SNIPPET),
13346 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13347 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13348 kind: lsp::MarkupKind::Markdown,
13349 value: "```rust\nSome(2)\n```".to_string(),
13350 })),
13351 deprecated: Some(false),
13352 sort_text: Some("Some".to_string()),
13353 filter_text: Some("Some".to_string()),
13354 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13355 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13356 range: lsp::Range {
13357 start: lsp::Position {
13358 line: 0,
13359 character: 22,
13360 },
13361 end: lsp::Position {
13362 line: 0,
13363 character: 22,
13364 },
13365 },
13366 new_text: "Some(2)".to_string(),
13367 })),
13368 additional_text_edits: Some(vec![lsp::TextEdit {
13369 range: lsp::Range {
13370 start: lsp::Position {
13371 line: 0,
13372 character: 20,
13373 },
13374 end: lsp::Position {
13375 line: 0,
13376 character: 22,
13377 },
13378 },
13379 new_text: "".to_string(),
13380 }]),
13381 ..Default::default()
13382 };
13383
13384 let closure_completion_item = completion_item.clone();
13385 let counter = Arc::new(AtomicUsize::new(0));
13386 let counter_clone = counter.clone();
13387 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13388 let task_completion_item = closure_completion_item.clone();
13389 counter_clone.fetch_add(1, atomic::Ordering::Release);
13390 async move {
13391 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13392 is_incomplete: true,
13393 item_defaults: None,
13394 items: vec![task_completion_item],
13395 })))
13396 }
13397 });
13398
13399 cx.condition(|editor, _| editor.context_menu_visible())
13400 .await;
13401 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
13402 assert!(request.next().await.is_some());
13403 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13404
13405 cx.simulate_keystrokes("S o m");
13406 cx.condition(|editor, _| editor.context_menu_visible())
13407 .await;
13408 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
13409 assert!(request.next().await.is_some());
13410 assert!(request.next().await.is_some());
13411 assert!(request.next().await.is_some());
13412 request.close();
13413 assert!(request.next().await.is_none());
13414 assert_eq!(
13415 counter.load(atomic::Ordering::Acquire),
13416 4,
13417 "With the completions menu open, only one LSP request should happen per input"
13418 );
13419}
13420
13421#[gpui::test]
13422async fn test_toggle_comment(cx: &mut TestAppContext) {
13423 init_test(cx, |_| {});
13424 let mut cx = EditorTestContext::new(cx).await;
13425 let language = Arc::new(Language::new(
13426 LanguageConfig {
13427 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13428 ..Default::default()
13429 },
13430 Some(tree_sitter_rust::LANGUAGE.into()),
13431 ));
13432 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13433
13434 // If multiple selections intersect a line, the line is only toggled once.
13435 cx.set_state(indoc! {"
13436 fn a() {
13437 «//b();
13438 ˇ»// «c();
13439 //ˇ» d();
13440 }
13441 "});
13442
13443 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13444
13445 cx.assert_editor_state(indoc! {"
13446 fn a() {
13447 «b();
13448 c();
13449 ˇ» d();
13450 }
13451 "});
13452
13453 // The comment prefix is inserted at the same column for every line in a
13454 // selection.
13455 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13456
13457 cx.assert_editor_state(indoc! {"
13458 fn a() {
13459 // «b();
13460 // c();
13461 ˇ»// d();
13462 }
13463 "});
13464
13465 // If a selection ends at the beginning of a line, that line is not toggled.
13466 cx.set_selections_state(indoc! {"
13467 fn a() {
13468 // b();
13469 «// c();
13470 ˇ» // d();
13471 }
13472 "});
13473
13474 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13475
13476 cx.assert_editor_state(indoc! {"
13477 fn a() {
13478 // b();
13479 «c();
13480 ˇ» // d();
13481 }
13482 "});
13483
13484 // If a selection span a single line and is empty, the line is toggled.
13485 cx.set_state(indoc! {"
13486 fn a() {
13487 a();
13488 b();
13489 ˇ
13490 }
13491 "});
13492
13493 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13494
13495 cx.assert_editor_state(indoc! {"
13496 fn a() {
13497 a();
13498 b();
13499 //•ˇ
13500 }
13501 "});
13502
13503 // If a selection span multiple lines, empty lines are not toggled.
13504 cx.set_state(indoc! {"
13505 fn a() {
13506 «a();
13507
13508 c();ˇ»
13509 }
13510 "});
13511
13512 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13513
13514 cx.assert_editor_state(indoc! {"
13515 fn a() {
13516 // «a();
13517
13518 // c();ˇ»
13519 }
13520 "});
13521
13522 // If a selection includes multiple comment prefixes, all lines are uncommented.
13523 cx.set_state(indoc! {"
13524 fn a() {
13525 «// a();
13526 /// b();
13527 //! c();ˇ»
13528 }
13529 "});
13530
13531 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13532
13533 cx.assert_editor_state(indoc! {"
13534 fn a() {
13535 «a();
13536 b();
13537 c();ˇ»
13538 }
13539 "});
13540}
13541
13542#[gpui::test]
13543async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
13544 init_test(cx, |_| {});
13545 let mut cx = EditorTestContext::new(cx).await;
13546 let language = Arc::new(Language::new(
13547 LanguageConfig {
13548 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13549 ..Default::default()
13550 },
13551 Some(tree_sitter_rust::LANGUAGE.into()),
13552 ));
13553 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13554
13555 let toggle_comments = &ToggleComments {
13556 advance_downwards: false,
13557 ignore_indent: true,
13558 };
13559
13560 // If multiple selections intersect a line, the line is only toggled once.
13561 cx.set_state(indoc! {"
13562 fn a() {
13563 // «b();
13564 // c();
13565 // ˇ» d();
13566 }
13567 "});
13568
13569 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13570
13571 cx.assert_editor_state(indoc! {"
13572 fn a() {
13573 «b();
13574 c();
13575 ˇ» d();
13576 }
13577 "});
13578
13579 // The comment prefix is inserted at the beginning of each line
13580 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13581
13582 cx.assert_editor_state(indoc! {"
13583 fn a() {
13584 // «b();
13585 // c();
13586 // ˇ» d();
13587 }
13588 "});
13589
13590 // If a selection ends at the beginning of a line, that line is not toggled.
13591 cx.set_selections_state(indoc! {"
13592 fn a() {
13593 // b();
13594 // «c();
13595 ˇ»// d();
13596 }
13597 "});
13598
13599 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13600
13601 cx.assert_editor_state(indoc! {"
13602 fn a() {
13603 // b();
13604 «c();
13605 ˇ»// d();
13606 }
13607 "});
13608
13609 // If a selection span a single line and is empty, the line is toggled.
13610 cx.set_state(indoc! {"
13611 fn a() {
13612 a();
13613 b();
13614 ˇ
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 a();
13623 b();
13624 //ˇ
13625 }
13626 "});
13627
13628 // If a selection span multiple lines, empty lines are not toggled.
13629 cx.set_state(indoc! {"
13630 fn a() {
13631 «a();
13632
13633 c();ˇ»
13634 }
13635 "});
13636
13637 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13638
13639 cx.assert_editor_state(indoc! {"
13640 fn a() {
13641 // «a();
13642
13643 // c();ˇ»
13644 }
13645 "});
13646
13647 // If a selection includes multiple comment prefixes, all lines are uncommented.
13648 cx.set_state(indoc! {"
13649 fn a() {
13650 // «a();
13651 /// b();
13652 //! c();ˇ»
13653 }
13654 "});
13655
13656 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13657
13658 cx.assert_editor_state(indoc! {"
13659 fn a() {
13660 «a();
13661 b();
13662 c();ˇ»
13663 }
13664 "});
13665}
13666
13667#[gpui::test]
13668async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
13669 init_test(cx, |_| {});
13670
13671 let language = Arc::new(Language::new(
13672 LanguageConfig {
13673 line_comments: vec!["// ".into()],
13674 ..Default::default()
13675 },
13676 Some(tree_sitter_rust::LANGUAGE.into()),
13677 ));
13678
13679 let mut cx = EditorTestContext::new(cx).await;
13680
13681 cx.language_registry().add(language.clone());
13682 cx.update_buffer(|buffer, cx| {
13683 buffer.set_language(Some(language), cx);
13684 });
13685
13686 let toggle_comments = &ToggleComments {
13687 advance_downwards: true,
13688 ignore_indent: false,
13689 };
13690
13691 // Single cursor on one line -> advance
13692 // Cursor moves horizontally 3 characters as well on non-blank line
13693 cx.set_state(indoc!(
13694 "fn a() {
13695 ˇdog();
13696 cat();
13697 }"
13698 ));
13699 cx.update_editor(|editor, window, cx| {
13700 editor.toggle_comments(toggle_comments, window, cx);
13701 });
13702 cx.assert_editor_state(indoc!(
13703 "fn a() {
13704 // dog();
13705 catˇ();
13706 }"
13707 ));
13708
13709 // Single selection on one line -> don't advance
13710 cx.set_state(indoc!(
13711 "fn a() {
13712 «dog()ˇ»;
13713 cat();
13714 }"
13715 ));
13716 cx.update_editor(|editor, window, cx| {
13717 editor.toggle_comments(toggle_comments, window, cx);
13718 });
13719 cx.assert_editor_state(indoc!(
13720 "fn a() {
13721 // «dog()ˇ»;
13722 cat();
13723 }"
13724 ));
13725
13726 // Multiple cursors on one line -> advance
13727 cx.set_state(indoc!(
13728 "fn a() {
13729 ˇdˇog();
13730 cat();
13731 }"
13732 ));
13733 cx.update_editor(|editor, window, cx| {
13734 editor.toggle_comments(toggle_comments, window, cx);
13735 });
13736 cx.assert_editor_state(indoc!(
13737 "fn a() {
13738 // dog();
13739 catˇ(ˇ);
13740 }"
13741 ));
13742
13743 // Multiple cursors on one line, with selection -> don't advance
13744 cx.set_state(indoc!(
13745 "fn a() {
13746 ˇdˇog«()ˇ»;
13747 cat();
13748 }"
13749 ));
13750 cx.update_editor(|editor, window, cx| {
13751 editor.toggle_comments(toggle_comments, window, cx);
13752 });
13753 cx.assert_editor_state(indoc!(
13754 "fn a() {
13755 // ˇdˇog«()ˇ»;
13756 cat();
13757 }"
13758 ));
13759
13760 // Single cursor on one line -> advance
13761 // Cursor moves to column 0 on blank line
13762 cx.set_state(indoc!(
13763 "fn a() {
13764 ˇdog();
13765
13766 cat();
13767 }"
13768 ));
13769 cx.update_editor(|editor, window, cx| {
13770 editor.toggle_comments(toggle_comments, window, cx);
13771 });
13772 cx.assert_editor_state(indoc!(
13773 "fn a() {
13774 // dog();
13775 ˇ
13776 cat();
13777 }"
13778 ));
13779
13780 // Single cursor on one line -> advance
13781 // Cursor starts and ends at column 0
13782 cx.set_state(indoc!(
13783 "fn a() {
13784 ˇ dog();
13785 cat();
13786 }"
13787 ));
13788 cx.update_editor(|editor, window, cx| {
13789 editor.toggle_comments(toggle_comments, window, cx);
13790 });
13791 cx.assert_editor_state(indoc!(
13792 "fn a() {
13793 // dog();
13794 ˇ cat();
13795 }"
13796 ));
13797}
13798
13799#[gpui::test]
13800async fn test_toggle_block_comment(cx: &mut TestAppContext) {
13801 init_test(cx, |_| {});
13802
13803 let mut cx = EditorTestContext::new(cx).await;
13804
13805 let html_language = Arc::new(
13806 Language::new(
13807 LanguageConfig {
13808 name: "HTML".into(),
13809 block_comment: Some(("<!-- ".into(), " -->".into())),
13810 ..Default::default()
13811 },
13812 Some(tree_sitter_html::LANGUAGE.into()),
13813 )
13814 .with_injection_query(
13815 r#"
13816 (script_element
13817 (raw_text) @injection.content
13818 (#set! injection.language "javascript"))
13819 "#,
13820 )
13821 .unwrap(),
13822 );
13823
13824 let javascript_language = Arc::new(Language::new(
13825 LanguageConfig {
13826 name: "JavaScript".into(),
13827 line_comments: vec!["// ".into()],
13828 ..Default::default()
13829 },
13830 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13831 ));
13832
13833 cx.language_registry().add(html_language.clone());
13834 cx.language_registry().add(javascript_language.clone());
13835 cx.update_buffer(|buffer, cx| {
13836 buffer.set_language(Some(html_language), cx);
13837 });
13838
13839 // Toggle comments for empty selections
13840 cx.set_state(
13841 &r#"
13842 <p>A</p>ˇ
13843 <p>B</p>ˇ
13844 <p>C</p>ˇ
13845 "#
13846 .unindent(),
13847 );
13848 cx.update_editor(|editor, window, cx| {
13849 editor.toggle_comments(&ToggleComments::default(), window, cx)
13850 });
13851 cx.assert_editor_state(
13852 &r#"
13853 <!-- <p>A</p>ˇ -->
13854 <!-- <p>B</p>ˇ -->
13855 <!-- <p>C</p>ˇ -->
13856 "#
13857 .unindent(),
13858 );
13859 cx.update_editor(|editor, window, cx| {
13860 editor.toggle_comments(&ToggleComments::default(), window, cx)
13861 });
13862 cx.assert_editor_state(
13863 &r#"
13864 <p>A</p>ˇ
13865 <p>B</p>ˇ
13866 <p>C</p>ˇ
13867 "#
13868 .unindent(),
13869 );
13870
13871 // Toggle comments for mixture of empty and non-empty selections, where
13872 // multiple selections occupy a given line.
13873 cx.set_state(
13874 &r#"
13875 <p>A«</p>
13876 <p>ˇ»B</p>ˇ
13877 <p>C«</p>
13878 <p>ˇ»D</p>ˇ
13879 "#
13880 .unindent(),
13881 );
13882
13883 cx.update_editor(|editor, window, cx| {
13884 editor.toggle_comments(&ToggleComments::default(), window, cx)
13885 });
13886 cx.assert_editor_state(
13887 &r#"
13888 <!-- <p>A«</p>
13889 <p>ˇ»B</p>ˇ -->
13890 <!-- <p>C«</p>
13891 <p>ˇ»D</p>ˇ -->
13892 "#
13893 .unindent(),
13894 );
13895 cx.update_editor(|editor, window, cx| {
13896 editor.toggle_comments(&ToggleComments::default(), window, cx)
13897 });
13898 cx.assert_editor_state(
13899 &r#"
13900 <p>A«</p>
13901 <p>ˇ»B</p>ˇ
13902 <p>C«</p>
13903 <p>ˇ»D</p>ˇ
13904 "#
13905 .unindent(),
13906 );
13907
13908 // Toggle comments when different languages are active for different
13909 // selections.
13910 cx.set_state(
13911 &r#"
13912 ˇ<script>
13913 ˇvar x = new Y();
13914 ˇ</script>
13915 "#
13916 .unindent(),
13917 );
13918 cx.executor().run_until_parked();
13919 cx.update_editor(|editor, window, cx| {
13920 editor.toggle_comments(&ToggleComments::default(), window, cx)
13921 });
13922 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
13923 // Uncommenting and commenting from this position brings in even more wrong artifacts.
13924 cx.assert_editor_state(
13925 &r#"
13926 <!-- ˇ<script> -->
13927 // ˇvar x = new Y();
13928 <!-- ˇ</script> -->
13929 "#
13930 .unindent(),
13931 );
13932}
13933
13934#[gpui::test]
13935fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
13936 init_test(cx, |_| {});
13937
13938 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13939 let multibuffer = cx.new(|cx| {
13940 let mut multibuffer = MultiBuffer::new(ReadWrite);
13941 multibuffer.push_excerpts(
13942 buffer.clone(),
13943 [
13944 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
13945 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
13946 ],
13947 cx,
13948 );
13949 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
13950 multibuffer
13951 });
13952
13953 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13954 editor.update_in(cx, |editor, window, cx| {
13955 assert_eq!(editor.text(cx), "aaaa\nbbbb");
13956 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13957 s.select_ranges([
13958 Point::new(0, 0)..Point::new(0, 0),
13959 Point::new(1, 0)..Point::new(1, 0),
13960 ])
13961 });
13962
13963 editor.handle_input("X", window, cx);
13964 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
13965 assert_eq!(
13966 editor.selections.ranges(cx),
13967 [
13968 Point::new(0, 1)..Point::new(0, 1),
13969 Point::new(1, 1)..Point::new(1, 1),
13970 ]
13971 );
13972
13973 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
13974 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13975 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
13976 });
13977 editor.backspace(&Default::default(), window, cx);
13978 assert_eq!(editor.text(cx), "Xa\nbbb");
13979 assert_eq!(
13980 editor.selections.ranges(cx),
13981 [Point::new(1, 0)..Point::new(1, 0)]
13982 );
13983
13984 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13985 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
13986 });
13987 editor.backspace(&Default::default(), window, cx);
13988 assert_eq!(editor.text(cx), "X\nbb");
13989 assert_eq!(
13990 editor.selections.ranges(cx),
13991 [Point::new(0, 1)..Point::new(0, 1)]
13992 );
13993 });
13994}
13995
13996#[gpui::test]
13997fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
13998 init_test(cx, |_| {});
13999
14000 let markers = vec![('[', ']').into(), ('(', ')').into()];
14001 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
14002 indoc! {"
14003 [aaaa
14004 (bbbb]
14005 cccc)",
14006 },
14007 markers.clone(),
14008 );
14009 let excerpt_ranges = markers.into_iter().map(|marker| {
14010 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
14011 ExcerptRange::new(context.clone())
14012 });
14013 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
14014 let multibuffer = cx.new(|cx| {
14015 let mut multibuffer = MultiBuffer::new(ReadWrite);
14016 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
14017 multibuffer
14018 });
14019
14020 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
14021 editor.update_in(cx, |editor, window, cx| {
14022 let (expected_text, selection_ranges) = marked_text_ranges(
14023 indoc! {"
14024 aaaa
14025 bˇbbb
14026 bˇbbˇb
14027 cccc"
14028 },
14029 true,
14030 );
14031 assert_eq!(editor.text(cx), expected_text);
14032 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14033 s.select_ranges(selection_ranges)
14034 });
14035
14036 editor.handle_input("X", window, cx);
14037
14038 let (expected_text, expected_selections) = marked_text_ranges(
14039 indoc! {"
14040 aaaa
14041 bXˇbbXb
14042 bXˇbbXˇb
14043 cccc"
14044 },
14045 false,
14046 );
14047 assert_eq!(editor.text(cx), expected_text);
14048 assert_eq!(editor.selections.ranges(cx), expected_selections);
14049
14050 editor.newline(&Newline, window, cx);
14051 let (expected_text, expected_selections) = marked_text_ranges(
14052 indoc! {"
14053 aaaa
14054 bX
14055 ˇbbX
14056 b
14057 bX
14058 ˇbbX
14059 ˇb
14060 cccc"
14061 },
14062 false,
14063 );
14064 assert_eq!(editor.text(cx), expected_text);
14065 assert_eq!(editor.selections.ranges(cx), expected_selections);
14066 });
14067}
14068
14069#[gpui::test]
14070fn test_refresh_selections(cx: &mut TestAppContext) {
14071 init_test(cx, |_| {});
14072
14073 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14074 let mut excerpt1_id = None;
14075 let multibuffer = cx.new(|cx| {
14076 let mut multibuffer = MultiBuffer::new(ReadWrite);
14077 excerpt1_id = multibuffer
14078 .push_excerpts(
14079 buffer.clone(),
14080 [
14081 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14082 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14083 ],
14084 cx,
14085 )
14086 .into_iter()
14087 .next();
14088 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14089 multibuffer
14090 });
14091
14092 let editor = cx.add_window(|window, cx| {
14093 let mut editor = build_editor(multibuffer.clone(), window, cx);
14094 let snapshot = editor.snapshot(window, cx);
14095 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14096 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
14097 });
14098 editor.begin_selection(
14099 Point::new(2, 1).to_display_point(&snapshot),
14100 true,
14101 1,
14102 window,
14103 cx,
14104 );
14105 assert_eq!(
14106 editor.selections.ranges(cx),
14107 [
14108 Point::new(1, 3)..Point::new(1, 3),
14109 Point::new(2, 1)..Point::new(2, 1),
14110 ]
14111 );
14112 editor
14113 });
14114
14115 // Refreshing selections is a no-op when excerpts haven't changed.
14116 _ = editor.update(cx, |editor, window, cx| {
14117 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14118 assert_eq!(
14119 editor.selections.ranges(cx),
14120 [
14121 Point::new(1, 3)..Point::new(1, 3),
14122 Point::new(2, 1)..Point::new(2, 1),
14123 ]
14124 );
14125 });
14126
14127 multibuffer.update(cx, |multibuffer, cx| {
14128 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14129 });
14130 _ = editor.update(cx, |editor, window, cx| {
14131 // Removing an excerpt causes the first selection to become degenerate.
14132 assert_eq!(
14133 editor.selections.ranges(cx),
14134 [
14135 Point::new(0, 0)..Point::new(0, 0),
14136 Point::new(0, 1)..Point::new(0, 1)
14137 ]
14138 );
14139
14140 // Refreshing selections will relocate the first selection to the original buffer
14141 // location.
14142 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14143 assert_eq!(
14144 editor.selections.ranges(cx),
14145 [
14146 Point::new(0, 1)..Point::new(0, 1),
14147 Point::new(0, 3)..Point::new(0, 3)
14148 ]
14149 );
14150 assert!(editor.selections.pending_anchor().is_some());
14151 });
14152}
14153
14154#[gpui::test]
14155fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
14156 init_test(cx, |_| {});
14157
14158 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14159 let mut excerpt1_id = None;
14160 let multibuffer = cx.new(|cx| {
14161 let mut multibuffer = MultiBuffer::new(ReadWrite);
14162 excerpt1_id = multibuffer
14163 .push_excerpts(
14164 buffer.clone(),
14165 [
14166 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14167 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14168 ],
14169 cx,
14170 )
14171 .into_iter()
14172 .next();
14173 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14174 multibuffer
14175 });
14176
14177 let editor = cx.add_window(|window, cx| {
14178 let mut editor = build_editor(multibuffer.clone(), window, cx);
14179 let snapshot = editor.snapshot(window, cx);
14180 editor.begin_selection(
14181 Point::new(1, 3).to_display_point(&snapshot),
14182 false,
14183 1,
14184 window,
14185 cx,
14186 );
14187 assert_eq!(
14188 editor.selections.ranges(cx),
14189 [Point::new(1, 3)..Point::new(1, 3)]
14190 );
14191 editor
14192 });
14193
14194 multibuffer.update(cx, |multibuffer, cx| {
14195 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14196 });
14197 _ = editor.update(cx, |editor, window, cx| {
14198 assert_eq!(
14199 editor.selections.ranges(cx),
14200 [Point::new(0, 0)..Point::new(0, 0)]
14201 );
14202
14203 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
14204 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14205 assert_eq!(
14206 editor.selections.ranges(cx),
14207 [Point::new(0, 3)..Point::new(0, 3)]
14208 );
14209 assert!(editor.selections.pending_anchor().is_some());
14210 });
14211}
14212
14213#[gpui::test]
14214async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
14215 init_test(cx, |_| {});
14216
14217 let language = Arc::new(
14218 Language::new(
14219 LanguageConfig {
14220 brackets: BracketPairConfig {
14221 pairs: vec![
14222 BracketPair {
14223 start: "{".to_string(),
14224 end: "}".to_string(),
14225 close: true,
14226 surround: true,
14227 newline: true,
14228 },
14229 BracketPair {
14230 start: "/* ".to_string(),
14231 end: " */".to_string(),
14232 close: true,
14233 surround: true,
14234 newline: true,
14235 },
14236 ],
14237 ..Default::default()
14238 },
14239 ..Default::default()
14240 },
14241 Some(tree_sitter_rust::LANGUAGE.into()),
14242 )
14243 .with_indents_query("")
14244 .unwrap(),
14245 );
14246
14247 let text = concat!(
14248 "{ }\n", //
14249 " x\n", //
14250 " /* */\n", //
14251 "x\n", //
14252 "{{} }\n", //
14253 );
14254
14255 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
14256 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14257 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14258 editor
14259 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
14260 .await;
14261
14262 editor.update_in(cx, |editor, window, cx| {
14263 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14264 s.select_display_ranges([
14265 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
14266 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
14267 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
14268 ])
14269 });
14270 editor.newline(&Newline, window, cx);
14271
14272 assert_eq!(
14273 editor.buffer().read(cx).read(cx).text(),
14274 concat!(
14275 "{ \n", // Suppress rustfmt
14276 "\n", //
14277 "}\n", //
14278 " x\n", //
14279 " /* \n", //
14280 " \n", //
14281 " */\n", //
14282 "x\n", //
14283 "{{} \n", //
14284 "}\n", //
14285 )
14286 );
14287 });
14288}
14289
14290#[gpui::test]
14291fn test_highlighted_ranges(cx: &mut TestAppContext) {
14292 init_test(cx, |_| {});
14293
14294 let editor = cx.add_window(|window, cx| {
14295 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
14296 build_editor(buffer.clone(), window, cx)
14297 });
14298
14299 _ = editor.update(cx, |editor, window, cx| {
14300 struct Type1;
14301 struct Type2;
14302
14303 let buffer = editor.buffer.read(cx).snapshot(cx);
14304
14305 let anchor_range =
14306 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
14307
14308 editor.highlight_background::<Type1>(
14309 &[
14310 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
14311 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
14312 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
14313 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
14314 ],
14315 |_| Hsla::red(),
14316 cx,
14317 );
14318 editor.highlight_background::<Type2>(
14319 &[
14320 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
14321 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
14322 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
14323 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
14324 ],
14325 |_| Hsla::green(),
14326 cx,
14327 );
14328
14329 let snapshot = editor.snapshot(window, cx);
14330 let mut highlighted_ranges = editor.background_highlights_in_range(
14331 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
14332 &snapshot,
14333 cx.theme(),
14334 );
14335 // Enforce a consistent ordering based on color without relying on the ordering of the
14336 // highlight's `TypeId` which is non-executor.
14337 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
14338 assert_eq!(
14339 highlighted_ranges,
14340 &[
14341 (
14342 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
14343 Hsla::red(),
14344 ),
14345 (
14346 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14347 Hsla::red(),
14348 ),
14349 (
14350 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
14351 Hsla::green(),
14352 ),
14353 (
14354 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
14355 Hsla::green(),
14356 ),
14357 ]
14358 );
14359 assert_eq!(
14360 editor.background_highlights_in_range(
14361 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
14362 &snapshot,
14363 cx.theme(),
14364 ),
14365 &[(
14366 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14367 Hsla::red(),
14368 )]
14369 );
14370 });
14371}
14372
14373#[gpui::test]
14374async fn test_following(cx: &mut TestAppContext) {
14375 init_test(cx, |_| {});
14376
14377 let fs = FakeFs::new(cx.executor());
14378 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14379
14380 let buffer = project.update(cx, |project, cx| {
14381 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
14382 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
14383 });
14384 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
14385 let follower = cx.update(|cx| {
14386 cx.open_window(
14387 WindowOptions {
14388 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
14389 gpui::Point::new(px(0.), px(0.)),
14390 gpui::Point::new(px(10.), px(80.)),
14391 ))),
14392 ..Default::default()
14393 },
14394 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
14395 )
14396 .unwrap()
14397 });
14398
14399 let is_still_following = Rc::new(RefCell::new(true));
14400 let follower_edit_event_count = Rc::new(RefCell::new(0));
14401 let pending_update = Rc::new(RefCell::new(None));
14402 let leader_entity = leader.root(cx).unwrap();
14403 let follower_entity = follower.root(cx).unwrap();
14404 _ = follower.update(cx, {
14405 let update = pending_update.clone();
14406 let is_still_following = is_still_following.clone();
14407 let follower_edit_event_count = follower_edit_event_count.clone();
14408 |_, window, cx| {
14409 cx.subscribe_in(
14410 &leader_entity,
14411 window,
14412 move |_, leader, event, window, cx| {
14413 leader.read(cx).add_event_to_update_proto(
14414 event,
14415 &mut update.borrow_mut(),
14416 window,
14417 cx,
14418 );
14419 },
14420 )
14421 .detach();
14422
14423 cx.subscribe_in(
14424 &follower_entity,
14425 window,
14426 move |_, _, event: &EditorEvent, _window, _cx| {
14427 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
14428 *is_still_following.borrow_mut() = false;
14429 }
14430
14431 if let EditorEvent::BufferEdited = event {
14432 *follower_edit_event_count.borrow_mut() += 1;
14433 }
14434 },
14435 )
14436 .detach();
14437 }
14438 });
14439
14440 // Update the selections only
14441 _ = leader.update(cx, |leader, window, cx| {
14442 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14443 s.select_ranges([1..1])
14444 });
14445 });
14446 follower
14447 .update(cx, |follower, window, cx| {
14448 follower.apply_update_proto(
14449 &project,
14450 pending_update.borrow_mut().take().unwrap(),
14451 window,
14452 cx,
14453 )
14454 })
14455 .unwrap()
14456 .await
14457 .unwrap();
14458 _ = follower.update(cx, |follower, _, cx| {
14459 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
14460 });
14461 assert!(*is_still_following.borrow());
14462 assert_eq!(*follower_edit_event_count.borrow(), 0);
14463
14464 // Update the scroll position only
14465 _ = leader.update(cx, |leader, window, cx| {
14466 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14467 });
14468 follower
14469 .update(cx, |follower, window, cx| {
14470 follower.apply_update_proto(
14471 &project,
14472 pending_update.borrow_mut().take().unwrap(),
14473 window,
14474 cx,
14475 )
14476 })
14477 .unwrap()
14478 .await
14479 .unwrap();
14480 assert_eq!(
14481 follower
14482 .update(cx, |follower, _, cx| follower.scroll_position(cx))
14483 .unwrap(),
14484 gpui::Point::new(1.5, 3.5)
14485 );
14486 assert!(*is_still_following.borrow());
14487 assert_eq!(*follower_edit_event_count.borrow(), 0);
14488
14489 // Update the selections and scroll position. The follower's scroll position is updated
14490 // via autoscroll, not via the leader's exact scroll position.
14491 _ = leader.update(cx, |leader, window, cx| {
14492 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14493 s.select_ranges([0..0])
14494 });
14495 leader.request_autoscroll(Autoscroll::newest(), cx);
14496 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14497 });
14498 follower
14499 .update(cx, |follower, window, cx| {
14500 follower.apply_update_proto(
14501 &project,
14502 pending_update.borrow_mut().take().unwrap(),
14503 window,
14504 cx,
14505 )
14506 })
14507 .unwrap()
14508 .await
14509 .unwrap();
14510 _ = follower.update(cx, |follower, _, cx| {
14511 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
14512 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
14513 });
14514 assert!(*is_still_following.borrow());
14515
14516 // Creating a pending selection that precedes another selection
14517 _ = leader.update(cx, |leader, window, cx| {
14518 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14519 s.select_ranges([1..1])
14520 });
14521 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
14522 });
14523 follower
14524 .update(cx, |follower, window, cx| {
14525 follower.apply_update_proto(
14526 &project,
14527 pending_update.borrow_mut().take().unwrap(),
14528 window,
14529 cx,
14530 )
14531 })
14532 .unwrap()
14533 .await
14534 .unwrap();
14535 _ = follower.update(cx, |follower, _, cx| {
14536 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
14537 });
14538 assert!(*is_still_following.borrow());
14539
14540 // Extend the pending selection so that it surrounds another selection
14541 _ = leader.update(cx, |leader, window, cx| {
14542 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
14543 });
14544 follower
14545 .update(cx, |follower, window, cx| {
14546 follower.apply_update_proto(
14547 &project,
14548 pending_update.borrow_mut().take().unwrap(),
14549 window,
14550 cx,
14551 )
14552 })
14553 .unwrap()
14554 .await
14555 .unwrap();
14556 _ = follower.update(cx, |follower, _, cx| {
14557 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
14558 });
14559
14560 // Scrolling locally breaks the follow
14561 _ = follower.update(cx, |follower, window, cx| {
14562 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
14563 follower.set_scroll_anchor(
14564 ScrollAnchor {
14565 anchor: top_anchor,
14566 offset: gpui::Point::new(0.0, 0.5),
14567 },
14568 window,
14569 cx,
14570 );
14571 });
14572 assert!(!(*is_still_following.borrow()));
14573}
14574
14575#[gpui::test]
14576async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
14577 init_test(cx, |_| {});
14578
14579 let fs = FakeFs::new(cx.executor());
14580 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14581 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14582 let pane = workspace
14583 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14584 .unwrap();
14585
14586 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14587
14588 let leader = pane.update_in(cx, |_, window, cx| {
14589 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
14590 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
14591 });
14592
14593 // Start following the editor when it has no excerpts.
14594 let mut state_message =
14595 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14596 let workspace_entity = workspace.root(cx).unwrap();
14597 let follower_1 = cx
14598 .update_window(*workspace.deref(), |_, window, cx| {
14599 Editor::from_state_proto(
14600 workspace_entity,
14601 ViewId {
14602 creator: CollaboratorId::PeerId(PeerId::default()),
14603 id: 0,
14604 },
14605 &mut state_message,
14606 window,
14607 cx,
14608 )
14609 })
14610 .unwrap()
14611 .unwrap()
14612 .await
14613 .unwrap();
14614
14615 let update_message = Rc::new(RefCell::new(None));
14616 follower_1.update_in(cx, {
14617 let update = update_message.clone();
14618 |_, window, cx| {
14619 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
14620 leader.read(cx).add_event_to_update_proto(
14621 event,
14622 &mut update.borrow_mut(),
14623 window,
14624 cx,
14625 );
14626 })
14627 .detach();
14628 }
14629 });
14630
14631 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
14632 (
14633 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
14634 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
14635 )
14636 });
14637
14638 // Insert some excerpts.
14639 leader.update(cx, |leader, cx| {
14640 leader.buffer.update(cx, |multibuffer, cx| {
14641 multibuffer.set_excerpts_for_path(
14642 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
14643 buffer_1.clone(),
14644 vec![
14645 Point::row_range(0..3),
14646 Point::row_range(1..6),
14647 Point::row_range(12..15),
14648 ],
14649 0,
14650 cx,
14651 );
14652 multibuffer.set_excerpts_for_path(
14653 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
14654 buffer_2.clone(),
14655 vec![Point::row_range(0..6), Point::row_range(8..12)],
14656 0,
14657 cx,
14658 );
14659 });
14660 });
14661
14662 // Apply the update of adding the excerpts.
14663 follower_1
14664 .update_in(cx, |follower, window, cx| {
14665 follower.apply_update_proto(
14666 &project,
14667 update_message.borrow().clone().unwrap(),
14668 window,
14669 cx,
14670 )
14671 })
14672 .await
14673 .unwrap();
14674 assert_eq!(
14675 follower_1.update(cx, |editor, cx| editor.text(cx)),
14676 leader.update(cx, |editor, cx| editor.text(cx))
14677 );
14678 update_message.borrow_mut().take();
14679
14680 // Start following separately after it already has excerpts.
14681 let mut state_message =
14682 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14683 let workspace_entity = workspace.root(cx).unwrap();
14684 let follower_2 = cx
14685 .update_window(*workspace.deref(), |_, window, cx| {
14686 Editor::from_state_proto(
14687 workspace_entity,
14688 ViewId {
14689 creator: CollaboratorId::PeerId(PeerId::default()),
14690 id: 0,
14691 },
14692 &mut state_message,
14693 window,
14694 cx,
14695 )
14696 })
14697 .unwrap()
14698 .unwrap()
14699 .await
14700 .unwrap();
14701 assert_eq!(
14702 follower_2.update(cx, |editor, cx| editor.text(cx)),
14703 leader.update(cx, |editor, cx| editor.text(cx))
14704 );
14705
14706 // Remove some excerpts.
14707 leader.update(cx, |leader, cx| {
14708 leader.buffer.update(cx, |multibuffer, cx| {
14709 let excerpt_ids = multibuffer.excerpt_ids();
14710 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
14711 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
14712 });
14713 });
14714
14715 // Apply the update of removing the excerpts.
14716 follower_1
14717 .update_in(cx, |follower, window, cx| {
14718 follower.apply_update_proto(
14719 &project,
14720 update_message.borrow().clone().unwrap(),
14721 window,
14722 cx,
14723 )
14724 })
14725 .await
14726 .unwrap();
14727 follower_2
14728 .update_in(cx, |follower, window, cx| {
14729 follower.apply_update_proto(
14730 &project,
14731 update_message.borrow().clone().unwrap(),
14732 window,
14733 cx,
14734 )
14735 })
14736 .await
14737 .unwrap();
14738 update_message.borrow_mut().take();
14739 assert_eq!(
14740 follower_1.update(cx, |editor, cx| editor.text(cx)),
14741 leader.update(cx, |editor, cx| editor.text(cx))
14742 );
14743}
14744
14745#[gpui::test]
14746async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14747 init_test(cx, |_| {});
14748
14749 let mut cx = EditorTestContext::new(cx).await;
14750 let lsp_store =
14751 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
14752
14753 cx.set_state(indoc! {"
14754 ˇfn func(abc def: i32) -> u32 {
14755 }
14756 "});
14757
14758 cx.update(|_, cx| {
14759 lsp_store.update(cx, |lsp_store, cx| {
14760 lsp_store
14761 .update_diagnostics(
14762 LanguageServerId(0),
14763 lsp::PublishDiagnosticsParams {
14764 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
14765 version: None,
14766 diagnostics: vec![
14767 lsp::Diagnostic {
14768 range: lsp::Range::new(
14769 lsp::Position::new(0, 11),
14770 lsp::Position::new(0, 12),
14771 ),
14772 severity: Some(lsp::DiagnosticSeverity::ERROR),
14773 ..Default::default()
14774 },
14775 lsp::Diagnostic {
14776 range: lsp::Range::new(
14777 lsp::Position::new(0, 12),
14778 lsp::Position::new(0, 15),
14779 ),
14780 severity: Some(lsp::DiagnosticSeverity::ERROR),
14781 ..Default::default()
14782 },
14783 lsp::Diagnostic {
14784 range: lsp::Range::new(
14785 lsp::Position::new(0, 25),
14786 lsp::Position::new(0, 28),
14787 ),
14788 severity: Some(lsp::DiagnosticSeverity::ERROR),
14789 ..Default::default()
14790 },
14791 ],
14792 },
14793 None,
14794 DiagnosticSourceKind::Pushed,
14795 &[],
14796 cx,
14797 )
14798 .unwrap()
14799 });
14800 });
14801
14802 executor.run_until_parked();
14803
14804 cx.update_editor(|editor, window, cx| {
14805 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14806 });
14807
14808 cx.assert_editor_state(indoc! {"
14809 fn func(abc def: i32) -> ˇu32 {
14810 }
14811 "});
14812
14813 cx.update_editor(|editor, window, cx| {
14814 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14815 });
14816
14817 cx.assert_editor_state(indoc! {"
14818 fn func(abc ˇdef: i32) -> u32 {
14819 }
14820 "});
14821
14822 cx.update_editor(|editor, window, cx| {
14823 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14824 });
14825
14826 cx.assert_editor_state(indoc! {"
14827 fn func(abcˇ def: i32) -> u32 {
14828 }
14829 "});
14830
14831 cx.update_editor(|editor, window, cx| {
14832 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14833 });
14834
14835 cx.assert_editor_state(indoc! {"
14836 fn func(abc def: i32) -> ˇu32 {
14837 }
14838 "});
14839}
14840
14841#[gpui::test]
14842async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14843 init_test(cx, |_| {});
14844
14845 let mut cx = EditorTestContext::new(cx).await;
14846
14847 let diff_base = r#"
14848 use some::mod;
14849
14850 const A: u32 = 42;
14851
14852 fn main() {
14853 println!("hello");
14854
14855 println!("world");
14856 }
14857 "#
14858 .unindent();
14859
14860 // Edits are modified, removed, modified, added
14861 cx.set_state(
14862 &r#"
14863 use some::modified;
14864
14865 ˇ
14866 fn main() {
14867 println!("hello there");
14868
14869 println!("around the");
14870 println!("world");
14871 }
14872 "#
14873 .unindent(),
14874 );
14875
14876 cx.set_head_text(&diff_base);
14877 executor.run_until_parked();
14878
14879 cx.update_editor(|editor, window, cx| {
14880 //Wrap around the bottom of the buffer
14881 for _ in 0..3 {
14882 editor.go_to_next_hunk(&GoToHunk, window, cx);
14883 }
14884 });
14885
14886 cx.assert_editor_state(
14887 &r#"
14888 ˇuse some::modified;
14889
14890
14891 fn main() {
14892 println!("hello there");
14893
14894 println!("around the");
14895 println!("world");
14896 }
14897 "#
14898 .unindent(),
14899 );
14900
14901 cx.update_editor(|editor, window, cx| {
14902 //Wrap around the top of the buffer
14903 for _ in 0..2 {
14904 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14905 }
14906 });
14907
14908 cx.assert_editor_state(
14909 &r#"
14910 use some::modified;
14911
14912
14913 fn main() {
14914 ˇ println!("hello there");
14915
14916 println!("around the");
14917 println!("world");
14918 }
14919 "#
14920 .unindent(),
14921 );
14922
14923 cx.update_editor(|editor, window, cx| {
14924 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14925 });
14926
14927 cx.assert_editor_state(
14928 &r#"
14929 use some::modified;
14930
14931 ˇ
14932 fn main() {
14933 println!("hello there");
14934
14935 println!("around the");
14936 println!("world");
14937 }
14938 "#
14939 .unindent(),
14940 );
14941
14942 cx.update_editor(|editor, window, cx| {
14943 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14944 });
14945
14946 cx.assert_editor_state(
14947 &r#"
14948 ˇuse some::modified;
14949
14950
14951 fn main() {
14952 println!("hello there");
14953
14954 println!("around the");
14955 println!("world");
14956 }
14957 "#
14958 .unindent(),
14959 );
14960
14961 cx.update_editor(|editor, window, cx| {
14962 for _ in 0..2 {
14963 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14964 }
14965 });
14966
14967 cx.assert_editor_state(
14968 &r#"
14969 use some::modified;
14970
14971
14972 fn main() {
14973 ˇ println!("hello there");
14974
14975 println!("around the");
14976 println!("world");
14977 }
14978 "#
14979 .unindent(),
14980 );
14981
14982 cx.update_editor(|editor, window, cx| {
14983 editor.fold(&Fold, window, cx);
14984 });
14985
14986 cx.update_editor(|editor, window, cx| {
14987 editor.go_to_next_hunk(&GoToHunk, window, cx);
14988 });
14989
14990 cx.assert_editor_state(
14991 &r#"
14992 ˇuse some::modified;
14993
14994
14995 fn main() {
14996 println!("hello there");
14997
14998 println!("around the");
14999 println!("world");
15000 }
15001 "#
15002 .unindent(),
15003 );
15004}
15005
15006#[test]
15007fn test_split_words() {
15008 fn split(text: &str) -> Vec<&str> {
15009 split_words(text).collect()
15010 }
15011
15012 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
15013 assert_eq!(split("hello_world"), &["hello_", "world"]);
15014 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
15015 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
15016 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
15017 assert_eq!(split("helloworld"), &["helloworld"]);
15018
15019 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
15020}
15021
15022#[gpui::test]
15023async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
15024 init_test(cx, |_| {});
15025
15026 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
15027 let mut assert = |before, after| {
15028 let _state_context = cx.set_state(before);
15029 cx.run_until_parked();
15030 cx.update_editor(|editor, window, cx| {
15031 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
15032 });
15033 cx.run_until_parked();
15034 cx.assert_editor_state(after);
15035 };
15036
15037 // Outside bracket jumps to outside of matching bracket
15038 assert("console.logˇ(var);", "console.log(var)ˇ;");
15039 assert("console.log(var)ˇ;", "console.logˇ(var);");
15040
15041 // Inside bracket jumps to inside of matching bracket
15042 assert("console.log(ˇvar);", "console.log(varˇ);");
15043 assert("console.log(varˇ);", "console.log(ˇvar);");
15044
15045 // When outside a bracket and inside, favor jumping to the inside bracket
15046 assert(
15047 "console.log('foo', [1, 2, 3]ˇ);",
15048 "console.log(ˇ'foo', [1, 2, 3]);",
15049 );
15050 assert(
15051 "console.log(ˇ'foo', [1, 2, 3]);",
15052 "console.log('foo', [1, 2, 3]ˇ);",
15053 );
15054
15055 // Bias forward if two options are equally likely
15056 assert(
15057 "let result = curried_fun()ˇ();",
15058 "let result = curried_fun()()ˇ;",
15059 );
15060
15061 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
15062 assert(
15063 indoc! {"
15064 function test() {
15065 console.log('test')ˇ
15066 }"},
15067 indoc! {"
15068 function test() {
15069 console.logˇ('test')
15070 }"},
15071 );
15072}
15073
15074#[gpui::test]
15075async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
15076 init_test(cx, |_| {});
15077
15078 let fs = FakeFs::new(cx.executor());
15079 fs.insert_tree(
15080 path!("/a"),
15081 json!({
15082 "main.rs": "fn main() { let a = 5; }",
15083 "other.rs": "// Test file",
15084 }),
15085 )
15086 .await;
15087 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15088
15089 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15090 language_registry.add(Arc::new(Language::new(
15091 LanguageConfig {
15092 name: "Rust".into(),
15093 matcher: LanguageMatcher {
15094 path_suffixes: vec!["rs".to_string()],
15095 ..Default::default()
15096 },
15097 brackets: BracketPairConfig {
15098 pairs: vec![BracketPair {
15099 start: "{".to_string(),
15100 end: "}".to_string(),
15101 close: true,
15102 surround: true,
15103 newline: true,
15104 }],
15105 disabled_scopes_by_bracket_ix: Vec::new(),
15106 },
15107 ..Default::default()
15108 },
15109 Some(tree_sitter_rust::LANGUAGE.into()),
15110 )));
15111 let mut fake_servers = language_registry.register_fake_lsp(
15112 "Rust",
15113 FakeLspAdapter {
15114 capabilities: lsp::ServerCapabilities {
15115 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15116 first_trigger_character: "{".to_string(),
15117 more_trigger_character: None,
15118 }),
15119 ..Default::default()
15120 },
15121 ..Default::default()
15122 },
15123 );
15124
15125 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15126
15127 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15128
15129 let worktree_id = workspace
15130 .update(cx, |workspace, _, cx| {
15131 workspace.project().update(cx, |project, cx| {
15132 project.worktrees(cx).next().unwrap().read(cx).id()
15133 })
15134 })
15135 .unwrap();
15136
15137 let buffer = project
15138 .update(cx, |project, cx| {
15139 project.open_local_buffer(path!("/a/main.rs"), cx)
15140 })
15141 .await
15142 .unwrap();
15143 let editor_handle = workspace
15144 .update(cx, |workspace, window, cx| {
15145 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
15146 })
15147 .unwrap()
15148 .await
15149 .unwrap()
15150 .downcast::<Editor>()
15151 .unwrap();
15152
15153 cx.executor().start_waiting();
15154 let fake_server = fake_servers.next().await.unwrap();
15155
15156 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
15157 |params, _| async move {
15158 assert_eq!(
15159 params.text_document_position.text_document.uri,
15160 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
15161 );
15162 assert_eq!(
15163 params.text_document_position.position,
15164 lsp::Position::new(0, 21),
15165 );
15166
15167 Ok(Some(vec![lsp::TextEdit {
15168 new_text: "]".to_string(),
15169 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15170 }]))
15171 },
15172 );
15173
15174 editor_handle.update_in(cx, |editor, window, cx| {
15175 window.focus(&editor.focus_handle(cx));
15176 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15177 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
15178 });
15179 editor.handle_input("{", window, cx);
15180 });
15181
15182 cx.executor().run_until_parked();
15183
15184 buffer.update(cx, |buffer, _| {
15185 assert_eq!(
15186 buffer.text(),
15187 "fn main() { let a = {5}; }",
15188 "No extra braces from on type formatting should appear in the buffer"
15189 )
15190 });
15191}
15192
15193#[gpui::test(iterations = 20, seeds(31))]
15194async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
15195 init_test(cx, |_| {});
15196
15197 let mut cx = EditorLspTestContext::new_rust(
15198 lsp::ServerCapabilities {
15199 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15200 first_trigger_character: ".".to_string(),
15201 more_trigger_character: None,
15202 }),
15203 ..Default::default()
15204 },
15205 cx,
15206 )
15207 .await;
15208
15209 cx.update_buffer(|buffer, _| {
15210 // This causes autoindent to be async.
15211 buffer.set_sync_parse_timeout(Duration::ZERO)
15212 });
15213
15214 cx.set_state("fn c() {\n d()ˇ\n}\n");
15215 cx.simulate_keystroke("\n");
15216 cx.run_until_parked();
15217
15218 let buffer_cloned =
15219 cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap().clone());
15220 let mut request =
15221 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
15222 let buffer_cloned = buffer_cloned.clone();
15223 async move {
15224 buffer_cloned.update(&mut cx, |buffer, _| {
15225 assert_eq!(
15226 buffer.text(),
15227 "fn c() {\n d()\n .\n}\n",
15228 "OnTypeFormatting should triggered after autoindent applied"
15229 )
15230 })?;
15231
15232 Ok(Some(vec![]))
15233 }
15234 });
15235
15236 cx.simulate_keystroke(".");
15237 cx.run_until_parked();
15238
15239 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
15240 assert!(request.next().await.is_some());
15241 request.close();
15242 assert!(request.next().await.is_none());
15243}
15244
15245#[gpui::test]
15246async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
15247 init_test(cx, |_| {});
15248
15249 let fs = FakeFs::new(cx.executor());
15250 fs.insert_tree(
15251 path!("/a"),
15252 json!({
15253 "main.rs": "fn main() { let a = 5; }",
15254 "other.rs": "// Test file",
15255 }),
15256 )
15257 .await;
15258
15259 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15260
15261 let server_restarts = Arc::new(AtomicUsize::new(0));
15262 let closure_restarts = Arc::clone(&server_restarts);
15263 let language_server_name = "test language server";
15264 let language_name: LanguageName = "Rust".into();
15265
15266 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15267 language_registry.add(Arc::new(Language::new(
15268 LanguageConfig {
15269 name: language_name.clone(),
15270 matcher: LanguageMatcher {
15271 path_suffixes: vec!["rs".to_string()],
15272 ..Default::default()
15273 },
15274 ..Default::default()
15275 },
15276 Some(tree_sitter_rust::LANGUAGE.into()),
15277 )));
15278 let mut fake_servers = language_registry.register_fake_lsp(
15279 "Rust",
15280 FakeLspAdapter {
15281 name: language_server_name,
15282 initialization_options: Some(json!({
15283 "testOptionValue": true
15284 })),
15285 initializer: Some(Box::new(move |fake_server| {
15286 let task_restarts = Arc::clone(&closure_restarts);
15287 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
15288 task_restarts.fetch_add(1, atomic::Ordering::Release);
15289 futures::future::ready(Ok(()))
15290 });
15291 })),
15292 ..Default::default()
15293 },
15294 );
15295
15296 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15297 let _buffer = project
15298 .update(cx, |project, cx| {
15299 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
15300 })
15301 .await
15302 .unwrap();
15303 let _fake_server = fake_servers.next().await.unwrap();
15304 update_test_language_settings(cx, |language_settings| {
15305 language_settings.languages.0.insert(
15306 language_name.clone(),
15307 LanguageSettingsContent {
15308 tab_size: NonZeroU32::new(8),
15309 ..Default::default()
15310 },
15311 );
15312 });
15313 cx.executor().run_until_parked();
15314 assert_eq!(
15315 server_restarts.load(atomic::Ordering::Acquire),
15316 0,
15317 "Should not restart LSP server on an unrelated change"
15318 );
15319
15320 update_test_project_settings(cx, |project_settings| {
15321 project_settings.lsp.insert(
15322 "Some other server name".into(),
15323 LspSettings {
15324 binary: None,
15325 settings: None,
15326 initialization_options: Some(json!({
15327 "some other init value": false
15328 })),
15329 enable_lsp_tasks: false,
15330 },
15331 );
15332 });
15333 cx.executor().run_until_parked();
15334 assert_eq!(
15335 server_restarts.load(atomic::Ordering::Acquire),
15336 0,
15337 "Should not restart LSP server on an unrelated LSP settings change"
15338 );
15339
15340 update_test_project_settings(cx, |project_settings| {
15341 project_settings.lsp.insert(
15342 language_server_name.into(),
15343 LspSettings {
15344 binary: None,
15345 settings: None,
15346 initialization_options: Some(json!({
15347 "anotherInitValue": false
15348 })),
15349 enable_lsp_tasks: false,
15350 },
15351 );
15352 });
15353 cx.executor().run_until_parked();
15354 assert_eq!(
15355 server_restarts.load(atomic::Ordering::Acquire),
15356 1,
15357 "Should restart LSP server on a related LSP settings change"
15358 );
15359
15360 update_test_project_settings(cx, |project_settings| {
15361 project_settings.lsp.insert(
15362 language_server_name.into(),
15363 LspSettings {
15364 binary: None,
15365 settings: None,
15366 initialization_options: Some(json!({
15367 "anotherInitValue": false
15368 })),
15369 enable_lsp_tasks: false,
15370 },
15371 );
15372 });
15373 cx.executor().run_until_parked();
15374 assert_eq!(
15375 server_restarts.load(atomic::Ordering::Acquire),
15376 1,
15377 "Should not restart LSP server on a related LSP settings change that is the same"
15378 );
15379
15380 update_test_project_settings(cx, |project_settings| {
15381 project_settings.lsp.insert(
15382 language_server_name.into(),
15383 LspSettings {
15384 binary: None,
15385 settings: None,
15386 initialization_options: None,
15387 enable_lsp_tasks: false,
15388 },
15389 );
15390 });
15391 cx.executor().run_until_parked();
15392 assert_eq!(
15393 server_restarts.load(atomic::Ordering::Acquire),
15394 2,
15395 "Should restart LSP server on another related LSP settings change"
15396 );
15397}
15398
15399#[gpui::test]
15400async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
15401 init_test(cx, |_| {});
15402
15403 let mut cx = EditorLspTestContext::new_rust(
15404 lsp::ServerCapabilities {
15405 completion_provider: Some(lsp::CompletionOptions {
15406 trigger_characters: Some(vec![".".to_string()]),
15407 resolve_provider: Some(true),
15408 ..Default::default()
15409 }),
15410 ..Default::default()
15411 },
15412 cx,
15413 )
15414 .await;
15415
15416 cx.set_state("fn main() { let a = 2ˇ; }");
15417 cx.simulate_keystroke(".");
15418 let completion_item = lsp::CompletionItem {
15419 label: "some".into(),
15420 kind: Some(lsp::CompletionItemKind::SNIPPET),
15421 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15422 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15423 kind: lsp::MarkupKind::Markdown,
15424 value: "```rust\nSome(2)\n```".to_string(),
15425 })),
15426 deprecated: Some(false),
15427 sort_text: Some("fffffff2".to_string()),
15428 filter_text: Some("some".to_string()),
15429 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15430 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15431 range: lsp::Range {
15432 start: lsp::Position {
15433 line: 0,
15434 character: 22,
15435 },
15436 end: lsp::Position {
15437 line: 0,
15438 character: 22,
15439 },
15440 },
15441 new_text: "Some(2)".to_string(),
15442 })),
15443 additional_text_edits: Some(vec![lsp::TextEdit {
15444 range: lsp::Range {
15445 start: lsp::Position {
15446 line: 0,
15447 character: 20,
15448 },
15449 end: lsp::Position {
15450 line: 0,
15451 character: 22,
15452 },
15453 },
15454 new_text: "".to_string(),
15455 }]),
15456 ..Default::default()
15457 };
15458
15459 let closure_completion_item = completion_item.clone();
15460 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15461 let task_completion_item = closure_completion_item.clone();
15462 async move {
15463 Ok(Some(lsp::CompletionResponse::Array(vec![
15464 task_completion_item,
15465 ])))
15466 }
15467 });
15468
15469 request.next().await;
15470
15471 cx.condition(|editor, _| editor.context_menu_visible())
15472 .await;
15473 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15474 editor
15475 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15476 .unwrap()
15477 });
15478 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
15479
15480 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
15481 let task_completion_item = completion_item.clone();
15482 async move { Ok(task_completion_item) }
15483 })
15484 .next()
15485 .await
15486 .unwrap();
15487 apply_additional_edits.await.unwrap();
15488 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
15489}
15490
15491#[gpui::test]
15492async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
15493 init_test(cx, |_| {});
15494
15495 let mut cx = EditorLspTestContext::new_rust(
15496 lsp::ServerCapabilities {
15497 completion_provider: Some(lsp::CompletionOptions {
15498 trigger_characters: Some(vec![".".to_string()]),
15499 resolve_provider: Some(true),
15500 ..Default::default()
15501 }),
15502 ..Default::default()
15503 },
15504 cx,
15505 )
15506 .await;
15507
15508 cx.set_state("fn main() { let a = 2ˇ; }");
15509 cx.simulate_keystroke(".");
15510
15511 let item1 = lsp::CompletionItem {
15512 label: "method id()".to_string(),
15513 filter_text: Some("id".to_string()),
15514 detail: None,
15515 documentation: None,
15516 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15517 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15518 new_text: ".id".to_string(),
15519 })),
15520 ..lsp::CompletionItem::default()
15521 };
15522
15523 let item2 = lsp::CompletionItem {
15524 label: "other".to_string(),
15525 filter_text: Some("other".to_string()),
15526 detail: None,
15527 documentation: None,
15528 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15529 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15530 new_text: ".other".to_string(),
15531 })),
15532 ..lsp::CompletionItem::default()
15533 };
15534
15535 let item1 = item1.clone();
15536 cx.set_request_handler::<lsp::request::Completion, _, _>({
15537 let item1 = item1.clone();
15538 move |_, _, _| {
15539 let item1 = item1.clone();
15540 let item2 = item2.clone();
15541 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
15542 }
15543 })
15544 .next()
15545 .await;
15546
15547 cx.condition(|editor, _| editor.context_menu_visible())
15548 .await;
15549 cx.update_editor(|editor, _, _| {
15550 let context_menu = editor.context_menu.borrow_mut();
15551 let context_menu = context_menu
15552 .as_ref()
15553 .expect("Should have the context menu deployed");
15554 match context_menu {
15555 CodeContextMenu::Completions(completions_menu) => {
15556 let completions = completions_menu.completions.borrow_mut();
15557 assert_eq!(
15558 completions
15559 .iter()
15560 .map(|completion| &completion.label.text)
15561 .collect::<Vec<_>>(),
15562 vec!["method id()", "other"]
15563 )
15564 }
15565 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15566 }
15567 });
15568
15569 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
15570 let item1 = item1.clone();
15571 move |_, item_to_resolve, _| {
15572 let item1 = item1.clone();
15573 async move {
15574 if item1 == item_to_resolve {
15575 Ok(lsp::CompletionItem {
15576 label: "method id()".to_string(),
15577 filter_text: Some("id".to_string()),
15578 detail: Some("Now resolved!".to_string()),
15579 documentation: Some(lsp::Documentation::String("Docs".to_string())),
15580 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15581 range: lsp::Range::new(
15582 lsp::Position::new(0, 22),
15583 lsp::Position::new(0, 22),
15584 ),
15585 new_text: ".id".to_string(),
15586 })),
15587 ..lsp::CompletionItem::default()
15588 })
15589 } else {
15590 Ok(item_to_resolve)
15591 }
15592 }
15593 }
15594 })
15595 .next()
15596 .await
15597 .unwrap();
15598 cx.run_until_parked();
15599
15600 cx.update_editor(|editor, window, cx| {
15601 editor.context_menu_next(&Default::default(), window, cx);
15602 });
15603
15604 cx.update_editor(|editor, _, _| {
15605 let context_menu = editor.context_menu.borrow_mut();
15606 let context_menu = context_menu
15607 .as_ref()
15608 .expect("Should have the context menu deployed");
15609 match context_menu {
15610 CodeContextMenu::Completions(completions_menu) => {
15611 let completions = completions_menu.completions.borrow_mut();
15612 assert_eq!(
15613 completions
15614 .iter()
15615 .map(|completion| &completion.label.text)
15616 .collect::<Vec<_>>(),
15617 vec!["method id() Now resolved!", "other"],
15618 "Should update first completion label, but not second as the filter text did not match."
15619 );
15620 }
15621 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15622 }
15623 });
15624}
15625
15626#[gpui::test]
15627async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
15628 init_test(cx, |_| {});
15629 let mut cx = EditorLspTestContext::new_rust(
15630 lsp::ServerCapabilities {
15631 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
15632 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
15633 completion_provider: Some(lsp::CompletionOptions {
15634 resolve_provider: Some(true),
15635 ..Default::default()
15636 }),
15637 ..Default::default()
15638 },
15639 cx,
15640 )
15641 .await;
15642 cx.set_state(indoc! {"
15643 struct TestStruct {
15644 field: i32
15645 }
15646
15647 fn mainˇ() {
15648 let unused_var = 42;
15649 let test_struct = TestStruct { field: 42 };
15650 }
15651 "});
15652 let symbol_range = cx.lsp_range(indoc! {"
15653 struct TestStruct {
15654 field: i32
15655 }
15656
15657 «fn main»() {
15658 let unused_var = 42;
15659 let test_struct = TestStruct { field: 42 };
15660 }
15661 "});
15662 let mut hover_requests =
15663 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
15664 Ok(Some(lsp::Hover {
15665 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
15666 kind: lsp::MarkupKind::Markdown,
15667 value: "Function documentation".to_string(),
15668 }),
15669 range: Some(symbol_range),
15670 }))
15671 });
15672
15673 // Case 1: Test that code action menu hide hover popover
15674 cx.dispatch_action(Hover);
15675 hover_requests.next().await;
15676 cx.condition(|editor, _| editor.hover_state.visible()).await;
15677 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
15678 move |_, _, _| async move {
15679 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
15680 lsp::CodeAction {
15681 title: "Remove unused variable".to_string(),
15682 kind: Some(CodeActionKind::QUICKFIX),
15683 edit: Some(lsp::WorkspaceEdit {
15684 changes: Some(
15685 [(
15686 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
15687 vec![lsp::TextEdit {
15688 range: lsp::Range::new(
15689 lsp::Position::new(5, 4),
15690 lsp::Position::new(5, 27),
15691 ),
15692 new_text: "".to_string(),
15693 }],
15694 )]
15695 .into_iter()
15696 .collect(),
15697 ),
15698 ..Default::default()
15699 }),
15700 ..Default::default()
15701 },
15702 )]))
15703 },
15704 );
15705 cx.update_editor(|editor, window, cx| {
15706 editor.toggle_code_actions(
15707 &ToggleCodeActions {
15708 deployed_from: None,
15709 quick_launch: false,
15710 },
15711 window,
15712 cx,
15713 );
15714 });
15715 code_action_requests.next().await;
15716 cx.run_until_parked();
15717 cx.condition(|editor, _| editor.context_menu_visible())
15718 .await;
15719 cx.update_editor(|editor, _, _| {
15720 assert!(
15721 !editor.hover_state.visible(),
15722 "Hover popover should be hidden when code action menu is shown"
15723 );
15724 // Hide code actions
15725 editor.context_menu.take();
15726 });
15727
15728 // Case 2: Test that code completions hide hover popover
15729 cx.dispatch_action(Hover);
15730 hover_requests.next().await;
15731 cx.condition(|editor, _| editor.hover_state.visible()).await;
15732 let counter = Arc::new(AtomicUsize::new(0));
15733 let mut completion_requests =
15734 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15735 let counter = counter.clone();
15736 async move {
15737 counter.fetch_add(1, atomic::Ordering::Release);
15738 Ok(Some(lsp::CompletionResponse::Array(vec![
15739 lsp::CompletionItem {
15740 label: "main".into(),
15741 kind: Some(lsp::CompletionItemKind::FUNCTION),
15742 detail: Some("() -> ()".to_string()),
15743 ..Default::default()
15744 },
15745 lsp::CompletionItem {
15746 label: "TestStruct".into(),
15747 kind: Some(lsp::CompletionItemKind::STRUCT),
15748 detail: Some("struct TestStruct".to_string()),
15749 ..Default::default()
15750 },
15751 ])))
15752 }
15753 });
15754 cx.update_editor(|editor, window, cx| {
15755 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15756 });
15757 completion_requests.next().await;
15758 cx.condition(|editor, _| editor.context_menu_visible())
15759 .await;
15760 cx.update_editor(|editor, _, _| {
15761 assert!(
15762 !editor.hover_state.visible(),
15763 "Hover popover should be hidden when completion menu is shown"
15764 );
15765 });
15766}
15767
15768#[gpui::test]
15769async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
15770 init_test(cx, |_| {});
15771
15772 let mut cx = EditorLspTestContext::new_rust(
15773 lsp::ServerCapabilities {
15774 completion_provider: Some(lsp::CompletionOptions {
15775 trigger_characters: Some(vec![".".to_string()]),
15776 resolve_provider: Some(true),
15777 ..Default::default()
15778 }),
15779 ..Default::default()
15780 },
15781 cx,
15782 )
15783 .await;
15784
15785 cx.set_state("fn main() { let a = 2ˇ; }");
15786 cx.simulate_keystroke(".");
15787
15788 let unresolved_item_1 = lsp::CompletionItem {
15789 label: "id".to_string(),
15790 filter_text: Some("id".to_string()),
15791 detail: None,
15792 documentation: None,
15793 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15794 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15795 new_text: ".id".to_string(),
15796 })),
15797 ..lsp::CompletionItem::default()
15798 };
15799 let resolved_item_1 = lsp::CompletionItem {
15800 additional_text_edits: Some(vec![lsp::TextEdit {
15801 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15802 new_text: "!!".to_string(),
15803 }]),
15804 ..unresolved_item_1.clone()
15805 };
15806 let unresolved_item_2 = lsp::CompletionItem {
15807 label: "other".to_string(),
15808 filter_text: Some("other".to_string()),
15809 detail: None,
15810 documentation: None,
15811 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15812 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15813 new_text: ".other".to_string(),
15814 })),
15815 ..lsp::CompletionItem::default()
15816 };
15817 let resolved_item_2 = lsp::CompletionItem {
15818 additional_text_edits: Some(vec![lsp::TextEdit {
15819 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15820 new_text: "??".to_string(),
15821 }]),
15822 ..unresolved_item_2.clone()
15823 };
15824
15825 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
15826 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
15827 cx.lsp
15828 .server
15829 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15830 let unresolved_item_1 = unresolved_item_1.clone();
15831 let resolved_item_1 = resolved_item_1.clone();
15832 let unresolved_item_2 = unresolved_item_2.clone();
15833 let resolved_item_2 = resolved_item_2.clone();
15834 let resolve_requests_1 = resolve_requests_1.clone();
15835 let resolve_requests_2 = resolve_requests_2.clone();
15836 move |unresolved_request, _| {
15837 let unresolved_item_1 = unresolved_item_1.clone();
15838 let resolved_item_1 = resolved_item_1.clone();
15839 let unresolved_item_2 = unresolved_item_2.clone();
15840 let resolved_item_2 = resolved_item_2.clone();
15841 let resolve_requests_1 = resolve_requests_1.clone();
15842 let resolve_requests_2 = resolve_requests_2.clone();
15843 async move {
15844 if unresolved_request == unresolved_item_1 {
15845 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
15846 Ok(resolved_item_1.clone())
15847 } else if unresolved_request == unresolved_item_2 {
15848 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
15849 Ok(resolved_item_2.clone())
15850 } else {
15851 panic!("Unexpected completion item {unresolved_request:?}")
15852 }
15853 }
15854 }
15855 })
15856 .detach();
15857
15858 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15859 let unresolved_item_1 = unresolved_item_1.clone();
15860 let unresolved_item_2 = unresolved_item_2.clone();
15861 async move {
15862 Ok(Some(lsp::CompletionResponse::Array(vec![
15863 unresolved_item_1,
15864 unresolved_item_2,
15865 ])))
15866 }
15867 })
15868 .next()
15869 .await;
15870
15871 cx.condition(|editor, _| editor.context_menu_visible())
15872 .await;
15873 cx.update_editor(|editor, _, _| {
15874 let context_menu = editor.context_menu.borrow_mut();
15875 let context_menu = context_menu
15876 .as_ref()
15877 .expect("Should have the context menu deployed");
15878 match context_menu {
15879 CodeContextMenu::Completions(completions_menu) => {
15880 let completions = completions_menu.completions.borrow_mut();
15881 assert_eq!(
15882 completions
15883 .iter()
15884 .map(|completion| &completion.label.text)
15885 .collect::<Vec<_>>(),
15886 vec!["id", "other"]
15887 )
15888 }
15889 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15890 }
15891 });
15892 cx.run_until_parked();
15893
15894 cx.update_editor(|editor, window, cx| {
15895 editor.context_menu_next(&ContextMenuNext, window, cx);
15896 });
15897 cx.run_until_parked();
15898 cx.update_editor(|editor, window, cx| {
15899 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15900 });
15901 cx.run_until_parked();
15902 cx.update_editor(|editor, window, cx| {
15903 editor.context_menu_next(&ContextMenuNext, window, cx);
15904 });
15905 cx.run_until_parked();
15906 cx.update_editor(|editor, window, cx| {
15907 editor
15908 .compose_completion(&ComposeCompletion::default(), window, cx)
15909 .expect("No task returned")
15910 })
15911 .await
15912 .expect("Completion failed");
15913 cx.run_until_parked();
15914
15915 cx.update_editor(|editor, _, cx| {
15916 assert_eq!(
15917 resolve_requests_1.load(atomic::Ordering::Acquire),
15918 1,
15919 "Should always resolve once despite multiple selections"
15920 );
15921 assert_eq!(
15922 resolve_requests_2.load(atomic::Ordering::Acquire),
15923 1,
15924 "Should always resolve once after multiple selections and applying the completion"
15925 );
15926 assert_eq!(
15927 editor.text(cx),
15928 "fn main() { let a = ??.other; }",
15929 "Should use resolved data when applying the completion"
15930 );
15931 });
15932}
15933
15934#[gpui::test]
15935async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
15936 init_test(cx, |_| {});
15937
15938 let item_0 = lsp::CompletionItem {
15939 label: "abs".into(),
15940 insert_text: Some("abs".into()),
15941 data: Some(json!({ "very": "special"})),
15942 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
15943 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
15944 lsp::InsertReplaceEdit {
15945 new_text: "abs".to_string(),
15946 insert: lsp::Range::default(),
15947 replace: lsp::Range::default(),
15948 },
15949 )),
15950 ..lsp::CompletionItem::default()
15951 };
15952 let items = iter::once(item_0.clone())
15953 .chain((11..51).map(|i| lsp::CompletionItem {
15954 label: format!("item_{}", i),
15955 insert_text: Some(format!("item_{}", i)),
15956 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15957 ..lsp::CompletionItem::default()
15958 }))
15959 .collect::<Vec<_>>();
15960
15961 let default_commit_characters = vec!["?".to_string()];
15962 let default_data = json!({ "default": "data"});
15963 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
15964 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
15965 let default_edit_range = lsp::Range {
15966 start: lsp::Position {
15967 line: 0,
15968 character: 5,
15969 },
15970 end: lsp::Position {
15971 line: 0,
15972 character: 5,
15973 },
15974 };
15975
15976 let mut cx = EditorLspTestContext::new_rust(
15977 lsp::ServerCapabilities {
15978 completion_provider: Some(lsp::CompletionOptions {
15979 trigger_characters: Some(vec![".".to_string()]),
15980 resolve_provider: Some(true),
15981 ..Default::default()
15982 }),
15983 ..Default::default()
15984 },
15985 cx,
15986 )
15987 .await;
15988
15989 cx.set_state("fn main() { let a = 2ˇ; }");
15990 cx.simulate_keystroke(".");
15991
15992 let completion_data = default_data.clone();
15993 let completion_characters = default_commit_characters.clone();
15994 let completion_items = items.clone();
15995 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15996 let default_data = completion_data.clone();
15997 let default_commit_characters = completion_characters.clone();
15998 let items = completion_items.clone();
15999 async move {
16000 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16001 items,
16002 item_defaults: Some(lsp::CompletionListItemDefaults {
16003 data: Some(default_data.clone()),
16004 commit_characters: Some(default_commit_characters.clone()),
16005 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
16006 default_edit_range,
16007 )),
16008 insert_text_format: Some(default_insert_text_format),
16009 insert_text_mode: Some(default_insert_text_mode),
16010 }),
16011 ..lsp::CompletionList::default()
16012 })))
16013 }
16014 })
16015 .next()
16016 .await;
16017
16018 let resolved_items = Arc::new(Mutex::new(Vec::new()));
16019 cx.lsp
16020 .server
16021 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
16022 let closure_resolved_items = resolved_items.clone();
16023 move |item_to_resolve, _| {
16024 let closure_resolved_items = closure_resolved_items.clone();
16025 async move {
16026 closure_resolved_items.lock().push(item_to_resolve.clone());
16027 Ok(item_to_resolve)
16028 }
16029 }
16030 })
16031 .detach();
16032
16033 cx.condition(|editor, _| editor.context_menu_visible())
16034 .await;
16035 cx.run_until_parked();
16036 cx.update_editor(|editor, _, _| {
16037 let menu = editor.context_menu.borrow_mut();
16038 match menu.as_ref().expect("should have the completions menu") {
16039 CodeContextMenu::Completions(completions_menu) => {
16040 assert_eq!(
16041 completions_menu
16042 .entries
16043 .borrow()
16044 .iter()
16045 .map(|mat| mat.string.clone())
16046 .collect::<Vec<String>>(),
16047 items
16048 .iter()
16049 .map(|completion| completion.label.clone())
16050 .collect::<Vec<String>>()
16051 );
16052 }
16053 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
16054 }
16055 });
16056 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
16057 // with 4 from the end.
16058 assert_eq!(
16059 *resolved_items.lock(),
16060 [&items[0..16], &items[items.len() - 4..items.len()]]
16061 .concat()
16062 .iter()
16063 .cloned()
16064 .map(|mut item| {
16065 if item.data.is_none() {
16066 item.data = Some(default_data.clone());
16067 }
16068 item
16069 })
16070 .collect::<Vec<lsp::CompletionItem>>(),
16071 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
16072 );
16073 resolved_items.lock().clear();
16074
16075 cx.update_editor(|editor, window, cx| {
16076 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
16077 });
16078 cx.run_until_parked();
16079 // Completions that have already been resolved are skipped.
16080 assert_eq!(
16081 *resolved_items.lock(),
16082 items[items.len() - 17..items.len() - 4]
16083 .iter()
16084 .cloned()
16085 .map(|mut item| {
16086 if item.data.is_none() {
16087 item.data = Some(default_data.clone());
16088 }
16089 item
16090 })
16091 .collect::<Vec<lsp::CompletionItem>>()
16092 );
16093 resolved_items.lock().clear();
16094}
16095
16096#[gpui::test]
16097async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
16098 init_test(cx, |_| {});
16099
16100 let mut cx = EditorLspTestContext::new(
16101 Language::new(
16102 LanguageConfig {
16103 matcher: LanguageMatcher {
16104 path_suffixes: vec!["jsx".into()],
16105 ..Default::default()
16106 },
16107 overrides: [(
16108 "element".into(),
16109 LanguageConfigOverride {
16110 completion_query_characters: Override::Set(['-'].into_iter().collect()),
16111 ..Default::default()
16112 },
16113 )]
16114 .into_iter()
16115 .collect(),
16116 ..Default::default()
16117 },
16118 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16119 )
16120 .with_override_query("(jsx_self_closing_element) @element")
16121 .unwrap(),
16122 lsp::ServerCapabilities {
16123 completion_provider: Some(lsp::CompletionOptions {
16124 trigger_characters: Some(vec![":".to_string()]),
16125 ..Default::default()
16126 }),
16127 ..Default::default()
16128 },
16129 cx,
16130 )
16131 .await;
16132
16133 cx.lsp
16134 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16135 Ok(Some(lsp::CompletionResponse::Array(vec![
16136 lsp::CompletionItem {
16137 label: "bg-blue".into(),
16138 ..Default::default()
16139 },
16140 lsp::CompletionItem {
16141 label: "bg-red".into(),
16142 ..Default::default()
16143 },
16144 lsp::CompletionItem {
16145 label: "bg-yellow".into(),
16146 ..Default::default()
16147 },
16148 ])))
16149 });
16150
16151 cx.set_state(r#"<p class="bgˇ" />"#);
16152
16153 // Trigger completion when typing a dash, because the dash is an extra
16154 // word character in the 'element' scope, which contains the cursor.
16155 cx.simulate_keystroke("-");
16156 cx.executor().run_until_parked();
16157 cx.update_editor(|editor, _, _| {
16158 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16159 {
16160 assert_eq!(
16161 completion_menu_entries(&menu),
16162 &["bg-blue", "bg-red", "bg-yellow"]
16163 );
16164 } else {
16165 panic!("expected completion menu to be open");
16166 }
16167 });
16168
16169 cx.simulate_keystroke("l");
16170 cx.executor().run_until_parked();
16171 cx.update_editor(|editor, _, _| {
16172 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16173 {
16174 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
16175 } else {
16176 panic!("expected completion menu to be open");
16177 }
16178 });
16179
16180 // When filtering completions, consider the character after the '-' to
16181 // be the start of a subword.
16182 cx.set_state(r#"<p class="yelˇ" />"#);
16183 cx.simulate_keystroke("l");
16184 cx.executor().run_until_parked();
16185 cx.update_editor(|editor, _, _| {
16186 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16187 {
16188 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
16189 } else {
16190 panic!("expected completion menu to be open");
16191 }
16192 });
16193}
16194
16195fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
16196 let entries = menu.entries.borrow();
16197 entries.iter().map(|mat| mat.string.clone()).collect()
16198}
16199
16200#[gpui::test]
16201async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
16202 init_test(cx, |settings| {
16203 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
16204 Formatter::Prettier,
16205 )))
16206 });
16207
16208 let fs = FakeFs::new(cx.executor());
16209 fs.insert_file(path!("/file.ts"), Default::default()).await;
16210
16211 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
16212 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16213
16214 language_registry.add(Arc::new(Language::new(
16215 LanguageConfig {
16216 name: "TypeScript".into(),
16217 matcher: LanguageMatcher {
16218 path_suffixes: vec!["ts".to_string()],
16219 ..Default::default()
16220 },
16221 ..Default::default()
16222 },
16223 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
16224 )));
16225 update_test_language_settings(cx, |settings| {
16226 settings.defaults.prettier = Some(PrettierSettings {
16227 allowed: true,
16228 ..PrettierSettings::default()
16229 });
16230 });
16231
16232 let test_plugin = "test_plugin";
16233 let _ = language_registry.register_fake_lsp(
16234 "TypeScript",
16235 FakeLspAdapter {
16236 prettier_plugins: vec![test_plugin],
16237 ..Default::default()
16238 },
16239 );
16240
16241 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
16242 let buffer = project
16243 .update(cx, |project, cx| {
16244 project.open_local_buffer(path!("/file.ts"), cx)
16245 })
16246 .await
16247 .unwrap();
16248
16249 let buffer_text = "one\ntwo\nthree\n";
16250 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16251 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16252 editor.update_in(cx, |editor, window, cx| {
16253 editor.set_text(buffer_text, window, cx)
16254 });
16255
16256 editor
16257 .update_in(cx, |editor, window, cx| {
16258 editor.perform_format(
16259 project.clone(),
16260 FormatTrigger::Manual,
16261 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16262 window,
16263 cx,
16264 )
16265 })
16266 .unwrap()
16267 .await;
16268 assert_eq!(
16269 editor.update(cx, |editor, cx| editor.text(cx)),
16270 buffer_text.to_string() + prettier_format_suffix,
16271 "Test prettier formatting was not applied to the original buffer text",
16272 );
16273
16274 update_test_language_settings(cx, |settings| {
16275 settings.defaults.formatter = Some(SelectedFormatter::Auto)
16276 });
16277 let format = editor.update_in(cx, |editor, window, cx| {
16278 editor.perform_format(
16279 project.clone(),
16280 FormatTrigger::Manual,
16281 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16282 window,
16283 cx,
16284 )
16285 });
16286 format.await.unwrap();
16287 assert_eq!(
16288 editor.update(cx, |editor, cx| editor.text(cx)),
16289 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
16290 "Autoformatting (via test prettier) was not applied to the original buffer text",
16291 );
16292}
16293
16294#[gpui::test]
16295async fn test_addition_reverts(cx: &mut TestAppContext) {
16296 init_test(cx, |_| {});
16297 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16298 let base_text = indoc! {r#"
16299 struct Row;
16300 struct Row1;
16301 struct Row2;
16302
16303 struct Row4;
16304 struct Row5;
16305 struct Row6;
16306
16307 struct Row8;
16308 struct Row9;
16309 struct Row10;"#};
16310
16311 // When addition hunks are not adjacent to carets, no hunk revert is performed
16312 assert_hunk_revert(
16313 indoc! {r#"struct Row;
16314 struct Row1;
16315 struct Row1.1;
16316 struct Row1.2;
16317 struct Row2;ˇ
16318
16319 struct Row4;
16320 struct Row5;
16321 struct Row6;
16322
16323 struct Row8;
16324 ˇstruct Row9;
16325 struct Row9.1;
16326 struct Row9.2;
16327 struct Row9.3;
16328 struct Row10;"#},
16329 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16330 indoc! {r#"struct Row;
16331 struct Row1;
16332 struct Row1.1;
16333 struct Row1.2;
16334 struct Row2;ˇ
16335
16336 struct Row4;
16337 struct Row5;
16338 struct Row6;
16339
16340 struct Row8;
16341 ˇstruct Row9;
16342 struct Row9.1;
16343 struct Row9.2;
16344 struct Row9.3;
16345 struct Row10;"#},
16346 base_text,
16347 &mut cx,
16348 );
16349 // Same for selections
16350 assert_hunk_revert(
16351 indoc! {r#"struct Row;
16352 struct Row1;
16353 struct Row2;
16354 struct Row2.1;
16355 struct Row2.2;
16356 «ˇ
16357 struct Row4;
16358 struct» Row5;
16359 «struct Row6;
16360 ˇ»
16361 struct Row9.1;
16362 struct Row9.2;
16363 struct Row9.3;
16364 struct Row8;
16365 struct Row9;
16366 struct Row10;"#},
16367 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16368 indoc! {r#"struct Row;
16369 struct Row1;
16370 struct Row2;
16371 struct Row2.1;
16372 struct Row2.2;
16373 «ˇ
16374 struct Row4;
16375 struct» Row5;
16376 «struct Row6;
16377 ˇ»
16378 struct Row9.1;
16379 struct Row9.2;
16380 struct Row9.3;
16381 struct Row8;
16382 struct Row9;
16383 struct Row10;"#},
16384 base_text,
16385 &mut cx,
16386 );
16387
16388 // When carets and selections intersect the addition hunks, those are reverted.
16389 // Adjacent carets got merged.
16390 assert_hunk_revert(
16391 indoc! {r#"struct Row;
16392 ˇ// something on the top
16393 struct Row1;
16394 struct Row2;
16395 struct Roˇw3.1;
16396 struct Row2.2;
16397 struct Row2.3;ˇ
16398
16399 struct Row4;
16400 struct ˇRow5.1;
16401 struct Row5.2;
16402 struct «Rowˇ»5.3;
16403 struct Row5;
16404 struct Row6;
16405 ˇ
16406 struct Row9.1;
16407 struct «Rowˇ»9.2;
16408 struct «ˇRow»9.3;
16409 struct Row8;
16410 struct Row9;
16411 «ˇ// something on bottom»
16412 struct Row10;"#},
16413 vec![
16414 DiffHunkStatusKind::Added,
16415 DiffHunkStatusKind::Added,
16416 DiffHunkStatusKind::Added,
16417 DiffHunkStatusKind::Added,
16418 DiffHunkStatusKind::Added,
16419 ],
16420 indoc! {r#"struct Row;
16421 ˇstruct Row1;
16422 struct Row2;
16423 ˇ
16424 struct Row4;
16425 ˇstruct Row5;
16426 struct Row6;
16427 ˇ
16428 ˇstruct Row8;
16429 struct Row9;
16430 ˇstruct Row10;"#},
16431 base_text,
16432 &mut cx,
16433 );
16434}
16435
16436#[gpui::test]
16437async fn test_modification_reverts(cx: &mut TestAppContext) {
16438 init_test(cx, |_| {});
16439 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16440 let base_text = indoc! {r#"
16441 struct Row;
16442 struct Row1;
16443 struct Row2;
16444
16445 struct Row4;
16446 struct Row5;
16447 struct Row6;
16448
16449 struct Row8;
16450 struct Row9;
16451 struct Row10;"#};
16452
16453 // Modification hunks behave the same as the addition ones.
16454 assert_hunk_revert(
16455 indoc! {r#"struct Row;
16456 struct Row1;
16457 struct Row33;
16458 ˇ
16459 struct Row4;
16460 struct Row5;
16461 struct Row6;
16462 ˇ
16463 struct Row99;
16464 struct Row9;
16465 struct Row10;"#},
16466 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16467 indoc! {r#"struct Row;
16468 struct Row1;
16469 struct Row33;
16470 ˇ
16471 struct Row4;
16472 struct Row5;
16473 struct Row6;
16474 ˇ
16475 struct Row99;
16476 struct Row9;
16477 struct Row10;"#},
16478 base_text,
16479 &mut cx,
16480 );
16481 assert_hunk_revert(
16482 indoc! {r#"struct Row;
16483 struct Row1;
16484 struct Row33;
16485 «ˇ
16486 struct Row4;
16487 struct» Row5;
16488 «struct Row6;
16489 ˇ»
16490 struct Row99;
16491 struct Row9;
16492 struct Row10;"#},
16493 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16494 indoc! {r#"struct Row;
16495 struct Row1;
16496 struct Row33;
16497 «ˇ
16498 struct Row4;
16499 struct» Row5;
16500 «struct Row6;
16501 ˇ»
16502 struct Row99;
16503 struct Row9;
16504 struct Row10;"#},
16505 base_text,
16506 &mut cx,
16507 );
16508
16509 assert_hunk_revert(
16510 indoc! {r#"ˇstruct Row1.1;
16511 struct Row1;
16512 «ˇstr»uct Row22;
16513
16514 struct ˇRow44;
16515 struct Row5;
16516 struct «Rˇ»ow66;ˇ
16517
16518 «struˇ»ct Row88;
16519 struct Row9;
16520 struct Row1011;ˇ"#},
16521 vec![
16522 DiffHunkStatusKind::Modified,
16523 DiffHunkStatusKind::Modified,
16524 DiffHunkStatusKind::Modified,
16525 DiffHunkStatusKind::Modified,
16526 DiffHunkStatusKind::Modified,
16527 DiffHunkStatusKind::Modified,
16528 ],
16529 indoc! {r#"struct Row;
16530 ˇstruct Row1;
16531 struct Row2;
16532 ˇ
16533 struct Row4;
16534 ˇstruct Row5;
16535 struct Row6;
16536 ˇ
16537 struct Row8;
16538 ˇstruct Row9;
16539 struct Row10;ˇ"#},
16540 base_text,
16541 &mut cx,
16542 );
16543}
16544
16545#[gpui::test]
16546async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
16547 init_test(cx, |_| {});
16548 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16549 let base_text = indoc! {r#"
16550 one
16551
16552 two
16553 three
16554 "#};
16555
16556 cx.set_head_text(base_text);
16557 cx.set_state("\nˇ\n");
16558 cx.executor().run_until_parked();
16559 cx.update_editor(|editor, _window, cx| {
16560 editor.expand_selected_diff_hunks(cx);
16561 });
16562 cx.executor().run_until_parked();
16563 cx.update_editor(|editor, window, cx| {
16564 editor.backspace(&Default::default(), window, cx);
16565 });
16566 cx.run_until_parked();
16567 cx.assert_state_with_diff(
16568 indoc! {r#"
16569
16570 - two
16571 - threeˇ
16572 +
16573 "#}
16574 .to_string(),
16575 );
16576}
16577
16578#[gpui::test]
16579async fn test_deletion_reverts(cx: &mut TestAppContext) {
16580 init_test(cx, |_| {});
16581 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16582 let base_text = indoc! {r#"struct Row;
16583struct Row1;
16584struct Row2;
16585
16586struct Row4;
16587struct Row5;
16588struct Row6;
16589
16590struct Row8;
16591struct Row9;
16592struct Row10;"#};
16593
16594 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
16595 assert_hunk_revert(
16596 indoc! {r#"struct Row;
16597 struct Row2;
16598
16599 ˇstruct Row4;
16600 struct Row5;
16601 struct Row6;
16602 ˇ
16603 struct Row8;
16604 struct Row10;"#},
16605 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16606 indoc! {r#"struct Row;
16607 struct Row2;
16608
16609 ˇstruct Row4;
16610 struct Row5;
16611 struct Row6;
16612 ˇ
16613 struct Row8;
16614 struct Row10;"#},
16615 base_text,
16616 &mut cx,
16617 );
16618 assert_hunk_revert(
16619 indoc! {r#"struct Row;
16620 struct Row2;
16621
16622 «ˇstruct Row4;
16623 struct» Row5;
16624 «struct Row6;
16625 ˇ»
16626 struct Row8;
16627 struct Row10;"#},
16628 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16629 indoc! {r#"struct Row;
16630 struct Row2;
16631
16632 «ˇstruct Row4;
16633 struct» Row5;
16634 «struct Row6;
16635 ˇ»
16636 struct Row8;
16637 struct Row10;"#},
16638 base_text,
16639 &mut cx,
16640 );
16641
16642 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
16643 assert_hunk_revert(
16644 indoc! {r#"struct Row;
16645 ˇstruct Row2;
16646
16647 struct Row4;
16648 struct Row5;
16649 struct Row6;
16650
16651 struct Row8;ˇ
16652 struct Row10;"#},
16653 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16654 indoc! {r#"struct Row;
16655 struct Row1;
16656 ˇstruct Row2;
16657
16658 struct Row4;
16659 struct Row5;
16660 struct Row6;
16661
16662 struct Row8;ˇ
16663 struct Row9;
16664 struct Row10;"#},
16665 base_text,
16666 &mut cx,
16667 );
16668 assert_hunk_revert(
16669 indoc! {r#"struct Row;
16670 struct Row2«ˇ;
16671 struct Row4;
16672 struct» Row5;
16673 «struct Row6;
16674
16675 struct Row8;ˇ»
16676 struct Row10;"#},
16677 vec![
16678 DiffHunkStatusKind::Deleted,
16679 DiffHunkStatusKind::Deleted,
16680 DiffHunkStatusKind::Deleted,
16681 ],
16682 indoc! {r#"struct Row;
16683 struct Row1;
16684 struct Row2«ˇ;
16685
16686 struct Row4;
16687 struct» Row5;
16688 «struct Row6;
16689
16690 struct Row8;ˇ»
16691 struct Row9;
16692 struct Row10;"#},
16693 base_text,
16694 &mut cx,
16695 );
16696}
16697
16698#[gpui::test]
16699async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
16700 init_test(cx, |_| {});
16701
16702 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
16703 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
16704 let base_text_3 =
16705 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
16706
16707 let text_1 = edit_first_char_of_every_line(base_text_1);
16708 let text_2 = edit_first_char_of_every_line(base_text_2);
16709 let text_3 = edit_first_char_of_every_line(base_text_3);
16710
16711 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
16712 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
16713 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
16714
16715 let multibuffer = cx.new(|cx| {
16716 let mut multibuffer = MultiBuffer::new(ReadWrite);
16717 multibuffer.push_excerpts(
16718 buffer_1.clone(),
16719 [
16720 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16721 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16722 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16723 ],
16724 cx,
16725 );
16726 multibuffer.push_excerpts(
16727 buffer_2.clone(),
16728 [
16729 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16730 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16731 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16732 ],
16733 cx,
16734 );
16735 multibuffer.push_excerpts(
16736 buffer_3.clone(),
16737 [
16738 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16739 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16740 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16741 ],
16742 cx,
16743 );
16744 multibuffer
16745 });
16746
16747 let fs = FakeFs::new(cx.executor());
16748 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
16749 let (editor, cx) = cx
16750 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
16751 editor.update_in(cx, |editor, _window, cx| {
16752 for (buffer, diff_base) in [
16753 (buffer_1.clone(), base_text_1),
16754 (buffer_2.clone(), base_text_2),
16755 (buffer_3.clone(), base_text_3),
16756 ] {
16757 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16758 editor
16759 .buffer
16760 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16761 }
16762 });
16763 cx.executor().run_until_parked();
16764
16765 editor.update_in(cx, |editor, window, cx| {
16766 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}");
16767 editor.select_all(&SelectAll, window, cx);
16768 editor.git_restore(&Default::default(), window, cx);
16769 });
16770 cx.executor().run_until_parked();
16771
16772 // When all ranges are selected, all buffer hunks are reverted.
16773 editor.update(cx, |editor, cx| {
16774 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");
16775 });
16776 buffer_1.update(cx, |buffer, _| {
16777 assert_eq!(buffer.text(), base_text_1);
16778 });
16779 buffer_2.update(cx, |buffer, _| {
16780 assert_eq!(buffer.text(), base_text_2);
16781 });
16782 buffer_3.update(cx, |buffer, _| {
16783 assert_eq!(buffer.text(), base_text_3);
16784 });
16785
16786 editor.update_in(cx, |editor, window, cx| {
16787 editor.undo(&Default::default(), window, cx);
16788 });
16789
16790 editor.update_in(cx, |editor, window, cx| {
16791 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16792 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
16793 });
16794 editor.git_restore(&Default::default(), window, cx);
16795 });
16796
16797 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
16798 // but not affect buffer_2 and its related excerpts.
16799 editor.update(cx, |editor, cx| {
16800 assert_eq!(
16801 editor.text(cx),
16802 "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}"
16803 );
16804 });
16805 buffer_1.update(cx, |buffer, _| {
16806 assert_eq!(buffer.text(), base_text_1);
16807 });
16808 buffer_2.update(cx, |buffer, _| {
16809 assert_eq!(
16810 buffer.text(),
16811 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
16812 );
16813 });
16814 buffer_3.update(cx, |buffer, _| {
16815 assert_eq!(
16816 buffer.text(),
16817 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
16818 );
16819 });
16820
16821 fn edit_first_char_of_every_line(text: &str) -> String {
16822 text.split('\n')
16823 .map(|line| format!("X{}", &line[1..]))
16824 .collect::<Vec<_>>()
16825 .join("\n")
16826 }
16827}
16828
16829#[gpui::test]
16830async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
16831 init_test(cx, |_| {});
16832
16833 let cols = 4;
16834 let rows = 10;
16835 let sample_text_1 = sample_text(rows, cols, 'a');
16836 assert_eq!(
16837 sample_text_1,
16838 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
16839 );
16840 let sample_text_2 = sample_text(rows, cols, 'l');
16841 assert_eq!(
16842 sample_text_2,
16843 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
16844 );
16845 let sample_text_3 = sample_text(rows, cols, 'v');
16846 assert_eq!(
16847 sample_text_3,
16848 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
16849 );
16850
16851 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
16852 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
16853 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
16854
16855 let multi_buffer = cx.new(|cx| {
16856 let mut multibuffer = MultiBuffer::new(ReadWrite);
16857 multibuffer.push_excerpts(
16858 buffer_1.clone(),
16859 [
16860 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16861 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16862 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16863 ],
16864 cx,
16865 );
16866 multibuffer.push_excerpts(
16867 buffer_2.clone(),
16868 [
16869 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16870 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16871 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16872 ],
16873 cx,
16874 );
16875 multibuffer.push_excerpts(
16876 buffer_3.clone(),
16877 [
16878 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16879 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16880 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16881 ],
16882 cx,
16883 );
16884 multibuffer
16885 });
16886
16887 let fs = FakeFs::new(cx.executor());
16888 fs.insert_tree(
16889 "/a",
16890 json!({
16891 "main.rs": sample_text_1,
16892 "other.rs": sample_text_2,
16893 "lib.rs": sample_text_3,
16894 }),
16895 )
16896 .await;
16897 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16898 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16899 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16900 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16901 Editor::new(
16902 EditorMode::full(),
16903 multi_buffer,
16904 Some(project.clone()),
16905 window,
16906 cx,
16907 )
16908 });
16909 let multibuffer_item_id = workspace
16910 .update(cx, |workspace, window, cx| {
16911 assert!(
16912 workspace.active_item(cx).is_none(),
16913 "active item should be None before the first item is added"
16914 );
16915 workspace.add_item_to_active_pane(
16916 Box::new(multi_buffer_editor.clone()),
16917 None,
16918 true,
16919 window,
16920 cx,
16921 );
16922 let active_item = workspace
16923 .active_item(cx)
16924 .expect("should have an active item after adding the multi buffer");
16925 assert!(
16926 !active_item.is_singleton(cx),
16927 "A multi buffer was expected to active after adding"
16928 );
16929 active_item.item_id()
16930 })
16931 .unwrap();
16932 cx.executor().run_until_parked();
16933
16934 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16935 editor.change_selections(
16936 SelectionEffects::scroll(Autoscroll::Next),
16937 window,
16938 cx,
16939 |s| s.select_ranges(Some(1..2)),
16940 );
16941 editor.open_excerpts(&OpenExcerpts, window, cx);
16942 });
16943 cx.executor().run_until_parked();
16944 let first_item_id = workspace
16945 .update(cx, |workspace, window, cx| {
16946 let active_item = workspace
16947 .active_item(cx)
16948 .expect("should have an active item after navigating into the 1st buffer");
16949 let first_item_id = active_item.item_id();
16950 assert_ne!(
16951 first_item_id, multibuffer_item_id,
16952 "Should navigate into the 1st buffer and activate it"
16953 );
16954 assert!(
16955 active_item.is_singleton(cx),
16956 "New active item should be a singleton buffer"
16957 );
16958 assert_eq!(
16959 active_item
16960 .act_as::<Editor>(cx)
16961 .expect("should have navigated into an editor for the 1st buffer")
16962 .read(cx)
16963 .text(cx),
16964 sample_text_1
16965 );
16966
16967 workspace
16968 .go_back(workspace.active_pane().downgrade(), window, cx)
16969 .detach_and_log_err(cx);
16970
16971 first_item_id
16972 })
16973 .unwrap();
16974 cx.executor().run_until_parked();
16975 workspace
16976 .update(cx, |workspace, _, cx| {
16977 let active_item = workspace
16978 .active_item(cx)
16979 .expect("should have an active item after navigating back");
16980 assert_eq!(
16981 active_item.item_id(),
16982 multibuffer_item_id,
16983 "Should navigate back to the multi buffer"
16984 );
16985 assert!(!active_item.is_singleton(cx));
16986 })
16987 .unwrap();
16988
16989 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16990 editor.change_selections(
16991 SelectionEffects::scroll(Autoscroll::Next),
16992 window,
16993 cx,
16994 |s| s.select_ranges(Some(39..40)),
16995 );
16996 editor.open_excerpts(&OpenExcerpts, window, cx);
16997 });
16998 cx.executor().run_until_parked();
16999 let second_item_id = workspace
17000 .update(cx, |workspace, window, cx| {
17001 let active_item = workspace
17002 .active_item(cx)
17003 .expect("should have an active item after navigating into the 2nd buffer");
17004 let second_item_id = active_item.item_id();
17005 assert_ne!(
17006 second_item_id, multibuffer_item_id,
17007 "Should navigate away from the multibuffer"
17008 );
17009 assert_ne!(
17010 second_item_id, first_item_id,
17011 "Should navigate into the 2nd buffer and activate it"
17012 );
17013 assert!(
17014 active_item.is_singleton(cx),
17015 "New active item should be a singleton buffer"
17016 );
17017 assert_eq!(
17018 active_item
17019 .act_as::<Editor>(cx)
17020 .expect("should have navigated into an editor")
17021 .read(cx)
17022 .text(cx),
17023 sample_text_2
17024 );
17025
17026 workspace
17027 .go_back(workspace.active_pane().downgrade(), window, cx)
17028 .detach_and_log_err(cx);
17029
17030 second_item_id
17031 })
17032 .unwrap();
17033 cx.executor().run_until_parked();
17034 workspace
17035 .update(cx, |workspace, _, cx| {
17036 let active_item = workspace
17037 .active_item(cx)
17038 .expect("should have an active item after navigating back from the 2nd buffer");
17039 assert_eq!(
17040 active_item.item_id(),
17041 multibuffer_item_id,
17042 "Should navigate back from the 2nd buffer to the multi buffer"
17043 );
17044 assert!(!active_item.is_singleton(cx));
17045 })
17046 .unwrap();
17047
17048 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17049 editor.change_selections(
17050 SelectionEffects::scroll(Autoscroll::Next),
17051 window,
17052 cx,
17053 |s| s.select_ranges(Some(70..70)),
17054 );
17055 editor.open_excerpts(&OpenExcerpts, window, cx);
17056 });
17057 cx.executor().run_until_parked();
17058 workspace
17059 .update(cx, |workspace, window, cx| {
17060 let active_item = workspace
17061 .active_item(cx)
17062 .expect("should have an active item after navigating into the 3rd buffer");
17063 let third_item_id = active_item.item_id();
17064 assert_ne!(
17065 third_item_id, multibuffer_item_id,
17066 "Should navigate into the 3rd buffer and activate it"
17067 );
17068 assert_ne!(third_item_id, first_item_id);
17069 assert_ne!(third_item_id, second_item_id);
17070 assert!(
17071 active_item.is_singleton(cx),
17072 "New active item should be a singleton buffer"
17073 );
17074 assert_eq!(
17075 active_item
17076 .act_as::<Editor>(cx)
17077 .expect("should have navigated into an editor")
17078 .read(cx)
17079 .text(cx),
17080 sample_text_3
17081 );
17082
17083 workspace
17084 .go_back(workspace.active_pane().downgrade(), window, cx)
17085 .detach_and_log_err(cx);
17086 })
17087 .unwrap();
17088 cx.executor().run_until_parked();
17089 workspace
17090 .update(cx, |workspace, _, cx| {
17091 let active_item = workspace
17092 .active_item(cx)
17093 .expect("should have an active item after navigating back from the 3rd buffer");
17094 assert_eq!(
17095 active_item.item_id(),
17096 multibuffer_item_id,
17097 "Should navigate back from the 3rd buffer to the multi buffer"
17098 );
17099 assert!(!active_item.is_singleton(cx));
17100 })
17101 .unwrap();
17102}
17103
17104#[gpui::test]
17105async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17106 init_test(cx, |_| {});
17107
17108 let mut cx = EditorTestContext::new(cx).await;
17109
17110 let diff_base = r#"
17111 use some::mod;
17112
17113 const A: u32 = 42;
17114
17115 fn main() {
17116 println!("hello");
17117
17118 println!("world");
17119 }
17120 "#
17121 .unindent();
17122
17123 cx.set_state(
17124 &r#"
17125 use some::modified;
17126
17127 ˇ
17128 fn main() {
17129 println!("hello there");
17130
17131 println!("around the");
17132 println!("world");
17133 }
17134 "#
17135 .unindent(),
17136 );
17137
17138 cx.set_head_text(&diff_base);
17139 executor.run_until_parked();
17140
17141 cx.update_editor(|editor, window, cx| {
17142 editor.go_to_next_hunk(&GoToHunk, window, cx);
17143 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17144 });
17145 executor.run_until_parked();
17146 cx.assert_state_with_diff(
17147 r#"
17148 use some::modified;
17149
17150
17151 fn main() {
17152 - println!("hello");
17153 + ˇ println!("hello there");
17154
17155 println!("around the");
17156 println!("world");
17157 }
17158 "#
17159 .unindent(),
17160 );
17161
17162 cx.update_editor(|editor, window, cx| {
17163 for _ in 0..2 {
17164 editor.go_to_next_hunk(&GoToHunk, window, cx);
17165 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17166 }
17167 });
17168 executor.run_until_parked();
17169 cx.assert_state_with_diff(
17170 r#"
17171 - use some::mod;
17172 + ˇuse some::modified;
17173
17174
17175 fn main() {
17176 - println!("hello");
17177 + println!("hello there");
17178
17179 + println!("around the");
17180 println!("world");
17181 }
17182 "#
17183 .unindent(),
17184 );
17185
17186 cx.update_editor(|editor, window, cx| {
17187 editor.go_to_next_hunk(&GoToHunk, window, cx);
17188 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17189 });
17190 executor.run_until_parked();
17191 cx.assert_state_with_diff(
17192 r#"
17193 - use some::mod;
17194 + use some::modified;
17195
17196 - const A: u32 = 42;
17197 ˇ
17198 fn main() {
17199 - println!("hello");
17200 + println!("hello there");
17201
17202 + println!("around the");
17203 println!("world");
17204 }
17205 "#
17206 .unindent(),
17207 );
17208
17209 cx.update_editor(|editor, window, cx| {
17210 editor.cancel(&Cancel, window, cx);
17211 });
17212
17213 cx.assert_state_with_diff(
17214 r#"
17215 use some::modified;
17216
17217 ˇ
17218 fn main() {
17219 println!("hello there");
17220
17221 println!("around the");
17222 println!("world");
17223 }
17224 "#
17225 .unindent(),
17226 );
17227}
17228
17229#[gpui::test]
17230async fn test_diff_base_change_with_expanded_diff_hunks(
17231 executor: BackgroundExecutor,
17232 cx: &mut TestAppContext,
17233) {
17234 init_test(cx, |_| {});
17235
17236 let mut cx = EditorTestContext::new(cx).await;
17237
17238 let diff_base = r#"
17239 use some::mod1;
17240 use some::mod2;
17241
17242 const A: u32 = 42;
17243 const B: u32 = 42;
17244 const C: u32 = 42;
17245
17246 fn main() {
17247 println!("hello");
17248
17249 println!("world");
17250 }
17251 "#
17252 .unindent();
17253
17254 cx.set_state(
17255 &r#"
17256 use some::mod2;
17257
17258 const A: u32 = 42;
17259 const C: u32 = 42;
17260
17261 fn main(ˇ) {
17262 //println!("hello");
17263
17264 println!("world");
17265 //
17266 //
17267 }
17268 "#
17269 .unindent(),
17270 );
17271
17272 cx.set_head_text(&diff_base);
17273 executor.run_until_parked();
17274
17275 cx.update_editor(|editor, window, cx| {
17276 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17277 });
17278 executor.run_until_parked();
17279 cx.assert_state_with_diff(
17280 r#"
17281 - use some::mod1;
17282 use some::mod2;
17283
17284 const A: u32 = 42;
17285 - const B: u32 = 42;
17286 const C: u32 = 42;
17287
17288 fn main(ˇ) {
17289 - println!("hello");
17290 + //println!("hello");
17291
17292 println!("world");
17293 + //
17294 + //
17295 }
17296 "#
17297 .unindent(),
17298 );
17299
17300 cx.set_head_text("new diff base!");
17301 executor.run_until_parked();
17302 cx.assert_state_with_diff(
17303 r#"
17304 - new diff base!
17305 + use some::mod2;
17306 +
17307 + const A: u32 = 42;
17308 + const C: u32 = 42;
17309 +
17310 + fn main(ˇ) {
17311 + //println!("hello");
17312 +
17313 + println!("world");
17314 + //
17315 + //
17316 + }
17317 "#
17318 .unindent(),
17319 );
17320}
17321
17322#[gpui::test]
17323async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
17324 init_test(cx, |_| {});
17325
17326 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17327 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17328 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17329 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17330 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
17331 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
17332
17333 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
17334 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
17335 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
17336
17337 let multi_buffer = cx.new(|cx| {
17338 let mut multibuffer = MultiBuffer::new(ReadWrite);
17339 multibuffer.push_excerpts(
17340 buffer_1.clone(),
17341 [
17342 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17343 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17344 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17345 ],
17346 cx,
17347 );
17348 multibuffer.push_excerpts(
17349 buffer_2.clone(),
17350 [
17351 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17352 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17353 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17354 ],
17355 cx,
17356 );
17357 multibuffer.push_excerpts(
17358 buffer_3.clone(),
17359 [
17360 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17361 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17362 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17363 ],
17364 cx,
17365 );
17366 multibuffer
17367 });
17368
17369 let editor =
17370 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17371 editor
17372 .update(cx, |editor, _window, cx| {
17373 for (buffer, diff_base) in [
17374 (buffer_1.clone(), file_1_old),
17375 (buffer_2.clone(), file_2_old),
17376 (buffer_3.clone(), file_3_old),
17377 ] {
17378 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
17379 editor
17380 .buffer
17381 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
17382 }
17383 })
17384 .unwrap();
17385
17386 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17387 cx.run_until_parked();
17388
17389 cx.assert_editor_state(
17390 &"
17391 ˇaaa
17392 ccc
17393 ddd
17394
17395 ggg
17396 hhh
17397
17398
17399 lll
17400 mmm
17401 NNN
17402
17403 qqq
17404 rrr
17405
17406 uuu
17407 111
17408 222
17409 333
17410
17411 666
17412 777
17413
17414 000
17415 !!!"
17416 .unindent(),
17417 );
17418
17419 cx.update_editor(|editor, window, cx| {
17420 editor.select_all(&SelectAll, window, cx);
17421 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17422 });
17423 cx.executor().run_until_parked();
17424
17425 cx.assert_state_with_diff(
17426 "
17427 «aaa
17428 - bbb
17429 ccc
17430 ddd
17431
17432 ggg
17433 hhh
17434
17435
17436 lll
17437 mmm
17438 - nnn
17439 + NNN
17440
17441 qqq
17442 rrr
17443
17444 uuu
17445 111
17446 222
17447 333
17448
17449 + 666
17450 777
17451
17452 000
17453 !!!ˇ»"
17454 .unindent(),
17455 );
17456}
17457
17458#[gpui::test]
17459async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
17460 init_test(cx, |_| {});
17461
17462 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
17463 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
17464
17465 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
17466 let multi_buffer = cx.new(|cx| {
17467 let mut multibuffer = MultiBuffer::new(ReadWrite);
17468 multibuffer.push_excerpts(
17469 buffer.clone(),
17470 [
17471 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
17472 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
17473 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
17474 ],
17475 cx,
17476 );
17477 multibuffer
17478 });
17479
17480 let editor =
17481 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17482 editor
17483 .update(cx, |editor, _window, cx| {
17484 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
17485 editor
17486 .buffer
17487 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
17488 })
17489 .unwrap();
17490
17491 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17492 cx.run_until_parked();
17493
17494 cx.update_editor(|editor, window, cx| {
17495 editor.expand_all_diff_hunks(&Default::default(), window, cx)
17496 });
17497 cx.executor().run_until_parked();
17498
17499 // When the start of a hunk coincides with the start of its excerpt,
17500 // the hunk is expanded. When the start of a a hunk is earlier than
17501 // the start of its excerpt, the hunk is not expanded.
17502 cx.assert_state_with_diff(
17503 "
17504 ˇaaa
17505 - bbb
17506 + BBB
17507
17508 - ddd
17509 - eee
17510 + DDD
17511 + EEE
17512 fff
17513
17514 iii
17515 "
17516 .unindent(),
17517 );
17518}
17519
17520#[gpui::test]
17521async fn test_edits_around_expanded_insertion_hunks(
17522 executor: BackgroundExecutor,
17523 cx: &mut TestAppContext,
17524) {
17525 init_test(cx, |_| {});
17526
17527 let mut cx = EditorTestContext::new(cx).await;
17528
17529 let diff_base = r#"
17530 use some::mod1;
17531 use some::mod2;
17532
17533 const A: u32 = 42;
17534
17535 fn main() {
17536 println!("hello");
17537
17538 println!("world");
17539 }
17540 "#
17541 .unindent();
17542 executor.run_until_parked();
17543 cx.set_state(
17544 &r#"
17545 use some::mod1;
17546 use some::mod2;
17547
17548 const A: u32 = 42;
17549 const B: u32 = 42;
17550 const C: u32 = 42;
17551 ˇ
17552
17553 fn main() {
17554 println!("hello");
17555
17556 println!("world");
17557 }
17558 "#
17559 .unindent(),
17560 );
17561
17562 cx.set_head_text(&diff_base);
17563 executor.run_until_parked();
17564
17565 cx.update_editor(|editor, window, cx| {
17566 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17567 });
17568 executor.run_until_parked();
17569
17570 cx.assert_state_with_diff(
17571 r#"
17572 use some::mod1;
17573 use some::mod2;
17574
17575 const A: u32 = 42;
17576 + const B: u32 = 42;
17577 + const C: u32 = 42;
17578 + ˇ
17579
17580 fn main() {
17581 println!("hello");
17582
17583 println!("world");
17584 }
17585 "#
17586 .unindent(),
17587 );
17588
17589 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
17590 executor.run_until_parked();
17591
17592 cx.assert_state_with_diff(
17593 r#"
17594 use some::mod1;
17595 use some::mod2;
17596
17597 const A: u32 = 42;
17598 + const B: u32 = 42;
17599 + const C: u32 = 42;
17600 + const D: u32 = 42;
17601 + ˇ
17602
17603 fn main() {
17604 println!("hello");
17605
17606 println!("world");
17607 }
17608 "#
17609 .unindent(),
17610 );
17611
17612 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
17613 executor.run_until_parked();
17614
17615 cx.assert_state_with_diff(
17616 r#"
17617 use some::mod1;
17618 use some::mod2;
17619
17620 const A: u32 = 42;
17621 + const B: u32 = 42;
17622 + const C: u32 = 42;
17623 + const D: u32 = 42;
17624 + const E: u32 = 42;
17625 + ˇ
17626
17627 fn main() {
17628 println!("hello");
17629
17630 println!("world");
17631 }
17632 "#
17633 .unindent(),
17634 );
17635
17636 cx.update_editor(|editor, window, cx| {
17637 editor.delete_line(&DeleteLine, window, cx);
17638 });
17639 executor.run_until_parked();
17640
17641 cx.assert_state_with_diff(
17642 r#"
17643 use some::mod1;
17644 use some::mod2;
17645
17646 const A: u32 = 42;
17647 + const B: u32 = 42;
17648 + const C: u32 = 42;
17649 + const D: u32 = 42;
17650 + const E: u32 = 42;
17651 ˇ
17652 fn main() {
17653 println!("hello");
17654
17655 println!("world");
17656 }
17657 "#
17658 .unindent(),
17659 );
17660
17661 cx.update_editor(|editor, window, cx| {
17662 editor.move_up(&MoveUp, window, cx);
17663 editor.delete_line(&DeleteLine, window, cx);
17664 editor.move_up(&MoveUp, window, cx);
17665 editor.delete_line(&DeleteLine, window, cx);
17666 editor.move_up(&MoveUp, window, cx);
17667 editor.delete_line(&DeleteLine, window, cx);
17668 });
17669 executor.run_until_parked();
17670 cx.assert_state_with_diff(
17671 r#"
17672 use some::mod1;
17673 use some::mod2;
17674
17675 const A: u32 = 42;
17676 + const B: u32 = 42;
17677 ˇ
17678 fn main() {
17679 println!("hello");
17680
17681 println!("world");
17682 }
17683 "#
17684 .unindent(),
17685 );
17686
17687 cx.update_editor(|editor, window, cx| {
17688 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
17689 editor.delete_line(&DeleteLine, window, cx);
17690 });
17691 executor.run_until_parked();
17692 cx.assert_state_with_diff(
17693 r#"
17694 ˇ
17695 fn main() {
17696 println!("hello");
17697
17698 println!("world");
17699 }
17700 "#
17701 .unindent(),
17702 );
17703}
17704
17705#[gpui::test]
17706async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
17707 init_test(cx, |_| {});
17708
17709 let mut cx = EditorTestContext::new(cx).await;
17710 cx.set_head_text(indoc! { "
17711 one
17712 two
17713 three
17714 four
17715 five
17716 "
17717 });
17718 cx.set_state(indoc! { "
17719 one
17720 ˇthree
17721 five
17722 "});
17723 cx.run_until_parked();
17724 cx.update_editor(|editor, window, cx| {
17725 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17726 });
17727 cx.assert_state_with_diff(
17728 indoc! { "
17729 one
17730 - two
17731 ˇthree
17732 - four
17733 five
17734 "}
17735 .to_string(),
17736 );
17737 cx.update_editor(|editor, window, cx| {
17738 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17739 });
17740
17741 cx.assert_state_with_diff(
17742 indoc! { "
17743 one
17744 ˇthree
17745 five
17746 "}
17747 .to_string(),
17748 );
17749
17750 cx.set_state(indoc! { "
17751 one
17752 ˇTWO
17753 three
17754 four
17755 five
17756 "});
17757 cx.run_until_parked();
17758 cx.update_editor(|editor, window, cx| {
17759 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17760 });
17761
17762 cx.assert_state_with_diff(
17763 indoc! { "
17764 one
17765 - two
17766 + ˇTWO
17767 three
17768 four
17769 five
17770 "}
17771 .to_string(),
17772 );
17773 cx.update_editor(|editor, window, cx| {
17774 editor.move_up(&Default::default(), window, cx);
17775 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17776 });
17777 cx.assert_state_with_diff(
17778 indoc! { "
17779 one
17780 ˇTWO
17781 three
17782 four
17783 five
17784 "}
17785 .to_string(),
17786 );
17787}
17788
17789#[gpui::test]
17790async fn test_edits_around_expanded_deletion_hunks(
17791 executor: BackgroundExecutor,
17792 cx: &mut TestAppContext,
17793) {
17794 init_test(cx, |_| {});
17795
17796 let mut cx = EditorTestContext::new(cx).await;
17797
17798 let diff_base = r#"
17799 use some::mod1;
17800 use some::mod2;
17801
17802 const A: u32 = 42;
17803 const B: u32 = 42;
17804 const C: u32 = 42;
17805
17806
17807 fn main() {
17808 println!("hello");
17809
17810 println!("world");
17811 }
17812 "#
17813 .unindent();
17814 executor.run_until_parked();
17815 cx.set_state(
17816 &r#"
17817 use some::mod1;
17818 use some::mod2;
17819
17820 ˇconst B: u32 = 42;
17821 const C: u32 = 42;
17822
17823
17824 fn main() {
17825 println!("hello");
17826
17827 println!("world");
17828 }
17829 "#
17830 .unindent(),
17831 );
17832
17833 cx.set_head_text(&diff_base);
17834 executor.run_until_parked();
17835
17836 cx.update_editor(|editor, window, cx| {
17837 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17838 });
17839 executor.run_until_parked();
17840
17841 cx.assert_state_with_diff(
17842 r#"
17843 use some::mod1;
17844 use some::mod2;
17845
17846 - const A: u32 = 42;
17847 ˇconst B: u32 = 42;
17848 const C: u32 = 42;
17849
17850
17851 fn main() {
17852 println!("hello");
17853
17854 println!("world");
17855 }
17856 "#
17857 .unindent(),
17858 );
17859
17860 cx.update_editor(|editor, window, cx| {
17861 editor.delete_line(&DeleteLine, window, cx);
17862 });
17863 executor.run_until_parked();
17864 cx.assert_state_with_diff(
17865 r#"
17866 use some::mod1;
17867 use some::mod2;
17868
17869 - const A: u32 = 42;
17870 - const B: u32 = 42;
17871 ˇconst C: u32 = 42;
17872
17873
17874 fn main() {
17875 println!("hello");
17876
17877 println!("world");
17878 }
17879 "#
17880 .unindent(),
17881 );
17882
17883 cx.update_editor(|editor, window, cx| {
17884 editor.delete_line(&DeleteLine, window, cx);
17885 });
17886 executor.run_until_parked();
17887 cx.assert_state_with_diff(
17888 r#"
17889 use some::mod1;
17890 use some::mod2;
17891
17892 - const A: u32 = 42;
17893 - const B: u32 = 42;
17894 - const C: u32 = 42;
17895 ˇ
17896
17897 fn main() {
17898 println!("hello");
17899
17900 println!("world");
17901 }
17902 "#
17903 .unindent(),
17904 );
17905
17906 cx.update_editor(|editor, window, cx| {
17907 editor.handle_input("replacement", window, cx);
17908 });
17909 executor.run_until_parked();
17910 cx.assert_state_with_diff(
17911 r#"
17912 use some::mod1;
17913 use some::mod2;
17914
17915 - const A: u32 = 42;
17916 - const B: u32 = 42;
17917 - const C: u32 = 42;
17918 -
17919 + replacementˇ
17920
17921 fn main() {
17922 println!("hello");
17923
17924 println!("world");
17925 }
17926 "#
17927 .unindent(),
17928 );
17929}
17930
17931#[gpui::test]
17932async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17933 init_test(cx, |_| {});
17934
17935 let mut cx = EditorTestContext::new(cx).await;
17936
17937 let base_text = r#"
17938 one
17939 two
17940 three
17941 four
17942 five
17943 "#
17944 .unindent();
17945 executor.run_until_parked();
17946 cx.set_state(
17947 &r#"
17948 one
17949 two
17950 fˇour
17951 five
17952 "#
17953 .unindent(),
17954 );
17955
17956 cx.set_head_text(&base_text);
17957 executor.run_until_parked();
17958
17959 cx.update_editor(|editor, window, cx| {
17960 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17961 });
17962 executor.run_until_parked();
17963
17964 cx.assert_state_with_diff(
17965 r#"
17966 one
17967 two
17968 - three
17969 fˇour
17970 five
17971 "#
17972 .unindent(),
17973 );
17974
17975 cx.update_editor(|editor, window, cx| {
17976 editor.backspace(&Backspace, window, cx);
17977 editor.backspace(&Backspace, window, cx);
17978 });
17979 executor.run_until_parked();
17980 cx.assert_state_with_diff(
17981 r#"
17982 one
17983 two
17984 - threeˇ
17985 - four
17986 + our
17987 five
17988 "#
17989 .unindent(),
17990 );
17991}
17992
17993#[gpui::test]
17994async fn test_edit_after_expanded_modification_hunk(
17995 executor: BackgroundExecutor,
17996 cx: &mut TestAppContext,
17997) {
17998 init_test(cx, |_| {});
17999
18000 let mut cx = EditorTestContext::new(cx).await;
18001
18002 let diff_base = r#"
18003 use some::mod1;
18004 use some::mod2;
18005
18006 const A: u32 = 42;
18007 const B: u32 = 42;
18008 const C: u32 = 42;
18009 const D: u32 = 42;
18010
18011
18012 fn main() {
18013 println!("hello");
18014
18015 println!("world");
18016 }"#
18017 .unindent();
18018
18019 cx.set_state(
18020 &r#"
18021 use some::mod1;
18022 use some::mod2;
18023
18024 const A: u32 = 42;
18025 const B: u32 = 42;
18026 const C: u32 = 43ˇ
18027 const D: u32 = 42;
18028
18029
18030 fn main() {
18031 println!("hello");
18032
18033 println!("world");
18034 }"#
18035 .unindent(),
18036 );
18037
18038 cx.set_head_text(&diff_base);
18039 executor.run_until_parked();
18040 cx.update_editor(|editor, window, cx| {
18041 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18042 });
18043 executor.run_until_parked();
18044
18045 cx.assert_state_with_diff(
18046 r#"
18047 use some::mod1;
18048 use some::mod2;
18049
18050 const A: u32 = 42;
18051 const B: u32 = 42;
18052 - const C: u32 = 42;
18053 + const C: u32 = 43ˇ
18054 const D: u32 = 42;
18055
18056
18057 fn main() {
18058 println!("hello");
18059
18060 println!("world");
18061 }"#
18062 .unindent(),
18063 );
18064
18065 cx.update_editor(|editor, window, cx| {
18066 editor.handle_input("\nnew_line\n", window, cx);
18067 });
18068 executor.run_until_parked();
18069
18070 cx.assert_state_with_diff(
18071 r#"
18072 use some::mod1;
18073 use some::mod2;
18074
18075 const A: u32 = 42;
18076 const B: u32 = 42;
18077 - const C: u32 = 42;
18078 + const C: u32 = 43
18079 + new_line
18080 + ˇ
18081 const D: u32 = 42;
18082
18083
18084 fn main() {
18085 println!("hello");
18086
18087 println!("world");
18088 }"#
18089 .unindent(),
18090 );
18091}
18092
18093#[gpui::test]
18094async fn test_stage_and_unstage_added_file_hunk(
18095 executor: BackgroundExecutor,
18096 cx: &mut TestAppContext,
18097) {
18098 init_test(cx, |_| {});
18099
18100 let mut cx = EditorTestContext::new(cx).await;
18101 cx.update_editor(|editor, _, cx| {
18102 editor.set_expand_all_diff_hunks(cx);
18103 });
18104
18105 let working_copy = r#"
18106 ˇfn main() {
18107 println!("hello, world!");
18108 }
18109 "#
18110 .unindent();
18111
18112 cx.set_state(&working_copy);
18113 executor.run_until_parked();
18114
18115 cx.assert_state_with_diff(
18116 r#"
18117 + ˇfn main() {
18118 + println!("hello, world!");
18119 + }
18120 "#
18121 .unindent(),
18122 );
18123 cx.assert_index_text(None);
18124
18125 cx.update_editor(|editor, window, cx| {
18126 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18127 });
18128 executor.run_until_parked();
18129 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
18130 cx.assert_state_with_diff(
18131 r#"
18132 + ˇfn main() {
18133 + println!("hello, world!");
18134 + }
18135 "#
18136 .unindent(),
18137 );
18138
18139 cx.update_editor(|editor, window, cx| {
18140 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18141 });
18142 executor.run_until_parked();
18143 cx.assert_index_text(None);
18144}
18145
18146async fn setup_indent_guides_editor(
18147 text: &str,
18148 cx: &mut TestAppContext,
18149) -> (BufferId, EditorTestContext) {
18150 init_test(cx, |_| {});
18151
18152 let mut cx = EditorTestContext::new(cx).await;
18153
18154 let buffer_id = cx.update_editor(|editor, window, cx| {
18155 editor.set_text(text, window, cx);
18156 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
18157
18158 buffer_ids[0]
18159 });
18160
18161 (buffer_id, cx)
18162}
18163
18164fn assert_indent_guides(
18165 range: Range<u32>,
18166 expected: Vec<IndentGuide>,
18167 active_indices: Option<Vec<usize>>,
18168 cx: &mut EditorTestContext,
18169) {
18170 let indent_guides = cx.update_editor(|editor, window, cx| {
18171 let snapshot = editor.snapshot(window, cx).display_snapshot;
18172 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
18173 editor,
18174 MultiBufferRow(range.start)..MultiBufferRow(range.end),
18175 true,
18176 &snapshot,
18177 cx,
18178 );
18179
18180 indent_guides.sort_by(|a, b| {
18181 a.depth.cmp(&b.depth).then(
18182 a.start_row
18183 .cmp(&b.start_row)
18184 .then(a.end_row.cmp(&b.end_row)),
18185 )
18186 });
18187 indent_guides
18188 });
18189
18190 if let Some(expected) = active_indices {
18191 let active_indices = cx.update_editor(|editor, window, cx| {
18192 let snapshot = editor.snapshot(window, cx).display_snapshot;
18193 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
18194 });
18195
18196 assert_eq!(
18197 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
18198 expected,
18199 "Active indent guide indices do not match"
18200 );
18201 }
18202
18203 assert_eq!(indent_guides, expected, "Indent guides do not match");
18204}
18205
18206fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
18207 IndentGuide {
18208 buffer_id,
18209 start_row: MultiBufferRow(start_row),
18210 end_row: MultiBufferRow(end_row),
18211 depth,
18212 tab_size: 4,
18213 settings: IndentGuideSettings {
18214 enabled: true,
18215 line_width: 1,
18216 active_line_width: 1,
18217 ..Default::default()
18218 },
18219 }
18220}
18221
18222#[gpui::test]
18223async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
18224 let (buffer_id, mut cx) = setup_indent_guides_editor(
18225 &"
18226 fn main() {
18227 let a = 1;
18228 }"
18229 .unindent(),
18230 cx,
18231 )
18232 .await;
18233
18234 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18235}
18236
18237#[gpui::test]
18238async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
18239 let (buffer_id, mut cx) = setup_indent_guides_editor(
18240 &"
18241 fn main() {
18242 let a = 1;
18243 let b = 2;
18244 }"
18245 .unindent(),
18246 cx,
18247 )
18248 .await;
18249
18250 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
18251}
18252
18253#[gpui::test]
18254async fn test_indent_guide_nested(cx: &mut TestAppContext) {
18255 let (buffer_id, mut cx) = setup_indent_guides_editor(
18256 &"
18257 fn main() {
18258 let a = 1;
18259 if a == 3 {
18260 let b = 2;
18261 } else {
18262 let c = 3;
18263 }
18264 }"
18265 .unindent(),
18266 cx,
18267 )
18268 .await;
18269
18270 assert_indent_guides(
18271 0..8,
18272 vec![
18273 indent_guide(buffer_id, 1, 6, 0),
18274 indent_guide(buffer_id, 3, 3, 1),
18275 indent_guide(buffer_id, 5, 5, 1),
18276 ],
18277 None,
18278 &mut cx,
18279 );
18280}
18281
18282#[gpui::test]
18283async fn test_indent_guide_tab(cx: &mut TestAppContext) {
18284 let (buffer_id, mut cx) = setup_indent_guides_editor(
18285 &"
18286 fn main() {
18287 let a = 1;
18288 let b = 2;
18289 let c = 3;
18290 }"
18291 .unindent(),
18292 cx,
18293 )
18294 .await;
18295
18296 assert_indent_guides(
18297 0..5,
18298 vec![
18299 indent_guide(buffer_id, 1, 3, 0),
18300 indent_guide(buffer_id, 2, 2, 1),
18301 ],
18302 None,
18303 &mut cx,
18304 );
18305}
18306
18307#[gpui::test]
18308async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
18309 let (buffer_id, mut cx) = setup_indent_guides_editor(
18310 &"
18311 fn main() {
18312 let a = 1;
18313
18314 let c = 3;
18315 }"
18316 .unindent(),
18317 cx,
18318 )
18319 .await;
18320
18321 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
18322}
18323
18324#[gpui::test]
18325async fn test_indent_guide_complex(cx: &mut TestAppContext) {
18326 let (buffer_id, mut cx) = setup_indent_guides_editor(
18327 &"
18328 fn main() {
18329 let a = 1;
18330
18331 let c = 3;
18332
18333 if a == 3 {
18334 let b = 2;
18335 } else {
18336 let c = 3;
18337 }
18338 }"
18339 .unindent(),
18340 cx,
18341 )
18342 .await;
18343
18344 assert_indent_guides(
18345 0..11,
18346 vec![
18347 indent_guide(buffer_id, 1, 9, 0),
18348 indent_guide(buffer_id, 6, 6, 1),
18349 indent_guide(buffer_id, 8, 8, 1),
18350 ],
18351 None,
18352 &mut cx,
18353 );
18354}
18355
18356#[gpui::test]
18357async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
18358 let (buffer_id, mut cx) = setup_indent_guides_editor(
18359 &"
18360 fn main() {
18361 let a = 1;
18362
18363 let c = 3;
18364
18365 if a == 3 {
18366 let b = 2;
18367 } else {
18368 let c = 3;
18369 }
18370 }"
18371 .unindent(),
18372 cx,
18373 )
18374 .await;
18375
18376 assert_indent_guides(
18377 1..11,
18378 vec![
18379 indent_guide(buffer_id, 1, 9, 0),
18380 indent_guide(buffer_id, 6, 6, 1),
18381 indent_guide(buffer_id, 8, 8, 1),
18382 ],
18383 None,
18384 &mut cx,
18385 );
18386}
18387
18388#[gpui::test]
18389async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
18390 let (buffer_id, mut cx) = setup_indent_guides_editor(
18391 &"
18392 fn main() {
18393 let a = 1;
18394
18395 let c = 3;
18396
18397 if a == 3 {
18398 let b = 2;
18399 } else {
18400 let c = 3;
18401 }
18402 }"
18403 .unindent(),
18404 cx,
18405 )
18406 .await;
18407
18408 assert_indent_guides(
18409 1..10,
18410 vec![
18411 indent_guide(buffer_id, 1, 9, 0),
18412 indent_guide(buffer_id, 6, 6, 1),
18413 indent_guide(buffer_id, 8, 8, 1),
18414 ],
18415 None,
18416 &mut cx,
18417 );
18418}
18419
18420#[gpui::test]
18421async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
18422 let (buffer_id, mut cx) = setup_indent_guides_editor(
18423 &"
18424 fn main() {
18425 if a {
18426 b(
18427 c,
18428 d,
18429 )
18430 } else {
18431 e(
18432 f
18433 )
18434 }
18435 }"
18436 .unindent(),
18437 cx,
18438 )
18439 .await;
18440
18441 assert_indent_guides(
18442 0..11,
18443 vec![
18444 indent_guide(buffer_id, 1, 10, 0),
18445 indent_guide(buffer_id, 2, 5, 1),
18446 indent_guide(buffer_id, 7, 9, 1),
18447 indent_guide(buffer_id, 3, 4, 2),
18448 indent_guide(buffer_id, 8, 8, 2),
18449 ],
18450 None,
18451 &mut cx,
18452 );
18453
18454 cx.update_editor(|editor, window, cx| {
18455 editor.fold_at(MultiBufferRow(2), window, cx);
18456 assert_eq!(
18457 editor.display_text(cx),
18458 "
18459 fn main() {
18460 if a {
18461 b(⋯
18462 )
18463 } else {
18464 e(
18465 f
18466 )
18467 }
18468 }"
18469 .unindent()
18470 );
18471 });
18472
18473 assert_indent_guides(
18474 0..11,
18475 vec![
18476 indent_guide(buffer_id, 1, 10, 0),
18477 indent_guide(buffer_id, 2, 5, 1),
18478 indent_guide(buffer_id, 7, 9, 1),
18479 indent_guide(buffer_id, 8, 8, 2),
18480 ],
18481 None,
18482 &mut cx,
18483 );
18484}
18485
18486#[gpui::test]
18487async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
18488 let (buffer_id, mut cx) = setup_indent_guides_editor(
18489 &"
18490 block1
18491 block2
18492 block3
18493 block4
18494 block2
18495 block1
18496 block1"
18497 .unindent(),
18498 cx,
18499 )
18500 .await;
18501
18502 assert_indent_guides(
18503 1..10,
18504 vec![
18505 indent_guide(buffer_id, 1, 4, 0),
18506 indent_guide(buffer_id, 2, 3, 1),
18507 indent_guide(buffer_id, 3, 3, 2),
18508 ],
18509 None,
18510 &mut cx,
18511 );
18512}
18513
18514#[gpui::test]
18515async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
18516 let (buffer_id, mut cx) = setup_indent_guides_editor(
18517 &"
18518 block1
18519 block2
18520 block3
18521
18522 block1
18523 block1"
18524 .unindent(),
18525 cx,
18526 )
18527 .await;
18528
18529 assert_indent_guides(
18530 0..6,
18531 vec![
18532 indent_guide(buffer_id, 1, 2, 0),
18533 indent_guide(buffer_id, 2, 2, 1),
18534 ],
18535 None,
18536 &mut cx,
18537 );
18538}
18539
18540#[gpui::test]
18541async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
18542 let (buffer_id, mut cx) = setup_indent_guides_editor(
18543 &"
18544 function component() {
18545 \treturn (
18546 \t\t\t
18547 \t\t<div>
18548 \t\t\t<abc></abc>
18549 \t\t</div>
18550 \t)
18551 }"
18552 .unindent(),
18553 cx,
18554 )
18555 .await;
18556
18557 assert_indent_guides(
18558 0..8,
18559 vec![
18560 indent_guide(buffer_id, 1, 6, 0),
18561 indent_guide(buffer_id, 2, 5, 1),
18562 indent_guide(buffer_id, 4, 4, 2),
18563 ],
18564 None,
18565 &mut cx,
18566 );
18567}
18568
18569#[gpui::test]
18570async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
18571 let (buffer_id, mut cx) = setup_indent_guides_editor(
18572 &"
18573 function component() {
18574 \treturn (
18575 \t
18576 \t\t<div>
18577 \t\t\t<abc></abc>
18578 \t\t</div>
18579 \t)
18580 }"
18581 .unindent(),
18582 cx,
18583 )
18584 .await;
18585
18586 assert_indent_guides(
18587 0..8,
18588 vec![
18589 indent_guide(buffer_id, 1, 6, 0),
18590 indent_guide(buffer_id, 2, 5, 1),
18591 indent_guide(buffer_id, 4, 4, 2),
18592 ],
18593 None,
18594 &mut cx,
18595 );
18596}
18597
18598#[gpui::test]
18599async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
18600 let (buffer_id, mut cx) = setup_indent_guides_editor(
18601 &"
18602 block1
18603
18604
18605
18606 block2
18607 "
18608 .unindent(),
18609 cx,
18610 )
18611 .await;
18612
18613 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18614}
18615
18616#[gpui::test]
18617async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
18618 let (buffer_id, mut cx) = setup_indent_guides_editor(
18619 &"
18620 def a:
18621 \tb = 3
18622 \tif True:
18623 \t\tc = 4
18624 \t\td = 5
18625 \tprint(b)
18626 "
18627 .unindent(),
18628 cx,
18629 )
18630 .await;
18631
18632 assert_indent_guides(
18633 0..6,
18634 vec![
18635 indent_guide(buffer_id, 1, 5, 0),
18636 indent_guide(buffer_id, 3, 4, 1),
18637 ],
18638 None,
18639 &mut cx,
18640 );
18641}
18642
18643#[gpui::test]
18644async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
18645 let (buffer_id, mut cx) = setup_indent_guides_editor(
18646 &"
18647 fn main() {
18648 let a = 1;
18649 }"
18650 .unindent(),
18651 cx,
18652 )
18653 .await;
18654
18655 cx.update_editor(|editor, window, cx| {
18656 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18657 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18658 });
18659 });
18660
18661 assert_indent_guides(
18662 0..3,
18663 vec![indent_guide(buffer_id, 1, 1, 0)],
18664 Some(vec![0]),
18665 &mut cx,
18666 );
18667}
18668
18669#[gpui::test]
18670async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
18671 let (buffer_id, mut cx) = setup_indent_guides_editor(
18672 &"
18673 fn main() {
18674 if 1 == 2 {
18675 let a = 1;
18676 }
18677 }"
18678 .unindent(),
18679 cx,
18680 )
18681 .await;
18682
18683 cx.update_editor(|editor, window, cx| {
18684 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18685 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18686 });
18687 });
18688
18689 assert_indent_guides(
18690 0..4,
18691 vec![
18692 indent_guide(buffer_id, 1, 3, 0),
18693 indent_guide(buffer_id, 2, 2, 1),
18694 ],
18695 Some(vec![1]),
18696 &mut cx,
18697 );
18698
18699 cx.update_editor(|editor, window, cx| {
18700 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18701 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18702 });
18703 });
18704
18705 assert_indent_guides(
18706 0..4,
18707 vec![
18708 indent_guide(buffer_id, 1, 3, 0),
18709 indent_guide(buffer_id, 2, 2, 1),
18710 ],
18711 Some(vec![1]),
18712 &mut cx,
18713 );
18714
18715 cx.update_editor(|editor, window, cx| {
18716 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18717 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
18718 });
18719 });
18720
18721 assert_indent_guides(
18722 0..4,
18723 vec![
18724 indent_guide(buffer_id, 1, 3, 0),
18725 indent_guide(buffer_id, 2, 2, 1),
18726 ],
18727 Some(vec![0]),
18728 &mut cx,
18729 );
18730}
18731
18732#[gpui::test]
18733async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
18734 let (buffer_id, mut cx) = setup_indent_guides_editor(
18735 &"
18736 fn main() {
18737 let a = 1;
18738
18739 let b = 2;
18740 }"
18741 .unindent(),
18742 cx,
18743 )
18744 .await;
18745
18746 cx.update_editor(|editor, window, cx| {
18747 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18748 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18749 });
18750 });
18751
18752 assert_indent_guides(
18753 0..5,
18754 vec![indent_guide(buffer_id, 1, 3, 0)],
18755 Some(vec![0]),
18756 &mut cx,
18757 );
18758}
18759
18760#[gpui::test]
18761async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
18762 let (buffer_id, mut cx) = setup_indent_guides_editor(
18763 &"
18764 def m:
18765 a = 1
18766 pass"
18767 .unindent(),
18768 cx,
18769 )
18770 .await;
18771
18772 cx.update_editor(|editor, window, cx| {
18773 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18774 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18775 });
18776 });
18777
18778 assert_indent_guides(
18779 0..3,
18780 vec![indent_guide(buffer_id, 1, 2, 0)],
18781 Some(vec![0]),
18782 &mut cx,
18783 );
18784}
18785
18786#[gpui::test]
18787async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
18788 init_test(cx, |_| {});
18789 let mut cx = EditorTestContext::new(cx).await;
18790 let text = indoc! {
18791 "
18792 impl A {
18793 fn b() {
18794 0;
18795 3;
18796 5;
18797 6;
18798 7;
18799 }
18800 }
18801 "
18802 };
18803 let base_text = indoc! {
18804 "
18805 impl A {
18806 fn b() {
18807 0;
18808 1;
18809 2;
18810 3;
18811 4;
18812 }
18813 fn c() {
18814 5;
18815 6;
18816 7;
18817 }
18818 }
18819 "
18820 };
18821
18822 cx.update_editor(|editor, window, cx| {
18823 editor.set_text(text, window, cx);
18824
18825 editor.buffer().update(cx, |multibuffer, cx| {
18826 let buffer = multibuffer.as_singleton().unwrap();
18827 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
18828
18829 multibuffer.set_all_diff_hunks_expanded(cx);
18830 multibuffer.add_diff(diff, cx);
18831
18832 buffer.read(cx).remote_id()
18833 })
18834 });
18835 cx.run_until_parked();
18836
18837 cx.assert_state_with_diff(
18838 indoc! { "
18839 impl A {
18840 fn b() {
18841 0;
18842 - 1;
18843 - 2;
18844 3;
18845 - 4;
18846 - }
18847 - fn c() {
18848 5;
18849 6;
18850 7;
18851 }
18852 }
18853 ˇ"
18854 }
18855 .to_string(),
18856 );
18857
18858 let mut actual_guides = cx.update_editor(|editor, window, cx| {
18859 editor
18860 .snapshot(window, cx)
18861 .buffer_snapshot
18862 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
18863 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
18864 .collect::<Vec<_>>()
18865 });
18866 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
18867 assert_eq!(
18868 actual_guides,
18869 vec![
18870 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
18871 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
18872 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
18873 ]
18874 );
18875}
18876
18877#[gpui::test]
18878async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18879 init_test(cx, |_| {});
18880 let mut cx = EditorTestContext::new(cx).await;
18881
18882 let diff_base = r#"
18883 a
18884 b
18885 c
18886 "#
18887 .unindent();
18888
18889 cx.set_state(
18890 &r#"
18891 ˇA
18892 b
18893 C
18894 "#
18895 .unindent(),
18896 );
18897 cx.set_head_text(&diff_base);
18898 cx.update_editor(|editor, window, cx| {
18899 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18900 });
18901 executor.run_until_parked();
18902
18903 let both_hunks_expanded = r#"
18904 - a
18905 + ˇA
18906 b
18907 - c
18908 + C
18909 "#
18910 .unindent();
18911
18912 cx.assert_state_with_diff(both_hunks_expanded.clone());
18913
18914 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18915 let snapshot = editor.snapshot(window, cx);
18916 let hunks = editor
18917 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18918 .collect::<Vec<_>>();
18919 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18920 let buffer_id = hunks[0].buffer_id;
18921 hunks
18922 .into_iter()
18923 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18924 .collect::<Vec<_>>()
18925 });
18926 assert_eq!(hunk_ranges.len(), 2);
18927
18928 cx.update_editor(|editor, _, cx| {
18929 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18930 });
18931 executor.run_until_parked();
18932
18933 let second_hunk_expanded = r#"
18934 ˇA
18935 b
18936 - c
18937 + C
18938 "#
18939 .unindent();
18940
18941 cx.assert_state_with_diff(second_hunk_expanded);
18942
18943 cx.update_editor(|editor, _, cx| {
18944 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18945 });
18946 executor.run_until_parked();
18947
18948 cx.assert_state_with_diff(both_hunks_expanded.clone());
18949
18950 cx.update_editor(|editor, _, cx| {
18951 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18952 });
18953 executor.run_until_parked();
18954
18955 let first_hunk_expanded = r#"
18956 - a
18957 + ˇA
18958 b
18959 C
18960 "#
18961 .unindent();
18962
18963 cx.assert_state_with_diff(first_hunk_expanded);
18964
18965 cx.update_editor(|editor, _, cx| {
18966 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18967 });
18968 executor.run_until_parked();
18969
18970 cx.assert_state_with_diff(both_hunks_expanded);
18971
18972 cx.set_state(
18973 &r#"
18974 ˇA
18975 b
18976 "#
18977 .unindent(),
18978 );
18979 cx.run_until_parked();
18980
18981 // TODO this cursor position seems bad
18982 cx.assert_state_with_diff(
18983 r#"
18984 - ˇa
18985 + A
18986 b
18987 "#
18988 .unindent(),
18989 );
18990
18991 cx.update_editor(|editor, window, cx| {
18992 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18993 });
18994
18995 cx.assert_state_with_diff(
18996 r#"
18997 - ˇa
18998 + A
18999 b
19000 - c
19001 "#
19002 .unindent(),
19003 );
19004
19005 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19006 let snapshot = editor.snapshot(window, cx);
19007 let hunks = editor
19008 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19009 .collect::<Vec<_>>();
19010 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19011 let buffer_id = hunks[0].buffer_id;
19012 hunks
19013 .into_iter()
19014 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19015 .collect::<Vec<_>>()
19016 });
19017 assert_eq!(hunk_ranges.len(), 2);
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(
19025 r#"
19026 - ˇa
19027 + A
19028 b
19029 "#
19030 .unindent(),
19031 );
19032}
19033
19034#[gpui::test]
19035async fn test_toggle_deletion_hunk_at_start_of_file(
19036 executor: BackgroundExecutor,
19037 cx: &mut TestAppContext,
19038) {
19039 init_test(cx, |_| {});
19040 let mut cx = EditorTestContext::new(cx).await;
19041
19042 let diff_base = r#"
19043 a
19044 b
19045 c
19046 "#
19047 .unindent();
19048
19049 cx.set_state(
19050 &r#"
19051 ˇb
19052 c
19053 "#
19054 .unindent(),
19055 );
19056 cx.set_head_text(&diff_base);
19057 cx.update_editor(|editor, window, cx| {
19058 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19059 });
19060 executor.run_until_parked();
19061
19062 let hunk_expanded = r#"
19063 - a
19064 ˇb
19065 c
19066 "#
19067 .unindent();
19068
19069 cx.assert_state_with_diff(hunk_expanded.clone());
19070
19071 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19072 let snapshot = editor.snapshot(window, cx);
19073 let hunks = editor
19074 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19075 .collect::<Vec<_>>();
19076 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19077 let buffer_id = hunks[0].buffer_id;
19078 hunks
19079 .into_iter()
19080 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19081 .collect::<Vec<_>>()
19082 });
19083 assert_eq!(hunk_ranges.len(), 1);
19084
19085 cx.update_editor(|editor, _, cx| {
19086 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19087 });
19088 executor.run_until_parked();
19089
19090 let hunk_collapsed = r#"
19091 ˇb
19092 c
19093 "#
19094 .unindent();
19095
19096 cx.assert_state_with_diff(hunk_collapsed);
19097
19098 cx.update_editor(|editor, _, cx| {
19099 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19100 });
19101 executor.run_until_parked();
19102
19103 cx.assert_state_with_diff(hunk_expanded.clone());
19104}
19105
19106#[gpui::test]
19107async fn test_display_diff_hunks(cx: &mut TestAppContext) {
19108 init_test(cx, |_| {});
19109
19110 let fs = FakeFs::new(cx.executor());
19111 fs.insert_tree(
19112 path!("/test"),
19113 json!({
19114 ".git": {},
19115 "file-1": "ONE\n",
19116 "file-2": "TWO\n",
19117 "file-3": "THREE\n",
19118 }),
19119 )
19120 .await;
19121
19122 fs.set_head_for_repo(
19123 path!("/test/.git").as_ref(),
19124 &[
19125 ("file-1".into(), "one\n".into()),
19126 ("file-2".into(), "two\n".into()),
19127 ("file-3".into(), "three\n".into()),
19128 ],
19129 "deadbeef",
19130 );
19131
19132 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
19133 let mut buffers = vec![];
19134 for i in 1..=3 {
19135 let buffer = project
19136 .update(cx, |project, cx| {
19137 let path = format!(path!("/test/file-{}"), i);
19138 project.open_local_buffer(path, cx)
19139 })
19140 .await
19141 .unwrap();
19142 buffers.push(buffer);
19143 }
19144
19145 let multibuffer = cx.new(|cx| {
19146 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
19147 multibuffer.set_all_diff_hunks_expanded(cx);
19148 for buffer in &buffers {
19149 let snapshot = buffer.read(cx).snapshot();
19150 multibuffer.set_excerpts_for_path(
19151 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
19152 buffer.clone(),
19153 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
19154 DEFAULT_MULTIBUFFER_CONTEXT,
19155 cx,
19156 );
19157 }
19158 multibuffer
19159 });
19160
19161 let editor = cx.add_window(|window, cx| {
19162 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
19163 });
19164 cx.run_until_parked();
19165
19166 let snapshot = editor
19167 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19168 .unwrap();
19169 let hunks = snapshot
19170 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
19171 .map(|hunk| match hunk {
19172 DisplayDiffHunk::Unfolded {
19173 display_row_range, ..
19174 } => display_row_range,
19175 DisplayDiffHunk::Folded { .. } => unreachable!(),
19176 })
19177 .collect::<Vec<_>>();
19178 assert_eq!(
19179 hunks,
19180 [
19181 DisplayRow(2)..DisplayRow(4),
19182 DisplayRow(7)..DisplayRow(9),
19183 DisplayRow(12)..DisplayRow(14),
19184 ]
19185 );
19186}
19187
19188#[gpui::test]
19189async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
19190 init_test(cx, |_| {});
19191
19192 let mut cx = EditorTestContext::new(cx).await;
19193 cx.set_head_text(indoc! { "
19194 one
19195 two
19196 three
19197 four
19198 five
19199 "
19200 });
19201 cx.set_index_text(indoc! { "
19202 one
19203 two
19204 three
19205 four
19206 five
19207 "
19208 });
19209 cx.set_state(indoc! {"
19210 one
19211 TWO
19212 ˇTHREE
19213 FOUR
19214 five
19215 "});
19216 cx.run_until_parked();
19217 cx.update_editor(|editor, window, cx| {
19218 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19219 });
19220 cx.run_until_parked();
19221 cx.assert_index_text(Some(indoc! {"
19222 one
19223 TWO
19224 THREE
19225 FOUR
19226 five
19227 "}));
19228 cx.set_state(indoc! { "
19229 one
19230 TWO
19231 ˇTHREE-HUNDRED
19232 FOUR
19233 five
19234 "});
19235 cx.run_until_parked();
19236 cx.update_editor(|editor, window, cx| {
19237 let snapshot = editor.snapshot(window, cx);
19238 let hunks = editor
19239 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19240 .collect::<Vec<_>>();
19241 assert_eq!(hunks.len(), 1);
19242 assert_eq!(
19243 hunks[0].status(),
19244 DiffHunkStatus {
19245 kind: DiffHunkStatusKind::Modified,
19246 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
19247 }
19248 );
19249
19250 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19251 });
19252 cx.run_until_parked();
19253 cx.assert_index_text(Some(indoc! {"
19254 one
19255 TWO
19256 THREE-HUNDRED
19257 FOUR
19258 five
19259 "}));
19260}
19261
19262#[gpui::test]
19263fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
19264 init_test(cx, |_| {});
19265
19266 let editor = cx.add_window(|window, cx| {
19267 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
19268 build_editor(buffer, window, cx)
19269 });
19270
19271 let render_args = Arc::new(Mutex::new(None));
19272 let snapshot = editor
19273 .update(cx, |editor, window, cx| {
19274 let snapshot = editor.buffer().read(cx).snapshot(cx);
19275 let range =
19276 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
19277
19278 struct RenderArgs {
19279 row: MultiBufferRow,
19280 folded: bool,
19281 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
19282 }
19283
19284 let crease = Crease::inline(
19285 range,
19286 FoldPlaceholder::test(),
19287 {
19288 let toggle_callback = render_args.clone();
19289 move |row, folded, callback, _window, _cx| {
19290 *toggle_callback.lock() = Some(RenderArgs {
19291 row,
19292 folded,
19293 callback,
19294 });
19295 div()
19296 }
19297 },
19298 |_row, _folded, _window, _cx| div(),
19299 );
19300
19301 editor.insert_creases(Some(crease), cx);
19302 let snapshot = editor.snapshot(window, cx);
19303 let _div = snapshot.render_crease_toggle(
19304 MultiBufferRow(1),
19305 false,
19306 cx.entity().clone(),
19307 window,
19308 cx,
19309 );
19310 snapshot
19311 })
19312 .unwrap();
19313
19314 let render_args = render_args.lock().take().unwrap();
19315 assert_eq!(render_args.row, MultiBufferRow(1));
19316 assert!(!render_args.folded);
19317 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19318
19319 cx.update_window(*editor, |_, window, cx| {
19320 (render_args.callback)(true, window, cx)
19321 })
19322 .unwrap();
19323 let snapshot = editor
19324 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19325 .unwrap();
19326 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
19327
19328 cx.update_window(*editor, |_, window, cx| {
19329 (render_args.callback)(false, window, cx)
19330 })
19331 .unwrap();
19332 let snapshot = editor
19333 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19334 .unwrap();
19335 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19336}
19337
19338#[gpui::test]
19339async fn test_input_text(cx: &mut TestAppContext) {
19340 init_test(cx, |_| {});
19341 let mut cx = EditorTestContext::new(cx).await;
19342
19343 cx.set_state(
19344 &r#"ˇone
19345 two
19346
19347 three
19348 fourˇ
19349 five
19350
19351 siˇx"#
19352 .unindent(),
19353 );
19354
19355 cx.dispatch_action(HandleInput(String::new()));
19356 cx.assert_editor_state(
19357 &r#"ˇone
19358 two
19359
19360 three
19361 fourˇ
19362 five
19363
19364 siˇx"#
19365 .unindent(),
19366 );
19367
19368 cx.dispatch_action(HandleInput("AAAA".to_string()));
19369 cx.assert_editor_state(
19370 &r#"AAAAˇone
19371 two
19372
19373 three
19374 fourAAAAˇ
19375 five
19376
19377 siAAAAˇx"#
19378 .unindent(),
19379 );
19380}
19381
19382#[gpui::test]
19383async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
19384 init_test(cx, |_| {});
19385
19386 let mut cx = EditorTestContext::new(cx).await;
19387 cx.set_state(
19388 r#"let foo = 1;
19389let foo = 2;
19390let foo = 3;
19391let fooˇ = 4;
19392let foo = 5;
19393let foo = 6;
19394let foo = 7;
19395let foo = 8;
19396let foo = 9;
19397let foo = 10;
19398let foo = 11;
19399let foo = 12;
19400let foo = 13;
19401let foo = 14;
19402let foo = 15;"#,
19403 );
19404
19405 cx.update_editor(|e, window, cx| {
19406 assert_eq!(
19407 e.next_scroll_position,
19408 NextScrollCursorCenterTopBottom::Center,
19409 "Default next scroll direction is center",
19410 );
19411
19412 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19413 assert_eq!(
19414 e.next_scroll_position,
19415 NextScrollCursorCenterTopBottom::Top,
19416 "After center, next scroll direction should be top",
19417 );
19418
19419 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19420 assert_eq!(
19421 e.next_scroll_position,
19422 NextScrollCursorCenterTopBottom::Bottom,
19423 "After top, next scroll direction should be bottom",
19424 );
19425
19426 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19427 assert_eq!(
19428 e.next_scroll_position,
19429 NextScrollCursorCenterTopBottom::Center,
19430 "After bottom, scrolling should start over",
19431 );
19432
19433 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19434 assert_eq!(
19435 e.next_scroll_position,
19436 NextScrollCursorCenterTopBottom::Top,
19437 "Scrolling continues if retriggered fast enough"
19438 );
19439 });
19440
19441 cx.executor()
19442 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
19443 cx.executor().run_until_parked();
19444 cx.update_editor(|e, _, _| {
19445 assert_eq!(
19446 e.next_scroll_position,
19447 NextScrollCursorCenterTopBottom::Center,
19448 "If scrolling is not triggered fast enough, it should reset"
19449 );
19450 });
19451}
19452
19453#[gpui::test]
19454async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
19455 init_test(cx, |_| {});
19456 let mut cx = EditorLspTestContext::new_rust(
19457 lsp::ServerCapabilities {
19458 definition_provider: Some(lsp::OneOf::Left(true)),
19459 references_provider: Some(lsp::OneOf::Left(true)),
19460 ..lsp::ServerCapabilities::default()
19461 },
19462 cx,
19463 )
19464 .await;
19465
19466 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
19467 let go_to_definition = cx
19468 .lsp
19469 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19470 move |params, _| async move {
19471 if empty_go_to_definition {
19472 Ok(None)
19473 } else {
19474 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
19475 uri: params.text_document_position_params.text_document.uri,
19476 range: lsp::Range::new(
19477 lsp::Position::new(4, 3),
19478 lsp::Position::new(4, 6),
19479 ),
19480 })))
19481 }
19482 },
19483 );
19484 let references = cx
19485 .lsp
19486 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
19487 Ok(Some(vec![lsp::Location {
19488 uri: params.text_document_position.text_document.uri,
19489 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
19490 }]))
19491 });
19492 (go_to_definition, references)
19493 };
19494
19495 cx.set_state(
19496 &r#"fn one() {
19497 let mut a = ˇtwo();
19498 }
19499
19500 fn two() {}"#
19501 .unindent(),
19502 );
19503 set_up_lsp_handlers(false, &mut cx);
19504 let navigated = cx
19505 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19506 .await
19507 .expect("Failed to navigate to definition");
19508 assert_eq!(
19509 navigated,
19510 Navigated::Yes,
19511 "Should have navigated to definition from the GetDefinition response"
19512 );
19513 cx.assert_editor_state(
19514 &r#"fn one() {
19515 let mut a = two();
19516 }
19517
19518 fn «twoˇ»() {}"#
19519 .unindent(),
19520 );
19521
19522 let editors = cx.update_workspace(|workspace, _, cx| {
19523 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19524 });
19525 cx.update_editor(|_, _, test_editor_cx| {
19526 assert_eq!(
19527 editors.len(),
19528 1,
19529 "Initially, only one, test, editor should be open in the workspace"
19530 );
19531 assert_eq!(
19532 test_editor_cx.entity(),
19533 editors.last().expect("Asserted len is 1").clone()
19534 );
19535 });
19536
19537 set_up_lsp_handlers(true, &mut cx);
19538 let navigated = cx
19539 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19540 .await
19541 .expect("Failed to navigate to lookup references");
19542 assert_eq!(
19543 navigated,
19544 Navigated::Yes,
19545 "Should have navigated to references as a fallback after empty GoToDefinition response"
19546 );
19547 // We should not change the selections in the existing file,
19548 // if opening another milti buffer with the references
19549 cx.assert_editor_state(
19550 &r#"fn one() {
19551 let mut a = two();
19552 }
19553
19554 fn «twoˇ»() {}"#
19555 .unindent(),
19556 );
19557 let editors = cx.update_workspace(|workspace, _, cx| {
19558 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19559 });
19560 cx.update_editor(|_, _, test_editor_cx| {
19561 assert_eq!(
19562 editors.len(),
19563 2,
19564 "After falling back to references search, we open a new editor with the results"
19565 );
19566 let references_fallback_text = editors
19567 .into_iter()
19568 .find(|new_editor| *new_editor != test_editor_cx.entity())
19569 .expect("Should have one non-test editor now")
19570 .read(test_editor_cx)
19571 .text(test_editor_cx);
19572 assert_eq!(
19573 references_fallback_text, "fn one() {\n let mut a = two();\n}",
19574 "Should use the range from the references response and not the GoToDefinition one"
19575 );
19576 });
19577}
19578
19579#[gpui::test]
19580async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
19581 init_test(cx, |_| {});
19582 cx.update(|cx| {
19583 let mut editor_settings = EditorSettings::get_global(cx).clone();
19584 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
19585 EditorSettings::override_global(editor_settings, cx);
19586 });
19587 let mut cx = EditorLspTestContext::new_rust(
19588 lsp::ServerCapabilities {
19589 definition_provider: Some(lsp::OneOf::Left(true)),
19590 references_provider: Some(lsp::OneOf::Left(true)),
19591 ..lsp::ServerCapabilities::default()
19592 },
19593 cx,
19594 )
19595 .await;
19596 let original_state = r#"fn one() {
19597 let mut a = ˇtwo();
19598 }
19599
19600 fn two() {}"#
19601 .unindent();
19602 cx.set_state(&original_state);
19603
19604 let mut go_to_definition = cx
19605 .lsp
19606 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19607 move |_, _| async move { Ok(None) },
19608 );
19609 let _references = cx
19610 .lsp
19611 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
19612 panic!("Should not call for references with no go to definition fallback")
19613 });
19614
19615 let navigated = cx
19616 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19617 .await
19618 .expect("Failed to navigate to lookup references");
19619 go_to_definition
19620 .next()
19621 .await
19622 .expect("Should have called the go_to_definition handler");
19623
19624 assert_eq!(
19625 navigated,
19626 Navigated::No,
19627 "Should have navigated to references as a fallback after empty GoToDefinition response"
19628 );
19629 cx.assert_editor_state(&original_state);
19630 let editors = cx.update_workspace(|workspace, _, cx| {
19631 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19632 });
19633 cx.update_editor(|_, _, _| {
19634 assert_eq!(
19635 editors.len(),
19636 1,
19637 "After unsuccessful fallback, no other editor should have been opened"
19638 );
19639 });
19640}
19641
19642#[gpui::test]
19643async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
19644 init_test(cx, |_| {});
19645
19646 let language = Arc::new(Language::new(
19647 LanguageConfig::default(),
19648 Some(tree_sitter_rust::LANGUAGE.into()),
19649 ));
19650
19651 let text = r#"
19652 #[cfg(test)]
19653 mod tests() {
19654 #[test]
19655 fn runnable_1() {
19656 let a = 1;
19657 }
19658
19659 #[test]
19660 fn runnable_2() {
19661 let a = 1;
19662 let b = 2;
19663 }
19664 }
19665 "#
19666 .unindent();
19667
19668 let fs = FakeFs::new(cx.executor());
19669 fs.insert_file("/file.rs", Default::default()).await;
19670
19671 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19672 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19673 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19674 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
19675 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
19676
19677 let editor = cx.new_window_entity(|window, cx| {
19678 Editor::new(
19679 EditorMode::full(),
19680 multi_buffer,
19681 Some(project.clone()),
19682 window,
19683 cx,
19684 )
19685 });
19686
19687 editor.update_in(cx, |editor, window, cx| {
19688 let snapshot = editor.buffer().read(cx).snapshot(cx);
19689 editor.tasks.insert(
19690 (buffer.read(cx).remote_id(), 3),
19691 RunnableTasks {
19692 templates: vec![],
19693 offset: snapshot.anchor_before(43),
19694 column: 0,
19695 extra_variables: HashMap::default(),
19696 context_range: BufferOffset(43)..BufferOffset(85),
19697 },
19698 );
19699 editor.tasks.insert(
19700 (buffer.read(cx).remote_id(), 8),
19701 RunnableTasks {
19702 templates: vec![],
19703 offset: snapshot.anchor_before(86),
19704 column: 0,
19705 extra_variables: HashMap::default(),
19706 context_range: BufferOffset(86)..BufferOffset(191),
19707 },
19708 );
19709
19710 // Test finding task when cursor is inside function body
19711 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19712 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
19713 });
19714 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19715 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
19716
19717 // Test finding task when cursor is on function name
19718 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19719 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
19720 });
19721 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19722 assert_eq!(row, 8, "Should find task when cursor is on function name");
19723 });
19724}
19725
19726#[gpui::test]
19727async fn test_folding_buffers(cx: &mut TestAppContext) {
19728 init_test(cx, |_| {});
19729
19730 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19731 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
19732 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
19733
19734 let fs = FakeFs::new(cx.executor());
19735 fs.insert_tree(
19736 path!("/a"),
19737 json!({
19738 "first.rs": sample_text_1,
19739 "second.rs": sample_text_2,
19740 "third.rs": sample_text_3,
19741 }),
19742 )
19743 .await;
19744 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19745 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19746 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19747 let worktree = project.update(cx, |project, cx| {
19748 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19749 assert_eq!(worktrees.len(), 1);
19750 worktrees.pop().unwrap()
19751 });
19752 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19753
19754 let buffer_1 = project
19755 .update(cx, |project, cx| {
19756 project.open_buffer((worktree_id, "first.rs"), cx)
19757 })
19758 .await
19759 .unwrap();
19760 let buffer_2 = project
19761 .update(cx, |project, cx| {
19762 project.open_buffer((worktree_id, "second.rs"), cx)
19763 })
19764 .await
19765 .unwrap();
19766 let buffer_3 = project
19767 .update(cx, |project, cx| {
19768 project.open_buffer((worktree_id, "third.rs"), cx)
19769 })
19770 .await
19771 .unwrap();
19772
19773 let multi_buffer = cx.new(|cx| {
19774 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19775 multi_buffer.push_excerpts(
19776 buffer_1.clone(),
19777 [
19778 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19779 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19780 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19781 ],
19782 cx,
19783 );
19784 multi_buffer.push_excerpts(
19785 buffer_2.clone(),
19786 [
19787 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19788 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19789 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19790 ],
19791 cx,
19792 );
19793 multi_buffer.push_excerpts(
19794 buffer_3.clone(),
19795 [
19796 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19797 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19798 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19799 ],
19800 cx,
19801 );
19802 multi_buffer
19803 });
19804 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19805 Editor::new(
19806 EditorMode::full(),
19807 multi_buffer.clone(),
19808 Some(project.clone()),
19809 window,
19810 cx,
19811 )
19812 });
19813
19814 assert_eq!(
19815 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19816 "\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",
19817 );
19818
19819 multi_buffer_editor.update(cx, |editor, cx| {
19820 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19821 });
19822 assert_eq!(
19823 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19824 "\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",
19825 "After folding the first buffer, its text should not be displayed"
19826 );
19827
19828 multi_buffer_editor.update(cx, |editor, cx| {
19829 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19830 });
19831 assert_eq!(
19832 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19833 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
19834 "After folding the second buffer, its text should not be displayed"
19835 );
19836
19837 multi_buffer_editor.update(cx, |editor, cx| {
19838 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19839 });
19840 assert_eq!(
19841 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19842 "\n\n\n\n\n",
19843 "After folding the third buffer, its text should not be displayed"
19844 );
19845
19846 // Emulate selection inside the fold logic, that should work
19847 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19848 editor
19849 .snapshot(window, cx)
19850 .next_line_boundary(Point::new(0, 4));
19851 });
19852
19853 multi_buffer_editor.update(cx, |editor, cx| {
19854 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19855 });
19856 assert_eq!(
19857 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19858 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19859 "After unfolding the second buffer, its text should be displayed"
19860 );
19861
19862 // Typing inside of buffer 1 causes that buffer to be unfolded.
19863 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19864 assert_eq!(
19865 multi_buffer
19866 .read(cx)
19867 .snapshot(cx)
19868 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
19869 .collect::<String>(),
19870 "bbbb"
19871 );
19872 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
19873 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
19874 });
19875 editor.handle_input("B", window, cx);
19876 });
19877
19878 assert_eq!(
19879 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19880 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19881 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
19882 );
19883
19884 multi_buffer_editor.update(cx, |editor, cx| {
19885 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19886 });
19887 assert_eq!(
19888 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19889 "\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",
19890 "After unfolding the all buffers, all original text should be displayed"
19891 );
19892}
19893
19894#[gpui::test]
19895async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
19896 init_test(cx, |_| {});
19897
19898 let sample_text_1 = "1111\n2222\n3333".to_string();
19899 let sample_text_2 = "4444\n5555\n6666".to_string();
19900 let sample_text_3 = "7777\n8888\n9999".to_string();
19901
19902 let fs = FakeFs::new(cx.executor());
19903 fs.insert_tree(
19904 path!("/a"),
19905 json!({
19906 "first.rs": sample_text_1,
19907 "second.rs": sample_text_2,
19908 "third.rs": sample_text_3,
19909 }),
19910 )
19911 .await;
19912 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19913 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19914 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19915 let worktree = project.update(cx, |project, cx| {
19916 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19917 assert_eq!(worktrees.len(), 1);
19918 worktrees.pop().unwrap()
19919 });
19920 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19921
19922 let buffer_1 = project
19923 .update(cx, |project, cx| {
19924 project.open_buffer((worktree_id, "first.rs"), cx)
19925 })
19926 .await
19927 .unwrap();
19928 let buffer_2 = project
19929 .update(cx, |project, cx| {
19930 project.open_buffer((worktree_id, "second.rs"), cx)
19931 })
19932 .await
19933 .unwrap();
19934 let buffer_3 = project
19935 .update(cx, |project, cx| {
19936 project.open_buffer((worktree_id, "third.rs"), cx)
19937 })
19938 .await
19939 .unwrap();
19940
19941 let multi_buffer = cx.new(|cx| {
19942 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19943 multi_buffer.push_excerpts(
19944 buffer_1.clone(),
19945 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19946 cx,
19947 );
19948 multi_buffer.push_excerpts(
19949 buffer_2.clone(),
19950 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19951 cx,
19952 );
19953 multi_buffer.push_excerpts(
19954 buffer_3.clone(),
19955 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19956 cx,
19957 );
19958 multi_buffer
19959 });
19960
19961 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19962 Editor::new(
19963 EditorMode::full(),
19964 multi_buffer,
19965 Some(project.clone()),
19966 window,
19967 cx,
19968 )
19969 });
19970
19971 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
19972 assert_eq!(
19973 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19974 full_text,
19975 );
19976
19977 multi_buffer_editor.update(cx, |editor, cx| {
19978 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19979 });
19980 assert_eq!(
19981 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19982 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
19983 "After folding the first buffer, its text should not be displayed"
19984 );
19985
19986 multi_buffer_editor.update(cx, |editor, cx| {
19987 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19988 });
19989
19990 assert_eq!(
19991 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19992 "\n\n\n\n\n\n7777\n8888\n9999",
19993 "After folding the second buffer, its text should not be displayed"
19994 );
19995
19996 multi_buffer_editor.update(cx, |editor, cx| {
19997 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19998 });
19999 assert_eq!(
20000 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20001 "\n\n\n\n\n",
20002 "After folding the third buffer, its text should not be displayed"
20003 );
20004
20005 multi_buffer_editor.update(cx, |editor, cx| {
20006 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
20007 });
20008 assert_eq!(
20009 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20010 "\n\n\n\n4444\n5555\n6666\n\n",
20011 "After unfolding the second buffer, its text should be displayed"
20012 );
20013
20014 multi_buffer_editor.update(cx, |editor, cx| {
20015 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
20016 });
20017 assert_eq!(
20018 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20019 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
20020 "After unfolding the first buffer, its text should be displayed"
20021 );
20022
20023 multi_buffer_editor.update(cx, |editor, cx| {
20024 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
20025 });
20026 assert_eq!(
20027 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20028 full_text,
20029 "After unfolding all buffers, all original text should be displayed"
20030 );
20031}
20032
20033#[gpui::test]
20034async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
20035 init_test(cx, |_| {});
20036
20037 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
20038
20039 let fs = FakeFs::new(cx.executor());
20040 fs.insert_tree(
20041 path!("/a"),
20042 json!({
20043 "main.rs": sample_text,
20044 }),
20045 )
20046 .await;
20047 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20048 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20049 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20050 let worktree = project.update(cx, |project, cx| {
20051 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20052 assert_eq!(worktrees.len(), 1);
20053 worktrees.pop().unwrap()
20054 });
20055 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20056
20057 let buffer_1 = project
20058 .update(cx, |project, cx| {
20059 project.open_buffer((worktree_id, "main.rs"), cx)
20060 })
20061 .await
20062 .unwrap();
20063
20064 let multi_buffer = cx.new(|cx| {
20065 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20066 multi_buffer.push_excerpts(
20067 buffer_1.clone(),
20068 [ExcerptRange::new(
20069 Point::new(0, 0)
20070 ..Point::new(
20071 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
20072 0,
20073 ),
20074 )],
20075 cx,
20076 );
20077 multi_buffer
20078 });
20079 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20080 Editor::new(
20081 EditorMode::full(),
20082 multi_buffer,
20083 Some(project.clone()),
20084 window,
20085 cx,
20086 )
20087 });
20088
20089 let selection_range = Point::new(1, 0)..Point::new(2, 0);
20090 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20091 enum TestHighlight {}
20092 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
20093 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
20094 editor.highlight_text::<TestHighlight>(
20095 vec![highlight_range.clone()],
20096 HighlightStyle::color(Hsla::green()),
20097 cx,
20098 );
20099 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20100 s.select_ranges(Some(highlight_range))
20101 });
20102 });
20103
20104 let full_text = format!("\n\n{sample_text}");
20105 assert_eq!(
20106 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20107 full_text,
20108 );
20109}
20110
20111#[gpui::test]
20112async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
20113 init_test(cx, |_| {});
20114 cx.update(|cx| {
20115 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
20116 "keymaps/default-linux.json",
20117 cx,
20118 )
20119 .unwrap();
20120 cx.bind_keys(default_key_bindings);
20121 });
20122
20123 let (editor, cx) = cx.add_window_view(|window, cx| {
20124 let multi_buffer = MultiBuffer::build_multi(
20125 [
20126 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
20127 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
20128 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
20129 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
20130 ],
20131 cx,
20132 );
20133 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
20134
20135 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
20136 // fold all but the second buffer, so that we test navigating between two
20137 // adjacent folded buffers, as well as folded buffers at the start and
20138 // end the multibuffer
20139 editor.fold_buffer(buffer_ids[0], cx);
20140 editor.fold_buffer(buffer_ids[2], cx);
20141 editor.fold_buffer(buffer_ids[3], cx);
20142
20143 editor
20144 });
20145 cx.simulate_resize(size(px(1000.), px(1000.)));
20146
20147 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
20148 cx.assert_excerpts_with_selections(indoc! {"
20149 [EXCERPT]
20150 ˇ[FOLDED]
20151 [EXCERPT]
20152 a1
20153 b1
20154 [EXCERPT]
20155 [FOLDED]
20156 [EXCERPT]
20157 [FOLDED]
20158 "
20159 });
20160 cx.simulate_keystroke("down");
20161 cx.assert_excerpts_with_selections(indoc! {"
20162 [EXCERPT]
20163 [FOLDED]
20164 [EXCERPT]
20165 ˇa1
20166 b1
20167 [EXCERPT]
20168 [FOLDED]
20169 [EXCERPT]
20170 [FOLDED]
20171 "
20172 });
20173 cx.simulate_keystroke("down");
20174 cx.assert_excerpts_with_selections(indoc! {"
20175 [EXCERPT]
20176 [FOLDED]
20177 [EXCERPT]
20178 a1
20179 ˇb1
20180 [EXCERPT]
20181 [FOLDED]
20182 [EXCERPT]
20183 [FOLDED]
20184 "
20185 });
20186 cx.simulate_keystroke("down");
20187 cx.assert_excerpts_with_selections(indoc! {"
20188 [EXCERPT]
20189 [FOLDED]
20190 [EXCERPT]
20191 a1
20192 b1
20193 ˇ[EXCERPT]
20194 [FOLDED]
20195 [EXCERPT]
20196 [FOLDED]
20197 "
20198 });
20199 cx.simulate_keystroke("down");
20200 cx.assert_excerpts_with_selections(indoc! {"
20201 [EXCERPT]
20202 [FOLDED]
20203 [EXCERPT]
20204 a1
20205 b1
20206 [EXCERPT]
20207 ˇ[FOLDED]
20208 [EXCERPT]
20209 [FOLDED]
20210 "
20211 });
20212 for _ in 0..5 {
20213 cx.simulate_keystroke("down");
20214 cx.assert_excerpts_with_selections(indoc! {"
20215 [EXCERPT]
20216 [FOLDED]
20217 [EXCERPT]
20218 a1
20219 b1
20220 [EXCERPT]
20221 [FOLDED]
20222 [EXCERPT]
20223 ˇ[FOLDED]
20224 "
20225 });
20226 }
20227
20228 cx.simulate_keystroke("up");
20229 cx.assert_excerpts_with_selections(indoc! {"
20230 [EXCERPT]
20231 [FOLDED]
20232 [EXCERPT]
20233 a1
20234 b1
20235 [EXCERPT]
20236 ˇ[FOLDED]
20237 [EXCERPT]
20238 [FOLDED]
20239 "
20240 });
20241 cx.simulate_keystroke("up");
20242 cx.assert_excerpts_with_selections(indoc! {"
20243 [EXCERPT]
20244 [FOLDED]
20245 [EXCERPT]
20246 a1
20247 b1
20248 ˇ[EXCERPT]
20249 [FOLDED]
20250 [EXCERPT]
20251 [FOLDED]
20252 "
20253 });
20254 cx.simulate_keystroke("up");
20255 cx.assert_excerpts_with_selections(indoc! {"
20256 [EXCERPT]
20257 [FOLDED]
20258 [EXCERPT]
20259 a1
20260 ˇb1
20261 [EXCERPT]
20262 [FOLDED]
20263 [EXCERPT]
20264 [FOLDED]
20265 "
20266 });
20267 cx.simulate_keystroke("up");
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 for _ in 0..5 {
20281 cx.simulate_keystroke("up");
20282 cx.assert_excerpts_with_selections(indoc! {"
20283 [EXCERPT]
20284 ˇ[FOLDED]
20285 [EXCERPT]
20286 a1
20287 b1
20288 [EXCERPT]
20289 [FOLDED]
20290 [EXCERPT]
20291 [FOLDED]
20292 "
20293 });
20294 }
20295}
20296
20297#[gpui::test]
20298async fn test_inline_completion_text(cx: &mut TestAppContext) {
20299 init_test(cx, |_| {});
20300
20301 // Simple insertion
20302 assert_highlighted_edits(
20303 "Hello, world!",
20304 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
20305 true,
20306 cx,
20307 |highlighted_edits, cx| {
20308 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
20309 assert_eq!(highlighted_edits.highlights.len(), 1);
20310 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
20311 assert_eq!(
20312 highlighted_edits.highlights[0].1.background_color,
20313 Some(cx.theme().status().created_background)
20314 );
20315 },
20316 )
20317 .await;
20318
20319 // Replacement
20320 assert_highlighted_edits(
20321 "This is a test.",
20322 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
20323 false,
20324 cx,
20325 |highlighted_edits, cx| {
20326 assert_eq!(highlighted_edits.text, "That is a test.");
20327 assert_eq!(highlighted_edits.highlights.len(), 1);
20328 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
20329 assert_eq!(
20330 highlighted_edits.highlights[0].1.background_color,
20331 Some(cx.theme().status().created_background)
20332 );
20333 },
20334 )
20335 .await;
20336
20337 // Multiple edits
20338 assert_highlighted_edits(
20339 "Hello, world!",
20340 vec![
20341 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
20342 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
20343 ],
20344 false,
20345 cx,
20346 |highlighted_edits, cx| {
20347 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
20348 assert_eq!(highlighted_edits.highlights.len(), 2);
20349 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
20350 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
20351 assert_eq!(
20352 highlighted_edits.highlights[0].1.background_color,
20353 Some(cx.theme().status().created_background)
20354 );
20355 assert_eq!(
20356 highlighted_edits.highlights[1].1.background_color,
20357 Some(cx.theme().status().created_background)
20358 );
20359 },
20360 )
20361 .await;
20362
20363 // Multiple lines with edits
20364 assert_highlighted_edits(
20365 "First line\nSecond line\nThird line\nFourth line",
20366 vec![
20367 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
20368 (
20369 Point::new(2, 0)..Point::new(2, 10),
20370 "New third line".to_string(),
20371 ),
20372 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
20373 ],
20374 false,
20375 cx,
20376 |highlighted_edits, cx| {
20377 assert_eq!(
20378 highlighted_edits.text,
20379 "Second modified\nNew third line\nFourth updated line"
20380 );
20381 assert_eq!(highlighted_edits.highlights.len(), 3);
20382 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
20383 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
20384 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
20385 for highlight in &highlighted_edits.highlights {
20386 assert_eq!(
20387 highlight.1.background_color,
20388 Some(cx.theme().status().created_background)
20389 );
20390 }
20391 },
20392 )
20393 .await;
20394}
20395
20396#[gpui::test]
20397async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
20398 init_test(cx, |_| {});
20399
20400 // Deletion
20401 assert_highlighted_edits(
20402 "Hello, world!",
20403 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
20404 true,
20405 cx,
20406 |highlighted_edits, cx| {
20407 assert_eq!(highlighted_edits.text, "Hello, world!");
20408 assert_eq!(highlighted_edits.highlights.len(), 1);
20409 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
20410 assert_eq!(
20411 highlighted_edits.highlights[0].1.background_color,
20412 Some(cx.theme().status().deleted_background)
20413 );
20414 },
20415 )
20416 .await;
20417
20418 // Insertion
20419 assert_highlighted_edits(
20420 "Hello, world!",
20421 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
20422 true,
20423 cx,
20424 |highlighted_edits, cx| {
20425 assert_eq!(highlighted_edits.highlights.len(), 1);
20426 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
20427 assert_eq!(
20428 highlighted_edits.highlights[0].1.background_color,
20429 Some(cx.theme().status().created_background)
20430 );
20431 },
20432 )
20433 .await;
20434}
20435
20436async fn assert_highlighted_edits(
20437 text: &str,
20438 edits: Vec<(Range<Point>, String)>,
20439 include_deletions: bool,
20440 cx: &mut TestAppContext,
20441 assertion_fn: impl Fn(HighlightedText, &App),
20442) {
20443 let window = cx.add_window(|window, cx| {
20444 let buffer = MultiBuffer::build_simple(text, cx);
20445 Editor::new(EditorMode::full(), buffer, None, window, cx)
20446 });
20447 let cx = &mut VisualTestContext::from_window(*window, cx);
20448
20449 let (buffer, snapshot) = window
20450 .update(cx, |editor, _window, cx| {
20451 (
20452 editor.buffer().clone(),
20453 editor.buffer().read(cx).snapshot(cx),
20454 )
20455 })
20456 .unwrap();
20457
20458 let edits = edits
20459 .into_iter()
20460 .map(|(range, edit)| {
20461 (
20462 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
20463 edit,
20464 )
20465 })
20466 .collect::<Vec<_>>();
20467
20468 let text_anchor_edits = edits
20469 .clone()
20470 .into_iter()
20471 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
20472 .collect::<Vec<_>>();
20473
20474 let edit_preview = window
20475 .update(cx, |_, _window, cx| {
20476 buffer
20477 .read(cx)
20478 .as_singleton()
20479 .unwrap()
20480 .read(cx)
20481 .preview_edits(text_anchor_edits.into(), cx)
20482 })
20483 .unwrap()
20484 .await;
20485
20486 cx.update(|_window, cx| {
20487 let highlighted_edits = inline_completion_edit_text(
20488 &snapshot.as_singleton().unwrap().2,
20489 &edits,
20490 &edit_preview,
20491 include_deletions,
20492 cx,
20493 );
20494 assertion_fn(highlighted_edits, cx)
20495 });
20496}
20497
20498#[track_caller]
20499fn assert_breakpoint(
20500 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
20501 path: &Arc<Path>,
20502 expected: Vec<(u32, Breakpoint)>,
20503) {
20504 if expected.len() == 0usize {
20505 assert!(!breakpoints.contains_key(path), "{}", path.display());
20506 } else {
20507 let mut breakpoint = breakpoints
20508 .get(path)
20509 .unwrap()
20510 .into_iter()
20511 .map(|breakpoint| {
20512 (
20513 breakpoint.row,
20514 Breakpoint {
20515 message: breakpoint.message.clone(),
20516 state: breakpoint.state,
20517 condition: breakpoint.condition.clone(),
20518 hit_condition: breakpoint.hit_condition.clone(),
20519 },
20520 )
20521 })
20522 .collect::<Vec<_>>();
20523
20524 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
20525
20526 assert_eq!(expected, breakpoint);
20527 }
20528}
20529
20530fn add_log_breakpoint_at_cursor(
20531 editor: &mut Editor,
20532 log_message: &str,
20533 window: &mut Window,
20534 cx: &mut Context<Editor>,
20535) {
20536 let (anchor, bp) = editor
20537 .breakpoints_at_cursors(window, cx)
20538 .first()
20539 .and_then(|(anchor, bp)| {
20540 if let Some(bp) = bp {
20541 Some((*anchor, bp.clone()))
20542 } else {
20543 None
20544 }
20545 })
20546 .unwrap_or_else(|| {
20547 let cursor_position: Point = editor.selections.newest(cx).head();
20548
20549 let breakpoint_position = editor
20550 .snapshot(window, cx)
20551 .display_snapshot
20552 .buffer_snapshot
20553 .anchor_before(Point::new(cursor_position.row, 0));
20554
20555 (breakpoint_position, Breakpoint::new_log(&log_message))
20556 });
20557
20558 editor.edit_breakpoint_at_anchor(
20559 anchor,
20560 bp,
20561 BreakpointEditAction::EditLogMessage(log_message.into()),
20562 cx,
20563 );
20564}
20565
20566#[gpui::test]
20567async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
20568 init_test(cx, |_| {});
20569
20570 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20571 let fs = FakeFs::new(cx.executor());
20572 fs.insert_tree(
20573 path!("/a"),
20574 json!({
20575 "main.rs": sample_text,
20576 }),
20577 )
20578 .await;
20579 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20580 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20581 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20582
20583 let fs = FakeFs::new(cx.executor());
20584 fs.insert_tree(
20585 path!("/a"),
20586 json!({
20587 "main.rs": sample_text,
20588 }),
20589 )
20590 .await;
20591 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20592 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20593 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20594 let worktree_id = workspace
20595 .update(cx, |workspace, _window, cx| {
20596 workspace.project().update(cx, |project, cx| {
20597 project.worktrees(cx).next().unwrap().read(cx).id()
20598 })
20599 })
20600 .unwrap();
20601
20602 let buffer = project
20603 .update(cx, |project, cx| {
20604 project.open_buffer((worktree_id, "main.rs"), cx)
20605 })
20606 .await
20607 .unwrap();
20608
20609 let (editor, cx) = cx.add_window_view(|window, cx| {
20610 Editor::new(
20611 EditorMode::full(),
20612 MultiBuffer::build_from_buffer(buffer, cx),
20613 Some(project.clone()),
20614 window,
20615 cx,
20616 )
20617 });
20618
20619 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20620 let abs_path = project.read_with(cx, |project, cx| {
20621 project
20622 .absolute_path(&project_path, cx)
20623 .map(|path_buf| Arc::from(path_buf.to_owned()))
20624 .unwrap()
20625 });
20626
20627 // assert we can add breakpoint on the first line
20628 editor.update_in(cx, |editor, window, cx| {
20629 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20630 editor.move_to_end(&MoveToEnd, window, cx);
20631 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20632 });
20633
20634 let breakpoints = editor.update(cx, |editor, cx| {
20635 editor
20636 .breakpoint_store()
20637 .as_ref()
20638 .unwrap()
20639 .read(cx)
20640 .all_source_breakpoints(cx)
20641 .clone()
20642 });
20643
20644 assert_eq!(1, breakpoints.len());
20645 assert_breakpoint(
20646 &breakpoints,
20647 &abs_path,
20648 vec![
20649 (0, Breakpoint::new_standard()),
20650 (3, Breakpoint::new_standard()),
20651 ],
20652 );
20653
20654 editor.update_in(cx, |editor, window, cx| {
20655 editor.move_to_beginning(&MoveToBeginning, window, cx);
20656 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20657 });
20658
20659 let breakpoints = editor.update(cx, |editor, cx| {
20660 editor
20661 .breakpoint_store()
20662 .as_ref()
20663 .unwrap()
20664 .read(cx)
20665 .all_source_breakpoints(cx)
20666 .clone()
20667 });
20668
20669 assert_eq!(1, breakpoints.len());
20670 assert_breakpoint(
20671 &breakpoints,
20672 &abs_path,
20673 vec![(3, Breakpoint::new_standard())],
20674 );
20675
20676 editor.update_in(cx, |editor, window, cx| {
20677 editor.move_to_end(&MoveToEnd, window, cx);
20678 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20679 });
20680
20681 let breakpoints = editor.update(cx, |editor, cx| {
20682 editor
20683 .breakpoint_store()
20684 .as_ref()
20685 .unwrap()
20686 .read(cx)
20687 .all_source_breakpoints(cx)
20688 .clone()
20689 });
20690
20691 assert_eq!(0, breakpoints.len());
20692 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20693}
20694
20695#[gpui::test]
20696async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
20697 init_test(cx, |_| {});
20698
20699 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20700
20701 let fs = FakeFs::new(cx.executor());
20702 fs.insert_tree(
20703 path!("/a"),
20704 json!({
20705 "main.rs": sample_text,
20706 }),
20707 )
20708 .await;
20709 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20710 let (workspace, cx) =
20711 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20712
20713 let worktree_id = workspace.update(cx, |workspace, cx| {
20714 workspace.project().update(cx, |project, cx| {
20715 project.worktrees(cx).next().unwrap().read(cx).id()
20716 })
20717 });
20718
20719 let buffer = project
20720 .update(cx, |project, cx| {
20721 project.open_buffer((worktree_id, "main.rs"), cx)
20722 })
20723 .await
20724 .unwrap();
20725
20726 let (editor, cx) = cx.add_window_view(|window, cx| {
20727 Editor::new(
20728 EditorMode::full(),
20729 MultiBuffer::build_from_buffer(buffer, cx),
20730 Some(project.clone()),
20731 window,
20732 cx,
20733 )
20734 });
20735
20736 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20737 let abs_path = project.read_with(cx, |project, cx| {
20738 project
20739 .absolute_path(&project_path, cx)
20740 .map(|path_buf| Arc::from(path_buf.to_owned()))
20741 .unwrap()
20742 });
20743
20744 editor.update_in(cx, |editor, window, cx| {
20745 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20746 });
20747
20748 let breakpoints = editor.update(cx, |editor, cx| {
20749 editor
20750 .breakpoint_store()
20751 .as_ref()
20752 .unwrap()
20753 .read(cx)
20754 .all_source_breakpoints(cx)
20755 .clone()
20756 });
20757
20758 assert_breakpoint(
20759 &breakpoints,
20760 &abs_path,
20761 vec![(0, Breakpoint::new_log("hello world"))],
20762 );
20763
20764 // Removing a log message from a log breakpoint should remove it
20765 editor.update_in(cx, |editor, window, cx| {
20766 add_log_breakpoint_at_cursor(editor, "", window, cx);
20767 });
20768
20769 let breakpoints = editor.update(cx, |editor, cx| {
20770 editor
20771 .breakpoint_store()
20772 .as_ref()
20773 .unwrap()
20774 .read(cx)
20775 .all_source_breakpoints(cx)
20776 .clone()
20777 });
20778
20779 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20780
20781 editor.update_in(cx, |editor, window, cx| {
20782 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20783 editor.move_to_end(&MoveToEnd, window, cx);
20784 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20785 // Not adding a log message to a standard breakpoint shouldn't remove it
20786 add_log_breakpoint_at_cursor(editor, "", window, cx);
20787 });
20788
20789 let breakpoints = editor.update(cx, |editor, cx| {
20790 editor
20791 .breakpoint_store()
20792 .as_ref()
20793 .unwrap()
20794 .read(cx)
20795 .all_source_breakpoints(cx)
20796 .clone()
20797 });
20798
20799 assert_breakpoint(
20800 &breakpoints,
20801 &abs_path,
20802 vec![
20803 (0, Breakpoint::new_standard()),
20804 (3, Breakpoint::new_standard()),
20805 ],
20806 );
20807
20808 editor.update_in(cx, |editor, window, cx| {
20809 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20810 });
20811
20812 let breakpoints = editor.update(cx, |editor, cx| {
20813 editor
20814 .breakpoint_store()
20815 .as_ref()
20816 .unwrap()
20817 .read(cx)
20818 .all_source_breakpoints(cx)
20819 .clone()
20820 });
20821
20822 assert_breakpoint(
20823 &breakpoints,
20824 &abs_path,
20825 vec![
20826 (0, Breakpoint::new_standard()),
20827 (3, Breakpoint::new_log("hello world")),
20828 ],
20829 );
20830
20831 editor.update_in(cx, |editor, window, cx| {
20832 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
20833 });
20834
20835 let breakpoints = editor.update(cx, |editor, cx| {
20836 editor
20837 .breakpoint_store()
20838 .as_ref()
20839 .unwrap()
20840 .read(cx)
20841 .all_source_breakpoints(cx)
20842 .clone()
20843 });
20844
20845 assert_breakpoint(
20846 &breakpoints,
20847 &abs_path,
20848 vec![
20849 (0, Breakpoint::new_standard()),
20850 (3, Breakpoint::new_log("hello Earth!!")),
20851 ],
20852 );
20853}
20854
20855/// This also tests that Editor::breakpoint_at_cursor_head is working properly
20856/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
20857/// or when breakpoints were placed out of order. This tests for a regression too
20858#[gpui::test]
20859async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
20860 init_test(cx, |_| {});
20861
20862 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20863 let fs = FakeFs::new(cx.executor());
20864 fs.insert_tree(
20865 path!("/a"),
20866 json!({
20867 "main.rs": sample_text,
20868 }),
20869 )
20870 .await;
20871 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20872 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20873 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20874
20875 let fs = FakeFs::new(cx.executor());
20876 fs.insert_tree(
20877 path!("/a"),
20878 json!({
20879 "main.rs": sample_text,
20880 }),
20881 )
20882 .await;
20883 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20884 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20885 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20886 let worktree_id = workspace
20887 .update(cx, |workspace, _window, cx| {
20888 workspace.project().update(cx, |project, cx| {
20889 project.worktrees(cx).next().unwrap().read(cx).id()
20890 })
20891 })
20892 .unwrap();
20893
20894 let buffer = project
20895 .update(cx, |project, cx| {
20896 project.open_buffer((worktree_id, "main.rs"), cx)
20897 })
20898 .await
20899 .unwrap();
20900
20901 let (editor, cx) = cx.add_window_view(|window, cx| {
20902 Editor::new(
20903 EditorMode::full(),
20904 MultiBuffer::build_from_buffer(buffer, cx),
20905 Some(project.clone()),
20906 window,
20907 cx,
20908 )
20909 });
20910
20911 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20912 let abs_path = project.read_with(cx, |project, cx| {
20913 project
20914 .absolute_path(&project_path, cx)
20915 .map(|path_buf| Arc::from(path_buf.to_owned()))
20916 .unwrap()
20917 });
20918
20919 // assert we can add breakpoint on the first line
20920 editor.update_in(cx, |editor, window, cx| {
20921 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20922 editor.move_to_end(&MoveToEnd, window, cx);
20923 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20924 editor.move_up(&MoveUp, window, cx);
20925 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20926 });
20927
20928 let breakpoints = editor.update(cx, |editor, cx| {
20929 editor
20930 .breakpoint_store()
20931 .as_ref()
20932 .unwrap()
20933 .read(cx)
20934 .all_source_breakpoints(cx)
20935 .clone()
20936 });
20937
20938 assert_eq!(1, breakpoints.len());
20939 assert_breakpoint(
20940 &breakpoints,
20941 &abs_path,
20942 vec![
20943 (0, Breakpoint::new_standard()),
20944 (2, Breakpoint::new_standard()),
20945 (3, Breakpoint::new_standard()),
20946 ],
20947 );
20948
20949 editor.update_in(cx, |editor, window, cx| {
20950 editor.move_to_beginning(&MoveToBeginning, window, cx);
20951 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20952 editor.move_to_end(&MoveToEnd, window, cx);
20953 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20954 // Disabling a breakpoint that doesn't exist should do nothing
20955 editor.move_up(&MoveUp, window, cx);
20956 editor.move_up(&MoveUp, window, cx);
20957 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20958 });
20959
20960 let breakpoints = editor.update(cx, |editor, cx| {
20961 editor
20962 .breakpoint_store()
20963 .as_ref()
20964 .unwrap()
20965 .read(cx)
20966 .all_source_breakpoints(cx)
20967 .clone()
20968 });
20969
20970 let disable_breakpoint = {
20971 let mut bp = Breakpoint::new_standard();
20972 bp.state = BreakpointState::Disabled;
20973 bp
20974 };
20975
20976 assert_eq!(1, breakpoints.len());
20977 assert_breakpoint(
20978 &breakpoints,
20979 &abs_path,
20980 vec![
20981 (0, disable_breakpoint.clone()),
20982 (2, Breakpoint::new_standard()),
20983 (3, disable_breakpoint.clone()),
20984 ],
20985 );
20986
20987 editor.update_in(cx, |editor, window, cx| {
20988 editor.move_to_beginning(&MoveToBeginning, window, cx);
20989 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20990 editor.move_to_end(&MoveToEnd, window, cx);
20991 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20992 editor.move_up(&MoveUp, window, cx);
20993 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20994 });
20995
20996 let breakpoints = editor.update(cx, |editor, cx| {
20997 editor
20998 .breakpoint_store()
20999 .as_ref()
21000 .unwrap()
21001 .read(cx)
21002 .all_source_breakpoints(cx)
21003 .clone()
21004 });
21005
21006 assert_eq!(1, breakpoints.len());
21007 assert_breakpoint(
21008 &breakpoints,
21009 &abs_path,
21010 vec![
21011 (0, Breakpoint::new_standard()),
21012 (2, disable_breakpoint),
21013 (3, Breakpoint::new_standard()),
21014 ],
21015 );
21016}
21017
21018#[gpui::test]
21019async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
21020 init_test(cx, |_| {});
21021 let capabilities = lsp::ServerCapabilities {
21022 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
21023 prepare_provider: Some(true),
21024 work_done_progress_options: Default::default(),
21025 })),
21026 ..Default::default()
21027 };
21028 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21029
21030 cx.set_state(indoc! {"
21031 struct Fˇoo {}
21032 "});
21033
21034 cx.update_editor(|editor, _, cx| {
21035 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21036 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21037 editor.highlight_background::<DocumentHighlightRead>(
21038 &[highlight_range],
21039 |theme| theme.colors().editor_document_highlight_read_background,
21040 cx,
21041 );
21042 });
21043
21044 let mut prepare_rename_handler = cx
21045 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
21046 move |_, _, _| async move {
21047 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
21048 start: lsp::Position {
21049 line: 0,
21050 character: 7,
21051 },
21052 end: lsp::Position {
21053 line: 0,
21054 character: 10,
21055 },
21056 })))
21057 },
21058 );
21059 let prepare_rename_task = cx
21060 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21061 .expect("Prepare rename was not started");
21062 prepare_rename_handler.next().await.unwrap();
21063 prepare_rename_task.await.expect("Prepare rename failed");
21064
21065 let mut rename_handler =
21066 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21067 let edit = lsp::TextEdit {
21068 range: lsp::Range {
21069 start: lsp::Position {
21070 line: 0,
21071 character: 7,
21072 },
21073 end: lsp::Position {
21074 line: 0,
21075 character: 10,
21076 },
21077 },
21078 new_text: "FooRenamed".to_string(),
21079 };
21080 Ok(Some(lsp::WorkspaceEdit::new(
21081 // Specify the same edit twice
21082 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
21083 )))
21084 });
21085 let rename_task = cx
21086 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21087 .expect("Confirm rename was not started");
21088 rename_handler.next().await.unwrap();
21089 rename_task.await.expect("Confirm rename failed");
21090 cx.run_until_parked();
21091
21092 // Despite two edits, only one is actually applied as those are identical
21093 cx.assert_editor_state(indoc! {"
21094 struct FooRenamedˇ {}
21095 "});
21096}
21097
21098#[gpui::test]
21099async fn test_rename_without_prepare(cx: &mut TestAppContext) {
21100 init_test(cx, |_| {});
21101 // These capabilities indicate that the server does not support prepare rename.
21102 let capabilities = lsp::ServerCapabilities {
21103 rename_provider: Some(lsp::OneOf::Left(true)),
21104 ..Default::default()
21105 };
21106 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21107
21108 cx.set_state(indoc! {"
21109 struct Fˇoo {}
21110 "});
21111
21112 cx.update_editor(|editor, _window, cx| {
21113 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21114 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21115 editor.highlight_background::<DocumentHighlightRead>(
21116 &[highlight_range],
21117 |theme| theme.colors().editor_document_highlight_read_background,
21118 cx,
21119 );
21120 });
21121
21122 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21123 .expect("Prepare rename was not started")
21124 .await
21125 .expect("Prepare rename failed");
21126
21127 let mut rename_handler =
21128 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21129 let edit = lsp::TextEdit {
21130 range: lsp::Range {
21131 start: lsp::Position {
21132 line: 0,
21133 character: 7,
21134 },
21135 end: lsp::Position {
21136 line: 0,
21137 character: 10,
21138 },
21139 },
21140 new_text: "FooRenamed".to_string(),
21141 };
21142 Ok(Some(lsp::WorkspaceEdit::new(
21143 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
21144 )))
21145 });
21146 let rename_task = cx
21147 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21148 .expect("Confirm rename was not started");
21149 rename_handler.next().await.unwrap();
21150 rename_task.await.expect("Confirm rename failed");
21151 cx.run_until_parked();
21152
21153 // Correct range is renamed, as `surrounding_word` is used to find it.
21154 cx.assert_editor_state(indoc! {"
21155 struct FooRenamedˇ {}
21156 "});
21157}
21158
21159#[gpui::test]
21160async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
21161 init_test(cx, |_| {});
21162 let mut cx = EditorTestContext::new(cx).await;
21163
21164 let language = Arc::new(
21165 Language::new(
21166 LanguageConfig::default(),
21167 Some(tree_sitter_html::LANGUAGE.into()),
21168 )
21169 .with_brackets_query(
21170 r#"
21171 ("<" @open "/>" @close)
21172 ("</" @open ">" @close)
21173 ("<" @open ">" @close)
21174 ("\"" @open "\"" @close)
21175 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
21176 "#,
21177 )
21178 .unwrap(),
21179 );
21180 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21181
21182 cx.set_state(indoc! {"
21183 <span>ˇ</span>
21184 "});
21185 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21186 cx.assert_editor_state(indoc! {"
21187 <span>
21188 ˇ
21189 </span>
21190 "});
21191
21192 cx.set_state(indoc! {"
21193 <span><span></span>ˇ</span>
21194 "});
21195 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21196 cx.assert_editor_state(indoc! {"
21197 <span><span></span>
21198 ˇ</span>
21199 "});
21200
21201 cx.set_state(indoc! {"
21202 <span>ˇ
21203 </span>
21204 "});
21205 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21206 cx.assert_editor_state(indoc! {"
21207 <span>
21208 ˇ
21209 </span>
21210 "});
21211}
21212
21213#[gpui::test(iterations = 10)]
21214async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
21215 init_test(cx, |_| {});
21216
21217 let fs = FakeFs::new(cx.executor());
21218 fs.insert_tree(
21219 path!("/dir"),
21220 json!({
21221 "a.ts": "a",
21222 }),
21223 )
21224 .await;
21225
21226 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
21227 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21228 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21229
21230 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21231 language_registry.add(Arc::new(Language::new(
21232 LanguageConfig {
21233 name: "TypeScript".into(),
21234 matcher: LanguageMatcher {
21235 path_suffixes: vec!["ts".to_string()],
21236 ..Default::default()
21237 },
21238 ..Default::default()
21239 },
21240 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
21241 )));
21242 let mut fake_language_servers = language_registry.register_fake_lsp(
21243 "TypeScript",
21244 FakeLspAdapter {
21245 capabilities: lsp::ServerCapabilities {
21246 code_lens_provider: Some(lsp::CodeLensOptions {
21247 resolve_provider: Some(true),
21248 }),
21249 execute_command_provider: Some(lsp::ExecuteCommandOptions {
21250 commands: vec!["_the/command".to_string()],
21251 ..lsp::ExecuteCommandOptions::default()
21252 }),
21253 ..lsp::ServerCapabilities::default()
21254 },
21255 ..FakeLspAdapter::default()
21256 },
21257 );
21258
21259 let (buffer, _handle) = project
21260 .update(cx, |p, cx| {
21261 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
21262 })
21263 .await
21264 .unwrap();
21265 cx.executor().run_until_parked();
21266
21267 let fake_server = fake_language_servers.next().await.unwrap();
21268
21269 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
21270 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
21271 drop(buffer_snapshot);
21272 let actions = cx
21273 .update_window(*workspace, |_, window, cx| {
21274 project.code_actions(&buffer, anchor..anchor, window, cx)
21275 })
21276 .unwrap();
21277
21278 fake_server
21279 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21280 Ok(Some(vec![
21281 lsp::CodeLens {
21282 range: lsp::Range::default(),
21283 command: Some(lsp::Command {
21284 title: "Code lens command".to_owned(),
21285 command: "_the/command".to_owned(),
21286 arguments: None,
21287 }),
21288 data: None,
21289 },
21290 lsp::CodeLens {
21291 range: lsp::Range::default(),
21292 command: Some(lsp::Command {
21293 title: "Command not in capabilities".to_owned(),
21294 command: "not in capabilities".to_owned(),
21295 arguments: None,
21296 }),
21297 data: None,
21298 },
21299 lsp::CodeLens {
21300 range: lsp::Range {
21301 start: lsp::Position {
21302 line: 1,
21303 character: 1,
21304 },
21305 end: lsp::Position {
21306 line: 1,
21307 character: 1,
21308 },
21309 },
21310 command: Some(lsp::Command {
21311 title: "Command not in range".to_owned(),
21312 command: "_the/command".to_owned(),
21313 arguments: None,
21314 }),
21315 data: None,
21316 },
21317 ]))
21318 })
21319 .next()
21320 .await;
21321
21322 let actions = actions.await.unwrap();
21323 assert_eq!(
21324 actions.len(),
21325 1,
21326 "Should have only one valid action for the 0..0 range"
21327 );
21328 let action = actions[0].clone();
21329 let apply = project.update(cx, |project, cx| {
21330 project.apply_code_action(buffer.clone(), action, true, cx)
21331 });
21332
21333 // Resolving the code action does not populate its edits. In absence of
21334 // edits, we must execute the given command.
21335 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
21336 |mut lens, _| async move {
21337 let lens_command = lens.command.as_mut().expect("should have a command");
21338 assert_eq!(lens_command.title, "Code lens command");
21339 lens_command.arguments = Some(vec![json!("the-argument")]);
21340 Ok(lens)
21341 },
21342 );
21343
21344 // While executing the command, the language server sends the editor
21345 // a `workspaceEdit` request.
21346 fake_server
21347 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
21348 let fake = fake_server.clone();
21349 move |params, _| {
21350 assert_eq!(params.command, "_the/command");
21351 let fake = fake.clone();
21352 async move {
21353 fake.server
21354 .request::<lsp::request::ApplyWorkspaceEdit>(
21355 lsp::ApplyWorkspaceEditParams {
21356 label: None,
21357 edit: lsp::WorkspaceEdit {
21358 changes: Some(
21359 [(
21360 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
21361 vec![lsp::TextEdit {
21362 range: lsp::Range::new(
21363 lsp::Position::new(0, 0),
21364 lsp::Position::new(0, 0),
21365 ),
21366 new_text: "X".into(),
21367 }],
21368 )]
21369 .into_iter()
21370 .collect(),
21371 ),
21372 ..Default::default()
21373 },
21374 },
21375 )
21376 .await
21377 .into_response()
21378 .unwrap();
21379 Ok(Some(json!(null)))
21380 }
21381 }
21382 })
21383 .next()
21384 .await;
21385
21386 // Applying the code lens command returns a project transaction containing the edits
21387 // sent by the language server in its `workspaceEdit` request.
21388 let transaction = apply.await.unwrap();
21389 assert!(transaction.0.contains_key(&buffer));
21390 buffer.update(cx, |buffer, cx| {
21391 assert_eq!(buffer.text(), "Xa");
21392 buffer.undo(cx);
21393 assert_eq!(buffer.text(), "a");
21394 });
21395}
21396
21397#[gpui::test]
21398async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
21399 init_test(cx, |_| {});
21400
21401 let fs = FakeFs::new(cx.executor());
21402 let main_text = r#"fn main() {
21403println!("1");
21404println!("2");
21405println!("3");
21406println!("4");
21407println!("5");
21408}"#;
21409 let lib_text = "mod foo {}";
21410 fs.insert_tree(
21411 path!("/a"),
21412 json!({
21413 "lib.rs": lib_text,
21414 "main.rs": main_text,
21415 }),
21416 )
21417 .await;
21418
21419 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21420 let (workspace, cx) =
21421 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21422 let worktree_id = workspace.update(cx, |workspace, cx| {
21423 workspace.project().update(cx, |project, cx| {
21424 project.worktrees(cx).next().unwrap().read(cx).id()
21425 })
21426 });
21427
21428 let expected_ranges = vec![
21429 Point::new(0, 0)..Point::new(0, 0),
21430 Point::new(1, 0)..Point::new(1, 1),
21431 Point::new(2, 0)..Point::new(2, 2),
21432 Point::new(3, 0)..Point::new(3, 3),
21433 ];
21434
21435 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21436 let editor_1 = workspace
21437 .update_in(cx, |workspace, window, cx| {
21438 workspace.open_path(
21439 (worktree_id, "main.rs"),
21440 Some(pane_1.downgrade()),
21441 true,
21442 window,
21443 cx,
21444 )
21445 })
21446 .unwrap()
21447 .await
21448 .downcast::<Editor>()
21449 .unwrap();
21450 pane_1.update(cx, |pane, cx| {
21451 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21452 open_editor.update(cx, |editor, cx| {
21453 assert_eq!(
21454 editor.display_text(cx),
21455 main_text,
21456 "Original main.rs text on initial open",
21457 );
21458 assert_eq!(
21459 editor
21460 .selections
21461 .all::<Point>(cx)
21462 .into_iter()
21463 .map(|s| s.range())
21464 .collect::<Vec<_>>(),
21465 vec![Point::zero()..Point::zero()],
21466 "Default selections on initial open",
21467 );
21468 })
21469 });
21470 editor_1.update_in(cx, |editor, window, cx| {
21471 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21472 s.select_ranges(expected_ranges.clone());
21473 });
21474 });
21475
21476 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
21477 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
21478 });
21479 let editor_2 = workspace
21480 .update_in(cx, |workspace, window, cx| {
21481 workspace.open_path(
21482 (worktree_id, "main.rs"),
21483 Some(pane_2.downgrade()),
21484 true,
21485 window,
21486 cx,
21487 )
21488 })
21489 .unwrap()
21490 .await
21491 .downcast::<Editor>()
21492 .unwrap();
21493 pane_2.update(cx, |pane, cx| {
21494 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21495 open_editor.update(cx, |editor, cx| {
21496 assert_eq!(
21497 editor.display_text(cx),
21498 main_text,
21499 "Original main.rs text on initial open in another panel",
21500 );
21501 assert_eq!(
21502 editor
21503 .selections
21504 .all::<Point>(cx)
21505 .into_iter()
21506 .map(|s| s.range())
21507 .collect::<Vec<_>>(),
21508 vec![Point::zero()..Point::zero()],
21509 "Default selections on initial open in another panel",
21510 );
21511 })
21512 });
21513
21514 editor_2.update_in(cx, |editor, window, cx| {
21515 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
21516 });
21517
21518 let _other_editor_1 = workspace
21519 .update_in(cx, |workspace, window, cx| {
21520 workspace.open_path(
21521 (worktree_id, "lib.rs"),
21522 Some(pane_1.downgrade()),
21523 true,
21524 window,
21525 cx,
21526 )
21527 })
21528 .unwrap()
21529 .await
21530 .downcast::<Editor>()
21531 .unwrap();
21532 pane_1
21533 .update_in(cx, |pane, window, cx| {
21534 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
21535 })
21536 .await
21537 .unwrap();
21538 drop(editor_1);
21539 pane_1.update(cx, |pane, cx| {
21540 pane.active_item()
21541 .unwrap()
21542 .downcast::<Editor>()
21543 .unwrap()
21544 .update(cx, |editor, cx| {
21545 assert_eq!(
21546 editor.display_text(cx),
21547 lib_text,
21548 "Other file should be open and active",
21549 );
21550 });
21551 assert_eq!(pane.items().count(), 1, "No other editors should be open");
21552 });
21553
21554 let _other_editor_2 = workspace
21555 .update_in(cx, |workspace, window, cx| {
21556 workspace.open_path(
21557 (worktree_id, "lib.rs"),
21558 Some(pane_2.downgrade()),
21559 true,
21560 window,
21561 cx,
21562 )
21563 })
21564 .unwrap()
21565 .await
21566 .downcast::<Editor>()
21567 .unwrap();
21568 pane_2
21569 .update_in(cx, |pane, window, cx| {
21570 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
21571 })
21572 .await
21573 .unwrap();
21574 drop(editor_2);
21575 pane_2.update(cx, |pane, cx| {
21576 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21577 open_editor.update(cx, |editor, cx| {
21578 assert_eq!(
21579 editor.display_text(cx),
21580 lib_text,
21581 "Other file should be open and active in another panel too",
21582 );
21583 });
21584 assert_eq!(
21585 pane.items().count(),
21586 1,
21587 "No other editors should be open in another pane",
21588 );
21589 });
21590
21591 let _editor_1_reopened = workspace
21592 .update_in(cx, |workspace, window, cx| {
21593 workspace.open_path(
21594 (worktree_id, "main.rs"),
21595 Some(pane_1.downgrade()),
21596 true,
21597 window,
21598 cx,
21599 )
21600 })
21601 .unwrap()
21602 .await
21603 .downcast::<Editor>()
21604 .unwrap();
21605 let _editor_2_reopened = workspace
21606 .update_in(cx, |workspace, window, cx| {
21607 workspace.open_path(
21608 (worktree_id, "main.rs"),
21609 Some(pane_2.downgrade()),
21610 true,
21611 window,
21612 cx,
21613 )
21614 })
21615 .unwrap()
21616 .await
21617 .downcast::<Editor>()
21618 .unwrap();
21619 pane_1.update(cx, |pane, cx| {
21620 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21621 open_editor.update(cx, |editor, cx| {
21622 assert_eq!(
21623 editor.display_text(cx),
21624 main_text,
21625 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
21626 );
21627 assert_eq!(
21628 editor
21629 .selections
21630 .all::<Point>(cx)
21631 .into_iter()
21632 .map(|s| s.range())
21633 .collect::<Vec<_>>(),
21634 expected_ranges,
21635 "Previous editor in the 1st panel had selections and should get them restored on reopen",
21636 );
21637 })
21638 });
21639 pane_2.update(cx, |pane, cx| {
21640 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21641 open_editor.update(cx, |editor, cx| {
21642 assert_eq!(
21643 editor.display_text(cx),
21644 r#"fn main() {
21645⋯rintln!("1");
21646⋯intln!("2");
21647⋯ntln!("3");
21648println!("4");
21649println!("5");
21650}"#,
21651 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
21652 );
21653 assert_eq!(
21654 editor
21655 .selections
21656 .all::<Point>(cx)
21657 .into_iter()
21658 .map(|s| s.range())
21659 .collect::<Vec<_>>(),
21660 vec![Point::zero()..Point::zero()],
21661 "Previous editor in the 2nd pane had no selections changed hence should restore none",
21662 );
21663 })
21664 });
21665}
21666
21667#[gpui::test]
21668async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
21669 init_test(cx, |_| {});
21670
21671 let fs = FakeFs::new(cx.executor());
21672 let main_text = r#"fn main() {
21673println!("1");
21674println!("2");
21675println!("3");
21676println!("4");
21677println!("5");
21678}"#;
21679 let lib_text = "mod foo {}";
21680 fs.insert_tree(
21681 path!("/a"),
21682 json!({
21683 "lib.rs": lib_text,
21684 "main.rs": main_text,
21685 }),
21686 )
21687 .await;
21688
21689 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21690 let (workspace, cx) =
21691 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21692 let worktree_id = workspace.update(cx, |workspace, cx| {
21693 workspace.project().update(cx, |project, cx| {
21694 project.worktrees(cx).next().unwrap().read(cx).id()
21695 })
21696 });
21697
21698 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21699 let editor = workspace
21700 .update_in(cx, |workspace, window, cx| {
21701 workspace.open_path(
21702 (worktree_id, "main.rs"),
21703 Some(pane.downgrade()),
21704 true,
21705 window,
21706 cx,
21707 )
21708 })
21709 .unwrap()
21710 .await
21711 .downcast::<Editor>()
21712 .unwrap();
21713 pane.update(cx, |pane, cx| {
21714 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21715 open_editor.update(cx, |editor, cx| {
21716 assert_eq!(
21717 editor.display_text(cx),
21718 main_text,
21719 "Original main.rs text on initial open",
21720 );
21721 })
21722 });
21723 editor.update_in(cx, |editor, window, cx| {
21724 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
21725 });
21726
21727 cx.update_global(|store: &mut SettingsStore, cx| {
21728 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21729 s.restore_on_file_reopen = Some(false);
21730 });
21731 });
21732 editor.update_in(cx, |editor, window, cx| {
21733 editor.fold_ranges(
21734 vec![
21735 Point::new(1, 0)..Point::new(1, 1),
21736 Point::new(2, 0)..Point::new(2, 2),
21737 Point::new(3, 0)..Point::new(3, 3),
21738 ],
21739 false,
21740 window,
21741 cx,
21742 );
21743 });
21744 pane.update_in(cx, |pane, window, cx| {
21745 pane.close_all_items(&CloseAllItems::default(), window, cx)
21746 })
21747 .await
21748 .unwrap();
21749 pane.update(cx, |pane, _| {
21750 assert!(pane.active_item().is_none());
21751 });
21752 cx.update_global(|store: &mut SettingsStore, cx| {
21753 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21754 s.restore_on_file_reopen = Some(true);
21755 });
21756 });
21757
21758 let _editor_reopened = workspace
21759 .update_in(cx, |workspace, window, cx| {
21760 workspace.open_path(
21761 (worktree_id, "main.rs"),
21762 Some(pane.downgrade()),
21763 true,
21764 window,
21765 cx,
21766 )
21767 })
21768 .unwrap()
21769 .await
21770 .downcast::<Editor>()
21771 .unwrap();
21772 pane.update(cx, |pane, cx| {
21773 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21774 open_editor.update(cx, |editor, cx| {
21775 assert_eq!(
21776 editor.display_text(cx),
21777 main_text,
21778 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
21779 );
21780 })
21781 });
21782}
21783
21784#[gpui::test]
21785async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
21786 struct EmptyModalView {
21787 focus_handle: gpui::FocusHandle,
21788 }
21789 impl EventEmitter<DismissEvent> for EmptyModalView {}
21790 impl Render for EmptyModalView {
21791 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
21792 div()
21793 }
21794 }
21795 impl Focusable for EmptyModalView {
21796 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
21797 self.focus_handle.clone()
21798 }
21799 }
21800 impl workspace::ModalView for EmptyModalView {}
21801 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
21802 EmptyModalView {
21803 focus_handle: cx.focus_handle(),
21804 }
21805 }
21806
21807 init_test(cx, |_| {});
21808
21809 let fs = FakeFs::new(cx.executor());
21810 let project = Project::test(fs, [], cx).await;
21811 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21812 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
21813 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21814 let editor = cx.new_window_entity(|window, cx| {
21815 Editor::new(
21816 EditorMode::full(),
21817 buffer,
21818 Some(project.clone()),
21819 window,
21820 cx,
21821 )
21822 });
21823 workspace
21824 .update(cx, |workspace, window, cx| {
21825 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
21826 })
21827 .unwrap();
21828 editor.update_in(cx, |editor, window, cx| {
21829 editor.open_context_menu(&OpenContextMenu, window, cx);
21830 assert!(editor.mouse_context_menu.is_some());
21831 });
21832 workspace
21833 .update(cx, |workspace, window, cx| {
21834 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
21835 })
21836 .unwrap();
21837 cx.read(|cx| {
21838 assert!(editor.read(cx).mouse_context_menu.is_none());
21839 });
21840}
21841
21842#[gpui::test]
21843async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
21844 init_test(cx, |_| {});
21845
21846 let fs = FakeFs::new(cx.executor());
21847 fs.insert_file(path!("/file.html"), Default::default())
21848 .await;
21849
21850 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
21851
21852 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21853 let html_language = Arc::new(Language::new(
21854 LanguageConfig {
21855 name: "HTML".into(),
21856 matcher: LanguageMatcher {
21857 path_suffixes: vec!["html".to_string()],
21858 ..LanguageMatcher::default()
21859 },
21860 brackets: BracketPairConfig {
21861 pairs: vec![BracketPair {
21862 start: "<".into(),
21863 end: ">".into(),
21864 close: true,
21865 ..Default::default()
21866 }],
21867 ..Default::default()
21868 },
21869 ..Default::default()
21870 },
21871 Some(tree_sitter_html::LANGUAGE.into()),
21872 ));
21873 language_registry.add(html_language);
21874 let mut fake_servers = language_registry.register_fake_lsp(
21875 "HTML",
21876 FakeLspAdapter {
21877 capabilities: lsp::ServerCapabilities {
21878 completion_provider: Some(lsp::CompletionOptions {
21879 resolve_provider: Some(true),
21880 ..Default::default()
21881 }),
21882 ..Default::default()
21883 },
21884 ..Default::default()
21885 },
21886 );
21887
21888 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21889 let cx = &mut VisualTestContext::from_window(*workspace, cx);
21890
21891 let worktree_id = workspace
21892 .update(cx, |workspace, _window, cx| {
21893 workspace.project().update(cx, |project, cx| {
21894 project.worktrees(cx).next().unwrap().read(cx).id()
21895 })
21896 })
21897 .unwrap();
21898 project
21899 .update(cx, |project, cx| {
21900 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
21901 })
21902 .await
21903 .unwrap();
21904 let editor = workspace
21905 .update(cx, |workspace, window, cx| {
21906 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
21907 })
21908 .unwrap()
21909 .await
21910 .unwrap()
21911 .downcast::<Editor>()
21912 .unwrap();
21913
21914 let fake_server = fake_servers.next().await.unwrap();
21915 editor.update_in(cx, |editor, window, cx| {
21916 editor.set_text("<ad></ad>", window, cx);
21917 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21918 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
21919 });
21920 let Some((buffer, _)) = editor
21921 .buffer
21922 .read(cx)
21923 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
21924 else {
21925 panic!("Failed to get buffer for selection position");
21926 };
21927 let buffer = buffer.read(cx);
21928 let buffer_id = buffer.remote_id();
21929 let opening_range =
21930 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
21931 let closing_range =
21932 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
21933 let mut linked_ranges = HashMap::default();
21934 linked_ranges.insert(
21935 buffer_id,
21936 vec![(opening_range.clone(), vec![closing_range.clone()])],
21937 );
21938 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
21939 });
21940 let mut completion_handle =
21941 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
21942 Ok(Some(lsp::CompletionResponse::Array(vec![
21943 lsp::CompletionItem {
21944 label: "head".to_string(),
21945 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21946 lsp::InsertReplaceEdit {
21947 new_text: "head".to_string(),
21948 insert: lsp::Range::new(
21949 lsp::Position::new(0, 1),
21950 lsp::Position::new(0, 3),
21951 ),
21952 replace: lsp::Range::new(
21953 lsp::Position::new(0, 1),
21954 lsp::Position::new(0, 3),
21955 ),
21956 },
21957 )),
21958 ..Default::default()
21959 },
21960 ])))
21961 });
21962 editor.update_in(cx, |editor, window, cx| {
21963 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
21964 });
21965 cx.run_until_parked();
21966 completion_handle.next().await.unwrap();
21967 editor.update(cx, |editor, _| {
21968 assert!(
21969 editor.context_menu_visible(),
21970 "Completion menu should be visible"
21971 );
21972 });
21973 editor.update_in(cx, |editor, window, cx| {
21974 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
21975 });
21976 cx.executor().run_until_parked();
21977 editor.update(cx, |editor, cx| {
21978 assert_eq!(editor.text(cx), "<head></head>");
21979 });
21980}
21981
21982#[gpui::test]
21983async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
21984 init_test(cx, |_| {});
21985
21986 let fs = FakeFs::new(cx.executor());
21987 fs.insert_tree(
21988 path!("/root"),
21989 json!({
21990 "a": {
21991 "main.rs": "fn main() {}",
21992 },
21993 "foo": {
21994 "bar": {
21995 "external_file.rs": "pub mod external {}",
21996 }
21997 }
21998 }),
21999 )
22000 .await;
22001
22002 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
22003 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22004 language_registry.add(rust_lang());
22005 let _fake_servers = language_registry.register_fake_lsp(
22006 "Rust",
22007 FakeLspAdapter {
22008 ..FakeLspAdapter::default()
22009 },
22010 );
22011 let (workspace, cx) =
22012 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22013 let worktree_id = workspace.update(cx, |workspace, cx| {
22014 workspace.project().update(cx, |project, cx| {
22015 project.worktrees(cx).next().unwrap().read(cx).id()
22016 })
22017 });
22018
22019 let assert_language_servers_count =
22020 |expected: usize, context: &str, cx: &mut VisualTestContext| {
22021 project.update(cx, |project, cx| {
22022 let current = project
22023 .lsp_store()
22024 .read(cx)
22025 .as_local()
22026 .unwrap()
22027 .language_servers
22028 .len();
22029 assert_eq!(expected, current, "{context}");
22030 });
22031 };
22032
22033 assert_language_servers_count(
22034 0,
22035 "No servers should be running before any file is open",
22036 cx,
22037 );
22038 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22039 let main_editor = workspace
22040 .update_in(cx, |workspace, window, cx| {
22041 workspace.open_path(
22042 (worktree_id, "main.rs"),
22043 Some(pane.downgrade()),
22044 true,
22045 window,
22046 cx,
22047 )
22048 })
22049 .unwrap()
22050 .await
22051 .downcast::<Editor>()
22052 .unwrap();
22053 pane.update(cx, |pane, cx| {
22054 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22055 open_editor.update(cx, |editor, cx| {
22056 assert_eq!(
22057 editor.display_text(cx),
22058 "fn main() {}",
22059 "Original main.rs text on initial open",
22060 );
22061 });
22062 assert_eq!(open_editor, main_editor);
22063 });
22064 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
22065
22066 let external_editor = workspace
22067 .update_in(cx, |workspace, window, cx| {
22068 workspace.open_abs_path(
22069 PathBuf::from("/root/foo/bar/external_file.rs"),
22070 OpenOptions::default(),
22071 window,
22072 cx,
22073 )
22074 })
22075 .await
22076 .expect("opening external file")
22077 .downcast::<Editor>()
22078 .expect("downcasted external file's open element to editor");
22079 pane.update(cx, |pane, cx| {
22080 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22081 open_editor.update(cx, |editor, cx| {
22082 assert_eq!(
22083 editor.display_text(cx),
22084 "pub mod external {}",
22085 "External file is open now",
22086 );
22087 });
22088 assert_eq!(open_editor, external_editor);
22089 });
22090 assert_language_servers_count(
22091 1,
22092 "Second, external, *.rs file should join the existing server",
22093 cx,
22094 );
22095
22096 pane.update_in(cx, |pane, window, cx| {
22097 pane.close_active_item(&CloseActiveItem::default(), window, cx)
22098 })
22099 .await
22100 .unwrap();
22101 pane.update_in(cx, |pane, window, cx| {
22102 pane.navigate_backward(window, cx);
22103 });
22104 cx.run_until_parked();
22105 pane.update(cx, |pane, cx| {
22106 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22107 open_editor.update(cx, |editor, cx| {
22108 assert_eq!(
22109 editor.display_text(cx),
22110 "pub mod external {}",
22111 "External file is open now",
22112 );
22113 });
22114 });
22115 assert_language_servers_count(
22116 1,
22117 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
22118 cx,
22119 );
22120
22121 cx.update(|_, cx| {
22122 workspace::reload(&workspace::Reload::default(), cx);
22123 });
22124 assert_language_servers_count(
22125 1,
22126 "After reloading the worktree with local and external files opened, only one project should be started",
22127 cx,
22128 );
22129}
22130
22131#[gpui::test]
22132async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
22133 init_test(cx, |_| {});
22134
22135 let mut cx = EditorTestContext::new(cx).await;
22136 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22137 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22138
22139 // test cursor move to start of each line on tab
22140 // for `if`, `elif`, `else`, `while`, `with` and `for`
22141 cx.set_state(indoc! {"
22142 def main():
22143 ˇ for item in items:
22144 ˇ while item.active:
22145 ˇ if item.value > 10:
22146 ˇ continue
22147 ˇ elif item.value < 0:
22148 ˇ break
22149 ˇ else:
22150 ˇ with item.context() as ctx:
22151 ˇ yield count
22152 ˇ else:
22153 ˇ log('while else')
22154 ˇ else:
22155 ˇ log('for else')
22156 "});
22157 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22158 cx.assert_editor_state(indoc! {"
22159 def main():
22160 ˇfor item in items:
22161 ˇwhile item.active:
22162 ˇif item.value > 10:
22163 ˇcontinue
22164 ˇelif item.value < 0:
22165 ˇbreak
22166 ˇelse:
22167 ˇwith item.context() as ctx:
22168 ˇyield count
22169 ˇelse:
22170 ˇlog('while else')
22171 ˇelse:
22172 ˇlog('for else')
22173 "});
22174 // test relative indent is preserved when tab
22175 // for `if`, `elif`, `else`, `while`, `with` and `for`
22176 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22177 cx.assert_editor_state(indoc! {"
22178 def main():
22179 ˇfor item in items:
22180 ˇwhile item.active:
22181 ˇif item.value > 10:
22182 ˇcontinue
22183 ˇelif item.value < 0:
22184 ˇbreak
22185 ˇelse:
22186 ˇwith item.context() as ctx:
22187 ˇyield count
22188 ˇelse:
22189 ˇlog('while else')
22190 ˇelse:
22191 ˇlog('for else')
22192 "});
22193
22194 // test cursor move to start of each line on tab
22195 // for `try`, `except`, `else`, `finally`, `match` and `def`
22196 cx.set_state(indoc! {"
22197 def main():
22198 ˇ try:
22199 ˇ fetch()
22200 ˇ except ValueError:
22201 ˇ handle_error()
22202 ˇ else:
22203 ˇ match value:
22204 ˇ case _:
22205 ˇ finally:
22206 ˇ def status():
22207 ˇ return 0
22208 "});
22209 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22210 cx.assert_editor_state(indoc! {"
22211 def main():
22212 ˇtry:
22213 ˇfetch()
22214 ˇexcept ValueError:
22215 ˇhandle_error()
22216 ˇelse:
22217 ˇmatch value:
22218 ˇcase _:
22219 ˇfinally:
22220 ˇdef status():
22221 ˇreturn 0
22222 "});
22223 // test relative indent is preserved when tab
22224 // for `try`, `except`, `else`, `finally`, `match` and `def`
22225 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22226 cx.assert_editor_state(indoc! {"
22227 def main():
22228 ˇtry:
22229 ˇfetch()
22230 ˇexcept ValueError:
22231 ˇhandle_error()
22232 ˇelse:
22233 ˇmatch value:
22234 ˇcase _:
22235 ˇfinally:
22236 ˇdef status():
22237 ˇreturn 0
22238 "});
22239}
22240
22241#[gpui::test]
22242async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
22243 init_test(cx, |_| {});
22244
22245 let mut cx = EditorTestContext::new(cx).await;
22246 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22247 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22248
22249 // test `else` auto outdents when typed inside `if` block
22250 cx.set_state(indoc! {"
22251 def main():
22252 if i == 2:
22253 return
22254 ˇ
22255 "});
22256 cx.update_editor(|editor, window, cx| {
22257 editor.handle_input("else:", window, cx);
22258 });
22259 cx.assert_editor_state(indoc! {"
22260 def main():
22261 if i == 2:
22262 return
22263 else:ˇ
22264 "});
22265
22266 // test `except` auto outdents when typed inside `try` block
22267 cx.set_state(indoc! {"
22268 def main():
22269 try:
22270 i = 2
22271 ˇ
22272 "});
22273 cx.update_editor(|editor, window, cx| {
22274 editor.handle_input("except:", window, cx);
22275 });
22276 cx.assert_editor_state(indoc! {"
22277 def main():
22278 try:
22279 i = 2
22280 except:ˇ
22281 "});
22282
22283 // test `else` auto outdents when typed inside `except` block
22284 cx.set_state(indoc! {"
22285 def main():
22286 try:
22287 i = 2
22288 except:
22289 j = 2
22290 ˇ
22291 "});
22292 cx.update_editor(|editor, window, cx| {
22293 editor.handle_input("else:", window, cx);
22294 });
22295 cx.assert_editor_state(indoc! {"
22296 def main():
22297 try:
22298 i = 2
22299 except:
22300 j = 2
22301 else:ˇ
22302 "});
22303
22304 // test `finally` auto outdents when typed inside `else` block
22305 cx.set_state(indoc! {"
22306 def main():
22307 try:
22308 i = 2
22309 except:
22310 j = 2
22311 else:
22312 k = 2
22313 ˇ
22314 "});
22315 cx.update_editor(|editor, window, cx| {
22316 editor.handle_input("finally:", window, cx);
22317 });
22318 cx.assert_editor_state(indoc! {"
22319 def main():
22320 try:
22321 i = 2
22322 except:
22323 j = 2
22324 else:
22325 k = 2
22326 finally:ˇ
22327 "});
22328
22329 // test `else` does not outdents when typed inside `except` block right after for block
22330 cx.set_state(indoc! {"
22331 def main():
22332 try:
22333 i = 2
22334 except:
22335 for i in range(n):
22336 pass
22337 ˇ
22338 "});
22339 cx.update_editor(|editor, window, cx| {
22340 editor.handle_input("else:", window, cx);
22341 });
22342 cx.assert_editor_state(indoc! {"
22343 def main():
22344 try:
22345 i = 2
22346 except:
22347 for i in range(n):
22348 pass
22349 else:ˇ
22350 "});
22351
22352 // test `finally` auto outdents when typed inside `else` block right after for block
22353 cx.set_state(indoc! {"
22354 def main():
22355 try:
22356 i = 2
22357 except:
22358 j = 2
22359 else:
22360 for i in range(n):
22361 pass
22362 ˇ
22363 "});
22364 cx.update_editor(|editor, window, cx| {
22365 editor.handle_input("finally:", window, cx);
22366 });
22367 cx.assert_editor_state(indoc! {"
22368 def main():
22369 try:
22370 i = 2
22371 except:
22372 j = 2
22373 else:
22374 for i in range(n):
22375 pass
22376 finally:ˇ
22377 "});
22378
22379 // test `except` outdents to inner "try" block
22380 cx.set_state(indoc! {"
22381 def main():
22382 try:
22383 i = 2
22384 if i == 2:
22385 try:
22386 i = 3
22387 ˇ
22388 "});
22389 cx.update_editor(|editor, window, cx| {
22390 editor.handle_input("except:", window, cx);
22391 });
22392 cx.assert_editor_state(indoc! {"
22393 def main():
22394 try:
22395 i = 2
22396 if i == 2:
22397 try:
22398 i = 3
22399 except:ˇ
22400 "});
22401
22402 // test `except` outdents to outer "try" block
22403 cx.set_state(indoc! {"
22404 def main():
22405 try:
22406 i = 2
22407 if i == 2:
22408 try:
22409 i = 3
22410 ˇ
22411 "});
22412 cx.update_editor(|editor, window, cx| {
22413 editor.handle_input("except:", window, cx);
22414 });
22415 cx.assert_editor_state(indoc! {"
22416 def main():
22417 try:
22418 i = 2
22419 if i == 2:
22420 try:
22421 i = 3
22422 except:ˇ
22423 "});
22424
22425 // test `else` stays at correct indent when typed after `for` block
22426 cx.set_state(indoc! {"
22427 def main():
22428 for i in range(10):
22429 if i == 3:
22430 break
22431 ˇ
22432 "});
22433 cx.update_editor(|editor, window, cx| {
22434 editor.handle_input("else:", window, cx);
22435 });
22436 cx.assert_editor_state(indoc! {"
22437 def main():
22438 for i in range(10):
22439 if i == 3:
22440 break
22441 else:ˇ
22442 "});
22443
22444 // test does not outdent on typing after line with square brackets
22445 cx.set_state(indoc! {"
22446 def f() -> list[str]:
22447 ˇ
22448 "});
22449 cx.update_editor(|editor, window, cx| {
22450 editor.handle_input("a", window, cx);
22451 });
22452 cx.assert_editor_state(indoc! {"
22453 def f() -> list[str]:
22454 aˇ
22455 "});
22456
22457 // test does not outdent on typing : after case keyword
22458 cx.set_state(indoc! {"
22459 match 1:
22460 caseˇ
22461 "});
22462 cx.update_editor(|editor, window, cx| {
22463 editor.handle_input(":", window, cx);
22464 });
22465 cx.assert_editor_state(indoc! {"
22466 match 1:
22467 case:ˇ
22468 "});
22469}
22470
22471#[gpui::test]
22472async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
22473 init_test(cx, |_| {});
22474 update_test_language_settings(cx, |settings| {
22475 settings.defaults.extend_comment_on_newline = Some(false);
22476 });
22477 let mut cx = EditorTestContext::new(cx).await;
22478 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22479 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22480
22481 // test correct indent after newline on comment
22482 cx.set_state(indoc! {"
22483 # COMMENT:ˇ
22484 "});
22485 cx.update_editor(|editor, window, cx| {
22486 editor.newline(&Newline, window, cx);
22487 });
22488 cx.assert_editor_state(indoc! {"
22489 # COMMENT:
22490 ˇ
22491 "});
22492
22493 // test correct indent after newline in brackets
22494 cx.set_state(indoc! {"
22495 {ˇ}
22496 "});
22497 cx.update_editor(|editor, window, cx| {
22498 editor.newline(&Newline, window, cx);
22499 });
22500 cx.run_until_parked();
22501 cx.assert_editor_state(indoc! {"
22502 {
22503 ˇ
22504 }
22505 "});
22506
22507 cx.set_state(indoc! {"
22508 (ˇ)
22509 "});
22510 cx.update_editor(|editor, window, cx| {
22511 editor.newline(&Newline, window, cx);
22512 });
22513 cx.run_until_parked();
22514 cx.assert_editor_state(indoc! {"
22515 (
22516 ˇ
22517 )
22518 "});
22519
22520 // do not indent after empty lists or dictionaries
22521 cx.set_state(indoc! {"
22522 a = []ˇ
22523 "});
22524 cx.update_editor(|editor, window, cx| {
22525 editor.newline(&Newline, window, cx);
22526 });
22527 cx.run_until_parked();
22528 cx.assert_editor_state(indoc! {"
22529 a = []
22530 ˇ
22531 "});
22532}
22533
22534fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
22535 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
22536 point..point
22537}
22538
22539#[track_caller]
22540fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
22541 let (text, ranges) = marked_text_ranges(marked_text, true);
22542 assert_eq!(editor.text(cx), text);
22543 assert_eq!(
22544 editor.selections.ranges(cx),
22545 ranges,
22546 "Assert selections are {}",
22547 marked_text
22548 );
22549}
22550
22551pub fn handle_signature_help_request(
22552 cx: &mut EditorLspTestContext,
22553 mocked_response: lsp::SignatureHelp,
22554) -> impl Future<Output = ()> + use<> {
22555 let mut request =
22556 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
22557 let mocked_response = mocked_response.clone();
22558 async move { Ok(Some(mocked_response)) }
22559 });
22560
22561 async move {
22562 request.next().await;
22563 }
22564}
22565
22566#[track_caller]
22567pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
22568 cx.update_editor(|editor, _, _| {
22569 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
22570 let entries = menu.entries.borrow();
22571 let entries = entries
22572 .iter()
22573 .map(|entry| entry.string.as_str())
22574 .collect::<Vec<_>>();
22575 assert_eq!(entries, expected);
22576 } else {
22577 panic!("Expected completions menu");
22578 }
22579 });
22580}
22581
22582/// Handle completion request passing a marked string specifying where the completion
22583/// should be triggered from using '|' character, what range should be replaced, and what completions
22584/// should be returned using '<' and '>' to delimit the range.
22585///
22586/// Also see `handle_completion_request_with_insert_and_replace`.
22587#[track_caller]
22588pub fn handle_completion_request(
22589 marked_string: &str,
22590 completions: Vec<&'static str>,
22591 is_incomplete: bool,
22592 counter: Arc<AtomicUsize>,
22593 cx: &mut EditorLspTestContext,
22594) -> impl Future<Output = ()> {
22595 let complete_from_marker: TextRangeMarker = '|'.into();
22596 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22597 let (_, mut marked_ranges) = marked_text_ranges_by(
22598 marked_string,
22599 vec![complete_from_marker.clone(), replace_range_marker.clone()],
22600 );
22601
22602 let complete_from_position =
22603 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22604 let replace_range =
22605 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22606
22607 let mut request =
22608 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22609 let completions = completions.clone();
22610 counter.fetch_add(1, atomic::Ordering::Release);
22611 async move {
22612 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22613 assert_eq!(
22614 params.text_document_position.position,
22615 complete_from_position
22616 );
22617 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
22618 is_incomplete: is_incomplete,
22619 item_defaults: None,
22620 items: completions
22621 .iter()
22622 .map(|completion_text| lsp::CompletionItem {
22623 label: completion_text.to_string(),
22624 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
22625 range: replace_range,
22626 new_text: completion_text.to_string(),
22627 })),
22628 ..Default::default()
22629 })
22630 .collect(),
22631 })))
22632 }
22633 });
22634
22635 async move {
22636 request.next().await;
22637 }
22638}
22639
22640/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
22641/// given instead, which also contains an `insert` range.
22642///
22643/// This function uses markers to define ranges:
22644/// - `|` marks the cursor position
22645/// - `<>` marks the replace range
22646/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
22647pub fn handle_completion_request_with_insert_and_replace(
22648 cx: &mut EditorLspTestContext,
22649 marked_string: &str,
22650 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
22651 counter: Arc<AtomicUsize>,
22652) -> impl Future<Output = ()> {
22653 let complete_from_marker: TextRangeMarker = '|'.into();
22654 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22655 let insert_range_marker: TextRangeMarker = ('{', '}').into();
22656
22657 let (_, mut marked_ranges) = marked_text_ranges_by(
22658 marked_string,
22659 vec![
22660 complete_from_marker.clone(),
22661 replace_range_marker.clone(),
22662 insert_range_marker.clone(),
22663 ],
22664 );
22665
22666 let complete_from_position =
22667 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22668 let replace_range =
22669 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22670
22671 let insert_range = match marked_ranges.remove(&insert_range_marker) {
22672 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
22673 _ => lsp::Range {
22674 start: replace_range.start,
22675 end: complete_from_position,
22676 },
22677 };
22678
22679 let mut request =
22680 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22681 let completions = completions.clone();
22682 counter.fetch_add(1, atomic::Ordering::Release);
22683 async move {
22684 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22685 assert_eq!(
22686 params.text_document_position.position, complete_from_position,
22687 "marker `|` position doesn't match",
22688 );
22689 Ok(Some(lsp::CompletionResponse::Array(
22690 completions
22691 .iter()
22692 .map(|(label, new_text)| lsp::CompletionItem {
22693 label: label.to_string(),
22694 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22695 lsp::InsertReplaceEdit {
22696 insert: insert_range,
22697 replace: replace_range,
22698 new_text: new_text.to_string(),
22699 },
22700 )),
22701 ..Default::default()
22702 })
22703 .collect(),
22704 )))
22705 }
22706 });
22707
22708 async move {
22709 request.next().await;
22710 }
22711}
22712
22713fn handle_resolve_completion_request(
22714 cx: &mut EditorLspTestContext,
22715 edits: Option<Vec<(&'static str, &'static str)>>,
22716) -> impl Future<Output = ()> {
22717 let edits = edits.map(|edits| {
22718 edits
22719 .iter()
22720 .map(|(marked_string, new_text)| {
22721 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
22722 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
22723 lsp::TextEdit::new(replace_range, new_text.to_string())
22724 })
22725 .collect::<Vec<_>>()
22726 });
22727
22728 let mut request =
22729 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
22730 let edits = edits.clone();
22731 async move {
22732 Ok(lsp::CompletionItem {
22733 additional_text_edits: edits,
22734 ..Default::default()
22735 })
22736 }
22737 });
22738
22739 async move {
22740 request.next().await;
22741 }
22742}
22743
22744pub(crate) fn update_test_language_settings(
22745 cx: &mut TestAppContext,
22746 f: impl Fn(&mut AllLanguageSettingsContent),
22747) {
22748 cx.update(|cx| {
22749 SettingsStore::update_global(cx, |store, cx| {
22750 store.update_user_settings::<AllLanguageSettings>(cx, f);
22751 });
22752 });
22753}
22754
22755pub(crate) fn update_test_project_settings(
22756 cx: &mut TestAppContext,
22757 f: impl Fn(&mut ProjectSettings),
22758) {
22759 cx.update(|cx| {
22760 SettingsStore::update_global(cx, |store, cx| {
22761 store.update_user_settings::<ProjectSettings>(cx, f);
22762 });
22763 });
22764}
22765
22766pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
22767 cx.update(|cx| {
22768 assets::Assets.load_test_fonts(cx);
22769 let store = SettingsStore::test(cx);
22770 cx.set_global(store);
22771 theme::init(theme::LoadThemes::JustBase, cx);
22772 release_channel::init(SemanticVersion::default(), cx);
22773 client::init_settings(cx);
22774 language::init(cx);
22775 Project::init_settings(cx);
22776 workspace::init_settings(cx);
22777 crate::init(cx);
22778 });
22779 zlog::init_test();
22780 update_test_language_settings(cx, f);
22781}
22782
22783#[track_caller]
22784fn assert_hunk_revert(
22785 not_reverted_text_with_selections: &str,
22786 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
22787 expected_reverted_text_with_selections: &str,
22788 base_text: &str,
22789 cx: &mut EditorLspTestContext,
22790) {
22791 cx.set_state(not_reverted_text_with_selections);
22792 cx.set_head_text(base_text);
22793 cx.executor().run_until_parked();
22794
22795 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
22796 let snapshot = editor.snapshot(window, cx);
22797 let reverted_hunk_statuses = snapshot
22798 .buffer_snapshot
22799 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
22800 .map(|hunk| hunk.status().kind)
22801 .collect::<Vec<_>>();
22802
22803 editor.git_restore(&Default::default(), window, cx);
22804 reverted_hunk_statuses
22805 });
22806 cx.executor().run_until_parked();
22807 cx.assert_editor_state(expected_reverted_text_with_selections);
22808 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
22809}
22810
22811#[gpui::test(iterations = 10)]
22812async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
22813 init_test(cx, |_| {});
22814
22815 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
22816 let counter = diagnostic_requests.clone();
22817
22818 let fs = FakeFs::new(cx.executor());
22819 fs.insert_tree(
22820 path!("/a"),
22821 json!({
22822 "first.rs": "fn main() { let a = 5; }",
22823 "second.rs": "// Test file",
22824 }),
22825 )
22826 .await;
22827
22828 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22829 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22830 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22831
22832 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22833 language_registry.add(rust_lang());
22834 let mut fake_servers = language_registry.register_fake_lsp(
22835 "Rust",
22836 FakeLspAdapter {
22837 capabilities: lsp::ServerCapabilities {
22838 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
22839 lsp::DiagnosticOptions {
22840 identifier: None,
22841 inter_file_dependencies: true,
22842 workspace_diagnostics: true,
22843 work_done_progress_options: Default::default(),
22844 },
22845 )),
22846 ..Default::default()
22847 },
22848 ..Default::default()
22849 },
22850 );
22851
22852 let editor = workspace
22853 .update(cx, |workspace, window, cx| {
22854 workspace.open_abs_path(
22855 PathBuf::from(path!("/a/first.rs")),
22856 OpenOptions::default(),
22857 window,
22858 cx,
22859 )
22860 })
22861 .unwrap()
22862 .await
22863 .unwrap()
22864 .downcast::<Editor>()
22865 .unwrap();
22866 let fake_server = fake_servers.next().await.unwrap();
22867 let server_id = fake_server.server.server_id();
22868 let mut first_request = fake_server
22869 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
22870 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
22871 let result_id = Some(new_result_id.to_string());
22872 assert_eq!(
22873 params.text_document.uri,
22874 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
22875 );
22876 async move {
22877 Ok(lsp::DocumentDiagnosticReportResult::Report(
22878 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
22879 related_documents: None,
22880 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
22881 items: Vec::new(),
22882 result_id,
22883 },
22884 }),
22885 ))
22886 }
22887 });
22888
22889 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
22890 project.update(cx, |project, cx| {
22891 let buffer_id = editor
22892 .read(cx)
22893 .buffer()
22894 .read(cx)
22895 .as_singleton()
22896 .expect("created a singleton buffer")
22897 .read(cx)
22898 .remote_id();
22899 let buffer_result_id = project
22900 .lsp_store()
22901 .read(cx)
22902 .result_id(server_id, buffer_id, cx);
22903 assert_eq!(expected, buffer_result_id);
22904 });
22905 };
22906
22907 ensure_result_id(None, cx);
22908 cx.executor().advance_clock(Duration::from_millis(60));
22909 cx.executor().run_until_parked();
22910 assert_eq!(
22911 diagnostic_requests.load(atomic::Ordering::Acquire),
22912 1,
22913 "Opening file should trigger diagnostic request"
22914 );
22915 first_request
22916 .next()
22917 .await
22918 .expect("should have sent the first diagnostics pull request");
22919 ensure_result_id(Some("1".to_string()), cx);
22920
22921 // Editing should trigger diagnostics
22922 editor.update_in(cx, |editor, window, cx| {
22923 editor.handle_input("2", window, cx)
22924 });
22925 cx.executor().advance_clock(Duration::from_millis(60));
22926 cx.executor().run_until_parked();
22927 assert_eq!(
22928 diagnostic_requests.load(atomic::Ordering::Acquire),
22929 2,
22930 "Editing should trigger diagnostic request"
22931 );
22932 ensure_result_id(Some("2".to_string()), cx);
22933
22934 // Moving cursor should not trigger diagnostic request
22935 editor.update_in(cx, |editor, window, cx| {
22936 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22937 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
22938 });
22939 });
22940 cx.executor().advance_clock(Duration::from_millis(60));
22941 cx.executor().run_until_parked();
22942 assert_eq!(
22943 diagnostic_requests.load(atomic::Ordering::Acquire),
22944 2,
22945 "Cursor movement should not trigger diagnostic request"
22946 );
22947 ensure_result_id(Some("2".to_string()), cx);
22948 // Multiple rapid edits should be debounced
22949 for _ in 0..5 {
22950 editor.update_in(cx, |editor, window, cx| {
22951 editor.handle_input("x", window, cx)
22952 });
22953 }
22954 cx.executor().advance_clock(Duration::from_millis(60));
22955 cx.executor().run_until_parked();
22956
22957 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
22958 assert!(
22959 final_requests <= 4,
22960 "Multiple rapid edits should be debounced (got {final_requests} requests)",
22961 );
22962 ensure_result_id(Some(final_requests.to_string()), cx);
22963}
22964
22965#[gpui::test]
22966async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
22967 // Regression test for issue #11671
22968 // Previously, adding a cursor after moving multiple cursors would reset
22969 // the cursor count instead of adding to the existing cursors.
22970 init_test(cx, |_| {});
22971 let mut cx = EditorTestContext::new(cx).await;
22972
22973 // Create a simple buffer with cursor at start
22974 cx.set_state(indoc! {"
22975 ˇaaaa
22976 bbbb
22977 cccc
22978 dddd
22979 eeee
22980 ffff
22981 gggg
22982 hhhh"});
22983
22984 // Add 2 cursors below (so we have 3 total)
22985 cx.update_editor(|editor, window, cx| {
22986 editor.add_selection_below(&Default::default(), window, cx);
22987 editor.add_selection_below(&Default::default(), window, cx);
22988 });
22989
22990 // Verify we have 3 cursors
22991 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
22992 assert_eq!(
22993 initial_count, 3,
22994 "Should have 3 cursors after adding 2 below"
22995 );
22996
22997 // Move down one line
22998 cx.update_editor(|editor, window, cx| {
22999 editor.move_down(&MoveDown, window, cx);
23000 });
23001
23002 // Add another cursor below
23003 cx.update_editor(|editor, window, cx| {
23004 editor.add_selection_below(&Default::default(), window, cx);
23005 });
23006
23007 // Should now have 4 cursors (3 original + 1 new)
23008 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
23009 assert_eq!(
23010 final_count, 4,
23011 "Should have 4 cursors after moving and adding another"
23012 );
23013}
23014
23015#[gpui::test(iterations = 10)]
23016async fn test_document_colors(cx: &mut TestAppContext) {
23017 let expected_color = Rgba {
23018 r: 0.33,
23019 g: 0.33,
23020 b: 0.33,
23021 a: 0.33,
23022 };
23023
23024 init_test(cx, |_| {});
23025
23026 let fs = FakeFs::new(cx.executor());
23027 fs.insert_tree(
23028 path!("/a"),
23029 json!({
23030 "first.rs": "fn main() { let a = 5; }",
23031 }),
23032 )
23033 .await;
23034
23035 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23036 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23037 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23038
23039 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23040 language_registry.add(rust_lang());
23041 let mut fake_servers = language_registry.register_fake_lsp(
23042 "Rust",
23043 FakeLspAdapter {
23044 capabilities: lsp::ServerCapabilities {
23045 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
23046 ..lsp::ServerCapabilities::default()
23047 },
23048 name: "rust-analyzer",
23049 ..FakeLspAdapter::default()
23050 },
23051 );
23052 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
23053 "Rust",
23054 FakeLspAdapter {
23055 capabilities: lsp::ServerCapabilities {
23056 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
23057 ..lsp::ServerCapabilities::default()
23058 },
23059 name: "not-rust-analyzer",
23060 ..FakeLspAdapter::default()
23061 },
23062 );
23063
23064 let editor = workspace
23065 .update(cx, |workspace, window, cx| {
23066 workspace.open_abs_path(
23067 PathBuf::from(path!("/a/first.rs")),
23068 OpenOptions::default(),
23069 window,
23070 cx,
23071 )
23072 })
23073 .unwrap()
23074 .await
23075 .unwrap()
23076 .downcast::<Editor>()
23077 .unwrap();
23078 let fake_language_server = fake_servers.next().await.unwrap();
23079 let fake_language_server_without_capabilities =
23080 fake_servers_without_capabilities.next().await.unwrap();
23081 let requests_made = Arc::new(AtomicUsize::new(0));
23082 let closure_requests_made = Arc::clone(&requests_made);
23083 let mut color_request_handle = fake_language_server
23084 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
23085 let requests_made = Arc::clone(&closure_requests_made);
23086 async move {
23087 assert_eq!(
23088 params.text_document.uri,
23089 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
23090 );
23091 requests_made.fetch_add(1, atomic::Ordering::Release);
23092 Ok(vec![
23093 lsp::ColorInformation {
23094 range: lsp::Range {
23095 start: lsp::Position {
23096 line: 0,
23097 character: 0,
23098 },
23099 end: lsp::Position {
23100 line: 0,
23101 character: 1,
23102 },
23103 },
23104 color: lsp::Color {
23105 red: 0.33,
23106 green: 0.33,
23107 blue: 0.33,
23108 alpha: 0.33,
23109 },
23110 },
23111 lsp::ColorInformation {
23112 range: lsp::Range {
23113 start: lsp::Position {
23114 line: 0,
23115 character: 0,
23116 },
23117 end: lsp::Position {
23118 line: 0,
23119 character: 1,
23120 },
23121 },
23122 color: lsp::Color {
23123 red: 0.33,
23124 green: 0.33,
23125 blue: 0.33,
23126 alpha: 0.33,
23127 },
23128 },
23129 ])
23130 }
23131 });
23132
23133 let _handle = fake_language_server_without_capabilities
23134 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
23135 panic!("Should not be called");
23136 });
23137 cx.executor().advance_clock(Duration::from_millis(100));
23138 color_request_handle.next().await.unwrap();
23139 cx.run_until_parked();
23140 assert_eq!(
23141 1,
23142 requests_made.load(atomic::Ordering::Acquire),
23143 "Should query for colors once per editor open"
23144 );
23145 editor.update_in(cx, |editor, _, cx| {
23146 assert_eq!(
23147 vec![expected_color],
23148 extract_color_inlays(editor, cx),
23149 "Should have an initial inlay"
23150 );
23151 });
23152
23153 // opening another file in a split should not influence the LSP query counter
23154 workspace
23155 .update(cx, |workspace, window, cx| {
23156 assert_eq!(
23157 workspace.panes().len(),
23158 1,
23159 "Should have one pane with one editor"
23160 );
23161 workspace.move_item_to_pane_in_direction(
23162 &MoveItemToPaneInDirection {
23163 direction: SplitDirection::Right,
23164 focus: false,
23165 clone: true,
23166 },
23167 window,
23168 cx,
23169 );
23170 })
23171 .unwrap();
23172 cx.run_until_parked();
23173 workspace
23174 .update(cx, |workspace, _, cx| {
23175 let panes = workspace.panes();
23176 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
23177 for pane in panes {
23178 let editor = pane
23179 .read(cx)
23180 .active_item()
23181 .and_then(|item| item.downcast::<Editor>())
23182 .expect("Should have opened an editor in each split");
23183 let editor_file = editor
23184 .read(cx)
23185 .buffer()
23186 .read(cx)
23187 .as_singleton()
23188 .expect("test deals with singleton buffers")
23189 .read(cx)
23190 .file()
23191 .expect("test buffese should have a file")
23192 .path();
23193 assert_eq!(
23194 editor_file.as_ref(),
23195 Path::new("first.rs"),
23196 "Both editors should be opened for the same file"
23197 )
23198 }
23199 })
23200 .unwrap();
23201
23202 cx.executor().advance_clock(Duration::from_millis(500));
23203 let save = editor.update_in(cx, |editor, window, cx| {
23204 editor.move_to_end(&MoveToEnd, window, cx);
23205 editor.handle_input("dirty", window, cx);
23206 editor.save(
23207 SaveOptions {
23208 format: true,
23209 autosave: true,
23210 },
23211 project.clone(),
23212 window,
23213 cx,
23214 )
23215 });
23216 save.await.unwrap();
23217
23218 color_request_handle.next().await.unwrap();
23219 cx.run_until_parked();
23220 assert_eq!(
23221 3,
23222 requests_made.load(atomic::Ordering::Acquire),
23223 "Should query for colors once per save and once per formatting after save"
23224 );
23225
23226 drop(editor);
23227 let close = workspace
23228 .update(cx, |workspace, window, cx| {
23229 workspace.active_pane().update(cx, |pane, cx| {
23230 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23231 })
23232 })
23233 .unwrap();
23234 close.await.unwrap();
23235 let close = workspace
23236 .update(cx, |workspace, window, cx| {
23237 workspace.active_pane().update(cx, |pane, cx| {
23238 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23239 })
23240 })
23241 .unwrap();
23242 close.await.unwrap();
23243 assert_eq!(
23244 3,
23245 requests_made.load(atomic::Ordering::Acquire),
23246 "After saving and closing all editors, no extra requests should be made"
23247 );
23248 workspace
23249 .update(cx, |workspace, _, cx| {
23250 assert!(
23251 workspace.active_item(cx).is_none(),
23252 "Should close all editors"
23253 )
23254 })
23255 .unwrap();
23256
23257 workspace
23258 .update(cx, |workspace, window, cx| {
23259 workspace.active_pane().update(cx, |pane, cx| {
23260 pane.navigate_backward(window, cx);
23261 })
23262 })
23263 .unwrap();
23264 cx.executor().advance_clock(Duration::from_millis(100));
23265 cx.run_until_parked();
23266 let editor = workspace
23267 .update(cx, |workspace, _, cx| {
23268 workspace
23269 .active_item(cx)
23270 .expect("Should have reopened the editor again after navigating back")
23271 .downcast::<Editor>()
23272 .expect("Should be an editor")
23273 })
23274 .unwrap();
23275 color_request_handle.next().await.unwrap();
23276 assert_eq!(
23277 3,
23278 requests_made.load(atomic::Ordering::Acquire),
23279 "Cache should be reused on buffer close and reopen"
23280 );
23281 editor.update(cx, |editor, cx| {
23282 assert_eq!(
23283 vec![expected_color],
23284 extract_color_inlays(editor, cx),
23285 "Should have an initial inlay"
23286 );
23287 });
23288}
23289
23290#[gpui::test]
23291async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
23292 init_test(cx, |_| {});
23293 let (editor, cx) = cx.add_window_view(Editor::single_line);
23294 editor.update_in(cx, |editor, window, cx| {
23295 editor.set_text("oops\n\nwow\n", window, cx)
23296 });
23297 cx.run_until_parked();
23298 editor.update(cx, |editor, cx| {
23299 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
23300 });
23301 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
23302 cx.run_until_parked();
23303 editor.update(cx, |editor, cx| {
23304 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
23305 });
23306}
23307
23308#[track_caller]
23309fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
23310 editor
23311 .all_inlays(cx)
23312 .into_iter()
23313 .filter_map(|inlay| inlay.get_color())
23314 .map(Rgba::from)
23315 .collect()
23316}