1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 inline_completion_tests::FakeInlineCompletionProvider,
6 linked_editing_ranges::LinkedEditingRanges,
7 scroll::scroll_amount::ScrollAmount,
8 test::{
9 assert_text_with_selections, build_editor,
10 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
11 editor_test_context::EditorTestContext,
12 select_ranges,
13 },
14};
15use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
16use futures::StreamExt;
17use gpui::{
18 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
19 VisualTestContext, WindowBounds, WindowOptions, div,
20};
21use indoc::indoc;
22use language::{
23 BracketPairConfig,
24 Capability::ReadWrite,
25 DiagnosticSourceKind, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher,
26 LanguageName, Override, Point,
27 language_settings::{
28 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings, FormatterList,
29 LanguageSettingsContent, LspInsertMode, PrettierSettings, SelectedFormatter,
30 },
31 tree_sitter_python,
32};
33use language_settings::{Formatter, IndentGuideSettings};
34use lsp::CompletionParams;
35use multi_buffer::{IndentGuide, PathKey};
36use parking_lot::Mutex;
37use pretty_assertions::{assert_eq, assert_ne};
38use project::{
39 FakeFs,
40 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
41 project_settings::{LspSettings, ProjectSettings},
42};
43use serde_json::{self, json};
44use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
45use std::{
46 iter,
47 sync::atomic::{self, AtomicUsize},
48};
49use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
50use text::ToPoint as _;
51use unindent::Unindent;
52use util::{
53 assert_set_eq, path,
54 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
55 uri,
56};
57use workspace::{
58 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
59 OpenOptions, ViewId,
60 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
61};
62
63#[gpui::test]
64fn test_edit_events(cx: &mut TestAppContext) {
65 init_test(cx, |_| {});
66
67 let buffer = cx.new(|cx| {
68 let mut buffer = language::Buffer::local("123456", cx);
69 buffer.set_group_interval(Duration::from_secs(1));
70 buffer
71 });
72
73 let events = Rc::new(RefCell::new(Vec::new()));
74 let editor1 = cx.add_window({
75 let events = events.clone();
76 |window, cx| {
77 let entity = cx.entity().clone();
78 cx.subscribe_in(
79 &entity,
80 window,
81 move |_, _, event: &EditorEvent, _, _| match event {
82 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
83 EditorEvent::BufferEdited => {
84 events.borrow_mut().push(("editor1", "buffer edited"))
85 }
86 _ => {}
87 },
88 )
89 .detach();
90 Editor::for_buffer(buffer.clone(), None, window, cx)
91 }
92 });
93
94 let editor2 = cx.add_window({
95 let events = events.clone();
96 |window, cx| {
97 cx.subscribe_in(
98 &cx.entity().clone(),
99 window,
100 move |_, _, event: &EditorEvent, _, _| match event {
101 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
102 EditorEvent::BufferEdited => {
103 events.borrow_mut().push(("editor2", "buffer edited"))
104 }
105 _ => {}
106 },
107 )
108 .detach();
109 Editor::for_buffer(buffer.clone(), None, window, cx)
110 }
111 });
112
113 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
114
115 // Mutating editor 1 will emit an `Edited` event only for that editor.
116 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
117 assert_eq!(
118 mem::take(&mut *events.borrow_mut()),
119 [
120 ("editor1", "edited"),
121 ("editor1", "buffer edited"),
122 ("editor2", "buffer edited"),
123 ]
124 );
125
126 // Mutating editor 2 will emit an `Edited` event only for that editor.
127 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
128 assert_eq!(
129 mem::take(&mut *events.borrow_mut()),
130 [
131 ("editor2", "edited"),
132 ("editor1", "buffer edited"),
133 ("editor2", "buffer edited"),
134 ]
135 );
136
137 // Undoing on editor 1 will emit an `Edited` event only for that editor.
138 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
139 assert_eq!(
140 mem::take(&mut *events.borrow_mut()),
141 [
142 ("editor1", "edited"),
143 ("editor1", "buffer edited"),
144 ("editor2", "buffer edited"),
145 ]
146 );
147
148 // Redoing on editor 1 will emit an `Edited` event only for that editor.
149 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
150 assert_eq!(
151 mem::take(&mut *events.borrow_mut()),
152 [
153 ("editor1", "edited"),
154 ("editor1", "buffer edited"),
155 ("editor2", "buffer edited"),
156 ]
157 );
158
159 // Undoing on editor 2 will emit an `Edited` event only for that editor.
160 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
161 assert_eq!(
162 mem::take(&mut *events.borrow_mut()),
163 [
164 ("editor2", "edited"),
165 ("editor1", "buffer edited"),
166 ("editor2", "buffer edited"),
167 ]
168 );
169
170 // Redoing on editor 2 will emit an `Edited` event only for that editor.
171 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
172 assert_eq!(
173 mem::take(&mut *events.borrow_mut()),
174 [
175 ("editor2", "edited"),
176 ("editor1", "buffer edited"),
177 ("editor2", "buffer edited"),
178 ]
179 );
180
181 // No event is emitted when the mutation is a no-op.
182 _ = editor2.update(cx, |editor, window, cx| {
183 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
184 s.select_ranges([0..0])
185 });
186
187 editor.backspace(&Backspace, window, cx);
188 });
189 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
190}
191
192#[gpui::test]
193fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
194 init_test(cx, |_| {});
195
196 let mut now = Instant::now();
197 let group_interval = Duration::from_millis(1);
198 let buffer = cx.new(|cx| {
199 let mut buf = language::Buffer::local("123456", cx);
200 buf.set_group_interval(group_interval);
201 buf
202 });
203 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
204 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
205
206 _ = editor.update(cx, |editor, window, cx| {
207 editor.start_transaction_at(now, window, cx);
208 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
209 s.select_ranges([2..4])
210 });
211
212 editor.insert("cd", window, cx);
213 editor.end_transaction_at(now, cx);
214 assert_eq!(editor.text(cx), "12cd56");
215 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
216
217 editor.start_transaction_at(now, window, cx);
218 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
219 s.select_ranges([4..5])
220 });
221 editor.insert("e", window, cx);
222 editor.end_transaction_at(now, cx);
223 assert_eq!(editor.text(cx), "12cde6");
224 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
225
226 now += group_interval + Duration::from_millis(1);
227 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
228 s.select_ranges([2..2])
229 });
230
231 // Simulate an edit in another editor
232 buffer.update(cx, |buffer, cx| {
233 buffer.start_transaction_at(now, cx);
234 buffer.edit([(0..1, "a")], None, cx);
235 buffer.edit([(1..1, "b")], None, cx);
236 buffer.end_transaction_at(now, cx);
237 });
238
239 assert_eq!(editor.text(cx), "ab2cde6");
240 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
241
242 // Last transaction happened past the group interval in a different editor.
243 // Undo it individually and don't restore selections.
244 editor.undo(&Undo, window, cx);
245 assert_eq!(editor.text(cx), "12cde6");
246 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
247
248 // First two transactions happened within the group interval in this editor.
249 // Undo them together and restore selections.
250 editor.undo(&Undo, window, cx);
251 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
252 assert_eq!(editor.text(cx), "123456");
253 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
254
255 // Redo the first two transactions together.
256 editor.redo(&Redo, window, cx);
257 assert_eq!(editor.text(cx), "12cde6");
258 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
259
260 // Redo the last transaction on its own.
261 editor.redo(&Redo, window, cx);
262 assert_eq!(editor.text(cx), "ab2cde6");
263 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
264
265 // Test empty transactions.
266 editor.start_transaction_at(now, window, cx);
267 editor.end_transaction_at(now, cx);
268 editor.undo(&Undo, window, cx);
269 assert_eq!(editor.text(cx), "12cde6");
270 });
271}
272
273#[gpui::test]
274fn test_ime_composition(cx: &mut TestAppContext) {
275 init_test(cx, |_| {});
276
277 let buffer = cx.new(|cx| {
278 let mut buffer = language::Buffer::local("abcde", cx);
279 // Ensure automatic grouping doesn't occur.
280 buffer.set_group_interval(Duration::ZERO);
281 buffer
282 });
283
284 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
285 cx.add_window(|window, cx| {
286 let mut editor = build_editor(buffer.clone(), window, cx);
287
288 // Start a new IME composition.
289 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
290 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
291 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
292 assert_eq!(editor.text(cx), "äbcde");
293 assert_eq!(
294 editor.marked_text_ranges(cx),
295 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
296 );
297
298 // Finalize IME composition.
299 editor.replace_text_in_range(None, "ā", window, cx);
300 assert_eq!(editor.text(cx), "ābcde");
301 assert_eq!(editor.marked_text_ranges(cx), None);
302
303 // IME composition edits are grouped and are undone/redone at once.
304 editor.undo(&Default::default(), window, cx);
305 assert_eq!(editor.text(cx), "abcde");
306 assert_eq!(editor.marked_text_ranges(cx), None);
307 editor.redo(&Default::default(), window, cx);
308 assert_eq!(editor.text(cx), "ābcde");
309 assert_eq!(editor.marked_text_ranges(cx), None);
310
311 // Start a new IME composition.
312 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
313 assert_eq!(
314 editor.marked_text_ranges(cx),
315 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
316 );
317
318 // Undoing during an IME composition cancels it.
319 editor.undo(&Default::default(), window, cx);
320 assert_eq!(editor.text(cx), "ābcde");
321 assert_eq!(editor.marked_text_ranges(cx), None);
322
323 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
324 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
325 assert_eq!(editor.text(cx), "ābcdè");
326 assert_eq!(
327 editor.marked_text_ranges(cx),
328 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
329 );
330
331 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
332 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
333 assert_eq!(editor.text(cx), "ābcdę");
334 assert_eq!(editor.marked_text_ranges(cx), None);
335
336 // Start a new IME composition with multiple cursors.
337 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
338 s.select_ranges([
339 OffsetUtf16(1)..OffsetUtf16(1),
340 OffsetUtf16(3)..OffsetUtf16(3),
341 OffsetUtf16(5)..OffsetUtf16(5),
342 ])
343 });
344 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
345 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
346 assert_eq!(
347 editor.marked_text_ranges(cx),
348 Some(vec![
349 OffsetUtf16(0)..OffsetUtf16(3),
350 OffsetUtf16(4)..OffsetUtf16(7),
351 OffsetUtf16(8)..OffsetUtf16(11)
352 ])
353 );
354
355 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
356 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
357 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
358 assert_eq!(
359 editor.marked_text_ranges(cx),
360 Some(vec![
361 OffsetUtf16(1)..OffsetUtf16(2),
362 OffsetUtf16(5)..OffsetUtf16(6),
363 OffsetUtf16(9)..OffsetUtf16(10)
364 ])
365 );
366
367 // Finalize IME composition with multiple cursors.
368 editor.replace_text_in_range(Some(9..10), "2", window, cx);
369 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
370 assert_eq!(editor.marked_text_ranges(cx), None);
371
372 editor
373 });
374}
375
376#[gpui::test]
377fn test_selection_with_mouse(cx: &mut TestAppContext) {
378 init_test(cx, |_| {});
379
380 let editor = cx.add_window(|window, cx| {
381 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
382 build_editor(buffer, window, cx)
383 });
384
385 _ = editor.update(cx, |editor, window, cx| {
386 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
387 });
388 assert_eq!(
389 editor
390 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
391 .unwrap(),
392 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
393 );
394
395 _ = editor.update(cx, |editor, window, cx| {
396 editor.update_selection(
397 DisplayPoint::new(DisplayRow(3), 3),
398 0,
399 gpui::Point::<f32>::default(),
400 window,
401 cx,
402 );
403 });
404
405 assert_eq!(
406 editor
407 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
408 .unwrap(),
409 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
410 );
411
412 _ = editor.update(cx, |editor, window, cx| {
413 editor.update_selection(
414 DisplayPoint::new(DisplayRow(1), 1),
415 0,
416 gpui::Point::<f32>::default(),
417 window,
418 cx,
419 );
420 });
421
422 assert_eq!(
423 editor
424 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
425 .unwrap(),
426 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
427 );
428
429 _ = editor.update(cx, |editor, window, cx| {
430 editor.end_selection(window, cx);
431 editor.update_selection(
432 DisplayPoint::new(DisplayRow(3), 3),
433 0,
434 gpui::Point::<f32>::default(),
435 window,
436 cx,
437 );
438 });
439
440 assert_eq!(
441 editor
442 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
443 .unwrap(),
444 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
445 );
446
447 _ = editor.update(cx, |editor, window, cx| {
448 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
449 editor.update_selection(
450 DisplayPoint::new(DisplayRow(0), 0),
451 0,
452 gpui::Point::<f32>::default(),
453 window,
454 cx,
455 );
456 });
457
458 assert_eq!(
459 editor
460 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
461 .unwrap(),
462 [
463 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
464 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
465 ]
466 );
467
468 _ = editor.update(cx, |editor, window, cx| {
469 editor.end_selection(window, cx);
470 });
471
472 assert_eq!(
473 editor
474 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
475 .unwrap(),
476 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
477 );
478}
479
480#[gpui::test]
481fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
482 init_test(cx, |_| {});
483
484 let editor = cx.add_window(|window, cx| {
485 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
486 build_editor(buffer, window, cx)
487 });
488
489 _ = editor.update(cx, |editor, window, cx| {
490 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
491 });
492
493 _ = editor.update(cx, |editor, window, cx| {
494 editor.end_selection(window, cx);
495 });
496
497 _ = editor.update(cx, |editor, window, cx| {
498 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
499 });
500
501 _ = editor.update(cx, |editor, window, cx| {
502 editor.end_selection(window, cx);
503 });
504
505 assert_eq!(
506 editor
507 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
508 .unwrap(),
509 [
510 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
511 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
512 ]
513 );
514
515 _ = editor.update(cx, |editor, window, cx| {
516 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
517 });
518
519 _ = editor.update(cx, |editor, window, cx| {
520 editor.end_selection(window, cx);
521 });
522
523 assert_eq!(
524 editor
525 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
526 .unwrap(),
527 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
528 );
529}
530
531#[gpui::test]
532fn test_canceling_pending_selection(cx: &mut TestAppContext) {
533 init_test(cx, |_| {});
534
535 let editor = cx.add_window(|window, cx| {
536 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
537 build_editor(buffer, window, cx)
538 });
539
540 _ = editor.update(cx, |editor, window, cx| {
541 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
542 assert_eq!(
543 editor.selections.display_ranges(cx),
544 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
545 );
546 });
547
548 _ = editor.update(cx, |editor, window, cx| {
549 editor.update_selection(
550 DisplayPoint::new(DisplayRow(3), 3),
551 0,
552 gpui::Point::<f32>::default(),
553 window,
554 cx,
555 );
556 assert_eq!(
557 editor.selections.display_ranges(cx),
558 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
559 );
560 });
561
562 _ = editor.update(cx, |editor, window, cx| {
563 editor.cancel(&Cancel, window, cx);
564 editor.update_selection(
565 DisplayPoint::new(DisplayRow(1), 1),
566 0,
567 gpui::Point::<f32>::default(),
568 window,
569 cx,
570 );
571 assert_eq!(
572 editor.selections.display_ranges(cx),
573 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
574 );
575 });
576}
577
578#[gpui::test]
579fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
580 init_test(cx, |_| {});
581
582 let editor = cx.add_window(|window, cx| {
583 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
584 build_editor(buffer, window, cx)
585 });
586
587 _ = editor.update(cx, |editor, window, cx| {
588 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
589 assert_eq!(
590 editor.selections.display_ranges(cx),
591 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
592 );
593
594 editor.move_down(&Default::default(), window, cx);
595 assert_eq!(
596 editor.selections.display_ranges(cx),
597 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
598 );
599
600 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
601 assert_eq!(
602 editor.selections.display_ranges(cx),
603 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
604 );
605
606 editor.move_up(&Default::default(), window, cx);
607 assert_eq!(
608 editor.selections.display_ranges(cx),
609 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
610 );
611 });
612}
613
614#[gpui::test]
615fn test_clone(cx: &mut TestAppContext) {
616 init_test(cx, |_| {});
617
618 let (text, selection_ranges) = marked_text_ranges(
619 indoc! {"
620 one
621 two
622 threeˇ
623 four
624 fiveˇ
625 "},
626 true,
627 );
628
629 let editor = cx.add_window(|window, cx| {
630 let buffer = MultiBuffer::build_simple(&text, cx);
631 build_editor(buffer, window, cx)
632 });
633
634 _ = editor.update(cx, |editor, window, cx| {
635 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
636 s.select_ranges(selection_ranges.clone())
637 });
638 editor.fold_creases(
639 vec![
640 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
641 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
642 ],
643 true,
644 window,
645 cx,
646 );
647 });
648
649 let cloned_editor = editor
650 .update(cx, |editor, _, cx| {
651 cx.open_window(Default::default(), |window, cx| {
652 cx.new(|cx| editor.clone(window, cx))
653 })
654 })
655 .unwrap()
656 .unwrap();
657
658 let snapshot = editor
659 .update(cx, |e, window, cx| e.snapshot(window, cx))
660 .unwrap();
661 let cloned_snapshot = cloned_editor
662 .update(cx, |e, window, cx| e.snapshot(window, cx))
663 .unwrap();
664
665 assert_eq!(
666 cloned_editor
667 .update(cx, |e, _, cx| e.display_text(cx))
668 .unwrap(),
669 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
670 );
671 assert_eq!(
672 cloned_snapshot
673 .folds_in_range(0..text.len())
674 .collect::<Vec<_>>(),
675 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
676 );
677 assert_set_eq!(
678 cloned_editor
679 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
680 .unwrap(),
681 editor
682 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
683 .unwrap()
684 );
685 assert_set_eq!(
686 cloned_editor
687 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
688 .unwrap(),
689 editor
690 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
691 .unwrap()
692 );
693}
694
695#[gpui::test]
696async fn test_navigation_history(cx: &mut TestAppContext) {
697 init_test(cx, |_| {});
698
699 use workspace::item::Item;
700
701 let fs = FakeFs::new(cx.executor());
702 let project = Project::test(fs, [], cx).await;
703 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
704 let pane = workspace
705 .update(cx, |workspace, _, _| workspace.active_pane().clone())
706 .unwrap();
707
708 _ = workspace.update(cx, |_v, window, cx| {
709 cx.new(|cx| {
710 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
711 let mut editor = build_editor(buffer.clone(), window, cx);
712 let handle = cx.entity();
713 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
714
715 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
716 editor.nav_history.as_mut().unwrap().pop_backward(cx)
717 }
718
719 // Move the cursor a small distance.
720 // Nothing is added to the navigation history.
721 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
722 s.select_display_ranges([
723 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
724 ])
725 });
726 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
727 s.select_display_ranges([
728 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
729 ])
730 });
731 assert!(pop_history(&mut editor, cx).is_none());
732
733 // Move the cursor a large distance.
734 // The history can jump back to the previous position.
735 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
736 s.select_display_ranges([
737 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
738 ])
739 });
740 let nav_entry = pop_history(&mut editor, cx).unwrap();
741 editor.navigate(nav_entry.data.unwrap(), window, cx);
742 assert_eq!(nav_entry.item.id(), cx.entity_id());
743 assert_eq!(
744 editor.selections.display_ranges(cx),
745 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
746 );
747 assert!(pop_history(&mut editor, cx).is_none());
748
749 // Move the cursor a small distance via the mouse.
750 // Nothing is added to the navigation history.
751 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
752 editor.end_selection(window, cx);
753 assert_eq!(
754 editor.selections.display_ranges(cx),
755 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
756 );
757 assert!(pop_history(&mut editor, cx).is_none());
758
759 // Move the cursor a large distance via the mouse.
760 // The history can jump back to the previous position.
761 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
762 editor.end_selection(window, cx);
763 assert_eq!(
764 editor.selections.display_ranges(cx),
765 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
766 );
767 let nav_entry = pop_history(&mut editor, cx).unwrap();
768 editor.navigate(nav_entry.data.unwrap(), window, cx);
769 assert_eq!(nav_entry.item.id(), cx.entity_id());
770 assert_eq!(
771 editor.selections.display_ranges(cx),
772 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
773 );
774 assert!(pop_history(&mut editor, cx).is_none());
775
776 // Set scroll position to check later
777 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
778 let original_scroll_position = editor.scroll_manager.anchor();
779
780 // Jump to the end of the document and adjust scroll
781 editor.move_to_end(&MoveToEnd, window, cx);
782 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
783 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
784
785 let nav_entry = pop_history(&mut editor, cx).unwrap();
786 editor.navigate(nav_entry.data.unwrap(), window, cx);
787 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
788
789 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
790 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
791 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
792 let invalid_point = Point::new(9999, 0);
793 editor.navigate(
794 Box::new(NavigationData {
795 cursor_anchor: invalid_anchor,
796 cursor_position: invalid_point,
797 scroll_anchor: ScrollAnchor {
798 anchor: invalid_anchor,
799 offset: Default::default(),
800 },
801 scroll_top_row: invalid_point.row,
802 }),
803 window,
804 cx,
805 );
806 assert_eq!(
807 editor.selections.display_ranges(cx),
808 &[editor.max_point(cx)..editor.max_point(cx)]
809 );
810 assert_eq!(
811 editor.scroll_position(cx),
812 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
813 );
814
815 editor
816 })
817 });
818}
819
820#[gpui::test]
821fn test_cancel(cx: &mut TestAppContext) {
822 init_test(cx, |_| {});
823
824 let editor = cx.add_window(|window, cx| {
825 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
826 build_editor(buffer, window, cx)
827 });
828
829 _ = editor.update(cx, |editor, window, cx| {
830 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
831 editor.update_selection(
832 DisplayPoint::new(DisplayRow(1), 1),
833 0,
834 gpui::Point::<f32>::default(),
835 window,
836 cx,
837 );
838 editor.end_selection(window, cx);
839
840 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
841 editor.update_selection(
842 DisplayPoint::new(DisplayRow(0), 3),
843 0,
844 gpui::Point::<f32>::default(),
845 window,
846 cx,
847 );
848 editor.end_selection(window, cx);
849 assert_eq!(
850 editor.selections.display_ranges(cx),
851 [
852 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
853 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
854 ]
855 );
856 });
857
858 _ = editor.update(cx, |editor, window, cx| {
859 editor.cancel(&Cancel, window, cx);
860 assert_eq!(
861 editor.selections.display_ranges(cx),
862 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
863 );
864 });
865
866 _ = editor.update(cx, |editor, window, cx| {
867 editor.cancel(&Cancel, window, cx);
868 assert_eq!(
869 editor.selections.display_ranges(cx),
870 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
871 );
872 });
873}
874
875#[gpui::test]
876fn test_fold_action(cx: &mut TestAppContext) {
877 init_test(cx, |_| {});
878
879 let editor = cx.add_window(|window, cx| {
880 let buffer = MultiBuffer::build_simple(
881 &"
882 impl Foo {
883 // Hello!
884
885 fn a() {
886 1
887 }
888
889 fn b() {
890 2
891 }
892
893 fn c() {
894 3
895 }
896 }
897 "
898 .unindent(),
899 cx,
900 );
901 build_editor(buffer.clone(), window, cx)
902 });
903
904 _ = editor.update(cx, |editor, window, cx| {
905 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
906 s.select_display_ranges([
907 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
908 ]);
909 });
910 editor.fold(&Fold, window, cx);
911 assert_eq!(
912 editor.display_text(cx),
913 "
914 impl Foo {
915 // Hello!
916
917 fn a() {
918 1
919 }
920
921 fn b() {⋯
922 }
923
924 fn c() {⋯
925 }
926 }
927 "
928 .unindent(),
929 );
930
931 editor.fold(&Fold, window, cx);
932 assert_eq!(
933 editor.display_text(cx),
934 "
935 impl Foo {⋯
936 }
937 "
938 .unindent(),
939 );
940
941 editor.unfold_lines(&UnfoldLines, window, cx);
942 assert_eq!(
943 editor.display_text(cx),
944 "
945 impl Foo {
946 // Hello!
947
948 fn a() {
949 1
950 }
951
952 fn b() {⋯
953 }
954
955 fn c() {⋯
956 }
957 }
958 "
959 .unindent(),
960 );
961
962 editor.unfold_lines(&UnfoldLines, window, cx);
963 assert_eq!(
964 editor.display_text(cx),
965 editor.buffer.read(cx).read(cx).text()
966 );
967 });
968}
969
970#[gpui::test]
971fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
972 init_test(cx, |_| {});
973
974 let editor = cx.add_window(|window, cx| {
975 let buffer = MultiBuffer::build_simple(
976 &"
977 class Foo:
978 # Hello!
979
980 def a():
981 print(1)
982
983 def b():
984 print(2)
985
986 def c():
987 print(3)
988 "
989 .unindent(),
990 cx,
991 );
992 build_editor(buffer.clone(), window, cx)
993 });
994
995 _ = editor.update(cx, |editor, window, cx| {
996 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
997 s.select_display_ranges([
998 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
999 ]);
1000 });
1001 editor.fold(&Fold, window, cx);
1002 assert_eq!(
1003 editor.display_text(cx),
1004 "
1005 class Foo:
1006 # Hello!
1007
1008 def a():
1009 print(1)
1010
1011 def b():⋯
1012
1013 def c():⋯
1014 "
1015 .unindent(),
1016 );
1017
1018 editor.fold(&Fold, window, cx);
1019 assert_eq!(
1020 editor.display_text(cx),
1021 "
1022 class Foo:⋯
1023 "
1024 .unindent(),
1025 );
1026
1027 editor.unfold_lines(&UnfoldLines, window, cx);
1028 assert_eq!(
1029 editor.display_text(cx),
1030 "
1031 class Foo:
1032 # Hello!
1033
1034 def a():
1035 print(1)
1036
1037 def b():⋯
1038
1039 def c():⋯
1040 "
1041 .unindent(),
1042 );
1043
1044 editor.unfold_lines(&UnfoldLines, window, cx);
1045 assert_eq!(
1046 editor.display_text(cx),
1047 editor.buffer.read(cx).read(cx).text()
1048 );
1049 });
1050}
1051
1052#[gpui::test]
1053fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1054 init_test(cx, |_| {});
1055
1056 let editor = cx.add_window(|window, cx| {
1057 let buffer = MultiBuffer::build_simple(
1058 &"
1059 class Foo:
1060 # Hello!
1061
1062 def a():
1063 print(1)
1064
1065 def b():
1066 print(2)
1067
1068
1069 def c():
1070 print(3)
1071
1072
1073 "
1074 .unindent(),
1075 cx,
1076 );
1077 build_editor(buffer.clone(), window, cx)
1078 });
1079
1080 _ = editor.update(cx, |editor, window, cx| {
1081 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1082 s.select_display_ranges([
1083 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1084 ]);
1085 });
1086 editor.fold(&Fold, window, cx);
1087 assert_eq!(
1088 editor.display_text(cx),
1089 "
1090 class Foo:
1091 # Hello!
1092
1093 def a():
1094 print(1)
1095
1096 def b():⋯
1097
1098
1099 def c():⋯
1100
1101
1102 "
1103 .unindent(),
1104 );
1105
1106 editor.fold(&Fold, window, cx);
1107 assert_eq!(
1108 editor.display_text(cx),
1109 "
1110 class Foo:⋯
1111
1112
1113 "
1114 .unindent(),
1115 );
1116
1117 editor.unfold_lines(&UnfoldLines, window, cx);
1118 assert_eq!(
1119 editor.display_text(cx),
1120 "
1121 class Foo:
1122 # Hello!
1123
1124 def a():
1125 print(1)
1126
1127 def b():⋯
1128
1129
1130 def c():⋯
1131
1132
1133 "
1134 .unindent(),
1135 );
1136
1137 editor.unfold_lines(&UnfoldLines, window, cx);
1138 assert_eq!(
1139 editor.display_text(cx),
1140 editor.buffer.read(cx).read(cx).text()
1141 );
1142 });
1143}
1144
1145#[gpui::test]
1146fn test_fold_at_level(cx: &mut TestAppContext) {
1147 init_test(cx, |_| {});
1148
1149 let editor = cx.add_window(|window, cx| {
1150 let buffer = MultiBuffer::build_simple(
1151 &"
1152 class Foo:
1153 # Hello!
1154
1155 def a():
1156 print(1)
1157
1158 def b():
1159 print(2)
1160
1161
1162 class Bar:
1163 # World!
1164
1165 def a():
1166 print(1)
1167
1168 def b():
1169 print(2)
1170
1171
1172 "
1173 .unindent(),
1174 cx,
1175 );
1176 build_editor(buffer.clone(), window, cx)
1177 });
1178
1179 _ = editor.update(cx, |editor, window, cx| {
1180 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1181 assert_eq!(
1182 editor.display_text(cx),
1183 "
1184 class Foo:
1185 # Hello!
1186
1187 def a():⋯
1188
1189 def b():⋯
1190
1191
1192 class Bar:
1193 # World!
1194
1195 def a():⋯
1196
1197 def b():⋯
1198
1199
1200 "
1201 .unindent(),
1202 );
1203
1204 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1205 assert_eq!(
1206 editor.display_text(cx),
1207 "
1208 class Foo:⋯
1209
1210
1211 class Bar:⋯
1212
1213
1214 "
1215 .unindent(),
1216 );
1217
1218 editor.unfold_all(&UnfoldAll, window, cx);
1219 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1220 assert_eq!(
1221 editor.display_text(cx),
1222 "
1223 class Foo:
1224 # Hello!
1225
1226 def a():
1227 print(1)
1228
1229 def b():
1230 print(2)
1231
1232
1233 class Bar:
1234 # World!
1235
1236 def a():
1237 print(1)
1238
1239 def b():
1240 print(2)
1241
1242
1243 "
1244 .unindent(),
1245 );
1246
1247 assert_eq!(
1248 editor.display_text(cx),
1249 editor.buffer.read(cx).read(cx).text()
1250 );
1251 });
1252}
1253
1254#[gpui::test]
1255fn test_move_cursor(cx: &mut TestAppContext) {
1256 init_test(cx, |_| {});
1257
1258 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1259 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1260
1261 buffer.update(cx, |buffer, cx| {
1262 buffer.edit(
1263 vec![
1264 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1265 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1266 ],
1267 None,
1268 cx,
1269 );
1270 });
1271 _ = editor.update(cx, |editor, window, cx| {
1272 assert_eq!(
1273 editor.selections.display_ranges(cx),
1274 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1275 );
1276
1277 editor.move_down(&MoveDown, window, cx);
1278 assert_eq!(
1279 editor.selections.display_ranges(cx),
1280 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1281 );
1282
1283 editor.move_right(&MoveRight, window, cx);
1284 assert_eq!(
1285 editor.selections.display_ranges(cx),
1286 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1287 );
1288
1289 editor.move_left(&MoveLeft, window, cx);
1290 assert_eq!(
1291 editor.selections.display_ranges(cx),
1292 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1293 );
1294
1295 editor.move_up(&MoveUp, window, cx);
1296 assert_eq!(
1297 editor.selections.display_ranges(cx),
1298 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1299 );
1300
1301 editor.move_to_end(&MoveToEnd, window, cx);
1302 assert_eq!(
1303 editor.selections.display_ranges(cx),
1304 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1305 );
1306
1307 editor.move_to_beginning(&MoveToBeginning, window, cx);
1308 assert_eq!(
1309 editor.selections.display_ranges(cx),
1310 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1311 );
1312
1313 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1314 s.select_display_ranges([
1315 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1316 ]);
1317 });
1318 editor.select_to_beginning(&SelectToBeginning, window, cx);
1319 assert_eq!(
1320 editor.selections.display_ranges(cx),
1321 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1322 );
1323
1324 editor.select_to_end(&SelectToEnd, window, cx);
1325 assert_eq!(
1326 editor.selections.display_ranges(cx),
1327 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1328 );
1329 });
1330}
1331
1332#[gpui::test]
1333fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1334 init_test(cx, |_| {});
1335
1336 let editor = cx.add_window(|window, cx| {
1337 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1338 build_editor(buffer.clone(), window, cx)
1339 });
1340
1341 assert_eq!('🟥'.len_utf8(), 4);
1342 assert_eq!('α'.len_utf8(), 2);
1343
1344 _ = editor.update(cx, |editor, window, cx| {
1345 editor.fold_creases(
1346 vec![
1347 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1348 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1349 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1350 ],
1351 true,
1352 window,
1353 cx,
1354 );
1355 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1356
1357 editor.move_right(&MoveRight, window, cx);
1358 assert_eq!(
1359 editor.selections.display_ranges(cx),
1360 &[empty_range(0, "🟥".len())]
1361 );
1362 editor.move_right(&MoveRight, window, cx);
1363 assert_eq!(
1364 editor.selections.display_ranges(cx),
1365 &[empty_range(0, "🟥🟧".len())]
1366 );
1367 editor.move_right(&MoveRight, window, cx);
1368 assert_eq!(
1369 editor.selections.display_ranges(cx),
1370 &[empty_range(0, "🟥🟧⋯".len())]
1371 );
1372
1373 editor.move_down(&MoveDown, window, cx);
1374 assert_eq!(
1375 editor.selections.display_ranges(cx),
1376 &[empty_range(1, "ab⋯e".len())]
1377 );
1378 editor.move_left(&MoveLeft, window, cx);
1379 assert_eq!(
1380 editor.selections.display_ranges(cx),
1381 &[empty_range(1, "ab⋯".len())]
1382 );
1383 editor.move_left(&MoveLeft, window, cx);
1384 assert_eq!(
1385 editor.selections.display_ranges(cx),
1386 &[empty_range(1, "ab".len())]
1387 );
1388 editor.move_left(&MoveLeft, window, cx);
1389 assert_eq!(
1390 editor.selections.display_ranges(cx),
1391 &[empty_range(1, "a".len())]
1392 );
1393
1394 editor.move_down(&MoveDown, window, cx);
1395 assert_eq!(
1396 editor.selections.display_ranges(cx),
1397 &[empty_range(2, "α".len())]
1398 );
1399 editor.move_right(&MoveRight, window, cx);
1400 assert_eq!(
1401 editor.selections.display_ranges(cx),
1402 &[empty_range(2, "αβ".len())]
1403 );
1404 editor.move_right(&MoveRight, window, cx);
1405 assert_eq!(
1406 editor.selections.display_ranges(cx),
1407 &[empty_range(2, "αβ⋯".len())]
1408 );
1409 editor.move_right(&MoveRight, window, cx);
1410 assert_eq!(
1411 editor.selections.display_ranges(cx),
1412 &[empty_range(2, "αβ⋯ε".len())]
1413 );
1414
1415 editor.move_up(&MoveUp, window, cx);
1416 assert_eq!(
1417 editor.selections.display_ranges(cx),
1418 &[empty_range(1, "ab⋯e".len())]
1419 );
1420 editor.move_down(&MoveDown, window, cx);
1421 assert_eq!(
1422 editor.selections.display_ranges(cx),
1423 &[empty_range(2, "αβ⋯ε".len())]
1424 );
1425 editor.move_up(&MoveUp, window, cx);
1426 assert_eq!(
1427 editor.selections.display_ranges(cx),
1428 &[empty_range(1, "ab⋯e".len())]
1429 );
1430
1431 editor.move_up(&MoveUp, window, cx);
1432 assert_eq!(
1433 editor.selections.display_ranges(cx),
1434 &[empty_range(0, "🟥🟧".len())]
1435 );
1436 editor.move_left(&MoveLeft, window, cx);
1437 assert_eq!(
1438 editor.selections.display_ranges(cx),
1439 &[empty_range(0, "🟥".len())]
1440 );
1441 editor.move_left(&MoveLeft, window, cx);
1442 assert_eq!(
1443 editor.selections.display_ranges(cx),
1444 &[empty_range(0, "".len())]
1445 );
1446 });
1447}
1448
1449#[gpui::test]
1450fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1451 init_test(cx, |_| {});
1452
1453 let editor = cx.add_window(|window, cx| {
1454 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1455 build_editor(buffer.clone(), window, cx)
1456 });
1457 _ = editor.update(cx, |editor, window, cx| {
1458 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1459 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1460 });
1461
1462 // moving above start of document should move selection to start of document,
1463 // but the next move down should still be at the original goal_x
1464 editor.move_up(&MoveUp, window, cx);
1465 assert_eq!(
1466 editor.selections.display_ranges(cx),
1467 &[empty_range(0, "".len())]
1468 );
1469
1470 editor.move_down(&MoveDown, window, cx);
1471 assert_eq!(
1472 editor.selections.display_ranges(cx),
1473 &[empty_range(1, "abcd".len())]
1474 );
1475
1476 editor.move_down(&MoveDown, window, cx);
1477 assert_eq!(
1478 editor.selections.display_ranges(cx),
1479 &[empty_range(2, "αβγ".len())]
1480 );
1481
1482 editor.move_down(&MoveDown, window, cx);
1483 assert_eq!(
1484 editor.selections.display_ranges(cx),
1485 &[empty_range(3, "abcd".len())]
1486 );
1487
1488 editor.move_down(&MoveDown, window, cx);
1489 assert_eq!(
1490 editor.selections.display_ranges(cx),
1491 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1492 );
1493
1494 // moving past end of document should not change goal_x
1495 editor.move_down(&MoveDown, window, cx);
1496 assert_eq!(
1497 editor.selections.display_ranges(cx),
1498 &[empty_range(5, "".len())]
1499 );
1500
1501 editor.move_down(&MoveDown, window, cx);
1502 assert_eq!(
1503 editor.selections.display_ranges(cx),
1504 &[empty_range(5, "".len())]
1505 );
1506
1507 editor.move_up(&MoveUp, window, cx);
1508 assert_eq!(
1509 editor.selections.display_ranges(cx),
1510 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1511 );
1512
1513 editor.move_up(&MoveUp, window, cx);
1514 assert_eq!(
1515 editor.selections.display_ranges(cx),
1516 &[empty_range(3, "abcd".len())]
1517 );
1518
1519 editor.move_up(&MoveUp, window, cx);
1520 assert_eq!(
1521 editor.selections.display_ranges(cx),
1522 &[empty_range(2, "αβγ".len())]
1523 );
1524 });
1525}
1526
1527#[gpui::test]
1528fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1529 init_test(cx, |_| {});
1530 let move_to_beg = MoveToBeginningOfLine {
1531 stop_at_soft_wraps: true,
1532 stop_at_indent: true,
1533 };
1534
1535 let delete_to_beg = DeleteToBeginningOfLine {
1536 stop_at_indent: false,
1537 };
1538
1539 let move_to_end = MoveToEndOfLine {
1540 stop_at_soft_wraps: true,
1541 };
1542
1543 let editor = cx.add_window(|window, cx| {
1544 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1545 build_editor(buffer, window, cx)
1546 });
1547 _ = editor.update(cx, |editor, window, cx| {
1548 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1549 s.select_display_ranges([
1550 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1551 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1552 ]);
1553 });
1554 });
1555
1556 _ = editor.update(cx, |editor, window, cx| {
1557 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1558 assert_eq!(
1559 editor.selections.display_ranges(cx),
1560 &[
1561 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1562 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1563 ]
1564 );
1565 });
1566
1567 _ = editor.update(cx, |editor, window, cx| {
1568 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1569 assert_eq!(
1570 editor.selections.display_ranges(cx),
1571 &[
1572 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1573 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1574 ]
1575 );
1576 });
1577
1578 _ = editor.update(cx, |editor, window, cx| {
1579 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1580 assert_eq!(
1581 editor.selections.display_ranges(cx),
1582 &[
1583 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1584 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1585 ]
1586 );
1587 });
1588
1589 _ = editor.update(cx, |editor, window, cx| {
1590 editor.move_to_end_of_line(&move_to_end, window, cx);
1591 assert_eq!(
1592 editor.selections.display_ranges(cx),
1593 &[
1594 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1595 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1596 ]
1597 );
1598 });
1599
1600 // Moving to the end of line again is a no-op.
1601 _ = editor.update(cx, |editor, window, cx| {
1602 editor.move_to_end_of_line(&move_to_end, window, cx);
1603 assert_eq!(
1604 editor.selections.display_ranges(cx),
1605 &[
1606 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1607 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1608 ]
1609 );
1610 });
1611
1612 _ = editor.update(cx, |editor, window, cx| {
1613 editor.move_left(&MoveLeft, window, cx);
1614 editor.select_to_beginning_of_line(
1615 &SelectToBeginningOfLine {
1616 stop_at_soft_wraps: true,
1617 stop_at_indent: true,
1618 },
1619 window,
1620 cx,
1621 );
1622 assert_eq!(
1623 editor.selections.display_ranges(cx),
1624 &[
1625 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1626 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1627 ]
1628 );
1629 });
1630
1631 _ = editor.update(cx, |editor, window, cx| {
1632 editor.select_to_beginning_of_line(
1633 &SelectToBeginningOfLine {
1634 stop_at_soft_wraps: true,
1635 stop_at_indent: true,
1636 },
1637 window,
1638 cx,
1639 );
1640 assert_eq!(
1641 editor.selections.display_ranges(cx),
1642 &[
1643 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1644 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1645 ]
1646 );
1647 });
1648
1649 _ = editor.update(cx, |editor, window, cx| {
1650 editor.select_to_beginning_of_line(
1651 &SelectToBeginningOfLine {
1652 stop_at_soft_wraps: true,
1653 stop_at_indent: true,
1654 },
1655 window,
1656 cx,
1657 );
1658 assert_eq!(
1659 editor.selections.display_ranges(cx),
1660 &[
1661 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1662 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1663 ]
1664 );
1665 });
1666
1667 _ = editor.update(cx, |editor, window, cx| {
1668 editor.select_to_end_of_line(
1669 &SelectToEndOfLine {
1670 stop_at_soft_wraps: true,
1671 },
1672 window,
1673 cx,
1674 );
1675 assert_eq!(
1676 editor.selections.display_ranges(cx),
1677 &[
1678 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1679 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1680 ]
1681 );
1682 });
1683
1684 _ = editor.update(cx, |editor, window, cx| {
1685 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1686 assert_eq!(editor.display_text(cx), "ab\n de");
1687 assert_eq!(
1688 editor.selections.display_ranges(cx),
1689 &[
1690 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1691 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1692 ]
1693 );
1694 });
1695
1696 _ = editor.update(cx, |editor, window, cx| {
1697 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1698 assert_eq!(editor.display_text(cx), "\n");
1699 assert_eq!(
1700 editor.selections.display_ranges(cx),
1701 &[
1702 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1703 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1704 ]
1705 );
1706 });
1707}
1708
1709#[gpui::test]
1710fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1711 init_test(cx, |_| {});
1712 let move_to_beg = MoveToBeginningOfLine {
1713 stop_at_soft_wraps: false,
1714 stop_at_indent: false,
1715 };
1716
1717 let move_to_end = MoveToEndOfLine {
1718 stop_at_soft_wraps: false,
1719 };
1720
1721 let editor = cx.add_window(|window, cx| {
1722 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1723 build_editor(buffer, window, cx)
1724 });
1725
1726 _ = editor.update(cx, |editor, window, cx| {
1727 editor.set_wrap_width(Some(140.0.into()), cx);
1728
1729 // We expect the following lines after wrapping
1730 // ```
1731 // thequickbrownfox
1732 // jumpedoverthelazydo
1733 // gs
1734 // ```
1735 // The final `gs` was soft-wrapped onto a new line.
1736 assert_eq!(
1737 "thequickbrownfox\njumpedoverthelaz\nydogs",
1738 editor.display_text(cx),
1739 );
1740
1741 // First, let's assert behavior on the first line, that was not soft-wrapped.
1742 // Start the cursor at the `k` on the first line
1743 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1744 s.select_display_ranges([
1745 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1746 ]);
1747 });
1748
1749 // Moving to the beginning of the line should put us at the beginning of the line.
1750 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1751 assert_eq!(
1752 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1753 editor.selections.display_ranges(cx)
1754 );
1755
1756 // Moving to the end of the line should put us at the end of the line.
1757 editor.move_to_end_of_line(&move_to_end, window, cx);
1758 assert_eq!(
1759 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1760 editor.selections.display_ranges(cx)
1761 );
1762
1763 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1764 // Start the cursor at the last line (`y` that was wrapped to a new line)
1765 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1766 s.select_display_ranges([
1767 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1768 ]);
1769 });
1770
1771 // Moving to the beginning of the line should put us at the start of the second line of
1772 // display text, i.e., the `j`.
1773 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1774 assert_eq!(
1775 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1776 editor.selections.display_ranges(cx)
1777 );
1778
1779 // Moving to the beginning of the line again should be a no-op.
1780 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1781 assert_eq!(
1782 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1783 editor.selections.display_ranges(cx)
1784 );
1785
1786 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1787 // next display line.
1788 editor.move_to_end_of_line(&move_to_end, window, cx);
1789 assert_eq!(
1790 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1791 editor.selections.display_ranges(cx)
1792 );
1793
1794 // Moving to the end of the line again should be a no-op.
1795 editor.move_to_end_of_line(&move_to_end, window, cx);
1796 assert_eq!(
1797 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1798 editor.selections.display_ranges(cx)
1799 );
1800 });
1801}
1802
1803#[gpui::test]
1804fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1805 init_test(cx, |_| {});
1806
1807 let move_to_beg = MoveToBeginningOfLine {
1808 stop_at_soft_wraps: true,
1809 stop_at_indent: true,
1810 };
1811
1812 let select_to_beg = SelectToBeginningOfLine {
1813 stop_at_soft_wraps: true,
1814 stop_at_indent: true,
1815 };
1816
1817 let delete_to_beg = DeleteToBeginningOfLine {
1818 stop_at_indent: true,
1819 };
1820
1821 let move_to_end = MoveToEndOfLine {
1822 stop_at_soft_wraps: false,
1823 };
1824
1825 let editor = cx.add_window(|window, cx| {
1826 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1827 build_editor(buffer, window, cx)
1828 });
1829
1830 _ = editor.update(cx, |editor, window, cx| {
1831 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1832 s.select_display_ranges([
1833 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1834 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1835 ]);
1836 });
1837
1838 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1839 // and the second cursor at the first non-whitespace character in the line.
1840 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1841 assert_eq!(
1842 editor.selections.display_ranges(cx),
1843 &[
1844 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1845 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1846 ]
1847 );
1848
1849 // Moving to the beginning of the line again should be a no-op for the first cursor,
1850 // and should move the second cursor to the beginning of the line.
1851 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1852 assert_eq!(
1853 editor.selections.display_ranges(cx),
1854 &[
1855 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1856 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1857 ]
1858 );
1859
1860 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1861 // and should move the second cursor back to the first non-whitespace character in the line.
1862 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1863 assert_eq!(
1864 editor.selections.display_ranges(cx),
1865 &[
1866 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1867 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1868 ]
1869 );
1870
1871 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1872 // and to the first non-whitespace character in the line for the second cursor.
1873 editor.move_to_end_of_line(&move_to_end, window, cx);
1874 editor.move_left(&MoveLeft, window, cx);
1875 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1876 assert_eq!(
1877 editor.selections.display_ranges(cx),
1878 &[
1879 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1880 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1881 ]
1882 );
1883
1884 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1885 // and should select to the beginning of the line for the second cursor.
1886 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1887 assert_eq!(
1888 editor.selections.display_ranges(cx),
1889 &[
1890 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1891 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1892 ]
1893 );
1894
1895 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1896 // and should delete to the first non-whitespace character in the line for the second cursor.
1897 editor.move_to_end_of_line(&move_to_end, window, cx);
1898 editor.move_left(&MoveLeft, window, cx);
1899 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1900 assert_eq!(editor.text(cx), "c\n f");
1901 });
1902}
1903
1904#[gpui::test]
1905fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1906 init_test(cx, |_| {});
1907
1908 let editor = cx.add_window(|window, cx| {
1909 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1910 build_editor(buffer, window, cx)
1911 });
1912 _ = editor.update(cx, |editor, window, cx| {
1913 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1914 s.select_display_ranges([
1915 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1916 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1917 ])
1918 });
1919 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1920 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1921
1922 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1923 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1924
1925 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1926 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1927
1928 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1929 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1930
1931 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1932 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
1933
1934 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1935 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1936
1937 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1938 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1939
1940 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1941 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1942
1943 editor.move_right(&MoveRight, window, cx);
1944 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1945 assert_selection_ranges(
1946 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
1947 editor,
1948 cx,
1949 );
1950
1951 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1952 assert_selection_ranges(
1953 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
1954 editor,
1955 cx,
1956 );
1957
1958 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1959 assert_selection_ranges(
1960 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
1961 editor,
1962 cx,
1963 );
1964 });
1965}
1966
1967#[gpui::test]
1968fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1969 init_test(cx, |_| {});
1970
1971 let editor = cx.add_window(|window, cx| {
1972 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1973 build_editor(buffer, window, cx)
1974 });
1975
1976 _ = editor.update(cx, |editor, window, cx| {
1977 editor.set_wrap_width(Some(140.0.into()), cx);
1978 assert_eq!(
1979 editor.display_text(cx),
1980 "use one::{\n two::three::\n four::five\n};"
1981 );
1982
1983 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1984 s.select_display_ranges([
1985 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1986 ]);
1987 });
1988
1989 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1990 assert_eq!(
1991 editor.selections.display_ranges(cx),
1992 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1993 );
1994
1995 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1996 assert_eq!(
1997 editor.selections.display_ranges(cx),
1998 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1999 );
2000
2001 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2002 assert_eq!(
2003 editor.selections.display_ranges(cx),
2004 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2005 );
2006
2007 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2008 assert_eq!(
2009 editor.selections.display_ranges(cx),
2010 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2011 );
2012
2013 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2014 assert_eq!(
2015 editor.selections.display_ranges(cx),
2016 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2017 );
2018
2019 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2020 assert_eq!(
2021 editor.selections.display_ranges(cx),
2022 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2023 );
2024 });
2025}
2026
2027#[gpui::test]
2028async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2029 init_test(cx, |_| {});
2030 let mut cx = EditorTestContext::new(cx).await;
2031
2032 let line_height = cx.editor(|editor, window, _| {
2033 editor
2034 .style()
2035 .unwrap()
2036 .text
2037 .line_height_in_pixels(window.rem_size())
2038 });
2039 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2040
2041 cx.set_state(
2042 &r#"ˇone
2043 two
2044
2045 three
2046 fourˇ
2047 five
2048
2049 six"#
2050 .unindent(),
2051 );
2052
2053 cx.update_editor(|editor, window, cx| {
2054 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2055 });
2056 cx.assert_editor_state(
2057 &r#"one
2058 two
2059 ˇ
2060 three
2061 four
2062 five
2063 ˇ
2064 six"#
2065 .unindent(),
2066 );
2067
2068 cx.update_editor(|editor, window, cx| {
2069 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2070 });
2071 cx.assert_editor_state(
2072 &r#"one
2073 two
2074
2075 three
2076 four
2077 five
2078 ˇ
2079 sixˇ"#
2080 .unindent(),
2081 );
2082
2083 cx.update_editor(|editor, window, cx| {
2084 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2085 });
2086 cx.assert_editor_state(
2087 &r#"one
2088 two
2089
2090 three
2091 four
2092 five
2093
2094 sixˇ"#
2095 .unindent(),
2096 );
2097
2098 cx.update_editor(|editor, window, cx| {
2099 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2100 });
2101 cx.assert_editor_state(
2102 &r#"one
2103 two
2104
2105 three
2106 four
2107 five
2108 ˇ
2109 six"#
2110 .unindent(),
2111 );
2112
2113 cx.update_editor(|editor, window, cx| {
2114 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2115 });
2116 cx.assert_editor_state(
2117 &r#"one
2118 two
2119 ˇ
2120 three
2121 four
2122 five
2123
2124 six"#
2125 .unindent(),
2126 );
2127
2128 cx.update_editor(|editor, window, cx| {
2129 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2130 });
2131 cx.assert_editor_state(
2132 &r#"ˇone
2133 two
2134
2135 three
2136 four
2137 five
2138
2139 six"#
2140 .unindent(),
2141 );
2142}
2143
2144#[gpui::test]
2145async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2146 init_test(cx, |_| {});
2147 let mut cx = EditorTestContext::new(cx).await;
2148 let line_height = cx.editor(|editor, window, _| {
2149 editor
2150 .style()
2151 .unwrap()
2152 .text
2153 .line_height_in_pixels(window.rem_size())
2154 });
2155 let window = cx.window;
2156 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2157
2158 cx.set_state(
2159 r#"ˇone
2160 two
2161 three
2162 four
2163 five
2164 six
2165 seven
2166 eight
2167 nine
2168 ten
2169 "#,
2170 );
2171
2172 cx.update_editor(|editor, window, cx| {
2173 assert_eq!(
2174 editor.snapshot(window, cx).scroll_position(),
2175 gpui::Point::new(0., 0.)
2176 );
2177 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2178 assert_eq!(
2179 editor.snapshot(window, cx).scroll_position(),
2180 gpui::Point::new(0., 3.)
2181 );
2182 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2183 assert_eq!(
2184 editor.snapshot(window, cx).scroll_position(),
2185 gpui::Point::new(0., 6.)
2186 );
2187 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2188 assert_eq!(
2189 editor.snapshot(window, cx).scroll_position(),
2190 gpui::Point::new(0., 3.)
2191 );
2192
2193 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2194 assert_eq!(
2195 editor.snapshot(window, cx).scroll_position(),
2196 gpui::Point::new(0., 1.)
2197 );
2198 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2199 assert_eq!(
2200 editor.snapshot(window, cx).scroll_position(),
2201 gpui::Point::new(0., 3.)
2202 );
2203 });
2204}
2205
2206#[gpui::test]
2207async fn test_autoscroll(cx: &mut TestAppContext) {
2208 init_test(cx, |_| {});
2209 let mut cx = EditorTestContext::new(cx).await;
2210
2211 let line_height = cx.update_editor(|editor, window, cx| {
2212 editor.set_vertical_scroll_margin(2, cx);
2213 editor
2214 .style()
2215 .unwrap()
2216 .text
2217 .line_height_in_pixels(window.rem_size())
2218 });
2219 let window = cx.window;
2220 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2221
2222 cx.set_state(
2223 r#"ˇone
2224 two
2225 three
2226 four
2227 five
2228 six
2229 seven
2230 eight
2231 nine
2232 ten
2233 "#,
2234 );
2235 cx.update_editor(|editor, window, cx| {
2236 assert_eq!(
2237 editor.snapshot(window, cx).scroll_position(),
2238 gpui::Point::new(0., 0.0)
2239 );
2240 });
2241
2242 // Add a cursor below the visible area. Since both cursors cannot fit
2243 // on screen, the editor autoscrolls to reveal the newest cursor, and
2244 // allows the vertical scroll margin below that cursor.
2245 cx.update_editor(|editor, window, cx| {
2246 editor.change_selections(Default::default(), window, cx, |selections| {
2247 selections.select_ranges([
2248 Point::new(0, 0)..Point::new(0, 0),
2249 Point::new(6, 0)..Point::new(6, 0),
2250 ]);
2251 })
2252 });
2253 cx.update_editor(|editor, window, cx| {
2254 assert_eq!(
2255 editor.snapshot(window, cx).scroll_position(),
2256 gpui::Point::new(0., 3.0)
2257 );
2258 });
2259
2260 // Move down. The editor cursor scrolls down to track the newest cursor.
2261 cx.update_editor(|editor, window, cx| {
2262 editor.move_down(&Default::default(), window, cx);
2263 });
2264 cx.update_editor(|editor, window, cx| {
2265 assert_eq!(
2266 editor.snapshot(window, cx).scroll_position(),
2267 gpui::Point::new(0., 4.0)
2268 );
2269 });
2270
2271 // Add a cursor above the visible area. Since both cursors fit on screen,
2272 // the editor scrolls to show both.
2273 cx.update_editor(|editor, window, cx| {
2274 editor.change_selections(Default::default(), window, cx, |selections| {
2275 selections.select_ranges([
2276 Point::new(1, 0)..Point::new(1, 0),
2277 Point::new(6, 0)..Point::new(6, 0),
2278 ]);
2279 })
2280 });
2281 cx.update_editor(|editor, window, cx| {
2282 assert_eq!(
2283 editor.snapshot(window, cx).scroll_position(),
2284 gpui::Point::new(0., 1.0)
2285 );
2286 });
2287}
2288
2289#[gpui::test]
2290async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2291 init_test(cx, |_| {});
2292 let mut cx = EditorTestContext::new(cx).await;
2293
2294 let line_height = cx.editor(|editor, window, _cx| {
2295 editor
2296 .style()
2297 .unwrap()
2298 .text
2299 .line_height_in_pixels(window.rem_size())
2300 });
2301 let window = cx.window;
2302 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2303 cx.set_state(
2304 &r#"
2305 ˇone
2306 two
2307 threeˇ
2308 four
2309 five
2310 six
2311 seven
2312 eight
2313 nine
2314 ten
2315 "#
2316 .unindent(),
2317 );
2318
2319 cx.update_editor(|editor, window, cx| {
2320 editor.move_page_down(&MovePageDown::default(), window, cx)
2321 });
2322 cx.assert_editor_state(
2323 &r#"
2324 one
2325 two
2326 three
2327 ˇfour
2328 five
2329 sixˇ
2330 seven
2331 eight
2332 nine
2333 ten
2334 "#
2335 .unindent(),
2336 );
2337
2338 cx.update_editor(|editor, window, cx| {
2339 editor.move_page_down(&MovePageDown::default(), window, cx)
2340 });
2341 cx.assert_editor_state(
2342 &r#"
2343 one
2344 two
2345 three
2346 four
2347 five
2348 six
2349 ˇseven
2350 eight
2351 nineˇ
2352 ten
2353 "#
2354 .unindent(),
2355 );
2356
2357 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2358 cx.assert_editor_state(
2359 &r#"
2360 one
2361 two
2362 three
2363 ˇfour
2364 five
2365 sixˇ
2366 seven
2367 eight
2368 nine
2369 ten
2370 "#
2371 .unindent(),
2372 );
2373
2374 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2375 cx.assert_editor_state(
2376 &r#"
2377 ˇone
2378 two
2379 threeˇ
2380 four
2381 five
2382 six
2383 seven
2384 eight
2385 nine
2386 ten
2387 "#
2388 .unindent(),
2389 );
2390
2391 // Test select collapsing
2392 cx.update_editor(|editor, window, cx| {
2393 editor.move_page_down(&MovePageDown::default(), window, cx);
2394 editor.move_page_down(&MovePageDown::default(), window, cx);
2395 editor.move_page_down(&MovePageDown::default(), window, cx);
2396 });
2397 cx.assert_editor_state(
2398 &r#"
2399 one
2400 two
2401 three
2402 four
2403 five
2404 six
2405 seven
2406 eight
2407 nine
2408 ˇten
2409 ˇ"#
2410 .unindent(),
2411 );
2412}
2413
2414#[gpui::test]
2415async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2416 init_test(cx, |_| {});
2417 let mut cx = EditorTestContext::new(cx).await;
2418 cx.set_state("one «two threeˇ» four");
2419 cx.update_editor(|editor, window, cx| {
2420 editor.delete_to_beginning_of_line(
2421 &DeleteToBeginningOfLine {
2422 stop_at_indent: false,
2423 },
2424 window,
2425 cx,
2426 );
2427 assert_eq!(editor.text(cx), " four");
2428 });
2429}
2430
2431#[gpui::test]
2432fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2433 init_test(cx, |_| {});
2434
2435 let editor = cx.add_window(|window, cx| {
2436 let buffer = MultiBuffer::build_simple("one two three four", cx);
2437 build_editor(buffer.clone(), window, cx)
2438 });
2439
2440 _ = editor.update(cx, |editor, window, cx| {
2441 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2442 s.select_display_ranges([
2443 // an empty selection - the preceding word fragment is deleted
2444 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2445 // characters selected - they are deleted
2446 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2447 ])
2448 });
2449 editor.delete_to_previous_word_start(
2450 &DeleteToPreviousWordStart {
2451 ignore_newlines: false,
2452 },
2453 window,
2454 cx,
2455 );
2456 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2457 });
2458
2459 _ = editor.update(cx, |editor, window, cx| {
2460 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2461 s.select_display_ranges([
2462 // an empty selection - the following word fragment is deleted
2463 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2464 // characters selected - they are deleted
2465 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2466 ])
2467 });
2468 editor.delete_to_next_word_end(
2469 &DeleteToNextWordEnd {
2470 ignore_newlines: false,
2471 },
2472 window,
2473 cx,
2474 );
2475 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2476 });
2477}
2478
2479#[gpui::test]
2480fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2481 init_test(cx, |_| {});
2482
2483 let editor = cx.add_window(|window, cx| {
2484 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2485 build_editor(buffer.clone(), window, cx)
2486 });
2487 let del_to_prev_word_start = DeleteToPreviousWordStart {
2488 ignore_newlines: false,
2489 };
2490 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2491 ignore_newlines: true,
2492 };
2493
2494 _ = editor.update(cx, |editor, window, cx| {
2495 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2496 s.select_display_ranges([
2497 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2498 ])
2499 });
2500 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2501 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2502 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2503 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2504 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2505 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2506 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2507 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2508 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2509 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2510 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2511 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2512 });
2513}
2514
2515#[gpui::test]
2516fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2517 init_test(cx, |_| {});
2518
2519 let editor = cx.add_window(|window, cx| {
2520 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2521 build_editor(buffer.clone(), window, cx)
2522 });
2523 let del_to_next_word_end = DeleteToNextWordEnd {
2524 ignore_newlines: false,
2525 };
2526 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2527 ignore_newlines: true,
2528 };
2529
2530 _ = editor.update(cx, |editor, window, cx| {
2531 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2532 s.select_display_ranges([
2533 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2534 ])
2535 });
2536 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2537 assert_eq!(
2538 editor.buffer.read(cx).read(cx).text(),
2539 "one\n two\nthree\n four"
2540 );
2541 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2542 assert_eq!(
2543 editor.buffer.read(cx).read(cx).text(),
2544 "\n two\nthree\n four"
2545 );
2546 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2547 assert_eq!(
2548 editor.buffer.read(cx).read(cx).text(),
2549 "two\nthree\n four"
2550 );
2551 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2552 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2553 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2554 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2555 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2556 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2557 });
2558}
2559
2560#[gpui::test]
2561fn test_newline(cx: &mut TestAppContext) {
2562 init_test(cx, |_| {});
2563
2564 let editor = cx.add_window(|window, cx| {
2565 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2566 build_editor(buffer.clone(), window, cx)
2567 });
2568
2569 _ = editor.update(cx, |editor, window, cx| {
2570 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2571 s.select_display_ranges([
2572 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2573 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2574 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2575 ])
2576 });
2577
2578 editor.newline(&Newline, window, cx);
2579 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2580 });
2581}
2582
2583#[gpui::test]
2584fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2585 init_test(cx, |_| {});
2586
2587 let editor = cx.add_window(|window, cx| {
2588 let buffer = MultiBuffer::build_simple(
2589 "
2590 a
2591 b(
2592 X
2593 )
2594 c(
2595 X
2596 )
2597 "
2598 .unindent()
2599 .as_str(),
2600 cx,
2601 );
2602 let mut editor = build_editor(buffer.clone(), window, cx);
2603 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2604 s.select_ranges([
2605 Point::new(2, 4)..Point::new(2, 5),
2606 Point::new(5, 4)..Point::new(5, 5),
2607 ])
2608 });
2609 editor
2610 });
2611
2612 _ = editor.update(cx, |editor, window, cx| {
2613 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2614 editor.buffer.update(cx, |buffer, cx| {
2615 buffer.edit(
2616 [
2617 (Point::new(1, 2)..Point::new(3, 0), ""),
2618 (Point::new(4, 2)..Point::new(6, 0), ""),
2619 ],
2620 None,
2621 cx,
2622 );
2623 assert_eq!(
2624 buffer.read(cx).text(),
2625 "
2626 a
2627 b()
2628 c()
2629 "
2630 .unindent()
2631 );
2632 });
2633 assert_eq!(
2634 editor.selections.ranges(cx),
2635 &[
2636 Point::new(1, 2)..Point::new(1, 2),
2637 Point::new(2, 2)..Point::new(2, 2),
2638 ],
2639 );
2640
2641 editor.newline(&Newline, window, cx);
2642 assert_eq!(
2643 editor.text(cx),
2644 "
2645 a
2646 b(
2647 )
2648 c(
2649 )
2650 "
2651 .unindent()
2652 );
2653
2654 // The selections are moved after the inserted newlines
2655 assert_eq!(
2656 editor.selections.ranges(cx),
2657 &[
2658 Point::new(2, 0)..Point::new(2, 0),
2659 Point::new(4, 0)..Point::new(4, 0),
2660 ],
2661 );
2662 });
2663}
2664
2665#[gpui::test]
2666async fn test_newline_above(cx: &mut TestAppContext) {
2667 init_test(cx, |settings| {
2668 settings.defaults.tab_size = NonZeroU32::new(4)
2669 });
2670
2671 let language = Arc::new(
2672 Language::new(
2673 LanguageConfig::default(),
2674 Some(tree_sitter_rust::LANGUAGE.into()),
2675 )
2676 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2677 .unwrap(),
2678 );
2679
2680 let mut cx = EditorTestContext::new(cx).await;
2681 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2682 cx.set_state(indoc! {"
2683 const a: ˇA = (
2684 (ˇ
2685 «const_functionˇ»(ˇ),
2686 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2687 )ˇ
2688 ˇ);ˇ
2689 "});
2690
2691 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2692 cx.assert_editor_state(indoc! {"
2693 ˇ
2694 const a: A = (
2695 ˇ
2696 (
2697 ˇ
2698 ˇ
2699 const_function(),
2700 ˇ
2701 ˇ
2702 ˇ
2703 ˇ
2704 something_else,
2705 ˇ
2706 )
2707 ˇ
2708 ˇ
2709 );
2710 "});
2711}
2712
2713#[gpui::test]
2714async fn test_newline_below(cx: &mut TestAppContext) {
2715 init_test(cx, |settings| {
2716 settings.defaults.tab_size = NonZeroU32::new(4)
2717 });
2718
2719 let language = Arc::new(
2720 Language::new(
2721 LanguageConfig::default(),
2722 Some(tree_sitter_rust::LANGUAGE.into()),
2723 )
2724 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2725 .unwrap(),
2726 );
2727
2728 let mut cx = EditorTestContext::new(cx).await;
2729 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2730 cx.set_state(indoc! {"
2731 const a: ˇA = (
2732 (ˇ
2733 «const_functionˇ»(ˇ),
2734 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2735 )ˇ
2736 ˇ);ˇ
2737 "});
2738
2739 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2740 cx.assert_editor_state(indoc! {"
2741 const a: A = (
2742 ˇ
2743 (
2744 ˇ
2745 const_function(),
2746 ˇ
2747 ˇ
2748 something_else,
2749 ˇ
2750 ˇ
2751 ˇ
2752 ˇ
2753 )
2754 ˇ
2755 );
2756 ˇ
2757 ˇ
2758 "});
2759}
2760
2761#[gpui::test]
2762async fn test_newline_comments(cx: &mut TestAppContext) {
2763 init_test(cx, |settings| {
2764 settings.defaults.tab_size = NonZeroU32::new(4)
2765 });
2766
2767 let language = Arc::new(Language::new(
2768 LanguageConfig {
2769 line_comments: vec!["// ".into()],
2770 ..LanguageConfig::default()
2771 },
2772 None,
2773 ));
2774 {
2775 let mut cx = EditorTestContext::new(cx).await;
2776 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2777 cx.set_state(indoc! {"
2778 // Fooˇ
2779 "});
2780
2781 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2782 cx.assert_editor_state(indoc! {"
2783 // Foo
2784 // ˇ
2785 "});
2786 // Ensure that we add comment prefix when existing line contains space
2787 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2788 cx.assert_editor_state(
2789 indoc! {"
2790 // Foo
2791 //s
2792 // ˇ
2793 "}
2794 .replace("s", " ") // s is used as space placeholder to prevent format on save
2795 .as_str(),
2796 );
2797 // Ensure that we add comment prefix when existing line does not contain space
2798 cx.set_state(indoc! {"
2799 // Foo
2800 //ˇ
2801 "});
2802 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2803 cx.assert_editor_state(indoc! {"
2804 // Foo
2805 //
2806 // ˇ
2807 "});
2808 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2809 cx.set_state(indoc! {"
2810 ˇ// Foo
2811 "});
2812 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2813 cx.assert_editor_state(indoc! {"
2814
2815 ˇ// Foo
2816 "});
2817 }
2818 // Ensure that comment continuations can be disabled.
2819 update_test_language_settings(cx, |settings| {
2820 settings.defaults.extend_comment_on_newline = Some(false);
2821 });
2822 let mut cx = EditorTestContext::new(cx).await;
2823 cx.set_state(indoc! {"
2824 // Fooˇ
2825 "});
2826 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2827 cx.assert_editor_state(indoc! {"
2828 // Foo
2829 ˇ
2830 "});
2831}
2832
2833#[gpui::test]
2834async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
2835 init_test(cx, |settings| {
2836 settings.defaults.tab_size = NonZeroU32::new(4)
2837 });
2838
2839 let language = Arc::new(Language::new(
2840 LanguageConfig {
2841 line_comments: vec!["// ".into(), "/// ".into()],
2842 ..LanguageConfig::default()
2843 },
2844 None,
2845 ));
2846 {
2847 let mut cx = EditorTestContext::new(cx).await;
2848 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2849 cx.set_state(indoc! {"
2850 //ˇ
2851 "});
2852 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2853 cx.assert_editor_state(indoc! {"
2854 //
2855 // ˇ
2856 "});
2857
2858 cx.set_state(indoc! {"
2859 ///ˇ
2860 "});
2861 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2862 cx.assert_editor_state(indoc! {"
2863 ///
2864 /// ˇ
2865 "});
2866 }
2867}
2868
2869#[gpui::test]
2870async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
2871 init_test(cx, |settings| {
2872 settings.defaults.tab_size = NonZeroU32::new(4)
2873 });
2874
2875 let language = Arc::new(
2876 Language::new(
2877 LanguageConfig {
2878 documentation_comment: Some(language::BlockCommentConfig {
2879 start: "/**".into(),
2880 end: "*/".into(),
2881 prefix: "* ".into(),
2882 tab_size: 1,
2883 }),
2884
2885 ..LanguageConfig::default()
2886 },
2887 Some(tree_sitter_rust::LANGUAGE.into()),
2888 )
2889 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
2890 .unwrap(),
2891 );
2892
2893 {
2894 let mut cx = EditorTestContext::new(cx).await;
2895 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2896 cx.set_state(indoc! {"
2897 /**ˇ
2898 "});
2899
2900 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2901 cx.assert_editor_state(indoc! {"
2902 /**
2903 * ˇ
2904 "});
2905 // Ensure that if cursor is before the comment start,
2906 // we do not actually insert a comment prefix.
2907 cx.set_state(indoc! {"
2908 ˇ/**
2909 "});
2910 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2911 cx.assert_editor_state(indoc! {"
2912
2913 ˇ/**
2914 "});
2915 // Ensure that if cursor is between it doesn't add comment prefix.
2916 cx.set_state(indoc! {"
2917 /*ˇ*
2918 "});
2919 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2920 cx.assert_editor_state(indoc! {"
2921 /*
2922 ˇ*
2923 "});
2924 // Ensure that if suffix exists on same line after cursor it adds new line.
2925 cx.set_state(indoc! {"
2926 /**ˇ*/
2927 "});
2928 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2929 cx.assert_editor_state(indoc! {"
2930 /**
2931 * ˇ
2932 */
2933 "});
2934 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2935 cx.set_state(indoc! {"
2936 /**ˇ */
2937 "});
2938 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2939 cx.assert_editor_state(indoc! {"
2940 /**
2941 * ˇ
2942 */
2943 "});
2944 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2945 cx.set_state(indoc! {"
2946 /** ˇ*/
2947 "});
2948 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2949 cx.assert_editor_state(
2950 indoc! {"
2951 /**s
2952 * ˇ
2953 */
2954 "}
2955 .replace("s", " ") // s is used as space placeholder to prevent format on save
2956 .as_str(),
2957 );
2958 // Ensure that delimiter space is preserved when newline on already
2959 // spaced delimiter.
2960 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2961 cx.assert_editor_state(
2962 indoc! {"
2963 /**s
2964 *s
2965 * ˇ
2966 */
2967 "}
2968 .replace("s", " ") // s is used as space placeholder to prevent format on save
2969 .as_str(),
2970 );
2971 // Ensure that delimiter space is preserved when space is not
2972 // on existing delimiter.
2973 cx.set_state(indoc! {"
2974 /**
2975 *ˇ
2976 */
2977 "});
2978 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2979 cx.assert_editor_state(indoc! {"
2980 /**
2981 *
2982 * ˇ
2983 */
2984 "});
2985 // Ensure that if suffix exists on same line after cursor it
2986 // doesn't add extra new line if prefix is not on same line.
2987 cx.set_state(indoc! {"
2988 /**
2989 ˇ*/
2990 "});
2991 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2992 cx.assert_editor_state(indoc! {"
2993 /**
2994
2995 ˇ*/
2996 "});
2997 // Ensure that it detects suffix after existing prefix.
2998 cx.set_state(indoc! {"
2999 /**ˇ/
3000 "});
3001 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3002 cx.assert_editor_state(indoc! {"
3003 /**
3004 ˇ/
3005 "});
3006 // Ensure that if suffix exists on same line before
3007 // cursor it does not add comment prefix.
3008 cx.set_state(indoc! {"
3009 /** */ˇ
3010 "});
3011 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3012 cx.assert_editor_state(indoc! {"
3013 /** */
3014 ˇ
3015 "});
3016 // Ensure that if suffix exists on same line before
3017 // cursor it does not add comment prefix.
3018 cx.set_state(indoc! {"
3019 /**
3020 *
3021 */ˇ
3022 "});
3023 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3024 cx.assert_editor_state(indoc! {"
3025 /**
3026 *
3027 */
3028 ˇ
3029 "});
3030
3031 // Ensure that inline comment followed by code
3032 // doesn't add comment prefix on newline
3033 cx.set_state(indoc! {"
3034 /** */ textˇ
3035 "});
3036 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3037 cx.assert_editor_state(indoc! {"
3038 /** */ text
3039 ˇ
3040 "});
3041
3042 // Ensure that text after comment end tag
3043 // doesn't add comment prefix on newline
3044 cx.set_state(indoc! {"
3045 /**
3046 *
3047 */ˇtext
3048 "});
3049 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3050 cx.assert_editor_state(indoc! {"
3051 /**
3052 *
3053 */
3054 ˇtext
3055 "});
3056
3057 // Ensure if not comment block it doesn't
3058 // add comment prefix on newline
3059 cx.set_state(indoc! {"
3060 * textˇ
3061 "});
3062 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3063 cx.assert_editor_state(indoc! {"
3064 * text
3065 ˇ
3066 "});
3067 }
3068 // Ensure that comment continuations can be disabled.
3069 update_test_language_settings(cx, |settings| {
3070 settings.defaults.extend_comment_on_newline = Some(false);
3071 });
3072 let mut cx = EditorTestContext::new(cx).await;
3073 cx.set_state(indoc! {"
3074 /**ˇ
3075 "});
3076 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3077 cx.assert_editor_state(indoc! {"
3078 /**
3079 ˇ
3080 "});
3081}
3082
3083#[gpui::test]
3084async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3085 init_test(cx, |settings| {
3086 settings.defaults.tab_size = NonZeroU32::new(4)
3087 });
3088
3089 let lua_language = Arc::new(Language::new(
3090 LanguageConfig {
3091 line_comments: vec!["--".into()],
3092 block_comment: Some(language::BlockCommentConfig {
3093 start: "--[[".into(),
3094 prefix: "".into(),
3095 end: "]]".into(),
3096 tab_size: 0,
3097 }),
3098 ..LanguageConfig::default()
3099 },
3100 None,
3101 ));
3102
3103 let mut cx = EditorTestContext::new(cx).await;
3104 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3105
3106 // Line with line comment should extend
3107 cx.set_state(indoc! {"
3108 --ˇ
3109 "});
3110 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3111 cx.assert_editor_state(indoc! {"
3112 --
3113 --ˇ
3114 "});
3115
3116 // Line with block comment that matches line comment should not extend
3117 cx.set_state(indoc! {"
3118 --[[ˇ
3119 "});
3120 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3121 cx.assert_editor_state(indoc! {"
3122 --[[
3123 ˇ
3124 "});
3125}
3126
3127#[gpui::test]
3128fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3129 init_test(cx, |_| {});
3130
3131 let editor = cx.add_window(|window, cx| {
3132 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3133 let mut editor = build_editor(buffer.clone(), window, cx);
3134 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3135 s.select_ranges([3..4, 11..12, 19..20])
3136 });
3137 editor
3138 });
3139
3140 _ = editor.update(cx, |editor, window, cx| {
3141 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3142 editor.buffer.update(cx, |buffer, cx| {
3143 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3144 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3145 });
3146 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3147
3148 editor.insert("Z", window, cx);
3149 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3150
3151 // The selections are moved after the inserted characters
3152 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3153 });
3154}
3155
3156#[gpui::test]
3157async fn test_tab(cx: &mut TestAppContext) {
3158 init_test(cx, |settings| {
3159 settings.defaults.tab_size = NonZeroU32::new(3)
3160 });
3161
3162 let mut cx = EditorTestContext::new(cx).await;
3163 cx.set_state(indoc! {"
3164 ˇabˇc
3165 ˇ🏀ˇ🏀ˇefg
3166 dˇ
3167 "});
3168 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3169 cx.assert_editor_state(indoc! {"
3170 ˇab ˇc
3171 ˇ🏀 ˇ🏀 ˇefg
3172 d ˇ
3173 "});
3174
3175 cx.set_state(indoc! {"
3176 a
3177 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3178 "});
3179 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3180 cx.assert_editor_state(indoc! {"
3181 a
3182 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3183 "});
3184}
3185
3186#[gpui::test]
3187async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3188 init_test(cx, |_| {});
3189
3190 let mut cx = EditorTestContext::new(cx).await;
3191 let language = Arc::new(
3192 Language::new(
3193 LanguageConfig::default(),
3194 Some(tree_sitter_rust::LANGUAGE.into()),
3195 )
3196 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3197 .unwrap(),
3198 );
3199 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3200
3201 // test when all cursors are not at suggested indent
3202 // then simply move to their suggested indent location
3203 cx.set_state(indoc! {"
3204 const a: B = (
3205 c(
3206 ˇ
3207 ˇ )
3208 );
3209 "});
3210 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3211 cx.assert_editor_state(indoc! {"
3212 const a: B = (
3213 c(
3214 ˇ
3215 ˇ)
3216 );
3217 "});
3218
3219 // test cursor already at suggested indent not moving when
3220 // other cursors are yet to reach their suggested indents
3221 cx.set_state(indoc! {"
3222 ˇ
3223 const a: B = (
3224 c(
3225 d(
3226 ˇ
3227 )
3228 ˇ
3229 ˇ )
3230 );
3231 "});
3232 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3233 cx.assert_editor_state(indoc! {"
3234 ˇ
3235 const a: B = (
3236 c(
3237 d(
3238 ˇ
3239 )
3240 ˇ
3241 ˇ)
3242 );
3243 "});
3244 // test when all cursors are at suggested indent then tab is inserted
3245 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3246 cx.assert_editor_state(indoc! {"
3247 ˇ
3248 const a: B = (
3249 c(
3250 d(
3251 ˇ
3252 )
3253 ˇ
3254 ˇ)
3255 );
3256 "});
3257
3258 // test when current indent is less than suggested indent,
3259 // we adjust line to match suggested indent and move cursor to it
3260 //
3261 // when no other cursor is at word boundary, all of them should move
3262 cx.set_state(indoc! {"
3263 const a: B = (
3264 c(
3265 d(
3266 ˇ
3267 ˇ )
3268 ˇ )
3269 );
3270 "});
3271 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3272 cx.assert_editor_state(indoc! {"
3273 const a: B = (
3274 c(
3275 d(
3276 ˇ
3277 ˇ)
3278 ˇ)
3279 );
3280 "});
3281
3282 // test when current indent is less than suggested indent,
3283 // we adjust line to match suggested indent and move cursor to it
3284 //
3285 // when some other cursor is at word boundary, it should not move
3286 cx.set_state(indoc! {"
3287 const a: B = (
3288 c(
3289 d(
3290 ˇ
3291 ˇ )
3292 ˇ)
3293 );
3294 "});
3295 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3296 cx.assert_editor_state(indoc! {"
3297 const a: B = (
3298 c(
3299 d(
3300 ˇ
3301 ˇ)
3302 ˇ)
3303 );
3304 "});
3305
3306 // test when current indent is more than suggested indent,
3307 // we just move cursor to current indent instead of suggested indent
3308 //
3309 // when no other cursor is at word boundary, all of them should move
3310 cx.set_state(indoc! {"
3311 const a: B = (
3312 c(
3313 d(
3314 ˇ
3315 ˇ )
3316 ˇ )
3317 );
3318 "});
3319 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3320 cx.assert_editor_state(indoc! {"
3321 const a: B = (
3322 c(
3323 d(
3324 ˇ
3325 ˇ)
3326 ˇ)
3327 );
3328 "});
3329 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3330 cx.assert_editor_state(indoc! {"
3331 const a: B = (
3332 c(
3333 d(
3334 ˇ
3335 ˇ)
3336 ˇ)
3337 );
3338 "});
3339
3340 // test when current indent is more than suggested indent,
3341 // we just move cursor to current indent instead of suggested indent
3342 //
3343 // when some other cursor is at word boundary, it doesn't move
3344 cx.set_state(indoc! {"
3345 const a: B = (
3346 c(
3347 d(
3348 ˇ
3349 ˇ )
3350 ˇ)
3351 );
3352 "});
3353 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3354 cx.assert_editor_state(indoc! {"
3355 const a: B = (
3356 c(
3357 d(
3358 ˇ
3359 ˇ)
3360 ˇ)
3361 );
3362 "});
3363
3364 // handle auto-indent when there are multiple cursors on the same line
3365 cx.set_state(indoc! {"
3366 const a: B = (
3367 c(
3368 ˇ ˇ
3369 ˇ )
3370 );
3371 "});
3372 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3373 cx.assert_editor_state(indoc! {"
3374 const a: B = (
3375 c(
3376 ˇ
3377 ˇ)
3378 );
3379 "});
3380}
3381
3382#[gpui::test]
3383async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3384 init_test(cx, |settings| {
3385 settings.defaults.tab_size = NonZeroU32::new(3)
3386 });
3387
3388 let mut cx = EditorTestContext::new(cx).await;
3389 cx.set_state(indoc! {"
3390 ˇ
3391 \t ˇ
3392 \t ˇ
3393 \t ˇ
3394 \t \t\t \t \t\t \t\t \t \t ˇ
3395 "});
3396
3397 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3398 cx.assert_editor_state(indoc! {"
3399 ˇ
3400 \t ˇ
3401 \t ˇ
3402 \t ˇ
3403 \t \t\t \t \t\t \t\t \t \t ˇ
3404 "});
3405}
3406
3407#[gpui::test]
3408async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3409 init_test(cx, |settings| {
3410 settings.defaults.tab_size = NonZeroU32::new(4)
3411 });
3412
3413 let language = Arc::new(
3414 Language::new(
3415 LanguageConfig::default(),
3416 Some(tree_sitter_rust::LANGUAGE.into()),
3417 )
3418 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3419 .unwrap(),
3420 );
3421
3422 let mut cx = EditorTestContext::new(cx).await;
3423 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3424 cx.set_state(indoc! {"
3425 fn a() {
3426 if b {
3427 \t ˇc
3428 }
3429 }
3430 "});
3431
3432 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3433 cx.assert_editor_state(indoc! {"
3434 fn a() {
3435 if b {
3436 ˇc
3437 }
3438 }
3439 "});
3440}
3441
3442#[gpui::test]
3443async fn test_indent_outdent(cx: &mut TestAppContext) {
3444 init_test(cx, |settings| {
3445 settings.defaults.tab_size = NonZeroU32::new(4);
3446 });
3447
3448 let mut cx = EditorTestContext::new(cx).await;
3449
3450 cx.set_state(indoc! {"
3451 «oneˇ» «twoˇ»
3452 three
3453 four
3454 "});
3455 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3456 cx.assert_editor_state(indoc! {"
3457 «oneˇ» «twoˇ»
3458 three
3459 four
3460 "});
3461
3462 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3463 cx.assert_editor_state(indoc! {"
3464 «oneˇ» «twoˇ»
3465 three
3466 four
3467 "});
3468
3469 // select across line ending
3470 cx.set_state(indoc! {"
3471 one two
3472 t«hree
3473 ˇ» four
3474 "});
3475 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3476 cx.assert_editor_state(indoc! {"
3477 one two
3478 t«hree
3479 ˇ» four
3480 "});
3481
3482 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3483 cx.assert_editor_state(indoc! {"
3484 one two
3485 t«hree
3486 ˇ» four
3487 "});
3488
3489 // Ensure that indenting/outdenting works when the cursor is at column 0.
3490 cx.set_state(indoc! {"
3491 one two
3492 ˇthree
3493 four
3494 "});
3495 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3496 cx.assert_editor_state(indoc! {"
3497 one two
3498 ˇthree
3499 four
3500 "});
3501
3502 cx.set_state(indoc! {"
3503 one two
3504 ˇ three
3505 four
3506 "});
3507 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3508 cx.assert_editor_state(indoc! {"
3509 one two
3510 ˇthree
3511 four
3512 "});
3513}
3514
3515#[gpui::test]
3516async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3517 // This is a regression test for issue #33761
3518 init_test(cx, |_| {});
3519
3520 let mut cx = EditorTestContext::new(cx).await;
3521 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3522 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3523
3524 cx.set_state(
3525 r#"ˇ# ingress:
3526ˇ# api:
3527ˇ# enabled: false
3528ˇ# pathType: Prefix
3529ˇ# console:
3530ˇ# enabled: false
3531ˇ# pathType: Prefix
3532"#,
3533 );
3534
3535 // Press tab to indent all lines
3536 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3537
3538 cx.assert_editor_state(
3539 r#" ˇ# ingress:
3540 ˇ# api:
3541 ˇ# enabled: false
3542 ˇ# pathType: Prefix
3543 ˇ# console:
3544 ˇ# enabled: false
3545 ˇ# pathType: Prefix
3546"#,
3547 );
3548}
3549
3550#[gpui::test]
3551async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3552 // This is a test to make sure our fix for issue #33761 didn't break anything
3553 init_test(cx, |_| {});
3554
3555 let mut cx = EditorTestContext::new(cx).await;
3556 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3557 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3558
3559 cx.set_state(
3560 r#"ˇingress:
3561ˇ api:
3562ˇ enabled: false
3563ˇ pathType: Prefix
3564"#,
3565 );
3566
3567 // Press tab to indent all lines
3568 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3569
3570 cx.assert_editor_state(
3571 r#"ˇingress:
3572 ˇapi:
3573 ˇenabled: false
3574 ˇpathType: Prefix
3575"#,
3576 );
3577}
3578
3579#[gpui::test]
3580async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3581 init_test(cx, |settings| {
3582 settings.defaults.hard_tabs = Some(true);
3583 });
3584
3585 let mut cx = EditorTestContext::new(cx).await;
3586
3587 // select two ranges on one line
3588 cx.set_state(indoc! {"
3589 «oneˇ» «twoˇ»
3590 three
3591 four
3592 "});
3593 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3594 cx.assert_editor_state(indoc! {"
3595 \t«oneˇ» «twoˇ»
3596 three
3597 four
3598 "});
3599 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3600 cx.assert_editor_state(indoc! {"
3601 \t\t«oneˇ» «twoˇ»
3602 three
3603 four
3604 "});
3605 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3606 cx.assert_editor_state(indoc! {"
3607 \t«oneˇ» «twoˇ»
3608 three
3609 four
3610 "});
3611 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3612 cx.assert_editor_state(indoc! {"
3613 «oneˇ» «twoˇ»
3614 three
3615 four
3616 "});
3617
3618 // select across a line ending
3619 cx.set_state(indoc! {"
3620 one two
3621 t«hree
3622 ˇ»four
3623 "});
3624 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3625 cx.assert_editor_state(indoc! {"
3626 one two
3627 \tt«hree
3628 ˇ»four
3629 "});
3630 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3631 cx.assert_editor_state(indoc! {"
3632 one two
3633 \t\tt«hree
3634 ˇ»four
3635 "});
3636 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3637 cx.assert_editor_state(indoc! {"
3638 one two
3639 \tt«hree
3640 ˇ»four
3641 "});
3642 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3643 cx.assert_editor_state(indoc! {"
3644 one two
3645 t«hree
3646 ˇ»four
3647 "});
3648
3649 // Ensure that indenting/outdenting works when the cursor is at column 0.
3650 cx.set_state(indoc! {"
3651 one two
3652 ˇthree
3653 four
3654 "});
3655 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3656 cx.assert_editor_state(indoc! {"
3657 one two
3658 ˇthree
3659 four
3660 "});
3661 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3662 cx.assert_editor_state(indoc! {"
3663 one two
3664 \tˇthree
3665 four
3666 "});
3667 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3668 cx.assert_editor_state(indoc! {"
3669 one two
3670 ˇthree
3671 four
3672 "});
3673}
3674
3675#[gpui::test]
3676fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3677 init_test(cx, |settings| {
3678 settings.languages.0.extend([
3679 (
3680 "TOML".into(),
3681 LanguageSettingsContent {
3682 tab_size: NonZeroU32::new(2),
3683 ..Default::default()
3684 },
3685 ),
3686 (
3687 "Rust".into(),
3688 LanguageSettingsContent {
3689 tab_size: NonZeroU32::new(4),
3690 ..Default::default()
3691 },
3692 ),
3693 ]);
3694 });
3695
3696 let toml_language = Arc::new(Language::new(
3697 LanguageConfig {
3698 name: "TOML".into(),
3699 ..Default::default()
3700 },
3701 None,
3702 ));
3703 let rust_language = Arc::new(Language::new(
3704 LanguageConfig {
3705 name: "Rust".into(),
3706 ..Default::default()
3707 },
3708 None,
3709 ));
3710
3711 let toml_buffer =
3712 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3713 let rust_buffer =
3714 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3715 let multibuffer = cx.new(|cx| {
3716 let mut multibuffer = MultiBuffer::new(ReadWrite);
3717 multibuffer.push_excerpts(
3718 toml_buffer.clone(),
3719 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3720 cx,
3721 );
3722 multibuffer.push_excerpts(
3723 rust_buffer.clone(),
3724 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3725 cx,
3726 );
3727 multibuffer
3728 });
3729
3730 cx.add_window(|window, cx| {
3731 let mut editor = build_editor(multibuffer, window, cx);
3732
3733 assert_eq!(
3734 editor.text(cx),
3735 indoc! {"
3736 a = 1
3737 b = 2
3738
3739 const c: usize = 3;
3740 "}
3741 );
3742
3743 select_ranges(
3744 &mut editor,
3745 indoc! {"
3746 «aˇ» = 1
3747 b = 2
3748
3749 «const c:ˇ» usize = 3;
3750 "},
3751 window,
3752 cx,
3753 );
3754
3755 editor.tab(&Tab, window, cx);
3756 assert_text_with_selections(
3757 &mut editor,
3758 indoc! {"
3759 «aˇ» = 1
3760 b = 2
3761
3762 «const c:ˇ» usize = 3;
3763 "},
3764 cx,
3765 );
3766 editor.backtab(&Backtab, window, cx);
3767 assert_text_with_selections(
3768 &mut editor,
3769 indoc! {"
3770 «aˇ» = 1
3771 b = 2
3772
3773 «const c:ˇ» usize = 3;
3774 "},
3775 cx,
3776 );
3777
3778 editor
3779 });
3780}
3781
3782#[gpui::test]
3783async fn test_backspace(cx: &mut TestAppContext) {
3784 init_test(cx, |_| {});
3785
3786 let mut cx = EditorTestContext::new(cx).await;
3787
3788 // Basic backspace
3789 cx.set_state(indoc! {"
3790 onˇe two three
3791 fou«rˇ» five six
3792 seven «ˇeight nine
3793 »ten
3794 "});
3795 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3796 cx.assert_editor_state(indoc! {"
3797 oˇe two three
3798 fouˇ five six
3799 seven ˇten
3800 "});
3801
3802 // Test backspace inside and around indents
3803 cx.set_state(indoc! {"
3804 zero
3805 ˇone
3806 ˇtwo
3807 ˇ ˇ ˇ three
3808 ˇ ˇ four
3809 "});
3810 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3811 cx.assert_editor_state(indoc! {"
3812 zero
3813 ˇone
3814 ˇtwo
3815 ˇ threeˇ four
3816 "});
3817}
3818
3819#[gpui::test]
3820async fn test_delete(cx: &mut TestAppContext) {
3821 init_test(cx, |_| {});
3822
3823 let mut cx = EditorTestContext::new(cx).await;
3824 cx.set_state(indoc! {"
3825 onˇe two three
3826 fou«rˇ» five six
3827 seven «ˇeight nine
3828 »ten
3829 "});
3830 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3831 cx.assert_editor_state(indoc! {"
3832 onˇ two three
3833 fouˇ five six
3834 seven ˇten
3835 "});
3836}
3837
3838#[gpui::test]
3839fn test_delete_line(cx: &mut TestAppContext) {
3840 init_test(cx, |_| {});
3841
3842 let editor = cx.add_window(|window, cx| {
3843 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3844 build_editor(buffer, window, cx)
3845 });
3846 _ = editor.update(cx, |editor, window, cx| {
3847 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3848 s.select_display_ranges([
3849 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3850 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3851 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3852 ])
3853 });
3854 editor.delete_line(&DeleteLine, window, cx);
3855 assert_eq!(editor.display_text(cx), "ghi");
3856 assert_eq!(
3857 editor.selections.display_ranges(cx),
3858 vec![
3859 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3860 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3861 ]
3862 );
3863 });
3864
3865 let editor = cx.add_window(|window, cx| {
3866 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3867 build_editor(buffer, window, cx)
3868 });
3869 _ = editor.update(cx, |editor, window, cx| {
3870 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3871 s.select_display_ranges([
3872 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3873 ])
3874 });
3875 editor.delete_line(&DeleteLine, window, cx);
3876 assert_eq!(editor.display_text(cx), "ghi\n");
3877 assert_eq!(
3878 editor.selections.display_ranges(cx),
3879 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3880 );
3881 });
3882}
3883
3884#[gpui::test]
3885fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3886 init_test(cx, |_| {});
3887
3888 cx.add_window(|window, cx| {
3889 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3890 let mut editor = build_editor(buffer.clone(), window, cx);
3891 let buffer = buffer.read(cx).as_singleton().unwrap();
3892
3893 assert_eq!(
3894 editor.selections.ranges::<Point>(cx),
3895 &[Point::new(0, 0)..Point::new(0, 0)]
3896 );
3897
3898 // When on single line, replace newline at end by space
3899 editor.join_lines(&JoinLines, window, cx);
3900 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3901 assert_eq!(
3902 editor.selections.ranges::<Point>(cx),
3903 &[Point::new(0, 3)..Point::new(0, 3)]
3904 );
3905
3906 // When multiple lines are selected, remove newlines that are spanned by the selection
3907 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3908 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3909 });
3910 editor.join_lines(&JoinLines, window, cx);
3911 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3912 assert_eq!(
3913 editor.selections.ranges::<Point>(cx),
3914 &[Point::new(0, 11)..Point::new(0, 11)]
3915 );
3916
3917 // Undo should be transactional
3918 editor.undo(&Undo, window, cx);
3919 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3920 assert_eq!(
3921 editor.selections.ranges::<Point>(cx),
3922 &[Point::new(0, 5)..Point::new(2, 2)]
3923 );
3924
3925 // When joining an empty line don't insert a space
3926 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3927 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3928 });
3929 editor.join_lines(&JoinLines, window, cx);
3930 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3931 assert_eq!(
3932 editor.selections.ranges::<Point>(cx),
3933 [Point::new(2, 3)..Point::new(2, 3)]
3934 );
3935
3936 // We can remove trailing newlines
3937 editor.join_lines(&JoinLines, window, cx);
3938 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3939 assert_eq!(
3940 editor.selections.ranges::<Point>(cx),
3941 [Point::new(2, 3)..Point::new(2, 3)]
3942 );
3943
3944 // We don't blow up on the last line
3945 editor.join_lines(&JoinLines, window, cx);
3946 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3947 assert_eq!(
3948 editor.selections.ranges::<Point>(cx),
3949 [Point::new(2, 3)..Point::new(2, 3)]
3950 );
3951
3952 // reset to test indentation
3953 editor.buffer.update(cx, |buffer, cx| {
3954 buffer.edit(
3955 [
3956 (Point::new(1, 0)..Point::new(1, 2), " "),
3957 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3958 ],
3959 None,
3960 cx,
3961 )
3962 });
3963
3964 // We remove any leading spaces
3965 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3966 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3967 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3968 });
3969 editor.join_lines(&JoinLines, window, cx);
3970 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3971
3972 // We don't insert a space for a line containing only spaces
3973 editor.join_lines(&JoinLines, window, cx);
3974 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3975
3976 // We ignore any leading tabs
3977 editor.join_lines(&JoinLines, window, cx);
3978 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3979
3980 editor
3981 });
3982}
3983
3984#[gpui::test]
3985fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3986 init_test(cx, |_| {});
3987
3988 cx.add_window(|window, cx| {
3989 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3990 let mut editor = build_editor(buffer.clone(), window, cx);
3991 let buffer = buffer.read(cx).as_singleton().unwrap();
3992
3993 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3994 s.select_ranges([
3995 Point::new(0, 2)..Point::new(1, 1),
3996 Point::new(1, 2)..Point::new(1, 2),
3997 Point::new(3, 1)..Point::new(3, 2),
3998 ])
3999 });
4000
4001 editor.join_lines(&JoinLines, window, cx);
4002 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4003
4004 assert_eq!(
4005 editor.selections.ranges::<Point>(cx),
4006 [
4007 Point::new(0, 7)..Point::new(0, 7),
4008 Point::new(1, 3)..Point::new(1, 3)
4009 ]
4010 );
4011 editor
4012 });
4013}
4014
4015#[gpui::test]
4016async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4017 init_test(cx, |_| {});
4018
4019 let mut cx = EditorTestContext::new(cx).await;
4020
4021 let diff_base = r#"
4022 Line 0
4023 Line 1
4024 Line 2
4025 Line 3
4026 "#
4027 .unindent();
4028
4029 cx.set_state(
4030 &r#"
4031 ˇLine 0
4032 Line 1
4033 Line 2
4034 Line 3
4035 "#
4036 .unindent(),
4037 );
4038
4039 cx.set_head_text(&diff_base);
4040 executor.run_until_parked();
4041
4042 // Join lines
4043 cx.update_editor(|editor, window, cx| {
4044 editor.join_lines(&JoinLines, window, cx);
4045 });
4046 executor.run_until_parked();
4047
4048 cx.assert_editor_state(
4049 &r#"
4050 Line 0ˇ Line 1
4051 Line 2
4052 Line 3
4053 "#
4054 .unindent(),
4055 );
4056 // Join again
4057 cx.update_editor(|editor, window, cx| {
4058 editor.join_lines(&JoinLines, window, cx);
4059 });
4060 executor.run_until_parked();
4061
4062 cx.assert_editor_state(
4063 &r#"
4064 Line 0 Line 1ˇ Line 2
4065 Line 3
4066 "#
4067 .unindent(),
4068 );
4069}
4070
4071#[gpui::test]
4072async fn test_custom_newlines_cause_no_false_positive_diffs(
4073 executor: BackgroundExecutor,
4074 cx: &mut TestAppContext,
4075) {
4076 init_test(cx, |_| {});
4077 let mut cx = EditorTestContext::new(cx).await;
4078 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4079 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4080 executor.run_until_parked();
4081
4082 cx.update_editor(|editor, window, cx| {
4083 let snapshot = editor.snapshot(window, cx);
4084 assert_eq!(
4085 snapshot
4086 .buffer_snapshot
4087 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
4088 .collect::<Vec<_>>(),
4089 Vec::new(),
4090 "Should not have any diffs for files with custom newlines"
4091 );
4092 });
4093}
4094
4095#[gpui::test]
4096async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4097 init_test(cx, |_| {});
4098
4099 let mut cx = EditorTestContext::new(cx).await;
4100
4101 // Test sort_lines_case_insensitive()
4102 cx.set_state(indoc! {"
4103 «z
4104 y
4105 x
4106 Z
4107 Y
4108 Xˇ»
4109 "});
4110 cx.update_editor(|e, window, cx| {
4111 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4112 });
4113 cx.assert_editor_state(indoc! {"
4114 «x
4115 X
4116 y
4117 Y
4118 z
4119 Zˇ»
4120 "});
4121
4122 // Test sort_lines_by_length()
4123 //
4124 // Demonstrates:
4125 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4126 // - sort is stable
4127 cx.set_state(indoc! {"
4128 «123
4129 æ
4130 12
4131 ∞
4132 1
4133 æˇ»
4134 "});
4135 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4136 cx.assert_editor_state(indoc! {"
4137 «æ
4138 ∞
4139 1
4140 æ
4141 12
4142 123ˇ»
4143 "});
4144
4145 // Test reverse_lines()
4146 cx.set_state(indoc! {"
4147 «5
4148 4
4149 3
4150 2
4151 1ˇ»
4152 "});
4153 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4154 cx.assert_editor_state(indoc! {"
4155 «1
4156 2
4157 3
4158 4
4159 5ˇ»
4160 "});
4161
4162 // Skip testing shuffle_line()
4163
4164 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4165 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4166
4167 // Don't manipulate when cursor is on single line, but expand the selection
4168 cx.set_state(indoc! {"
4169 ddˇdd
4170 ccc
4171 bb
4172 a
4173 "});
4174 cx.update_editor(|e, window, cx| {
4175 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4176 });
4177 cx.assert_editor_state(indoc! {"
4178 «ddddˇ»
4179 ccc
4180 bb
4181 a
4182 "});
4183
4184 // Basic manipulate case
4185 // Start selection moves to column 0
4186 // End of selection shrinks to fit shorter line
4187 cx.set_state(indoc! {"
4188 dd«d
4189 ccc
4190 bb
4191 aaaaaˇ»
4192 "});
4193 cx.update_editor(|e, window, cx| {
4194 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4195 });
4196 cx.assert_editor_state(indoc! {"
4197 «aaaaa
4198 bb
4199 ccc
4200 dddˇ»
4201 "});
4202
4203 // Manipulate case with newlines
4204 cx.set_state(indoc! {"
4205 dd«d
4206 ccc
4207
4208 bb
4209 aaaaa
4210
4211 ˇ»
4212 "});
4213 cx.update_editor(|e, window, cx| {
4214 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4215 });
4216 cx.assert_editor_state(indoc! {"
4217 «
4218
4219 aaaaa
4220 bb
4221 ccc
4222 dddˇ»
4223
4224 "});
4225
4226 // Adding new line
4227 cx.set_state(indoc! {"
4228 aa«a
4229 bbˇ»b
4230 "});
4231 cx.update_editor(|e, window, cx| {
4232 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4233 });
4234 cx.assert_editor_state(indoc! {"
4235 «aaa
4236 bbb
4237 added_lineˇ»
4238 "});
4239
4240 // Removing line
4241 cx.set_state(indoc! {"
4242 aa«a
4243 bbbˇ»
4244 "});
4245 cx.update_editor(|e, window, cx| {
4246 e.manipulate_immutable_lines(window, cx, |lines| {
4247 lines.pop();
4248 })
4249 });
4250 cx.assert_editor_state(indoc! {"
4251 «aaaˇ»
4252 "});
4253
4254 // Removing all lines
4255 cx.set_state(indoc! {"
4256 aa«a
4257 bbbˇ»
4258 "});
4259 cx.update_editor(|e, window, cx| {
4260 e.manipulate_immutable_lines(window, cx, |lines| {
4261 lines.drain(..);
4262 })
4263 });
4264 cx.assert_editor_state(indoc! {"
4265 ˇ
4266 "});
4267}
4268
4269#[gpui::test]
4270async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4271 init_test(cx, |_| {});
4272
4273 let mut cx = EditorTestContext::new(cx).await;
4274
4275 // Consider continuous selection as single selection
4276 cx.set_state(indoc! {"
4277 Aaa«aa
4278 cˇ»c«c
4279 bb
4280 aaaˇ»aa
4281 "});
4282 cx.update_editor(|e, window, cx| {
4283 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4284 });
4285 cx.assert_editor_state(indoc! {"
4286 «Aaaaa
4287 ccc
4288 bb
4289 aaaaaˇ»
4290 "});
4291
4292 cx.set_state(indoc! {"
4293 Aaa«aa
4294 cˇ»c«c
4295 bb
4296 aaaˇ»aa
4297 "});
4298 cx.update_editor(|e, window, cx| {
4299 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4300 });
4301 cx.assert_editor_state(indoc! {"
4302 «Aaaaa
4303 ccc
4304 bbˇ»
4305 "});
4306
4307 // Consider non continuous selection as distinct dedup operations
4308 cx.set_state(indoc! {"
4309 «aaaaa
4310 bb
4311 aaaaa
4312 aaaaaˇ»
4313
4314 aaa«aaˇ»
4315 "});
4316 cx.update_editor(|e, window, cx| {
4317 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4318 });
4319 cx.assert_editor_state(indoc! {"
4320 «aaaaa
4321 bbˇ»
4322
4323 «aaaaaˇ»
4324 "});
4325}
4326
4327#[gpui::test]
4328async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4329 init_test(cx, |_| {});
4330
4331 let mut cx = EditorTestContext::new(cx).await;
4332
4333 cx.set_state(indoc! {"
4334 «Aaa
4335 aAa
4336 Aaaˇ»
4337 "});
4338 cx.update_editor(|e, window, cx| {
4339 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4340 });
4341 cx.assert_editor_state(indoc! {"
4342 «Aaa
4343 aAaˇ»
4344 "});
4345
4346 cx.set_state(indoc! {"
4347 «Aaa
4348 aAa
4349 aaAˇ»
4350 "});
4351 cx.update_editor(|e, window, cx| {
4352 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4353 });
4354 cx.assert_editor_state(indoc! {"
4355 «Aaaˇ»
4356 "});
4357}
4358
4359#[gpui::test]
4360async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4361 init_test(cx, |_| {});
4362
4363 let mut cx = EditorTestContext::new(cx).await;
4364
4365 // Manipulate with multiple selections on a single line
4366 cx.set_state(indoc! {"
4367 dd«dd
4368 cˇ»c«c
4369 bb
4370 aaaˇ»aa
4371 "});
4372 cx.update_editor(|e, window, cx| {
4373 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4374 });
4375 cx.assert_editor_state(indoc! {"
4376 «aaaaa
4377 bb
4378 ccc
4379 ddddˇ»
4380 "});
4381
4382 // Manipulate with multiple disjoin selections
4383 cx.set_state(indoc! {"
4384 5«
4385 4
4386 3
4387 2
4388 1ˇ»
4389
4390 dd«dd
4391 ccc
4392 bb
4393 aaaˇ»aa
4394 "});
4395 cx.update_editor(|e, window, cx| {
4396 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4397 });
4398 cx.assert_editor_state(indoc! {"
4399 «1
4400 2
4401 3
4402 4
4403 5ˇ»
4404
4405 «aaaaa
4406 bb
4407 ccc
4408 ddddˇ»
4409 "});
4410
4411 // Adding lines on each selection
4412 cx.set_state(indoc! {"
4413 2«
4414 1ˇ»
4415
4416 bb«bb
4417 aaaˇ»aa
4418 "});
4419 cx.update_editor(|e, window, cx| {
4420 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4421 });
4422 cx.assert_editor_state(indoc! {"
4423 «2
4424 1
4425 added lineˇ»
4426
4427 «bbbb
4428 aaaaa
4429 added lineˇ»
4430 "});
4431
4432 // Removing lines on each selection
4433 cx.set_state(indoc! {"
4434 2«
4435 1ˇ»
4436
4437 bb«bb
4438 aaaˇ»aa
4439 "});
4440 cx.update_editor(|e, window, cx| {
4441 e.manipulate_immutable_lines(window, cx, |lines| {
4442 lines.pop();
4443 })
4444 });
4445 cx.assert_editor_state(indoc! {"
4446 «2ˇ»
4447
4448 «bbbbˇ»
4449 "});
4450}
4451
4452#[gpui::test]
4453async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4454 init_test(cx, |settings| {
4455 settings.defaults.tab_size = NonZeroU32::new(3)
4456 });
4457
4458 let mut cx = EditorTestContext::new(cx).await;
4459
4460 // MULTI SELECTION
4461 // Ln.1 "«" tests empty lines
4462 // Ln.9 tests just leading whitespace
4463 cx.set_state(indoc! {"
4464 «
4465 abc // No indentationˇ»
4466 «\tabc // 1 tabˇ»
4467 \t\tabc « ˇ» // 2 tabs
4468 \t ab«c // Tab followed by space
4469 \tabc // Space followed by tab (3 spaces should be the result)
4470 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4471 abˇ»ˇc ˇ ˇ // Already space indented«
4472 \t
4473 \tabc\tdef // Only the leading tab is manipulatedˇ»
4474 "});
4475 cx.update_editor(|e, window, cx| {
4476 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4477 });
4478 cx.assert_editor_state(
4479 indoc! {"
4480 «
4481 abc // No indentation
4482 abc // 1 tab
4483 abc // 2 tabs
4484 abc // Tab followed by space
4485 abc // Space followed by tab (3 spaces should be the result)
4486 abc // Mixed indentation (tab conversion depends on the column)
4487 abc // Already space indented
4488 ·
4489 abc\tdef // Only the leading tab is manipulatedˇ»
4490 "}
4491 .replace("·", "")
4492 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4493 );
4494
4495 // Test on just a few lines, the others should remain unchanged
4496 // Only lines (3, 5, 10, 11) should change
4497 cx.set_state(
4498 indoc! {"
4499 ·
4500 abc // No indentation
4501 \tabcˇ // 1 tab
4502 \t\tabc // 2 tabs
4503 \t abcˇ // Tab followed by space
4504 \tabc // Space followed by tab (3 spaces should be the result)
4505 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4506 abc // Already space indented
4507 «\t
4508 \tabc\tdef // Only the leading tab is manipulatedˇ»
4509 "}
4510 .replace("·", "")
4511 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4512 );
4513 cx.update_editor(|e, window, cx| {
4514 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4515 });
4516 cx.assert_editor_state(
4517 indoc! {"
4518 ·
4519 abc // No indentation
4520 « abc // 1 tabˇ»
4521 \t\tabc // 2 tabs
4522 « abc // Tab followed by spaceˇ»
4523 \tabc // Space followed by tab (3 spaces should be the result)
4524 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4525 abc // Already space indented
4526 « ·
4527 abc\tdef // Only the leading tab is manipulatedˇ»
4528 "}
4529 .replace("·", "")
4530 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4531 );
4532
4533 // SINGLE SELECTION
4534 // Ln.1 "«" tests empty lines
4535 // Ln.9 tests just leading whitespace
4536 cx.set_state(indoc! {"
4537 «
4538 abc // No indentation
4539 \tabc // 1 tab
4540 \t\tabc // 2 tabs
4541 \t abc // Tab followed by space
4542 \tabc // Space followed by tab (3 spaces should be the result)
4543 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4544 abc // Already space indented
4545 \t
4546 \tabc\tdef // Only the leading tab is manipulatedˇ»
4547 "});
4548 cx.update_editor(|e, window, cx| {
4549 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4550 });
4551 cx.assert_editor_state(
4552 indoc! {"
4553 «
4554 abc // No indentation
4555 abc // 1 tab
4556 abc // 2 tabs
4557 abc // Tab followed by space
4558 abc // Space followed by tab (3 spaces should be the result)
4559 abc // Mixed indentation (tab conversion depends on the column)
4560 abc // Already space indented
4561 ·
4562 abc\tdef // Only the leading tab is manipulatedˇ»
4563 "}
4564 .replace("·", "")
4565 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4566 );
4567}
4568
4569#[gpui::test]
4570async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
4571 init_test(cx, |settings| {
4572 settings.defaults.tab_size = NonZeroU32::new(3)
4573 });
4574
4575 let mut cx = EditorTestContext::new(cx).await;
4576
4577 // MULTI SELECTION
4578 // Ln.1 "«" tests empty lines
4579 // Ln.11 tests just leading whitespace
4580 cx.set_state(indoc! {"
4581 «
4582 abˇ»ˇc // No indentation
4583 abc ˇ ˇ // 1 space (< 3 so dont convert)
4584 abc « // 2 spaces (< 3 so dont convert)
4585 abc // 3 spaces (convert)
4586 abc ˇ» // 5 spaces (1 tab + 2 spaces)
4587 «\tˇ»\t«\tˇ»abc // Already tab indented
4588 «\t abc // Tab followed by space
4589 \tabc // Space followed by tab (should be consumed due to tab)
4590 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4591 \tˇ» «\t
4592 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
4593 "});
4594 cx.update_editor(|e, window, cx| {
4595 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4596 });
4597 cx.assert_editor_state(indoc! {"
4598 «
4599 abc // No indentation
4600 abc // 1 space (< 3 so dont convert)
4601 abc // 2 spaces (< 3 so dont convert)
4602 \tabc // 3 spaces (convert)
4603 \t abc // 5 spaces (1 tab + 2 spaces)
4604 \t\t\tabc // Already tab indented
4605 \t abc // Tab followed by space
4606 \tabc // Space followed by tab (should be consumed due to tab)
4607 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4608 \t\t\t
4609 \tabc \t // Only the leading spaces should be convertedˇ»
4610 "});
4611
4612 // Test on just a few lines, the other should remain unchanged
4613 // Only lines (4, 8, 11, 12) should change
4614 cx.set_state(
4615 indoc! {"
4616 ·
4617 abc // No indentation
4618 abc // 1 space (< 3 so dont convert)
4619 abc // 2 spaces (< 3 so dont convert)
4620 « abc // 3 spaces (convert)ˇ»
4621 abc // 5 spaces (1 tab + 2 spaces)
4622 \t\t\tabc // Already tab indented
4623 \t abc // Tab followed by space
4624 \tabc ˇ // Space followed by tab (should be consumed due to tab)
4625 \t\t \tabc // Mixed indentation
4626 \t \t \t \tabc // Mixed indentation
4627 \t \tˇ
4628 « abc \t // Only the leading spaces should be convertedˇ»
4629 "}
4630 .replace("·", "")
4631 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4632 );
4633 cx.update_editor(|e, window, cx| {
4634 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4635 });
4636 cx.assert_editor_state(
4637 indoc! {"
4638 ·
4639 abc // No indentation
4640 abc // 1 space (< 3 so dont convert)
4641 abc // 2 spaces (< 3 so dont convert)
4642 «\tabc // 3 spaces (convert)ˇ»
4643 abc // 5 spaces (1 tab + 2 spaces)
4644 \t\t\tabc // Already tab indented
4645 \t abc // Tab followed by space
4646 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
4647 \t\t \tabc // Mixed indentation
4648 \t \t \t \tabc // Mixed indentation
4649 «\t\t\t
4650 \tabc \t // Only the leading spaces should be convertedˇ»
4651 "}
4652 .replace("·", "")
4653 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4654 );
4655
4656 // SINGLE SELECTION
4657 // Ln.1 "«" tests empty lines
4658 // Ln.11 tests just leading whitespace
4659 cx.set_state(indoc! {"
4660 «
4661 abc // No indentation
4662 abc // 1 space (< 3 so dont convert)
4663 abc // 2 spaces (< 3 so dont convert)
4664 abc // 3 spaces (convert)
4665 abc // 5 spaces (1 tab + 2 spaces)
4666 \t\t\tabc // Already tab indented
4667 \t abc // Tab followed by space
4668 \tabc // Space followed by tab (should be consumed due to tab)
4669 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4670 \t \t
4671 abc \t // Only the leading spaces should be convertedˇ»
4672 "});
4673 cx.update_editor(|e, window, cx| {
4674 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4675 });
4676 cx.assert_editor_state(indoc! {"
4677 «
4678 abc // No indentation
4679 abc // 1 space (< 3 so dont convert)
4680 abc // 2 spaces (< 3 so dont convert)
4681 \tabc // 3 spaces (convert)
4682 \t abc // 5 spaces (1 tab + 2 spaces)
4683 \t\t\tabc // Already tab indented
4684 \t abc // Tab followed by space
4685 \tabc // Space followed by tab (should be consumed due to tab)
4686 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4687 \t\t\t
4688 \tabc \t // Only the leading spaces should be convertedˇ»
4689 "});
4690}
4691
4692#[gpui::test]
4693async fn test_toggle_case(cx: &mut TestAppContext) {
4694 init_test(cx, |_| {});
4695
4696 let mut cx = EditorTestContext::new(cx).await;
4697
4698 // If all lower case -> upper case
4699 cx.set_state(indoc! {"
4700 «hello worldˇ»
4701 "});
4702 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4703 cx.assert_editor_state(indoc! {"
4704 «HELLO WORLDˇ»
4705 "});
4706
4707 // If all upper case -> lower case
4708 cx.set_state(indoc! {"
4709 «HELLO WORLDˇ»
4710 "});
4711 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4712 cx.assert_editor_state(indoc! {"
4713 «hello worldˇ»
4714 "});
4715
4716 // If any upper case characters are identified -> lower case
4717 // This matches JetBrains IDEs
4718 cx.set_state(indoc! {"
4719 «hEllo worldˇ»
4720 "});
4721 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4722 cx.assert_editor_state(indoc! {"
4723 «hello worldˇ»
4724 "});
4725}
4726
4727#[gpui::test]
4728async fn test_manipulate_text(cx: &mut TestAppContext) {
4729 init_test(cx, |_| {});
4730
4731 let mut cx = EditorTestContext::new(cx).await;
4732
4733 // Test convert_to_upper_case()
4734 cx.set_state(indoc! {"
4735 «hello worldˇ»
4736 "});
4737 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4738 cx.assert_editor_state(indoc! {"
4739 «HELLO WORLDˇ»
4740 "});
4741
4742 // Test convert_to_lower_case()
4743 cx.set_state(indoc! {"
4744 «HELLO WORLDˇ»
4745 "});
4746 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4747 cx.assert_editor_state(indoc! {"
4748 «hello worldˇ»
4749 "});
4750
4751 // Test multiple line, single selection case
4752 cx.set_state(indoc! {"
4753 «The quick brown
4754 fox jumps over
4755 the lazy dogˇ»
4756 "});
4757 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4758 cx.assert_editor_state(indoc! {"
4759 «The Quick Brown
4760 Fox Jumps Over
4761 The Lazy Dogˇ»
4762 "});
4763
4764 // Test multiple line, single selection case
4765 cx.set_state(indoc! {"
4766 «The quick brown
4767 fox jumps over
4768 the lazy dogˇ»
4769 "});
4770 cx.update_editor(|e, window, cx| {
4771 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4772 });
4773 cx.assert_editor_state(indoc! {"
4774 «TheQuickBrown
4775 FoxJumpsOver
4776 TheLazyDogˇ»
4777 "});
4778
4779 // From here on out, test more complex cases of manipulate_text()
4780
4781 // Test no selection case - should affect words cursors are in
4782 // Cursor at beginning, middle, and end of word
4783 cx.set_state(indoc! {"
4784 ˇhello big beauˇtiful worldˇ
4785 "});
4786 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4787 cx.assert_editor_state(indoc! {"
4788 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4789 "});
4790
4791 // Test multiple selections on a single line and across multiple lines
4792 cx.set_state(indoc! {"
4793 «Theˇ» quick «brown
4794 foxˇ» jumps «overˇ»
4795 the «lazyˇ» dog
4796 "});
4797 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4798 cx.assert_editor_state(indoc! {"
4799 «THEˇ» quick «BROWN
4800 FOXˇ» jumps «OVERˇ»
4801 the «LAZYˇ» dog
4802 "});
4803
4804 // Test case where text length grows
4805 cx.set_state(indoc! {"
4806 «tschüߡ»
4807 "});
4808 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4809 cx.assert_editor_state(indoc! {"
4810 «TSCHÜSSˇ»
4811 "});
4812
4813 // Test to make sure we don't crash when text shrinks
4814 cx.set_state(indoc! {"
4815 aaa_bbbˇ
4816 "});
4817 cx.update_editor(|e, window, cx| {
4818 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4819 });
4820 cx.assert_editor_state(indoc! {"
4821 «aaaBbbˇ»
4822 "});
4823
4824 // Test to make sure we all aware of the fact that each word can grow and shrink
4825 // Final selections should be aware of this fact
4826 cx.set_state(indoc! {"
4827 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4828 "});
4829 cx.update_editor(|e, window, cx| {
4830 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4831 });
4832 cx.assert_editor_state(indoc! {"
4833 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4834 "});
4835
4836 cx.set_state(indoc! {"
4837 «hElLo, WoRld!ˇ»
4838 "});
4839 cx.update_editor(|e, window, cx| {
4840 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4841 });
4842 cx.assert_editor_state(indoc! {"
4843 «HeLlO, wOrLD!ˇ»
4844 "});
4845}
4846
4847#[gpui::test]
4848fn test_duplicate_line(cx: &mut TestAppContext) {
4849 init_test(cx, |_| {});
4850
4851 let editor = cx.add_window(|window, cx| {
4852 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4853 build_editor(buffer, window, cx)
4854 });
4855 _ = editor.update(cx, |editor, window, cx| {
4856 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4857 s.select_display_ranges([
4858 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4859 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4860 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4861 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4862 ])
4863 });
4864 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4865 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4866 assert_eq!(
4867 editor.selections.display_ranges(cx),
4868 vec![
4869 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4870 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4871 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4872 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4873 ]
4874 );
4875 });
4876
4877 let editor = cx.add_window(|window, cx| {
4878 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4879 build_editor(buffer, window, cx)
4880 });
4881 _ = editor.update(cx, |editor, window, cx| {
4882 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4883 s.select_display_ranges([
4884 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4885 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4886 ])
4887 });
4888 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4889 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4890 assert_eq!(
4891 editor.selections.display_ranges(cx),
4892 vec![
4893 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4894 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4895 ]
4896 );
4897 });
4898
4899 // With `move_upwards` the selections stay in place, except for
4900 // the lines inserted above them
4901 let editor = cx.add_window(|window, cx| {
4902 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4903 build_editor(buffer, window, cx)
4904 });
4905 _ = editor.update(cx, |editor, window, cx| {
4906 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4907 s.select_display_ranges([
4908 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4909 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4910 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4911 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4912 ])
4913 });
4914 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4915 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4916 assert_eq!(
4917 editor.selections.display_ranges(cx),
4918 vec![
4919 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4920 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4921 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4922 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4923 ]
4924 );
4925 });
4926
4927 let editor = cx.add_window(|window, cx| {
4928 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4929 build_editor(buffer, window, cx)
4930 });
4931 _ = editor.update(cx, |editor, window, cx| {
4932 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4933 s.select_display_ranges([
4934 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4935 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4936 ])
4937 });
4938 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4939 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4940 assert_eq!(
4941 editor.selections.display_ranges(cx),
4942 vec![
4943 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4944 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4945 ]
4946 );
4947 });
4948
4949 let editor = cx.add_window(|window, cx| {
4950 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4951 build_editor(buffer, window, cx)
4952 });
4953 _ = editor.update(cx, |editor, window, cx| {
4954 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4955 s.select_display_ranges([
4956 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4957 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4958 ])
4959 });
4960 editor.duplicate_selection(&DuplicateSelection, window, cx);
4961 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4962 assert_eq!(
4963 editor.selections.display_ranges(cx),
4964 vec![
4965 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4966 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4967 ]
4968 );
4969 });
4970}
4971
4972#[gpui::test]
4973fn test_move_line_up_down(cx: &mut TestAppContext) {
4974 init_test(cx, |_| {});
4975
4976 let editor = cx.add_window(|window, cx| {
4977 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4978 build_editor(buffer, window, cx)
4979 });
4980 _ = editor.update(cx, |editor, window, cx| {
4981 editor.fold_creases(
4982 vec![
4983 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4984 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4985 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4986 ],
4987 true,
4988 window,
4989 cx,
4990 );
4991 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4992 s.select_display_ranges([
4993 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4994 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4995 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4996 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4997 ])
4998 });
4999 assert_eq!(
5000 editor.display_text(cx),
5001 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5002 );
5003
5004 editor.move_line_up(&MoveLineUp, window, cx);
5005 assert_eq!(
5006 editor.display_text(cx),
5007 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5008 );
5009 assert_eq!(
5010 editor.selections.display_ranges(cx),
5011 vec![
5012 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5013 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5014 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5015 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5016 ]
5017 );
5018 });
5019
5020 _ = editor.update(cx, |editor, window, cx| {
5021 editor.move_line_down(&MoveLineDown, window, cx);
5022 assert_eq!(
5023 editor.display_text(cx),
5024 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5025 );
5026 assert_eq!(
5027 editor.selections.display_ranges(cx),
5028 vec![
5029 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5030 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5031 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5032 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5033 ]
5034 );
5035 });
5036
5037 _ = editor.update(cx, |editor, window, cx| {
5038 editor.move_line_down(&MoveLineDown, window, cx);
5039 assert_eq!(
5040 editor.display_text(cx),
5041 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5042 );
5043 assert_eq!(
5044 editor.selections.display_ranges(cx),
5045 vec![
5046 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5047 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5048 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5049 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5050 ]
5051 );
5052 });
5053
5054 _ = editor.update(cx, |editor, window, cx| {
5055 editor.move_line_up(&MoveLineUp, window, cx);
5056 assert_eq!(
5057 editor.display_text(cx),
5058 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5059 );
5060 assert_eq!(
5061 editor.selections.display_ranges(cx),
5062 vec![
5063 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5064 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5065 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5066 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5067 ]
5068 );
5069 });
5070}
5071
5072#[gpui::test]
5073fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5074 init_test(cx, |_| {});
5075
5076 let editor = cx.add_window(|window, cx| {
5077 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5078 build_editor(buffer, window, cx)
5079 });
5080 _ = editor.update(cx, |editor, window, cx| {
5081 let snapshot = editor.buffer.read(cx).snapshot(cx);
5082 editor.insert_blocks(
5083 [BlockProperties {
5084 style: BlockStyle::Fixed,
5085 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5086 height: Some(1),
5087 render: Arc::new(|_| div().into_any()),
5088 priority: 0,
5089 }],
5090 Some(Autoscroll::fit()),
5091 cx,
5092 );
5093 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5094 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5095 });
5096 editor.move_line_down(&MoveLineDown, window, cx);
5097 });
5098}
5099
5100#[gpui::test]
5101async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5102 init_test(cx, |_| {});
5103
5104 let mut cx = EditorTestContext::new(cx).await;
5105 cx.set_state(
5106 &"
5107 ˇzero
5108 one
5109 two
5110 three
5111 four
5112 five
5113 "
5114 .unindent(),
5115 );
5116
5117 // Create a four-line block that replaces three lines of text.
5118 cx.update_editor(|editor, window, cx| {
5119 let snapshot = editor.snapshot(window, cx);
5120 let snapshot = &snapshot.buffer_snapshot;
5121 let placement = BlockPlacement::Replace(
5122 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5123 );
5124 editor.insert_blocks(
5125 [BlockProperties {
5126 placement,
5127 height: Some(4),
5128 style: BlockStyle::Sticky,
5129 render: Arc::new(|_| gpui::div().into_any_element()),
5130 priority: 0,
5131 }],
5132 None,
5133 cx,
5134 );
5135 });
5136
5137 // Move down so that the cursor touches the block.
5138 cx.update_editor(|editor, window, cx| {
5139 editor.move_down(&Default::default(), window, cx);
5140 });
5141 cx.assert_editor_state(
5142 &"
5143 zero
5144 «one
5145 two
5146 threeˇ»
5147 four
5148 five
5149 "
5150 .unindent(),
5151 );
5152
5153 // Move down past the block.
5154 cx.update_editor(|editor, window, cx| {
5155 editor.move_down(&Default::default(), window, cx);
5156 });
5157 cx.assert_editor_state(
5158 &"
5159 zero
5160 one
5161 two
5162 three
5163 ˇfour
5164 five
5165 "
5166 .unindent(),
5167 );
5168}
5169
5170#[gpui::test]
5171fn test_transpose(cx: &mut TestAppContext) {
5172 init_test(cx, |_| {});
5173
5174 _ = cx.add_window(|window, cx| {
5175 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5176 editor.set_style(EditorStyle::default(), window, cx);
5177 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5178 s.select_ranges([1..1])
5179 });
5180 editor.transpose(&Default::default(), window, cx);
5181 assert_eq!(editor.text(cx), "bac");
5182 assert_eq!(editor.selections.ranges(cx), [2..2]);
5183
5184 editor.transpose(&Default::default(), window, cx);
5185 assert_eq!(editor.text(cx), "bca");
5186 assert_eq!(editor.selections.ranges(cx), [3..3]);
5187
5188 editor.transpose(&Default::default(), window, cx);
5189 assert_eq!(editor.text(cx), "bac");
5190 assert_eq!(editor.selections.ranges(cx), [3..3]);
5191
5192 editor
5193 });
5194
5195 _ = cx.add_window(|window, cx| {
5196 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5197 editor.set_style(EditorStyle::default(), window, cx);
5198 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5199 s.select_ranges([3..3])
5200 });
5201 editor.transpose(&Default::default(), window, cx);
5202 assert_eq!(editor.text(cx), "acb\nde");
5203 assert_eq!(editor.selections.ranges(cx), [3..3]);
5204
5205 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5206 s.select_ranges([4..4])
5207 });
5208 editor.transpose(&Default::default(), window, cx);
5209 assert_eq!(editor.text(cx), "acbd\ne");
5210 assert_eq!(editor.selections.ranges(cx), [5..5]);
5211
5212 editor.transpose(&Default::default(), window, cx);
5213 assert_eq!(editor.text(cx), "acbde\n");
5214 assert_eq!(editor.selections.ranges(cx), [6..6]);
5215
5216 editor.transpose(&Default::default(), window, cx);
5217 assert_eq!(editor.text(cx), "acbd\ne");
5218 assert_eq!(editor.selections.ranges(cx), [6..6]);
5219
5220 editor
5221 });
5222
5223 _ = cx.add_window(|window, cx| {
5224 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5225 editor.set_style(EditorStyle::default(), window, cx);
5226 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5227 s.select_ranges([1..1, 2..2, 4..4])
5228 });
5229 editor.transpose(&Default::default(), window, cx);
5230 assert_eq!(editor.text(cx), "bacd\ne");
5231 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5232
5233 editor.transpose(&Default::default(), window, cx);
5234 assert_eq!(editor.text(cx), "bcade\n");
5235 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5236
5237 editor.transpose(&Default::default(), window, cx);
5238 assert_eq!(editor.text(cx), "bcda\ne");
5239 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5240
5241 editor.transpose(&Default::default(), window, cx);
5242 assert_eq!(editor.text(cx), "bcade\n");
5243 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5244
5245 editor.transpose(&Default::default(), window, cx);
5246 assert_eq!(editor.text(cx), "bcaed\n");
5247 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5248
5249 editor
5250 });
5251
5252 _ = cx.add_window(|window, cx| {
5253 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5254 editor.set_style(EditorStyle::default(), window, cx);
5255 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5256 s.select_ranges([4..4])
5257 });
5258 editor.transpose(&Default::default(), window, cx);
5259 assert_eq!(editor.text(cx), "🏀🍐✋");
5260 assert_eq!(editor.selections.ranges(cx), [8..8]);
5261
5262 editor.transpose(&Default::default(), window, cx);
5263 assert_eq!(editor.text(cx), "🏀✋🍐");
5264 assert_eq!(editor.selections.ranges(cx), [11..11]);
5265
5266 editor.transpose(&Default::default(), window, cx);
5267 assert_eq!(editor.text(cx), "🏀🍐✋");
5268 assert_eq!(editor.selections.ranges(cx), [11..11]);
5269
5270 editor
5271 });
5272}
5273
5274#[gpui::test]
5275async fn test_rewrap(cx: &mut TestAppContext) {
5276 init_test(cx, |settings| {
5277 settings.languages.0.extend([
5278 (
5279 "Markdown".into(),
5280 LanguageSettingsContent {
5281 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5282 preferred_line_length: Some(40),
5283 ..Default::default()
5284 },
5285 ),
5286 (
5287 "Plain Text".into(),
5288 LanguageSettingsContent {
5289 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5290 preferred_line_length: Some(40),
5291 ..Default::default()
5292 },
5293 ),
5294 (
5295 "C++".into(),
5296 LanguageSettingsContent {
5297 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5298 preferred_line_length: Some(40),
5299 ..Default::default()
5300 },
5301 ),
5302 (
5303 "Python".into(),
5304 LanguageSettingsContent {
5305 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5306 preferred_line_length: Some(40),
5307 ..Default::default()
5308 },
5309 ),
5310 (
5311 "Rust".into(),
5312 LanguageSettingsContent {
5313 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5314 preferred_line_length: Some(40),
5315 ..Default::default()
5316 },
5317 ),
5318 ])
5319 });
5320
5321 let mut cx = EditorTestContext::new(cx).await;
5322
5323 let cpp_language = Arc::new(Language::new(
5324 LanguageConfig {
5325 name: "C++".into(),
5326 line_comments: vec!["// ".into()],
5327 ..LanguageConfig::default()
5328 },
5329 None,
5330 ));
5331 let python_language = Arc::new(Language::new(
5332 LanguageConfig {
5333 name: "Python".into(),
5334 line_comments: vec!["# ".into()],
5335 ..LanguageConfig::default()
5336 },
5337 None,
5338 ));
5339 let markdown_language = Arc::new(Language::new(
5340 LanguageConfig {
5341 name: "Markdown".into(),
5342 rewrap_prefixes: vec![
5343 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5344 regex::Regex::new("[-*+]\\s+").unwrap(),
5345 ],
5346 ..LanguageConfig::default()
5347 },
5348 None,
5349 ));
5350 let rust_language = Arc::new(Language::new(
5351 LanguageConfig {
5352 name: "Rust".into(),
5353 line_comments: vec!["// ".into(), "/// ".into()],
5354 ..LanguageConfig::default()
5355 },
5356 Some(tree_sitter_rust::LANGUAGE.into()),
5357 ));
5358
5359 let plaintext_language = Arc::new(Language::new(
5360 LanguageConfig {
5361 name: "Plain Text".into(),
5362 ..LanguageConfig::default()
5363 },
5364 None,
5365 ));
5366
5367 // Test basic rewrapping of a long line with a cursor
5368 assert_rewrap(
5369 indoc! {"
5370 // ˇThis is a long comment that needs to be wrapped.
5371 "},
5372 indoc! {"
5373 // ˇThis is a long comment that needs to
5374 // be wrapped.
5375 "},
5376 cpp_language.clone(),
5377 &mut cx,
5378 );
5379
5380 // Test rewrapping a full selection
5381 assert_rewrap(
5382 indoc! {"
5383 «// This selected long comment needs to be wrapped.ˇ»"
5384 },
5385 indoc! {"
5386 «// This selected long comment needs to
5387 // be wrapped.ˇ»"
5388 },
5389 cpp_language.clone(),
5390 &mut cx,
5391 );
5392
5393 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5394 assert_rewrap(
5395 indoc! {"
5396 // ˇThis is the first line.
5397 // Thisˇ is the second line.
5398 // This is the thirdˇ line, all part of one paragraph.
5399 "},
5400 indoc! {"
5401 // ˇThis is the first line. Thisˇ is the
5402 // second line. This is the thirdˇ line,
5403 // all part of one paragraph.
5404 "},
5405 cpp_language.clone(),
5406 &mut cx,
5407 );
5408
5409 // Test multiple cursors in different paragraphs trigger separate rewraps
5410 assert_rewrap(
5411 indoc! {"
5412 // ˇThis is the first paragraph, first line.
5413 // ˇThis is the first paragraph, second line.
5414
5415 // ˇThis is the second paragraph, first line.
5416 // ˇThis is the second paragraph, second line.
5417 "},
5418 indoc! {"
5419 // ˇThis is the first paragraph, first
5420 // line. ˇThis is the first paragraph,
5421 // second line.
5422
5423 // ˇThis is the second paragraph, first
5424 // line. ˇThis is the second paragraph,
5425 // second line.
5426 "},
5427 cpp_language.clone(),
5428 &mut cx,
5429 );
5430
5431 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
5432 assert_rewrap(
5433 indoc! {"
5434 «// A regular long long comment to be wrapped.
5435 /// A documentation long comment to be wrapped.ˇ»
5436 "},
5437 indoc! {"
5438 «// A regular long long comment to be
5439 // wrapped.
5440 /// A documentation long comment to be
5441 /// wrapped.ˇ»
5442 "},
5443 rust_language.clone(),
5444 &mut cx,
5445 );
5446
5447 // Test that change in indentation level trigger seperate rewraps
5448 assert_rewrap(
5449 indoc! {"
5450 fn foo() {
5451 «// This is a long comment at the base indent.
5452 // This is a long comment at the next indent.ˇ»
5453 }
5454 "},
5455 indoc! {"
5456 fn foo() {
5457 «// This is a long comment at the
5458 // base indent.
5459 // This is a long comment at the
5460 // next indent.ˇ»
5461 }
5462 "},
5463 rust_language.clone(),
5464 &mut cx,
5465 );
5466
5467 // Test that different comment prefix characters (e.g., '#') are handled correctly
5468 assert_rewrap(
5469 indoc! {"
5470 # ˇThis is a long comment using a pound sign.
5471 "},
5472 indoc! {"
5473 # ˇThis is a long comment using a pound
5474 # sign.
5475 "},
5476 python_language.clone(),
5477 &mut cx,
5478 );
5479
5480 // Test rewrapping only affects comments, not code even when selected
5481 assert_rewrap(
5482 indoc! {"
5483 «/// This doc comment is long and should be wrapped.
5484 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5485 "},
5486 indoc! {"
5487 «/// This doc comment is long and should
5488 /// be wrapped.
5489 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5490 "},
5491 rust_language.clone(),
5492 &mut cx,
5493 );
5494
5495 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
5496 assert_rewrap(
5497 indoc! {"
5498 # Header
5499
5500 A long long long line of markdown text to wrap.ˇ
5501 "},
5502 indoc! {"
5503 # Header
5504
5505 A long long long line of markdown text
5506 to wrap.ˇ
5507 "},
5508 markdown_language.clone(),
5509 &mut cx,
5510 );
5511
5512 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
5513 assert_rewrap(
5514 indoc! {"
5515 «1. This is a numbered list item that is very long and needs to be wrapped properly.
5516 2. This is a numbered list item that is very long and needs to be wrapped properly.
5517 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
5518 "},
5519 indoc! {"
5520 «1. This is a numbered list item that is
5521 very long and needs to be wrapped
5522 properly.
5523 2. This is a numbered list item that is
5524 very long and needs to be wrapped
5525 properly.
5526 - This is an unordered list item that is
5527 also very long and should not merge
5528 with the numbered item.ˇ»
5529 "},
5530 markdown_language.clone(),
5531 &mut cx,
5532 );
5533
5534 // Test that rewrapping add indents for rewrapping boundary if not exists already.
5535 assert_rewrap(
5536 indoc! {"
5537 «1. This is a numbered list item that is
5538 very long and needs to be wrapped
5539 properly.
5540 2. This is a numbered list item that is
5541 very long and needs to be wrapped
5542 properly.
5543 - This is an unordered list item that is
5544 also very long and should not merge with
5545 the numbered item.ˇ»
5546 "},
5547 indoc! {"
5548 «1. This is a numbered list item that is
5549 very long and needs to be wrapped
5550 properly.
5551 2. This is a numbered list item that is
5552 very long and needs to be wrapped
5553 properly.
5554 - This is an unordered list item that is
5555 also very long and should not merge
5556 with the numbered item.ˇ»
5557 "},
5558 markdown_language.clone(),
5559 &mut cx,
5560 );
5561
5562 // Test that rewrapping maintain indents even when they already exists.
5563 assert_rewrap(
5564 indoc! {"
5565 «1. This is a numbered list
5566 item that is very long and needs to be wrapped properly.
5567 2. This is a numbered list
5568 item that is very long and needs to be wrapped properly.
5569 - This is an unordered list item that is also very long and
5570 should not merge with the numbered item.ˇ»
5571 "},
5572 indoc! {"
5573 «1. This is a numbered list item that is
5574 very long and needs to be wrapped
5575 properly.
5576 2. This is a numbered list item that is
5577 very long and needs to be wrapped
5578 properly.
5579 - This is an unordered list item that is
5580 also very long and should not merge
5581 with the numbered item.ˇ»
5582 "},
5583 markdown_language.clone(),
5584 &mut cx,
5585 );
5586
5587 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
5588 assert_rewrap(
5589 indoc! {"
5590 ˇThis is a very long line of plain text that will be wrapped.
5591 "},
5592 indoc! {"
5593 ˇThis is a very long line of plain text
5594 that will be wrapped.
5595 "},
5596 plaintext_language.clone(),
5597 &mut cx,
5598 );
5599
5600 // Test that non-commented code acts as a paragraph boundary within a selection
5601 assert_rewrap(
5602 indoc! {"
5603 «// This is the first long comment block to be wrapped.
5604 fn my_func(a: u32);
5605 // This is the second long comment block to be wrapped.ˇ»
5606 "},
5607 indoc! {"
5608 «// This is the first long comment block
5609 // to be wrapped.
5610 fn my_func(a: u32);
5611 // This is the second long comment block
5612 // to be wrapped.ˇ»
5613 "},
5614 rust_language.clone(),
5615 &mut cx,
5616 );
5617
5618 // Test rewrapping multiple selections, including ones with blank lines or tabs
5619 assert_rewrap(
5620 indoc! {"
5621 «ˇThis is a very long line that will be wrapped.
5622
5623 This is another paragraph in the same selection.»
5624
5625 «\tThis is a very long indented line that will be wrapped.ˇ»
5626 "},
5627 indoc! {"
5628 «ˇThis is a very long line that will be
5629 wrapped.
5630
5631 This is another paragraph in the same
5632 selection.»
5633
5634 «\tThis is a very long indented line
5635 \tthat will be wrapped.ˇ»
5636 "},
5637 plaintext_language.clone(),
5638 &mut cx,
5639 );
5640
5641 // Test that an empty comment line acts as a paragraph boundary
5642 assert_rewrap(
5643 indoc! {"
5644 // ˇThis is a long comment that will be wrapped.
5645 //
5646 // And this is another long comment that will also be wrapped.ˇ
5647 "},
5648 indoc! {"
5649 // ˇThis is a long comment that will be
5650 // wrapped.
5651 //
5652 // And this is another long comment that
5653 // will also be wrapped.ˇ
5654 "},
5655 cpp_language,
5656 &mut cx,
5657 );
5658
5659 #[track_caller]
5660 fn assert_rewrap(
5661 unwrapped_text: &str,
5662 wrapped_text: &str,
5663 language: Arc<Language>,
5664 cx: &mut EditorTestContext,
5665 ) {
5666 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5667 cx.set_state(unwrapped_text);
5668 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5669 cx.assert_editor_state(wrapped_text);
5670 }
5671}
5672
5673#[gpui::test]
5674async fn test_hard_wrap(cx: &mut TestAppContext) {
5675 init_test(cx, |_| {});
5676 let mut cx = EditorTestContext::new(cx).await;
5677
5678 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5679 cx.update_editor(|editor, _, cx| {
5680 editor.set_hard_wrap(Some(14), cx);
5681 });
5682
5683 cx.set_state(indoc!(
5684 "
5685 one two three ˇ
5686 "
5687 ));
5688 cx.simulate_input("four");
5689 cx.run_until_parked();
5690
5691 cx.assert_editor_state(indoc!(
5692 "
5693 one two three
5694 fourˇ
5695 "
5696 ));
5697
5698 cx.update_editor(|editor, window, cx| {
5699 editor.newline(&Default::default(), window, cx);
5700 });
5701 cx.run_until_parked();
5702 cx.assert_editor_state(indoc!(
5703 "
5704 one two three
5705 four
5706 ˇ
5707 "
5708 ));
5709
5710 cx.simulate_input("five");
5711 cx.run_until_parked();
5712 cx.assert_editor_state(indoc!(
5713 "
5714 one two three
5715 four
5716 fiveˇ
5717 "
5718 ));
5719
5720 cx.update_editor(|editor, window, cx| {
5721 editor.newline(&Default::default(), window, cx);
5722 });
5723 cx.run_until_parked();
5724 cx.simulate_input("# ");
5725 cx.run_until_parked();
5726 cx.assert_editor_state(indoc!(
5727 "
5728 one two three
5729 four
5730 five
5731 # ˇ
5732 "
5733 ));
5734
5735 cx.update_editor(|editor, window, cx| {
5736 editor.newline(&Default::default(), window, cx);
5737 });
5738 cx.run_until_parked();
5739 cx.assert_editor_state(indoc!(
5740 "
5741 one two three
5742 four
5743 five
5744 #\x20
5745 #ˇ
5746 "
5747 ));
5748
5749 cx.simulate_input(" 6");
5750 cx.run_until_parked();
5751 cx.assert_editor_state(indoc!(
5752 "
5753 one two three
5754 four
5755 five
5756 #
5757 # 6ˇ
5758 "
5759 ));
5760}
5761
5762#[gpui::test]
5763async fn test_clipboard(cx: &mut TestAppContext) {
5764 init_test(cx, |_| {});
5765
5766 let mut cx = EditorTestContext::new(cx).await;
5767
5768 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5769 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5770 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5771
5772 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5773 cx.set_state("two ˇfour ˇsix ˇ");
5774 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5775 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5776
5777 // Paste again but with only two cursors. Since the number of cursors doesn't
5778 // match the number of slices in the clipboard, the entire clipboard text
5779 // is pasted at each cursor.
5780 cx.set_state("ˇtwo one✅ four three six five ˇ");
5781 cx.update_editor(|e, window, cx| {
5782 e.handle_input("( ", window, cx);
5783 e.paste(&Paste, window, cx);
5784 e.handle_input(") ", window, cx);
5785 });
5786 cx.assert_editor_state(
5787 &([
5788 "( one✅ ",
5789 "three ",
5790 "five ) ˇtwo one✅ four three six five ( one✅ ",
5791 "three ",
5792 "five ) ˇ",
5793 ]
5794 .join("\n")),
5795 );
5796
5797 // Cut with three selections, one of which is full-line.
5798 cx.set_state(indoc! {"
5799 1«2ˇ»3
5800 4ˇ567
5801 «8ˇ»9"});
5802 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5803 cx.assert_editor_state(indoc! {"
5804 1ˇ3
5805 ˇ9"});
5806
5807 // Paste with three selections, noticing how the copied selection that was full-line
5808 // gets inserted before the second cursor.
5809 cx.set_state(indoc! {"
5810 1ˇ3
5811 9ˇ
5812 «oˇ»ne"});
5813 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5814 cx.assert_editor_state(indoc! {"
5815 12ˇ3
5816 4567
5817 9ˇ
5818 8ˇne"});
5819
5820 // Copy with a single cursor only, which writes the whole line into the clipboard.
5821 cx.set_state(indoc! {"
5822 The quick brown
5823 fox juˇmps over
5824 the lazy dog"});
5825 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5826 assert_eq!(
5827 cx.read_from_clipboard()
5828 .and_then(|item| item.text().as_deref().map(str::to_string)),
5829 Some("fox jumps over\n".to_string())
5830 );
5831
5832 // Paste with three selections, noticing how the copied full-line selection is inserted
5833 // before the empty selections but replaces the selection that is non-empty.
5834 cx.set_state(indoc! {"
5835 Tˇhe quick brown
5836 «foˇ»x jumps over
5837 tˇhe lazy dog"});
5838 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5839 cx.assert_editor_state(indoc! {"
5840 fox jumps over
5841 Tˇhe quick brown
5842 fox jumps over
5843 ˇx jumps over
5844 fox jumps over
5845 tˇhe lazy dog"});
5846}
5847
5848#[gpui::test]
5849async fn test_copy_trim(cx: &mut TestAppContext) {
5850 init_test(cx, |_| {});
5851
5852 let mut cx = EditorTestContext::new(cx).await;
5853 cx.set_state(
5854 r#" «for selection in selections.iter() {
5855 let mut start = selection.start;
5856 let mut end = selection.end;
5857 let is_entire_line = selection.is_empty();
5858 if is_entire_line {
5859 start = Point::new(start.row, 0);ˇ»
5860 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5861 }
5862 "#,
5863 );
5864 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5865 assert_eq!(
5866 cx.read_from_clipboard()
5867 .and_then(|item| item.text().as_deref().map(str::to_string)),
5868 Some(
5869 "for selection in selections.iter() {
5870 let mut start = selection.start;
5871 let mut end = selection.end;
5872 let is_entire_line = selection.is_empty();
5873 if is_entire_line {
5874 start = Point::new(start.row, 0);"
5875 .to_string()
5876 ),
5877 "Regular copying preserves all indentation selected",
5878 );
5879 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5880 assert_eq!(
5881 cx.read_from_clipboard()
5882 .and_then(|item| item.text().as_deref().map(str::to_string)),
5883 Some(
5884 "for selection in selections.iter() {
5885let mut start = selection.start;
5886let mut end = selection.end;
5887let is_entire_line = selection.is_empty();
5888if is_entire_line {
5889 start = Point::new(start.row, 0);"
5890 .to_string()
5891 ),
5892 "Copying with stripping should strip all leading whitespaces"
5893 );
5894
5895 cx.set_state(
5896 r#" « for selection in selections.iter() {
5897 let mut start = selection.start;
5898 let mut end = selection.end;
5899 let is_entire_line = selection.is_empty();
5900 if is_entire_line {
5901 start = Point::new(start.row, 0);ˇ»
5902 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5903 }
5904 "#,
5905 );
5906 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5907 assert_eq!(
5908 cx.read_from_clipboard()
5909 .and_then(|item| item.text().as_deref().map(str::to_string)),
5910 Some(
5911 " for selection in selections.iter() {
5912 let mut start = selection.start;
5913 let mut end = selection.end;
5914 let is_entire_line = selection.is_empty();
5915 if is_entire_line {
5916 start = Point::new(start.row, 0);"
5917 .to_string()
5918 ),
5919 "Regular copying preserves all indentation selected",
5920 );
5921 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5922 assert_eq!(
5923 cx.read_from_clipboard()
5924 .and_then(|item| item.text().as_deref().map(str::to_string)),
5925 Some(
5926 "for selection in selections.iter() {
5927let mut start = selection.start;
5928let mut end = selection.end;
5929let is_entire_line = selection.is_empty();
5930if is_entire_line {
5931 start = Point::new(start.row, 0);"
5932 .to_string()
5933 ),
5934 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5935 );
5936
5937 cx.set_state(
5938 r#" «ˇ for selection in selections.iter() {
5939 let mut start = selection.start;
5940 let mut end = selection.end;
5941 let is_entire_line = selection.is_empty();
5942 if is_entire_line {
5943 start = Point::new(start.row, 0);»
5944 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5945 }
5946 "#,
5947 );
5948 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5949 assert_eq!(
5950 cx.read_from_clipboard()
5951 .and_then(|item| item.text().as_deref().map(str::to_string)),
5952 Some(
5953 " for selection in selections.iter() {
5954 let mut start = selection.start;
5955 let mut end = selection.end;
5956 let is_entire_line = selection.is_empty();
5957 if is_entire_line {
5958 start = Point::new(start.row, 0);"
5959 .to_string()
5960 ),
5961 "Regular copying for reverse selection works the same",
5962 );
5963 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5964 assert_eq!(
5965 cx.read_from_clipboard()
5966 .and_then(|item| item.text().as_deref().map(str::to_string)),
5967 Some(
5968 "for selection in selections.iter() {
5969let mut start = selection.start;
5970let mut end = selection.end;
5971let is_entire_line = selection.is_empty();
5972if is_entire_line {
5973 start = Point::new(start.row, 0);"
5974 .to_string()
5975 ),
5976 "Copying with stripping for reverse selection works the same"
5977 );
5978
5979 cx.set_state(
5980 r#" for selection «in selections.iter() {
5981 let mut start = selection.start;
5982 let mut end = selection.end;
5983 let is_entire_line = selection.is_empty();
5984 if is_entire_line {
5985 start = Point::new(start.row, 0);ˇ»
5986 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5987 }
5988 "#,
5989 );
5990 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5991 assert_eq!(
5992 cx.read_from_clipboard()
5993 .and_then(|item| item.text().as_deref().map(str::to_string)),
5994 Some(
5995 "in selections.iter() {
5996 let mut start = selection.start;
5997 let mut end = selection.end;
5998 let is_entire_line = selection.is_empty();
5999 if is_entire_line {
6000 start = Point::new(start.row, 0);"
6001 .to_string()
6002 ),
6003 "When selecting past the indent, the copying works as usual",
6004 );
6005 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6006 assert_eq!(
6007 cx.read_from_clipboard()
6008 .and_then(|item| item.text().as_deref().map(str::to_string)),
6009 Some(
6010 "in selections.iter() {
6011 let mut start = selection.start;
6012 let mut end = selection.end;
6013 let is_entire_line = selection.is_empty();
6014 if is_entire_line {
6015 start = Point::new(start.row, 0);"
6016 .to_string()
6017 ),
6018 "When selecting past the indent, nothing is trimmed"
6019 );
6020
6021 cx.set_state(
6022 r#" «for selection in selections.iter() {
6023 let mut start = selection.start;
6024
6025 let mut end = selection.end;
6026 let is_entire_line = selection.is_empty();
6027 if is_entire_line {
6028 start = Point::new(start.row, 0);
6029ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
6030 }
6031 "#,
6032 );
6033 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6034 assert_eq!(
6035 cx.read_from_clipboard()
6036 .and_then(|item| item.text().as_deref().map(str::to_string)),
6037 Some(
6038 "for selection in selections.iter() {
6039let mut start = selection.start;
6040
6041let mut end = selection.end;
6042let is_entire_line = selection.is_empty();
6043if is_entire_line {
6044 start = Point::new(start.row, 0);
6045"
6046 .to_string()
6047 ),
6048 "Copying with stripping should ignore empty lines"
6049 );
6050}
6051
6052#[gpui::test]
6053async fn test_paste_multiline(cx: &mut TestAppContext) {
6054 init_test(cx, |_| {});
6055
6056 let mut cx = EditorTestContext::new(cx).await;
6057 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6058
6059 // Cut an indented block, without the leading whitespace.
6060 cx.set_state(indoc! {"
6061 const a: B = (
6062 c(),
6063 «d(
6064 e,
6065 f
6066 )ˇ»
6067 );
6068 "});
6069 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6070 cx.assert_editor_state(indoc! {"
6071 const a: B = (
6072 c(),
6073 ˇ
6074 );
6075 "});
6076
6077 // Paste it at the same position.
6078 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6079 cx.assert_editor_state(indoc! {"
6080 const a: B = (
6081 c(),
6082 d(
6083 e,
6084 f
6085 )ˇ
6086 );
6087 "});
6088
6089 // Paste it at a line with a lower indent level.
6090 cx.set_state(indoc! {"
6091 ˇ
6092 const a: B = (
6093 c(),
6094 );
6095 "});
6096 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6097 cx.assert_editor_state(indoc! {"
6098 d(
6099 e,
6100 f
6101 )ˇ
6102 const a: B = (
6103 c(),
6104 );
6105 "});
6106
6107 // Cut an indented block, with the leading whitespace.
6108 cx.set_state(indoc! {"
6109 const a: B = (
6110 c(),
6111 « d(
6112 e,
6113 f
6114 )
6115 ˇ»);
6116 "});
6117 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6118 cx.assert_editor_state(indoc! {"
6119 const a: B = (
6120 c(),
6121 ˇ);
6122 "});
6123
6124 // Paste it at the same position.
6125 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6126 cx.assert_editor_state(indoc! {"
6127 const a: B = (
6128 c(),
6129 d(
6130 e,
6131 f
6132 )
6133 ˇ);
6134 "});
6135
6136 // Paste it at a line with a higher indent level.
6137 cx.set_state(indoc! {"
6138 const a: B = (
6139 c(),
6140 d(
6141 e,
6142 fˇ
6143 )
6144 );
6145 "});
6146 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6147 cx.assert_editor_state(indoc! {"
6148 const a: B = (
6149 c(),
6150 d(
6151 e,
6152 f d(
6153 e,
6154 f
6155 )
6156 ˇ
6157 )
6158 );
6159 "});
6160
6161 // Copy an indented block, starting mid-line
6162 cx.set_state(indoc! {"
6163 const a: B = (
6164 c(),
6165 somethin«g(
6166 e,
6167 f
6168 )ˇ»
6169 );
6170 "});
6171 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6172
6173 // Paste it on a line with a lower indent level
6174 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
6175 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6176 cx.assert_editor_state(indoc! {"
6177 const a: B = (
6178 c(),
6179 something(
6180 e,
6181 f
6182 )
6183 );
6184 g(
6185 e,
6186 f
6187 )ˇ"});
6188}
6189
6190#[gpui::test]
6191async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
6192 init_test(cx, |_| {});
6193
6194 cx.write_to_clipboard(ClipboardItem::new_string(
6195 " d(\n e\n );\n".into(),
6196 ));
6197
6198 let mut cx = EditorTestContext::new(cx).await;
6199 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6200
6201 cx.set_state(indoc! {"
6202 fn a() {
6203 b();
6204 if c() {
6205 ˇ
6206 }
6207 }
6208 "});
6209
6210 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6211 cx.assert_editor_state(indoc! {"
6212 fn a() {
6213 b();
6214 if c() {
6215 d(
6216 e
6217 );
6218 ˇ
6219 }
6220 }
6221 "});
6222
6223 cx.set_state(indoc! {"
6224 fn a() {
6225 b();
6226 ˇ
6227 }
6228 "});
6229
6230 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6231 cx.assert_editor_state(indoc! {"
6232 fn a() {
6233 b();
6234 d(
6235 e
6236 );
6237 ˇ
6238 }
6239 "});
6240}
6241
6242#[gpui::test]
6243fn test_select_all(cx: &mut TestAppContext) {
6244 init_test(cx, |_| {});
6245
6246 let editor = cx.add_window(|window, cx| {
6247 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
6248 build_editor(buffer, window, cx)
6249 });
6250 _ = editor.update(cx, |editor, window, cx| {
6251 editor.select_all(&SelectAll, window, cx);
6252 assert_eq!(
6253 editor.selections.display_ranges(cx),
6254 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
6255 );
6256 });
6257}
6258
6259#[gpui::test]
6260fn test_select_line(cx: &mut TestAppContext) {
6261 init_test(cx, |_| {});
6262
6263 let editor = cx.add_window(|window, cx| {
6264 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
6265 build_editor(buffer, window, cx)
6266 });
6267 _ = editor.update(cx, |editor, window, cx| {
6268 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6269 s.select_display_ranges([
6270 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6271 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6272 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6273 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
6274 ])
6275 });
6276 editor.select_line(&SelectLine, window, cx);
6277 assert_eq!(
6278 editor.selections.display_ranges(cx),
6279 vec![
6280 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
6281 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
6282 ]
6283 );
6284 });
6285
6286 _ = editor.update(cx, |editor, window, cx| {
6287 editor.select_line(&SelectLine, window, cx);
6288 assert_eq!(
6289 editor.selections.display_ranges(cx),
6290 vec![
6291 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
6292 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
6293 ]
6294 );
6295 });
6296
6297 _ = editor.update(cx, |editor, window, cx| {
6298 editor.select_line(&SelectLine, window, cx);
6299 assert_eq!(
6300 editor.selections.display_ranges(cx),
6301 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
6302 );
6303 });
6304}
6305
6306#[gpui::test]
6307async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
6308 init_test(cx, |_| {});
6309 let mut cx = EditorTestContext::new(cx).await;
6310
6311 #[track_caller]
6312 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
6313 cx.set_state(initial_state);
6314 cx.update_editor(|e, window, cx| {
6315 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
6316 });
6317 cx.assert_editor_state(expected_state);
6318 }
6319
6320 // Selection starts and ends at the middle of lines, left-to-right
6321 test(
6322 &mut cx,
6323 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
6324 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6325 );
6326 // Same thing, right-to-left
6327 test(
6328 &mut cx,
6329 "aa\nb«b\ncc\ndd\neˇ»e\nff",
6330 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6331 );
6332
6333 // Whole buffer, left-to-right, last line *doesn't* end with newline
6334 test(
6335 &mut cx,
6336 "«ˇaa\nbb\ncc\ndd\nee\nff»",
6337 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6338 );
6339 // Same thing, right-to-left
6340 test(
6341 &mut cx,
6342 "«aa\nbb\ncc\ndd\nee\nffˇ»",
6343 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6344 );
6345
6346 // Whole buffer, left-to-right, last line ends with newline
6347 test(
6348 &mut cx,
6349 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
6350 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6351 );
6352 // Same thing, right-to-left
6353 test(
6354 &mut cx,
6355 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
6356 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6357 );
6358
6359 // Starts at the end of a line, ends at the start of another
6360 test(
6361 &mut cx,
6362 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
6363 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
6364 );
6365}
6366
6367#[gpui::test]
6368async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
6369 init_test(cx, |_| {});
6370
6371 let editor = cx.add_window(|window, cx| {
6372 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
6373 build_editor(buffer, window, cx)
6374 });
6375
6376 // setup
6377 _ = editor.update(cx, |editor, window, cx| {
6378 editor.fold_creases(
6379 vec![
6380 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
6381 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
6382 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
6383 ],
6384 true,
6385 window,
6386 cx,
6387 );
6388 assert_eq!(
6389 editor.display_text(cx),
6390 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6391 );
6392 });
6393
6394 _ = editor.update(cx, |editor, window, cx| {
6395 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6396 s.select_display_ranges([
6397 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6398 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6399 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6400 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
6401 ])
6402 });
6403 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6404 assert_eq!(
6405 editor.display_text(cx),
6406 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6407 );
6408 });
6409 EditorTestContext::for_editor(editor, cx)
6410 .await
6411 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
6412
6413 _ = editor.update(cx, |editor, window, cx| {
6414 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6415 s.select_display_ranges([
6416 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
6417 ])
6418 });
6419 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6420 assert_eq!(
6421 editor.display_text(cx),
6422 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
6423 );
6424 assert_eq!(
6425 editor.selections.display_ranges(cx),
6426 [
6427 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
6428 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
6429 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
6430 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
6431 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
6432 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
6433 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
6434 ]
6435 );
6436 });
6437 EditorTestContext::for_editor(editor, cx)
6438 .await
6439 .assert_editor_state(
6440 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
6441 );
6442}
6443
6444#[gpui::test]
6445async fn test_add_selection_above_below(cx: &mut TestAppContext) {
6446 init_test(cx, |_| {});
6447
6448 let mut cx = EditorTestContext::new(cx).await;
6449
6450 cx.set_state(indoc!(
6451 r#"abc
6452 defˇghi
6453
6454 jk
6455 nlmo
6456 "#
6457 ));
6458
6459 cx.update_editor(|editor, window, cx| {
6460 editor.add_selection_above(&Default::default(), window, cx);
6461 });
6462
6463 cx.assert_editor_state(indoc!(
6464 r#"abcˇ
6465 defˇghi
6466
6467 jk
6468 nlmo
6469 "#
6470 ));
6471
6472 cx.update_editor(|editor, window, cx| {
6473 editor.add_selection_above(&Default::default(), window, cx);
6474 });
6475
6476 cx.assert_editor_state(indoc!(
6477 r#"abcˇ
6478 defˇghi
6479
6480 jk
6481 nlmo
6482 "#
6483 ));
6484
6485 cx.update_editor(|editor, window, cx| {
6486 editor.add_selection_below(&Default::default(), window, cx);
6487 });
6488
6489 cx.assert_editor_state(indoc!(
6490 r#"abc
6491 defˇghi
6492
6493 jk
6494 nlmo
6495 "#
6496 ));
6497
6498 cx.update_editor(|editor, window, cx| {
6499 editor.undo_selection(&Default::default(), window, cx);
6500 });
6501
6502 cx.assert_editor_state(indoc!(
6503 r#"abcˇ
6504 defˇghi
6505
6506 jk
6507 nlmo
6508 "#
6509 ));
6510
6511 cx.update_editor(|editor, window, cx| {
6512 editor.redo_selection(&Default::default(), window, cx);
6513 });
6514
6515 cx.assert_editor_state(indoc!(
6516 r#"abc
6517 defˇghi
6518
6519 jk
6520 nlmo
6521 "#
6522 ));
6523
6524 cx.update_editor(|editor, window, cx| {
6525 editor.add_selection_below(&Default::default(), window, cx);
6526 });
6527
6528 cx.assert_editor_state(indoc!(
6529 r#"abc
6530 defˇghi
6531 ˇ
6532 jk
6533 nlmo
6534 "#
6535 ));
6536
6537 cx.update_editor(|editor, window, cx| {
6538 editor.add_selection_below(&Default::default(), window, cx);
6539 });
6540
6541 cx.assert_editor_state(indoc!(
6542 r#"abc
6543 defˇghi
6544 ˇ
6545 jkˇ
6546 nlmo
6547 "#
6548 ));
6549
6550 cx.update_editor(|editor, window, cx| {
6551 editor.add_selection_below(&Default::default(), window, cx);
6552 });
6553
6554 cx.assert_editor_state(indoc!(
6555 r#"abc
6556 defˇghi
6557 ˇ
6558 jkˇ
6559 nlmˇo
6560 "#
6561 ));
6562
6563 cx.update_editor(|editor, window, cx| {
6564 editor.add_selection_below(&Default::default(), window, cx);
6565 });
6566
6567 cx.assert_editor_state(indoc!(
6568 r#"abc
6569 defˇghi
6570 ˇ
6571 jkˇ
6572 nlmˇo
6573 ˇ"#
6574 ));
6575
6576 // change selections
6577 cx.set_state(indoc!(
6578 r#"abc
6579 def«ˇg»hi
6580
6581 jk
6582 nlmo
6583 "#
6584 ));
6585
6586 cx.update_editor(|editor, window, cx| {
6587 editor.add_selection_below(&Default::default(), window, cx);
6588 });
6589
6590 cx.assert_editor_state(indoc!(
6591 r#"abc
6592 def«ˇg»hi
6593
6594 jk
6595 nlm«ˇo»
6596 "#
6597 ));
6598
6599 cx.update_editor(|editor, window, cx| {
6600 editor.add_selection_below(&Default::default(), window, cx);
6601 });
6602
6603 cx.assert_editor_state(indoc!(
6604 r#"abc
6605 def«ˇg»hi
6606
6607 jk
6608 nlm«ˇo»
6609 "#
6610 ));
6611
6612 cx.update_editor(|editor, window, cx| {
6613 editor.add_selection_above(&Default::default(), window, cx);
6614 });
6615
6616 cx.assert_editor_state(indoc!(
6617 r#"abc
6618 def«ˇg»hi
6619
6620 jk
6621 nlmo
6622 "#
6623 ));
6624
6625 cx.update_editor(|editor, window, cx| {
6626 editor.add_selection_above(&Default::default(), window, cx);
6627 });
6628
6629 cx.assert_editor_state(indoc!(
6630 r#"abc
6631 def«ˇg»hi
6632
6633 jk
6634 nlmo
6635 "#
6636 ));
6637
6638 // Change selections again
6639 cx.set_state(indoc!(
6640 r#"a«bc
6641 defgˇ»hi
6642
6643 jk
6644 nlmo
6645 "#
6646 ));
6647
6648 cx.update_editor(|editor, window, cx| {
6649 editor.add_selection_below(&Default::default(), window, cx);
6650 });
6651
6652 cx.assert_editor_state(indoc!(
6653 r#"a«bcˇ»
6654 d«efgˇ»hi
6655
6656 j«kˇ»
6657 nlmo
6658 "#
6659 ));
6660
6661 cx.update_editor(|editor, window, cx| {
6662 editor.add_selection_below(&Default::default(), window, cx);
6663 });
6664 cx.assert_editor_state(indoc!(
6665 r#"a«bcˇ»
6666 d«efgˇ»hi
6667
6668 j«kˇ»
6669 n«lmoˇ»
6670 "#
6671 ));
6672 cx.update_editor(|editor, window, cx| {
6673 editor.add_selection_above(&Default::default(), window, cx);
6674 });
6675
6676 cx.assert_editor_state(indoc!(
6677 r#"a«bcˇ»
6678 d«efgˇ»hi
6679
6680 j«kˇ»
6681 nlmo
6682 "#
6683 ));
6684
6685 // Change selections again
6686 cx.set_state(indoc!(
6687 r#"abc
6688 d«ˇefghi
6689
6690 jk
6691 nlm»o
6692 "#
6693 ));
6694
6695 cx.update_editor(|editor, window, cx| {
6696 editor.add_selection_above(&Default::default(), window, cx);
6697 });
6698
6699 cx.assert_editor_state(indoc!(
6700 r#"a«ˇbc»
6701 d«ˇef»ghi
6702
6703 j«ˇk»
6704 n«ˇlm»o
6705 "#
6706 ));
6707
6708 cx.update_editor(|editor, window, cx| {
6709 editor.add_selection_below(&Default::default(), window, cx);
6710 });
6711
6712 cx.assert_editor_state(indoc!(
6713 r#"abc
6714 d«ˇef»ghi
6715
6716 j«ˇk»
6717 n«ˇlm»o
6718 "#
6719 ));
6720}
6721
6722#[gpui::test]
6723async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
6724 init_test(cx, |_| {});
6725 let mut cx = EditorTestContext::new(cx).await;
6726
6727 cx.set_state(indoc!(
6728 r#"line onˇe
6729 liˇne two
6730 line three
6731 line four"#
6732 ));
6733
6734 cx.update_editor(|editor, window, cx| {
6735 editor.add_selection_below(&Default::default(), window, cx);
6736 });
6737
6738 // test multiple cursors expand in the same direction
6739 cx.assert_editor_state(indoc!(
6740 r#"line onˇe
6741 liˇne twˇo
6742 liˇne three
6743 line four"#
6744 ));
6745
6746 cx.update_editor(|editor, window, cx| {
6747 editor.add_selection_below(&Default::default(), window, cx);
6748 });
6749
6750 cx.update_editor(|editor, window, cx| {
6751 editor.add_selection_below(&Default::default(), window, cx);
6752 });
6753
6754 // test multiple cursors expand below overflow
6755 cx.assert_editor_state(indoc!(
6756 r#"line onˇe
6757 liˇne twˇo
6758 liˇne thˇree
6759 liˇne foˇur"#
6760 ));
6761
6762 cx.update_editor(|editor, window, cx| {
6763 editor.add_selection_above(&Default::default(), window, cx);
6764 });
6765
6766 // test multiple cursors retrieves back correctly
6767 cx.assert_editor_state(indoc!(
6768 r#"line onˇe
6769 liˇne twˇo
6770 liˇne thˇree
6771 line four"#
6772 ));
6773
6774 cx.update_editor(|editor, window, cx| {
6775 editor.add_selection_above(&Default::default(), window, cx);
6776 });
6777
6778 cx.update_editor(|editor, window, cx| {
6779 editor.add_selection_above(&Default::default(), window, cx);
6780 });
6781
6782 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
6783 cx.assert_editor_state(indoc!(
6784 r#"liˇne onˇe
6785 liˇne two
6786 line three
6787 line four"#
6788 ));
6789
6790 cx.update_editor(|editor, window, cx| {
6791 editor.undo_selection(&Default::default(), window, cx);
6792 });
6793
6794 // test undo
6795 cx.assert_editor_state(indoc!(
6796 r#"line onˇe
6797 liˇne twˇo
6798 line three
6799 line four"#
6800 ));
6801
6802 cx.update_editor(|editor, window, cx| {
6803 editor.redo_selection(&Default::default(), window, cx);
6804 });
6805
6806 // test redo
6807 cx.assert_editor_state(indoc!(
6808 r#"liˇne onˇe
6809 liˇne two
6810 line three
6811 line four"#
6812 ));
6813
6814 cx.set_state(indoc!(
6815 r#"abcd
6816 ef«ghˇ»
6817 ijkl
6818 «mˇ»nop"#
6819 ));
6820
6821 cx.update_editor(|editor, window, cx| {
6822 editor.add_selection_above(&Default::default(), window, cx);
6823 });
6824
6825 // test multiple selections expand in the same direction
6826 cx.assert_editor_state(indoc!(
6827 r#"ab«cdˇ»
6828 ef«ghˇ»
6829 «iˇ»jkl
6830 «mˇ»nop"#
6831 ));
6832
6833 cx.update_editor(|editor, window, cx| {
6834 editor.add_selection_above(&Default::default(), window, cx);
6835 });
6836
6837 // test multiple selection upward overflow
6838 cx.assert_editor_state(indoc!(
6839 r#"ab«cdˇ»
6840 «eˇ»f«ghˇ»
6841 «iˇ»jkl
6842 «mˇ»nop"#
6843 ));
6844
6845 cx.update_editor(|editor, window, cx| {
6846 editor.add_selection_below(&Default::default(), window, cx);
6847 });
6848
6849 // test multiple selection retrieves back correctly
6850 cx.assert_editor_state(indoc!(
6851 r#"abcd
6852 ef«ghˇ»
6853 «iˇ»jkl
6854 «mˇ»nop"#
6855 ));
6856
6857 cx.update_editor(|editor, window, cx| {
6858 editor.add_selection_below(&Default::default(), window, cx);
6859 });
6860
6861 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
6862 cx.assert_editor_state(indoc!(
6863 r#"abcd
6864 ef«ghˇ»
6865 ij«klˇ»
6866 «mˇ»nop"#
6867 ));
6868
6869 cx.update_editor(|editor, window, cx| {
6870 editor.undo_selection(&Default::default(), window, cx);
6871 });
6872
6873 // test undo
6874 cx.assert_editor_state(indoc!(
6875 r#"abcd
6876 ef«ghˇ»
6877 «iˇ»jkl
6878 «mˇ»nop"#
6879 ));
6880
6881 cx.update_editor(|editor, window, cx| {
6882 editor.redo_selection(&Default::default(), window, cx);
6883 });
6884
6885 // test redo
6886 cx.assert_editor_state(indoc!(
6887 r#"abcd
6888 ef«ghˇ»
6889 ij«klˇ»
6890 «mˇ»nop"#
6891 ));
6892}
6893
6894#[gpui::test]
6895async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
6896 init_test(cx, |_| {});
6897 let mut cx = EditorTestContext::new(cx).await;
6898
6899 cx.set_state(indoc!(
6900 r#"line onˇe
6901 liˇne two
6902 line three
6903 line four"#
6904 ));
6905
6906 cx.update_editor(|editor, window, cx| {
6907 editor.add_selection_below(&Default::default(), window, cx);
6908 editor.add_selection_below(&Default::default(), window, cx);
6909 editor.add_selection_below(&Default::default(), window, cx);
6910 });
6911
6912 // initial state with two multi cursor groups
6913 cx.assert_editor_state(indoc!(
6914 r#"line onˇe
6915 liˇne twˇo
6916 liˇne thˇree
6917 liˇne foˇur"#
6918 ));
6919
6920 // add single cursor in middle - simulate opt click
6921 cx.update_editor(|editor, window, cx| {
6922 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
6923 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6924 editor.end_selection(window, cx);
6925 });
6926
6927 cx.assert_editor_state(indoc!(
6928 r#"line onˇe
6929 liˇne twˇo
6930 liˇneˇ thˇree
6931 liˇne foˇur"#
6932 ));
6933
6934 cx.update_editor(|editor, window, cx| {
6935 editor.add_selection_above(&Default::default(), window, cx);
6936 });
6937
6938 // test new added selection expands above and existing selection shrinks
6939 cx.assert_editor_state(indoc!(
6940 r#"line onˇe
6941 liˇneˇ twˇo
6942 liˇneˇ thˇree
6943 line four"#
6944 ));
6945
6946 cx.update_editor(|editor, window, cx| {
6947 editor.add_selection_above(&Default::default(), window, cx);
6948 });
6949
6950 // test new added selection expands above and existing selection shrinks
6951 cx.assert_editor_state(indoc!(
6952 r#"lineˇ onˇe
6953 liˇneˇ twˇo
6954 lineˇ three
6955 line four"#
6956 ));
6957
6958 // intial state with two selection groups
6959 cx.set_state(indoc!(
6960 r#"abcd
6961 ef«ghˇ»
6962 ijkl
6963 «mˇ»nop"#
6964 ));
6965
6966 cx.update_editor(|editor, window, cx| {
6967 editor.add_selection_above(&Default::default(), window, cx);
6968 editor.add_selection_above(&Default::default(), window, cx);
6969 });
6970
6971 cx.assert_editor_state(indoc!(
6972 r#"ab«cdˇ»
6973 «eˇ»f«ghˇ»
6974 «iˇ»jkl
6975 «mˇ»nop"#
6976 ));
6977
6978 // add single selection in middle - simulate opt drag
6979 cx.update_editor(|editor, window, cx| {
6980 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
6981 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6982 editor.update_selection(
6983 DisplayPoint::new(DisplayRow(2), 4),
6984 0,
6985 gpui::Point::<f32>::default(),
6986 window,
6987 cx,
6988 );
6989 editor.end_selection(window, cx);
6990 });
6991
6992 cx.assert_editor_state(indoc!(
6993 r#"ab«cdˇ»
6994 «eˇ»f«ghˇ»
6995 «iˇ»jk«lˇ»
6996 «mˇ»nop"#
6997 ));
6998
6999 cx.update_editor(|editor, window, cx| {
7000 editor.add_selection_below(&Default::default(), window, cx);
7001 });
7002
7003 // test new added selection expands below, others shrinks from above
7004 cx.assert_editor_state(indoc!(
7005 r#"abcd
7006 ef«ghˇ»
7007 «iˇ»jk«lˇ»
7008 «mˇ»no«pˇ»"#
7009 ));
7010}
7011
7012#[gpui::test]
7013async fn test_select_next(cx: &mut TestAppContext) {
7014 init_test(cx, |_| {});
7015
7016 let mut cx = EditorTestContext::new(cx).await;
7017 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7018
7019 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7020 .unwrap();
7021 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7022
7023 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7024 .unwrap();
7025 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7026
7027 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7028 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7029
7030 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7031 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7032
7033 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7034 .unwrap();
7035 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7036
7037 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7038 .unwrap();
7039 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7040
7041 // Test selection direction should be preserved
7042 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7043
7044 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7045 .unwrap();
7046 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
7047}
7048
7049#[gpui::test]
7050async fn test_select_all_matches(cx: &mut TestAppContext) {
7051 init_test(cx, |_| {});
7052
7053 let mut cx = EditorTestContext::new(cx).await;
7054
7055 // Test caret-only selections
7056 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7057 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7058 .unwrap();
7059 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7060
7061 // Test left-to-right selections
7062 cx.set_state("abc\n«abcˇ»\nabc");
7063 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7064 .unwrap();
7065 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
7066
7067 // Test right-to-left selections
7068 cx.set_state("abc\n«ˇabc»\nabc");
7069 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7070 .unwrap();
7071 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
7072
7073 // Test selecting whitespace with caret selection
7074 cx.set_state("abc\nˇ abc\nabc");
7075 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7076 .unwrap();
7077 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7078
7079 // Test selecting whitespace with left-to-right selection
7080 cx.set_state("abc\n«ˇ »abc\nabc");
7081 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7082 .unwrap();
7083 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
7084
7085 // Test no matches with right-to-left selection
7086 cx.set_state("abc\n« ˇ»abc\nabc");
7087 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7088 .unwrap();
7089 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7090
7091 // Test with a single word and clip_at_line_ends=true (#29823)
7092 cx.set_state("aˇbc");
7093 cx.update_editor(|e, window, cx| {
7094 e.set_clip_at_line_ends(true, cx);
7095 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
7096 e.set_clip_at_line_ends(false, cx);
7097 });
7098 cx.assert_editor_state("«abcˇ»");
7099}
7100
7101#[gpui::test]
7102async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
7103 init_test(cx, |_| {});
7104
7105 let mut cx = EditorTestContext::new(cx).await;
7106
7107 let large_body_1 = "\nd".repeat(200);
7108 let large_body_2 = "\ne".repeat(200);
7109
7110 cx.set_state(&format!(
7111 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
7112 ));
7113 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
7114 let scroll_position = editor.scroll_position(cx);
7115 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
7116 scroll_position
7117 });
7118
7119 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7120 .unwrap();
7121 cx.assert_editor_state(&format!(
7122 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
7123 ));
7124 let scroll_position_after_selection =
7125 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
7126 assert_eq!(
7127 initial_scroll_position, scroll_position_after_selection,
7128 "Scroll position should not change after selecting all matches"
7129 );
7130}
7131
7132#[gpui::test]
7133async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
7134 init_test(cx, |_| {});
7135
7136 let mut cx = EditorLspTestContext::new_rust(
7137 lsp::ServerCapabilities {
7138 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7139 ..Default::default()
7140 },
7141 cx,
7142 )
7143 .await;
7144
7145 cx.set_state(indoc! {"
7146 line 1
7147 line 2
7148 linˇe 3
7149 line 4
7150 line 5
7151 "});
7152
7153 // Make an edit
7154 cx.update_editor(|editor, window, cx| {
7155 editor.handle_input("X", window, cx);
7156 });
7157
7158 // Move cursor to a different position
7159 cx.update_editor(|editor, window, cx| {
7160 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7161 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
7162 });
7163 });
7164
7165 cx.assert_editor_state(indoc! {"
7166 line 1
7167 line 2
7168 linXe 3
7169 line 4
7170 liˇne 5
7171 "});
7172
7173 cx.lsp
7174 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
7175 Ok(Some(vec![lsp::TextEdit::new(
7176 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
7177 "PREFIX ".to_string(),
7178 )]))
7179 });
7180
7181 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
7182 .unwrap()
7183 .await
7184 .unwrap();
7185
7186 cx.assert_editor_state(indoc! {"
7187 PREFIX line 1
7188 line 2
7189 linXe 3
7190 line 4
7191 liˇne 5
7192 "});
7193
7194 // Undo formatting
7195 cx.update_editor(|editor, window, cx| {
7196 editor.undo(&Default::default(), window, cx);
7197 });
7198
7199 // Verify cursor moved back to position after edit
7200 cx.assert_editor_state(indoc! {"
7201 line 1
7202 line 2
7203 linXˇe 3
7204 line 4
7205 line 5
7206 "});
7207}
7208
7209#[gpui::test]
7210async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) {
7211 init_test(cx, |_| {});
7212
7213 let mut cx = EditorTestContext::new(cx).await;
7214
7215 let provider = cx.new(|_| FakeInlineCompletionProvider::default());
7216 cx.update_editor(|editor, window, cx| {
7217 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
7218 });
7219
7220 cx.set_state(indoc! {"
7221 line 1
7222 line 2
7223 linˇe 3
7224 line 4
7225 line 5
7226 line 6
7227 line 7
7228 line 8
7229 line 9
7230 line 10
7231 "});
7232
7233 let snapshot = cx.buffer_snapshot();
7234 let edit_position = snapshot.anchor_after(Point::new(2, 4));
7235
7236 cx.update(|_, cx| {
7237 provider.update(cx, |provider, _| {
7238 provider.set_inline_completion(Some(inline_completion::InlineCompletion {
7239 id: None,
7240 edits: vec![(edit_position..edit_position, "X".into())],
7241 edit_preview: None,
7242 }))
7243 })
7244 });
7245
7246 cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
7247 cx.update_editor(|editor, window, cx| {
7248 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
7249 });
7250
7251 cx.assert_editor_state(indoc! {"
7252 line 1
7253 line 2
7254 lineXˇ 3
7255 line 4
7256 line 5
7257 line 6
7258 line 7
7259 line 8
7260 line 9
7261 line 10
7262 "});
7263
7264 cx.update_editor(|editor, window, cx| {
7265 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7266 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
7267 });
7268 });
7269
7270 cx.assert_editor_state(indoc! {"
7271 line 1
7272 line 2
7273 lineX 3
7274 line 4
7275 line 5
7276 line 6
7277 line 7
7278 line 8
7279 line 9
7280 liˇne 10
7281 "});
7282
7283 cx.update_editor(|editor, window, cx| {
7284 editor.undo(&Default::default(), window, cx);
7285 });
7286
7287 cx.assert_editor_state(indoc! {"
7288 line 1
7289 line 2
7290 lineˇ 3
7291 line 4
7292 line 5
7293 line 6
7294 line 7
7295 line 8
7296 line 9
7297 line 10
7298 "});
7299}
7300
7301#[gpui::test]
7302async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
7303 init_test(cx, |_| {});
7304
7305 let mut cx = EditorTestContext::new(cx).await;
7306 cx.set_state(
7307 r#"let foo = 2;
7308lˇet foo = 2;
7309let fooˇ = 2;
7310let foo = 2;
7311let foo = ˇ2;"#,
7312 );
7313
7314 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7315 .unwrap();
7316 cx.assert_editor_state(
7317 r#"let foo = 2;
7318«letˇ» foo = 2;
7319let «fooˇ» = 2;
7320let foo = 2;
7321let foo = «2ˇ»;"#,
7322 );
7323
7324 // noop for multiple selections with different contents
7325 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7326 .unwrap();
7327 cx.assert_editor_state(
7328 r#"let foo = 2;
7329«letˇ» foo = 2;
7330let «fooˇ» = 2;
7331let foo = 2;
7332let foo = «2ˇ»;"#,
7333 );
7334
7335 // Test last selection direction should be preserved
7336 cx.set_state(
7337 r#"let foo = 2;
7338let foo = 2;
7339let «fooˇ» = 2;
7340let «ˇfoo» = 2;
7341let foo = 2;"#,
7342 );
7343
7344 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7345 .unwrap();
7346 cx.assert_editor_state(
7347 r#"let foo = 2;
7348let foo = 2;
7349let «fooˇ» = 2;
7350let «ˇfoo» = 2;
7351let «ˇfoo» = 2;"#,
7352 );
7353}
7354
7355#[gpui::test]
7356async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
7357 init_test(cx, |_| {});
7358
7359 let mut cx =
7360 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
7361
7362 cx.assert_editor_state(indoc! {"
7363 ˇbbb
7364 ccc
7365
7366 bbb
7367 ccc
7368 "});
7369 cx.dispatch_action(SelectPrevious::default());
7370 cx.assert_editor_state(indoc! {"
7371 «bbbˇ»
7372 ccc
7373
7374 bbb
7375 ccc
7376 "});
7377 cx.dispatch_action(SelectPrevious::default());
7378 cx.assert_editor_state(indoc! {"
7379 «bbbˇ»
7380 ccc
7381
7382 «bbbˇ»
7383 ccc
7384 "});
7385}
7386
7387#[gpui::test]
7388async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
7389 init_test(cx, |_| {});
7390
7391 let mut cx = EditorTestContext::new(cx).await;
7392 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7393
7394 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7395 .unwrap();
7396 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7397
7398 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7399 .unwrap();
7400 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7401
7402 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7403 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7404
7405 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7406 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7407
7408 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7409 .unwrap();
7410 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
7411
7412 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7413 .unwrap();
7414 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7415}
7416
7417#[gpui::test]
7418async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
7419 init_test(cx, |_| {});
7420
7421 let mut cx = EditorTestContext::new(cx).await;
7422 cx.set_state("aˇ");
7423
7424 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7425 .unwrap();
7426 cx.assert_editor_state("«aˇ»");
7427 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7428 .unwrap();
7429 cx.assert_editor_state("«aˇ»");
7430}
7431
7432#[gpui::test]
7433async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
7434 init_test(cx, |_| {});
7435
7436 let mut cx = EditorTestContext::new(cx).await;
7437 cx.set_state(
7438 r#"let foo = 2;
7439lˇet foo = 2;
7440let fooˇ = 2;
7441let foo = 2;
7442let foo = ˇ2;"#,
7443 );
7444
7445 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7446 .unwrap();
7447 cx.assert_editor_state(
7448 r#"let foo = 2;
7449«letˇ» foo = 2;
7450let «fooˇ» = 2;
7451let foo = 2;
7452let foo = «2ˇ»;"#,
7453 );
7454
7455 // noop for multiple selections with different contents
7456 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7457 .unwrap();
7458 cx.assert_editor_state(
7459 r#"let foo = 2;
7460«letˇ» foo = 2;
7461let «fooˇ» = 2;
7462let foo = 2;
7463let foo = «2ˇ»;"#,
7464 );
7465}
7466
7467#[gpui::test]
7468async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
7469 init_test(cx, |_| {});
7470
7471 let mut cx = EditorTestContext::new(cx).await;
7472 cx.set_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 // selection direction is preserved
7477 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7478
7479 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7480 .unwrap();
7481 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7482
7483 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7484 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7485
7486 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7487 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7488
7489 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7490 .unwrap();
7491 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
7492
7493 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7494 .unwrap();
7495 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
7496}
7497
7498#[gpui::test]
7499async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
7500 init_test(cx, |_| {});
7501
7502 let language = Arc::new(Language::new(
7503 LanguageConfig::default(),
7504 Some(tree_sitter_rust::LANGUAGE.into()),
7505 ));
7506
7507 let text = r#"
7508 use mod1::mod2::{mod3, mod4};
7509
7510 fn fn_1(param1: bool, param2: &str) {
7511 let var1 = "text";
7512 }
7513 "#
7514 .unindent();
7515
7516 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7517 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7518 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7519
7520 editor
7521 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7522 .await;
7523
7524 editor.update_in(cx, |editor, window, cx| {
7525 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7526 s.select_display_ranges([
7527 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
7528 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
7529 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
7530 ]);
7531 });
7532 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7533 });
7534 editor.update(cx, |editor, cx| {
7535 assert_text_with_selections(
7536 editor,
7537 indoc! {r#"
7538 use mod1::mod2::{mod3, «mod4ˇ»};
7539
7540 fn fn_1«ˇ(param1: bool, param2: &str)» {
7541 let var1 = "«ˇtext»";
7542 }
7543 "#},
7544 cx,
7545 );
7546 });
7547
7548 editor.update_in(cx, |editor, window, cx| {
7549 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7550 });
7551 editor.update(cx, |editor, cx| {
7552 assert_text_with_selections(
7553 editor,
7554 indoc! {r#"
7555 use mod1::mod2::«{mod3, mod4}ˇ»;
7556
7557 «ˇfn fn_1(param1: bool, param2: &str) {
7558 let var1 = "text";
7559 }»
7560 "#},
7561 cx,
7562 );
7563 });
7564
7565 editor.update_in(cx, |editor, window, cx| {
7566 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7567 });
7568 assert_eq!(
7569 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7570 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7571 );
7572
7573 // Trying to expand the selected syntax node one more time has no effect.
7574 editor.update_in(cx, |editor, window, cx| {
7575 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7576 });
7577 assert_eq!(
7578 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7579 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7580 );
7581
7582 editor.update_in(cx, |editor, window, cx| {
7583 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7584 });
7585 editor.update(cx, |editor, cx| {
7586 assert_text_with_selections(
7587 editor,
7588 indoc! {r#"
7589 use mod1::mod2::«{mod3, mod4}ˇ»;
7590
7591 «ˇfn fn_1(param1: bool, param2: &str) {
7592 let var1 = "text";
7593 }»
7594 "#},
7595 cx,
7596 );
7597 });
7598
7599 editor.update_in(cx, |editor, window, cx| {
7600 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7601 });
7602 editor.update(cx, |editor, cx| {
7603 assert_text_with_selections(
7604 editor,
7605 indoc! {r#"
7606 use mod1::mod2::{mod3, «mod4ˇ»};
7607
7608 fn fn_1«ˇ(param1: bool, param2: &str)» {
7609 let var1 = "«ˇtext»";
7610 }
7611 "#},
7612 cx,
7613 );
7614 });
7615
7616 editor.update_in(cx, |editor, window, cx| {
7617 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7618 });
7619 editor.update(cx, |editor, cx| {
7620 assert_text_with_selections(
7621 editor,
7622 indoc! {r#"
7623 use mod1::mod2::{mod3, mo«ˇ»d4};
7624
7625 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7626 let var1 = "te«ˇ»xt";
7627 }
7628 "#},
7629 cx,
7630 );
7631 });
7632
7633 // Trying to shrink the selected syntax node one more time has no effect.
7634 editor.update_in(cx, |editor, window, cx| {
7635 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7636 });
7637 editor.update_in(cx, |editor, _, cx| {
7638 assert_text_with_selections(
7639 editor,
7640 indoc! {r#"
7641 use mod1::mod2::{mod3, mo«ˇ»d4};
7642
7643 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7644 let var1 = "te«ˇ»xt";
7645 }
7646 "#},
7647 cx,
7648 );
7649 });
7650
7651 // Ensure that we keep expanding the selection if the larger selection starts or ends within
7652 // a fold.
7653 editor.update_in(cx, |editor, window, cx| {
7654 editor.fold_creases(
7655 vec![
7656 Crease::simple(
7657 Point::new(0, 21)..Point::new(0, 24),
7658 FoldPlaceholder::test(),
7659 ),
7660 Crease::simple(
7661 Point::new(3, 20)..Point::new(3, 22),
7662 FoldPlaceholder::test(),
7663 ),
7664 ],
7665 true,
7666 window,
7667 cx,
7668 );
7669 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7670 });
7671 editor.update(cx, |editor, cx| {
7672 assert_text_with_selections(
7673 editor,
7674 indoc! {r#"
7675 use mod1::mod2::«{mod3, mod4}ˇ»;
7676
7677 fn fn_1«ˇ(param1: bool, param2: &str)» {
7678 let var1 = "«ˇtext»";
7679 }
7680 "#},
7681 cx,
7682 );
7683 });
7684}
7685
7686#[gpui::test]
7687async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
7688 init_test(cx, |_| {});
7689
7690 let language = Arc::new(Language::new(
7691 LanguageConfig::default(),
7692 Some(tree_sitter_rust::LANGUAGE.into()),
7693 ));
7694
7695 let text = "let a = 2;";
7696
7697 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7698 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7699 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7700
7701 editor
7702 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7703 .await;
7704
7705 // Test case 1: Cursor at end of word
7706 editor.update_in(cx, |editor, window, cx| {
7707 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7708 s.select_display_ranges([
7709 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
7710 ]);
7711 });
7712 });
7713 editor.update(cx, |editor, cx| {
7714 assert_text_with_selections(editor, "let aˇ = 2;", cx);
7715 });
7716 editor.update_in(cx, |editor, window, cx| {
7717 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7718 });
7719 editor.update(cx, |editor, cx| {
7720 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
7721 });
7722 editor.update_in(cx, |editor, window, cx| {
7723 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7724 });
7725 editor.update(cx, |editor, cx| {
7726 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7727 });
7728
7729 // Test case 2: Cursor at end of statement
7730 editor.update_in(cx, |editor, window, cx| {
7731 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7732 s.select_display_ranges([
7733 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
7734 ]);
7735 });
7736 });
7737 editor.update(cx, |editor, cx| {
7738 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
7739 });
7740 editor.update_in(cx, |editor, window, cx| {
7741 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7742 });
7743 editor.update(cx, |editor, cx| {
7744 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7745 });
7746}
7747
7748#[gpui::test]
7749async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
7750 init_test(cx, |_| {});
7751
7752 let language = Arc::new(Language::new(
7753 LanguageConfig::default(),
7754 Some(tree_sitter_rust::LANGUAGE.into()),
7755 ));
7756
7757 let text = r#"
7758 use mod1::mod2::{mod3, mod4};
7759
7760 fn fn_1(param1: bool, param2: &str) {
7761 let var1 = "hello world";
7762 }
7763 "#
7764 .unindent();
7765
7766 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7767 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7768 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7769
7770 editor
7771 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7772 .await;
7773
7774 // Test 1: Cursor on a letter of a string word
7775 editor.update_in(cx, |editor, window, cx| {
7776 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7777 s.select_display_ranges([
7778 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
7779 ]);
7780 });
7781 });
7782 editor.update_in(cx, |editor, window, cx| {
7783 assert_text_with_selections(
7784 editor,
7785 indoc! {r#"
7786 use mod1::mod2::{mod3, mod4};
7787
7788 fn fn_1(param1: bool, param2: &str) {
7789 let var1 = "hˇello world";
7790 }
7791 "#},
7792 cx,
7793 );
7794 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7795 assert_text_with_selections(
7796 editor,
7797 indoc! {r#"
7798 use mod1::mod2::{mod3, mod4};
7799
7800 fn fn_1(param1: bool, param2: &str) {
7801 let var1 = "«ˇhello» world";
7802 }
7803 "#},
7804 cx,
7805 );
7806 });
7807
7808 // Test 2: Partial selection within a word
7809 editor.update_in(cx, |editor, window, cx| {
7810 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7811 s.select_display_ranges([
7812 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7813 ]);
7814 });
7815 });
7816 editor.update_in(cx, |editor, window, cx| {
7817 assert_text_with_selections(
7818 editor,
7819 indoc! {r#"
7820 use mod1::mod2::{mod3, mod4};
7821
7822 fn fn_1(param1: bool, param2: &str) {
7823 let var1 = "h«elˇ»lo world";
7824 }
7825 "#},
7826 cx,
7827 );
7828 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7829 assert_text_with_selections(
7830 editor,
7831 indoc! {r#"
7832 use mod1::mod2::{mod3, mod4};
7833
7834 fn fn_1(param1: bool, param2: &str) {
7835 let var1 = "«ˇhello» world";
7836 }
7837 "#},
7838 cx,
7839 );
7840 });
7841
7842 // Test 3: Complete word already selected
7843 editor.update_in(cx, |editor, window, cx| {
7844 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7845 s.select_display_ranges([
7846 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7847 ]);
7848 });
7849 });
7850 editor.update_in(cx, |editor, window, cx| {
7851 assert_text_with_selections(
7852 editor,
7853 indoc! {r#"
7854 use mod1::mod2::{mod3, mod4};
7855
7856 fn fn_1(param1: bool, param2: &str) {
7857 let var1 = "«helloˇ» world";
7858 }
7859 "#},
7860 cx,
7861 );
7862 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7863 assert_text_with_selections(
7864 editor,
7865 indoc! {r#"
7866 use mod1::mod2::{mod3, mod4};
7867
7868 fn fn_1(param1: bool, param2: &str) {
7869 let var1 = "«hello worldˇ»";
7870 }
7871 "#},
7872 cx,
7873 );
7874 });
7875
7876 // Test 4: Selection spanning across words
7877 editor.update_in(cx, |editor, window, cx| {
7878 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7879 s.select_display_ranges([
7880 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7881 ]);
7882 });
7883 });
7884 editor.update_in(cx, |editor, window, cx| {
7885 assert_text_with_selections(
7886 editor,
7887 indoc! {r#"
7888 use mod1::mod2::{mod3, mod4};
7889
7890 fn fn_1(param1: bool, param2: &str) {
7891 let var1 = "hel«lo woˇ»rld";
7892 }
7893 "#},
7894 cx,
7895 );
7896 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7897 assert_text_with_selections(
7898 editor,
7899 indoc! {r#"
7900 use mod1::mod2::{mod3, mod4};
7901
7902 fn fn_1(param1: bool, param2: &str) {
7903 let var1 = "«ˇhello world»";
7904 }
7905 "#},
7906 cx,
7907 );
7908 });
7909
7910 // Test 5: Expansion beyond string
7911 editor.update_in(cx, |editor, window, cx| {
7912 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7913 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7914 assert_text_with_selections(
7915 editor,
7916 indoc! {r#"
7917 use mod1::mod2::{mod3, mod4};
7918
7919 fn fn_1(param1: bool, param2: &str) {
7920 «ˇlet var1 = "hello world";»
7921 }
7922 "#},
7923 cx,
7924 );
7925 });
7926}
7927
7928#[gpui::test]
7929async fn test_fold_function_bodies(cx: &mut TestAppContext) {
7930 init_test(cx, |_| {});
7931
7932 let base_text = r#"
7933 impl A {
7934 // this is an uncommitted comment
7935
7936 fn b() {
7937 c();
7938 }
7939
7940 // this is another uncommitted comment
7941
7942 fn d() {
7943 // e
7944 // f
7945 }
7946 }
7947
7948 fn g() {
7949 // h
7950 }
7951 "#
7952 .unindent();
7953
7954 let text = r#"
7955 ˇimpl A {
7956
7957 fn b() {
7958 c();
7959 }
7960
7961 fn d() {
7962 // e
7963 // f
7964 }
7965 }
7966
7967 fn g() {
7968 // h
7969 }
7970 "#
7971 .unindent();
7972
7973 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7974 cx.set_state(&text);
7975 cx.set_head_text(&base_text);
7976 cx.update_editor(|editor, window, cx| {
7977 editor.expand_all_diff_hunks(&Default::default(), window, cx);
7978 });
7979
7980 cx.assert_state_with_diff(
7981 "
7982 ˇimpl A {
7983 - // this is an uncommitted comment
7984
7985 fn b() {
7986 c();
7987 }
7988
7989 - // this is another uncommitted comment
7990 -
7991 fn d() {
7992 // e
7993 // f
7994 }
7995 }
7996
7997 fn g() {
7998 // h
7999 }
8000 "
8001 .unindent(),
8002 );
8003
8004 let expected_display_text = "
8005 impl A {
8006 // this is an uncommitted comment
8007
8008 fn b() {
8009 ⋯
8010 }
8011
8012 // this is another uncommitted comment
8013
8014 fn d() {
8015 ⋯
8016 }
8017 }
8018
8019 fn g() {
8020 ⋯
8021 }
8022 "
8023 .unindent();
8024
8025 cx.update_editor(|editor, window, cx| {
8026 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
8027 assert_eq!(editor.display_text(cx), expected_display_text);
8028 });
8029}
8030
8031#[gpui::test]
8032async fn test_autoindent(cx: &mut TestAppContext) {
8033 init_test(cx, |_| {});
8034
8035 let language = Arc::new(
8036 Language::new(
8037 LanguageConfig {
8038 brackets: BracketPairConfig {
8039 pairs: vec![
8040 BracketPair {
8041 start: "{".to_string(),
8042 end: "}".to_string(),
8043 close: false,
8044 surround: false,
8045 newline: true,
8046 },
8047 BracketPair {
8048 start: "(".to_string(),
8049 end: ")".to_string(),
8050 close: false,
8051 surround: false,
8052 newline: true,
8053 },
8054 ],
8055 ..Default::default()
8056 },
8057 ..Default::default()
8058 },
8059 Some(tree_sitter_rust::LANGUAGE.into()),
8060 )
8061 .with_indents_query(
8062 r#"
8063 (_ "(" ")" @end) @indent
8064 (_ "{" "}" @end) @indent
8065 "#,
8066 )
8067 .unwrap(),
8068 );
8069
8070 let text = "fn a() {}";
8071
8072 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8073 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8074 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8075 editor
8076 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8077 .await;
8078
8079 editor.update_in(cx, |editor, window, cx| {
8080 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8081 s.select_ranges([5..5, 8..8, 9..9])
8082 });
8083 editor.newline(&Newline, window, cx);
8084 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
8085 assert_eq!(
8086 editor.selections.ranges(cx),
8087 &[
8088 Point::new(1, 4)..Point::new(1, 4),
8089 Point::new(3, 4)..Point::new(3, 4),
8090 Point::new(5, 0)..Point::new(5, 0)
8091 ]
8092 );
8093 });
8094}
8095
8096#[gpui::test]
8097async fn test_autoindent_selections(cx: &mut TestAppContext) {
8098 init_test(cx, |_| {});
8099
8100 {
8101 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8102 cx.set_state(indoc! {"
8103 impl A {
8104
8105 fn b() {}
8106
8107 «fn c() {
8108
8109 }ˇ»
8110 }
8111 "});
8112
8113 cx.update_editor(|editor, window, cx| {
8114 editor.autoindent(&Default::default(), window, cx);
8115 });
8116
8117 cx.assert_editor_state(indoc! {"
8118 impl A {
8119
8120 fn b() {}
8121
8122 «fn c() {
8123
8124 }ˇ»
8125 }
8126 "});
8127 }
8128
8129 {
8130 let mut cx = EditorTestContext::new_multibuffer(
8131 cx,
8132 [indoc! { "
8133 impl A {
8134 «
8135 // a
8136 fn b(){}
8137 »
8138 «
8139 }
8140 fn c(){}
8141 »
8142 "}],
8143 );
8144
8145 let buffer = cx.update_editor(|editor, _, cx| {
8146 let buffer = editor.buffer().update(cx, |buffer, _| {
8147 buffer.all_buffers().iter().next().unwrap().clone()
8148 });
8149 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
8150 buffer
8151 });
8152
8153 cx.run_until_parked();
8154 cx.update_editor(|editor, window, cx| {
8155 editor.select_all(&Default::default(), window, cx);
8156 editor.autoindent(&Default::default(), window, cx)
8157 });
8158 cx.run_until_parked();
8159
8160 cx.update(|_, cx| {
8161 assert_eq!(
8162 buffer.read(cx).text(),
8163 indoc! { "
8164 impl A {
8165
8166 // a
8167 fn b(){}
8168
8169
8170 }
8171 fn c(){}
8172
8173 " }
8174 )
8175 });
8176 }
8177}
8178
8179#[gpui::test]
8180async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
8181 init_test(cx, |_| {});
8182
8183 let mut cx = EditorTestContext::new(cx).await;
8184
8185 let language = Arc::new(Language::new(
8186 LanguageConfig {
8187 brackets: BracketPairConfig {
8188 pairs: vec![
8189 BracketPair {
8190 start: "{".to_string(),
8191 end: "}".to_string(),
8192 close: true,
8193 surround: true,
8194 newline: true,
8195 },
8196 BracketPair {
8197 start: "(".to_string(),
8198 end: ")".to_string(),
8199 close: true,
8200 surround: true,
8201 newline: true,
8202 },
8203 BracketPair {
8204 start: "/*".to_string(),
8205 end: " */".to_string(),
8206 close: true,
8207 surround: true,
8208 newline: true,
8209 },
8210 BracketPair {
8211 start: "[".to_string(),
8212 end: "]".to_string(),
8213 close: false,
8214 surround: false,
8215 newline: true,
8216 },
8217 BracketPair {
8218 start: "\"".to_string(),
8219 end: "\"".to_string(),
8220 close: true,
8221 surround: true,
8222 newline: false,
8223 },
8224 BracketPair {
8225 start: "<".to_string(),
8226 end: ">".to_string(),
8227 close: false,
8228 surround: true,
8229 newline: true,
8230 },
8231 ],
8232 ..Default::default()
8233 },
8234 autoclose_before: "})]".to_string(),
8235 ..Default::default()
8236 },
8237 Some(tree_sitter_rust::LANGUAGE.into()),
8238 ));
8239
8240 cx.language_registry().add(language.clone());
8241 cx.update_buffer(|buffer, cx| {
8242 buffer.set_language(Some(language), cx);
8243 });
8244
8245 cx.set_state(
8246 &r#"
8247 🏀ˇ
8248 εˇ
8249 ❤️ˇ
8250 "#
8251 .unindent(),
8252 );
8253
8254 // autoclose multiple nested brackets at multiple cursors
8255 cx.update_editor(|editor, window, cx| {
8256 editor.handle_input("{", window, cx);
8257 editor.handle_input("{", window, cx);
8258 editor.handle_input("{", window, cx);
8259 });
8260 cx.assert_editor_state(
8261 &"
8262 🏀{{{ˇ}}}
8263 ε{{{ˇ}}}
8264 ❤️{{{ˇ}}}
8265 "
8266 .unindent(),
8267 );
8268
8269 // insert a different closing bracket
8270 cx.update_editor(|editor, window, cx| {
8271 editor.handle_input(")", window, cx);
8272 });
8273 cx.assert_editor_state(
8274 &"
8275 🏀{{{)ˇ}}}
8276 ε{{{)ˇ}}}
8277 ❤️{{{)ˇ}}}
8278 "
8279 .unindent(),
8280 );
8281
8282 // skip over the auto-closed brackets when typing a closing bracket
8283 cx.update_editor(|editor, window, cx| {
8284 editor.move_right(&MoveRight, window, cx);
8285 editor.handle_input("}", window, cx);
8286 editor.handle_input("}", window, cx);
8287 editor.handle_input("}", window, cx);
8288 });
8289 cx.assert_editor_state(
8290 &"
8291 🏀{{{)}}}}ˇ
8292 ε{{{)}}}}ˇ
8293 ❤️{{{)}}}}ˇ
8294 "
8295 .unindent(),
8296 );
8297
8298 // autoclose multi-character pairs
8299 cx.set_state(
8300 &"
8301 ˇ
8302 ˇ
8303 "
8304 .unindent(),
8305 );
8306 cx.update_editor(|editor, window, cx| {
8307 editor.handle_input("/", window, cx);
8308 editor.handle_input("*", window, cx);
8309 });
8310 cx.assert_editor_state(
8311 &"
8312 /*ˇ */
8313 /*ˇ */
8314 "
8315 .unindent(),
8316 );
8317
8318 // one cursor autocloses a multi-character pair, one cursor
8319 // does not autoclose.
8320 cx.set_state(
8321 &"
8322 /ˇ
8323 ˇ
8324 "
8325 .unindent(),
8326 );
8327 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
8328 cx.assert_editor_state(
8329 &"
8330 /*ˇ */
8331 *ˇ
8332 "
8333 .unindent(),
8334 );
8335
8336 // Don't autoclose if the next character isn't whitespace and isn't
8337 // listed in the language's "autoclose_before" section.
8338 cx.set_state("ˇa b");
8339 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8340 cx.assert_editor_state("{ˇa b");
8341
8342 // Don't autoclose if `close` is false for the bracket pair
8343 cx.set_state("ˇ");
8344 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
8345 cx.assert_editor_state("[ˇ");
8346
8347 // Surround with brackets if text is selected
8348 cx.set_state("«aˇ» b");
8349 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8350 cx.assert_editor_state("{«aˇ»} b");
8351
8352 // Autoclose when not immediately after a word character
8353 cx.set_state("a ˇ");
8354 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8355 cx.assert_editor_state("a \"ˇ\"");
8356
8357 // Autoclose pair where the start and end characters are the same
8358 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8359 cx.assert_editor_state("a \"\"ˇ");
8360
8361 // Don't autoclose when immediately after a word character
8362 cx.set_state("aˇ");
8363 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8364 cx.assert_editor_state("a\"ˇ");
8365
8366 // Do autoclose when after a non-word character
8367 cx.set_state("{ˇ");
8368 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8369 cx.assert_editor_state("{\"ˇ\"");
8370
8371 // Non identical pairs autoclose regardless of preceding character
8372 cx.set_state("aˇ");
8373 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8374 cx.assert_editor_state("a{ˇ}");
8375
8376 // Don't autoclose pair if autoclose is disabled
8377 cx.set_state("ˇ");
8378 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8379 cx.assert_editor_state("<ˇ");
8380
8381 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
8382 cx.set_state("«aˇ» b");
8383 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8384 cx.assert_editor_state("<«aˇ»> b");
8385}
8386
8387#[gpui::test]
8388async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
8389 init_test(cx, |settings| {
8390 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8391 });
8392
8393 let mut cx = EditorTestContext::new(cx).await;
8394
8395 let language = Arc::new(Language::new(
8396 LanguageConfig {
8397 brackets: BracketPairConfig {
8398 pairs: vec![
8399 BracketPair {
8400 start: "{".to_string(),
8401 end: "}".to_string(),
8402 close: true,
8403 surround: true,
8404 newline: true,
8405 },
8406 BracketPair {
8407 start: "(".to_string(),
8408 end: ")".to_string(),
8409 close: true,
8410 surround: true,
8411 newline: true,
8412 },
8413 BracketPair {
8414 start: "[".to_string(),
8415 end: "]".to_string(),
8416 close: false,
8417 surround: false,
8418 newline: true,
8419 },
8420 ],
8421 ..Default::default()
8422 },
8423 autoclose_before: "})]".to_string(),
8424 ..Default::default()
8425 },
8426 Some(tree_sitter_rust::LANGUAGE.into()),
8427 ));
8428
8429 cx.language_registry().add(language.clone());
8430 cx.update_buffer(|buffer, cx| {
8431 buffer.set_language(Some(language), cx);
8432 });
8433
8434 cx.set_state(
8435 &"
8436 ˇ
8437 ˇ
8438 ˇ
8439 "
8440 .unindent(),
8441 );
8442
8443 // ensure only matching closing brackets are skipped over
8444 cx.update_editor(|editor, window, cx| {
8445 editor.handle_input("}", window, cx);
8446 editor.move_left(&MoveLeft, window, cx);
8447 editor.handle_input(")", window, cx);
8448 editor.move_left(&MoveLeft, window, cx);
8449 });
8450 cx.assert_editor_state(
8451 &"
8452 ˇ)}
8453 ˇ)}
8454 ˇ)}
8455 "
8456 .unindent(),
8457 );
8458
8459 // skip-over closing brackets at multiple cursors
8460 cx.update_editor(|editor, window, cx| {
8461 editor.handle_input(")", window, cx);
8462 editor.handle_input("}", window, cx);
8463 });
8464 cx.assert_editor_state(
8465 &"
8466 )}ˇ
8467 )}ˇ
8468 )}ˇ
8469 "
8470 .unindent(),
8471 );
8472
8473 // ignore non-close brackets
8474 cx.update_editor(|editor, window, cx| {
8475 editor.handle_input("]", window, cx);
8476 editor.move_left(&MoveLeft, window, cx);
8477 editor.handle_input("]", window, cx);
8478 });
8479 cx.assert_editor_state(
8480 &"
8481 )}]ˇ]
8482 )}]ˇ]
8483 )}]ˇ]
8484 "
8485 .unindent(),
8486 );
8487}
8488
8489#[gpui::test]
8490async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8491 init_test(cx, |_| {});
8492
8493 let mut cx = EditorTestContext::new(cx).await;
8494
8495 let html_language = Arc::new(
8496 Language::new(
8497 LanguageConfig {
8498 name: "HTML".into(),
8499 brackets: BracketPairConfig {
8500 pairs: vec![
8501 BracketPair {
8502 start: "<".into(),
8503 end: ">".into(),
8504 close: true,
8505 ..Default::default()
8506 },
8507 BracketPair {
8508 start: "{".into(),
8509 end: "}".into(),
8510 close: true,
8511 ..Default::default()
8512 },
8513 BracketPair {
8514 start: "(".into(),
8515 end: ")".into(),
8516 close: true,
8517 ..Default::default()
8518 },
8519 ],
8520 ..Default::default()
8521 },
8522 autoclose_before: "})]>".into(),
8523 ..Default::default()
8524 },
8525 Some(tree_sitter_html::LANGUAGE.into()),
8526 )
8527 .with_injection_query(
8528 r#"
8529 (script_element
8530 (raw_text) @injection.content
8531 (#set! injection.language "javascript"))
8532 "#,
8533 )
8534 .unwrap(),
8535 );
8536
8537 let javascript_language = Arc::new(Language::new(
8538 LanguageConfig {
8539 name: "JavaScript".into(),
8540 brackets: BracketPairConfig {
8541 pairs: vec![
8542 BracketPair {
8543 start: "/*".into(),
8544 end: " */".into(),
8545 close: true,
8546 ..Default::default()
8547 },
8548 BracketPair {
8549 start: "{".into(),
8550 end: "}".into(),
8551 close: true,
8552 ..Default::default()
8553 },
8554 BracketPair {
8555 start: "(".into(),
8556 end: ")".into(),
8557 close: true,
8558 ..Default::default()
8559 },
8560 ],
8561 ..Default::default()
8562 },
8563 autoclose_before: "})]>".into(),
8564 ..Default::default()
8565 },
8566 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8567 ));
8568
8569 cx.language_registry().add(html_language.clone());
8570 cx.language_registry().add(javascript_language.clone());
8571
8572 cx.update_buffer(|buffer, cx| {
8573 buffer.set_language(Some(html_language), cx);
8574 });
8575
8576 cx.set_state(
8577 &r#"
8578 <body>ˇ
8579 <script>
8580 var x = 1;ˇ
8581 </script>
8582 </body>ˇ
8583 "#
8584 .unindent(),
8585 );
8586
8587 // Precondition: different languages are active at different locations.
8588 cx.update_editor(|editor, window, cx| {
8589 let snapshot = editor.snapshot(window, cx);
8590 let cursors = editor.selections.ranges::<usize>(cx);
8591 let languages = cursors
8592 .iter()
8593 .map(|c| snapshot.language_at(c.start).unwrap().name())
8594 .collect::<Vec<_>>();
8595 assert_eq!(
8596 languages,
8597 &["HTML".into(), "JavaScript".into(), "HTML".into()]
8598 );
8599 });
8600
8601 // Angle brackets autoclose in HTML, but not JavaScript.
8602 cx.update_editor(|editor, window, cx| {
8603 editor.handle_input("<", window, cx);
8604 editor.handle_input("a", window, cx);
8605 });
8606 cx.assert_editor_state(
8607 &r#"
8608 <body><aˇ>
8609 <script>
8610 var x = 1;<aˇ
8611 </script>
8612 </body><aˇ>
8613 "#
8614 .unindent(),
8615 );
8616
8617 // Curly braces and parens autoclose in both HTML and JavaScript.
8618 cx.update_editor(|editor, window, cx| {
8619 editor.handle_input(" b=", window, cx);
8620 editor.handle_input("{", window, cx);
8621 editor.handle_input("c", window, cx);
8622 editor.handle_input("(", window, cx);
8623 });
8624 cx.assert_editor_state(
8625 &r#"
8626 <body><a b={c(ˇ)}>
8627 <script>
8628 var x = 1;<a b={c(ˇ)}
8629 </script>
8630 </body><a b={c(ˇ)}>
8631 "#
8632 .unindent(),
8633 );
8634
8635 // Brackets that were already autoclosed are skipped.
8636 cx.update_editor(|editor, window, cx| {
8637 editor.handle_input(")", window, cx);
8638 editor.handle_input("d", window, cx);
8639 editor.handle_input("}", window, cx);
8640 });
8641 cx.assert_editor_state(
8642 &r#"
8643 <body><a b={c()d}ˇ>
8644 <script>
8645 var x = 1;<a b={c()d}ˇ
8646 </script>
8647 </body><a b={c()d}ˇ>
8648 "#
8649 .unindent(),
8650 );
8651 cx.update_editor(|editor, window, cx| {
8652 editor.handle_input(">", window, cx);
8653 });
8654 cx.assert_editor_state(
8655 &r#"
8656 <body><a b={c()d}>ˇ
8657 <script>
8658 var x = 1;<a b={c()d}>ˇ
8659 </script>
8660 </body><a b={c()d}>ˇ
8661 "#
8662 .unindent(),
8663 );
8664
8665 // Reset
8666 cx.set_state(
8667 &r#"
8668 <body>ˇ
8669 <script>
8670 var x = 1;ˇ
8671 </script>
8672 </body>ˇ
8673 "#
8674 .unindent(),
8675 );
8676
8677 cx.update_editor(|editor, window, cx| {
8678 editor.handle_input("<", window, cx);
8679 });
8680 cx.assert_editor_state(
8681 &r#"
8682 <body><ˇ>
8683 <script>
8684 var x = 1;<ˇ
8685 </script>
8686 </body><ˇ>
8687 "#
8688 .unindent(),
8689 );
8690
8691 // When backspacing, the closing angle brackets are removed.
8692 cx.update_editor(|editor, window, cx| {
8693 editor.backspace(&Backspace, window, cx);
8694 });
8695 cx.assert_editor_state(
8696 &r#"
8697 <body>ˇ
8698 <script>
8699 var x = 1;ˇ
8700 </script>
8701 </body>ˇ
8702 "#
8703 .unindent(),
8704 );
8705
8706 // Block comments autoclose in JavaScript, but not HTML.
8707 cx.update_editor(|editor, window, cx| {
8708 editor.handle_input("/", window, cx);
8709 editor.handle_input("*", window, cx);
8710 });
8711 cx.assert_editor_state(
8712 &r#"
8713 <body>/*ˇ
8714 <script>
8715 var x = 1;/*ˇ */
8716 </script>
8717 </body>/*ˇ
8718 "#
8719 .unindent(),
8720 );
8721}
8722
8723#[gpui::test]
8724async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
8725 init_test(cx, |_| {});
8726
8727 let mut cx = EditorTestContext::new(cx).await;
8728
8729 let rust_language = Arc::new(
8730 Language::new(
8731 LanguageConfig {
8732 name: "Rust".into(),
8733 brackets: serde_json::from_value(json!([
8734 { "start": "{", "end": "}", "close": true, "newline": true },
8735 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
8736 ]))
8737 .unwrap(),
8738 autoclose_before: "})]>".into(),
8739 ..Default::default()
8740 },
8741 Some(tree_sitter_rust::LANGUAGE.into()),
8742 )
8743 .with_override_query("(string_literal) @string")
8744 .unwrap(),
8745 );
8746
8747 cx.language_registry().add(rust_language.clone());
8748 cx.update_buffer(|buffer, cx| {
8749 buffer.set_language(Some(rust_language), cx);
8750 });
8751
8752 cx.set_state(
8753 &r#"
8754 let x = ˇ
8755 "#
8756 .unindent(),
8757 );
8758
8759 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
8760 cx.update_editor(|editor, window, cx| {
8761 editor.handle_input("\"", window, cx);
8762 });
8763 cx.assert_editor_state(
8764 &r#"
8765 let x = "ˇ"
8766 "#
8767 .unindent(),
8768 );
8769
8770 // Inserting another quotation mark. The cursor moves across the existing
8771 // automatically-inserted quotation mark.
8772 cx.update_editor(|editor, window, cx| {
8773 editor.handle_input("\"", window, cx);
8774 });
8775 cx.assert_editor_state(
8776 &r#"
8777 let x = ""ˇ
8778 "#
8779 .unindent(),
8780 );
8781
8782 // Reset
8783 cx.set_state(
8784 &r#"
8785 let x = ˇ
8786 "#
8787 .unindent(),
8788 );
8789
8790 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
8791 cx.update_editor(|editor, window, cx| {
8792 editor.handle_input("\"", window, cx);
8793 editor.handle_input(" ", window, cx);
8794 editor.move_left(&Default::default(), window, cx);
8795 editor.handle_input("\\", window, cx);
8796 editor.handle_input("\"", window, cx);
8797 });
8798 cx.assert_editor_state(
8799 &r#"
8800 let x = "\"ˇ "
8801 "#
8802 .unindent(),
8803 );
8804
8805 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8806 // mark. Nothing is inserted.
8807 cx.update_editor(|editor, window, cx| {
8808 editor.move_right(&Default::default(), window, cx);
8809 editor.handle_input("\"", window, cx);
8810 });
8811 cx.assert_editor_state(
8812 &r#"
8813 let x = "\" "ˇ
8814 "#
8815 .unindent(),
8816 );
8817}
8818
8819#[gpui::test]
8820async fn test_surround_with_pair(cx: &mut TestAppContext) {
8821 init_test(cx, |_| {});
8822
8823 let language = Arc::new(Language::new(
8824 LanguageConfig {
8825 brackets: BracketPairConfig {
8826 pairs: vec![
8827 BracketPair {
8828 start: "{".to_string(),
8829 end: "}".to_string(),
8830 close: true,
8831 surround: true,
8832 newline: true,
8833 },
8834 BracketPair {
8835 start: "/* ".to_string(),
8836 end: "*/".to_string(),
8837 close: true,
8838 surround: true,
8839 ..Default::default()
8840 },
8841 ],
8842 ..Default::default()
8843 },
8844 ..Default::default()
8845 },
8846 Some(tree_sitter_rust::LANGUAGE.into()),
8847 ));
8848
8849 let text = r#"
8850 a
8851 b
8852 c
8853 "#
8854 .unindent();
8855
8856 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8857 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8858 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8859 editor
8860 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8861 .await;
8862
8863 editor.update_in(cx, |editor, window, cx| {
8864 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8865 s.select_display_ranges([
8866 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8867 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8868 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8869 ])
8870 });
8871
8872 editor.handle_input("{", window, cx);
8873 editor.handle_input("{", window, cx);
8874 editor.handle_input("{", window, cx);
8875 assert_eq!(
8876 editor.text(cx),
8877 "
8878 {{{a}}}
8879 {{{b}}}
8880 {{{c}}}
8881 "
8882 .unindent()
8883 );
8884 assert_eq!(
8885 editor.selections.display_ranges(cx),
8886 [
8887 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
8888 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
8889 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
8890 ]
8891 );
8892
8893 editor.undo(&Undo, window, cx);
8894 editor.undo(&Undo, window, cx);
8895 editor.undo(&Undo, window, cx);
8896 assert_eq!(
8897 editor.text(cx),
8898 "
8899 a
8900 b
8901 c
8902 "
8903 .unindent()
8904 );
8905 assert_eq!(
8906 editor.selections.display_ranges(cx),
8907 [
8908 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8909 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8910 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8911 ]
8912 );
8913
8914 // Ensure inserting the first character of a multi-byte bracket pair
8915 // doesn't surround the selections with the bracket.
8916 editor.handle_input("/", window, cx);
8917 assert_eq!(
8918 editor.text(cx),
8919 "
8920 /
8921 /
8922 /
8923 "
8924 .unindent()
8925 );
8926 assert_eq!(
8927 editor.selections.display_ranges(cx),
8928 [
8929 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8930 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8931 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8932 ]
8933 );
8934
8935 editor.undo(&Undo, window, cx);
8936 assert_eq!(
8937 editor.text(cx),
8938 "
8939 a
8940 b
8941 c
8942 "
8943 .unindent()
8944 );
8945 assert_eq!(
8946 editor.selections.display_ranges(cx),
8947 [
8948 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8949 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8950 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8951 ]
8952 );
8953
8954 // Ensure inserting the last character of a multi-byte bracket pair
8955 // doesn't surround the selections with the bracket.
8956 editor.handle_input("*", window, cx);
8957 assert_eq!(
8958 editor.text(cx),
8959 "
8960 *
8961 *
8962 *
8963 "
8964 .unindent()
8965 );
8966 assert_eq!(
8967 editor.selections.display_ranges(cx),
8968 [
8969 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8970 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8971 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8972 ]
8973 );
8974 });
8975}
8976
8977#[gpui::test]
8978async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
8979 init_test(cx, |_| {});
8980
8981 let language = Arc::new(Language::new(
8982 LanguageConfig {
8983 brackets: BracketPairConfig {
8984 pairs: vec![BracketPair {
8985 start: "{".to_string(),
8986 end: "}".to_string(),
8987 close: true,
8988 surround: true,
8989 newline: true,
8990 }],
8991 ..Default::default()
8992 },
8993 autoclose_before: "}".to_string(),
8994 ..Default::default()
8995 },
8996 Some(tree_sitter_rust::LANGUAGE.into()),
8997 ));
8998
8999 let text = r#"
9000 a
9001 b
9002 c
9003 "#
9004 .unindent();
9005
9006 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9007 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9008 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9009 editor
9010 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9011 .await;
9012
9013 editor.update_in(cx, |editor, window, cx| {
9014 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9015 s.select_ranges([
9016 Point::new(0, 1)..Point::new(0, 1),
9017 Point::new(1, 1)..Point::new(1, 1),
9018 Point::new(2, 1)..Point::new(2, 1),
9019 ])
9020 });
9021
9022 editor.handle_input("{", window, cx);
9023 editor.handle_input("{", window, cx);
9024 editor.handle_input("_", window, cx);
9025 assert_eq!(
9026 editor.text(cx),
9027 "
9028 a{{_}}
9029 b{{_}}
9030 c{{_}}
9031 "
9032 .unindent()
9033 );
9034 assert_eq!(
9035 editor.selections.ranges::<Point>(cx),
9036 [
9037 Point::new(0, 4)..Point::new(0, 4),
9038 Point::new(1, 4)..Point::new(1, 4),
9039 Point::new(2, 4)..Point::new(2, 4)
9040 ]
9041 );
9042
9043 editor.backspace(&Default::default(), window, cx);
9044 editor.backspace(&Default::default(), window, cx);
9045 assert_eq!(
9046 editor.text(cx),
9047 "
9048 a{}
9049 b{}
9050 c{}
9051 "
9052 .unindent()
9053 );
9054 assert_eq!(
9055 editor.selections.ranges::<Point>(cx),
9056 [
9057 Point::new(0, 2)..Point::new(0, 2),
9058 Point::new(1, 2)..Point::new(1, 2),
9059 Point::new(2, 2)..Point::new(2, 2)
9060 ]
9061 );
9062
9063 editor.delete_to_previous_word_start(&Default::default(), window, cx);
9064 assert_eq!(
9065 editor.text(cx),
9066 "
9067 a
9068 b
9069 c
9070 "
9071 .unindent()
9072 );
9073 assert_eq!(
9074 editor.selections.ranges::<Point>(cx),
9075 [
9076 Point::new(0, 1)..Point::new(0, 1),
9077 Point::new(1, 1)..Point::new(1, 1),
9078 Point::new(2, 1)..Point::new(2, 1)
9079 ]
9080 );
9081 });
9082}
9083
9084#[gpui::test]
9085async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
9086 init_test(cx, |settings| {
9087 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9088 });
9089
9090 let mut cx = EditorTestContext::new(cx).await;
9091
9092 let language = Arc::new(Language::new(
9093 LanguageConfig {
9094 brackets: BracketPairConfig {
9095 pairs: vec![
9096 BracketPair {
9097 start: "{".to_string(),
9098 end: "}".to_string(),
9099 close: true,
9100 surround: true,
9101 newline: true,
9102 },
9103 BracketPair {
9104 start: "(".to_string(),
9105 end: ")".to_string(),
9106 close: true,
9107 surround: true,
9108 newline: true,
9109 },
9110 BracketPair {
9111 start: "[".to_string(),
9112 end: "]".to_string(),
9113 close: false,
9114 surround: true,
9115 newline: true,
9116 },
9117 ],
9118 ..Default::default()
9119 },
9120 autoclose_before: "})]".to_string(),
9121 ..Default::default()
9122 },
9123 Some(tree_sitter_rust::LANGUAGE.into()),
9124 ));
9125
9126 cx.language_registry().add(language.clone());
9127 cx.update_buffer(|buffer, cx| {
9128 buffer.set_language(Some(language), cx);
9129 });
9130
9131 cx.set_state(
9132 &"
9133 {(ˇ)}
9134 [[ˇ]]
9135 {(ˇ)}
9136 "
9137 .unindent(),
9138 );
9139
9140 cx.update_editor(|editor, window, cx| {
9141 editor.backspace(&Default::default(), window, cx);
9142 editor.backspace(&Default::default(), window, cx);
9143 });
9144
9145 cx.assert_editor_state(
9146 &"
9147 ˇ
9148 ˇ]]
9149 ˇ
9150 "
9151 .unindent(),
9152 );
9153
9154 cx.update_editor(|editor, window, cx| {
9155 editor.handle_input("{", window, cx);
9156 editor.handle_input("{", window, cx);
9157 editor.move_right(&MoveRight, window, cx);
9158 editor.move_right(&MoveRight, window, cx);
9159 editor.move_left(&MoveLeft, window, cx);
9160 editor.move_left(&MoveLeft, window, cx);
9161 editor.backspace(&Default::default(), window, cx);
9162 });
9163
9164 cx.assert_editor_state(
9165 &"
9166 {ˇ}
9167 {ˇ}]]
9168 {ˇ}
9169 "
9170 .unindent(),
9171 );
9172
9173 cx.update_editor(|editor, window, cx| {
9174 editor.backspace(&Default::default(), window, cx);
9175 });
9176
9177 cx.assert_editor_state(
9178 &"
9179 ˇ
9180 ˇ]]
9181 ˇ
9182 "
9183 .unindent(),
9184 );
9185}
9186
9187#[gpui::test]
9188async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
9189 init_test(cx, |_| {});
9190
9191 let language = Arc::new(Language::new(
9192 LanguageConfig::default(),
9193 Some(tree_sitter_rust::LANGUAGE.into()),
9194 ));
9195
9196 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
9197 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9198 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9199 editor
9200 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9201 .await;
9202
9203 editor.update_in(cx, |editor, window, cx| {
9204 editor.set_auto_replace_emoji_shortcode(true);
9205
9206 editor.handle_input("Hello ", window, cx);
9207 editor.handle_input(":wave", window, cx);
9208 assert_eq!(editor.text(cx), "Hello :wave".unindent());
9209
9210 editor.handle_input(":", window, cx);
9211 assert_eq!(editor.text(cx), "Hello 👋".unindent());
9212
9213 editor.handle_input(" :smile", window, cx);
9214 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
9215
9216 editor.handle_input(":", window, cx);
9217 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
9218
9219 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
9220 editor.handle_input(":wave", window, cx);
9221 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
9222
9223 editor.handle_input(":", window, cx);
9224 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
9225
9226 editor.handle_input(":1", window, cx);
9227 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
9228
9229 editor.handle_input(":", window, cx);
9230 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
9231
9232 // Ensure shortcode does not get replaced when it is part of a word
9233 editor.handle_input(" Test:wave", window, cx);
9234 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
9235
9236 editor.handle_input(":", window, cx);
9237 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
9238
9239 editor.set_auto_replace_emoji_shortcode(false);
9240
9241 // Ensure shortcode does not get replaced when auto replace is off
9242 editor.handle_input(" :wave", window, cx);
9243 assert_eq!(
9244 editor.text(cx),
9245 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
9246 );
9247
9248 editor.handle_input(":", window, cx);
9249 assert_eq!(
9250 editor.text(cx),
9251 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
9252 );
9253 });
9254}
9255
9256#[gpui::test]
9257async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
9258 init_test(cx, |_| {});
9259
9260 let (text, insertion_ranges) = marked_text_ranges(
9261 indoc! {"
9262 ˇ
9263 "},
9264 false,
9265 );
9266
9267 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
9268 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9269
9270 _ = editor.update_in(cx, |editor, window, cx| {
9271 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
9272
9273 editor
9274 .insert_snippet(&insertion_ranges, snippet, window, cx)
9275 .unwrap();
9276
9277 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
9278 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
9279 assert_eq!(editor.text(cx), expected_text);
9280 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
9281 }
9282
9283 assert(
9284 editor,
9285 cx,
9286 indoc! {"
9287 type «» =•
9288 "},
9289 );
9290
9291 assert!(editor.context_menu_visible(), "There should be a matches");
9292 });
9293}
9294
9295#[gpui::test]
9296async fn test_snippets(cx: &mut TestAppContext) {
9297 init_test(cx, |_| {});
9298
9299 let mut cx = EditorTestContext::new(cx).await;
9300
9301 cx.set_state(indoc! {"
9302 a.ˇ b
9303 a.ˇ b
9304 a.ˇ b
9305 "});
9306
9307 cx.update_editor(|editor, window, cx| {
9308 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
9309 let insertion_ranges = editor
9310 .selections
9311 .all(cx)
9312 .iter()
9313 .map(|s| s.range().clone())
9314 .collect::<Vec<_>>();
9315 editor
9316 .insert_snippet(&insertion_ranges, snippet, window, cx)
9317 .unwrap();
9318 });
9319
9320 cx.assert_editor_state(indoc! {"
9321 a.f(«oneˇ», two, «threeˇ») b
9322 a.f(«oneˇ», two, «threeˇ») b
9323 a.f(«oneˇ», two, «threeˇ») b
9324 "});
9325
9326 // Can't move earlier than the first tab stop
9327 cx.update_editor(|editor, window, cx| {
9328 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9329 });
9330 cx.assert_editor_state(indoc! {"
9331 a.f(«oneˇ», two, «threeˇ») b
9332 a.f(«oneˇ», two, «threeˇ») b
9333 a.f(«oneˇ», two, «threeˇ») b
9334 "});
9335
9336 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9337 cx.assert_editor_state(indoc! {"
9338 a.f(one, «twoˇ», three) b
9339 a.f(one, «twoˇ», three) b
9340 a.f(one, «twoˇ», three) b
9341 "});
9342
9343 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
9344 cx.assert_editor_state(indoc! {"
9345 a.f(«oneˇ», two, «threeˇ») b
9346 a.f(«oneˇ», two, «threeˇ») b
9347 a.f(«oneˇ», two, «threeˇ») b
9348 "});
9349
9350 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9351 cx.assert_editor_state(indoc! {"
9352 a.f(one, «twoˇ», three) b
9353 a.f(one, «twoˇ», three) b
9354 a.f(one, «twoˇ», three) b
9355 "});
9356 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9357 cx.assert_editor_state(indoc! {"
9358 a.f(one, two, three)ˇ b
9359 a.f(one, two, three)ˇ b
9360 a.f(one, two, three)ˇ b
9361 "});
9362
9363 // As soon as the last tab stop is reached, snippet state is gone
9364 cx.update_editor(|editor, window, cx| {
9365 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9366 });
9367 cx.assert_editor_state(indoc! {"
9368 a.f(one, two, three)ˇ b
9369 a.f(one, two, three)ˇ b
9370 a.f(one, two, three)ˇ b
9371 "});
9372}
9373
9374#[gpui::test]
9375async fn test_snippet_indentation(cx: &mut TestAppContext) {
9376 init_test(cx, |_| {});
9377
9378 let mut cx = EditorTestContext::new(cx).await;
9379
9380 cx.update_editor(|editor, window, cx| {
9381 let snippet = Snippet::parse(indoc! {"
9382 /*
9383 * Multiline comment with leading indentation
9384 *
9385 * $1
9386 */
9387 $0"})
9388 .unwrap();
9389 let insertion_ranges = editor
9390 .selections
9391 .all(cx)
9392 .iter()
9393 .map(|s| s.range().clone())
9394 .collect::<Vec<_>>();
9395 editor
9396 .insert_snippet(&insertion_ranges, snippet, window, cx)
9397 .unwrap();
9398 });
9399
9400 cx.assert_editor_state(indoc! {"
9401 /*
9402 * Multiline comment with leading indentation
9403 *
9404 * ˇ
9405 */
9406 "});
9407
9408 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9409 cx.assert_editor_state(indoc! {"
9410 /*
9411 * Multiline comment with leading indentation
9412 *
9413 *•
9414 */
9415 ˇ"});
9416}
9417
9418#[gpui::test]
9419async fn test_document_format_during_save(cx: &mut TestAppContext) {
9420 init_test(cx, |_| {});
9421
9422 let fs = FakeFs::new(cx.executor());
9423 fs.insert_file(path!("/file.rs"), Default::default()).await;
9424
9425 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
9426
9427 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9428 language_registry.add(rust_lang());
9429 let mut fake_servers = language_registry.register_fake_lsp(
9430 "Rust",
9431 FakeLspAdapter {
9432 capabilities: lsp::ServerCapabilities {
9433 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9434 ..Default::default()
9435 },
9436 ..Default::default()
9437 },
9438 );
9439
9440 let buffer = project
9441 .update(cx, |project, cx| {
9442 project.open_local_buffer(path!("/file.rs"), cx)
9443 })
9444 .await
9445 .unwrap();
9446
9447 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9448 let (editor, cx) = cx.add_window_view(|window, cx| {
9449 build_editor_with_project(project.clone(), buffer, window, cx)
9450 });
9451 editor.update_in(cx, |editor, window, cx| {
9452 editor.set_text("one\ntwo\nthree\n", window, cx)
9453 });
9454 assert!(cx.read(|cx| editor.is_dirty(cx)));
9455
9456 cx.executor().start_waiting();
9457 let fake_server = fake_servers.next().await.unwrap();
9458
9459 {
9460 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9461 move |params, _| async move {
9462 assert_eq!(
9463 params.text_document.uri,
9464 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9465 );
9466 assert_eq!(params.options.tab_size, 4);
9467 Ok(Some(vec![lsp::TextEdit::new(
9468 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9469 ", ".to_string(),
9470 )]))
9471 },
9472 );
9473 let save = editor
9474 .update_in(cx, |editor, window, cx| {
9475 editor.save(
9476 SaveOptions {
9477 format: true,
9478 autosave: false,
9479 },
9480 project.clone(),
9481 window,
9482 cx,
9483 )
9484 })
9485 .unwrap();
9486 cx.executor().start_waiting();
9487 save.await;
9488
9489 assert_eq!(
9490 editor.update(cx, |editor, cx| editor.text(cx)),
9491 "one, two\nthree\n"
9492 );
9493 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9494 }
9495
9496 {
9497 editor.update_in(cx, |editor, window, cx| {
9498 editor.set_text("one\ntwo\nthree\n", window, cx)
9499 });
9500 assert!(cx.read(|cx| editor.is_dirty(cx)));
9501
9502 // Ensure we can still save even if formatting hangs.
9503 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9504 move |params, _| async move {
9505 assert_eq!(
9506 params.text_document.uri,
9507 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9508 );
9509 futures::future::pending::<()>().await;
9510 unreachable!()
9511 },
9512 );
9513 let save = editor
9514 .update_in(cx, |editor, window, cx| {
9515 editor.save(
9516 SaveOptions {
9517 format: true,
9518 autosave: false,
9519 },
9520 project.clone(),
9521 window,
9522 cx,
9523 )
9524 })
9525 .unwrap();
9526 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9527 cx.executor().start_waiting();
9528 save.await;
9529 assert_eq!(
9530 editor.update(cx, |editor, cx| editor.text(cx)),
9531 "one\ntwo\nthree\n"
9532 );
9533 }
9534
9535 // Set rust language override and assert overridden tabsize is sent to language server
9536 update_test_language_settings(cx, |settings| {
9537 settings.languages.0.insert(
9538 "Rust".into(),
9539 LanguageSettingsContent {
9540 tab_size: NonZeroU32::new(8),
9541 ..Default::default()
9542 },
9543 );
9544 });
9545
9546 {
9547 editor.update_in(cx, |editor, window, cx| {
9548 editor.set_text("somehting_new\n", window, cx)
9549 });
9550 assert!(cx.read(|cx| editor.is_dirty(cx)));
9551 let _formatting_request_signal = fake_server
9552 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9553 assert_eq!(
9554 params.text_document.uri,
9555 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9556 );
9557 assert_eq!(params.options.tab_size, 8);
9558 Ok(Some(vec![]))
9559 });
9560 let save = editor
9561 .update_in(cx, |editor, window, cx| {
9562 editor.save(
9563 SaveOptions {
9564 format: true,
9565 autosave: false,
9566 },
9567 project.clone(),
9568 window,
9569 cx,
9570 )
9571 })
9572 .unwrap();
9573 cx.executor().start_waiting();
9574 save.await;
9575 }
9576}
9577
9578#[gpui::test]
9579async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
9580 init_test(cx, |settings| {
9581 settings.defaults.ensure_final_newline_on_save = Some(false);
9582 });
9583
9584 let fs = FakeFs::new(cx.executor());
9585 fs.insert_file(path!("/file.txt"), "foo".into()).await;
9586
9587 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
9588
9589 let buffer = project
9590 .update(cx, |project, cx| {
9591 project.open_local_buffer(path!("/file.txt"), cx)
9592 })
9593 .await
9594 .unwrap();
9595
9596 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9597 let (editor, cx) = cx.add_window_view(|window, cx| {
9598 build_editor_with_project(project.clone(), buffer, window, cx)
9599 });
9600 editor.update_in(cx, |editor, window, cx| {
9601 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9602 s.select_ranges([0..0])
9603 });
9604 });
9605 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9606
9607 editor.update_in(cx, |editor, window, cx| {
9608 editor.handle_input("\n", window, cx)
9609 });
9610 cx.run_until_parked();
9611 save(&editor, &project, cx).await;
9612 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9613
9614 editor.update_in(cx, |editor, window, cx| {
9615 editor.undo(&Default::default(), window, cx);
9616 });
9617 save(&editor, &project, cx).await;
9618 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9619
9620 editor.update_in(cx, |editor, window, cx| {
9621 editor.redo(&Default::default(), window, cx);
9622 });
9623 cx.run_until_parked();
9624 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9625
9626 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
9627 let save = editor
9628 .update_in(cx, |editor, window, cx| {
9629 editor.save(
9630 SaveOptions {
9631 format: true,
9632 autosave: false,
9633 },
9634 project.clone(),
9635 window,
9636 cx,
9637 )
9638 })
9639 .unwrap();
9640 cx.executor().start_waiting();
9641 save.await;
9642 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9643 }
9644}
9645
9646#[gpui::test]
9647async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9648 init_test(cx, |_| {});
9649
9650 let cols = 4;
9651 let rows = 10;
9652 let sample_text_1 = sample_text(rows, cols, 'a');
9653 assert_eq!(
9654 sample_text_1,
9655 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9656 );
9657 let sample_text_2 = sample_text(rows, cols, 'l');
9658 assert_eq!(
9659 sample_text_2,
9660 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9661 );
9662 let sample_text_3 = sample_text(rows, cols, 'v');
9663 assert_eq!(
9664 sample_text_3,
9665 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9666 );
9667
9668 let fs = FakeFs::new(cx.executor());
9669 fs.insert_tree(
9670 path!("/a"),
9671 json!({
9672 "main.rs": sample_text_1,
9673 "other.rs": sample_text_2,
9674 "lib.rs": sample_text_3,
9675 }),
9676 )
9677 .await;
9678
9679 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9680 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9681 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9682
9683 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9684 language_registry.add(rust_lang());
9685 let mut fake_servers = language_registry.register_fake_lsp(
9686 "Rust",
9687 FakeLspAdapter {
9688 capabilities: lsp::ServerCapabilities {
9689 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9690 ..Default::default()
9691 },
9692 ..Default::default()
9693 },
9694 );
9695
9696 let worktree = project.update(cx, |project, cx| {
9697 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
9698 assert_eq!(worktrees.len(), 1);
9699 worktrees.pop().unwrap()
9700 });
9701 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9702
9703 let buffer_1 = project
9704 .update(cx, |project, cx| {
9705 project.open_buffer((worktree_id, "main.rs"), cx)
9706 })
9707 .await
9708 .unwrap();
9709 let buffer_2 = project
9710 .update(cx, |project, cx| {
9711 project.open_buffer((worktree_id, "other.rs"), cx)
9712 })
9713 .await
9714 .unwrap();
9715 let buffer_3 = project
9716 .update(cx, |project, cx| {
9717 project.open_buffer((worktree_id, "lib.rs"), cx)
9718 })
9719 .await
9720 .unwrap();
9721
9722 let multi_buffer = cx.new(|cx| {
9723 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9724 multi_buffer.push_excerpts(
9725 buffer_1.clone(),
9726 [
9727 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9728 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9729 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9730 ],
9731 cx,
9732 );
9733 multi_buffer.push_excerpts(
9734 buffer_2.clone(),
9735 [
9736 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9737 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9738 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9739 ],
9740 cx,
9741 );
9742 multi_buffer.push_excerpts(
9743 buffer_3.clone(),
9744 [
9745 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9746 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9747 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9748 ],
9749 cx,
9750 );
9751 multi_buffer
9752 });
9753 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
9754 Editor::new(
9755 EditorMode::full(),
9756 multi_buffer,
9757 Some(project.clone()),
9758 window,
9759 cx,
9760 )
9761 });
9762
9763 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9764 editor.change_selections(
9765 SelectionEffects::scroll(Autoscroll::Next),
9766 window,
9767 cx,
9768 |s| s.select_ranges(Some(1..2)),
9769 );
9770 editor.insert("|one|two|three|", window, cx);
9771 });
9772 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9773 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9774 editor.change_selections(
9775 SelectionEffects::scroll(Autoscroll::Next),
9776 window,
9777 cx,
9778 |s| s.select_ranges(Some(60..70)),
9779 );
9780 editor.insert("|four|five|six|", window, cx);
9781 });
9782 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9783
9784 // First two buffers should be edited, but not the third one.
9785 assert_eq!(
9786 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9787 "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}",
9788 );
9789 buffer_1.update(cx, |buffer, _| {
9790 assert!(buffer.is_dirty());
9791 assert_eq!(
9792 buffer.text(),
9793 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
9794 )
9795 });
9796 buffer_2.update(cx, |buffer, _| {
9797 assert!(buffer.is_dirty());
9798 assert_eq!(
9799 buffer.text(),
9800 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
9801 )
9802 });
9803 buffer_3.update(cx, |buffer, _| {
9804 assert!(!buffer.is_dirty());
9805 assert_eq!(buffer.text(), sample_text_3,)
9806 });
9807 cx.executor().run_until_parked();
9808
9809 cx.executor().start_waiting();
9810 let save = multi_buffer_editor
9811 .update_in(cx, |editor, window, cx| {
9812 editor.save(
9813 SaveOptions {
9814 format: true,
9815 autosave: false,
9816 },
9817 project.clone(),
9818 window,
9819 cx,
9820 )
9821 })
9822 .unwrap();
9823
9824 let fake_server = fake_servers.next().await.unwrap();
9825 fake_server
9826 .server
9827 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
9828 Ok(Some(vec![lsp::TextEdit::new(
9829 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9830 format!("[{} formatted]", params.text_document.uri),
9831 )]))
9832 })
9833 .detach();
9834 save.await;
9835
9836 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
9837 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
9838 assert_eq!(
9839 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9840 uri!(
9841 "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}"
9842 ),
9843 );
9844 buffer_1.update(cx, |buffer, _| {
9845 assert!(!buffer.is_dirty());
9846 assert_eq!(
9847 buffer.text(),
9848 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
9849 )
9850 });
9851 buffer_2.update(cx, |buffer, _| {
9852 assert!(!buffer.is_dirty());
9853 assert_eq!(
9854 buffer.text(),
9855 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
9856 )
9857 });
9858 buffer_3.update(cx, |buffer, _| {
9859 assert!(!buffer.is_dirty());
9860 assert_eq!(buffer.text(), sample_text_3,)
9861 });
9862}
9863
9864#[gpui::test]
9865async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
9866 init_test(cx, |_| {});
9867
9868 let fs = FakeFs::new(cx.executor());
9869 fs.insert_tree(
9870 path!("/dir"),
9871 json!({
9872 "file1.rs": "fn main() { println!(\"hello\"); }",
9873 "file2.rs": "fn test() { println!(\"test\"); }",
9874 "file3.rs": "fn other() { println!(\"other\"); }\n",
9875 }),
9876 )
9877 .await;
9878
9879 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
9880 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9881 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9882
9883 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9884 language_registry.add(rust_lang());
9885
9886 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
9887 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9888
9889 // Open three buffers
9890 let buffer_1 = project
9891 .update(cx, |project, cx| {
9892 project.open_buffer((worktree_id, "file1.rs"), cx)
9893 })
9894 .await
9895 .unwrap();
9896 let buffer_2 = project
9897 .update(cx, |project, cx| {
9898 project.open_buffer((worktree_id, "file2.rs"), cx)
9899 })
9900 .await
9901 .unwrap();
9902 let buffer_3 = project
9903 .update(cx, |project, cx| {
9904 project.open_buffer((worktree_id, "file3.rs"), cx)
9905 })
9906 .await
9907 .unwrap();
9908
9909 // Create a multi-buffer with all three buffers
9910 let multi_buffer = cx.new(|cx| {
9911 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9912 multi_buffer.push_excerpts(
9913 buffer_1.clone(),
9914 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9915 cx,
9916 );
9917 multi_buffer.push_excerpts(
9918 buffer_2.clone(),
9919 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9920 cx,
9921 );
9922 multi_buffer.push_excerpts(
9923 buffer_3.clone(),
9924 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9925 cx,
9926 );
9927 multi_buffer
9928 });
9929
9930 let editor = cx.new_window_entity(|window, cx| {
9931 Editor::new(
9932 EditorMode::full(),
9933 multi_buffer,
9934 Some(project.clone()),
9935 window,
9936 cx,
9937 )
9938 });
9939
9940 // Edit only the first buffer
9941 editor.update_in(cx, |editor, window, cx| {
9942 editor.change_selections(
9943 SelectionEffects::scroll(Autoscroll::Next),
9944 window,
9945 cx,
9946 |s| s.select_ranges(Some(10..10)),
9947 );
9948 editor.insert("// edited", window, cx);
9949 });
9950
9951 // Verify that only buffer 1 is dirty
9952 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
9953 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9954 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9955
9956 // Get write counts after file creation (files were created with initial content)
9957 // We expect each file to have been written once during creation
9958 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
9959 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
9960 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
9961
9962 // Perform autosave
9963 let save_task = editor.update_in(cx, |editor, window, cx| {
9964 editor.save(
9965 SaveOptions {
9966 format: true,
9967 autosave: true,
9968 },
9969 project.clone(),
9970 window,
9971 cx,
9972 )
9973 });
9974 save_task.await.unwrap();
9975
9976 // Only the dirty buffer should have been saved
9977 assert_eq!(
9978 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9979 1,
9980 "Buffer 1 was dirty, so it should have been written once during autosave"
9981 );
9982 assert_eq!(
9983 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9984 0,
9985 "Buffer 2 was clean, so it should not have been written during autosave"
9986 );
9987 assert_eq!(
9988 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9989 0,
9990 "Buffer 3 was clean, so it should not have been written during autosave"
9991 );
9992
9993 // Verify buffer states after autosave
9994 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9995 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9996 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9997
9998 // Now perform a manual save (format = true)
9999 let save_task = editor.update_in(cx, |editor, window, cx| {
10000 editor.save(
10001 SaveOptions {
10002 format: true,
10003 autosave: false,
10004 },
10005 project.clone(),
10006 window,
10007 cx,
10008 )
10009 });
10010 save_task.await.unwrap();
10011
10012 // During manual save, clean buffers don't get written to disk
10013 // They just get did_save called for language server notifications
10014 assert_eq!(
10015 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
10016 1,
10017 "Buffer 1 should only have been written once total (during autosave, not manual save)"
10018 );
10019 assert_eq!(
10020 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
10021 0,
10022 "Buffer 2 should not have been written at all"
10023 );
10024 assert_eq!(
10025 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
10026 0,
10027 "Buffer 3 should not have been written at all"
10028 );
10029}
10030
10031async fn setup_range_format_test(
10032 cx: &mut TestAppContext,
10033) -> (
10034 Entity<Project>,
10035 Entity<Editor>,
10036 &mut gpui::VisualTestContext,
10037 lsp::FakeLanguageServer,
10038) {
10039 init_test(cx, |_| {});
10040
10041 let fs = FakeFs::new(cx.executor());
10042 fs.insert_file(path!("/file.rs"), Default::default()).await;
10043
10044 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10045
10046 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10047 language_registry.add(rust_lang());
10048 let mut fake_servers = language_registry.register_fake_lsp(
10049 "Rust",
10050 FakeLspAdapter {
10051 capabilities: lsp::ServerCapabilities {
10052 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
10053 ..lsp::ServerCapabilities::default()
10054 },
10055 ..FakeLspAdapter::default()
10056 },
10057 );
10058
10059 let buffer = project
10060 .update(cx, |project, cx| {
10061 project.open_local_buffer(path!("/file.rs"), cx)
10062 })
10063 .await
10064 .unwrap();
10065
10066 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10067 let (editor, cx) = cx.add_window_view(|window, cx| {
10068 build_editor_with_project(project.clone(), buffer, window, cx)
10069 });
10070
10071 cx.executor().start_waiting();
10072 let fake_server = fake_servers.next().await.unwrap();
10073
10074 (project, editor, cx, fake_server)
10075}
10076
10077#[gpui::test]
10078async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
10079 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10080
10081 editor.update_in(cx, |editor, window, cx| {
10082 editor.set_text("one\ntwo\nthree\n", window, cx)
10083 });
10084 assert!(cx.read(|cx| editor.is_dirty(cx)));
10085
10086 let save = editor
10087 .update_in(cx, |editor, window, cx| {
10088 editor.save(
10089 SaveOptions {
10090 format: true,
10091 autosave: false,
10092 },
10093 project.clone(),
10094 window,
10095 cx,
10096 )
10097 })
10098 .unwrap();
10099 fake_server
10100 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10101 assert_eq!(
10102 params.text_document.uri,
10103 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10104 );
10105 assert_eq!(params.options.tab_size, 4);
10106 Ok(Some(vec![lsp::TextEdit::new(
10107 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10108 ", ".to_string(),
10109 )]))
10110 })
10111 .next()
10112 .await;
10113 cx.executor().start_waiting();
10114 save.await;
10115 assert_eq!(
10116 editor.update(cx, |editor, cx| editor.text(cx)),
10117 "one, two\nthree\n"
10118 );
10119 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10120}
10121
10122#[gpui::test]
10123async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
10124 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10125
10126 editor.update_in(cx, |editor, window, cx| {
10127 editor.set_text("one\ntwo\nthree\n", window, cx)
10128 });
10129 assert!(cx.read(|cx| editor.is_dirty(cx)));
10130
10131 // Test that save still works when formatting hangs
10132 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
10133 move |params, _| async move {
10134 assert_eq!(
10135 params.text_document.uri,
10136 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10137 );
10138 futures::future::pending::<()>().await;
10139 unreachable!()
10140 },
10141 );
10142 let save = editor
10143 .update_in(cx, |editor, window, cx| {
10144 editor.save(
10145 SaveOptions {
10146 format: true,
10147 autosave: false,
10148 },
10149 project.clone(),
10150 window,
10151 cx,
10152 )
10153 })
10154 .unwrap();
10155 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10156 cx.executor().start_waiting();
10157 save.await;
10158 assert_eq!(
10159 editor.update(cx, |editor, cx| editor.text(cx)),
10160 "one\ntwo\nthree\n"
10161 );
10162 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10163}
10164
10165#[gpui::test]
10166async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
10167 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10168
10169 // Buffer starts clean, no formatting should be requested
10170 let save = editor
10171 .update_in(cx, |editor, window, cx| {
10172 editor.save(
10173 SaveOptions {
10174 format: false,
10175 autosave: false,
10176 },
10177 project.clone(),
10178 window,
10179 cx,
10180 )
10181 })
10182 .unwrap();
10183 let _pending_format_request = fake_server
10184 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
10185 panic!("Should not be invoked");
10186 })
10187 .next();
10188 cx.executor().start_waiting();
10189 save.await;
10190 cx.run_until_parked();
10191}
10192
10193#[gpui::test]
10194async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
10195 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10196
10197 // Set Rust language override and assert overridden tabsize is sent to language server
10198 update_test_language_settings(cx, |settings| {
10199 settings.languages.0.insert(
10200 "Rust".into(),
10201 LanguageSettingsContent {
10202 tab_size: NonZeroU32::new(8),
10203 ..Default::default()
10204 },
10205 );
10206 });
10207
10208 editor.update_in(cx, |editor, window, cx| {
10209 editor.set_text("something_new\n", window, cx)
10210 });
10211 assert!(cx.read(|cx| editor.is_dirty(cx)));
10212 let save = editor
10213 .update_in(cx, |editor, window, cx| {
10214 editor.save(
10215 SaveOptions {
10216 format: true,
10217 autosave: false,
10218 },
10219 project.clone(),
10220 window,
10221 cx,
10222 )
10223 })
10224 .unwrap();
10225 fake_server
10226 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10227 assert_eq!(
10228 params.text_document.uri,
10229 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10230 );
10231 assert_eq!(params.options.tab_size, 8);
10232 Ok(Some(Vec::new()))
10233 })
10234 .next()
10235 .await;
10236 save.await;
10237}
10238
10239#[gpui::test]
10240async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
10241 init_test(cx, |settings| {
10242 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
10243 Formatter::LanguageServer { name: None },
10244 )))
10245 });
10246
10247 let fs = FakeFs::new(cx.executor());
10248 fs.insert_file(path!("/file.rs"), Default::default()).await;
10249
10250 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10251
10252 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10253 language_registry.add(Arc::new(Language::new(
10254 LanguageConfig {
10255 name: "Rust".into(),
10256 matcher: LanguageMatcher {
10257 path_suffixes: vec!["rs".to_string()],
10258 ..Default::default()
10259 },
10260 ..LanguageConfig::default()
10261 },
10262 Some(tree_sitter_rust::LANGUAGE.into()),
10263 )));
10264 update_test_language_settings(cx, |settings| {
10265 // Enable Prettier formatting for the same buffer, and ensure
10266 // LSP is called instead of Prettier.
10267 settings.defaults.prettier = Some(PrettierSettings {
10268 allowed: true,
10269 ..PrettierSettings::default()
10270 });
10271 });
10272 let mut fake_servers = language_registry.register_fake_lsp(
10273 "Rust",
10274 FakeLspAdapter {
10275 capabilities: lsp::ServerCapabilities {
10276 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10277 ..Default::default()
10278 },
10279 ..Default::default()
10280 },
10281 );
10282
10283 let buffer = project
10284 .update(cx, |project, cx| {
10285 project.open_local_buffer(path!("/file.rs"), cx)
10286 })
10287 .await
10288 .unwrap();
10289
10290 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10291 let (editor, cx) = cx.add_window_view(|window, cx| {
10292 build_editor_with_project(project.clone(), buffer, window, cx)
10293 });
10294 editor.update_in(cx, |editor, window, cx| {
10295 editor.set_text("one\ntwo\nthree\n", window, cx)
10296 });
10297
10298 cx.executor().start_waiting();
10299 let fake_server = fake_servers.next().await.unwrap();
10300
10301 let format = editor
10302 .update_in(cx, |editor, window, cx| {
10303 editor.perform_format(
10304 project.clone(),
10305 FormatTrigger::Manual,
10306 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10307 window,
10308 cx,
10309 )
10310 })
10311 .unwrap();
10312 fake_server
10313 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10314 assert_eq!(
10315 params.text_document.uri,
10316 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10317 );
10318 assert_eq!(params.options.tab_size, 4);
10319 Ok(Some(vec![lsp::TextEdit::new(
10320 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10321 ", ".to_string(),
10322 )]))
10323 })
10324 .next()
10325 .await;
10326 cx.executor().start_waiting();
10327 format.await;
10328 assert_eq!(
10329 editor.update(cx, |editor, cx| editor.text(cx)),
10330 "one, two\nthree\n"
10331 );
10332
10333 editor.update_in(cx, |editor, window, cx| {
10334 editor.set_text("one\ntwo\nthree\n", window, cx)
10335 });
10336 // Ensure we don't lock if formatting hangs.
10337 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10338 move |params, _| async move {
10339 assert_eq!(
10340 params.text_document.uri,
10341 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10342 );
10343 futures::future::pending::<()>().await;
10344 unreachable!()
10345 },
10346 );
10347 let format = editor
10348 .update_in(cx, |editor, window, cx| {
10349 editor.perform_format(
10350 project,
10351 FormatTrigger::Manual,
10352 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10353 window,
10354 cx,
10355 )
10356 })
10357 .unwrap();
10358 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10359 cx.executor().start_waiting();
10360 format.await;
10361 assert_eq!(
10362 editor.update(cx, |editor, cx| editor.text(cx)),
10363 "one\ntwo\nthree\n"
10364 );
10365}
10366
10367#[gpui::test]
10368async fn test_multiple_formatters(cx: &mut TestAppContext) {
10369 init_test(cx, |settings| {
10370 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
10371 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10372 Formatter::LanguageServer { name: None },
10373 Formatter::CodeActions(
10374 [
10375 ("code-action-1".into(), true),
10376 ("code-action-2".into(), true),
10377 ]
10378 .into_iter()
10379 .collect(),
10380 ),
10381 ])))
10382 });
10383
10384 let fs = FakeFs::new(cx.executor());
10385 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
10386 .await;
10387
10388 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10389 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10390 language_registry.add(rust_lang());
10391
10392 let mut fake_servers = language_registry.register_fake_lsp(
10393 "Rust",
10394 FakeLspAdapter {
10395 capabilities: lsp::ServerCapabilities {
10396 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10397 execute_command_provider: Some(lsp::ExecuteCommandOptions {
10398 commands: vec!["the-command-for-code-action-1".into()],
10399 ..Default::default()
10400 }),
10401 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10402 ..Default::default()
10403 },
10404 ..Default::default()
10405 },
10406 );
10407
10408 let buffer = project
10409 .update(cx, |project, cx| {
10410 project.open_local_buffer(path!("/file.rs"), cx)
10411 })
10412 .await
10413 .unwrap();
10414
10415 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10416 let (editor, cx) = cx.add_window_view(|window, cx| {
10417 build_editor_with_project(project.clone(), buffer, window, cx)
10418 });
10419
10420 cx.executor().start_waiting();
10421
10422 let fake_server = fake_servers.next().await.unwrap();
10423 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10424 move |_params, _| async move {
10425 Ok(Some(vec![lsp::TextEdit::new(
10426 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10427 "applied-formatting\n".to_string(),
10428 )]))
10429 },
10430 );
10431 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10432 move |params, _| async move {
10433 assert_eq!(
10434 params.context.only,
10435 Some(vec!["code-action-1".into(), "code-action-2".into()])
10436 );
10437 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
10438 Ok(Some(vec![
10439 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10440 kind: Some("code-action-1".into()),
10441 edit: Some(lsp::WorkspaceEdit::new(
10442 [(
10443 uri.clone(),
10444 vec![lsp::TextEdit::new(
10445 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10446 "applied-code-action-1-edit\n".to_string(),
10447 )],
10448 )]
10449 .into_iter()
10450 .collect(),
10451 )),
10452 command: Some(lsp::Command {
10453 command: "the-command-for-code-action-1".into(),
10454 ..Default::default()
10455 }),
10456 ..Default::default()
10457 }),
10458 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10459 kind: Some("code-action-2".into()),
10460 edit: Some(lsp::WorkspaceEdit::new(
10461 [(
10462 uri.clone(),
10463 vec![lsp::TextEdit::new(
10464 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10465 "applied-code-action-2-edit\n".to_string(),
10466 )],
10467 )]
10468 .into_iter()
10469 .collect(),
10470 )),
10471 ..Default::default()
10472 }),
10473 ]))
10474 },
10475 );
10476
10477 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
10478 move |params, _| async move { Ok(params) }
10479 });
10480
10481 let command_lock = Arc::new(futures::lock::Mutex::new(()));
10482 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
10483 let fake = fake_server.clone();
10484 let lock = command_lock.clone();
10485 move |params, _| {
10486 assert_eq!(params.command, "the-command-for-code-action-1");
10487 let fake = fake.clone();
10488 let lock = lock.clone();
10489 async move {
10490 lock.lock().await;
10491 fake.server
10492 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
10493 label: None,
10494 edit: lsp::WorkspaceEdit {
10495 changes: Some(
10496 [(
10497 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
10498 vec![lsp::TextEdit {
10499 range: lsp::Range::new(
10500 lsp::Position::new(0, 0),
10501 lsp::Position::new(0, 0),
10502 ),
10503 new_text: "applied-code-action-1-command\n".into(),
10504 }],
10505 )]
10506 .into_iter()
10507 .collect(),
10508 ),
10509 ..Default::default()
10510 },
10511 })
10512 .await
10513 .into_response()
10514 .unwrap();
10515 Ok(Some(json!(null)))
10516 }
10517 }
10518 });
10519
10520 cx.executor().start_waiting();
10521 editor
10522 .update_in(cx, |editor, window, cx| {
10523 editor.perform_format(
10524 project.clone(),
10525 FormatTrigger::Manual,
10526 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10527 window,
10528 cx,
10529 )
10530 })
10531 .unwrap()
10532 .await;
10533 editor.update(cx, |editor, cx| {
10534 assert_eq!(
10535 editor.text(cx),
10536 r#"
10537 applied-code-action-2-edit
10538 applied-code-action-1-command
10539 applied-code-action-1-edit
10540 applied-formatting
10541 one
10542 two
10543 three
10544 "#
10545 .unindent()
10546 );
10547 });
10548
10549 editor.update_in(cx, |editor, window, cx| {
10550 editor.undo(&Default::default(), window, cx);
10551 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10552 });
10553
10554 // Perform a manual edit while waiting for an LSP command
10555 // that's being run as part of a formatting code action.
10556 let lock_guard = command_lock.lock().await;
10557 let format = editor
10558 .update_in(cx, |editor, window, cx| {
10559 editor.perform_format(
10560 project.clone(),
10561 FormatTrigger::Manual,
10562 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10563 window,
10564 cx,
10565 )
10566 })
10567 .unwrap();
10568 cx.run_until_parked();
10569 editor.update(cx, |editor, cx| {
10570 assert_eq!(
10571 editor.text(cx),
10572 r#"
10573 applied-code-action-1-edit
10574 applied-formatting
10575 one
10576 two
10577 three
10578 "#
10579 .unindent()
10580 );
10581
10582 editor.buffer.update(cx, |buffer, cx| {
10583 let ix = buffer.len(cx);
10584 buffer.edit([(ix..ix, "edited\n")], None, cx);
10585 });
10586 });
10587
10588 // Allow the LSP command to proceed. Because the buffer was edited,
10589 // the second code action will not be run.
10590 drop(lock_guard);
10591 format.await;
10592 editor.update_in(cx, |editor, window, cx| {
10593 assert_eq!(
10594 editor.text(cx),
10595 r#"
10596 applied-code-action-1-command
10597 applied-code-action-1-edit
10598 applied-formatting
10599 one
10600 two
10601 three
10602 edited
10603 "#
10604 .unindent()
10605 );
10606
10607 // The manual edit is undone first, because it is the last thing the user did
10608 // (even though the command completed afterwards).
10609 editor.undo(&Default::default(), window, cx);
10610 assert_eq!(
10611 editor.text(cx),
10612 r#"
10613 applied-code-action-1-command
10614 applied-code-action-1-edit
10615 applied-formatting
10616 one
10617 two
10618 three
10619 "#
10620 .unindent()
10621 );
10622
10623 // All the formatting (including the command, which completed after the manual edit)
10624 // is undone together.
10625 editor.undo(&Default::default(), window, cx);
10626 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10627 });
10628}
10629
10630#[gpui::test]
10631async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
10632 init_test(cx, |settings| {
10633 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10634 Formatter::LanguageServer { name: None },
10635 ])))
10636 });
10637
10638 let fs = FakeFs::new(cx.executor());
10639 fs.insert_file(path!("/file.ts"), Default::default()).await;
10640
10641 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10642
10643 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10644 language_registry.add(Arc::new(Language::new(
10645 LanguageConfig {
10646 name: "TypeScript".into(),
10647 matcher: LanguageMatcher {
10648 path_suffixes: vec!["ts".to_string()],
10649 ..Default::default()
10650 },
10651 ..LanguageConfig::default()
10652 },
10653 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10654 )));
10655 update_test_language_settings(cx, |settings| {
10656 settings.defaults.prettier = Some(PrettierSettings {
10657 allowed: true,
10658 ..PrettierSettings::default()
10659 });
10660 });
10661 let mut fake_servers = language_registry.register_fake_lsp(
10662 "TypeScript",
10663 FakeLspAdapter {
10664 capabilities: lsp::ServerCapabilities {
10665 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10666 ..Default::default()
10667 },
10668 ..Default::default()
10669 },
10670 );
10671
10672 let buffer = project
10673 .update(cx, |project, cx| {
10674 project.open_local_buffer(path!("/file.ts"), cx)
10675 })
10676 .await
10677 .unwrap();
10678
10679 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10680 let (editor, cx) = cx.add_window_view(|window, cx| {
10681 build_editor_with_project(project.clone(), buffer, window, cx)
10682 });
10683 editor.update_in(cx, |editor, window, cx| {
10684 editor.set_text(
10685 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10686 window,
10687 cx,
10688 )
10689 });
10690
10691 cx.executor().start_waiting();
10692 let fake_server = fake_servers.next().await.unwrap();
10693
10694 let format = editor
10695 .update_in(cx, |editor, window, cx| {
10696 editor.perform_code_action_kind(
10697 project.clone(),
10698 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10699 window,
10700 cx,
10701 )
10702 })
10703 .unwrap();
10704 fake_server
10705 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
10706 assert_eq!(
10707 params.text_document.uri,
10708 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10709 );
10710 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
10711 lsp::CodeAction {
10712 title: "Organize Imports".to_string(),
10713 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
10714 edit: Some(lsp::WorkspaceEdit {
10715 changes: Some(
10716 [(
10717 params.text_document.uri.clone(),
10718 vec![lsp::TextEdit::new(
10719 lsp::Range::new(
10720 lsp::Position::new(1, 0),
10721 lsp::Position::new(2, 0),
10722 ),
10723 "".to_string(),
10724 )],
10725 )]
10726 .into_iter()
10727 .collect(),
10728 ),
10729 ..Default::default()
10730 }),
10731 ..Default::default()
10732 },
10733 )]))
10734 })
10735 .next()
10736 .await;
10737 cx.executor().start_waiting();
10738 format.await;
10739 assert_eq!(
10740 editor.update(cx, |editor, cx| editor.text(cx)),
10741 "import { a } from 'module';\n\nconst x = a;\n"
10742 );
10743
10744 editor.update_in(cx, |editor, window, cx| {
10745 editor.set_text(
10746 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10747 window,
10748 cx,
10749 )
10750 });
10751 // Ensure we don't lock if code action hangs.
10752 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10753 move |params, _| async move {
10754 assert_eq!(
10755 params.text_document.uri,
10756 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10757 );
10758 futures::future::pending::<()>().await;
10759 unreachable!()
10760 },
10761 );
10762 let format = editor
10763 .update_in(cx, |editor, window, cx| {
10764 editor.perform_code_action_kind(
10765 project,
10766 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10767 window,
10768 cx,
10769 )
10770 })
10771 .unwrap();
10772 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
10773 cx.executor().start_waiting();
10774 format.await;
10775 assert_eq!(
10776 editor.update(cx, |editor, cx| editor.text(cx)),
10777 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
10778 );
10779}
10780
10781#[gpui::test]
10782async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
10783 init_test(cx, |_| {});
10784
10785 let mut cx = EditorLspTestContext::new_rust(
10786 lsp::ServerCapabilities {
10787 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10788 ..Default::default()
10789 },
10790 cx,
10791 )
10792 .await;
10793
10794 cx.set_state(indoc! {"
10795 one.twoˇ
10796 "});
10797
10798 // The format request takes a long time. When it completes, it inserts
10799 // a newline and an indent before the `.`
10800 cx.lsp
10801 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
10802 let executor = cx.background_executor().clone();
10803 async move {
10804 executor.timer(Duration::from_millis(100)).await;
10805 Ok(Some(vec![lsp::TextEdit {
10806 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
10807 new_text: "\n ".into(),
10808 }]))
10809 }
10810 });
10811
10812 // Submit a format request.
10813 let format_1 = cx
10814 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10815 .unwrap();
10816 cx.executor().run_until_parked();
10817
10818 // Submit a second format request.
10819 let format_2 = cx
10820 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10821 .unwrap();
10822 cx.executor().run_until_parked();
10823
10824 // Wait for both format requests to complete
10825 cx.executor().advance_clock(Duration::from_millis(200));
10826 cx.executor().start_waiting();
10827 format_1.await.unwrap();
10828 cx.executor().start_waiting();
10829 format_2.await.unwrap();
10830
10831 // The formatting edits only happens once.
10832 cx.assert_editor_state(indoc! {"
10833 one
10834 .twoˇ
10835 "});
10836}
10837
10838#[gpui::test]
10839async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
10840 init_test(cx, |settings| {
10841 settings.defaults.formatter = Some(SelectedFormatter::Auto)
10842 });
10843
10844 let mut cx = EditorLspTestContext::new_rust(
10845 lsp::ServerCapabilities {
10846 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10847 ..Default::default()
10848 },
10849 cx,
10850 )
10851 .await;
10852
10853 // Set up a buffer white some trailing whitespace and no trailing newline.
10854 cx.set_state(
10855 &[
10856 "one ", //
10857 "twoˇ", //
10858 "three ", //
10859 "four", //
10860 ]
10861 .join("\n"),
10862 );
10863
10864 // Submit a format request.
10865 let format = cx
10866 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10867 .unwrap();
10868
10869 // Record which buffer changes have been sent to the language server
10870 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
10871 cx.lsp
10872 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
10873 let buffer_changes = buffer_changes.clone();
10874 move |params, _| {
10875 buffer_changes.lock().extend(
10876 params
10877 .content_changes
10878 .into_iter()
10879 .map(|e| (e.range.unwrap(), e.text)),
10880 );
10881 }
10882 });
10883
10884 // Handle formatting requests to the language server.
10885 cx.lsp
10886 .set_request_handler::<lsp::request::Formatting, _, _>({
10887 let buffer_changes = buffer_changes.clone();
10888 move |_, _| {
10889 // When formatting is requested, trailing whitespace has already been stripped,
10890 // and the trailing newline has already been added.
10891 assert_eq!(
10892 &buffer_changes.lock()[1..],
10893 &[
10894 (
10895 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
10896 "".into()
10897 ),
10898 (
10899 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
10900 "".into()
10901 ),
10902 (
10903 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
10904 "\n".into()
10905 ),
10906 ]
10907 );
10908
10909 // Insert blank lines between each line of the buffer.
10910 async move {
10911 Ok(Some(vec![
10912 lsp::TextEdit {
10913 range: lsp::Range::new(
10914 lsp::Position::new(1, 0),
10915 lsp::Position::new(1, 0),
10916 ),
10917 new_text: "\n".into(),
10918 },
10919 lsp::TextEdit {
10920 range: lsp::Range::new(
10921 lsp::Position::new(2, 0),
10922 lsp::Position::new(2, 0),
10923 ),
10924 new_text: "\n".into(),
10925 },
10926 ]))
10927 }
10928 }
10929 });
10930
10931 // After formatting the buffer, the trailing whitespace is stripped,
10932 // a newline is appended, and the edits provided by the language server
10933 // have been applied.
10934 format.await.unwrap();
10935 cx.assert_editor_state(
10936 &[
10937 "one", //
10938 "", //
10939 "twoˇ", //
10940 "", //
10941 "three", //
10942 "four", //
10943 "", //
10944 ]
10945 .join("\n"),
10946 );
10947
10948 // Undoing the formatting undoes the trailing whitespace removal, the
10949 // trailing newline, and the LSP edits.
10950 cx.update_buffer(|buffer, cx| buffer.undo(cx));
10951 cx.assert_editor_state(
10952 &[
10953 "one ", //
10954 "twoˇ", //
10955 "three ", //
10956 "four", //
10957 ]
10958 .join("\n"),
10959 );
10960}
10961
10962#[gpui::test]
10963async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
10964 cx: &mut TestAppContext,
10965) {
10966 init_test(cx, |_| {});
10967
10968 cx.update(|cx| {
10969 cx.update_global::<SettingsStore, _>(|settings, cx| {
10970 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10971 settings.auto_signature_help = Some(true);
10972 });
10973 });
10974 });
10975
10976 let mut cx = EditorLspTestContext::new_rust(
10977 lsp::ServerCapabilities {
10978 signature_help_provider: Some(lsp::SignatureHelpOptions {
10979 ..Default::default()
10980 }),
10981 ..Default::default()
10982 },
10983 cx,
10984 )
10985 .await;
10986
10987 let language = Language::new(
10988 LanguageConfig {
10989 name: "Rust".into(),
10990 brackets: BracketPairConfig {
10991 pairs: vec![
10992 BracketPair {
10993 start: "{".to_string(),
10994 end: "}".to_string(),
10995 close: true,
10996 surround: true,
10997 newline: true,
10998 },
10999 BracketPair {
11000 start: "(".to_string(),
11001 end: ")".to_string(),
11002 close: true,
11003 surround: true,
11004 newline: true,
11005 },
11006 BracketPair {
11007 start: "/*".to_string(),
11008 end: " */".to_string(),
11009 close: true,
11010 surround: true,
11011 newline: true,
11012 },
11013 BracketPair {
11014 start: "[".to_string(),
11015 end: "]".to_string(),
11016 close: false,
11017 surround: false,
11018 newline: true,
11019 },
11020 BracketPair {
11021 start: "\"".to_string(),
11022 end: "\"".to_string(),
11023 close: true,
11024 surround: true,
11025 newline: false,
11026 },
11027 BracketPair {
11028 start: "<".to_string(),
11029 end: ">".to_string(),
11030 close: false,
11031 surround: true,
11032 newline: true,
11033 },
11034 ],
11035 ..Default::default()
11036 },
11037 autoclose_before: "})]".to_string(),
11038 ..Default::default()
11039 },
11040 Some(tree_sitter_rust::LANGUAGE.into()),
11041 );
11042 let language = Arc::new(language);
11043
11044 cx.language_registry().add(language.clone());
11045 cx.update_buffer(|buffer, cx| {
11046 buffer.set_language(Some(language), cx);
11047 });
11048
11049 cx.set_state(
11050 &r#"
11051 fn main() {
11052 sampleˇ
11053 }
11054 "#
11055 .unindent(),
11056 );
11057
11058 cx.update_editor(|editor, window, cx| {
11059 editor.handle_input("(", window, cx);
11060 });
11061 cx.assert_editor_state(
11062 &"
11063 fn main() {
11064 sample(ˇ)
11065 }
11066 "
11067 .unindent(),
11068 );
11069
11070 let mocked_response = lsp::SignatureHelp {
11071 signatures: vec![lsp::SignatureInformation {
11072 label: "fn sample(param1: u8, param2: u8)".to_string(),
11073 documentation: None,
11074 parameters: Some(vec![
11075 lsp::ParameterInformation {
11076 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11077 documentation: None,
11078 },
11079 lsp::ParameterInformation {
11080 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11081 documentation: None,
11082 },
11083 ]),
11084 active_parameter: None,
11085 }],
11086 active_signature: Some(0),
11087 active_parameter: Some(0),
11088 };
11089 handle_signature_help_request(&mut cx, mocked_response).await;
11090
11091 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11092 .await;
11093
11094 cx.editor(|editor, _, _| {
11095 let signature_help_state = editor.signature_help_state.popover().cloned();
11096 let signature = signature_help_state.unwrap();
11097 assert_eq!(
11098 signature.signatures[signature.current_signature].label,
11099 "fn sample(param1: u8, param2: u8)"
11100 );
11101 });
11102}
11103
11104#[gpui::test]
11105async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
11106 init_test(cx, |_| {});
11107
11108 cx.update(|cx| {
11109 cx.update_global::<SettingsStore, _>(|settings, cx| {
11110 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11111 settings.auto_signature_help = Some(false);
11112 settings.show_signature_help_after_edits = Some(false);
11113 });
11114 });
11115 });
11116
11117 let mut cx = EditorLspTestContext::new_rust(
11118 lsp::ServerCapabilities {
11119 signature_help_provider: Some(lsp::SignatureHelpOptions {
11120 ..Default::default()
11121 }),
11122 ..Default::default()
11123 },
11124 cx,
11125 )
11126 .await;
11127
11128 let language = Language::new(
11129 LanguageConfig {
11130 name: "Rust".into(),
11131 brackets: BracketPairConfig {
11132 pairs: vec![
11133 BracketPair {
11134 start: "{".to_string(),
11135 end: "}".to_string(),
11136 close: true,
11137 surround: true,
11138 newline: true,
11139 },
11140 BracketPair {
11141 start: "(".to_string(),
11142 end: ")".to_string(),
11143 close: true,
11144 surround: true,
11145 newline: true,
11146 },
11147 BracketPair {
11148 start: "/*".to_string(),
11149 end: " */".to_string(),
11150 close: true,
11151 surround: true,
11152 newline: true,
11153 },
11154 BracketPair {
11155 start: "[".to_string(),
11156 end: "]".to_string(),
11157 close: false,
11158 surround: false,
11159 newline: true,
11160 },
11161 BracketPair {
11162 start: "\"".to_string(),
11163 end: "\"".to_string(),
11164 close: true,
11165 surround: true,
11166 newline: false,
11167 },
11168 BracketPair {
11169 start: "<".to_string(),
11170 end: ">".to_string(),
11171 close: false,
11172 surround: true,
11173 newline: true,
11174 },
11175 ],
11176 ..Default::default()
11177 },
11178 autoclose_before: "})]".to_string(),
11179 ..Default::default()
11180 },
11181 Some(tree_sitter_rust::LANGUAGE.into()),
11182 );
11183 let language = Arc::new(language);
11184
11185 cx.language_registry().add(language.clone());
11186 cx.update_buffer(|buffer, cx| {
11187 buffer.set_language(Some(language), cx);
11188 });
11189
11190 // Ensure that signature_help is not called when no signature help is enabled.
11191 cx.set_state(
11192 &r#"
11193 fn main() {
11194 sampleˇ
11195 }
11196 "#
11197 .unindent(),
11198 );
11199 cx.update_editor(|editor, window, cx| {
11200 editor.handle_input("(", window, cx);
11201 });
11202 cx.assert_editor_state(
11203 &"
11204 fn main() {
11205 sample(ˇ)
11206 }
11207 "
11208 .unindent(),
11209 );
11210 cx.editor(|editor, _, _| {
11211 assert!(editor.signature_help_state.task().is_none());
11212 });
11213
11214 let mocked_response = lsp::SignatureHelp {
11215 signatures: vec![lsp::SignatureInformation {
11216 label: "fn sample(param1: u8, param2: u8)".to_string(),
11217 documentation: None,
11218 parameters: Some(vec![
11219 lsp::ParameterInformation {
11220 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11221 documentation: None,
11222 },
11223 lsp::ParameterInformation {
11224 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11225 documentation: None,
11226 },
11227 ]),
11228 active_parameter: None,
11229 }],
11230 active_signature: Some(0),
11231 active_parameter: Some(0),
11232 };
11233
11234 // Ensure that signature_help is called when enabled afte edits
11235 cx.update(|_, cx| {
11236 cx.update_global::<SettingsStore, _>(|settings, cx| {
11237 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11238 settings.auto_signature_help = Some(false);
11239 settings.show_signature_help_after_edits = Some(true);
11240 });
11241 });
11242 });
11243 cx.set_state(
11244 &r#"
11245 fn main() {
11246 sampleˇ
11247 }
11248 "#
11249 .unindent(),
11250 );
11251 cx.update_editor(|editor, window, cx| {
11252 editor.handle_input("(", window, cx);
11253 });
11254 cx.assert_editor_state(
11255 &"
11256 fn main() {
11257 sample(ˇ)
11258 }
11259 "
11260 .unindent(),
11261 );
11262 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11263 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11264 .await;
11265 cx.update_editor(|editor, _, _| {
11266 let signature_help_state = editor.signature_help_state.popover().cloned();
11267 assert!(signature_help_state.is_some());
11268 let signature = signature_help_state.unwrap();
11269 assert_eq!(
11270 signature.signatures[signature.current_signature].label,
11271 "fn sample(param1: u8, param2: u8)"
11272 );
11273 editor.signature_help_state = SignatureHelpState::default();
11274 });
11275
11276 // Ensure that signature_help is called when auto signature help override is enabled
11277 cx.update(|_, cx| {
11278 cx.update_global::<SettingsStore, _>(|settings, cx| {
11279 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11280 settings.auto_signature_help = Some(true);
11281 settings.show_signature_help_after_edits = Some(false);
11282 });
11283 });
11284 });
11285 cx.set_state(
11286 &r#"
11287 fn main() {
11288 sampleˇ
11289 }
11290 "#
11291 .unindent(),
11292 );
11293 cx.update_editor(|editor, window, cx| {
11294 editor.handle_input("(", window, cx);
11295 });
11296 cx.assert_editor_state(
11297 &"
11298 fn main() {
11299 sample(ˇ)
11300 }
11301 "
11302 .unindent(),
11303 );
11304 handle_signature_help_request(&mut cx, mocked_response).await;
11305 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11306 .await;
11307 cx.editor(|editor, _, _| {
11308 let signature_help_state = editor.signature_help_state.popover().cloned();
11309 assert!(signature_help_state.is_some());
11310 let signature = signature_help_state.unwrap();
11311 assert_eq!(
11312 signature.signatures[signature.current_signature].label,
11313 "fn sample(param1: u8, param2: u8)"
11314 );
11315 });
11316}
11317
11318#[gpui::test]
11319async fn test_signature_help(cx: &mut TestAppContext) {
11320 init_test(cx, |_| {});
11321 cx.update(|cx| {
11322 cx.update_global::<SettingsStore, _>(|settings, cx| {
11323 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11324 settings.auto_signature_help = Some(true);
11325 });
11326 });
11327 });
11328
11329 let mut cx = EditorLspTestContext::new_rust(
11330 lsp::ServerCapabilities {
11331 signature_help_provider: Some(lsp::SignatureHelpOptions {
11332 ..Default::default()
11333 }),
11334 ..Default::default()
11335 },
11336 cx,
11337 )
11338 .await;
11339
11340 // A test that directly calls `show_signature_help`
11341 cx.update_editor(|editor, window, cx| {
11342 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11343 });
11344
11345 let mocked_response = lsp::SignatureHelp {
11346 signatures: vec![lsp::SignatureInformation {
11347 label: "fn sample(param1: u8, param2: u8)".to_string(),
11348 documentation: None,
11349 parameters: Some(vec![
11350 lsp::ParameterInformation {
11351 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11352 documentation: None,
11353 },
11354 lsp::ParameterInformation {
11355 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11356 documentation: None,
11357 },
11358 ]),
11359 active_parameter: None,
11360 }],
11361 active_signature: Some(0),
11362 active_parameter: Some(0),
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 let signature_help_state = editor.signature_help_state.popover().cloned();
11371 assert!(signature_help_state.is_some());
11372 let signature = signature_help_state.unwrap();
11373 assert_eq!(
11374 signature.signatures[signature.current_signature].label,
11375 "fn sample(param1: u8, param2: u8)"
11376 );
11377 });
11378
11379 // When exiting outside from inside the brackets, `signature_help` is closed.
11380 cx.set_state(indoc! {"
11381 fn main() {
11382 sample(ˇ);
11383 }
11384
11385 fn sample(param1: u8, param2: u8) {}
11386 "});
11387
11388 cx.update_editor(|editor, window, cx| {
11389 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11390 s.select_ranges([0..0])
11391 });
11392 });
11393
11394 let mocked_response = lsp::SignatureHelp {
11395 signatures: Vec::new(),
11396 active_signature: None,
11397 active_parameter: None,
11398 };
11399 handle_signature_help_request(&mut cx, mocked_response).await;
11400
11401 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11402 .await;
11403
11404 cx.editor(|editor, _, _| {
11405 assert!(!editor.signature_help_state.is_shown());
11406 });
11407
11408 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
11409 cx.set_state(indoc! {"
11410 fn main() {
11411 sample(ˇ);
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(0),
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 cx.editor(|editor, _, _| {
11440 assert!(editor.signature_help_state.is_shown());
11441 });
11442
11443 // Restore the popover with more parameter input
11444 cx.set_state(indoc! {"
11445 fn main() {
11446 sample(param1, param2ˇ);
11447 }
11448
11449 fn sample(param1: u8, param2: u8) {}
11450 "});
11451
11452 let mocked_response = lsp::SignatureHelp {
11453 signatures: vec![lsp::SignatureInformation {
11454 label: "fn sample(param1: u8, param2: u8)".to_string(),
11455 documentation: None,
11456 parameters: Some(vec![
11457 lsp::ParameterInformation {
11458 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11459 documentation: None,
11460 },
11461 lsp::ParameterInformation {
11462 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11463 documentation: None,
11464 },
11465 ]),
11466 active_parameter: None,
11467 }],
11468 active_signature: Some(0),
11469 active_parameter: Some(1),
11470 };
11471 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11472 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11473 .await;
11474
11475 // When selecting a range, the popover is gone.
11476 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
11477 cx.update_editor(|editor, window, cx| {
11478 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11479 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11480 })
11481 });
11482 cx.assert_editor_state(indoc! {"
11483 fn main() {
11484 sample(param1, «ˇparam2»);
11485 }
11486
11487 fn sample(param1: u8, param2: u8) {}
11488 "});
11489 cx.editor(|editor, _, _| {
11490 assert!(!editor.signature_help_state.is_shown());
11491 });
11492
11493 // When unselecting again, the popover is back if within the brackets.
11494 cx.update_editor(|editor, window, cx| {
11495 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11496 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11497 })
11498 });
11499 cx.assert_editor_state(indoc! {"
11500 fn main() {
11501 sample(param1, ˇparam2);
11502 }
11503
11504 fn sample(param1: u8, param2: u8) {}
11505 "});
11506 handle_signature_help_request(&mut cx, mocked_response).await;
11507 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11508 .await;
11509 cx.editor(|editor, _, _| {
11510 assert!(editor.signature_help_state.is_shown());
11511 });
11512
11513 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
11514 cx.update_editor(|editor, window, cx| {
11515 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11516 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
11517 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11518 })
11519 });
11520 cx.assert_editor_state(indoc! {"
11521 fn main() {
11522 sample(param1, ˇparam2);
11523 }
11524
11525 fn sample(param1: u8, param2: u8) {}
11526 "});
11527
11528 let mocked_response = lsp::SignatureHelp {
11529 signatures: vec![lsp::SignatureInformation {
11530 label: "fn sample(param1: u8, param2: u8)".to_string(),
11531 documentation: None,
11532 parameters: Some(vec![
11533 lsp::ParameterInformation {
11534 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11535 documentation: None,
11536 },
11537 lsp::ParameterInformation {
11538 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11539 documentation: None,
11540 },
11541 ]),
11542 active_parameter: None,
11543 }],
11544 active_signature: Some(0),
11545 active_parameter: Some(1),
11546 };
11547 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11548 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11549 .await;
11550 cx.update_editor(|editor, _, cx| {
11551 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
11552 });
11553 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11554 .await;
11555 cx.update_editor(|editor, window, cx| {
11556 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11557 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11558 })
11559 });
11560 cx.assert_editor_state(indoc! {"
11561 fn main() {
11562 sample(param1, «ˇparam2»);
11563 }
11564
11565 fn sample(param1: u8, param2: u8) {}
11566 "});
11567 cx.update_editor(|editor, window, cx| {
11568 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11569 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11570 })
11571 });
11572 cx.assert_editor_state(indoc! {"
11573 fn main() {
11574 sample(param1, ˇparam2);
11575 }
11576
11577 fn sample(param1: u8, param2: u8) {}
11578 "});
11579 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
11580 .await;
11581}
11582
11583#[gpui::test]
11584async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
11585 init_test(cx, |_| {});
11586
11587 let mut cx = EditorLspTestContext::new_rust(
11588 lsp::ServerCapabilities {
11589 signature_help_provider: Some(lsp::SignatureHelpOptions {
11590 ..Default::default()
11591 }),
11592 ..Default::default()
11593 },
11594 cx,
11595 )
11596 .await;
11597
11598 cx.set_state(indoc! {"
11599 fn main() {
11600 overloadedˇ
11601 }
11602 "});
11603
11604 cx.update_editor(|editor, window, cx| {
11605 editor.handle_input("(", window, cx);
11606 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11607 });
11608
11609 // Mock response with 3 signatures
11610 let mocked_response = lsp::SignatureHelp {
11611 signatures: vec![
11612 lsp::SignatureInformation {
11613 label: "fn overloaded(x: i32)".to_string(),
11614 documentation: None,
11615 parameters: Some(vec![lsp::ParameterInformation {
11616 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11617 documentation: None,
11618 }]),
11619 active_parameter: None,
11620 },
11621 lsp::SignatureInformation {
11622 label: "fn overloaded(x: i32, y: i32)".to_string(),
11623 documentation: None,
11624 parameters: Some(vec![
11625 lsp::ParameterInformation {
11626 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11627 documentation: None,
11628 },
11629 lsp::ParameterInformation {
11630 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11631 documentation: None,
11632 },
11633 ]),
11634 active_parameter: None,
11635 },
11636 lsp::SignatureInformation {
11637 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
11638 documentation: None,
11639 parameters: Some(vec![
11640 lsp::ParameterInformation {
11641 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11642 documentation: None,
11643 },
11644 lsp::ParameterInformation {
11645 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11646 documentation: None,
11647 },
11648 lsp::ParameterInformation {
11649 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
11650 documentation: None,
11651 },
11652 ]),
11653 active_parameter: None,
11654 },
11655 ],
11656 active_signature: Some(1),
11657 active_parameter: Some(0),
11658 };
11659 handle_signature_help_request(&mut cx, mocked_response).await;
11660
11661 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11662 .await;
11663
11664 // Verify we have multiple signatures and the right one is selected
11665 cx.editor(|editor, _, _| {
11666 let popover = editor.signature_help_state.popover().cloned().unwrap();
11667 assert_eq!(popover.signatures.len(), 3);
11668 // active_signature was 1, so that should be the current
11669 assert_eq!(popover.current_signature, 1);
11670 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
11671 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
11672 assert_eq!(
11673 popover.signatures[2].label,
11674 "fn overloaded(x: i32, y: i32, z: i32)"
11675 );
11676 });
11677
11678 // Test navigation functionality
11679 cx.update_editor(|editor, window, cx| {
11680 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11681 });
11682
11683 cx.editor(|editor, _, _| {
11684 let popover = editor.signature_help_state.popover().cloned().unwrap();
11685 assert_eq!(popover.current_signature, 2);
11686 });
11687
11688 // Test wrap around
11689 cx.update_editor(|editor, window, cx| {
11690 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11691 });
11692
11693 cx.editor(|editor, _, _| {
11694 let popover = editor.signature_help_state.popover().cloned().unwrap();
11695 assert_eq!(popover.current_signature, 0);
11696 });
11697
11698 // Test previous navigation
11699 cx.update_editor(|editor, window, cx| {
11700 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
11701 });
11702
11703 cx.editor(|editor, _, _| {
11704 let popover = editor.signature_help_state.popover().cloned().unwrap();
11705 assert_eq!(popover.current_signature, 2);
11706 });
11707}
11708
11709#[gpui::test]
11710async fn test_completion_mode(cx: &mut TestAppContext) {
11711 init_test(cx, |_| {});
11712 let mut cx = EditorLspTestContext::new_rust(
11713 lsp::ServerCapabilities {
11714 completion_provider: Some(lsp::CompletionOptions {
11715 resolve_provider: Some(true),
11716 ..Default::default()
11717 }),
11718 ..Default::default()
11719 },
11720 cx,
11721 )
11722 .await;
11723
11724 struct Run {
11725 run_description: &'static str,
11726 initial_state: String,
11727 buffer_marked_text: String,
11728 completion_label: &'static str,
11729 completion_text: &'static str,
11730 expected_with_insert_mode: String,
11731 expected_with_replace_mode: String,
11732 expected_with_replace_subsequence_mode: String,
11733 expected_with_replace_suffix_mode: String,
11734 }
11735
11736 let runs = [
11737 Run {
11738 run_description: "Start of word matches completion text",
11739 initial_state: "before ediˇ after".into(),
11740 buffer_marked_text: "before <edi|> after".into(),
11741 completion_label: "editor",
11742 completion_text: "editor",
11743 expected_with_insert_mode: "before editorˇ after".into(),
11744 expected_with_replace_mode: "before editorˇ after".into(),
11745 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11746 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11747 },
11748 Run {
11749 run_description: "Accept same text at the middle of the word",
11750 initial_state: "before ediˇtor after".into(),
11751 buffer_marked_text: "before <edi|tor> after".into(),
11752 completion_label: "editor",
11753 completion_text: "editor",
11754 expected_with_insert_mode: "before editorˇtor after".into(),
11755 expected_with_replace_mode: "before editorˇ after".into(),
11756 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11757 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11758 },
11759 Run {
11760 run_description: "End of word matches completion text -- cursor at end",
11761 initial_state: "before torˇ after".into(),
11762 buffer_marked_text: "before <tor|> after".into(),
11763 completion_label: "editor",
11764 completion_text: "editor",
11765 expected_with_insert_mode: "before editorˇ after".into(),
11766 expected_with_replace_mode: "before editorˇ after".into(),
11767 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11768 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11769 },
11770 Run {
11771 run_description: "End of word matches completion text -- cursor at start",
11772 initial_state: "before ˇtor after".into(),
11773 buffer_marked_text: "before <|tor> after".into(),
11774 completion_label: "editor",
11775 completion_text: "editor",
11776 expected_with_insert_mode: "before editorˇtor after".into(),
11777 expected_with_replace_mode: "before editorˇ after".into(),
11778 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11779 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11780 },
11781 Run {
11782 run_description: "Prepend text containing whitespace",
11783 initial_state: "pˇfield: bool".into(),
11784 buffer_marked_text: "<p|field>: bool".into(),
11785 completion_label: "pub ",
11786 completion_text: "pub ",
11787 expected_with_insert_mode: "pub ˇfield: bool".into(),
11788 expected_with_replace_mode: "pub ˇ: bool".into(),
11789 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
11790 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
11791 },
11792 Run {
11793 run_description: "Add element to start of list",
11794 initial_state: "[element_ˇelement_2]".into(),
11795 buffer_marked_text: "[<element_|element_2>]".into(),
11796 completion_label: "element_1",
11797 completion_text: "element_1",
11798 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
11799 expected_with_replace_mode: "[element_1ˇ]".into(),
11800 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
11801 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
11802 },
11803 Run {
11804 run_description: "Add element to start of list -- first and second elements are equal",
11805 initial_state: "[elˇelement]".into(),
11806 buffer_marked_text: "[<el|element>]".into(),
11807 completion_label: "element",
11808 completion_text: "element",
11809 expected_with_insert_mode: "[elementˇelement]".into(),
11810 expected_with_replace_mode: "[elementˇ]".into(),
11811 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
11812 expected_with_replace_suffix_mode: "[elementˇ]".into(),
11813 },
11814 Run {
11815 run_description: "Ends with matching suffix",
11816 initial_state: "SubˇError".into(),
11817 buffer_marked_text: "<Sub|Error>".into(),
11818 completion_label: "SubscriptionError",
11819 completion_text: "SubscriptionError",
11820 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
11821 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11822 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11823 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
11824 },
11825 Run {
11826 run_description: "Suffix is a subsequence -- contiguous",
11827 initial_state: "SubˇErr".into(),
11828 buffer_marked_text: "<Sub|Err>".into(),
11829 completion_label: "SubscriptionError",
11830 completion_text: "SubscriptionError",
11831 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
11832 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11833 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11834 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
11835 },
11836 Run {
11837 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
11838 initial_state: "Suˇscrirr".into(),
11839 buffer_marked_text: "<Su|scrirr>".into(),
11840 completion_label: "SubscriptionError",
11841 completion_text: "SubscriptionError",
11842 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
11843 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11844 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11845 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
11846 },
11847 Run {
11848 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
11849 initial_state: "foo(indˇix)".into(),
11850 buffer_marked_text: "foo(<ind|ix>)".into(),
11851 completion_label: "node_index",
11852 completion_text: "node_index",
11853 expected_with_insert_mode: "foo(node_indexˇix)".into(),
11854 expected_with_replace_mode: "foo(node_indexˇ)".into(),
11855 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
11856 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
11857 },
11858 Run {
11859 run_description: "Replace range ends before cursor - should extend to cursor",
11860 initial_state: "before editˇo after".into(),
11861 buffer_marked_text: "before <{ed}>it|o after".into(),
11862 completion_label: "editor",
11863 completion_text: "editor",
11864 expected_with_insert_mode: "before editorˇo after".into(),
11865 expected_with_replace_mode: "before editorˇo after".into(),
11866 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
11867 expected_with_replace_suffix_mode: "before editorˇo after".into(),
11868 },
11869 Run {
11870 run_description: "Uses label for suffix matching",
11871 initial_state: "before ediˇtor after".into(),
11872 buffer_marked_text: "before <edi|tor> after".into(),
11873 completion_label: "editor",
11874 completion_text: "editor()",
11875 expected_with_insert_mode: "before editor()ˇtor after".into(),
11876 expected_with_replace_mode: "before editor()ˇ after".into(),
11877 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
11878 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
11879 },
11880 Run {
11881 run_description: "Case insensitive subsequence and suffix matching",
11882 initial_state: "before EDiˇtoR after".into(),
11883 buffer_marked_text: "before <EDi|toR> after".into(),
11884 completion_label: "editor",
11885 completion_text: "editor",
11886 expected_with_insert_mode: "before editorˇtoR after".into(),
11887 expected_with_replace_mode: "before editorˇ after".into(),
11888 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11889 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11890 },
11891 ];
11892
11893 for run in runs {
11894 let run_variations = [
11895 (LspInsertMode::Insert, run.expected_with_insert_mode),
11896 (LspInsertMode::Replace, run.expected_with_replace_mode),
11897 (
11898 LspInsertMode::ReplaceSubsequence,
11899 run.expected_with_replace_subsequence_mode,
11900 ),
11901 (
11902 LspInsertMode::ReplaceSuffix,
11903 run.expected_with_replace_suffix_mode,
11904 ),
11905 ];
11906
11907 for (lsp_insert_mode, expected_text) in run_variations {
11908 eprintln!(
11909 "run = {:?}, mode = {lsp_insert_mode:.?}",
11910 run.run_description,
11911 );
11912
11913 update_test_language_settings(&mut cx, |settings| {
11914 settings.defaults.completions = Some(CompletionSettings {
11915 lsp_insert_mode,
11916 words: WordsCompletionMode::Disabled,
11917 lsp: true,
11918 lsp_fetch_timeout_ms: 0,
11919 });
11920 });
11921
11922 cx.set_state(&run.initial_state);
11923 cx.update_editor(|editor, window, cx| {
11924 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11925 });
11926
11927 let counter = Arc::new(AtomicUsize::new(0));
11928 handle_completion_request_with_insert_and_replace(
11929 &mut cx,
11930 &run.buffer_marked_text,
11931 vec![(run.completion_label, run.completion_text)],
11932 counter.clone(),
11933 )
11934 .await;
11935 cx.condition(|editor, _| editor.context_menu_visible())
11936 .await;
11937 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11938
11939 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11940 editor
11941 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11942 .unwrap()
11943 });
11944 cx.assert_editor_state(&expected_text);
11945 handle_resolve_completion_request(&mut cx, None).await;
11946 apply_additional_edits.await.unwrap();
11947 }
11948 }
11949}
11950
11951#[gpui::test]
11952async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
11953 init_test(cx, |_| {});
11954 let mut cx = EditorLspTestContext::new_rust(
11955 lsp::ServerCapabilities {
11956 completion_provider: Some(lsp::CompletionOptions {
11957 resolve_provider: Some(true),
11958 ..Default::default()
11959 }),
11960 ..Default::default()
11961 },
11962 cx,
11963 )
11964 .await;
11965
11966 let initial_state = "SubˇError";
11967 let buffer_marked_text = "<Sub|Error>";
11968 let completion_text = "SubscriptionError";
11969 let expected_with_insert_mode = "SubscriptionErrorˇError";
11970 let expected_with_replace_mode = "SubscriptionErrorˇ";
11971
11972 update_test_language_settings(&mut cx, |settings| {
11973 settings.defaults.completions = Some(CompletionSettings {
11974 words: WordsCompletionMode::Disabled,
11975 // set the opposite here to ensure that the action is overriding the default behavior
11976 lsp_insert_mode: LspInsertMode::Insert,
11977 lsp: true,
11978 lsp_fetch_timeout_ms: 0,
11979 });
11980 });
11981
11982 cx.set_state(initial_state);
11983 cx.update_editor(|editor, window, cx| {
11984 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11985 });
11986
11987 let counter = Arc::new(AtomicUsize::new(0));
11988 handle_completion_request_with_insert_and_replace(
11989 &mut cx,
11990 &buffer_marked_text,
11991 vec![(completion_text, completion_text)],
11992 counter.clone(),
11993 )
11994 .await;
11995 cx.condition(|editor, _| editor.context_menu_visible())
11996 .await;
11997 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11998
11999 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12000 editor
12001 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12002 .unwrap()
12003 });
12004 cx.assert_editor_state(&expected_with_replace_mode);
12005 handle_resolve_completion_request(&mut cx, None).await;
12006 apply_additional_edits.await.unwrap();
12007
12008 update_test_language_settings(&mut cx, |settings| {
12009 settings.defaults.completions = Some(CompletionSettings {
12010 words: WordsCompletionMode::Disabled,
12011 // set the opposite here to ensure that the action is overriding the default behavior
12012 lsp_insert_mode: LspInsertMode::Replace,
12013 lsp: true,
12014 lsp_fetch_timeout_ms: 0,
12015 });
12016 });
12017
12018 cx.set_state(initial_state);
12019 cx.update_editor(|editor, window, cx| {
12020 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12021 });
12022 handle_completion_request_with_insert_and_replace(
12023 &mut cx,
12024 &buffer_marked_text,
12025 vec![(completion_text, completion_text)],
12026 counter.clone(),
12027 )
12028 .await;
12029 cx.condition(|editor, _| editor.context_menu_visible())
12030 .await;
12031 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12032
12033 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12034 editor
12035 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
12036 .unwrap()
12037 });
12038 cx.assert_editor_state(&expected_with_insert_mode);
12039 handle_resolve_completion_request(&mut cx, None).await;
12040 apply_additional_edits.await.unwrap();
12041}
12042
12043#[gpui::test]
12044async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
12045 init_test(cx, |_| {});
12046 let mut cx = EditorLspTestContext::new_rust(
12047 lsp::ServerCapabilities {
12048 completion_provider: Some(lsp::CompletionOptions {
12049 resolve_provider: Some(true),
12050 ..Default::default()
12051 }),
12052 ..Default::default()
12053 },
12054 cx,
12055 )
12056 .await;
12057
12058 // scenario: surrounding text matches completion text
12059 let completion_text = "to_offset";
12060 let initial_state = indoc! {"
12061 1. buf.to_offˇsuffix
12062 2. buf.to_offˇsuf
12063 3. buf.to_offˇfix
12064 4. buf.to_offˇ
12065 5. into_offˇensive
12066 6. ˇsuffix
12067 7. let ˇ //
12068 8. aaˇzz
12069 9. buf.to_off«zzzzzˇ»suffix
12070 10. buf.«ˇzzzzz»suffix
12071 11. to_off«ˇzzzzz»
12072
12073 buf.to_offˇsuffix // newest cursor
12074 "};
12075 let completion_marked_buffer = indoc! {"
12076 1. buf.to_offsuffix
12077 2. buf.to_offsuf
12078 3. buf.to_offfix
12079 4. buf.to_off
12080 5. into_offensive
12081 6. suffix
12082 7. let //
12083 8. aazz
12084 9. buf.to_offzzzzzsuffix
12085 10. buf.zzzzzsuffix
12086 11. to_offzzzzz
12087
12088 buf.<to_off|suffix> // newest cursor
12089 "};
12090 let expected = indoc! {"
12091 1. buf.to_offsetˇ
12092 2. buf.to_offsetˇsuf
12093 3. buf.to_offsetˇfix
12094 4. buf.to_offsetˇ
12095 5. into_offsetˇensive
12096 6. to_offsetˇsuffix
12097 7. let to_offsetˇ //
12098 8. aato_offsetˇzz
12099 9. buf.to_offsetˇ
12100 10. buf.to_offsetˇsuffix
12101 11. to_offsetˇ
12102
12103 buf.to_offsetˇ // newest cursor
12104 "};
12105 cx.set_state(initial_state);
12106 cx.update_editor(|editor, window, cx| {
12107 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12108 });
12109 handle_completion_request_with_insert_and_replace(
12110 &mut cx,
12111 completion_marked_buffer,
12112 vec![(completion_text, completion_text)],
12113 Arc::new(AtomicUsize::new(0)),
12114 )
12115 .await;
12116 cx.condition(|editor, _| editor.context_menu_visible())
12117 .await;
12118 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12119 editor
12120 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12121 .unwrap()
12122 });
12123 cx.assert_editor_state(expected);
12124 handle_resolve_completion_request(&mut cx, None).await;
12125 apply_additional_edits.await.unwrap();
12126
12127 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
12128 let completion_text = "foo_and_bar";
12129 let initial_state = indoc! {"
12130 1. ooanbˇ
12131 2. zooanbˇ
12132 3. ooanbˇz
12133 4. zooanbˇz
12134 5. ooanˇ
12135 6. oanbˇ
12136
12137 ooanbˇ
12138 "};
12139 let completion_marked_buffer = indoc! {"
12140 1. ooanb
12141 2. zooanb
12142 3. ooanbz
12143 4. zooanbz
12144 5. ooan
12145 6. oanb
12146
12147 <ooanb|>
12148 "};
12149 let expected = indoc! {"
12150 1. foo_and_barˇ
12151 2. zfoo_and_barˇ
12152 3. foo_and_barˇz
12153 4. zfoo_and_barˇz
12154 5. ooanfoo_and_barˇ
12155 6. oanbfoo_and_barˇ
12156
12157 foo_and_barˇ
12158 "};
12159 cx.set_state(initial_state);
12160 cx.update_editor(|editor, window, cx| {
12161 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12162 });
12163 handle_completion_request_with_insert_and_replace(
12164 &mut cx,
12165 completion_marked_buffer,
12166 vec![(completion_text, completion_text)],
12167 Arc::new(AtomicUsize::new(0)),
12168 )
12169 .await;
12170 cx.condition(|editor, _| editor.context_menu_visible())
12171 .await;
12172 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12173 editor
12174 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12175 .unwrap()
12176 });
12177 cx.assert_editor_state(expected);
12178 handle_resolve_completion_request(&mut cx, None).await;
12179 apply_additional_edits.await.unwrap();
12180
12181 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
12182 // (expects the same as if it was inserted at the end)
12183 let completion_text = "foo_and_bar";
12184 let initial_state = indoc! {"
12185 1. ooˇanb
12186 2. zooˇanb
12187 3. ooˇanbz
12188 4. zooˇanbz
12189
12190 ooˇanb
12191 "};
12192 let completion_marked_buffer = indoc! {"
12193 1. ooanb
12194 2. zooanb
12195 3. ooanbz
12196 4. zooanbz
12197
12198 <oo|anb>
12199 "};
12200 let expected = indoc! {"
12201 1. foo_and_barˇ
12202 2. zfoo_and_barˇ
12203 3. foo_and_barˇz
12204 4. zfoo_and_barˇz
12205
12206 foo_and_barˇ
12207 "};
12208 cx.set_state(initial_state);
12209 cx.update_editor(|editor, window, cx| {
12210 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12211 });
12212 handle_completion_request_with_insert_and_replace(
12213 &mut cx,
12214 completion_marked_buffer,
12215 vec![(completion_text, completion_text)],
12216 Arc::new(AtomicUsize::new(0)),
12217 )
12218 .await;
12219 cx.condition(|editor, _| editor.context_menu_visible())
12220 .await;
12221 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12222 editor
12223 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12224 .unwrap()
12225 });
12226 cx.assert_editor_state(expected);
12227 handle_resolve_completion_request(&mut cx, None).await;
12228 apply_additional_edits.await.unwrap();
12229}
12230
12231// This used to crash
12232#[gpui::test]
12233async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
12234 init_test(cx, |_| {});
12235
12236 let buffer_text = indoc! {"
12237 fn main() {
12238 10.satu;
12239
12240 //
12241 // separate cursors so they open in different excerpts (manually reproducible)
12242 //
12243
12244 10.satu20;
12245 }
12246 "};
12247 let multibuffer_text_with_selections = indoc! {"
12248 fn main() {
12249 10.satuˇ;
12250
12251 //
12252
12253 //
12254
12255 10.satuˇ20;
12256 }
12257 "};
12258 let expected_multibuffer = indoc! {"
12259 fn main() {
12260 10.saturating_sub()ˇ;
12261
12262 //
12263
12264 //
12265
12266 10.saturating_sub()ˇ;
12267 }
12268 "};
12269
12270 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
12271 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
12272
12273 let fs = FakeFs::new(cx.executor());
12274 fs.insert_tree(
12275 path!("/a"),
12276 json!({
12277 "main.rs": buffer_text,
12278 }),
12279 )
12280 .await;
12281
12282 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12283 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12284 language_registry.add(rust_lang());
12285 let mut fake_servers = language_registry.register_fake_lsp(
12286 "Rust",
12287 FakeLspAdapter {
12288 capabilities: lsp::ServerCapabilities {
12289 completion_provider: Some(lsp::CompletionOptions {
12290 resolve_provider: None,
12291 ..lsp::CompletionOptions::default()
12292 }),
12293 ..lsp::ServerCapabilities::default()
12294 },
12295 ..FakeLspAdapter::default()
12296 },
12297 );
12298 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12299 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12300 let buffer = project
12301 .update(cx, |project, cx| {
12302 project.open_local_buffer(path!("/a/main.rs"), cx)
12303 })
12304 .await
12305 .unwrap();
12306
12307 let multi_buffer = cx.new(|cx| {
12308 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
12309 multi_buffer.push_excerpts(
12310 buffer.clone(),
12311 [ExcerptRange::new(0..first_excerpt_end)],
12312 cx,
12313 );
12314 multi_buffer.push_excerpts(
12315 buffer.clone(),
12316 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
12317 cx,
12318 );
12319 multi_buffer
12320 });
12321
12322 let editor = workspace
12323 .update(cx, |_, window, cx| {
12324 cx.new(|cx| {
12325 Editor::new(
12326 EditorMode::Full {
12327 scale_ui_elements_with_buffer_font_size: false,
12328 show_active_line_background: false,
12329 sized_by_content: false,
12330 },
12331 multi_buffer.clone(),
12332 Some(project.clone()),
12333 window,
12334 cx,
12335 )
12336 })
12337 })
12338 .unwrap();
12339
12340 let pane = workspace
12341 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12342 .unwrap();
12343 pane.update_in(cx, |pane, window, cx| {
12344 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
12345 });
12346
12347 let fake_server = fake_servers.next().await.unwrap();
12348
12349 editor.update_in(cx, |editor, window, cx| {
12350 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12351 s.select_ranges([
12352 Point::new(1, 11)..Point::new(1, 11),
12353 Point::new(7, 11)..Point::new(7, 11),
12354 ])
12355 });
12356
12357 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
12358 });
12359
12360 editor.update_in(cx, |editor, window, cx| {
12361 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12362 });
12363
12364 fake_server
12365 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12366 let completion_item = lsp::CompletionItem {
12367 label: "saturating_sub()".into(),
12368 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
12369 lsp::InsertReplaceEdit {
12370 new_text: "saturating_sub()".to_owned(),
12371 insert: lsp::Range::new(
12372 lsp::Position::new(7, 7),
12373 lsp::Position::new(7, 11),
12374 ),
12375 replace: lsp::Range::new(
12376 lsp::Position::new(7, 7),
12377 lsp::Position::new(7, 13),
12378 ),
12379 },
12380 )),
12381 ..lsp::CompletionItem::default()
12382 };
12383
12384 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
12385 })
12386 .next()
12387 .await
12388 .unwrap();
12389
12390 cx.condition(&editor, |editor, _| editor.context_menu_visible())
12391 .await;
12392
12393 editor
12394 .update_in(cx, |editor, window, cx| {
12395 editor
12396 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12397 .unwrap()
12398 })
12399 .await
12400 .unwrap();
12401
12402 editor.update(cx, |editor, cx| {
12403 assert_text_with_selections(editor, expected_multibuffer, cx);
12404 })
12405}
12406
12407#[gpui::test]
12408async fn test_completion(cx: &mut TestAppContext) {
12409 init_test(cx, |_| {});
12410
12411 let mut cx = EditorLspTestContext::new_rust(
12412 lsp::ServerCapabilities {
12413 completion_provider: Some(lsp::CompletionOptions {
12414 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12415 resolve_provider: Some(true),
12416 ..Default::default()
12417 }),
12418 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12419 ..Default::default()
12420 },
12421 cx,
12422 )
12423 .await;
12424 let counter = Arc::new(AtomicUsize::new(0));
12425
12426 cx.set_state(indoc! {"
12427 oneˇ
12428 two
12429 three
12430 "});
12431 cx.simulate_keystroke(".");
12432 handle_completion_request(
12433 indoc! {"
12434 one.|<>
12435 two
12436 three
12437 "},
12438 vec!["first_completion", "second_completion"],
12439 true,
12440 counter.clone(),
12441 &mut cx,
12442 )
12443 .await;
12444 cx.condition(|editor, _| editor.context_menu_visible())
12445 .await;
12446 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12447
12448 let _handler = handle_signature_help_request(
12449 &mut cx,
12450 lsp::SignatureHelp {
12451 signatures: vec![lsp::SignatureInformation {
12452 label: "test signature".to_string(),
12453 documentation: None,
12454 parameters: Some(vec![lsp::ParameterInformation {
12455 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
12456 documentation: None,
12457 }]),
12458 active_parameter: None,
12459 }],
12460 active_signature: None,
12461 active_parameter: None,
12462 },
12463 );
12464 cx.update_editor(|editor, window, cx| {
12465 assert!(
12466 !editor.signature_help_state.is_shown(),
12467 "No signature help was called for"
12468 );
12469 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12470 });
12471 cx.run_until_parked();
12472 cx.update_editor(|editor, _, _| {
12473 assert!(
12474 !editor.signature_help_state.is_shown(),
12475 "No signature help should be shown when completions menu is open"
12476 );
12477 });
12478
12479 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12480 editor.context_menu_next(&Default::default(), window, cx);
12481 editor
12482 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12483 .unwrap()
12484 });
12485 cx.assert_editor_state(indoc! {"
12486 one.second_completionˇ
12487 two
12488 three
12489 "});
12490
12491 handle_resolve_completion_request(
12492 &mut cx,
12493 Some(vec![
12494 (
12495 //This overlaps with the primary completion edit which is
12496 //misbehavior from the LSP spec, test that we filter it out
12497 indoc! {"
12498 one.second_ˇcompletion
12499 two
12500 threeˇ
12501 "},
12502 "overlapping additional edit",
12503 ),
12504 (
12505 indoc! {"
12506 one.second_completion
12507 two
12508 threeˇ
12509 "},
12510 "\nadditional edit",
12511 ),
12512 ]),
12513 )
12514 .await;
12515 apply_additional_edits.await.unwrap();
12516 cx.assert_editor_state(indoc! {"
12517 one.second_completionˇ
12518 two
12519 three
12520 additional edit
12521 "});
12522
12523 cx.set_state(indoc! {"
12524 one.second_completion
12525 twoˇ
12526 threeˇ
12527 additional edit
12528 "});
12529 cx.simulate_keystroke(" ");
12530 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12531 cx.simulate_keystroke("s");
12532 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12533
12534 cx.assert_editor_state(indoc! {"
12535 one.second_completion
12536 two sˇ
12537 three sˇ
12538 additional edit
12539 "});
12540 handle_completion_request(
12541 indoc! {"
12542 one.second_completion
12543 two s
12544 three <s|>
12545 additional edit
12546 "},
12547 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12548 true,
12549 counter.clone(),
12550 &mut cx,
12551 )
12552 .await;
12553 cx.condition(|editor, _| editor.context_menu_visible())
12554 .await;
12555 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12556
12557 cx.simulate_keystroke("i");
12558
12559 handle_completion_request(
12560 indoc! {"
12561 one.second_completion
12562 two si
12563 three <si|>
12564 additional edit
12565 "},
12566 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12567 true,
12568 counter.clone(),
12569 &mut cx,
12570 )
12571 .await;
12572 cx.condition(|editor, _| editor.context_menu_visible())
12573 .await;
12574 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12575
12576 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12577 editor
12578 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12579 .unwrap()
12580 });
12581 cx.assert_editor_state(indoc! {"
12582 one.second_completion
12583 two sixth_completionˇ
12584 three sixth_completionˇ
12585 additional edit
12586 "});
12587
12588 apply_additional_edits.await.unwrap();
12589
12590 update_test_language_settings(&mut cx, |settings| {
12591 settings.defaults.show_completions_on_input = Some(false);
12592 });
12593 cx.set_state("editorˇ");
12594 cx.simulate_keystroke(".");
12595 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12596 cx.simulate_keystrokes("c l o");
12597 cx.assert_editor_state("editor.cloˇ");
12598 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12599 cx.update_editor(|editor, window, cx| {
12600 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12601 });
12602 handle_completion_request(
12603 "editor.<clo|>",
12604 vec!["close", "clobber"],
12605 true,
12606 counter.clone(),
12607 &mut cx,
12608 )
12609 .await;
12610 cx.condition(|editor, _| editor.context_menu_visible())
12611 .await;
12612 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
12613
12614 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12615 editor
12616 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12617 .unwrap()
12618 });
12619 cx.assert_editor_state("editor.clobberˇ");
12620 handle_resolve_completion_request(&mut cx, None).await;
12621 apply_additional_edits.await.unwrap();
12622}
12623
12624#[gpui::test]
12625async fn test_completion_reuse(cx: &mut TestAppContext) {
12626 init_test(cx, |_| {});
12627
12628 let mut cx = EditorLspTestContext::new_rust(
12629 lsp::ServerCapabilities {
12630 completion_provider: Some(lsp::CompletionOptions {
12631 trigger_characters: Some(vec![".".to_string()]),
12632 ..Default::default()
12633 }),
12634 ..Default::default()
12635 },
12636 cx,
12637 )
12638 .await;
12639
12640 let counter = Arc::new(AtomicUsize::new(0));
12641 cx.set_state("objˇ");
12642 cx.simulate_keystroke(".");
12643
12644 // Initial completion request returns complete results
12645 let is_incomplete = false;
12646 handle_completion_request(
12647 "obj.|<>",
12648 vec!["a", "ab", "abc"],
12649 is_incomplete,
12650 counter.clone(),
12651 &mut cx,
12652 )
12653 .await;
12654 cx.run_until_parked();
12655 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12656 cx.assert_editor_state("obj.ˇ");
12657 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12658
12659 // Type "a" - filters existing completions
12660 cx.simulate_keystroke("a");
12661 cx.run_until_parked();
12662 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12663 cx.assert_editor_state("obj.aˇ");
12664 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12665
12666 // Type "b" - filters existing completions
12667 cx.simulate_keystroke("b");
12668 cx.run_until_parked();
12669 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12670 cx.assert_editor_state("obj.abˇ");
12671 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12672
12673 // Type "c" - filters existing completions
12674 cx.simulate_keystroke("c");
12675 cx.run_until_parked();
12676 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12677 cx.assert_editor_state("obj.abcˇ");
12678 check_displayed_completions(vec!["abc"], &mut cx);
12679
12680 // Backspace to delete "c" - filters existing completions
12681 cx.update_editor(|editor, window, cx| {
12682 editor.backspace(&Backspace, window, cx);
12683 });
12684 cx.run_until_parked();
12685 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12686 cx.assert_editor_state("obj.abˇ");
12687 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12688
12689 // Moving cursor to the left dismisses menu.
12690 cx.update_editor(|editor, window, cx| {
12691 editor.move_left(&MoveLeft, window, cx);
12692 });
12693 cx.run_until_parked();
12694 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12695 cx.assert_editor_state("obj.aˇb");
12696 cx.update_editor(|editor, _, _| {
12697 assert_eq!(editor.context_menu_visible(), false);
12698 });
12699
12700 // Type "b" - new request
12701 cx.simulate_keystroke("b");
12702 let is_incomplete = false;
12703 handle_completion_request(
12704 "obj.<ab|>a",
12705 vec!["ab", "abc"],
12706 is_incomplete,
12707 counter.clone(),
12708 &mut cx,
12709 )
12710 .await;
12711 cx.run_until_parked();
12712 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12713 cx.assert_editor_state("obj.abˇb");
12714 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12715
12716 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
12717 cx.update_editor(|editor, window, cx| {
12718 editor.backspace(&Backspace, window, cx);
12719 });
12720 let is_incomplete = false;
12721 handle_completion_request(
12722 "obj.<a|>b",
12723 vec!["a", "ab", "abc"],
12724 is_incomplete,
12725 counter.clone(),
12726 &mut cx,
12727 )
12728 .await;
12729 cx.run_until_parked();
12730 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12731 cx.assert_editor_state("obj.aˇb");
12732 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12733
12734 // Backspace to delete "a" - dismisses menu.
12735 cx.update_editor(|editor, window, cx| {
12736 editor.backspace(&Backspace, window, cx);
12737 });
12738 cx.run_until_parked();
12739 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12740 cx.assert_editor_state("obj.ˇb");
12741 cx.update_editor(|editor, _, _| {
12742 assert_eq!(editor.context_menu_visible(), false);
12743 });
12744}
12745
12746#[gpui::test]
12747async fn test_word_completion(cx: &mut TestAppContext) {
12748 let lsp_fetch_timeout_ms = 10;
12749 init_test(cx, |language_settings| {
12750 language_settings.defaults.completions = Some(CompletionSettings {
12751 words: WordsCompletionMode::Fallback,
12752 lsp: true,
12753 lsp_fetch_timeout_ms: 10,
12754 lsp_insert_mode: LspInsertMode::Insert,
12755 });
12756 });
12757
12758 let mut cx = EditorLspTestContext::new_rust(
12759 lsp::ServerCapabilities {
12760 completion_provider: Some(lsp::CompletionOptions {
12761 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12762 ..lsp::CompletionOptions::default()
12763 }),
12764 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12765 ..lsp::ServerCapabilities::default()
12766 },
12767 cx,
12768 )
12769 .await;
12770
12771 let throttle_completions = Arc::new(AtomicBool::new(false));
12772
12773 let lsp_throttle_completions = throttle_completions.clone();
12774 let _completion_requests_handler =
12775 cx.lsp
12776 .server
12777 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
12778 let lsp_throttle_completions = lsp_throttle_completions.clone();
12779 let cx = cx.clone();
12780 async move {
12781 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
12782 cx.background_executor()
12783 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
12784 .await;
12785 }
12786 Ok(Some(lsp::CompletionResponse::Array(vec![
12787 lsp::CompletionItem {
12788 label: "first".into(),
12789 ..lsp::CompletionItem::default()
12790 },
12791 lsp::CompletionItem {
12792 label: "last".into(),
12793 ..lsp::CompletionItem::default()
12794 },
12795 ])))
12796 }
12797 });
12798
12799 cx.set_state(indoc! {"
12800 oneˇ
12801 two
12802 three
12803 "});
12804 cx.simulate_keystroke(".");
12805 cx.executor().run_until_parked();
12806 cx.condition(|editor, _| editor.context_menu_visible())
12807 .await;
12808 cx.update_editor(|editor, window, cx| {
12809 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12810 {
12811 assert_eq!(
12812 completion_menu_entries(&menu),
12813 &["first", "last"],
12814 "When LSP server is fast to reply, no fallback word completions are used"
12815 );
12816 } else {
12817 panic!("expected completion menu to be open");
12818 }
12819 editor.cancel(&Cancel, window, cx);
12820 });
12821 cx.executor().run_until_parked();
12822 cx.condition(|editor, _| !editor.context_menu_visible())
12823 .await;
12824
12825 throttle_completions.store(true, atomic::Ordering::Release);
12826 cx.simulate_keystroke(".");
12827 cx.executor()
12828 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
12829 cx.executor().run_until_parked();
12830 cx.condition(|editor, _| editor.context_menu_visible())
12831 .await;
12832 cx.update_editor(|editor, _, _| {
12833 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12834 {
12835 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
12836 "When LSP server is slow, document words can be shown instead, if configured accordingly");
12837 } else {
12838 panic!("expected completion menu to be open");
12839 }
12840 });
12841}
12842
12843#[gpui::test]
12844async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
12845 init_test(cx, |language_settings| {
12846 language_settings.defaults.completions = Some(CompletionSettings {
12847 words: WordsCompletionMode::Enabled,
12848 lsp: true,
12849 lsp_fetch_timeout_ms: 0,
12850 lsp_insert_mode: LspInsertMode::Insert,
12851 });
12852 });
12853
12854 let mut cx = EditorLspTestContext::new_rust(
12855 lsp::ServerCapabilities {
12856 completion_provider: Some(lsp::CompletionOptions {
12857 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12858 ..lsp::CompletionOptions::default()
12859 }),
12860 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12861 ..lsp::ServerCapabilities::default()
12862 },
12863 cx,
12864 )
12865 .await;
12866
12867 let _completion_requests_handler =
12868 cx.lsp
12869 .server
12870 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12871 Ok(Some(lsp::CompletionResponse::Array(vec![
12872 lsp::CompletionItem {
12873 label: "first".into(),
12874 ..lsp::CompletionItem::default()
12875 },
12876 lsp::CompletionItem {
12877 label: "last".into(),
12878 ..lsp::CompletionItem::default()
12879 },
12880 ])))
12881 });
12882
12883 cx.set_state(indoc! {"ˇ
12884 first
12885 last
12886 second
12887 "});
12888 cx.simulate_keystroke(".");
12889 cx.executor().run_until_parked();
12890 cx.condition(|editor, _| editor.context_menu_visible())
12891 .await;
12892 cx.update_editor(|editor, _, _| {
12893 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12894 {
12895 assert_eq!(
12896 completion_menu_entries(&menu),
12897 &["first", "last", "second"],
12898 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
12899 );
12900 } else {
12901 panic!("expected completion menu to be open");
12902 }
12903 });
12904}
12905
12906#[gpui::test]
12907async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
12908 init_test(cx, |language_settings| {
12909 language_settings.defaults.completions = Some(CompletionSettings {
12910 words: WordsCompletionMode::Disabled,
12911 lsp: true,
12912 lsp_fetch_timeout_ms: 0,
12913 lsp_insert_mode: LspInsertMode::Insert,
12914 });
12915 });
12916
12917 let mut cx = EditorLspTestContext::new_rust(
12918 lsp::ServerCapabilities {
12919 completion_provider: Some(lsp::CompletionOptions {
12920 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12921 ..lsp::CompletionOptions::default()
12922 }),
12923 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12924 ..lsp::ServerCapabilities::default()
12925 },
12926 cx,
12927 )
12928 .await;
12929
12930 let _completion_requests_handler =
12931 cx.lsp
12932 .server
12933 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12934 panic!("LSP completions should not be queried when dealing with word completions")
12935 });
12936
12937 cx.set_state(indoc! {"ˇ
12938 first
12939 last
12940 second
12941 "});
12942 cx.update_editor(|editor, window, cx| {
12943 editor.show_word_completions(&ShowWordCompletions, window, cx);
12944 });
12945 cx.executor().run_until_parked();
12946 cx.condition(|editor, _| editor.context_menu_visible())
12947 .await;
12948 cx.update_editor(|editor, _, _| {
12949 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12950 {
12951 assert_eq!(
12952 completion_menu_entries(&menu),
12953 &["first", "last", "second"],
12954 "`ShowWordCompletions` action should show word completions"
12955 );
12956 } else {
12957 panic!("expected completion menu to be open");
12958 }
12959 });
12960
12961 cx.simulate_keystroke("l");
12962 cx.executor().run_until_parked();
12963 cx.condition(|editor, _| editor.context_menu_visible())
12964 .await;
12965 cx.update_editor(|editor, _, _| {
12966 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12967 {
12968 assert_eq!(
12969 completion_menu_entries(&menu),
12970 &["last"],
12971 "After showing word completions, further editing should filter them and not query the LSP"
12972 );
12973 } else {
12974 panic!("expected completion menu to be open");
12975 }
12976 });
12977}
12978
12979#[gpui::test]
12980async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
12981 init_test(cx, |language_settings| {
12982 language_settings.defaults.completions = Some(CompletionSettings {
12983 words: WordsCompletionMode::Fallback,
12984 lsp: false,
12985 lsp_fetch_timeout_ms: 0,
12986 lsp_insert_mode: LspInsertMode::Insert,
12987 });
12988 });
12989
12990 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12991
12992 cx.set_state(indoc! {"ˇ
12993 0_usize
12994 let
12995 33
12996 4.5f32
12997 "});
12998 cx.update_editor(|editor, window, cx| {
12999 editor.show_completions(&ShowCompletions::default(), window, cx);
13000 });
13001 cx.executor().run_until_parked();
13002 cx.condition(|editor, _| editor.context_menu_visible())
13003 .await;
13004 cx.update_editor(|editor, window, cx| {
13005 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13006 {
13007 assert_eq!(
13008 completion_menu_entries(&menu),
13009 &["let"],
13010 "With no digits in the completion query, no digits should be in the word completions"
13011 );
13012 } else {
13013 panic!("expected completion menu to be open");
13014 }
13015 editor.cancel(&Cancel, window, cx);
13016 });
13017
13018 cx.set_state(indoc! {"3ˇ
13019 0_usize
13020 let
13021 3
13022 33.35f32
13023 "});
13024 cx.update_editor(|editor, window, cx| {
13025 editor.show_completions(&ShowCompletions::default(), window, cx);
13026 });
13027 cx.executor().run_until_parked();
13028 cx.condition(|editor, _| editor.context_menu_visible())
13029 .await;
13030 cx.update_editor(|editor, _, _| {
13031 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13032 {
13033 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
13034 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
13035 } else {
13036 panic!("expected completion menu to be open");
13037 }
13038 });
13039}
13040
13041fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
13042 let position = || lsp::Position {
13043 line: params.text_document_position.position.line,
13044 character: params.text_document_position.position.character,
13045 };
13046 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13047 range: lsp::Range {
13048 start: position(),
13049 end: position(),
13050 },
13051 new_text: text.to_string(),
13052 }))
13053}
13054
13055#[gpui::test]
13056async fn test_multiline_completion(cx: &mut TestAppContext) {
13057 init_test(cx, |_| {});
13058
13059 let fs = FakeFs::new(cx.executor());
13060 fs.insert_tree(
13061 path!("/a"),
13062 json!({
13063 "main.ts": "a",
13064 }),
13065 )
13066 .await;
13067
13068 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13069 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13070 let typescript_language = Arc::new(Language::new(
13071 LanguageConfig {
13072 name: "TypeScript".into(),
13073 matcher: LanguageMatcher {
13074 path_suffixes: vec!["ts".to_string()],
13075 ..LanguageMatcher::default()
13076 },
13077 line_comments: vec!["// ".into()],
13078 ..LanguageConfig::default()
13079 },
13080 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13081 ));
13082 language_registry.add(typescript_language.clone());
13083 let mut fake_servers = language_registry.register_fake_lsp(
13084 "TypeScript",
13085 FakeLspAdapter {
13086 capabilities: lsp::ServerCapabilities {
13087 completion_provider: Some(lsp::CompletionOptions {
13088 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13089 ..lsp::CompletionOptions::default()
13090 }),
13091 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13092 ..lsp::ServerCapabilities::default()
13093 },
13094 // Emulate vtsls label generation
13095 label_for_completion: Some(Box::new(|item, _| {
13096 let text = if let Some(description) = item
13097 .label_details
13098 .as_ref()
13099 .and_then(|label_details| label_details.description.as_ref())
13100 {
13101 format!("{} {}", item.label, description)
13102 } else if let Some(detail) = &item.detail {
13103 format!("{} {}", item.label, detail)
13104 } else {
13105 item.label.clone()
13106 };
13107 let len = text.len();
13108 Some(language::CodeLabel {
13109 text,
13110 runs: Vec::new(),
13111 filter_range: 0..len,
13112 })
13113 })),
13114 ..FakeLspAdapter::default()
13115 },
13116 );
13117 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13118 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13119 let worktree_id = workspace
13120 .update(cx, |workspace, _window, cx| {
13121 workspace.project().update(cx, |project, cx| {
13122 project.worktrees(cx).next().unwrap().read(cx).id()
13123 })
13124 })
13125 .unwrap();
13126 let _buffer = project
13127 .update(cx, |project, cx| {
13128 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
13129 })
13130 .await
13131 .unwrap();
13132 let editor = workspace
13133 .update(cx, |workspace, window, cx| {
13134 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
13135 })
13136 .unwrap()
13137 .await
13138 .unwrap()
13139 .downcast::<Editor>()
13140 .unwrap();
13141 let fake_server = fake_servers.next().await.unwrap();
13142
13143 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
13144 let multiline_label_2 = "a\nb\nc\n";
13145 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
13146 let multiline_description = "d\ne\nf\n";
13147 let multiline_detail_2 = "g\nh\ni\n";
13148
13149 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
13150 move |params, _| async move {
13151 Ok(Some(lsp::CompletionResponse::Array(vec![
13152 lsp::CompletionItem {
13153 label: multiline_label.to_string(),
13154 text_edit: gen_text_edit(¶ms, "new_text_1"),
13155 ..lsp::CompletionItem::default()
13156 },
13157 lsp::CompletionItem {
13158 label: "single line label 1".to_string(),
13159 detail: Some(multiline_detail.to_string()),
13160 text_edit: gen_text_edit(¶ms, "new_text_2"),
13161 ..lsp::CompletionItem::default()
13162 },
13163 lsp::CompletionItem {
13164 label: "single line label 2".to_string(),
13165 label_details: Some(lsp::CompletionItemLabelDetails {
13166 description: Some(multiline_description.to_string()),
13167 detail: None,
13168 }),
13169 text_edit: gen_text_edit(¶ms, "new_text_2"),
13170 ..lsp::CompletionItem::default()
13171 },
13172 lsp::CompletionItem {
13173 label: multiline_label_2.to_string(),
13174 detail: Some(multiline_detail_2.to_string()),
13175 text_edit: gen_text_edit(¶ms, "new_text_3"),
13176 ..lsp::CompletionItem::default()
13177 },
13178 lsp::CompletionItem {
13179 label: "Label with many spaces and \t but without newlines".to_string(),
13180 detail: Some(
13181 "Details with many spaces and \t but without newlines".to_string(),
13182 ),
13183 text_edit: gen_text_edit(¶ms, "new_text_4"),
13184 ..lsp::CompletionItem::default()
13185 },
13186 ])))
13187 },
13188 );
13189
13190 editor.update_in(cx, |editor, window, cx| {
13191 cx.focus_self(window);
13192 editor.move_to_end(&MoveToEnd, window, cx);
13193 editor.handle_input(".", window, cx);
13194 });
13195 cx.run_until_parked();
13196 completion_handle.next().await.unwrap();
13197
13198 editor.update(cx, |editor, _| {
13199 assert!(editor.context_menu_visible());
13200 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13201 {
13202 let completion_labels = menu
13203 .completions
13204 .borrow()
13205 .iter()
13206 .map(|c| c.label.text.clone())
13207 .collect::<Vec<_>>();
13208 assert_eq!(
13209 completion_labels,
13210 &[
13211 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
13212 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
13213 "single line label 2 d e f ",
13214 "a b c g h i ",
13215 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
13216 ],
13217 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
13218 );
13219
13220 for completion in menu
13221 .completions
13222 .borrow()
13223 .iter() {
13224 assert_eq!(
13225 completion.label.filter_range,
13226 0..completion.label.text.len(),
13227 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
13228 );
13229 }
13230 } else {
13231 panic!("expected completion menu to be open");
13232 }
13233 });
13234}
13235
13236#[gpui::test]
13237async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
13238 init_test(cx, |_| {});
13239 let mut cx = EditorLspTestContext::new_rust(
13240 lsp::ServerCapabilities {
13241 completion_provider: Some(lsp::CompletionOptions {
13242 trigger_characters: Some(vec![".".to_string()]),
13243 ..Default::default()
13244 }),
13245 ..Default::default()
13246 },
13247 cx,
13248 )
13249 .await;
13250 cx.lsp
13251 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13252 Ok(Some(lsp::CompletionResponse::Array(vec![
13253 lsp::CompletionItem {
13254 label: "first".into(),
13255 ..Default::default()
13256 },
13257 lsp::CompletionItem {
13258 label: "last".into(),
13259 ..Default::default()
13260 },
13261 ])))
13262 });
13263 cx.set_state("variableˇ");
13264 cx.simulate_keystroke(".");
13265 cx.executor().run_until_parked();
13266
13267 cx.update_editor(|editor, _, _| {
13268 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13269 {
13270 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
13271 } else {
13272 panic!("expected completion menu to be open");
13273 }
13274 });
13275
13276 cx.update_editor(|editor, window, cx| {
13277 editor.move_page_down(&MovePageDown::default(), window, cx);
13278 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13279 {
13280 assert!(
13281 menu.selected_item == 1,
13282 "expected PageDown to select the last item from the context menu"
13283 );
13284 } else {
13285 panic!("expected completion menu to stay open after PageDown");
13286 }
13287 });
13288
13289 cx.update_editor(|editor, window, cx| {
13290 editor.move_page_up(&MovePageUp::default(), window, cx);
13291 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13292 {
13293 assert!(
13294 menu.selected_item == 0,
13295 "expected PageUp to select the first item from the context menu"
13296 );
13297 } else {
13298 panic!("expected completion menu to stay open after PageUp");
13299 }
13300 });
13301}
13302
13303#[gpui::test]
13304async fn test_as_is_completions(cx: &mut TestAppContext) {
13305 init_test(cx, |_| {});
13306 let mut cx = EditorLspTestContext::new_rust(
13307 lsp::ServerCapabilities {
13308 completion_provider: Some(lsp::CompletionOptions {
13309 ..Default::default()
13310 }),
13311 ..Default::default()
13312 },
13313 cx,
13314 )
13315 .await;
13316 cx.lsp
13317 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13318 Ok(Some(lsp::CompletionResponse::Array(vec![
13319 lsp::CompletionItem {
13320 label: "unsafe".into(),
13321 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13322 range: lsp::Range {
13323 start: lsp::Position {
13324 line: 1,
13325 character: 2,
13326 },
13327 end: lsp::Position {
13328 line: 1,
13329 character: 3,
13330 },
13331 },
13332 new_text: "unsafe".to_string(),
13333 })),
13334 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
13335 ..Default::default()
13336 },
13337 ])))
13338 });
13339 cx.set_state("fn a() {}\n nˇ");
13340 cx.executor().run_until_parked();
13341 cx.update_editor(|editor, window, cx| {
13342 editor.show_completions(
13343 &ShowCompletions {
13344 trigger: Some("\n".into()),
13345 },
13346 window,
13347 cx,
13348 );
13349 });
13350 cx.executor().run_until_parked();
13351
13352 cx.update_editor(|editor, window, cx| {
13353 editor.confirm_completion(&Default::default(), window, cx)
13354 });
13355 cx.executor().run_until_parked();
13356 cx.assert_editor_state("fn a() {}\n unsafeˇ");
13357}
13358
13359#[gpui::test]
13360async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
13361 init_test(cx, |_| {});
13362
13363 let mut cx = EditorLspTestContext::new_rust(
13364 lsp::ServerCapabilities {
13365 completion_provider: Some(lsp::CompletionOptions {
13366 trigger_characters: Some(vec![".".to_string()]),
13367 resolve_provider: Some(true),
13368 ..Default::default()
13369 }),
13370 ..Default::default()
13371 },
13372 cx,
13373 )
13374 .await;
13375
13376 cx.set_state("fn main() { let a = 2ˇ; }");
13377 cx.simulate_keystroke(".");
13378 let completion_item = lsp::CompletionItem {
13379 label: "Some".into(),
13380 kind: Some(lsp::CompletionItemKind::SNIPPET),
13381 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13382 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13383 kind: lsp::MarkupKind::Markdown,
13384 value: "```rust\nSome(2)\n```".to_string(),
13385 })),
13386 deprecated: Some(false),
13387 sort_text: Some("Some".to_string()),
13388 filter_text: Some("Some".to_string()),
13389 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13390 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13391 range: lsp::Range {
13392 start: lsp::Position {
13393 line: 0,
13394 character: 22,
13395 },
13396 end: lsp::Position {
13397 line: 0,
13398 character: 22,
13399 },
13400 },
13401 new_text: "Some(2)".to_string(),
13402 })),
13403 additional_text_edits: Some(vec![lsp::TextEdit {
13404 range: lsp::Range {
13405 start: lsp::Position {
13406 line: 0,
13407 character: 20,
13408 },
13409 end: lsp::Position {
13410 line: 0,
13411 character: 22,
13412 },
13413 },
13414 new_text: "".to_string(),
13415 }]),
13416 ..Default::default()
13417 };
13418
13419 let closure_completion_item = completion_item.clone();
13420 let counter = Arc::new(AtomicUsize::new(0));
13421 let counter_clone = counter.clone();
13422 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13423 let task_completion_item = closure_completion_item.clone();
13424 counter_clone.fetch_add(1, atomic::Ordering::Release);
13425 async move {
13426 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13427 is_incomplete: true,
13428 item_defaults: None,
13429 items: vec![task_completion_item],
13430 })))
13431 }
13432 });
13433
13434 cx.condition(|editor, _| editor.context_menu_visible())
13435 .await;
13436 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
13437 assert!(request.next().await.is_some());
13438 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13439
13440 cx.simulate_keystrokes("S o m");
13441 cx.condition(|editor, _| editor.context_menu_visible())
13442 .await;
13443 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
13444 assert!(request.next().await.is_some());
13445 assert!(request.next().await.is_some());
13446 assert!(request.next().await.is_some());
13447 request.close();
13448 assert!(request.next().await.is_none());
13449 assert_eq!(
13450 counter.load(atomic::Ordering::Acquire),
13451 4,
13452 "With the completions menu open, only one LSP request should happen per input"
13453 );
13454}
13455
13456#[gpui::test]
13457async fn test_toggle_comment(cx: &mut TestAppContext) {
13458 init_test(cx, |_| {});
13459 let mut cx = EditorTestContext::new(cx).await;
13460 let language = Arc::new(Language::new(
13461 LanguageConfig {
13462 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13463 ..Default::default()
13464 },
13465 Some(tree_sitter_rust::LANGUAGE.into()),
13466 ));
13467 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13468
13469 // If multiple selections intersect a line, the line is only toggled once.
13470 cx.set_state(indoc! {"
13471 fn a() {
13472 «//b();
13473 ˇ»// «c();
13474 //ˇ» d();
13475 }
13476 "});
13477
13478 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13479
13480 cx.assert_editor_state(indoc! {"
13481 fn a() {
13482 «b();
13483 c();
13484 ˇ» d();
13485 }
13486 "});
13487
13488 // The comment prefix is inserted at the same column for every line in a
13489 // selection.
13490 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13491
13492 cx.assert_editor_state(indoc! {"
13493 fn a() {
13494 // «b();
13495 // c();
13496 ˇ»// d();
13497 }
13498 "});
13499
13500 // If a selection ends at the beginning of a line, that line is not toggled.
13501 cx.set_selections_state(indoc! {"
13502 fn a() {
13503 // b();
13504 «// c();
13505 ˇ» // d();
13506 }
13507 "});
13508
13509 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13510
13511 cx.assert_editor_state(indoc! {"
13512 fn a() {
13513 // b();
13514 «c();
13515 ˇ» // d();
13516 }
13517 "});
13518
13519 // If a selection span a single line and is empty, the line is toggled.
13520 cx.set_state(indoc! {"
13521 fn a() {
13522 a();
13523 b();
13524 ˇ
13525 }
13526 "});
13527
13528 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13529
13530 cx.assert_editor_state(indoc! {"
13531 fn a() {
13532 a();
13533 b();
13534 //•ˇ
13535 }
13536 "});
13537
13538 // If a selection span multiple lines, empty lines are not toggled.
13539 cx.set_state(indoc! {"
13540 fn a() {
13541 «a();
13542
13543 c();ˇ»
13544 }
13545 "});
13546
13547 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13548
13549 cx.assert_editor_state(indoc! {"
13550 fn a() {
13551 // «a();
13552
13553 // c();ˇ»
13554 }
13555 "});
13556
13557 // If a selection includes multiple comment prefixes, all lines are uncommented.
13558 cx.set_state(indoc! {"
13559 fn a() {
13560 «// a();
13561 /// b();
13562 //! c();ˇ»
13563 }
13564 "});
13565
13566 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13567
13568 cx.assert_editor_state(indoc! {"
13569 fn a() {
13570 «a();
13571 b();
13572 c();ˇ»
13573 }
13574 "});
13575}
13576
13577#[gpui::test]
13578async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
13579 init_test(cx, |_| {});
13580 let mut cx = EditorTestContext::new(cx).await;
13581 let language = Arc::new(Language::new(
13582 LanguageConfig {
13583 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13584 ..Default::default()
13585 },
13586 Some(tree_sitter_rust::LANGUAGE.into()),
13587 ));
13588 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13589
13590 let toggle_comments = &ToggleComments {
13591 advance_downwards: false,
13592 ignore_indent: true,
13593 };
13594
13595 // If multiple selections intersect a line, the line is only toggled once.
13596 cx.set_state(indoc! {"
13597 fn a() {
13598 // «b();
13599 // c();
13600 // ˇ» d();
13601 }
13602 "});
13603
13604 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13605
13606 cx.assert_editor_state(indoc! {"
13607 fn a() {
13608 «b();
13609 c();
13610 ˇ» d();
13611 }
13612 "});
13613
13614 // The comment prefix is inserted at the beginning of each line
13615 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13616
13617 cx.assert_editor_state(indoc! {"
13618 fn a() {
13619 // «b();
13620 // c();
13621 // ˇ» d();
13622 }
13623 "});
13624
13625 // If a selection ends at the beginning of a line, that line is not toggled.
13626 cx.set_selections_state(indoc! {"
13627 fn a() {
13628 // b();
13629 // «c();
13630 ˇ»// d();
13631 }
13632 "});
13633
13634 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13635
13636 cx.assert_editor_state(indoc! {"
13637 fn a() {
13638 // b();
13639 «c();
13640 ˇ»// d();
13641 }
13642 "});
13643
13644 // If a selection span a single line and is empty, the line is toggled.
13645 cx.set_state(indoc! {"
13646 fn a() {
13647 a();
13648 b();
13649 ˇ
13650 }
13651 "});
13652
13653 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13654
13655 cx.assert_editor_state(indoc! {"
13656 fn a() {
13657 a();
13658 b();
13659 //ˇ
13660 }
13661 "});
13662
13663 // If a selection span multiple lines, empty lines are not toggled.
13664 cx.set_state(indoc! {"
13665 fn a() {
13666 «a();
13667
13668 c();ˇ»
13669 }
13670 "});
13671
13672 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13673
13674 cx.assert_editor_state(indoc! {"
13675 fn a() {
13676 // «a();
13677
13678 // c();ˇ»
13679 }
13680 "});
13681
13682 // If a selection includes multiple comment prefixes, all lines are uncommented.
13683 cx.set_state(indoc! {"
13684 fn a() {
13685 // «a();
13686 /// b();
13687 //! c();ˇ»
13688 }
13689 "});
13690
13691 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13692
13693 cx.assert_editor_state(indoc! {"
13694 fn a() {
13695 «a();
13696 b();
13697 c();ˇ»
13698 }
13699 "});
13700}
13701
13702#[gpui::test]
13703async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
13704 init_test(cx, |_| {});
13705
13706 let language = Arc::new(Language::new(
13707 LanguageConfig {
13708 line_comments: vec!["// ".into()],
13709 ..Default::default()
13710 },
13711 Some(tree_sitter_rust::LANGUAGE.into()),
13712 ));
13713
13714 let mut cx = EditorTestContext::new(cx).await;
13715
13716 cx.language_registry().add(language.clone());
13717 cx.update_buffer(|buffer, cx| {
13718 buffer.set_language(Some(language), cx);
13719 });
13720
13721 let toggle_comments = &ToggleComments {
13722 advance_downwards: true,
13723 ignore_indent: false,
13724 };
13725
13726 // Single cursor on one line -> advance
13727 // Cursor moves horizontally 3 characters as well on non-blank line
13728 cx.set_state(indoc!(
13729 "fn a() {
13730 ˇdog();
13731 cat();
13732 }"
13733 ));
13734 cx.update_editor(|editor, window, cx| {
13735 editor.toggle_comments(toggle_comments, window, cx);
13736 });
13737 cx.assert_editor_state(indoc!(
13738 "fn a() {
13739 // dog();
13740 catˇ();
13741 }"
13742 ));
13743
13744 // Single selection on one line -> don't advance
13745 cx.set_state(indoc!(
13746 "fn a() {
13747 «dog()ˇ»;
13748 cat();
13749 }"
13750 ));
13751 cx.update_editor(|editor, window, cx| {
13752 editor.toggle_comments(toggle_comments, window, cx);
13753 });
13754 cx.assert_editor_state(indoc!(
13755 "fn a() {
13756 // «dog()ˇ»;
13757 cat();
13758 }"
13759 ));
13760
13761 // Multiple cursors on one line -> advance
13762 cx.set_state(indoc!(
13763 "fn a() {
13764 ˇdˇog();
13765 cat();
13766 }"
13767 ));
13768 cx.update_editor(|editor, window, cx| {
13769 editor.toggle_comments(toggle_comments, window, cx);
13770 });
13771 cx.assert_editor_state(indoc!(
13772 "fn a() {
13773 // dog();
13774 catˇ(ˇ);
13775 }"
13776 ));
13777
13778 // Multiple cursors on one line, with selection -> don't advance
13779 cx.set_state(indoc!(
13780 "fn a() {
13781 ˇdˇog«()ˇ»;
13782 cat();
13783 }"
13784 ));
13785 cx.update_editor(|editor, window, cx| {
13786 editor.toggle_comments(toggle_comments, window, cx);
13787 });
13788 cx.assert_editor_state(indoc!(
13789 "fn a() {
13790 // ˇdˇog«()ˇ»;
13791 cat();
13792 }"
13793 ));
13794
13795 // Single cursor on one line -> advance
13796 // Cursor moves to column 0 on blank line
13797 cx.set_state(indoc!(
13798 "fn a() {
13799 ˇdog();
13800
13801 cat();
13802 }"
13803 ));
13804 cx.update_editor(|editor, window, cx| {
13805 editor.toggle_comments(toggle_comments, window, cx);
13806 });
13807 cx.assert_editor_state(indoc!(
13808 "fn a() {
13809 // dog();
13810 ˇ
13811 cat();
13812 }"
13813 ));
13814
13815 // Single cursor on one line -> advance
13816 // Cursor starts and ends at column 0
13817 cx.set_state(indoc!(
13818 "fn a() {
13819 ˇ dog();
13820 cat();
13821 }"
13822 ));
13823 cx.update_editor(|editor, window, cx| {
13824 editor.toggle_comments(toggle_comments, window, cx);
13825 });
13826 cx.assert_editor_state(indoc!(
13827 "fn a() {
13828 // dog();
13829 ˇ cat();
13830 }"
13831 ));
13832}
13833
13834#[gpui::test]
13835async fn test_toggle_block_comment(cx: &mut TestAppContext) {
13836 init_test(cx, |_| {});
13837
13838 let mut cx = EditorTestContext::new(cx).await;
13839
13840 let html_language = Arc::new(
13841 Language::new(
13842 LanguageConfig {
13843 name: "HTML".into(),
13844 block_comment: Some(BlockCommentConfig {
13845 start: "<!-- ".into(),
13846 prefix: "".into(),
13847 end: " -->".into(),
13848 tab_size: 0,
13849 }),
13850 ..Default::default()
13851 },
13852 Some(tree_sitter_html::LANGUAGE.into()),
13853 )
13854 .with_injection_query(
13855 r#"
13856 (script_element
13857 (raw_text) @injection.content
13858 (#set! injection.language "javascript"))
13859 "#,
13860 )
13861 .unwrap(),
13862 );
13863
13864 let javascript_language = Arc::new(Language::new(
13865 LanguageConfig {
13866 name: "JavaScript".into(),
13867 line_comments: vec!["// ".into()],
13868 ..Default::default()
13869 },
13870 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13871 ));
13872
13873 cx.language_registry().add(html_language.clone());
13874 cx.language_registry().add(javascript_language.clone());
13875 cx.update_buffer(|buffer, cx| {
13876 buffer.set_language(Some(html_language), cx);
13877 });
13878
13879 // Toggle comments for empty selections
13880 cx.set_state(
13881 &r#"
13882 <p>A</p>ˇ
13883 <p>B</p>ˇ
13884 <p>C</p>ˇ
13885 "#
13886 .unindent(),
13887 );
13888 cx.update_editor(|editor, window, cx| {
13889 editor.toggle_comments(&ToggleComments::default(), window, cx)
13890 });
13891 cx.assert_editor_state(
13892 &r#"
13893 <!-- <p>A</p>ˇ -->
13894 <!-- <p>B</p>ˇ -->
13895 <!-- <p>C</p>ˇ -->
13896 "#
13897 .unindent(),
13898 );
13899 cx.update_editor(|editor, window, cx| {
13900 editor.toggle_comments(&ToggleComments::default(), window, cx)
13901 });
13902 cx.assert_editor_state(
13903 &r#"
13904 <p>A</p>ˇ
13905 <p>B</p>ˇ
13906 <p>C</p>ˇ
13907 "#
13908 .unindent(),
13909 );
13910
13911 // Toggle comments for mixture of empty and non-empty selections, where
13912 // multiple selections occupy a given line.
13913 cx.set_state(
13914 &r#"
13915 <p>A«</p>
13916 <p>ˇ»B</p>ˇ
13917 <p>C«</p>
13918 <p>ˇ»D</p>ˇ
13919 "#
13920 .unindent(),
13921 );
13922
13923 cx.update_editor(|editor, window, cx| {
13924 editor.toggle_comments(&ToggleComments::default(), window, cx)
13925 });
13926 cx.assert_editor_state(
13927 &r#"
13928 <!-- <p>A«</p>
13929 <p>ˇ»B</p>ˇ -->
13930 <!-- <p>C«</p>
13931 <p>ˇ»D</p>ˇ -->
13932 "#
13933 .unindent(),
13934 );
13935 cx.update_editor(|editor, window, cx| {
13936 editor.toggle_comments(&ToggleComments::default(), window, cx)
13937 });
13938 cx.assert_editor_state(
13939 &r#"
13940 <p>A«</p>
13941 <p>ˇ»B</p>ˇ
13942 <p>C«</p>
13943 <p>ˇ»D</p>ˇ
13944 "#
13945 .unindent(),
13946 );
13947
13948 // Toggle comments when different languages are active for different
13949 // selections.
13950 cx.set_state(
13951 &r#"
13952 ˇ<script>
13953 ˇvar x = new Y();
13954 ˇ</script>
13955 "#
13956 .unindent(),
13957 );
13958 cx.executor().run_until_parked();
13959 cx.update_editor(|editor, window, cx| {
13960 editor.toggle_comments(&ToggleComments::default(), window, cx)
13961 });
13962 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
13963 // Uncommenting and commenting from this position brings in even more wrong artifacts.
13964 cx.assert_editor_state(
13965 &r#"
13966 <!-- ˇ<script> -->
13967 // ˇvar x = new Y();
13968 <!-- ˇ</script> -->
13969 "#
13970 .unindent(),
13971 );
13972}
13973
13974#[gpui::test]
13975fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
13976 init_test(cx, |_| {});
13977
13978 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13979 let multibuffer = cx.new(|cx| {
13980 let mut multibuffer = MultiBuffer::new(ReadWrite);
13981 multibuffer.push_excerpts(
13982 buffer.clone(),
13983 [
13984 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
13985 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
13986 ],
13987 cx,
13988 );
13989 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
13990 multibuffer
13991 });
13992
13993 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13994 editor.update_in(cx, |editor, window, cx| {
13995 assert_eq!(editor.text(cx), "aaaa\nbbbb");
13996 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13997 s.select_ranges([
13998 Point::new(0, 0)..Point::new(0, 0),
13999 Point::new(1, 0)..Point::new(1, 0),
14000 ])
14001 });
14002
14003 editor.handle_input("X", window, cx);
14004 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
14005 assert_eq!(
14006 editor.selections.ranges(cx),
14007 [
14008 Point::new(0, 1)..Point::new(0, 1),
14009 Point::new(1, 1)..Point::new(1, 1),
14010 ]
14011 );
14012
14013 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
14014 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14015 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
14016 });
14017 editor.backspace(&Default::default(), window, cx);
14018 assert_eq!(editor.text(cx), "Xa\nbbb");
14019 assert_eq!(
14020 editor.selections.ranges(cx),
14021 [Point::new(1, 0)..Point::new(1, 0)]
14022 );
14023
14024 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14025 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
14026 });
14027 editor.backspace(&Default::default(), window, cx);
14028 assert_eq!(editor.text(cx), "X\nbb");
14029 assert_eq!(
14030 editor.selections.ranges(cx),
14031 [Point::new(0, 1)..Point::new(0, 1)]
14032 );
14033 });
14034}
14035
14036#[gpui::test]
14037fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
14038 init_test(cx, |_| {});
14039
14040 let markers = vec![('[', ']').into(), ('(', ')').into()];
14041 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
14042 indoc! {"
14043 [aaaa
14044 (bbbb]
14045 cccc)",
14046 },
14047 markers.clone(),
14048 );
14049 let excerpt_ranges = markers.into_iter().map(|marker| {
14050 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
14051 ExcerptRange::new(context.clone())
14052 });
14053 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
14054 let multibuffer = cx.new(|cx| {
14055 let mut multibuffer = MultiBuffer::new(ReadWrite);
14056 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
14057 multibuffer
14058 });
14059
14060 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
14061 editor.update_in(cx, |editor, window, cx| {
14062 let (expected_text, selection_ranges) = marked_text_ranges(
14063 indoc! {"
14064 aaaa
14065 bˇbbb
14066 bˇbbˇb
14067 cccc"
14068 },
14069 true,
14070 );
14071 assert_eq!(editor.text(cx), expected_text);
14072 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14073 s.select_ranges(selection_ranges)
14074 });
14075
14076 editor.handle_input("X", window, cx);
14077
14078 let (expected_text, expected_selections) = marked_text_ranges(
14079 indoc! {"
14080 aaaa
14081 bXˇbbXb
14082 bXˇbbXˇb
14083 cccc"
14084 },
14085 false,
14086 );
14087 assert_eq!(editor.text(cx), expected_text);
14088 assert_eq!(editor.selections.ranges(cx), expected_selections);
14089
14090 editor.newline(&Newline, window, cx);
14091 let (expected_text, expected_selections) = marked_text_ranges(
14092 indoc! {"
14093 aaaa
14094 bX
14095 ˇbbX
14096 b
14097 bX
14098 ˇbbX
14099 ˇb
14100 cccc"
14101 },
14102 false,
14103 );
14104 assert_eq!(editor.text(cx), expected_text);
14105 assert_eq!(editor.selections.ranges(cx), expected_selections);
14106 });
14107}
14108
14109#[gpui::test]
14110fn test_refresh_selections(cx: &mut TestAppContext) {
14111 init_test(cx, |_| {});
14112
14113 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14114 let mut excerpt1_id = None;
14115 let multibuffer = cx.new(|cx| {
14116 let mut multibuffer = MultiBuffer::new(ReadWrite);
14117 excerpt1_id = multibuffer
14118 .push_excerpts(
14119 buffer.clone(),
14120 [
14121 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14122 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14123 ],
14124 cx,
14125 )
14126 .into_iter()
14127 .next();
14128 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14129 multibuffer
14130 });
14131
14132 let editor = cx.add_window(|window, cx| {
14133 let mut editor = build_editor(multibuffer.clone(), window, cx);
14134 let snapshot = editor.snapshot(window, cx);
14135 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14136 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
14137 });
14138 editor.begin_selection(
14139 Point::new(2, 1).to_display_point(&snapshot),
14140 true,
14141 1,
14142 window,
14143 cx,
14144 );
14145 assert_eq!(
14146 editor.selections.ranges(cx),
14147 [
14148 Point::new(1, 3)..Point::new(1, 3),
14149 Point::new(2, 1)..Point::new(2, 1),
14150 ]
14151 );
14152 editor
14153 });
14154
14155 // Refreshing selections is a no-op when excerpts haven't changed.
14156 _ = editor.update(cx, |editor, window, cx| {
14157 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14158 assert_eq!(
14159 editor.selections.ranges(cx),
14160 [
14161 Point::new(1, 3)..Point::new(1, 3),
14162 Point::new(2, 1)..Point::new(2, 1),
14163 ]
14164 );
14165 });
14166
14167 multibuffer.update(cx, |multibuffer, cx| {
14168 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14169 });
14170 _ = editor.update(cx, |editor, window, cx| {
14171 // Removing an excerpt causes the first selection to become degenerate.
14172 assert_eq!(
14173 editor.selections.ranges(cx),
14174 [
14175 Point::new(0, 0)..Point::new(0, 0),
14176 Point::new(0, 1)..Point::new(0, 1)
14177 ]
14178 );
14179
14180 // Refreshing selections will relocate the first selection to the original buffer
14181 // location.
14182 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14183 assert_eq!(
14184 editor.selections.ranges(cx),
14185 [
14186 Point::new(0, 1)..Point::new(0, 1),
14187 Point::new(0, 3)..Point::new(0, 3)
14188 ]
14189 );
14190 assert!(editor.selections.pending_anchor().is_some());
14191 });
14192}
14193
14194#[gpui::test]
14195fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
14196 init_test(cx, |_| {});
14197
14198 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14199 let mut excerpt1_id = None;
14200 let multibuffer = cx.new(|cx| {
14201 let mut multibuffer = MultiBuffer::new(ReadWrite);
14202 excerpt1_id = multibuffer
14203 .push_excerpts(
14204 buffer.clone(),
14205 [
14206 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14207 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14208 ],
14209 cx,
14210 )
14211 .into_iter()
14212 .next();
14213 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14214 multibuffer
14215 });
14216
14217 let editor = cx.add_window(|window, cx| {
14218 let mut editor = build_editor(multibuffer.clone(), window, cx);
14219 let snapshot = editor.snapshot(window, cx);
14220 editor.begin_selection(
14221 Point::new(1, 3).to_display_point(&snapshot),
14222 false,
14223 1,
14224 window,
14225 cx,
14226 );
14227 assert_eq!(
14228 editor.selections.ranges(cx),
14229 [Point::new(1, 3)..Point::new(1, 3)]
14230 );
14231 editor
14232 });
14233
14234 multibuffer.update(cx, |multibuffer, cx| {
14235 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14236 });
14237 _ = editor.update(cx, |editor, window, cx| {
14238 assert_eq!(
14239 editor.selections.ranges(cx),
14240 [Point::new(0, 0)..Point::new(0, 0)]
14241 );
14242
14243 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
14244 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14245 assert_eq!(
14246 editor.selections.ranges(cx),
14247 [Point::new(0, 3)..Point::new(0, 3)]
14248 );
14249 assert!(editor.selections.pending_anchor().is_some());
14250 });
14251}
14252
14253#[gpui::test]
14254async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
14255 init_test(cx, |_| {});
14256
14257 let language = Arc::new(
14258 Language::new(
14259 LanguageConfig {
14260 brackets: BracketPairConfig {
14261 pairs: vec![
14262 BracketPair {
14263 start: "{".to_string(),
14264 end: "}".to_string(),
14265 close: true,
14266 surround: true,
14267 newline: true,
14268 },
14269 BracketPair {
14270 start: "/* ".to_string(),
14271 end: " */".to_string(),
14272 close: true,
14273 surround: true,
14274 newline: true,
14275 },
14276 ],
14277 ..Default::default()
14278 },
14279 ..Default::default()
14280 },
14281 Some(tree_sitter_rust::LANGUAGE.into()),
14282 )
14283 .with_indents_query("")
14284 .unwrap(),
14285 );
14286
14287 let text = concat!(
14288 "{ }\n", //
14289 " x\n", //
14290 " /* */\n", //
14291 "x\n", //
14292 "{{} }\n", //
14293 );
14294
14295 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
14296 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14297 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14298 editor
14299 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
14300 .await;
14301
14302 editor.update_in(cx, |editor, window, cx| {
14303 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14304 s.select_display_ranges([
14305 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
14306 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
14307 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
14308 ])
14309 });
14310 editor.newline(&Newline, window, cx);
14311
14312 assert_eq!(
14313 editor.buffer().read(cx).read(cx).text(),
14314 concat!(
14315 "{ \n", // Suppress rustfmt
14316 "\n", //
14317 "}\n", //
14318 " x\n", //
14319 " /* \n", //
14320 " \n", //
14321 " */\n", //
14322 "x\n", //
14323 "{{} \n", //
14324 "}\n", //
14325 )
14326 );
14327 });
14328}
14329
14330#[gpui::test]
14331fn test_highlighted_ranges(cx: &mut TestAppContext) {
14332 init_test(cx, |_| {});
14333
14334 let editor = cx.add_window(|window, cx| {
14335 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
14336 build_editor(buffer.clone(), window, cx)
14337 });
14338
14339 _ = editor.update(cx, |editor, window, cx| {
14340 struct Type1;
14341 struct Type2;
14342
14343 let buffer = editor.buffer.read(cx).snapshot(cx);
14344
14345 let anchor_range =
14346 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
14347
14348 editor.highlight_background::<Type1>(
14349 &[
14350 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
14351 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
14352 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
14353 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
14354 ],
14355 |_| Hsla::red(),
14356 cx,
14357 );
14358 editor.highlight_background::<Type2>(
14359 &[
14360 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
14361 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
14362 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
14363 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
14364 ],
14365 |_| Hsla::green(),
14366 cx,
14367 );
14368
14369 let snapshot = editor.snapshot(window, cx);
14370 let mut highlighted_ranges = editor.background_highlights_in_range(
14371 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
14372 &snapshot,
14373 cx.theme(),
14374 );
14375 // Enforce a consistent ordering based on color without relying on the ordering of the
14376 // highlight's `TypeId` which is non-executor.
14377 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
14378 assert_eq!(
14379 highlighted_ranges,
14380 &[
14381 (
14382 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
14383 Hsla::red(),
14384 ),
14385 (
14386 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14387 Hsla::red(),
14388 ),
14389 (
14390 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
14391 Hsla::green(),
14392 ),
14393 (
14394 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
14395 Hsla::green(),
14396 ),
14397 ]
14398 );
14399 assert_eq!(
14400 editor.background_highlights_in_range(
14401 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
14402 &snapshot,
14403 cx.theme(),
14404 ),
14405 &[(
14406 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14407 Hsla::red(),
14408 )]
14409 );
14410 });
14411}
14412
14413#[gpui::test]
14414async fn test_following(cx: &mut TestAppContext) {
14415 init_test(cx, |_| {});
14416
14417 let fs = FakeFs::new(cx.executor());
14418 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14419
14420 let buffer = project.update(cx, |project, cx| {
14421 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
14422 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
14423 });
14424 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
14425 let follower = cx.update(|cx| {
14426 cx.open_window(
14427 WindowOptions {
14428 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
14429 gpui::Point::new(px(0.), px(0.)),
14430 gpui::Point::new(px(10.), px(80.)),
14431 ))),
14432 ..Default::default()
14433 },
14434 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
14435 )
14436 .unwrap()
14437 });
14438
14439 let is_still_following = Rc::new(RefCell::new(true));
14440 let follower_edit_event_count = Rc::new(RefCell::new(0));
14441 let pending_update = Rc::new(RefCell::new(None));
14442 let leader_entity = leader.root(cx).unwrap();
14443 let follower_entity = follower.root(cx).unwrap();
14444 _ = follower.update(cx, {
14445 let update = pending_update.clone();
14446 let is_still_following = is_still_following.clone();
14447 let follower_edit_event_count = follower_edit_event_count.clone();
14448 |_, window, cx| {
14449 cx.subscribe_in(
14450 &leader_entity,
14451 window,
14452 move |_, leader, event, window, cx| {
14453 leader.read(cx).add_event_to_update_proto(
14454 event,
14455 &mut update.borrow_mut(),
14456 window,
14457 cx,
14458 );
14459 },
14460 )
14461 .detach();
14462
14463 cx.subscribe_in(
14464 &follower_entity,
14465 window,
14466 move |_, _, event: &EditorEvent, _window, _cx| {
14467 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
14468 *is_still_following.borrow_mut() = false;
14469 }
14470
14471 if let EditorEvent::BufferEdited = event {
14472 *follower_edit_event_count.borrow_mut() += 1;
14473 }
14474 },
14475 )
14476 .detach();
14477 }
14478 });
14479
14480 // Update the selections only
14481 _ = leader.update(cx, |leader, window, cx| {
14482 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14483 s.select_ranges([1..1])
14484 });
14485 });
14486 follower
14487 .update(cx, |follower, window, cx| {
14488 follower.apply_update_proto(
14489 &project,
14490 pending_update.borrow_mut().take().unwrap(),
14491 window,
14492 cx,
14493 )
14494 })
14495 .unwrap()
14496 .await
14497 .unwrap();
14498 _ = follower.update(cx, |follower, _, cx| {
14499 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
14500 });
14501 assert!(*is_still_following.borrow());
14502 assert_eq!(*follower_edit_event_count.borrow(), 0);
14503
14504 // Update the scroll position only
14505 _ = leader.update(cx, |leader, window, cx| {
14506 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14507 });
14508 follower
14509 .update(cx, |follower, window, cx| {
14510 follower.apply_update_proto(
14511 &project,
14512 pending_update.borrow_mut().take().unwrap(),
14513 window,
14514 cx,
14515 )
14516 })
14517 .unwrap()
14518 .await
14519 .unwrap();
14520 assert_eq!(
14521 follower
14522 .update(cx, |follower, _, cx| follower.scroll_position(cx))
14523 .unwrap(),
14524 gpui::Point::new(1.5, 3.5)
14525 );
14526 assert!(*is_still_following.borrow());
14527 assert_eq!(*follower_edit_event_count.borrow(), 0);
14528
14529 // Update the selections and scroll position. The follower's scroll position is updated
14530 // via autoscroll, not via the leader's exact scroll position.
14531 _ = leader.update(cx, |leader, window, cx| {
14532 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14533 s.select_ranges([0..0])
14534 });
14535 leader.request_autoscroll(Autoscroll::newest(), cx);
14536 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14537 });
14538 follower
14539 .update(cx, |follower, window, cx| {
14540 follower.apply_update_proto(
14541 &project,
14542 pending_update.borrow_mut().take().unwrap(),
14543 window,
14544 cx,
14545 )
14546 })
14547 .unwrap()
14548 .await
14549 .unwrap();
14550 _ = follower.update(cx, |follower, _, cx| {
14551 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
14552 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
14553 });
14554 assert!(*is_still_following.borrow());
14555
14556 // Creating a pending selection that precedes another selection
14557 _ = leader.update(cx, |leader, window, cx| {
14558 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14559 s.select_ranges([1..1])
14560 });
14561 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
14562 });
14563 follower
14564 .update(cx, |follower, window, cx| {
14565 follower.apply_update_proto(
14566 &project,
14567 pending_update.borrow_mut().take().unwrap(),
14568 window,
14569 cx,
14570 )
14571 })
14572 .unwrap()
14573 .await
14574 .unwrap();
14575 _ = follower.update(cx, |follower, _, cx| {
14576 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
14577 });
14578 assert!(*is_still_following.borrow());
14579
14580 // Extend the pending selection so that it surrounds another selection
14581 _ = leader.update(cx, |leader, window, cx| {
14582 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
14583 });
14584 follower
14585 .update(cx, |follower, window, cx| {
14586 follower.apply_update_proto(
14587 &project,
14588 pending_update.borrow_mut().take().unwrap(),
14589 window,
14590 cx,
14591 )
14592 })
14593 .unwrap()
14594 .await
14595 .unwrap();
14596 _ = follower.update(cx, |follower, _, cx| {
14597 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
14598 });
14599
14600 // Scrolling locally breaks the follow
14601 _ = follower.update(cx, |follower, window, cx| {
14602 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
14603 follower.set_scroll_anchor(
14604 ScrollAnchor {
14605 anchor: top_anchor,
14606 offset: gpui::Point::new(0.0, 0.5),
14607 },
14608 window,
14609 cx,
14610 );
14611 });
14612 assert!(!(*is_still_following.borrow()));
14613}
14614
14615#[gpui::test]
14616async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
14617 init_test(cx, |_| {});
14618
14619 let fs = FakeFs::new(cx.executor());
14620 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14621 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14622 let pane = workspace
14623 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14624 .unwrap();
14625
14626 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14627
14628 let leader = pane.update_in(cx, |_, window, cx| {
14629 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
14630 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
14631 });
14632
14633 // Start following the editor when it has no excerpts.
14634 let mut state_message =
14635 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14636 let workspace_entity = workspace.root(cx).unwrap();
14637 let follower_1 = cx
14638 .update_window(*workspace.deref(), |_, window, cx| {
14639 Editor::from_state_proto(
14640 workspace_entity,
14641 ViewId {
14642 creator: CollaboratorId::PeerId(PeerId::default()),
14643 id: 0,
14644 },
14645 &mut state_message,
14646 window,
14647 cx,
14648 )
14649 })
14650 .unwrap()
14651 .unwrap()
14652 .await
14653 .unwrap();
14654
14655 let update_message = Rc::new(RefCell::new(None));
14656 follower_1.update_in(cx, {
14657 let update = update_message.clone();
14658 |_, window, cx| {
14659 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
14660 leader.read(cx).add_event_to_update_proto(
14661 event,
14662 &mut update.borrow_mut(),
14663 window,
14664 cx,
14665 );
14666 })
14667 .detach();
14668 }
14669 });
14670
14671 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
14672 (
14673 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
14674 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
14675 )
14676 });
14677
14678 // Insert some excerpts.
14679 leader.update(cx, |leader, cx| {
14680 leader.buffer.update(cx, |multibuffer, cx| {
14681 multibuffer.set_excerpts_for_path(
14682 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
14683 buffer_1.clone(),
14684 vec![
14685 Point::row_range(0..3),
14686 Point::row_range(1..6),
14687 Point::row_range(12..15),
14688 ],
14689 0,
14690 cx,
14691 );
14692 multibuffer.set_excerpts_for_path(
14693 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
14694 buffer_2.clone(),
14695 vec![Point::row_range(0..6), Point::row_range(8..12)],
14696 0,
14697 cx,
14698 );
14699 });
14700 });
14701
14702 // Apply the update of adding the excerpts.
14703 follower_1
14704 .update_in(cx, |follower, window, cx| {
14705 follower.apply_update_proto(
14706 &project,
14707 update_message.borrow().clone().unwrap(),
14708 window,
14709 cx,
14710 )
14711 })
14712 .await
14713 .unwrap();
14714 assert_eq!(
14715 follower_1.update(cx, |editor, cx| editor.text(cx)),
14716 leader.update(cx, |editor, cx| editor.text(cx))
14717 );
14718 update_message.borrow_mut().take();
14719
14720 // Start following separately after it already has excerpts.
14721 let mut state_message =
14722 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14723 let workspace_entity = workspace.root(cx).unwrap();
14724 let follower_2 = cx
14725 .update_window(*workspace.deref(), |_, window, cx| {
14726 Editor::from_state_proto(
14727 workspace_entity,
14728 ViewId {
14729 creator: CollaboratorId::PeerId(PeerId::default()),
14730 id: 0,
14731 },
14732 &mut state_message,
14733 window,
14734 cx,
14735 )
14736 })
14737 .unwrap()
14738 .unwrap()
14739 .await
14740 .unwrap();
14741 assert_eq!(
14742 follower_2.update(cx, |editor, cx| editor.text(cx)),
14743 leader.update(cx, |editor, cx| editor.text(cx))
14744 );
14745
14746 // Remove some excerpts.
14747 leader.update(cx, |leader, cx| {
14748 leader.buffer.update(cx, |multibuffer, cx| {
14749 let excerpt_ids = multibuffer.excerpt_ids();
14750 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
14751 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
14752 });
14753 });
14754
14755 // Apply the update of removing the excerpts.
14756 follower_1
14757 .update_in(cx, |follower, window, cx| {
14758 follower.apply_update_proto(
14759 &project,
14760 update_message.borrow().clone().unwrap(),
14761 window,
14762 cx,
14763 )
14764 })
14765 .await
14766 .unwrap();
14767 follower_2
14768 .update_in(cx, |follower, window, cx| {
14769 follower.apply_update_proto(
14770 &project,
14771 update_message.borrow().clone().unwrap(),
14772 window,
14773 cx,
14774 )
14775 })
14776 .await
14777 .unwrap();
14778 update_message.borrow_mut().take();
14779 assert_eq!(
14780 follower_1.update(cx, |editor, cx| editor.text(cx)),
14781 leader.update(cx, |editor, cx| editor.text(cx))
14782 );
14783}
14784
14785#[gpui::test]
14786async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14787 init_test(cx, |_| {});
14788
14789 let mut cx = EditorTestContext::new(cx).await;
14790 let lsp_store =
14791 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
14792
14793 cx.set_state(indoc! {"
14794 ˇfn func(abc def: i32) -> u32 {
14795 }
14796 "});
14797
14798 cx.update(|_, cx| {
14799 lsp_store.update(cx, |lsp_store, cx| {
14800 lsp_store
14801 .update_diagnostics(
14802 LanguageServerId(0),
14803 lsp::PublishDiagnosticsParams {
14804 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
14805 version: None,
14806 diagnostics: vec![
14807 lsp::Diagnostic {
14808 range: lsp::Range::new(
14809 lsp::Position::new(0, 11),
14810 lsp::Position::new(0, 12),
14811 ),
14812 severity: Some(lsp::DiagnosticSeverity::ERROR),
14813 ..Default::default()
14814 },
14815 lsp::Diagnostic {
14816 range: lsp::Range::new(
14817 lsp::Position::new(0, 12),
14818 lsp::Position::new(0, 15),
14819 ),
14820 severity: Some(lsp::DiagnosticSeverity::ERROR),
14821 ..Default::default()
14822 },
14823 lsp::Diagnostic {
14824 range: lsp::Range::new(
14825 lsp::Position::new(0, 25),
14826 lsp::Position::new(0, 28),
14827 ),
14828 severity: Some(lsp::DiagnosticSeverity::ERROR),
14829 ..Default::default()
14830 },
14831 ],
14832 },
14833 None,
14834 DiagnosticSourceKind::Pushed,
14835 &[],
14836 cx,
14837 )
14838 .unwrap()
14839 });
14840 });
14841
14842 executor.run_until_parked();
14843
14844 cx.update_editor(|editor, window, cx| {
14845 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14846 });
14847
14848 cx.assert_editor_state(indoc! {"
14849 fn func(abc def: i32) -> ˇu32 {
14850 }
14851 "});
14852
14853 cx.update_editor(|editor, window, cx| {
14854 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14855 });
14856
14857 cx.assert_editor_state(indoc! {"
14858 fn func(abc ˇdef: i32) -> u32 {
14859 }
14860 "});
14861
14862 cx.update_editor(|editor, window, cx| {
14863 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14864 });
14865
14866 cx.assert_editor_state(indoc! {"
14867 fn func(abcˇ def: i32) -> u32 {
14868 }
14869 "});
14870
14871 cx.update_editor(|editor, window, cx| {
14872 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14873 });
14874
14875 cx.assert_editor_state(indoc! {"
14876 fn func(abc def: i32) -> ˇu32 {
14877 }
14878 "});
14879}
14880
14881#[gpui::test]
14882async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14883 init_test(cx, |_| {});
14884
14885 let mut cx = EditorTestContext::new(cx).await;
14886
14887 let diff_base = r#"
14888 use some::mod;
14889
14890 const A: u32 = 42;
14891
14892 fn main() {
14893 println!("hello");
14894
14895 println!("world");
14896 }
14897 "#
14898 .unindent();
14899
14900 // Edits are modified, removed, modified, added
14901 cx.set_state(
14902 &r#"
14903 use some::modified;
14904
14905 ˇ
14906 fn main() {
14907 println!("hello there");
14908
14909 println!("around the");
14910 println!("world");
14911 }
14912 "#
14913 .unindent(),
14914 );
14915
14916 cx.set_head_text(&diff_base);
14917 executor.run_until_parked();
14918
14919 cx.update_editor(|editor, window, cx| {
14920 //Wrap around the bottom of the buffer
14921 for _ in 0..3 {
14922 editor.go_to_next_hunk(&GoToHunk, window, cx);
14923 }
14924 });
14925
14926 cx.assert_editor_state(
14927 &r#"
14928 ˇuse some::modified;
14929
14930
14931 fn main() {
14932 println!("hello there");
14933
14934 println!("around the");
14935 println!("world");
14936 }
14937 "#
14938 .unindent(),
14939 );
14940
14941 cx.update_editor(|editor, window, cx| {
14942 //Wrap around the top of the buffer
14943 for _ in 0..2 {
14944 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14945 }
14946 });
14947
14948 cx.assert_editor_state(
14949 &r#"
14950 use some::modified;
14951
14952
14953 fn main() {
14954 ˇ println!("hello there");
14955
14956 println!("around the");
14957 println!("world");
14958 }
14959 "#
14960 .unindent(),
14961 );
14962
14963 cx.update_editor(|editor, window, cx| {
14964 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
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.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14984 });
14985
14986 cx.assert_editor_state(
14987 &r#"
14988 ˇuse some::modified;
14989
14990
14991 fn main() {
14992 println!("hello there");
14993
14994 println!("around the");
14995 println!("world");
14996 }
14997 "#
14998 .unindent(),
14999 );
15000
15001 cx.update_editor(|editor, window, cx| {
15002 for _ in 0..2 {
15003 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15004 }
15005 });
15006
15007 cx.assert_editor_state(
15008 &r#"
15009 use some::modified;
15010
15011
15012 fn main() {
15013 ˇ println!("hello there");
15014
15015 println!("around the");
15016 println!("world");
15017 }
15018 "#
15019 .unindent(),
15020 );
15021
15022 cx.update_editor(|editor, window, cx| {
15023 editor.fold(&Fold, window, cx);
15024 });
15025
15026 cx.update_editor(|editor, window, cx| {
15027 editor.go_to_next_hunk(&GoToHunk, window, cx);
15028 });
15029
15030 cx.assert_editor_state(
15031 &r#"
15032 ˇuse some::modified;
15033
15034
15035 fn main() {
15036 println!("hello there");
15037
15038 println!("around the");
15039 println!("world");
15040 }
15041 "#
15042 .unindent(),
15043 );
15044}
15045
15046#[test]
15047fn test_split_words() {
15048 fn split(text: &str) -> Vec<&str> {
15049 split_words(text).collect()
15050 }
15051
15052 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
15053 assert_eq!(split("hello_world"), &["hello_", "world"]);
15054 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
15055 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
15056 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
15057 assert_eq!(split("helloworld"), &["helloworld"]);
15058
15059 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
15060}
15061
15062#[gpui::test]
15063async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
15064 init_test(cx, |_| {});
15065
15066 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
15067 let mut assert = |before, after| {
15068 let _state_context = cx.set_state(before);
15069 cx.run_until_parked();
15070 cx.update_editor(|editor, window, cx| {
15071 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
15072 });
15073 cx.run_until_parked();
15074 cx.assert_editor_state(after);
15075 };
15076
15077 // Outside bracket jumps to outside of matching bracket
15078 assert("console.logˇ(var);", "console.log(var)ˇ;");
15079 assert("console.log(var)ˇ;", "console.logˇ(var);");
15080
15081 // Inside bracket jumps to inside of matching bracket
15082 assert("console.log(ˇvar);", "console.log(varˇ);");
15083 assert("console.log(varˇ);", "console.log(ˇvar);");
15084
15085 // When outside a bracket and inside, favor jumping to the inside bracket
15086 assert(
15087 "console.log('foo', [1, 2, 3]ˇ);",
15088 "console.log(ˇ'foo', [1, 2, 3]);",
15089 );
15090 assert(
15091 "console.log(ˇ'foo', [1, 2, 3]);",
15092 "console.log('foo', [1, 2, 3]ˇ);",
15093 );
15094
15095 // Bias forward if two options are equally likely
15096 assert(
15097 "let result = curried_fun()ˇ();",
15098 "let result = curried_fun()()ˇ;",
15099 );
15100
15101 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
15102 assert(
15103 indoc! {"
15104 function test() {
15105 console.log('test')ˇ
15106 }"},
15107 indoc! {"
15108 function test() {
15109 console.logˇ('test')
15110 }"},
15111 );
15112}
15113
15114#[gpui::test]
15115async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
15116 init_test(cx, |_| {});
15117
15118 let fs = FakeFs::new(cx.executor());
15119 fs.insert_tree(
15120 path!("/a"),
15121 json!({
15122 "main.rs": "fn main() { let a = 5; }",
15123 "other.rs": "// Test file",
15124 }),
15125 )
15126 .await;
15127 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15128
15129 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15130 language_registry.add(Arc::new(Language::new(
15131 LanguageConfig {
15132 name: "Rust".into(),
15133 matcher: LanguageMatcher {
15134 path_suffixes: vec!["rs".to_string()],
15135 ..Default::default()
15136 },
15137 brackets: BracketPairConfig {
15138 pairs: vec![BracketPair {
15139 start: "{".to_string(),
15140 end: "}".to_string(),
15141 close: true,
15142 surround: true,
15143 newline: true,
15144 }],
15145 disabled_scopes_by_bracket_ix: Vec::new(),
15146 },
15147 ..Default::default()
15148 },
15149 Some(tree_sitter_rust::LANGUAGE.into()),
15150 )));
15151 let mut fake_servers = language_registry.register_fake_lsp(
15152 "Rust",
15153 FakeLspAdapter {
15154 capabilities: lsp::ServerCapabilities {
15155 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15156 first_trigger_character: "{".to_string(),
15157 more_trigger_character: None,
15158 }),
15159 ..Default::default()
15160 },
15161 ..Default::default()
15162 },
15163 );
15164
15165 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15166
15167 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15168
15169 let worktree_id = workspace
15170 .update(cx, |workspace, _, cx| {
15171 workspace.project().update(cx, |project, cx| {
15172 project.worktrees(cx).next().unwrap().read(cx).id()
15173 })
15174 })
15175 .unwrap();
15176
15177 let buffer = project
15178 .update(cx, |project, cx| {
15179 project.open_local_buffer(path!("/a/main.rs"), cx)
15180 })
15181 .await
15182 .unwrap();
15183 let editor_handle = workspace
15184 .update(cx, |workspace, window, cx| {
15185 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
15186 })
15187 .unwrap()
15188 .await
15189 .unwrap()
15190 .downcast::<Editor>()
15191 .unwrap();
15192
15193 cx.executor().start_waiting();
15194 let fake_server = fake_servers.next().await.unwrap();
15195
15196 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
15197 |params, _| async move {
15198 assert_eq!(
15199 params.text_document_position.text_document.uri,
15200 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
15201 );
15202 assert_eq!(
15203 params.text_document_position.position,
15204 lsp::Position::new(0, 21),
15205 );
15206
15207 Ok(Some(vec![lsp::TextEdit {
15208 new_text: "]".to_string(),
15209 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15210 }]))
15211 },
15212 );
15213
15214 editor_handle.update_in(cx, |editor, window, cx| {
15215 window.focus(&editor.focus_handle(cx));
15216 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15217 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
15218 });
15219 editor.handle_input("{", window, cx);
15220 });
15221
15222 cx.executor().run_until_parked();
15223
15224 buffer.update(cx, |buffer, _| {
15225 assert_eq!(
15226 buffer.text(),
15227 "fn main() { let a = {5}; }",
15228 "No extra braces from on type formatting should appear in the buffer"
15229 )
15230 });
15231}
15232
15233#[gpui::test(iterations = 20, seeds(31))]
15234async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
15235 init_test(cx, |_| {});
15236
15237 let mut cx = EditorLspTestContext::new_rust(
15238 lsp::ServerCapabilities {
15239 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15240 first_trigger_character: ".".to_string(),
15241 more_trigger_character: None,
15242 }),
15243 ..Default::default()
15244 },
15245 cx,
15246 )
15247 .await;
15248
15249 cx.update_buffer(|buffer, _| {
15250 // This causes autoindent to be async.
15251 buffer.set_sync_parse_timeout(Duration::ZERO)
15252 });
15253
15254 cx.set_state("fn c() {\n d()ˇ\n}\n");
15255 cx.simulate_keystroke("\n");
15256 cx.run_until_parked();
15257
15258 let buffer_cloned =
15259 cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap().clone());
15260 let mut request =
15261 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
15262 let buffer_cloned = buffer_cloned.clone();
15263 async move {
15264 buffer_cloned.update(&mut cx, |buffer, _| {
15265 assert_eq!(
15266 buffer.text(),
15267 "fn c() {\n d()\n .\n}\n",
15268 "OnTypeFormatting should triggered after autoindent applied"
15269 )
15270 })?;
15271
15272 Ok(Some(vec![]))
15273 }
15274 });
15275
15276 cx.simulate_keystroke(".");
15277 cx.run_until_parked();
15278
15279 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
15280 assert!(request.next().await.is_some());
15281 request.close();
15282 assert!(request.next().await.is_none());
15283}
15284
15285#[gpui::test]
15286async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
15287 init_test(cx, |_| {});
15288
15289 let fs = FakeFs::new(cx.executor());
15290 fs.insert_tree(
15291 path!("/a"),
15292 json!({
15293 "main.rs": "fn main() { let a = 5; }",
15294 "other.rs": "// Test file",
15295 }),
15296 )
15297 .await;
15298
15299 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15300
15301 let server_restarts = Arc::new(AtomicUsize::new(0));
15302 let closure_restarts = Arc::clone(&server_restarts);
15303 let language_server_name = "test language server";
15304 let language_name: LanguageName = "Rust".into();
15305
15306 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15307 language_registry.add(Arc::new(Language::new(
15308 LanguageConfig {
15309 name: language_name.clone(),
15310 matcher: LanguageMatcher {
15311 path_suffixes: vec!["rs".to_string()],
15312 ..Default::default()
15313 },
15314 ..Default::default()
15315 },
15316 Some(tree_sitter_rust::LANGUAGE.into()),
15317 )));
15318 let mut fake_servers = language_registry.register_fake_lsp(
15319 "Rust",
15320 FakeLspAdapter {
15321 name: language_server_name,
15322 initialization_options: Some(json!({
15323 "testOptionValue": true
15324 })),
15325 initializer: Some(Box::new(move |fake_server| {
15326 let task_restarts = Arc::clone(&closure_restarts);
15327 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
15328 task_restarts.fetch_add(1, atomic::Ordering::Release);
15329 futures::future::ready(Ok(()))
15330 });
15331 })),
15332 ..Default::default()
15333 },
15334 );
15335
15336 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15337 let _buffer = project
15338 .update(cx, |project, cx| {
15339 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
15340 })
15341 .await
15342 .unwrap();
15343 let _fake_server = fake_servers.next().await.unwrap();
15344 update_test_language_settings(cx, |language_settings| {
15345 language_settings.languages.0.insert(
15346 language_name.clone(),
15347 LanguageSettingsContent {
15348 tab_size: NonZeroU32::new(8),
15349 ..Default::default()
15350 },
15351 );
15352 });
15353 cx.executor().run_until_parked();
15354 assert_eq!(
15355 server_restarts.load(atomic::Ordering::Acquire),
15356 0,
15357 "Should not restart LSP server on an unrelated change"
15358 );
15359
15360 update_test_project_settings(cx, |project_settings| {
15361 project_settings.lsp.insert(
15362 "Some other server name".into(),
15363 LspSettings {
15364 binary: None,
15365 settings: None,
15366 initialization_options: Some(json!({
15367 "some other init value": 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 0,
15377 "Should not restart LSP server on an unrelated LSP settings change"
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: Some(json!({
15387 "anotherInitValue": false
15388 })),
15389 enable_lsp_tasks: false,
15390 },
15391 );
15392 });
15393 cx.executor().run_until_parked();
15394 assert_eq!(
15395 server_restarts.load(atomic::Ordering::Acquire),
15396 1,
15397 "Should restart LSP server on a related LSP settings change"
15398 );
15399
15400 update_test_project_settings(cx, |project_settings| {
15401 project_settings.lsp.insert(
15402 language_server_name.into(),
15403 LspSettings {
15404 binary: None,
15405 settings: None,
15406 initialization_options: Some(json!({
15407 "anotherInitValue": false
15408 })),
15409 enable_lsp_tasks: false,
15410 },
15411 );
15412 });
15413 cx.executor().run_until_parked();
15414 assert_eq!(
15415 server_restarts.load(atomic::Ordering::Acquire),
15416 1,
15417 "Should not restart LSP server on a related LSP settings change that is the same"
15418 );
15419
15420 update_test_project_settings(cx, |project_settings| {
15421 project_settings.lsp.insert(
15422 language_server_name.into(),
15423 LspSettings {
15424 binary: None,
15425 settings: None,
15426 initialization_options: None,
15427 enable_lsp_tasks: false,
15428 },
15429 );
15430 });
15431 cx.executor().run_until_parked();
15432 assert_eq!(
15433 server_restarts.load(atomic::Ordering::Acquire),
15434 2,
15435 "Should restart LSP server on another related LSP settings change"
15436 );
15437}
15438
15439#[gpui::test]
15440async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
15441 init_test(cx, |_| {});
15442
15443 let mut cx = EditorLspTestContext::new_rust(
15444 lsp::ServerCapabilities {
15445 completion_provider: Some(lsp::CompletionOptions {
15446 trigger_characters: Some(vec![".".to_string()]),
15447 resolve_provider: Some(true),
15448 ..Default::default()
15449 }),
15450 ..Default::default()
15451 },
15452 cx,
15453 )
15454 .await;
15455
15456 cx.set_state("fn main() { let a = 2ˇ; }");
15457 cx.simulate_keystroke(".");
15458 let completion_item = lsp::CompletionItem {
15459 label: "some".into(),
15460 kind: Some(lsp::CompletionItemKind::SNIPPET),
15461 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15462 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15463 kind: lsp::MarkupKind::Markdown,
15464 value: "```rust\nSome(2)\n```".to_string(),
15465 })),
15466 deprecated: Some(false),
15467 sort_text: Some("fffffff2".to_string()),
15468 filter_text: Some("some".to_string()),
15469 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15470 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15471 range: lsp::Range {
15472 start: lsp::Position {
15473 line: 0,
15474 character: 22,
15475 },
15476 end: lsp::Position {
15477 line: 0,
15478 character: 22,
15479 },
15480 },
15481 new_text: "Some(2)".to_string(),
15482 })),
15483 additional_text_edits: Some(vec![lsp::TextEdit {
15484 range: lsp::Range {
15485 start: lsp::Position {
15486 line: 0,
15487 character: 20,
15488 },
15489 end: lsp::Position {
15490 line: 0,
15491 character: 22,
15492 },
15493 },
15494 new_text: "".to_string(),
15495 }]),
15496 ..Default::default()
15497 };
15498
15499 let closure_completion_item = completion_item.clone();
15500 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15501 let task_completion_item = closure_completion_item.clone();
15502 async move {
15503 Ok(Some(lsp::CompletionResponse::Array(vec![
15504 task_completion_item,
15505 ])))
15506 }
15507 });
15508
15509 request.next().await;
15510
15511 cx.condition(|editor, _| editor.context_menu_visible())
15512 .await;
15513 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15514 editor
15515 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15516 .unwrap()
15517 });
15518 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
15519
15520 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
15521 let task_completion_item = completion_item.clone();
15522 async move { Ok(task_completion_item) }
15523 })
15524 .next()
15525 .await
15526 .unwrap();
15527 apply_additional_edits.await.unwrap();
15528 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
15529}
15530
15531#[gpui::test]
15532async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
15533 init_test(cx, |_| {});
15534
15535 let mut cx = EditorLspTestContext::new_rust(
15536 lsp::ServerCapabilities {
15537 completion_provider: Some(lsp::CompletionOptions {
15538 trigger_characters: Some(vec![".".to_string()]),
15539 resolve_provider: Some(true),
15540 ..Default::default()
15541 }),
15542 ..Default::default()
15543 },
15544 cx,
15545 )
15546 .await;
15547
15548 cx.set_state("fn main() { let a = 2ˇ; }");
15549 cx.simulate_keystroke(".");
15550
15551 let item1 = lsp::CompletionItem {
15552 label: "method id()".to_string(),
15553 filter_text: Some("id".to_string()),
15554 detail: None,
15555 documentation: None,
15556 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15557 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15558 new_text: ".id".to_string(),
15559 })),
15560 ..lsp::CompletionItem::default()
15561 };
15562
15563 let item2 = lsp::CompletionItem {
15564 label: "other".to_string(),
15565 filter_text: Some("other".to_string()),
15566 detail: None,
15567 documentation: None,
15568 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15569 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15570 new_text: ".other".to_string(),
15571 })),
15572 ..lsp::CompletionItem::default()
15573 };
15574
15575 let item1 = item1.clone();
15576 cx.set_request_handler::<lsp::request::Completion, _, _>({
15577 let item1 = item1.clone();
15578 move |_, _, _| {
15579 let item1 = item1.clone();
15580 let item2 = item2.clone();
15581 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
15582 }
15583 })
15584 .next()
15585 .await;
15586
15587 cx.condition(|editor, _| editor.context_menu_visible())
15588 .await;
15589 cx.update_editor(|editor, _, _| {
15590 let context_menu = editor.context_menu.borrow_mut();
15591 let context_menu = context_menu
15592 .as_ref()
15593 .expect("Should have the context menu deployed");
15594 match context_menu {
15595 CodeContextMenu::Completions(completions_menu) => {
15596 let completions = completions_menu.completions.borrow_mut();
15597 assert_eq!(
15598 completions
15599 .iter()
15600 .map(|completion| &completion.label.text)
15601 .collect::<Vec<_>>(),
15602 vec!["method id()", "other"]
15603 )
15604 }
15605 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15606 }
15607 });
15608
15609 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
15610 let item1 = item1.clone();
15611 move |_, item_to_resolve, _| {
15612 let item1 = item1.clone();
15613 async move {
15614 if item1 == item_to_resolve {
15615 Ok(lsp::CompletionItem {
15616 label: "method id()".to_string(),
15617 filter_text: Some("id".to_string()),
15618 detail: Some("Now resolved!".to_string()),
15619 documentation: Some(lsp::Documentation::String("Docs".to_string())),
15620 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15621 range: lsp::Range::new(
15622 lsp::Position::new(0, 22),
15623 lsp::Position::new(0, 22),
15624 ),
15625 new_text: ".id".to_string(),
15626 })),
15627 ..lsp::CompletionItem::default()
15628 })
15629 } else {
15630 Ok(item_to_resolve)
15631 }
15632 }
15633 }
15634 })
15635 .next()
15636 .await
15637 .unwrap();
15638 cx.run_until_parked();
15639
15640 cx.update_editor(|editor, window, cx| {
15641 editor.context_menu_next(&Default::default(), window, cx);
15642 });
15643
15644 cx.update_editor(|editor, _, _| {
15645 let context_menu = editor.context_menu.borrow_mut();
15646 let context_menu = context_menu
15647 .as_ref()
15648 .expect("Should have the context menu deployed");
15649 match context_menu {
15650 CodeContextMenu::Completions(completions_menu) => {
15651 let completions = completions_menu.completions.borrow_mut();
15652 assert_eq!(
15653 completions
15654 .iter()
15655 .map(|completion| &completion.label.text)
15656 .collect::<Vec<_>>(),
15657 vec!["method id() Now resolved!", "other"],
15658 "Should update first completion label, but not second as the filter text did not match."
15659 );
15660 }
15661 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15662 }
15663 });
15664}
15665
15666#[gpui::test]
15667async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
15668 init_test(cx, |_| {});
15669 let mut cx = EditorLspTestContext::new_rust(
15670 lsp::ServerCapabilities {
15671 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
15672 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
15673 completion_provider: Some(lsp::CompletionOptions {
15674 resolve_provider: Some(true),
15675 ..Default::default()
15676 }),
15677 ..Default::default()
15678 },
15679 cx,
15680 )
15681 .await;
15682 cx.set_state(indoc! {"
15683 struct TestStruct {
15684 field: i32
15685 }
15686
15687 fn mainˇ() {
15688 let unused_var = 42;
15689 let test_struct = TestStruct { field: 42 };
15690 }
15691 "});
15692 let symbol_range = cx.lsp_range(indoc! {"
15693 struct TestStruct {
15694 field: i32
15695 }
15696
15697 «fn main»() {
15698 let unused_var = 42;
15699 let test_struct = TestStruct { field: 42 };
15700 }
15701 "});
15702 let mut hover_requests =
15703 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
15704 Ok(Some(lsp::Hover {
15705 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
15706 kind: lsp::MarkupKind::Markdown,
15707 value: "Function documentation".to_string(),
15708 }),
15709 range: Some(symbol_range),
15710 }))
15711 });
15712
15713 // Case 1: Test that code action menu hide hover popover
15714 cx.dispatch_action(Hover);
15715 hover_requests.next().await;
15716 cx.condition(|editor, _| editor.hover_state.visible()).await;
15717 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
15718 move |_, _, _| async move {
15719 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
15720 lsp::CodeAction {
15721 title: "Remove unused variable".to_string(),
15722 kind: Some(CodeActionKind::QUICKFIX),
15723 edit: Some(lsp::WorkspaceEdit {
15724 changes: Some(
15725 [(
15726 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
15727 vec![lsp::TextEdit {
15728 range: lsp::Range::new(
15729 lsp::Position::new(5, 4),
15730 lsp::Position::new(5, 27),
15731 ),
15732 new_text: "".to_string(),
15733 }],
15734 )]
15735 .into_iter()
15736 .collect(),
15737 ),
15738 ..Default::default()
15739 }),
15740 ..Default::default()
15741 },
15742 )]))
15743 },
15744 );
15745 cx.update_editor(|editor, window, cx| {
15746 editor.toggle_code_actions(
15747 &ToggleCodeActions {
15748 deployed_from: None,
15749 quick_launch: false,
15750 },
15751 window,
15752 cx,
15753 );
15754 });
15755 code_action_requests.next().await;
15756 cx.run_until_parked();
15757 cx.condition(|editor, _| editor.context_menu_visible())
15758 .await;
15759 cx.update_editor(|editor, _, _| {
15760 assert!(
15761 !editor.hover_state.visible(),
15762 "Hover popover should be hidden when code action menu is shown"
15763 );
15764 // Hide code actions
15765 editor.context_menu.take();
15766 });
15767
15768 // Case 2: Test that code completions hide hover popover
15769 cx.dispatch_action(Hover);
15770 hover_requests.next().await;
15771 cx.condition(|editor, _| editor.hover_state.visible()).await;
15772 let counter = Arc::new(AtomicUsize::new(0));
15773 let mut completion_requests =
15774 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15775 let counter = counter.clone();
15776 async move {
15777 counter.fetch_add(1, atomic::Ordering::Release);
15778 Ok(Some(lsp::CompletionResponse::Array(vec![
15779 lsp::CompletionItem {
15780 label: "main".into(),
15781 kind: Some(lsp::CompletionItemKind::FUNCTION),
15782 detail: Some("() -> ()".to_string()),
15783 ..Default::default()
15784 },
15785 lsp::CompletionItem {
15786 label: "TestStruct".into(),
15787 kind: Some(lsp::CompletionItemKind::STRUCT),
15788 detail: Some("struct TestStruct".to_string()),
15789 ..Default::default()
15790 },
15791 ])))
15792 }
15793 });
15794 cx.update_editor(|editor, window, cx| {
15795 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15796 });
15797 completion_requests.next().await;
15798 cx.condition(|editor, _| editor.context_menu_visible())
15799 .await;
15800 cx.update_editor(|editor, _, _| {
15801 assert!(
15802 !editor.hover_state.visible(),
15803 "Hover popover should be hidden when completion menu is shown"
15804 );
15805 });
15806}
15807
15808#[gpui::test]
15809async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
15810 init_test(cx, |_| {});
15811
15812 let mut cx = EditorLspTestContext::new_rust(
15813 lsp::ServerCapabilities {
15814 completion_provider: Some(lsp::CompletionOptions {
15815 trigger_characters: Some(vec![".".to_string()]),
15816 resolve_provider: Some(true),
15817 ..Default::default()
15818 }),
15819 ..Default::default()
15820 },
15821 cx,
15822 )
15823 .await;
15824
15825 cx.set_state("fn main() { let a = 2ˇ; }");
15826 cx.simulate_keystroke(".");
15827
15828 let unresolved_item_1 = lsp::CompletionItem {
15829 label: "id".to_string(),
15830 filter_text: Some("id".to_string()),
15831 detail: None,
15832 documentation: None,
15833 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15834 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15835 new_text: ".id".to_string(),
15836 })),
15837 ..lsp::CompletionItem::default()
15838 };
15839 let resolved_item_1 = lsp::CompletionItem {
15840 additional_text_edits: Some(vec![lsp::TextEdit {
15841 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15842 new_text: "!!".to_string(),
15843 }]),
15844 ..unresolved_item_1.clone()
15845 };
15846 let unresolved_item_2 = lsp::CompletionItem {
15847 label: "other".to_string(),
15848 filter_text: Some("other".to_string()),
15849 detail: None,
15850 documentation: None,
15851 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15852 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15853 new_text: ".other".to_string(),
15854 })),
15855 ..lsp::CompletionItem::default()
15856 };
15857 let resolved_item_2 = lsp::CompletionItem {
15858 additional_text_edits: Some(vec![lsp::TextEdit {
15859 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15860 new_text: "??".to_string(),
15861 }]),
15862 ..unresolved_item_2.clone()
15863 };
15864
15865 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
15866 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
15867 cx.lsp
15868 .server
15869 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15870 let unresolved_item_1 = unresolved_item_1.clone();
15871 let resolved_item_1 = resolved_item_1.clone();
15872 let unresolved_item_2 = unresolved_item_2.clone();
15873 let resolved_item_2 = resolved_item_2.clone();
15874 let resolve_requests_1 = resolve_requests_1.clone();
15875 let resolve_requests_2 = resolve_requests_2.clone();
15876 move |unresolved_request, _| {
15877 let unresolved_item_1 = unresolved_item_1.clone();
15878 let resolved_item_1 = resolved_item_1.clone();
15879 let unresolved_item_2 = unresolved_item_2.clone();
15880 let resolved_item_2 = resolved_item_2.clone();
15881 let resolve_requests_1 = resolve_requests_1.clone();
15882 let resolve_requests_2 = resolve_requests_2.clone();
15883 async move {
15884 if unresolved_request == unresolved_item_1 {
15885 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
15886 Ok(resolved_item_1.clone())
15887 } else if unresolved_request == unresolved_item_2 {
15888 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
15889 Ok(resolved_item_2.clone())
15890 } else {
15891 panic!("Unexpected completion item {unresolved_request:?}")
15892 }
15893 }
15894 }
15895 })
15896 .detach();
15897
15898 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15899 let unresolved_item_1 = unresolved_item_1.clone();
15900 let unresolved_item_2 = unresolved_item_2.clone();
15901 async move {
15902 Ok(Some(lsp::CompletionResponse::Array(vec![
15903 unresolved_item_1,
15904 unresolved_item_2,
15905 ])))
15906 }
15907 })
15908 .next()
15909 .await;
15910
15911 cx.condition(|editor, _| editor.context_menu_visible())
15912 .await;
15913 cx.update_editor(|editor, _, _| {
15914 let context_menu = editor.context_menu.borrow_mut();
15915 let context_menu = context_menu
15916 .as_ref()
15917 .expect("Should have the context menu deployed");
15918 match context_menu {
15919 CodeContextMenu::Completions(completions_menu) => {
15920 let completions = completions_menu.completions.borrow_mut();
15921 assert_eq!(
15922 completions
15923 .iter()
15924 .map(|completion| &completion.label.text)
15925 .collect::<Vec<_>>(),
15926 vec!["id", "other"]
15927 )
15928 }
15929 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15930 }
15931 });
15932 cx.run_until_parked();
15933
15934 cx.update_editor(|editor, window, cx| {
15935 editor.context_menu_next(&ContextMenuNext, window, cx);
15936 });
15937 cx.run_until_parked();
15938 cx.update_editor(|editor, window, cx| {
15939 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15940 });
15941 cx.run_until_parked();
15942 cx.update_editor(|editor, window, cx| {
15943 editor.context_menu_next(&ContextMenuNext, window, cx);
15944 });
15945 cx.run_until_parked();
15946 cx.update_editor(|editor, window, cx| {
15947 editor
15948 .compose_completion(&ComposeCompletion::default(), window, cx)
15949 .expect("No task returned")
15950 })
15951 .await
15952 .expect("Completion failed");
15953 cx.run_until_parked();
15954
15955 cx.update_editor(|editor, _, cx| {
15956 assert_eq!(
15957 resolve_requests_1.load(atomic::Ordering::Acquire),
15958 1,
15959 "Should always resolve once despite multiple selections"
15960 );
15961 assert_eq!(
15962 resolve_requests_2.load(atomic::Ordering::Acquire),
15963 1,
15964 "Should always resolve once after multiple selections and applying the completion"
15965 );
15966 assert_eq!(
15967 editor.text(cx),
15968 "fn main() { let a = ??.other; }",
15969 "Should use resolved data when applying the completion"
15970 );
15971 });
15972}
15973
15974#[gpui::test]
15975async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
15976 init_test(cx, |_| {});
15977
15978 let item_0 = lsp::CompletionItem {
15979 label: "abs".into(),
15980 insert_text: Some("abs".into()),
15981 data: Some(json!({ "very": "special"})),
15982 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
15983 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
15984 lsp::InsertReplaceEdit {
15985 new_text: "abs".to_string(),
15986 insert: lsp::Range::default(),
15987 replace: lsp::Range::default(),
15988 },
15989 )),
15990 ..lsp::CompletionItem::default()
15991 };
15992 let items = iter::once(item_0.clone())
15993 .chain((11..51).map(|i| lsp::CompletionItem {
15994 label: format!("item_{}", i),
15995 insert_text: Some(format!("item_{}", i)),
15996 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15997 ..lsp::CompletionItem::default()
15998 }))
15999 .collect::<Vec<_>>();
16000
16001 let default_commit_characters = vec!["?".to_string()];
16002 let default_data = json!({ "default": "data"});
16003 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
16004 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
16005 let default_edit_range = lsp::Range {
16006 start: lsp::Position {
16007 line: 0,
16008 character: 5,
16009 },
16010 end: lsp::Position {
16011 line: 0,
16012 character: 5,
16013 },
16014 };
16015
16016 let mut cx = EditorLspTestContext::new_rust(
16017 lsp::ServerCapabilities {
16018 completion_provider: Some(lsp::CompletionOptions {
16019 trigger_characters: Some(vec![".".to_string()]),
16020 resolve_provider: Some(true),
16021 ..Default::default()
16022 }),
16023 ..Default::default()
16024 },
16025 cx,
16026 )
16027 .await;
16028
16029 cx.set_state("fn main() { let a = 2ˇ; }");
16030 cx.simulate_keystroke(".");
16031
16032 let completion_data = default_data.clone();
16033 let completion_characters = default_commit_characters.clone();
16034 let completion_items = items.clone();
16035 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16036 let default_data = completion_data.clone();
16037 let default_commit_characters = completion_characters.clone();
16038 let items = completion_items.clone();
16039 async move {
16040 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16041 items,
16042 item_defaults: Some(lsp::CompletionListItemDefaults {
16043 data: Some(default_data.clone()),
16044 commit_characters: Some(default_commit_characters.clone()),
16045 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
16046 default_edit_range,
16047 )),
16048 insert_text_format: Some(default_insert_text_format),
16049 insert_text_mode: Some(default_insert_text_mode),
16050 }),
16051 ..lsp::CompletionList::default()
16052 })))
16053 }
16054 })
16055 .next()
16056 .await;
16057
16058 let resolved_items = Arc::new(Mutex::new(Vec::new()));
16059 cx.lsp
16060 .server
16061 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
16062 let closure_resolved_items = resolved_items.clone();
16063 move |item_to_resolve, _| {
16064 let closure_resolved_items = closure_resolved_items.clone();
16065 async move {
16066 closure_resolved_items.lock().push(item_to_resolve.clone());
16067 Ok(item_to_resolve)
16068 }
16069 }
16070 })
16071 .detach();
16072
16073 cx.condition(|editor, _| editor.context_menu_visible())
16074 .await;
16075 cx.run_until_parked();
16076 cx.update_editor(|editor, _, _| {
16077 let menu = editor.context_menu.borrow_mut();
16078 match menu.as_ref().expect("should have the completions menu") {
16079 CodeContextMenu::Completions(completions_menu) => {
16080 assert_eq!(
16081 completions_menu
16082 .entries
16083 .borrow()
16084 .iter()
16085 .map(|mat| mat.string.clone())
16086 .collect::<Vec<String>>(),
16087 items
16088 .iter()
16089 .map(|completion| completion.label.clone())
16090 .collect::<Vec<String>>()
16091 );
16092 }
16093 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
16094 }
16095 });
16096 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
16097 // with 4 from the end.
16098 assert_eq!(
16099 *resolved_items.lock(),
16100 [&items[0..16], &items[items.len() - 4..items.len()]]
16101 .concat()
16102 .iter()
16103 .cloned()
16104 .map(|mut item| {
16105 if item.data.is_none() {
16106 item.data = Some(default_data.clone());
16107 }
16108 item
16109 })
16110 .collect::<Vec<lsp::CompletionItem>>(),
16111 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
16112 );
16113 resolved_items.lock().clear();
16114
16115 cx.update_editor(|editor, window, cx| {
16116 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
16117 });
16118 cx.run_until_parked();
16119 // Completions that have already been resolved are skipped.
16120 assert_eq!(
16121 *resolved_items.lock(),
16122 items[items.len() - 17..items.len() - 4]
16123 .iter()
16124 .cloned()
16125 .map(|mut item| {
16126 if item.data.is_none() {
16127 item.data = Some(default_data.clone());
16128 }
16129 item
16130 })
16131 .collect::<Vec<lsp::CompletionItem>>()
16132 );
16133 resolved_items.lock().clear();
16134}
16135
16136#[gpui::test]
16137async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
16138 init_test(cx, |_| {});
16139
16140 let mut cx = EditorLspTestContext::new(
16141 Language::new(
16142 LanguageConfig {
16143 matcher: LanguageMatcher {
16144 path_suffixes: vec!["jsx".into()],
16145 ..Default::default()
16146 },
16147 overrides: [(
16148 "element".into(),
16149 LanguageConfigOverride {
16150 completion_query_characters: Override::Set(['-'].into_iter().collect()),
16151 ..Default::default()
16152 },
16153 )]
16154 .into_iter()
16155 .collect(),
16156 ..Default::default()
16157 },
16158 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16159 )
16160 .with_override_query("(jsx_self_closing_element) @element")
16161 .unwrap(),
16162 lsp::ServerCapabilities {
16163 completion_provider: Some(lsp::CompletionOptions {
16164 trigger_characters: Some(vec![":".to_string()]),
16165 ..Default::default()
16166 }),
16167 ..Default::default()
16168 },
16169 cx,
16170 )
16171 .await;
16172
16173 cx.lsp
16174 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16175 Ok(Some(lsp::CompletionResponse::Array(vec![
16176 lsp::CompletionItem {
16177 label: "bg-blue".into(),
16178 ..Default::default()
16179 },
16180 lsp::CompletionItem {
16181 label: "bg-red".into(),
16182 ..Default::default()
16183 },
16184 lsp::CompletionItem {
16185 label: "bg-yellow".into(),
16186 ..Default::default()
16187 },
16188 ])))
16189 });
16190
16191 cx.set_state(r#"<p class="bgˇ" />"#);
16192
16193 // Trigger completion when typing a dash, because the dash is an extra
16194 // word character in the 'element' scope, which contains the cursor.
16195 cx.simulate_keystroke("-");
16196 cx.executor().run_until_parked();
16197 cx.update_editor(|editor, _, _| {
16198 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16199 {
16200 assert_eq!(
16201 completion_menu_entries(&menu),
16202 &["bg-blue", "bg-red", "bg-yellow"]
16203 );
16204 } else {
16205 panic!("expected completion menu to be open");
16206 }
16207 });
16208
16209 cx.simulate_keystroke("l");
16210 cx.executor().run_until_parked();
16211 cx.update_editor(|editor, _, _| {
16212 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16213 {
16214 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
16215 } else {
16216 panic!("expected completion menu to be open");
16217 }
16218 });
16219
16220 // When filtering completions, consider the character after the '-' to
16221 // be the start of a subword.
16222 cx.set_state(r#"<p class="yelˇ" />"#);
16223 cx.simulate_keystroke("l");
16224 cx.executor().run_until_parked();
16225 cx.update_editor(|editor, _, _| {
16226 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16227 {
16228 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
16229 } else {
16230 panic!("expected completion menu to be open");
16231 }
16232 });
16233}
16234
16235fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
16236 let entries = menu.entries.borrow();
16237 entries.iter().map(|mat| mat.string.clone()).collect()
16238}
16239
16240#[gpui::test]
16241async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
16242 init_test(cx, |settings| {
16243 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
16244 Formatter::Prettier,
16245 )))
16246 });
16247
16248 let fs = FakeFs::new(cx.executor());
16249 fs.insert_file(path!("/file.ts"), Default::default()).await;
16250
16251 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
16252 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16253
16254 language_registry.add(Arc::new(Language::new(
16255 LanguageConfig {
16256 name: "TypeScript".into(),
16257 matcher: LanguageMatcher {
16258 path_suffixes: vec!["ts".to_string()],
16259 ..Default::default()
16260 },
16261 ..Default::default()
16262 },
16263 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
16264 )));
16265 update_test_language_settings(cx, |settings| {
16266 settings.defaults.prettier = Some(PrettierSettings {
16267 allowed: true,
16268 ..PrettierSettings::default()
16269 });
16270 });
16271
16272 let test_plugin = "test_plugin";
16273 let _ = language_registry.register_fake_lsp(
16274 "TypeScript",
16275 FakeLspAdapter {
16276 prettier_plugins: vec![test_plugin],
16277 ..Default::default()
16278 },
16279 );
16280
16281 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
16282 let buffer = project
16283 .update(cx, |project, cx| {
16284 project.open_local_buffer(path!("/file.ts"), cx)
16285 })
16286 .await
16287 .unwrap();
16288
16289 let buffer_text = "one\ntwo\nthree\n";
16290 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16291 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16292 editor.update_in(cx, |editor, window, cx| {
16293 editor.set_text(buffer_text, window, cx)
16294 });
16295
16296 editor
16297 .update_in(cx, |editor, window, cx| {
16298 editor.perform_format(
16299 project.clone(),
16300 FormatTrigger::Manual,
16301 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16302 window,
16303 cx,
16304 )
16305 })
16306 .unwrap()
16307 .await;
16308 assert_eq!(
16309 editor.update(cx, |editor, cx| editor.text(cx)),
16310 buffer_text.to_string() + prettier_format_suffix,
16311 "Test prettier formatting was not applied to the original buffer text",
16312 );
16313
16314 update_test_language_settings(cx, |settings| {
16315 settings.defaults.formatter = Some(SelectedFormatter::Auto)
16316 });
16317 let format = editor.update_in(cx, |editor, window, cx| {
16318 editor.perform_format(
16319 project.clone(),
16320 FormatTrigger::Manual,
16321 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16322 window,
16323 cx,
16324 )
16325 });
16326 format.await.unwrap();
16327 assert_eq!(
16328 editor.update(cx, |editor, cx| editor.text(cx)),
16329 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
16330 "Autoformatting (via test prettier) was not applied to the original buffer text",
16331 );
16332}
16333
16334#[gpui::test]
16335async fn test_addition_reverts(cx: &mut TestAppContext) {
16336 init_test(cx, |_| {});
16337 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16338 let base_text = indoc! {r#"
16339 struct Row;
16340 struct Row1;
16341 struct Row2;
16342
16343 struct Row4;
16344 struct Row5;
16345 struct Row6;
16346
16347 struct Row8;
16348 struct Row9;
16349 struct Row10;"#};
16350
16351 // When addition hunks are not adjacent to carets, no hunk revert is performed
16352 assert_hunk_revert(
16353 indoc! {r#"struct Row;
16354 struct Row1;
16355 struct Row1.1;
16356 struct Row1.2;
16357 struct Row2;ˇ
16358
16359 struct Row4;
16360 struct Row5;
16361 struct Row6;
16362
16363 struct Row8;
16364 ˇstruct Row9;
16365 struct Row9.1;
16366 struct Row9.2;
16367 struct Row9.3;
16368 struct Row10;"#},
16369 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16370 indoc! {r#"struct Row;
16371 struct Row1;
16372 struct Row1.1;
16373 struct Row1.2;
16374 struct Row2;ˇ
16375
16376 struct Row4;
16377 struct Row5;
16378 struct Row6;
16379
16380 struct Row8;
16381 ˇstruct Row9;
16382 struct Row9.1;
16383 struct Row9.2;
16384 struct Row9.3;
16385 struct Row10;"#},
16386 base_text,
16387 &mut cx,
16388 );
16389 // Same for selections
16390 assert_hunk_revert(
16391 indoc! {r#"struct Row;
16392 struct Row1;
16393 struct Row2;
16394 struct Row2.1;
16395 struct Row2.2;
16396 «ˇ
16397 struct Row4;
16398 struct» Row5;
16399 «struct Row6;
16400 ˇ»
16401 struct Row9.1;
16402 struct Row9.2;
16403 struct Row9.3;
16404 struct Row8;
16405 struct Row9;
16406 struct Row10;"#},
16407 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16408 indoc! {r#"struct Row;
16409 struct Row1;
16410 struct Row2;
16411 struct Row2.1;
16412 struct Row2.2;
16413 «ˇ
16414 struct Row4;
16415 struct» Row5;
16416 «struct Row6;
16417 ˇ»
16418 struct Row9.1;
16419 struct Row9.2;
16420 struct Row9.3;
16421 struct Row8;
16422 struct Row9;
16423 struct Row10;"#},
16424 base_text,
16425 &mut cx,
16426 );
16427
16428 // When carets and selections intersect the addition hunks, those are reverted.
16429 // Adjacent carets got merged.
16430 assert_hunk_revert(
16431 indoc! {r#"struct Row;
16432 ˇ// something on the top
16433 struct Row1;
16434 struct Row2;
16435 struct Roˇw3.1;
16436 struct Row2.2;
16437 struct Row2.3;ˇ
16438
16439 struct Row4;
16440 struct ˇRow5.1;
16441 struct Row5.2;
16442 struct «Rowˇ»5.3;
16443 struct Row5;
16444 struct Row6;
16445 ˇ
16446 struct Row9.1;
16447 struct «Rowˇ»9.2;
16448 struct «ˇRow»9.3;
16449 struct Row8;
16450 struct Row9;
16451 «ˇ// something on bottom»
16452 struct Row10;"#},
16453 vec![
16454 DiffHunkStatusKind::Added,
16455 DiffHunkStatusKind::Added,
16456 DiffHunkStatusKind::Added,
16457 DiffHunkStatusKind::Added,
16458 DiffHunkStatusKind::Added,
16459 ],
16460 indoc! {r#"struct Row;
16461 ˇstruct Row1;
16462 struct Row2;
16463 ˇ
16464 struct Row4;
16465 ˇstruct Row5;
16466 struct Row6;
16467 ˇ
16468 ˇstruct Row8;
16469 struct Row9;
16470 ˇstruct Row10;"#},
16471 base_text,
16472 &mut cx,
16473 );
16474}
16475
16476#[gpui::test]
16477async fn test_modification_reverts(cx: &mut TestAppContext) {
16478 init_test(cx, |_| {});
16479 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16480 let base_text = indoc! {r#"
16481 struct Row;
16482 struct Row1;
16483 struct Row2;
16484
16485 struct Row4;
16486 struct Row5;
16487 struct Row6;
16488
16489 struct Row8;
16490 struct Row9;
16491 struct Row10;"#};
16492
16493 // Modification hunks behave the same as the addition ones.
16494 assert_hunk_revert(
16495 indoc! {r#"struct Row;
16496 struct Row1;
16497 struct Row33;
16498 ˇ
16499 struct Row4;
16500 struct Row5;
16501 struct Row6;
16502 ˇ
16503 struct Row99;
16504 struct Row9;
16505 struct Row10;"#},
16506 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16507 indoc! {r#"struct Row;
16508 struct Row1;
16509 struct Row33;
16510 ˇ
16511 struct Row4;
16512 struct Row5;
16513 struct Row6;
16514 ˇ
16515 struct Row99;
16516 struct Row9;
16517 struct Row10;"#},
16518 base_text,
16519 &mut cx,
16520 );
16521 assert_hunk_revert(
16522 indoc! {r#"struct Row;
16523 struct Row1;
16524 struct Row33;
16525 «ˇ
16526 struct Row4;
16527 struct» Row5;
16528 «struct Row6;
16529 ˇ»
16530 struct Row99;
16531 struct Row9;
16532 struct Row10;"#},
16533 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16534 indoc! {r#"struct Row;
16535 struct Row1;
16536 struct Row33;
16537 «ˇ
16538 struct Row4;
16539 struct» Row5;
16540 «struct Row6;
16541 ˇ»
16542 struct Row99;
16543 struct Row9;
16544 struct Row10;"#},
16545 base_text,
16546 &mut cx,
16547 );
16548
16549 assert_hunk_revert(
16550 indoc! {r#"ˇstruct Row1.1;
16551 struct Row1;
16552 «ˇstr»uct Row22;
16553
16554 struct ˇRow44;
16555 struct Row5;
16556 struct «Rˇ»ow66;ˇ
16557
16558 «struˇ»ct Row88;
16559 struct Row9;
16560 struct Row1011;ˇ"#},
16561 vec![
16562 DiffHunkStatusKind::Modified,
16563 DiffHunkStatusKind::Modified,
16564 DiffHunkStatusKind::Modified,
16565 DiffHunkStatusKind::Modified,
16566 DiffHunkStatusKind::Modified,
16567 DiffHunkStatusKind::Modified,
16568 ],
16569 indoc! {r#"struct Row;
16570 ˇstruct Row1;
16571 struct Row2;
16572 ˇ
16573 struct Row4;
16574 ˇstruct Row5;
16575 struct Row6;
16576 ˇ
16577 struct Row8;
16578 ˇstruct Row9;
16579 struct Row10;ˇ"#},
16580 base_text,
16581 &mut cx,
16582 );
16583}
16584
16585#[gpui::test]
16586async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
16587 init_test(cx, |_| {});
16588 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16589 let base_text = indoc! {r#"
16590 one
16591
16592 two
16593 three
16594 "#};
16595
16596 cx.set_head_text(base_text);
16597 cx.set_state("\nˇ\n");
16598 cx.executor().run_until_parked();
16599 cx.update_editor(|editor, _window, cx| {
16600 editor.expand_selected_diff_hunks(cx);
16601 });
16602 cx.executor().run_until_parked();
16603 cx.update_editor(|editor, window, cx| {
16604 editor.backspace(&Default::default(), window, cx);
16605 });
16606 cx.run_until_parked();
16607 cx.assert_state_with_diff(
16608 indoc! {r#"
16609
16610 - two
16611 - threeˇ
16612 +
16613 "#}
16614 .to_string(),
16615 );
16616}
16617
16618#[gpui::test]
16619async fn test_deletion_reverts(cx: &mut TestAppContext) {
16620 init_test(cx, |_| {});
16621 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16622 let base_text = indoc! {r#"struct Row;
16623struct Row1;
16624struct Row2;
16625
16626struct Row4;
16627struct Row5;
16628struct Row6;
16629
16630struct Row8;
16631struct Row9;
16632struct Row10;"#};
16633
16634 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
16635 assert_hunk_revert(
16636 indoc! {r#"struct Row;
16637 struct Row2;
16638
16639 ˇstruct Row4;
16640 struct Row5;
16641 struct Row6;
16642 ˇ
16643 struct Row8;
16644 struct Row10;"#},
16645 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16646 indoc! {r#"struct Row;
16647 struct Row2;
16648
16649 ˇstruct Row4;
16650 struct Row5;
16651 struct Row6;
16652 ˇ
16653 struct Row8;
16654 struct Row10;"#},
16655 base_text,
16656 &mut cx,
16657 );
16658 assert_hunk_revert(
16659 indoc! {r#"struct Row;
16660 struct Row2;
16661
16662 «ˇstruct Row4;
16663 struct» Row5;
16664 «struct Row6;
16665 ˇ»
16666 struct Row8;
16667 struct Row10;"#},
16668 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16669 indoc! {r#"struct Row;
16670 struct Row2;
16671
16672 «ˇstruct Row4;
16673 struct» Row5;
16674 «struct Row6;
16675 ˇ»
16676 struct Row8;
16677 struct Row10;"#},
16678 base_text,
16679 &mut cx,
16680 );
16681
16682 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
16683 assert_hunk_revert(
16684 indoc! {r#"struct Row;
16685 ˇstruct Row2;
16686
16687 struct Row4;
16688 struct Row5;
16689 struct Row6;
16690
16691 struct Row8;ˇ
16692 struct Row10;"#},
16693 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16694 indoc! {r#"struct Row;
16695 struct Row1;
16696 ˇstruct Row2;
16697
16698 struct Row4;
16699 struct Row5;
16700 struct Row6;
16701
16702 struct Row8;ˇ
16703 struct Row9;
16704 struct Row10;"#},
16705 base_text,
16706 &mut cx,
16707 );
16708 assert_hunk_revert(
16709 indoc! {r#"struct Row;
16710 struct Row2«ˇ;
16711 struct Row4;
16712 struct» Row5;
16713 «struct Row6;
16714
16715 struct Row8;ˇ»
16716 struct Row10;"#},
16717 vec![
16718 DiffHunkStatusKind::Deleted,
16719 DiffHunkStatusKind::Deleted,
16720 DiffHunkStatusKind::Deleted,
16721 ],
16722 indoc! {r#"struct Row;
16723 struct Row1;
16724 struct Row2«ˇ;
16725
16726 struct Row4;
16727 struct» Row5;
16728 «struct Row6;
16729
16730 struct Row8;ˇ»
16731 struct Row9;
16732 struct Row10;"#},
16733 base_text,
16734 &mut cx,
16735 );
16736}
16737
16738#[gpui::test]
16739async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
16740 init_test(cx, |_| {});
16741
16742 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
16743 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
16744 let base_text_3 =
16745 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
16746
16747 let text_1 = edit_first_char_of_every_line(base_text_1);
16748 let text_2 = edit_first_char_of_every_line(base_text_2);
16749 let text_3 = edit_first_char_of_every_line(base_text_3);
16750
16751 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
16752 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
16753 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
16754
16755 let multibuffer = cx.new(|cx| {
16756 let mut multibuffer = MultiBuffer::new(ReadWrite);
16757 multibuffer.push_excerpts(
16758 buffer_1.clone(),
16759 [
16760 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16761 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16762 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16763 ],
16764 cx,
16765 );
16766 multibuffer.push_excerpts(
16767 buffer_2.clone(),
16768 [
16769 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16770 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16771 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16772 ],
16773 cx,
16774 );
16775 multibuffer.push_excerpts(
16776 buffer_3.clone(),
16777 [
16778 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16779 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16780 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16781 ],
16782 cx,
16783 );
16784 multibuffer
16785 });
16786
16787 let fs = FakeFs::new(cx.executor());
16788 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
16789 let (editor, cx) = cx
16790 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
16791 editor.update_in(cx, |editor, _window, cx| {
16792 for (buffer, diff_base) in [
16793 (buffer_1.clone(), base_text_1),
16794 (buffer_2.clone(), base_text_2),
16795 (buffer_3.clone(), base_text_3),
16796 ] {
16797 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16798 editor
16799 .buffer
16800 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16801 }
16802 });
16803 cx.executor().run_until_parked();
16804
16805 editor.update_in(cx, |editor, window, cx| {
16806 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}");
16807 editor.select_all(&SelectAll, window, cx);
16808 editor.git_restore(&Default::default(), window, cx);
16809 });
16810 cx.executor().run_until_parked();
16811
16812 // When all ranges are selected, all buffer hunks are reverted.
16813 editor.update(cx, |editor, cx| {
16814 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");
16815 });
16816 buffer_1.update(cx, |buffer, _| {
16817 assert_eq!(buffer.text(), base_text_1);
16818 });
16819 buffer_2.update(cx, |buffer, _| {
16820 assert_eq!(buffer.text(), base_text_2);
16821 });
16822 buffer_3.update(cx, |buffer, _| {
16823 assert_eq!(buffer.text(), base_text_3);
16824 });
16825
16826 editor.update_in(cx, |editor, window, cx| {
16827 editor.undo(&Default::default(), window, cx);
16828 });
16829
16830 editor.update_in(cx, |editor, window, cx| {
16831 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16832 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
16833 });
16834 editor.git_restore(&Default::default(), window, cx);
16835 });
16836
16837 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
16838 // but not affect buffer_2 and its related excerpts.
16839 editor.update(cx, |editor, cx| {
16840 assert_eq!(
16841 editor.text(cx),
16842 "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}"
16843 );
16844 });
16845 buffer_1.update(cx, |buffer, _| {
16846 assert_eq!(buffer.text(), base_text_1);
16847 });
16848 buffer_2.update(cx, |buffer, _| {
16849 assert_eq!(
16850 buffer.text(),
16851 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
16852 );
16853 });
16854 buffer_3.update(cx, |buffer, _| {
16855 assert_eq!(
16856 buffer.text(),
16857 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
16858 );
16859 });
16860
16861 fn edit_first_char_of_every_line(text: &str) -> String {
16862 text.split('\n')
16863 .map(|line| format!("X{}", &line[1..]))
16864 .collect::<Vec<_>>()
16865 .join("\n")
16866 }
16867}
16868
16869#[gpui::test]
16870async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
16871 init_test(cx, |_| {});
16872
16873 let cols = 4;
16874 let rows = 10;
16875 let sample_text_1 = sample_text(rows, cols, 'a');
16876 assert_eq!(
16877 sample_text_1,
16878 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
16879 );
16880 let sample_text_2 = sample_text(rows, cols, 'l');
16881 assert_eq!(
16882 sample_text_2,
16883 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
16884 );
16885 let sample_text_3 = sample_text(rows, cols, 'v');
16886 assert_eq!(
16887 sample_text_3,
16888 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
16889 );
16890
16891 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
16892 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
16893 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
16894
16895 let multi_buffer = cx.new(|cx| {
16896 let mut multibuffer = MultiBuffer::new(ReadWrite);
16897 multibuffer.push_excerpts(
16898 buffer_1.clone(),
16899 [
16900 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16901 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16902 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16903 ],
16904 cx,
16905 );
16906 multibuffer.push_excerpts(
16907 buffer_2.clone(),
16908 [
16909 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16910 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16911 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16912 ],
16913 cx,
16914 );
16915 multibuffer.push_excerpts(
16916 buffer_3.clone(),
16917 [
16918 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16919 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16920 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16921 ],
16922 cx,
16923 );
16924 multibuffer
16925 });
16926
16927 let fs = FakeFs::new(cx.executor());
16928 fs.insert_tree(
16929 "/a",
16930 json!({
16931 "main.rs": sample_text_1,
16932 "other.rs": sample_text_2,
16933 "lib.rs": sample_text_3,
16934 }),
16935 )
16936 .await;
16937 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16938 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16939 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16940 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16941 Editor::new(
16942 EditorMode::full(),
16943 multi_buffer,
16944 Some(project.clone()),
16945 window,
16946 cx,
16947 )
16948 });
16949 let multibuffer_item_id = workspace
16950 .update(cx, |workspace, window, cx| {
16951 assert!(
16952 workspace.active_item(cx).is_none(),
16953 "active item should be None before the first item is added"
16954 );
16955 workspace.add_item_to_active_pane(
16956 Box::new(multi_buffer_editor.clone()),
16957 None,
16958 true,
16959 window,
16960 cx,
16961 );
16962 let active_item = workspace
16963 .active_item(cx)
16964 .expect("should have an active item after adding the multi buffer");
16965 assert!(
16966 !active_item.is_singleton(cx),
16967 "A multi buffer was expected to active after adding"
16968 );
16969 active_item.item_id()
16970 })
16971 .unwrap();
16972 cx.executor().run_until_parked();
16973
16974 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16975 editor.change_selections(
16976 SelectionEffects::scroll(Autoscroll::Next),
16977 window,
16978 cx,
16979 |s| s.select_ranges(Some(1..2)),
16980 );
16981 editor.open_excerpts(&OpenExcerpts, window, cx);
16982 });
16983 cx.executor().run_until_parked();
16984 let first_item_id = workspace
16985 .update(cx, |workspace, window, cx| {
16986 let active_item = workspace
16987 .active_item(cx)
16988 .expect("should have an active item after navigating into the 1st buffer");
16989 let first_item_id = active_item.item_id();
16990 assert_ne!(
16991 first_item_id, multibuffer_item_id,
16992 "Should navigate into the 1st buffer and activate it"
16993 );
16994 assert!(
16995 active_item.is_singleton(cx),
16996 "New active item should be a singleton buffer"
16997 );
16998 assert_eq!(
16999 active_item
17000 .act_as::<Editor>(cx)
17001 .expect("should have navigated into an editor for the 1st buffer")
17002 .read(cx)
17003 .text(cx),
17004 sample_text_1
17005 );
17006
17007 workspace
17008 .go_back(workspace.active_pane().downgrade(), window, cx)
17009 .detach_and_log_err(cx);
17010
17011 first_item_id
17012 })
17013 .unwrap();
17014 cx.executor().run_until_parked();
17015 workspace
17016 .update(cx, |workspace, _, cx| {
17017 let active_item = workspace
17018 .active_item(cx)
17019 .expect("should have an active item after navigating back");
17020 assert_eq!(
17021 active_item.item_id(),
17022 multibuffer_item_id,
17023 "Should navigate back to the multi buffer"
17024 );
17025 assert!(!active_item.is_singleton(cx));
17026 })
17027 .unwrap();
17028
17029 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17030 editor.change_selections(
17031 SelectionEffects::scroll(Autoscroll::Next),
17032 window,
17033 cx,
17034 |s| s.select_ranges(Some(39..40)),
17035 );
17036 editor.open_excerpts(&OpenExcerpts, window, cx);
17037 });
17038 cx.executor().run_until_parked();
17039 let second_item_id = workspace
17040 .update(cx, |workspace, window, cx| {
17041 let active_item = workspace
17042 .active_item(cx)
17043 .expect("should have an active item after navigating into the 2nd buffer");
17044 let second_item_id = active_item.item_id();
17045 assert_ne!(
17046 second_item_id, multibuffer_item_id,
17047 "Should navigate away from the multibuffer"
17048 );
17049 assert_ne!(
17050 second_item_id, first_item_id,
17051 "Should navigate into the 2nd buffer and activate it"
17052 );
17053 assert!(
17054 active_item.is_singleton(cx),
17055 "New active item should be a singleton buffer"
17056 );
17057 assert_eq!(
17058 active_item
17059 .act_as::<Editor>(cx)
17060 .expect("should have navigated into an editor")
17061 .read(cx)
17062 .text(cx),
17063 sample_text_2
17064 );
17065
17066 workspace
17067 .go_back(workspace.active_pane().downgrade(), window, cx)
17068 .detach_and_log_err(cx);
17069
17070 second_item_id
17071 })
17072 .unwrap();
17073 cx.executor().run_until_parked();
17074 workspace
17075 .update(cx, |workspace, _, cx| {
17076 let active_item = workspace
17077 .active_item(cx)
17078 .expect("should have an active item after navigating back from the 2nd buffer");
17079 assert_eq!(
17080 active_item.item_id(),
17081 multibuffer_item_id,
17082 "Should navigate back from the 2nd buffer to the multi buffer"
17083 );
17084 assert!(!active_item.is_singleton(cx));
17085 })
17086 .unwrap();
17087
17088 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17089 editor.change_selections(
17090 SelectionEffects::scroll(Autoscroll::Next),
17091 window,
17092 cx,
17093 |s| s.select_ranges(Some(70..70)),
17094 );
17095 editor.open_excerpts(&OpenExcerpts, window, cx);
17096 });
17097 cx.executor().run_until_parked();
17098 workspace
17099 .update(cx, |workspace, window, cx| {
17100 let active_item = workspace
17101 .active_item(cx)
17102 .expect("should have an active item after navigating into the 3rd buffer");
17103 let third_item_id = active_item.item_id();
17104 assert_ne!(
17105 third_item_id, multibuffer_item_id,
17106 "Should navigate into the 3rd buffer and activate it"
17107 );
17108 assert_ne!(third_item_id, first_item_id);
17109 assert_ne!(third_item_id, second_item_id);
17110 assert!(
17111 active_item.is_singleton(cx),
17112 "New active item should be a singleton buffer"
17113 );
17114 assert_eq!(
17115 active_item
17116 .act_as::<Editor>(cx)
17117 .expect("should have navigated into an editor")
17118 .read(cx)
17119 .text(cx),
17120 sample_text_3
17121 );
17122
17123 workspace
17124 .go_back(workspace.active_pane().downgrade(), window, cx)
17125 .detach_and_log_err(cx);
17126 })
17127 .unwrap();
17128 cx.executor().run_until_parked();
17129 workspace
17130 .update(cx, |workspace, _, cx| {
17131 let active_item = workspace
17132 .active_item(cx)
17133 .expect("should have an active item after navigating back from the 3rd buffer");
17134 assert_eq!(
17135 active_item.item_id(),
17136 multibuffer_item_id,
17137 "Should navigate back from the 3rd buffer to the multi buffer"
17138 );
17139 assert!(!active_item.is_singleton(cx));
17140 })
17141 .unwrap();
17142}
17143
17144#[gpui::test]
17145async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17146 init_test(cx, |_| {});
17147
17148 let mut cx = EditorTestContext::new(cx).await;
17149
17150 let diff_base = r#"
17151 use some::mod;
17152
17153 const A: u32 = 42;
17154
17155 fn main() {
17156 println!("hello");
17157
17158 println!("world");
17159 }
17160 "#
17161 .unindent();
17162
17163 cx.set_state(
17164 &r#"
17165 use some::modified;
17166
17167 ˇ
17168 fn main() {
17169 println!("hello there");
17170
17171 println!("around the");
17172 println!("world");
17173 }
17174 "#
17175 .unindent(),
17176 );
17177
17178 cx.set_head_text(&diff_base);
17179 executor.run_until_parked();
17180
17181 cx.update_editor(|editor, window, cx| {
17182 editor.go_to_next_hunk(&GoToHunk, window, cx);
17183 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17184 });
17185 executor.run_until_parked();
17186 cx.assert_state_with_diff(
17187 r#"
17188 use some::modified;
17189
17190
17191 fn main() {
17192 - println!("hello");
17193 + ˇ println!("hello there");
17194
17195 println!("around the");
17196 println!("world");
17197 }
17198 "#
17199 .unindent(),
17200 );
17201
17202 cx.update_editor(|editor, window, cx| {
17203 for _ in 0..2 {
17204 editor.go_to_next_hunk(&GoToHunk, window, cx);
17205 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17206 }
17207 });
17208 executor.run_until_parked();
17209 cx.assert_state_with_diff(
17210 r#"
17211 - use some::mod;
17212 + ˇuse some::modified;
17213
17214
17215 fn main() {
17216 - println!("hello");
17217 + println!("hello there");
17218
17219 + println!("around the");
17220 println!("world");
17221 }
17222 "#
17223 .unindent(),
17224 );
17225
17226 cx.update_editor(|editor, window, cx| {
17227 editor.go_to_next_hunk(&GoToHunk, window, cx);
17228 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17229 });
17230 executor.run_until_parked();
17231 cx.assert_state_with_diff(
17232 r#"
17233 - use some::mod;
17234 + use some::modified;
17235
17236 - const A: u32 = 42;
17237 ˇ
17238 fn main() {
17239 - println!("hello");
17240 + println!("hello there");
17241
17242 + println!("around the");
17243 println!("world");
17244 }
17245 "#
17246 .unindent(),
17247 );
17248
17249 cx.update_editor(|editor, window, cx| {
17250 editor.cancel(&Cancel, window, cx);
17251 });
17252
17253 cx.assert_state_with_diff(
17254 r#"
17255 use some::modified;
17256
17257 ˇ
17258 fn main() {
17259 println!("hello there");
17260
17261 println!("around the");
17262 println!("world");
17263 }
17264 "#
17265 .unindent(),
17266 );
17267}
17268
17269#[gpui::test]
17270async fn test_diff_base_change_with_expanded_diff_hunks(
17271 executor: BackgroundExecutor,
17272 cx: &mut TestAppContext,
17273) {
17274 init_test(cx, |_| {});
17275
17276 let mut cx = EditorTestContext::new(cx).await;
17277
17278 let diff_base = r#"
17279 use some::mod1;
17280 use some::mod2;
17281
17282 const A: u32 = 42;
17283 const B: u32 = 42;
17284 const C: u32 = 42;
17285
17286 fn main() {
17287 println!("hello");
17288
17289 println!("world");
17290 }
17291 "#
17292 .unindent();
17293
17294 cx.set_state(
17295 &r#"
17296 use some::mod2;
17297
17298 const A: u32 = 42;
17299 const C: u32 = 42;
17300
17301 fn main(ˇ) {
17302 //println!("hello");
17303
17304 println!("world");
17305 //
17306 //
17307 }
17308 "#
17309 .unindent(),
17310 );
17311
17312 cx.set_head_text(&diff_base);
17313 executor.run_until_parked();
17314
17315 cx.update_editor(|editor, window, cx| {
17316 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17317 });
17318 executor.run_until_parked();
17319 cx.assert_state_with_diff(
17320 r#"
17321 - use some::mod1;
17322 use some::mod2;
17323
17324 const A: u32 = 42;
17325 - const B: u32 = 42;
17326 const C: u32 = 42;
17327
17328 fn main(ˇ) {
17329 - println!("hello");
17330 + //println!("hello");
17331
17332 println!("world");
17333 + //
17334 + //
17335 }
17336 "#
17337 .unindent(),
17338 );
17339
17340 cx.set_head_text("new diff base!");
17341 executor.run_until_parked();
17342 cx.assert_state_with_diff(
17343 r#"
17344 - new diff base!
17345 + use some::mod2;
17346 +
17347 + const A: u32 = 42;
17348 + const C: u32 = 42;
17349 +
17350 + fn main(ˇ) {
17351 + //println!("hello");
17352 +
17353 + println!("world");
17354 + //
17355 + //
17356 + }
17357 "#
17358 .unindent(),
17359 );
17360}
17361
17362#[gpui::test]
17363async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
17364 init_test(cx, |_| {});
17365
17366 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17367 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17368 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17369 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17370 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
17371 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
17372
17373 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
17374 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
17375 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
17376
17377 let multi_buffer = cx.new(|cx| {
17378 let mut multibuffer = MultiBuffer::new(ReadWrite);
17379 multibuffer.push_excerpts(
17380 buffer_1.clone(),
17381 [
17382 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17383 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17384 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17385 ],
17386 cx,
17387 );
17388 multibuffer.push_excerpts(
17389 buffer_2.clone(),
17390 [
17391 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17392 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17393 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17394 ],
17395 cx,
17396 );
17397 multibuffer.push_excerpts(
17398 buffer_3.clone(),
17399 [
17400 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17401 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17402 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17403 ],
17404 cx,
17405 );
17406 multibuffer
17407 });
17408
17409 let editor =
17410 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17411 editor
17412 .update(cx, |editor, _window, cx| {
17413 for (buffer, diff_base) in [
17414 (buffer_1.clone(), file_1_old),
17415 (buffer_2.clone(), file_2_old),
17416 (buffer_3.clone(), file_3_old),
17417 ] {
17418 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
17419 editor
17420 .buffer
17421 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
17422 }
17423 })
17424 .unwrap();
17425
17426 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17427 cx.run_until_parked();
17428
17429 cx.assert_editor_state(
17430 &"
17431 ˇaaa
17432 ccc
17433 ddd
17434
17435 ggg
17436 hhh
17437
17438
17439 lll
17440 mmm
17441 NNN
17442
17443 qqq
17444 rrr
17445
17446 uuu
17447 111
17448 222
17449 333
17450
17451 666
17452 777
17453
17454 000
17455 !!!"
17456 .unindent(),
17457 );
17458
17459 cx.update_editor(|editor, window, cx| {
17460 editor.select_all(&SelectAll, window, cx);
17461 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17462 });
17463 cx.executor().run_until_parked();
17464
17465 cx.assert_state_with_diff(
17466 "
17467 «aaa
17468 - bbb
17469 ccc
17470 ddd
17471
17472 ggg
17473 hhh
17474
17475
17476 lll
17477 mmm
17478 - nnn
17479 + NNN
17480
17481 qqq
17482 rrr
17483
17484 uuu
17485 111
17486 222
17487 333
17488
17489 + 666
17490 777
17491
17492 000
17493 !!!ˇ»"
17494 .unindent(),
17495 );
17496}
17497
17498#[gpui::test]
17499async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
17500 init_test(cx, |_| {});
17501
17502 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
17503 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
17504
17505 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
17506 let multi_buffer = cx.new(|cx| {
17507 let mut multibuffer = MultiBuffer::new(ReadWrite);
17508 multibuffer.push_excerpts(
17509 buffer.clone(),
17510 [
17511 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
17512 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
17513 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
17514 ],
17515 cx,
17516 );
17517 multibuffer
17518 });
17519
17520 let editor =
17521 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17522 editor
17523 .update(cx, |editor, _window, cx| {
17524 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
17525 editor
17526 .buffer
17527 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
17528 })
17529 .unwrap();
17530
17531 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17532 cx.run_until_parked();
17533
17534 cx.update_editor(|editor, window, cx| {
17535 editor.expand_all_diff_hunks(&Default::default(), window, cx)
17536 });
17537 cx.executor().run_until_parked();
17538
17539 // When the start of a hunk coincides with the start of its excerpt,
17540 // the hunk is expanded. When the start of a a hunk is earlier than
17541 // the start of its excerpt, the hunk is not expanded.
17542 cx.assert_state_with_diff(
17543 "
17544 ˇaaa
17545 - bbb
17546 + BBB
17547
17548 - ddd
17549 - eee
17550 + DDD
17551 + EEE
17552 fff
17553
17554 iii
17555 "
17556 .unindent(),
17557 );
17558}
17559
17560#[gpui::test]
17561async fn test_edits_around_expanded_insertion_hunks(
17562 executor: BackgroundExecutor,
17563 cx: &mut TestAppContext,
17564) {
17565 init_test(cx, |_| {});
17566
17567 let mut cx = EditorTestContext::new(cx).await;
17568
17569 let diff_base = r#"
17570 use some::mod1;
17571 use some::mod2;
17572
17573 const A: u32 = 42;
17574
17575 fn main() {
17576 println!("hello");
17577
17578 println!("world");
17579 }
17580 "#
17581 .unindent();
17582 executor.run_until_parked();
17583 cx.set_state(
17584 &r#"
17585 use some::mod1;
17586 use some::mod2;
17587
17588 const A: u32 = 42;
17589 const B: u32 = 42;
17590 const C: u32 = 42;
17591 ˇ
17592
17593 fn main() {
17594 println!("hello");
17595
17596 println!("world");
17597 }
17598 "#
17599 .unindent(),
17600 );
17601
17602 cx.set_head_text(&diff_base);
17603 executor.run_until_parked();
17604
17605 cx.update_editor(|editor, window, cx| {
17606 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17607 });
17608 executor.run_until_parked();
17609
17610 cx.assert_state_with_diff(
17611 r#"
17612 use some::mod1;
17613 use some::mod2;
17614
17615 const A: u32 = 42;
17616 + const B: u32 = 42;
17617 + const C: u32 = 42;
17618 + ˇ
17619
17620 fn main() {
17621 println!("hello");
17622
17623 println!("world");
17624 }
17625 "#
17626 .unindent(),
17627 );
17628
17629 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
17630 executor.run_until_parked();
17631
17632 cx.assert_state_with_diff(
17633 r#"
17634 use some::mod1;
17635 use some::mod2;
17636
17637 const A: u32 = 42;
17638 + const B: u32 = 42;
17639 + const C: u32 = 42;
17640 + const D: u32 = 42;
17641 + ˇ
17642
17643 fn main() {
17644 println!("hello");
17645
17646 println!("world");
17647 }
17648 "#
17649 .unindent(),
17650 );
17651
17652 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
17653 executor.run_until_parked();
17654
17655 cx.assert_state_with_diff(
17656 r#"
17657 use some::mod1;
17658 use some::mod2;
17659
17660 const A: u32 = 42;
17661 + const B: u32 = 42;
17662 + const C: u32 = 42;
17663 + const D: u32 = 42;
17664 + const E: u32 = 42;
17665 + ˇ
17666
17667 fn main() {
17668 println!("hello");
17669
17670 println!("world");
17671 }
17672 "#
17673 .unindent(),
17674 );
17675
17676 cx.update_editor(|editor, window, cx| {
17677 editor.delete_line(&DeleteLine, window, cx);
17678 });
17679 executor.run_until_parked();
17680
17681 cx.assert_state_with_diff(
17682 r#"
17683 use some::mod1;
17684 use some::mod2;
17685
17686 const A: u32 = 42;
17687 + const B: u32 = 42;
17688 + const C: u32 = 42;
17689 + const D: u32 = 42;
17690 + const E: u32 = 42;
17691 ˇ
17692 fn main() {
17693 println!("hello");
17694
17695 println!("world");
17696 }
17697 "#
17698 .unindent(),
17699 );
17700
17701 cx.update_editor(|editor, window, cx| {
17702 editor.move_up(&MoveUp, window, cx);
17703 editor.delete_line(&DeleteLine, window, cx);
17704 editor.move_up(&MoveUp, window, cx);
17705 editor.delete_line(&DeleteLine, window, cx);
17706 editor.move_up(&MoveUp, window, cx);
17707 editor.delete_line(&DeleteLine, window, cx);
17708 });
17709 executor.run_until_parked();
17710 cx.assert_state_with_diff(
17711 r#"
17712 use some::mod1;
17713 use some::mod2;
17714
17715 const A: u32 = 42;
17716 + const B: u32 = 42;
17717 ˇ
17718 fn main() {
17719 println!("hello");
17720
17721 println!("world");
17722 }
17723 "#
17724 .unindent(),
17725 );
17726
17727 cx.update_editor(|editor, window, cx| {
17728 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
17729 editor.delete_line(&DeleteLine, window, cx);
17730 });
17731 executor.run_until_parked();
17732 cx.assert_state_with_diff(
17733 r#"
17734 ˇ
17735 fn main() {
17736 println!("hello");
17737
17738 println!("world");
17739 }
17740 "#
17741 .unindent(),
17742 );
17743}
17744
17745#[gpui::test]
17746async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
17747 init_test(cx, |_| {});
17748
17749 let mut cx = EditorTestContext::new(cx).await;
17750 cx.set_head_text(indoc! { "
17751 one
17752 two
17753 three
17754 four
17755 five
17756 "
17757 });
17758 cx.set_state(indoc! { "
17759 one
17760 ˇthree
17761 five
17762 "});
17763 cx.run_until_parked();
17764 cx.update_editor(|editor, window, cx| {
17765 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17766 });
17767 cx.assert_state_with_diff(
17768 indoc! { "
17769 one
17770 - two
17771 ˇthree
17772 - four
17773 five
17774 "}
17775 .to_string(),
17776 );
17777 cx.update_editor(|editor, window, cx| {
17778 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17779 });
17780
17781 cx.assert_state_with_diff(
17782 indoc! { "
17783 one
17784 ˇthree
17785 five
17786 "}
17787 .to_string(),
17788 );
17789
17790 cx.set_state(indoc! { "
17791 one
17792 ˇTWO
17793 three
17794 four
17795 five
17796 "});
17797 cx.run_until_parked();
17798 cx.update_editor(|editor, window, cx| {
17799 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17800 });
17801
17802 cx.assert_state_with_diff(
17803 indoc! { "
17804 one
17805 - two
17806 + ˇTWO
17807 three
17808 four
17809 five
17810 "}
17811 .to_string(),
17812 );
17813 cx.update_editor(|editor, window, cx| {
17814 editor.move_up(&Default::default(), window, cx);
17815 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17816 });
17817 cx.assert_state_with_diff(
17818 indoc! { "
17819 one
17820 ˇTWO
17821 three
17822 four
17823 five
17824 "}
17825 .to_string(),
17826 );
17827}
17828
17829#[gpui::test]
17830async fn test_edits_around_expanded_deletion_hunks(
17831 executor: BackgroundExecutor,
17832 cx: &mut TestAppContext,
17833) {
17834 init_test(cx, |_| {});
17835
17836 let mut cx = EditorTestContext::new(cx).await;
17837
17838 let diff_base = r#"
17839 use some::mod1;
17840 use some::mod2;
17841
17842 const A: u32 = 42;
17843 const B: u32 = 42;
17844 const C: u32 = 42;
17845
17846
17847 fn main() {
17848 println!("hello");
17849
17850 println!("world");
17851 }
17852 "#
17853 .unindent();
17854 executor.run_until_parked();
17855 cx.set_state(
17856 &r#"
17857 use some::mod1;
17858 use some::mod2;
17859
17860 ˇconst B: u32 = 42;
17861 const C: u32 = 42;
17862
17863
17864 fn main() {
17865 println!("hello");
17866
17867 println!("world");
17868 }
17869 "#
17870 .unindent(),
17871 );
17872
17873 cx.set_head_text(&diff_base);
17874 executor.run_until_parked();
17875
17876 cx.update_editor(|editor, window, cx| {
17877 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17878 });
17879 executor.run_until_parked();
17880
17881 cx.assert_state_with_diff(
17882 r#"
17883 use some::mod1;
17884 use some::mod2;
17885
17886 - const A: u32 = 42;
17887 ˇconst B: u32 = 42;
17888 const C: u32 = 42;
17889
17890
17891 fn main() {
17892 println!("hello");
17893
17894 println!("world");
17895 }
17896 "#
17897 .unindent(),
17898 );
17899
17900 cx.update_editor(|editor, window, cx| {
17901 editor.delete_line(&DeleteLine, window, cx);
17902 });
17903 executor.run_until_parked();
17904 cx.assert_state_with_diff(
17905 r#"
17906 use some::mod1;
17907 use some::mod2;
17908
17909 - const A: u32 = 42;
17910 - const B: u32 = 42;
17911 ˇconst C: u32 = 42;
17912
17913
17914 fn main() {
17915 println!("hello");
17916
17917 println!("world");
17918 }
17919 "#
17920 .unindent(),
17921 );
17922
17923 cx.update_editor(|editor, window, cx| {
17924 editor.delete_line(&DeleteLine, window, cx);
17925 });
17926 executor.run_until_parked();
17927 cx.assert_state_with_diff(
17928 r#"
17929 use some::mod1;
17930 use some::mod2;
17931
17932 - const A: u32 = 42;
17933 - const B: u32 = 42;
17934 - const C: u32 = 42;
17935 ˇ
17936
17937 fn main() {
17938 println!("hello");
17939
17940 println!("world");
17941 }
17942 "#
17943 .unindent(),
17944 );
17945
17946 cx.update_editor(|editor, window, cx| {
17947 editor.handle_input("replacement", window, cx);
17948 });
17949 executor.run_until_parked();
17950 cx.assert_state_with_diff(
17951 r#"
17952 use some::mod1;
17953 use some::mod2;
17954
17955 - const A: u32 = 42;
17956 - const B: u32 = 42;
17957 - const C: u32 = 42;
17958 -
17959 + replacementˇ
17960
17961 fn main() {
17962 println!("hello");
17963
17964 println!("world");
17965 }
17966 "#
17967 .unindent(),
17968 );
17969}
17970
17971#[gpui::test]
17972async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17973 init_test(cx, |_| {});
17974
17975 let mut cx = EditorTestContext::new(cx).await;
17976
17977 let base_text = r#"
17978 one
17979 two
17980 three
17981 four
17982 five
17983 "#
17984 .unindent();
17985 executor.run_until_parked();
17986 cx.set_state(
17987 &r#"
17988 one
17989 two
17990 fˇour
17991 five
17992 "#
17993 .unindent(),
17994 );
17995
17996 cx.set_head_text(&base_text);
17997 executor.run_until_parked();
17998
17999 cx.update_editor(|editor, window, cx| {
18000 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18001 });
18002 executor.run_until_parked();
18003
18004 cx.assert_state_with_diff(
18005 r#"
18006 one
18007 two
18008 - three
18009 fˇour
18010 five
18011 "#
18012 .unindent(),
18013 );
18014
18015 cx.update_editor(|editor, window, cx| {
18016 editor.backspace(&Backspace, window, cx);
18017 editor.backspace(&Backspace, window, cx);
18018 });
18019 executor.run_until_parked();
18020 cx.assert_state_with_diff(
18021 r#"
18022 one
18023 two
18024 - threeˇ
18025 - four
18026 + our
18027 five
18028 "#
18029 .unindent(),
18030 );
18031}
18032
18033#[gpui::test]
18034async fn test_edit_after_expanded_modification_hunk(
18035 executor: BackgroundExecutor,
18036 cx: &mut TestAppContext,
18037) {
18038 init_test(cx, |_| {});
18039
18040 let mut cx = EditorTestContext::new(cx).await;
18041
18042 let diff_base = r#"
18043 use some::mod1;
18044 use some::mod2;
18045
18046 const A: u32 = 42;
18047 const B: u32 = 42;
18048 const C: u32 = 42;
18049 const D: u32 = 42;
18050
18051
18052 fn main() {
18053 println!("hello");
18054
18055 println!("world");
18056 }"#
18057 .unindent();
18058
18059 cx.set_state(
18060 &r#"
18061 use some::mod1;
18062 use some::mod2;
18063
18064 const A: u32 = 42;
18065 const B: u32 = 42;
18066 const C: u32 = 43ˇ
18067 const D: u32 = 42;
18068
18069
18070 fn main() {
18071 println!("hello");
18072
18073 println!("world");
18074 }"#
18075 .unindent(),
18076 );
18077
18078 cx.set_head_text(&diff_base);
18079 executor.run_until_parked();
18080 cx.update_editor(|editor, window, cx| {
18081 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18082 });
18083 executor.run_until_parked();
18084
18085 cx.assert_state_with_diff(
18086 r#"
18087 use some::mod1;
18088 use some::mod2;
18089
18090 const A: u32 = 42;
18091 const B: u32 = 42;
18092 - const C: u32 = 42;
18093 + const C: u32 = 43ˇ
18094 const D: u32 = 42;
18095
18096
18097 fn main() {
18098 println!("hello");
18099
18100 println!("world");
18101 }"#
18102 .unindent(),
18103 );
18104
18105 cx.update_editor(|editor, window, cx| {
18106 editor.handle_input("\nnew_line\n", window, cx);
18107 });
18108 executor.run_until_parked();
18109
18110 cx.assert_state_with_diff(
18111 r#"
18112 use some::mod1;
18113 use some::mod2;
18114
18115 const A: u32 = 42;
18116 const B: u32 = 42;
18117 - const C: u32 = 42;
18118 + const C: u32 = 43
18119 + new_line
18120 + ˇ
18121 const D: u32 = 42;
18122
18123
18124 fn main() {
18125 println!("hello");
18126
18127 println!("world");
18128 }"#
18129 .unindent(),
18130 );
18131}
18132
18133#[gpui::test]
18134async fn test_stage_and_unstage_added_file_hunk(
18135 executor: BackgroundExecutor,
18136 cx: &mut TestAppContext,
18137) {
18138 init_test(cx, |_| {});
18139
18140 let mut cx = EditorTestContext::new(cx).await;
18141 cx.update_editor(|editor, _, cx| {
18142 editor.set_expand_all_diff_hunks(cx);
18143 });
18144
18145 let working_copy = r#"
18146 ˇfn main() {
18147 println!("hello, world!");
18148 }
18149 "#
18150 .unindent();
18151
18152 cx.set_state(&working_copy);
18153 executor.run_until_parked();
18154
18155 cx.assert_state_with_diff(
18156 r#"
18157 + ˇfn main() {
18158 + println!("hello, world!");
18159 + }
18160 "#
18161 .unindent(),
18162 );
18163 cx.assert_index_text(None);
18164
18165 cx.update_editor(|editor, window, cx| {
18166 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18167 });
18168 executor.run_until_parked();
18169 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
18170 cx.assert_state_with_diff(
18171 r#"
18172 + ˇfn main() {
18173 + println!("hello, world!");
18174 + }
18175 "#
18176 .unindent(),
18177 );
18178
18179 cx.update_editor(|editor, window, cx| {
18180 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18181 });
18182 executor.run_until_parked();
18183 cx.assert_index_text(None);
18184}
18185
18186async fn setup_indent_guides_editor(
18187 text: &str,
18188 cx: &mut TestAppContext,
18189) -> (BufferId, EditorTestContext) {
18190 init_test(cx, |_| {});
18191
18192 let mut cx = EditorTestContext::new(cx).await;
18193
18194 let buffer_id = cx.update_editor(|editor, window, cx| {
18195 editor.set_text(text, window, cx);
18196 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
18197
18198 buffer_ids[0]
18199 });
18200
18201 (buffer_id, cx)
18202}
18203
18204fn assert_indent_guides(
18205 range: Range<u32>,
18206 expected: Vec<IndentGuide>,
18207 active_indices: Option<Vec<usize>>,
18208 cx: &mut EditorTestContext,
18209) {
18210 let indent_guides = cx.update_editor(|editor, window, cx| {
18211 let snapshot = editor.snapshot(window, cx).display_snapshot;
18212 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
18213 editor,
18214 MultiBufferRow(range.start)..MultiBufferRow(range.end),
18215 true,
18216 &snapshot,
18217 cx,
18218 );
18219
18220 indent_guides.sort_by(|a, b| {
18221 a.depth.cmp(&b.depth).then(
18222 a.start_row
18223 .cmp(&b.start_row)
18224 .then(a.end_row.cmp(&b.end_row)),
18225 )
18226 });
18227 indent_guides
18228 });
18229
18230 if let Some(expected) = active_indices {
18231 let active_indices = cx.update_editor(|editor, window, cx| {
18232 let snapshot = editor.snapshot(window, cx).display_snapshot;
18233 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
18234 });
18235
18236 assert_eq!(
18237 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
18238 expected,
18239 "Active indent guide indices do not match"
18240 );
18241 }
18242
18243 assert_eq!(indent_guides, expected, "Indent guides do not match");
18244}
18245
18246fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
18247 IndentGuide {
18248 buffer_id,
18249 start_row: MultiBufferRow(start_row),
18250 end_row: MultiBufferRow(end_row),
18251 depth,
18252 tab_size: 4,
18253 settings: IndentGuideSettings {
18254 enabled: true,
18255 line_width: 1,
18256 active_line_width: 1,
18257 ..Default::default()
18258 },
18259 }
18260}
18261
18262#[gpui::test]
18263async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
18264 let (buffer_id, mut cx) = setup_indent_guides_editor(
18265 &"
18266 fn main() {
18267 let a = 1;
18268 }"
18269 .unindent(),
18270 cx,
18271 )
18272 .await;
18273
18274 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18275}
18276
18277#[gpui::test]
18278async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
18279 let (buffer_id, mut cx) = setup_indent_guides_editor(
18280 &"
18281 fn main() {
18282 let a = 1;
18283 let b = 2;
18284 }"
18285 .unindent(),
18286 cx,
18287 )
18288 .await;
18289
18290 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
18291}
18292
18293#[gpui::test]
18294async fn test_indent_guide_nested(cx: &mut TestAppContext) {
18295 let (buffer_id, mut cx) = setup_indent_guides_editor(
18296 &"
18297 fn main() {
18298 let a = 1;
18299 if a == 3 {
18300 let b = 2;
18301 } else {
18302 let c = 3;
18303 }
18304 }"
18305 .unindent(),
18306 cx,
18307 )
18308 .await;
18309
18310 assert_indent_guides(
18311 0..8,
18312 vec![
18313 indent_guide(buffer_id, 1, 6, 0),
18314 indent_guide(buffer_id, 3, 3, 1),
18315 indent_guide(buffer_id, 5, 5, 1),
18316 ],
18317 None,
18318 &mut cx,
18319 );
18320}
18321
18322#[gpui::test]
18323async fn test_indent_guide_tab(cx: &mut TestAppContext) {
18324 let (buffer_id, mut cx) = setup_indent_guides_editor(
18325 &"
18326 fn main() {
18327 let a = 1;
18328 let b = 2;
18329 let c = 3;
18330 }"
18331 .unindent(),
18332 cx,
18333 )
18334 .await;
18335
18336 assert_indent_guides(
18337 0..5,
18338 vec![
18339 indent_guide(buffer_id, 1, 3, 0),
18340 indent_guide(buffer_id, 2, 2, 1),
18341 ],
18342 None,
18343 &mut cx,
18344 );
18345}
18346
18347#[gpui::test]
18348async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
18349 let (buffer_id, mut cx) = setup_indent_guides_editor(
18350 &"
18351 fn main() {
18352 let a = 1;
18353
18354 let c = 3;
18355 }"
18356 .unindent(),
18357 cx,
18358 )
18359 .await;
18360
18361 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
18362}
18363
18364#[gpui::test]
18365async fn test_indent_guide_complex(cx: &mut TestAppContext) {
18366 let (buffer_id, mut cx) = setup_indent_guides_editor(
18367 &"
18368 fn main() {
18369 let a = 1;
18370
18371 let c = 3;
18372
18373 if a == 3 {
18374 let b = 2;
18375 } else {
18376 let c = 3;
18377 }
18378 }"
18379 .unindent(),
18380 cx,
18381 )
18382 .await;
18383
18384 assert_indent_guides(
18385 0..11,
18386 vec![
18387 indent_guide(buffer_id, 1, 9, 0),
18388 indent_guide(buffer_id, 6, 6, 1),
18389 indent_guide(buffer_id, 8, 8, 1),
18390 ],
18391 None,
18392 &mut cx,
18393 );
18394}
18395
18396#[gpui::test]
18397async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
18398 let (buffer_id, mut cx) = setup_indent_guides_editor(
18399 &"
18400 fn main() {
18401 let a = 1;
18402
18403 let c = 3;
18404
18405 if a == 3 {
18406 let b = 2;
18407 } else {
18408 let c = 3;
18409 }
18410 }"
18411 .unindent(),
18412 cx,
18413 )
18414 .await;
18415
18416 assert_indent_guides(
18417 1..11,
18418 vec![
18419 indent_guide(buffer_id, 1, 9, 0),
18420 indent_guide(buffer_id, 6, 6, 1),
18421 indent_guide(buffer_id, 8, 8, 1),
18422 ],
18423 None,
18424 &mut cx,
18425 );
18426}
18427
18428#[gpui::test]
18429async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
18430 let (buffer_id, mut cx) = setup_indent_guides_editor(
18431 &"
18432 fn main() {
18433 let a = 1;
18434
18435 let c = 3;
18436
18437 if a == 3 {
18438 let b = 2;
18439 } else {
18440 let c = 3;
18441 }
18442 }"
18443 .unindent(),
18444 cx,
18445 )
18446 .await;
18447
18448 assert_indent_guides(
18449 1..10,
18450 vec![
18451 indent_guide(buffer_id, 1, 9, 0),
18452 indent_guide(buffer_id, 6, 6, 1),
18453 indent_guide(buffer_id, 8, 8, 1),
18454 ],
18455 None,
18456 &mut cx,
18457 );
18458}
18459
18460#[gpui::test]
18461async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
18462 let (buffer_id, mut cx) = setup_indent_guides_editor(
18463 &"
18464 fn main() {
18465 if a {
18466 b(
18467 c,
18468 d,
18469 )
18470 } else {
18471 e(
18472 f
18473 )
18474 }
18475 }"
18476 .unindent(),
18477 cx,
18478 )
18479 .await;
18480
18481 assert_indent_guides(
18482 0..11,
18483 vec![
18484 indent_guide(buffer_id, 1, 10, 0),
18485 indent_guide(buffer_id, 2, 5, 1),
18486 indent_guide(buffer_id, 7, 9, 1),
18487 indent_guide(buffer_id, 3, 4, 2),
18488 indent_guide(buffer_id, 8, 8, 2),
18489 ],
18490 None,
18491 &mut cx,
18492 );
18493
18494 cx.update_editor(|editor, window, cx| {
18495 editor.fold_at(MultiBufferRow(2), window, cx);
18496 assert_eq!(
18497 editor.display_text(cx),
18498 "
18499 fn main() {
18500 if a {
18501 b(⋯
18502 )
18503 } else {
18504 e(
18505 f
18506 )
18507 }
18508 }"
18509 .unindent()
18510 );
18511 });
18512
18513 assert_indent_guides(
18514 0..11,
18515 vec![
18516 indent_guide(buffer_id, 1, 10, 0),
18517 indent_guide(buffer_id, 2, 5, 1),
18518 indent_guide(buffer_id, 7, 9, 1),
18519 indent_guide(buffer_id, 8, 8, 2),
18520 ],
18521 None,
18522 &mut cx,
18523 );
18524}
18525
18526#[gpui::test]
18527async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
18528 let (buffer_id, mut cx) = setup_indent_guides_editor(
18529 &"
18530 block1
18531 block2
18532 block3
18533 block4
18534 block2
18535 block1
18536 block1"
18537 .unindent(),
18538 cx,
18539 )
18540 .await;
18541
18542 assert_indent_guides(
18543 1..10,
18544 vec![
18545 indent_guide(buffer_id, 1, 4, 0),
18546 indent_guide(buffer_id, 2, 3, 1),
18547 indent_guide(buffer_id, 3, 3, 2),
18548 ],
18549 None,
18550 &mut cx,
18551 );
18552}
18553
18554#[gpui::test]
18555async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
18556 let (buffer_id, mut cx) = setup_indent_guides_editor(
18557 &"
18558 block1
18559 block2
18560 block3
18561
18562 block1
18563 block1"
18564 .unindent(),
18565 cx,
18566 )
18567 .await;
18568
18569 assert_indent_guides(
18570 0..6,
18571 vec![
18572 indent_guide(buffer_id, 1, 2, 0),
18573 indent_guide(buffer_id, 2, 2, 1),
18574 ],
18575 None,
18576 &mut cx,
18577 );
18578}
18579
18580#[gpui::test]
18581async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
18582 let (buffer_id, mut cx) = setup_indent_guides_editor(
18583 &"
18584 function component() {
18585 \treturn (
18586 \t\t\t
18587 \t\t<div>
18588 \t\t\t<abc></abc>
18589 \t\t</div>
18590 \t)
18591 }"
18592 .unindent(),
18593 cx,
18594 )
18595 .await;
18596
18597 assert_indent_guides(
18598 0..8,
18599 vec![
18600 indent_guide(buffer_id, 1, 6, 0),
18601 indent_guide(buffer_id, 2, 5, 1),
18602 indent_guide(buffer_id, 4, 4, 2),
18603 ],
18604 None,
18605 &mut cx,
18606 );
18607}
18608
18609#[gpui::test]
18610async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
18611 let (buffer_id, mut cx) = setup_indent_guides_editor(
18612 &"
18613 function component() {
18614 \treturn (
18615 \t
18616 \t\t<div>
18617 \t\t\t<abc></abc>
18618 \t\t</div>
18619 \t)
18620 }"
18621 .unindent(),
18622 cx,
18623 )
18624 .await;
18625
18626 assert_indent_guides(
18627 0..8,
18628 vec![
18629 indent_guide(buffer_id, 1, 6, 0),
18630 indent_guide(buffer_id, 2, 5, 1),
18631 indent_guide(buffer_id, 4, 4, 2),
18632 ],
18633 None,
18634 &mut cx,
18635 );
18636}
18637
18638#[gpui::test]
18639async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
18640 let (buffer_id, mut cx) = setup_indent_guides_editor(
18641 &"
18642 block1
18643
18644
18645
18646 block2
18647 "
18648 .unindent(),
18649 cx,
18650 )
18651 .await;
18652
18653 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18654}
18655
18656#[gpui::test]
18657async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
18658 let (buffer_id, mut cx) = setup_indent_guides_editor(
18659 &"
18660 def a:
18661 \tb = 3
18662 \tif True:
18663 \t\tc = 4
18664 \t\td = 5
18665 \tprint(b)
18666 "
18667 .unindent(),
18668 cx,
18669 )
18670 .await;
18671
18672 assert_indent_guides(
18673 0..6,
18674 vec![
18675 indent_guide(buffer_id, 1, 5, 0),
18676 indent_guide(buffer_id, 3, 4, 1),
18677 ],
18678 None,
18679 &mut cx,
18680 );
18681}
18682
18683#[gpui::test]
18684async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
18685 let (buffer_id, mut cx) = setup_indent_guides_editor(
18686 &"
18687 fn main() {
18688 let a = 1;
18689 }"
18690 .unindent(),
18691 cx,
18692 )
18693 .await;
18694
18695 cx.update_editor(|editor, window, cx| {
18696 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18697 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18698 });
18699 });
18700
18701 assert_indent_guides(
18702 0..3,
18703 vec![indent_guide(buffer_id, 1, 1, 0)],
18704 Some(vec![0]),
18705 &mut cx,
18706 );
18707}
18708
18709#[gpui::test]
18710async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
18711 let (buffer_id, mut cx) = setup_indent_guides_editor(
18712 &"
18713 fn main() {
18714 if 1 == 2 {
18715 let a = 1;
18716 }
18717 }"
18718 .unindent(),
18719 cx,
18720 )
18721 .await;
18722
18723 cx.update_editor(|editor, window, cx| {
18724 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18725 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18726 });
18727 });
18728
18729 assert_indent_guides(
18730 0..4,
18731 vec![
18732 indent_guide(buffer_id, 1, 3, 0),
18733 indent_guide(buffer_id, 2, 2, 1),
18734 ],
18735 Some(vec![1]),
18736 &mut cx,
18737 );
18738
18739 cx.update_editor(|editor, window, cx| {
18740 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18741 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18742 });
18743 });
18744
18745 assert_indent_guides(
18746 0..4,
18747 vec![
18748 indent_guide(buffer_id, 1, 3, 0),
18749 indent_guide(buffer_id, 2, 2, 1),
18750 ],
18751 Some(vec![1]),
18752 &mut cx,
18753 );
18754
18755 cx.update_editor(|editor, window, cx| {
18756 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18757 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
18758 });
18759 });
18760
18761 assert_indent_guides(
18762 0..4,
18763 vec![
18764 indent_guide(buffer_id, 1, 3, 0),
18765 indent_guide(buffer_id, 2, 2, 1),
18766 ],
18767 Some(vec![0]),
18768 &mut cx,
18769 );
18770}
18771
18772#[gpui::test]
18773async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
18774 let (buffer_id, mut cx) = setup_indent_guides_editor(
18775 &"
18776 fn main() {
18777 let a = 1;
18778
18779 let b = 2;
18780 }"
18781 .unindent(),
18782 cx,
18783 )
18784 .await;
18785
18786 cx.update_editor(|editor, window, cx| {
18787 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18788 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18789 });
18790 });
18791
18792 assert_indent_guides(
18793 0..5,
18794 vec![indent_guide(buffer_id, 1, 3, 0)],
18795 Some(vec![0]),
18796 &mut cx,
18797 );
18798}
18799
18800#[gpui::test]
18801async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
18802 let (buffer_id, mut cx) = setup_indent_guides_editor(
18803 &"
18804 def m:
18805 a = 1
18806 pass"
18807 .unindent(),
18808 cx,
18809 )
18810 .await;
18811
18812 cx.update_editor(|editor, window, cx| {
18813 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18814 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18815 });
18816 });
18817
18818 assert_indent_guides(
18819 0..3,
18820 vec![indent_guide(buffer_id, 1, 2, 0)],
18821 Some(vec![0]),
18822 &mut cx,
18823 );
18824}
18825
18826#[gpui::test]
18827async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
18828 init_test(cx, |_| {});
18829 let mut cx = EditorTestContext::new(cx).await;
18830 let text = indoc! {
18831 "
18832 impl A {
18833 fn b() {
18834 0;
18835 3;
18836 5;
18837 6;
18838 7;
18839 }
18840 }
18841 "
18842 };
18843 let base_text = indoc! {
18844 "
18845 impl A {
18846 fn b() {
18847 0;
18848 1;
18849 2;
18850 3;
18851 4;
18852 }
18853 fn c() {
18854 5;
18855 6;
18856 7;
18857 }
18858 }
18859 "
18860 };
18861
18862 cx.update_editor(|editor, window, cx| {
18863 editor.set_text(text, window, cx);
18864
18865 editor.buffer().update(cx, |multibuffer, cx| {
18866 let buffer = multibuffer.as_singleton().unwrap();
18867 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
18868
18869 multibuffer.set_all_diff_hunks_expanded(cx);
18870 multibuffer.add_diff(diff, cx);
18871
18872 buffer.read(cx).remote_id()
18873 })
18874 });
18875 cx.run_until_parked();
18876
18877 cx.assert_state_with_diff(
18878 indoc! { "
18879 impl A {
18880 fn b() {
18881 0;
18882 - 1;
18883 - 2;
18884 3;
18885 - 4;
18886 - }
18887 - fn c() {
18888 5;
18889 6;
18890 7;
18891 }
18892 }
18893 ˇ"
18894 }
18895 .to_string(),
18896 );
18897
18898 let mut actual_guides = cx.update_editor(|editor, window, cx| {
18899 editor
18900 .snapshot(window, cx)
18901 .buffer_snapshot
18902 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
18903 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
18904 .collect::<Vec<_>>()
18905 });
18906 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
18907 assert_eq!(
18908 actual_guides,
18909 vec![
18910 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
18911 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
18912 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
18913 ]
18914 );
18915}
18916
18917#[gpui::test]
18918async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18919 init_test(cx, |_| {});
18920 let mut cx = EditorTestContext::new(cx).await;
18921
18922 let diff_base = r#"
18923 a
18924 b
18925 c
18926 "#
18927 .unindent();
18928
18929 cx.set_state(
18930 &r#"
18931 ˇA
18932 b
18933 C
18934 "#
18935 .unindent(),
18936 );
18937 cx.set_head_text(&diff_base);
18938 cx.update_editor(|editor, window, cx| {
18939 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18940 });
18941 executor.run_until_parked();
18942
18943 let both_hunks_expanded = r#"
18944 - a
18945 + ˇA
18946 b
18947 - c
18948 + C
18949 "#
18950 .unindent();
18951
18952 cx.assert_state_with_diff(both_hunks_expanded.clone());
18953
18954 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18955 let snapshot = editor.snapshot(window, cx);
18956 let hunks = editor
18957 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18958 .collect::<Vec<_>>();
18959 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18960 let buffer_id = hunks[0].buffer_id;
18961 hunks
18962 .into_iter()
18963 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18964 .collect::<Vec<_>>()
18965 });
18966 assert_eq!(hunk_ranges.len(), 2);
18967
18968 cx.update_editor(|editor, _, cx| {
18969 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18970 });
18971 executor.run_until_parked();
18972
18973 let second_hunk_expanded = r#"
18974 ˇA
18975 b
18976 - c
18977 + C
18978 "#
18979 .unindent();
18980
18981 cx.assert_state_with_diff(second_hunk_expanded);
18982
18983 cx.update_editor(|editor, _, cx| {
18984 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18985 });
18986 executor.run_until_parked();
18987
18988 cx.assert_state_with_diff(both_hunks_expanded.clone());
18989
18990 cx.update_editor(|editor, _, cx| {
18991 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18992 });
18993 executor.run_until_parked();
18994
18995 let first_hunk_expanded = r#"
18996 - a
18997 + ˇA
18998 b
18999 C
19000 "#
19001 .unindent();
19002
19003 cx.assert_state_with_diff(first_hunk_expanded);
19004
19005 cx.update_editor(|editor, _, cx| {
19006 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19007 });
19008 executor.run_until_parked();
19009
19010 cx.assert_state_with_diff(both_hunks_expanded);
19011
19012 cx.set_state(
19013 &r#"
19014 ˇA
19015 b
19016 "#
19017 .unindent(),
19018 );
19019 cx.run_until_parked();
19020
19021 // TODO this cursor position seems bad
19022 cx.assert_state_with_diff(
19023 r#"
19024 - ˇa
19025 + A
19026 b
19027 "#
19028 .unindent(),
19029 );
19030
19031 cx.update_editor(|editor, window, cx| {
19032 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19033 });
19034
19035 cx.assert_state_with_diff(
19036 r#"
19037 - ˇa
19038 + A
19039 b
19040 - c
19041 "#
19042 .unindent(),
19043 );
19044
19045 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19046 let snapshot = editor.snapshot(window, cx);
19047 let hunks = editor
19048 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19049 .collect::<Vec<_>>();
19050 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19051 let buffer_id = hunks[0].buffer_id;
19052 hunks
19053 .into_iter()
19054 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19055 .collect::<Vec<_>>()
19056 });
19057 assert_eq!(hunk_ranges.len(), 2);
19058
19059 cx.update_editor(|editor, _, cx| {
19060 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19061 });
19062 executor.run_until_parked();
19063
19064 cx.assert_state_with_diff(
19065 r#"
19066 - ˇa
19067 + A
19068 b
19069 "#
19070 .unindent(),
19071 );
19072}
19073
19074#[gpui::test]
19075async fn test_toggle_deletion_hunk_at_start_of_file(
19076 executor: BackgroundExecutor,
19077 cx: &mut TestAppContext,
19078) {
19079 init_test(cx, |_| {});
19080 let mut cx = EditorTestContext::new(cx).await;
19081
19082 let diff_base = r#"
19083 a
19084 b
19085 c
19086 "#
19087 .unindent();
19088
19089 cx.set_state(
19090 &r#"
19091 ˇb
19092 c
19093 "#
19094 .unindent(),
19095 );
19096 cx.set_head_text(&diff_base);
19097 cx.update_editor(|editor, window, cx| {
19098 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19099 });
19100 executor.run_until_parked();
19101
19102 let hunk_expanded = r#"
19103 - a
19104 ˇb
19105 c
19106 "#
19107 .unindent();
19108
19109 cx.assert_state_with_diff(hunk_expanded.clone());
19110
19111 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19112 let snapshot = editor.snapshot(window, cx);
19113 let hunks = editor
19114 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19115 .collect::<Vec<_>>();
19116 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19117 let buffer_id = hunks[0].buffer_id;
19118 hunks
19119 .into_iter()
19120 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19121 .collect::<Vec<_>>()
19122 });
19123 assert_eq!(hunk_ranges.len(), 1);
19124
19125 cx.update_editor(|editor, _, cx| {
19126 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19127 });
19128 executor.run_until_parked();
19129
19130 let hunk_collapsed = r#"
19131 ˇb
19132 c
19133 "#
19134 .unindent();
19135
19136 cx.assert_state_with_diff(hunk_collapsed);
19137
19138 cx.update_editor(|editor, _, cx| {
19139 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19140 });
19141 executor.run_until_parked();
19142
19143 cx.assert_state_with_diff(hunk_expanded.clone());
19144}
19145
19146#[gpui::test]
19147async fn test_display_diff_hunks(cx: &mut TestAppContext) {
19148 init_test(cx, |_| {});
19149
19150 let fs = FakeFs::new(cx.executor());
19151 fs.insert_tree(
19152 path!("/test"),
19153 json!({
19154 ".git": {},
19155 "file-1": "ONE\n",
19156 "file-2": "TWO\n",
19157 "file-3": "THREE\n",
19158 }),
19159 )
19160 .await;
19161
19162 fs.set_head_for_repo(
19163 path!("/test/.git").as_ref(),
19164 &[
19165 ("file-1".into(), "one\n".into()),
19166 ("file-2".into(), "two\n".into()),
19167 ("file-3".into(), "three\n".into()),
19168 ],
19169 "deadbeef",
19170 );
19171
19172 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
19173 let mut buffers = vec![];
19174 for i in 1..=3 {
19175 let buffer = project
19176 .update(cx, |project, cx| {
19177 let path = format!(path!("/test/file-{}"), i);
19178 project.open_local_buffer(path, cx)
19179 })
19180 .await
19181 .unwrap();
19182 buffers.push(buffer);
19183 }
19184
19185 let multibuffer = cx.new(|cx| {
19186 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
19187 multibuffer.set_all_diff_hunks_expanded(cx);
19188 for buffer in &buffers {
19189 let snapshot = buffer.read(cx).snapshot();
19190 multibuffer.set_excerpts_for_path(
19191 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
19192 buffer.clone(),
19193 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
19194 DEFAULT_MULTIBUFFER_CONTEXT,
19195 cx,
19196 );
19197 }
19198 multibuffer
19199 });
19200
19201 let editor = cx.add_window(|window, cx| {
19202 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
19203 });
19204 cx.run_until_parked();
19205
19206 let snapshot = editor
19207 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19208 .unwrap();
19209 let hunks = snapshot
19210 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
19211 .map(|hunk| match hunk {
19212 DisplayDiffHunk::Unfolded {
19213 display_row_range, ..
19214 } => display_row_range,
19215 DisplayDiffHunk::Folded { .. } => unreachable!(),
19216 })
19217 .collect::<Vec<_>>();
19218 assert_eq!(
19219 hunks,
19220 [
19221 DisplayRow(2)..DisplayRow(4),
19222 DisplayRow(7)..DisplayRow(9),
19223 DisplayRow(12)..DisplayRow(14),
19224 ]
19225 );
19226}
19227
19228#[gpui::test]
19229async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
19230 init_test(cx, |_| {});
19231
19232 let mut cx = EditorTestContext::new(cx).await;
19233 cx.set_head_text(indoc! { "
19234 one
19235 two
19236 three
19237 four
19238 five
19239 "
19240 });
19241 cx.set_index_text(indoc! { "
19242 one
19243 two
19244 three
19245 four
19246 five
19247 "
19248 });
19249 cx.set_state(indoc! {"
19250 one
19251 TWO
19252 ˇTHREE
19253 FOUR
19254 five
19255 "});
19256 cx.run_until_parked();
19257 cx.update_editor(|editor, window, cx| {
19258 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19259 });
19260 cx.run_until_parked();
19261 cx.assert_index_text(Some(indoc! {"
19262 one
19263 TWO
19264 THREE
19265 FOUR
19266 five
19267 "}));
19268 cx.set_state(indoc! { "
19269 one
19270 TWO
19271 ˇTHREE-HUNDRED
19272 FOUR
19273 five
19274 "});
19275 cx.run_until_parked();
19276 cx.update_editor(|editor, window, cx| {
19277 let snapshot = editor.snapshot(window, cx);
19278 let hunks = editor
19279 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19280 .collect::<Vec<_>>();
19281 assert_eq!(hunks.len(), 1);
19282 assert_eq!(
19283 hunks[0].status(),
19284 DiffHunkStatus {
19285 kind: DiffHunkStatusKind::Modified,
19286 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
19287 }
19288 );
19289
19290 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19291 });
19292 cx.run_until_parked();
19293 cx.assert_index_text(Some(indoc! {"
19294 one
19295 TWO
19296 THREE-HUNDRED
19297 FOUR
19298 five
19299 "}));
19300}
19301
19302#[gpui::test]
19303fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
19304 init_test(cx, |_| {});
19305
19306 let editor = cx.add_window(|window, cx| {
19307 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
19308 build_editor(buffer, window, cx)
19309 });
19310
19311 let render_args = Arc::new(Mutex::new(None));
19312 let snapshot = editor
19313 .update(cx, |editor, window, cx| {
19314 let snapshot = editor.buffer().read(cx).snapshot(cx);
19315 let range =
19316 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
19317
19318 struct RenderArgs {
19319 row: MultiBufferRow,
19320 folded: bool,
19321 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
19322 }
19323
19324 let crease = Crease::inline(
19325 range,
19326 FoldPlaceholder::test(),
19327 {
19328 let toggle_callback = render_args.clone();
19329 move |row, folded, callback, _window, _cx| {
19330 *toggle_callback.lock() = Some(RenderArgs {
19331 row,
19332 folded,
19333 callback,
19334 });
19335 div()
19336 }
19337 },
19338 |_row, _folded, _window, _cx| div(),
19339 );
19340
19341 editor.insert_creases(Some(crease), cx);
19342 let snapshot = editor.snapshot(window, cx);
19343 let _div = snapshot.render_crease_toggle(
19344 MultiBufferRow(1),
19345 false,
19346 cx.entity().clone(),
19347 window,
19348 cx,
19349 );
19350 snapshot
19351 })
19352 .unwrap();
19353
19354 let render_args = render_args.lock().take().unwrap();
19355 assert_eq!(render_args.row, MultiBufferRow(1));
19356 assert!(!render_args.folded);
19357 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19358
19359 cx.update_window(*editor, |_, window, cx| {
19360 (render_args.callback)(true, window, cx)
19361 })
19362 .unwrap();
19363 let snapshot = editor
19364 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19365 .unwrap();
19366 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
19367
19368 cx.update_window(*editor, |_, window, cx| {
19369 (render_args.callback)(false, window, cx)
19370 })
19371 .unwrap();
19372 let snapshot = editor
19373 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19374 .unwrap();
19375 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19376}
19377
19378#[gpui::test]
19379async fn test_input_text(cx: &mut TestAppContext) {
19380 init_test(cx, |_| {});
19381 let mut cx = EditorTestContext::new(cx).await;
19382
19383 cx.set_state(
19384 &r#"ˇone
19385 two
19386
19387 three
19388 fourˇ
19389 five
19390
19391 siˇx"#
19392 .unindent(),
19393 );
19394
19395 cx.dispatch_action(HandleInput(String::new()));
19396 cx.assert_editor_state(
19397 &r#"ˇone
19398 two
19399
19400 three
19401 fourˇ
19402 five
19403
19404 siˇx"#
19405 .unindent(),
19406 );
19407
19408 cx.dispatch_action(HandleInput("AAAA".to_string()));
19409 cx.assert_editor_state(
19410 &r#"AAAAˇone
19411 two
19412
19413 three
19414 fourAAAAˇ
19415 five
19416
19417 siAAAAˇx"#
19418 .unindent(),
19419 );
19420}
19421
19422#[gpui::test]
19423async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
19424 init_test(cx, |_| {});
19425
19426 let mut cx = EditorTestContext::new(cx).await;
19427 cx.set_state(
19428 r#"let foo = 1;
19429let foo = 2;
19430let foo = 3;
19431let fooˇ = 4;
19432let foo = 5;
19433let foo = 6;
19434let foo = 7;
19435let foo = 8;
19436let foo = 9;
19437let foo = 10;
19438let foo = 11;
19439let foo = 12;
19440let foo = 13;
19441let foo = 14;
19442let foo = 15;"#,
19443 );
19444
19445 cx.update_editor(|e, window, cx| {
19446 assert_eq!(
19447 e.next_scroll_position,
19448 NextScrollCursorCenterTopBottom::Center,
19449 "Default next scroll direction is center",
19450 );
19451
19452 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19453 assert_eq!(
19454 e.next_scroll_position,
19455 NextScrollCursorCenterTopBottom::Top,
19456 "After center, next scroll direction should be top",
19457 );
19458
19459 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19460 assert_eq!(
19461 e.next_scroll_position,
19462 NextScrollCursorCenterTopBottom::Bottom,
19463 "After top, next scroll direction should be bottom",
19464 );
19465
19466 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19467 assert_eq!(
19468 e.next_scroll_position,
19469 NextScrollCursorCenterTopBottom::Center,
19470 "After bottom, scrolling should start over",
19471 );
19472
19473 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19474 assert_eq!(
19475 e.next_scroll_position,
19476 NextScrollCursorCenterTopBottom::Top,
19477 "Scrolling continues if retriggered fast enough"
19478 );
19479 });
19480
19481 cx.executor()
19482 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
19483 cx.executor().run_until_parked();
19484 cx.update_editor(|e, _, _| {
19485 assert_eq!(
19486 e.next_scroll_position,
19487 NextScrollCursorCenterTopBottom::Center,
19488 "If scrolling is not triggered fast enough, it should reset"
19489 );
19490 });
19491}
19492
19493#[gpui::test]
19494async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
19495 init_test(cx, |_| {});
19496 let mut cx = EditorLspTestContext::new_rust(
19497 lsp::ServerCapabilities {
19498 definition_provider: Some(lsp::OneOf::Left(true)),
19499 references_provider: Some(lsp::OneOf::Left(true)),
19500 ..lsp::ServerCapabilities::default()
19501 },
19502 cx,
19503 )
19504 .await;
19505
19506 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
19507 let go_to_definition = cx
19508 .lsp
19509 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19510 move |params, _| async move {
19511 if empty_go_to_definition {
19512 Ok(None)
19513 } else {
19514 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
19515 uri: params.text_document_position_params.text_document.uri,
19516 range: lsp::Range::new(
19517 lsp::Position::new(4, 3),
19518 lsp::Position::new(4, 6),
19519 ),
19520 })))
19521 }
19522 },
19523 );
19524 let references = cx
19525 .lsp
19526 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
19527 Ok(Some(vec![lsp::Location {
19528 uri: params.text_document_position.text_document.uri,
19529 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
19530 }]))
19531 });
19532 (go_to_definition, references)
19533 };
19534
19535 cx.set_state(
19536 &r#"fn one() {
19537 let mut a = ˇtwo();
19538 }
19539
19540 fn two() {}"#
19541 .unindent(),
19542 );
19543 set_up_lsp_handlers(false, &mut cx);
19544 let navigated = cx
19545 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19546 .await
19547 .expect("Failed to navigate to definition");
19548 assert_eq!(
19549 navigated,
19550 Navigated::Yes,
19551 "Should have navigated to definition from the GetDefinition response"
19552 );
19553 cx.assert_editor_state(
19554 &r#"fn one() {
19555 let mut a = two();
19556 }
19557
19558 fn «twoˇ»() {}"#
19559 .unindent(),
19560 );
19561
19562 let editors = cx.update_workspace(|workspace, _, cx| {
19563 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19564 });
19565 cx.update_editor(|_, _, test_editor_cx| {
19566 assert_eq!(
19567 editors.len(),
19568 1,
19569 "Initially, only one, test, editor should be open in the workspace"
19570 );
19571 assert_eq!(
19572 test_editor_cx.entity(),
19573 editors.last().expect("Asserted len is 1").clone()
19574 );
19575 });
19576
19577 set_up_lsp_handlers(true, &mut cx);
19578 let navigated = cx
19579 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19580 .await
19581 .expect("Failed to navigate to lookup references");
19582 assert_eq!(
19583 navigated,
19584 Navigated::Yes,
19585 "Should have navigated to references as a fallback after empty GoToDefinition response"
19586 );
19587 // We should not change the selections in the existing file,
19588 // if opening another milti buffer with the references
19589 cx.assert_editor_state(
19590 &r#"fn one() {
19591 let mut a = two();
19592 }
19593
19594 fn «twoˇ»() {}"#
19595 .unindent(),
19596 );
19597 let editors = cx.update_workspace(|workspace, _, cx| {
19598 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19599 });
19600 cx.update_editor(|_, _, test_editor_cx| {
19601 assert_eq!(
19602 editors.len(),
19603 2,
19604 "After falling back to references search, we open a new editor with the results"
19605 );
19606 let references_fallback_text = editors
19607 .into_iter()
19608 .find(|new_editor| *new_editor != test_editor_cx.entity())
19609 .expect("Should have one non-test editor now")
19610 .read(test_editor_cx)
19611 .text(test_editor_cx);
19612 assert_eq!(
19613 references_fallback_text, "fn one() {\n let mut a = two();\n}",
19614 "Should use the range from the references response and not the GoToDefinition one"
19615 );
19616 });
19617}
19618
19619#[gpui::test]
19620async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
19621 init_test(cx, |_| {});
19622 cx.update(|cx| {
19623 let mut editor_settings = EditorSettings::get_global(cx).clone();
19624 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
19625 EditorSettings::override_global(editor_settings, cx);
19626 });
19627 let mut cx = EditorLspTestContext::new_rust(
19628 lsp::ServerCapabilities {
19629 definition_provider: Some(lsp::OneOf::Left(true)),
19630 references_provider: Some(lsp::OneOf::Left(true)),
19631 ..lsp::ServerCapabilities::default()
19632 },
19633 cx,
19634 )
19635 .await;
19636 let original_state = r#"fn one() {
19637 let mut a = ˇtwo();
19638 }
19639
19640 fn two() {}"#
19641 .unindent();
19642 cx.set_state(&original_state);
19643
19644 let mut go_to_definition = cx
19645 .lsp
19646 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19647 move |_, _| async move { Ok(None) },
19648 );
19649 let _references = cx
19650 .lsp
19651 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
19652 panic!("Should not call for references with no go to definition fallback")
19653 });
19654
19655 let navigated = cx
19656 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19657 .await
19658 .expect("Failed to navigate to lookup references");
19659 go_to_definition
19660 .next()
19661 .await
19662 .expect("Should have called the go_to_definition handler");
19663
19664 assert_eq!(
19665 navigated,
19666 Navigated::No,
19667 "Should have navigated to references as a fallback after empty GoToDefinition response"
19668 );
19669 cx.assert_editor_state(&original_state);
19670 let editors = cx.update_workspace(|workspace, _, cx| {
19671 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19672 });
19673 cx.update_editor(|_, _, _| {
19674 assert_eq!(
19675 editors.len(),
19676 1,
19677 "After unsuccessful fallback, no other editor should have been opened"
19678 );
19679 });
19680}
19681
19682#[gpui::test]
19683async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
19684 init_test(cx, |_| {});
19685
19686 let language = Arc::new(Language::new(
19687 LanguageConfig::default(),
19688 Some(tree_sitter_rust::LANGUAGE.into()),
19689 ));
19690
19691 let text = r#"
19692 #[cfg(test)]
19693 mod tests() {
19694 #[test]
19695 fn runnable_1() {
19696 let a = 1;
19697 }
19698
19699 #[test]
19700 fn runnable_2() {
19701 let a = 1;
19702 let b = 2;
19703 }
19704 }
19705 "#
19706 .unindent();
19707
19708 let fs = FakeFs::new(cx.executor());
19709 fs.insert_file("/file.rs", Default::default()).await;
19710
19711 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19712 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19713 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19714 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
19715 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
19716
19717 let editor = cx.new_window_entity(|window, cx| {
19718 Editor::new(
19719 EditorMode::full(),
19720 multi_buffer,
19721 Some(project.clone()),
19722 window,
19723 cx,
19724 )
19725 });
19726
19727 editor.update_in(cx, |editor, window, cx| {
19728 let snapshot = editor.buffer().read(cx).snapshot(cx);
19729 editor.tasks.insert(
19730 (buffer.read(cx).remote_id(), 3),
19731 RunnableTasks {
19732 templates: vec![],
19733 offset: snapshot.anchor_before(43),
19734 column: 0,
19735 extra_variables: HashMap::default(),
19736 context_range: BufferOffset(43)..BufferOffset(85),
19737 },
19738 );
19739 editor.tasks.insert(
19740 (buffer.read(cx).remote_id(), 8),
19741 RunnableTasks {
19742 templates: vec![],
19743 offset: snapshot.anchor_before(86),
19744 column: 0,
19745 extra_variables: HashMap::default(),
19746 context_range: BufferOffset(86)..BufferOffset(191),
19747 },
19748 );
19749
19750 // Test finding task when cursor is inside function body
19751 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19752 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
19753 });
19754 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19755 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
19756
19757 // Test finding task when cursor is on function name
19758 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19759 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
19760 });
19761 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19762 assert_eq!(row, 8, "Should find task when cursor is on function name");
19763 });
19764}
19765
19766#[gpui::test]
19767async fn test_folding_buffers(cx: &mut TestAppContext) {
19768 init_test(cx, |_| {});
19769
19770 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19771 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
19772 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
19773
19774 let fs = FakeFs::new(cx.executor());
19775 fs.insert_tree(
19776 path!("/a"),
19777 json!({
19778 "first.rs": sample_text_1,
19779 "second.rs": sample_text_2,
19780 "third.rs": sample_text_3,
19781 }),
19782 )
19783 .await;
19784 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19785 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19786 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19787 let worktree = project.update(cx, |project, cx| {
19788 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19789 assert_eq!(worktrees.len(), 1);
19790 worktrees.pop().unwrap()
19791 });
19792 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19793
19794 let buffer_1 = project
19795 .update(cx, |project, cx| {
19796 project.open_buffer((worktree_id, "first.rs"), cx)
19797 })
19798 .await
19799 .unwrap();
19800 let buffer_2 = project
19801 .update(cx, |project, cx| {
19802 project.open_buffer((worktree_id, "second.rs"), cx)
19803 })
19804 .await
19805 .unwrap();
19806 let buffer_3 = project
19807 .update(cx, |project, cx| {
19808 project.open_buffer((worktree_id, "third.rs"), cx)
19809 })
19810 .await
19811 .unwrap();
19812
19813 let multi_buffer = cx.new(|cx| {
19814 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19815 multi_buffer.push_excerpts(
19816 buffer_1.clone(),
19817 [
19818 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19819 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19820 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19821 ],
19822 cx,
19823 );
19824 multi_buffer.push_excerpts(
19825 buffer_2.clone(),
19826 [
19827 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19828 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19829 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19830 ],
19831 cx,
19832 );
19833 multi_buffer.push_excerpts(
19834 buffer_3.clone(),
19835 [
19836 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19837 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19838 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19839 ],
19840 cx,
19841 );
19842 multi_buffer
19843 });
19844 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19845 Editor::new(
19846 EditorMode::full(),
19847 multi_buffer.clone(),
19848 Some(project.clone()),
19849 window,
19850 cx,
19851 )
19852 });
19853
19854 assert_eq!(
19855 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19856 "\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",
19857 );
19858
19859 multi_buffer_editor.update(cx, |editor, cx| {
19860 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19861 });
19862 assert_eq!(
19863 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19864 "\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",
19865 "After folding the first buffer, its text should not be displayed"
19866 );
19867
19868 multi_buffer_editor.update(cx, |editor, cx| {
19869 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19870 });
19871 assert_eq!(
19872 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19873 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
19874 "After folding the second buffer, its text should not be displayed"
19875 );
19876
19877 multi_buffer_editor.update(cx, |editor, cx| {
19878 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19879 });
19880 assert_eq!(
19881 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19882 "\n\n\n\n\n",
19883 "After folding the third buffer, its text should not be displayed"
19884 );
19885
19886 // Emulate selection inside the fold logic, that should work
19887 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19888 editor
19889 .snapshot(window, cx)
19890 .next_line_boundary(Point::new(0, 4));
19891 });
19892
19893 multi_buffer_editor.update(cx, |editor, cx| {
19894 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19895 });
19896 assert_eq!(
19897 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19898 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19899 "After unfolding the second buffer, its text should be displayed"
19900 );
19901
19902 // Typing inside of buffer 1 causes that buffer to be unfolded.
19903 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19904 assert_eq!(
19905 multi_buffer
19906 .read(cx)
19907 .snapshot(cx)
19908 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
19909 .collect::<String>(),
19910 "bbbb"
19911 );
19912 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
19913 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
19914 });
19915 editor.handle_input("B", window, cx);
19916 });
19917
19918 assert_eq!(
19919 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19920 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19921 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
19922 );
19923
19924 multi_buffer_editor.update(cx, |editor, cx| {
19925 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19926 });
19927 assert_eq!(
19928 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19929 "\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",
19930 "After unfolding the all buffers, all original text should be displayed"
19931 );
19932}
19933
19934#[gpui::test]
19935async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
19936 init_test(cx, |_| {});
19937
19938 let sample_text_1 = "1111\n2222\n3333".to_string();
19939 let sample_text_2 = "4444\n5555\n6666".to_string();
19940 let sample_text_3 = "7777\n8888\n9999".to_string();
19941
19942 let fs = FakeFs::new(cx.executor());
19943 fs.insert_tree(
19944 path!("/a"),
19945 json!({
19946 "first.rs": sample_text_1,
19947 "second.rs": sample_text_2,
19948 "third.rs": sample_text_3,
19949 }),
19950 )
19951 .await;
19952 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19953 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19954 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19955 let worktree = project.update(cx, |project, cx| {
19956 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19957 assert_eq!(worktrees.len(), 1);
19958 worktrees.pop().unwrap()
19959 });
19960 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19961
19962 let buffer_1 = project
19963 .update(cx, |project, cx| {
19964 project.open_buffer((worktree_id, "first.rs"), cx)
19965 })
19966 .await
19967 .unwrap();
19968 let buffer_2 = project
19969 .update(cx, |project, cx| {
19970 project.open_buffer((worktree_id, "second.rs"), cx)
19971 })
19972 .await
19973 .unwrap();
19974 let buffer_3 = project
19975 .update(cx, |project, cx| {
19976 project.open_buffer((worktree_id, "third.rs"), cx)
19977 })
19978 .await
19979 .unwrap();
19980
19981 let multi_buffer = cx.new(|cx| {
19982 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19983 multi_buffer.push_excerpts(
19984 buffer_1.clone(),
19985 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19986 cx,
19987 );
19988 multi_buffer.push_excerpts(
19989 buffer_2.clone(),
19990 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19991 cx,
19992 );
19993 multi_buffer.push_excerpts(
19994 buffer_3.clone(),
19995 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19996 cx,
19997 );
19998 multi_buffer
19999 });
20000
20001 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20002 Editor::new(
20003 EditorMode::full(),
20004 multi_buffer,
20005 Some(project.clone()),
20006 window,
20007 cx,
20008 )
20009 });
20010
20011 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
20012 assert_eq!(
20013 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20014 full_text,
20015 );
20016
20017 multi_buffer_editor.update(cx, |editor, cx| {
20018 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
20019 });
20020 assert_eq!(
20021 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20022 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
20023 "After folding the first buffer, its text should not be displayed"
20024 );
20025
20026 multi_buffer_editor.update(cx, |editor, cx| {
20027 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
20028 });
20029
20030 assert_eq!(
20031 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20032 "\n\n\n\n\n\n7777\n8888\n9999",
20033 "After folding the second buffer, its text should not be displayed"
20034 );
20035
20036 multi_buffer_editor.update(cx, |editor, cx| {
20037 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
20038 });
20039 assert_eq!(
20040 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20041 "\n\n\n\n\n",
20042 "After folding the third buffer, its text should not be displayed"
20043 );
20044
20045 multi_buffer_editor.update(cx, |editor, cx| {
20046 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
20047 });
20048 assert_eq!(
20049 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20050 "\n\n\n\n4444\n5555\n6666\n\n",
20051 "After unfolding the second buffer, its text should be displayed"
20052 );
20053
20054 multi_buffer_editor.update(cx, |editor, cx| {
20055 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
20056 });
20057 assert_eq!(
20058 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20059 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
20060 "After unfolding the first buffer, its text should be displayed"
20061 );
20062
20063 multi_buffer_editor.update(cx, |editor, cx| {
20064 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
20065 });
20066 assert_eq!(
20067 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20068 full_text,
20069 "After unfolding all buffers, all original text should be displayed"
20070 );
20071}
20072
20073#[gpui::test]
20074async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
20075 init_test(cx, |_| {});
20076
20077 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
20078
20079 let fs = FakeFs::new(cx.executor());
20080 fs.insert_tree(
20081 path!("/a"),
20082 json!({
20083 "main.rs": sample_text,
20084 }),
20085 )
20086 .await;
20087 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20088 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20089 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20090 let worktree = project.update(cx, |project, cx| {
20091 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20092 assert_eq!(worktrees.len(), 1);
20093 worktrees.pop().unwrap()
20094 });
20095 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20096
20097 let buffer_1 = project
20098 .update(cx, |project, cx| {
20099 project.open_buffer((worktree_id, "main.rs"), cx)
20100 })
20101 .await
20102 .unwrap();
20103
20104 let multi_buffer = cx.new(|cx| {
20105 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20106 multi_buffer.push_excerpts(
20107 buffer_1.clone(),
20108 [ExcerptRange::new(
20109 Point::new(0, 0)
20110 ..Point::new(
20111 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
20112 0,
20113 ),
20114 )],
20115 cx,
20116 );
20117 multi_buffer
20118 });
20119 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20120 Editor::new(
20121 EditorMode::full(),
20122 multi_buffer,
20123 Some(project.clone()),
20124 window,
20125 cx,
20126 )
20127 });
20128
20129 let selection_range = Point::new(1, 0)..Point::new(2, 0);
20130 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20131 enum TestHighlight {}
20132 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
20133 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
20134 editor.highlight_text::<TestHighlight>(
20135 vec![highlight_range.clone()],
20136 HighlightStyle::color(Hsla::green()),
20137 cx,
20138 );
20139 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20140 s.select_ranges(Some(highlight_range))
20141 });
20142 });
20143
20144 let full_text = format!("\n\n{sample_text}");
20145 assert_eq!(
20146 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20147 full_text,
20148 );
20149}
20150
20151#[gpui::test]
20152async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
20153 init_test(cx, |_| {});
20154 cx.update(|cx| {
20155 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
20156 "keymaps/default-linux.json",
20157 cx,
20158 )
20159 .unwrap();
20160 cx.bind_keys(default_key_bindings);
20161 });
20162
20163 let (editor, cx) = cx.add_window_view(|window, cx| {
20164 let multi_buffer = MultiBuffer::build_multi(
20165 [
20166 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
20167 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
20168 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
20169 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
20170 ],
20171 cx,
20172 );
20173 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
20174
20175 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
20176 // fold all but the second buffer, so that we test navigating between two
20177 // adjacent folded buffers, as well as folded buffers at the start and
20178 // end the multibuffer
20179 editor.fold_buffer(buffer_ids[0], cx);
20180 editor.fold_buffer(buffer_ids[2], cx);
20181 editor.fold_buffer(buffer_ids[3], cx);
20182
20183 editor
20184 });
20185 cx.simulate_resize(size(px(1000.), px(1000.)));
20186
20187 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
20188 cx.assert_excerpts_with_selections(indoc! {"
20189 [EXCERPT]
20190 ˇ[FOLDED]
20191 [EXCERPT]
20192 a1
20193 b1
20194 [EXCERPT]
20195 [FOLDED]
20196 [EXCERPT]
20197 [FOLDED]
20198 "
20199 });
20200 cx.simulate_keystroke("down");
20201 cx.assert_excerpts_with_selections(indoc! {"
20202 [EXCERPT]
20203 [FOLDED]
20204 [EXCERPT]
20205 ˇa1
20206 b1
20207 [EXCERPT]
20208 [FOLDED]
20209 [EXCERPT]
20210 [FOLDED]
20211 "
20212 });
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 cx.simulate_keystroke("down");
20227 cx.assert_excerpts_with_selections(indoc! {"
20228 [EXCERPT]
20229 [FOLDED]
20230 [EXCERPT]
20231 a1
20232 b1
20233 ˇ[EXCERPT]
20234 [FOLDED]
20235 [EXCERPT]
20236 [FOLDED]
20237 "
20238 });
20239 cx.simulate_keystroke("down");
20240 cx.assert_excerpts_with_selections(indoc! {"
20241 [EXCERPT]
20242 [FOLDED]
20243 [EXCERPT]
20244 a1
20245 b1
20246 [EXCERPT]
20247 ˇ[FOLDED]
20248 [EXCERPT]
20249 [FOLDED]
20250 "
20251 });
20252 for _ in 0..5 {
20253 cx.simulate_keystroke("down");
20254 cx.assert_excerpts_with_selections(indoc! {"
20255 [EXCERPT]
20256 [FOLDED]
20257 [EXCERPT]
20258 a1
20259 b1
20260 [EXCERPT]
20261 [FOLDED]
20262 [EXCERPT]
20263 ˇ[FOLDED]
20264 "
20265 });
20266 }
20267
20268 cx.simulate_keystroke("up");
20269 cx.assert_excerpts_with_selections(indoc! {"
20270 [EXCERPT]
20271 [FOLDED]
20272 [EXCERPT]
20273 a1
20274 b1
20275 [EXCERPT]
20276 ˇ[FOLDED]
20277 [EXCERPT]
20278 [FOLDED]
20279 "
20280 });
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 cx.simulate_keystroke("up");
20295 cx.assert_excerpts_with_selections(indoc! {"
20296 [EXCERPT]
20297 [FOLDED]
20298 [EXCERPT]
20299 a1
20300 ˇb1
20301 [EXCERPT]
20302 [FOLDED]
20303 [EXCERPT]
20304 [FOLDED]
20305 "
20306 });
20307 cx.simulate_keystroke("up");
20308 cx.assert_excerpts_with_selections(indoc! {"
20309 [EXCERPT]
20310 [FOLDED]
20311 [EXCERPT]
20312 ˇa1
20313 b1
20314 [EXCERPT]
20315 [FOLDED]
20316 [EXCERPT]
20317 [FOLDED]
20318 "
20319 });
20320 for _ in 0..5 {
20321 cx.simulate_keystroke("up");
20322 cx.assert_excerpts_with_selections(indoc! {"
20323 [EXCERPT]
20324 ˇ[FOLDED]
20325 [EXCERPT]
20326 a1
20327 b1
20328 [EXCERPT]
20329 [FOLDED]
20330 [EXCERPT]
20331 [FOLDED]
20332 "
20333 });
20334 }
20335}
20336
20337#[gpui::test]
20338async fn test_inline_completion_text(cx: &mut TestAppContext) {
20339 init_test(cx, |_| {});
20340
20341 // Simple insertion
20342 assert_highlighted_edits(
20343 "Hello, world!",
20344 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
20345 true,
20346 cx,
20347 |highlighted_edits, cx| {
20348 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
20349 assert_eq!(highlighted_edits.highlights.len(), 1);
20350 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
20351 assert_eq!(
20352 highlighted_edits.highlights[0].1.background_color,
20353 Some(cx.theme().status().created_background)
20354 );
20355 },
20356 )
20357 .await;
20358
20359 // Replacement
20360 assert_highlighted_edits(
20361 "This is a test.",
20362 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
20363 false,
20364 cx,
20365 |highlighted_edits, cx| {
20366 assert_eq!(highlighted_edits.text, "That is a test.");
20367 assert_eq!(highlighted_edits.highlights.len(), 1);
20368 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
20369 assert_eq!(
20370 highlighted_edits.highlights[0].1.background_color,
20371 Some(cx.theme().status().created_background)
20372 );
20373 },
20374 )
20375 .await;
20376
20377 // Multiple edits
20378 assert_highlighted_edits(
20379 "Hello, world!",
20380 vec![
20381 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
20382 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
20383 ],
20384 false,
20385 cx,
20386 |highlighted_edits, cx| {
20387 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
20388 assert_eq!(highlighted_edits.highlights.len(), 2);
20389 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
20390 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
20391 assert_eq!(
20392 highlighted_edits.highlights[0].1.background_color,
20393 Some(cx.theme().status().created_background)
20394 );
20395 assert_eq!(
20396 highlighted_edits.highlights[1].1.background_color,
20397 Some(cx.theme().status().created_background)
20398 );
20399 },
20400 )
20401 .await;
20402
20403 // Multiple lines with edits
20404 assert_highlighted_edits(
20405 "First line\nSecond line\nThird line\nFourth line",
20406 vec![
20407 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
20408 (
20409 Point::new(2, 0)..Point::new(2, 10),
20410 "New third line".to_string(),
20411 ),
20412 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
20413 ],
20414 false,
20415 cx,
20416 |highlighted_edits, cx| {
20417 assert_eq!(
20418 highlighted_edits.text,
20419 "Second modified\nNew third line\nFourth updated line"
20420 );
20421 assert_eq!(highlighted_edits.highlights.len(), 3);
20422 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
20423 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
20424 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
20425 for highlight in &highlighted_edits.highlights {
20426 assert_eq!(
20427 highlight.1.background_color,
20428 Some(cx.theme().status().created_background)
20429 );
20430 }
20431 },
20432 )
20433 .await;
20434}
20435
20436#[gpui::test]
20437async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
20438 init_test(cx, |_| {});
20439
20440 // Deletion
20441 assert_highlighted_edits(
20442 "Hello, world!",
20443 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
20444 true,
20445 cx,
20446 |highlighted_edits, cx| {
20447 assert_eq!(highlighted_edits.text, "Hello, world!");
20448 assert_eq!(highlighted_edits.highlights.len(), 1);
20449 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
20450 assert_eq!(
20451 highlighted_edits.highlights[0].1.background_color,
20452 Some(cx.theme().status().deleted_background)
20453 );
20454 },
20455 )
20456 .await;
20457
20458 // Insertion
20459 assert_highlighted_edits(
20460 "Hello, world!",
20461 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
20462 true,
20463 cx,
20464 |highlighted_edits, cx| {
20465 assert_eq!(highlighted_edits.highlights.len(), 1);
20466 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
20467 assert_eq!(
20468 highlighted_edits.highlights[0].1.background_color,
20469 Some(cx.theme().status().created_background)
20470 );
20471 },
20472 )
20473 .await;
20474}
20475
20476async fn assert_highlighted_edits(
20477 text: &str,
20478 edits: Vec<(Range<Point>, String)>,
20479 include_deletions: bool,
20480 cx: &mut TestAppContext,
20481 assertion_fn: impl Fn(HighlightedText, &App),
20482) {
20483 let window = cx.add_window(|window, cx| {
20484 let buffer = MultiBuffer::build_simple(text, cx);
20485 Editor::new(EditorMode::full(), buffer, None, window, cx)
20486 });
20487 let cx = &mut VisualTestContext::from_window(*window, cx);
20488
20489 let (buffer, snapshot) = window
20490 .update(cx, |editor, _window, cx| {
20491 (
20492 editor.buffer().clone(),
20493 editor.buffer().read(cx).snapshot(cx),
20494 )
20495 })
20496 .unwrap();
20497
20498 let edits = edits
20499 .into_iter()
20500 .map(|(range, edit)| {
20501 (
20502 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
20503 edit,
20504 )
20505 })
20506 .collect::<Vec<_>>();
20507
20508 let text_anchor_edits = edits
20509 .clone()
20510 .into_iter()
20511 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
20512 .collect::<Vec<_>>();
20513
20514 let edit_preview = window
20515 .update(cx, |_, _window, cx| {
20516 buffer
20517 .read(cx)
20518 .as_singleton()
20519 .unwrap()
20520 .read(cx)
20521 .preview_edits(text_anchor_edits.into(), cx)
20522 })
20523 .unwrap()
20524 .await;
20525
20526 cx.update(|_window, cx| {
20527 let highlighted_edits = inline_completion_edit_text(
20528 &snapshot.as_singleton().unwrap().2,
20529 &edits,
20530 &edit_preview,
20531 include_deletions,
20532 cx,
20533 );
20534 assertion_fn(highlighted_edits, cx)
20535 });
20536}
20537
20538#[track_caller]
20539fn assert_breakpoint(
20540 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
20541 path: &Arc<Path>,
20542 expected: Vec<(u32, Breakpoint)>,
20543) {
20544 if expected.len() == 0usize {
20545 assert!(!breakpoints.contains_key(path), "{}", path.display());
20546 } else {
20547 let mut breakpoint = breakpoints
20548 .get(path)
20549 .unwrap()
20550 .into_iter()
20551 .map(|breakpoint| {
20552 (
20553 breakpoint.row,
20554 Breakpoint {
20555 message: breakpoint.message.clone(),
20556 state: breakpoint.state,
20557 condition: breakpoint.condition.clone(),
20558 hit_condition: breakpoint.hit_condition.clone(),
20559 },
20560 )
20561 })
20562 .collect::<Vec<_>>();
20563
20564 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
20565
20566 assert_eq!(expected, breakpoint);
20567 }
20568}
20569
20570fn add_log_breakpoint_at_cursor(
20571 editor: &mut Editor,
20572 log_message: &str,
20573 window: &mut Window,
20574 cx: &mut Context<Editor>,
20575) {
20576 let (anchor, bp) = editor
20577 .breakpoints_at_cursors(window, cx)
20578 .first()
20579 .and_then(|(anchor, bp)| {
20580 if let Some(bp) = bp {
20581 Some((*anchor, bp.clone()))
20582 } else {
20583 None
20584 }
20585 })
20586 .unwrap_or_else(|| {
20587 let cursor_position: Point = editor.selections.newest(cx).head();
20588
20589 let breakpoint_position = editor
20590 .snapshot(window, cx)
20591 .display_snapshot
20592 .buffer_snapshot
20593 .anchor_before(Point::new(cursor_position.row, 0));
20594
20595 (breakpoint_position, Breakpoint::new_log(&log_message))
20596 });
20597
20598 editor.edit_breakpoint_at_anchor(
20599 anchor,
20600 bp,
20601 BreakpointEditAction::EditLogMessage(log_message.into()),
20602 cx,
20603 );
20604}
20605
20606#[gpui::test]
20607async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
20608 init_test(cx, |_| {});
20609
20610 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20611 let fs = FakeFs::new(cx.executor());
20612 fs.insert_tree(
20613 path!("/a"),
20614 json!({
20615 "main.rs": sample_text,
20616 }),
20617 )
20618 .await;
20619 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20620 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20621 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20622
20623 let fs = FakeFs::new(cx.executor());
20624 fs.insert_tree(
20625 path!("/a"),
20626 json!({
20627 "main.rs": sample_text,
20628 }),
20629 )
20630 .await;
20631 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20632 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20633 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20634 let worktree_id = workspace
20635 .update(cx, |workspace, _window, cx| {
20636 workspace.project().update(cx, |project, cx| {
20637 project.worktrees(cx).next().unwrap().read(cx).id()
20638 })
20639 })
20640 .unwrap();
20641
20642 let buffer = project
20643 .update(cx, |project, cx| {
20644 project.open_buffer((worktree_id, "main.rs"), cx)
20645 })
20646 .await
20647 .unwrap();
20648
20649 let (editor, cx) = cx.add_window_view(|window, cx| {
20650 Editor::new(
20651 EditorMode::full(),
20652 MultiBuffer::build_from_buffer(buffer, cx),
20653 Some(project.clone()),
20654 window,
20655 cx,
20656 )
20657 });
20658
20659 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20660 let abs_path = project.read_with(cx, |project, cx| {
20661 project
20662 .absolute_path(&project_path, cx)
20663 .map(|path_buf| Arc::from(path_buf.to_owned()))
20664 .unwrap()
20665 });
20666
20667 // assert we can add breakpoint on the first line
20668 editor.update_in(cx, |editor, window, cx| {
20669 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20670 editor.move_to_end(&MoveToEnd, window, cx);
20671 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20672 });
20673
20674 let breakpoints = editor.update(cx, |editor, cx| {
20675 editor
20676 .breakpoint_store()
20677 .as_ref()
20678 .unwrap()
20679 .read(cx)
20680 .all_source_breakpoints(cx)
20681 .clone()
20682 });
20683
20684 assert_eq!(1, breakpoints.len());
20685 assert_breakpoint(
20686 &breakpoints,
20687 &abs_path,
20688 vec![
20689 (0, Breakpoint::new_standard()),
20690 (3, Breakpoint::new_standard()),
20691 ],
20692 );
20693
20694 editor.update_in(cx, |editor, window, cx| {
20695 editor.move_to_beginning(&MoveToBeginning, window, cx);
20696 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20697 });
20698
20699 let breakpoints = editor.update(cx, |editor, cx| {
20700 editor
20701 .breakpoint_store()
20702 .as_ref()
20703 .unwrap()
20704 .read(cx)
20705 .all_source_breakpoints(cx)
20706 .clone()
20707 });
20708
20709 assert_eq!(1, breakpoints.len());
20710 assert_breakpoint(
20711 &breakpoints,
20712 &abs_path,
20713 vec![(3, Breakpoint::new_standard())],
20714 );
20715
20716 editor.update_in(cx, |editor, window, cx| {
20717 editor.move_to_end(&MoveToEnd, window, cx);
20718 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20719 });
20720
20721 let breakpoints = editor.update(cx, |editor, cx| {
20722 editor
20723 .breakpoint_store()
20724 .as_ref()
20725 .unwrap()
20726 .read(cx)
20727 .all_source_breakpoints(cx)
20728 .clone()
20729 });
20730
20731 assert_eq!(0, breakpoints.len());
20732 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20733}
20734
20735#[gpui::test]
20736async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
20737 init_test(cx, |_| {});
20738
20739 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20740
20741 let fs = FakeFs::new(cx.executor());
20742 fs.insert_tree(
20743 path!("/a"),
20744 json!({
20745 "main.rs": sample_text,
20746 }),
20747 )
20748 .await;
20749 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20750 let (workspace, cx) =
20751 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20752
20753 let worktree_id = workspace.update(cx, |workspace, cx| {
20754 workspace.project().update(cx, |project, cx| {
20755 project.worktrees(cx).next().unwrap().read(cx).id()
20756 })
20757 });
20758
20759 let buffer = project
20760 .update(cx, |project, cx| {
20761 project.open_buffer((worktree_id, "main.rs"), cx)
20762 })
20763 .await
20764 .unwrap();
20765
20766 let (editor, cx) = cx.add_window_view(|window, cx| {
20767 Editor::new(
20768 EditorMode::full(),
20769 MultiBuffer::build_from_buffer(buffer, cx),
20770 Some(project.clone()),
20771 window,
20772 cx,
20773 )
20774 });
20775
20776 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20777 let abs_path = project.read_with(cx, |project, cx| {
20778 project
20779 .absolute_path(&project_path, cx)
20780 .map(|path_buf| Arc::from(path_buf.to_owned()))
20781 .unwrap()
20782 });
20783
20784 editor.update_in(cx, |editor, window, cx| {
20785 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20786 });
20787
20788 let breakpoints = editor.update(cx, |editor, cx| {
20789 editor
20790 .breakpoint_store()
20791 .as_ref()
20792 .unwrap()
20793 .read(cx)
20794 .all_source_breakpoints(cx)
20795 .clone()
20796 });
20797
20798 assert_breakpoint(
20799 &breakpoints,
20800 &abs_path,
20801 vec![(0, Breakpoint::new_log("hello world"))],
20802 );
20803
20804 // Removing a log message from a log breakpoint should remove it
20805 editor.update_in(cx, |editor, window, cx| {
20806 add_log_breakpoint_at_cursor(editor, "", window, cx);
20807 });
20808
20809 let breakpoints = editor.update(cx, |editor, cx| {
20810 editor
20811 .breakpoint_store()
20812 .as_ref()
20813 .unwrap()
20814 .read(cx)
20815 .all_source_breakpoints(cx)
20816 .clone()
20817 });
20818
20819 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20820
20821 editor.update_in(cx, |editor, window, cx| {
20822 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20823 editor.move_to_end(&MoveToEnd, window, cx);
20824 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20825 // Not adding a log message to a standard breakpoint shouldn't remove it
20826 add_log_breakpoint_at_cursor(editor, "", window, cx);
20827 });
20828
20829 let breakpoints = editor.update(cx, |editor, cx| {
20830 editor
20831 .breakpoint_store()
20832 .as_ref()
20833 .unwrap()
20834 .read(cx)
20835 .all_source_breakpoints(cx)
20836 .clone()
20837 });
20838
20839 assert_breakpoint(
20840 &breakpoints,
20841 &abs_path,
20842 vec![
20843 (0, Breakpoint::new_standard()),
20844 (3, Breakpoint::new_standard()),
20845 ],
20846 );
20847
20848 editor.update_in(cx, |editor, window, cx| {
20849 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20850 });
20851
20852 let breakpoints = editor.update(cx, |editor, cx| {
20853 editor
20854 .breakpoint_store()
20855 .as_ref()
20856 .unwrap()
20857 .read(cx)
20858 .all_source_breakpoints(cx)
20859 .clone()
20860 });
20861
20862 assert_breakpoint(
20863 &breakpoints,
20864 &abs_path,
20865 vec![
20866 (0, Breakpoint::new_standard()),
20867 (3, Breakpoint::new_log("hello world")),
20868 ],
20869 );
20870
20871 editor.update_in(cx, |editor, window, cx| {
20872 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
20873 });
20874
20875 let breakpoints = editor.update(cx, |editor, cx| {
20876 editor
20877 .breakpoint_store()
20878 .as_ref()
20879 .unwrap()
20880 .read(cx)
20881 .all_source_breakpoints(cx)
20882 .clone()
20883 });
20884
20885 assert_breakpoint(
20886 &breakpoints,
20887 &abs_path,
20888 vec![
20889 (0, Breakpoint::new_standard()),
20890 (3, Breakpoint::new_log("hello Earth!!")),
20891 ],
20892 );
20893}
20894
20895/// This also tests that Editor::breakpoint_at_cursor_head is working properly
20896/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
20897/// or when breakpoints were placed out of order. This tests for a regression too
20898#[gpui::test]
20899async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
20900 init_test(cx, |_| {});
20901
20902 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20903 let fs = FakeFs::new(cx.executor());
20904 fs.insert_tree(
20905 path!("/a"),
20906 json!({
20907 "main.rs": sample_text,
20908 }),
20909 )
20910 .await;
20911 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20912 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20913 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20914
20915 let fs = FakeFs::new(cx.executor());
20916 fs.insert_tree(
20917 path!("/a"),
20918 json!({
20919 "main.rs": sample_text,
20920 }),
20921 )
20922 .await;
20923 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20924 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20925 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20926 let worktree_id = workspace
20927 .update(cx, |workspace, _window, cx| {
20928 workspace.project().update(cx, |project, cx| {
20929 project.worktrees(cx).next().unwrap().read(cx).id()
20930 })
20931 })
20932 .unwrap();
20933
20934 let buffer = project
20935 .update(cx, |project, cx| {
20936 project.open_buffer((worktree_id, "main.rs"), cx)
20937 })
20938 .await
20939 .unwrap();
20940
20941 let (editor, cx) = cx.add_window_view(|window, cx| {
20942 Editor::new(
20943 EditorMode::full(),
20944 MultiBuffer::build_from_buffer(buffer, cx),
20945 Some(project.clone()),
20946 window,
20947 cx,
20948 )
20949 });
20950
20951 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20952 let abs_path = project.read_with(cx, |project, cx| {
20953 project
20954 .absolute_path(&project_path, cx)
20955 .map(|path_buf| Arc::from(path_buf.to_owned()))
20956 .unwrap()
20957 });
20958
20959 // assert we can add breakpoint on the first line
20960 editor.update_in(cx, |editor, window, cx| {
20961 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20962 editor.move_to_end(&MoveToEnd, window, cx);
20963 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20964 editor.move_up(&MoveUp, window, cx);
20965 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20966 });
20967
20968 let breakpoints = editor.update(cx, |editor, cx| {
20969 editor
20970 .breakpoint_store()
20971 .as_ref()
20972 .unwrap()
20973 .read(cx)
20974 .all_source_breakpoints(cx)
20975 .clone()
20976 });
20977
20978 assert_eq!(1, breakpoints.len());
20979 assert_breakpoint(
20980 &breakpoints,
20981 &abs_path,
20982 vec![
20983 (0, Breakpoint::new_standard()),
20984 (2, Breakpoint::new_standard()),
20985 (3, Breakpoint::new_standard()),
20986 ],
20987 );
20988
20989 editor.update_in(cx, |editor, window, cx| {
20990 editor.move_to_beginning(&MoveToBeginning, window, cx);
20991 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20992 editor.move_to_end(&MoveToEnd, window, cx);
20993 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20994 // Disabling a breakpoint that doesn't exist should do nothing
20995 editor.move_up(&MoveUp, window, cx);
20996 editor.move_up(&MoveUp, window, cx);
20997 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20998 });
20999
21000 let breakpoints = editor.update(cx, |editor, cx| {
21001 editor
21002 .breakpoint_store()
21003 .as_ref()
21004 .unwrap()
21005 .read(cx)
21006 .all_source_breakpoints(cx)
21007 .clone()
21008 });
21009
21010 let disable_breakpoint = {
21011 let mut bp = Breakpoint::new_standard();
21012 bp.state = BreakpointState::Disabled;
21013 bp
21014 };
21015
21016 assert_eq!(1, breakpoints.len());
21017 assert_breakpoint(
21018 &breakpoints,
21019 &abs_path,
21020 vec![
21021 (0, disable_breakpoint.clone()),
21022 (2, Breakpoint::new_standard()),
21023 (3, disable_breakpoint.clone()),
21024 ],
21025 );
21026
21027 editor.update_in(cx, |editor, window, cx| {
21028 editor.move_to_beginning(&MoveToBeginning, window, cx);
21029 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
21030 editor.move_to_end(&MoveToEnd, window, cx);
21031 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
21032 editor.move_up(&MoveUp, window, cx);
21033 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21034 });
21035
21036 let breakpoints = editor.update(cx, |editor, cx| {
21037 editor
21038 .breakpoint_store()
21039 .as_ref()
21040 .unwrap()
21041 .read(cx)
21042 .all_source_breakpoints(cx)
21043 .clone()
21044 });
21045
21046 assert_eq!(1, breakpoints.len());
21047 assert_breakpoint(
21048 &breakpoints,
21049 &abs_path,
21050 vec![
21051 (0, Breakpoint::new_standard()),
21052 (2, disable_breakpoint),
21053 (3, Breakpoint::new_standard()),
21054 ],
21055 );
21056}
21057
21058#[gpui::test]
21059async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
21060 init_test(cx, |_| {});
21061 let capabilities = lsp::ServerCapabilities {
21062 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
21063 prepare_provider: Some(true),
21064 work_done_progress_options: Default::default(),
21065 })),
21066 ..Default::default()
21067 };
21068 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21069
21070 cx.set_state(indoc! {"
21071 struct Fˇoo {}
21072 "});
21073
21074 cx.update_editor(|editor, _, cx| {
21075 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21076 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21077 editor.highlight_background::<DocumentHighlightRead>(
21078 &[highlight_range],
21079 |theme| theme.colors().editor_document_highlight_read_background,
21080 cx,
21081 );
21082 });
21083
21084 let mut prepare_rename_handler = cx
21085 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
21086 move |_, _, _| async move {
21087 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
21088 start: lsp::Position {
21089 line: 0,
21090 character: 7,
21091 },
21092 end: lsp::Position {
21093 line: 0,
21094 character: 10,
21095 },
21096 })))
21097 },
21098 );
21099 let prepare_rename_task = cx
21100 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21101 .expect("Prepare rename was not started");
21102 prepare_rename_handler.next().await.unwrap();
21103 prepare_rename_task.await.expect("Prepare rename failed");
21104
21105 let mut rename_handler =
21106 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21107 let edit = lsp::TextEdit {
21108 range: lsp::Range {
21109 start: lsp::Position {
21110 line: 0,
21111 character: 7,
21112 },
21113 end: lsp::Position {
21114 line: 0,
21115 character: 10,
21116 },
21117 },
21118 new_text: "FooRenamed".to_string(),
21119 };
21120 Ok(Some(lsp::WorkspaceEdit::new(
21121 // Specify the same edit twice
21122 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
21123 )))
21124 });
21125 let rename_task = cx
21126 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21127 .expect("Confirm rename was not started");
21128 rename_handler.next().await.unwrap();
21129 rename_task.await.expect("Confirm rename failed");
21130 cx.run_until_parked();
21131
21132 // Despite two edits, only one is actually applied as those are identical
21133 cx.assert_editor_state(indoc! {"
21134 struct FooRenamedˇ {}
21135 "});
21136}
21137
21138#[gpui::test]
21139async fn test_rename_without_prepare(cx: &mut TestAppContext) {
21140 init_test(cx, |_| {});
21141 // These capabilities indicate that the server does not support prepare rename.
21142 let capabilities = lsp::ServerCapabilities {
21143 rename_provider: Some(lsp::OneOf::Left(true)),
21144 ..Default::default()
21145 };
21146 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21147
21148 cx.set_state(indoc! {"
21149 struct Fˇoo {}
21150 "});
21151
21152 cx.update_editor(|editor, _window, cx| {
21153 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21154 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21155 editor.highlight_background::<DocumentHighlightRead>(
21156 &[highlight_range],
21157 |theme| theme.colors().editor_document_highlight_read_background,
21158 cx,
21159 );
21160 });
21161
21162 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21163 .expect("Prepare rename was not started")
21164 .await
21165 .expect("Prepare rename failed");
21166
21167 let mut rename_handler =
21168 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21169 let edit = lsp::TextEdit {
21170 range: lsp::Range {
21171 start: lsp::Position {
21172 line: 0,
21173 character: 7,
21174 },
21175 end: lsp::Position {
21176 line: 0,
21177 character: 10,
21178 },
21179 },
21180 new_text: "FooRenamed".to_string(),
21181 };
21182 Ok(Some(lsp::WorkspaceEdit::new(
21183 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
21184 )))
21185 });
21186 let rename_task = cx
21187 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21188 .expect("Confirm rename was not started");
21189 rename_handler.next().await.unwrap();
21190 rename_task.await.expect("Confirm rename failed");
21191 cx.run_until_parked();
21192
21193 // Correct range is renamed, as `surrounding_word` is used to find it.
21194 cx.assert_editor_state(indoc! {"
21195 struct FooRenamedˇ {}
21196 "});
21197}
21198
21199#[gpui::test]
21200async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
21201 init_test(cx, |_| {});
21202 let mut cx = EditorTestContext::new(cx).await;
21203
21204 let language = Arc::new(
21205 Language::new(
21206 LanguageConfig::default(),
21207 Some(tree_sitter_html::LANGUAGE.into()),
21208 )
21209 .with_brackets_query(
21210 r#"
21211 ("<" @open "/>" @close)
21212 ("</" @open ">" @close)
21213 ("<" @open ">" @close)
21214 ("\"" @open "\"" @close)
21215 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
21216 "#,
21217 )
21218 .unwrap(),
21219 );
21220 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21221
21222 cx.set_state(indoc! {"
21223 <span>ˇ</span>
21224 "});
21225 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21226 cx.assert_editor_state(indoc! {"
21227 <span>
21228 ˇ
21229 </span>
21230 "});
21231
21232 cx.set_state(indoc! {"
21233 <span><span></span>ˇ</span>
21234 "});
21235 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21236 cx.assert_editor_state(indoc! {"
21237 <span><span></span>
21238 ˇ</span>
21239 "});
21240
21241 cx.set_state(indoc! {"
21242 <span>ˇ
21243 </span>
21244 "});
21245 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21246 cx.assert_editor_state(indoc! {"
21247 <span>
21248 ˇ
21249 </span>
21250 "});
21251}
21252
21253#[gpui::test(iterations = 10)]
21254async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
21255 init_test(cx, |_| {});
21256
21257 let fs = FakeFs::new(cx.executor());
21258 fs.insert_tree(
21259 path!("/dir"),
21260 json!({
21261 "a.ts": "a",
21262 }),
21263 )
21264 .await;
21265
21266 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
21267 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21268 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21269
21270 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21271 language_registry.add(Arc::new(Language::new(
21272 LanguageConfig {
21273 name: "TypeScript".into(),
21274 matcher: LanguageMatcher {
21275 path_suffixes: vec!["ts".to_string()],
21276 ..Default::default()
21277 },
21278 ..Default::default()
21279 },
21280 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
21281 )));
21282 let mut fake_language_servers = language_registry.register_fake_lsp(
21283 "TypeScript",
21284 FakeLspAdapter {
21285 capabilities: lsp::ServerCapabilities {
21286 code_lens_provider: Some(lsp::CodeLensOptions {
21287 resolve_provider: Some(true),
21288 }),
21289 execute_command_provider: Some(lsp::ExecuteCommandOptions {
21290 commands: vec!["_the/command".to_string()],
21291 ..lsp::ExecuteCommandOptions::default()
21292 }),
21293 ..lsp::ServerCapabilities::default()
21294 },
21295 ..FakeLspAdapter::default()
21296 },
21297 );
21298
21299 let editor = workspace
21300 .update(cx, |workspace, window, cx| {
21301 workspace.open_abs_path(
21302 PathBuf::from(path!("/dir/a.ts")),
21303 OpenOptions::default(),
21304 window,
21305 cx,
21306 )
21307 })
21308 .unwrap()
21309 .await
21310 .unwrap()
21311 .downcast::<Editor>()
21312 .unwrap();
21313 cx.executor().run_until_parked();
21314
21315 let fake_server = fake_language_servers.next().await.unwrap();
21316
21317 let buffer = editor.update(cx, |editor, cx| {
21318 editor
21319 .buffer()
21320 .read(cx)
21321 .as_singleton()
21322 .expect("have opened a single file by path")
21323 });
21324
21325 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
21326 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
21327 drop(buffer_snapshot);
21328 let actions = cx
21329 .update_window(*workspace, |_, window, cx| {
21330 project.code_actions(&buffer, anchor..anchor, window, cx)
21331 })
21332 .unwrap();
21333
21334 fake_server
21335 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21336 Ok(Some(vec![
21337 lsp::CodeLens {
21338 range: lsp::Range::default(),
21339 command: Some(lsp::Command {
21340 title: "Code lens command".to_owned(),
21341 command: "_the/command".to_owned(),
21342 arguments: None,
21343 }),
21344 data: None,
21345 },
21346 lsp::CodeLens {
21347 range: lsp::Range::default(),
21348 command: Some(lsp::Command {
21349 title: "Command not in capabilities".to_owned(),
21350 command: "not in capabilities".to_owned(),
21351 arguments: None,
21352 }),
21353 data: None,
21354 },
21355 lsp::CodeLens {
21356 range: lsp::Range {
21357 start: lsp::Position {
21358 line: 1,
21359 character: 1,
21360 },
21361 end: lsp::Position {
21362 line: 1,
21363 character: 1,
21364 },
21365 },
21366 command: Some(lsp::Command {
21367 title: "Command not in range".to_owned(),
21368 command: "_the/command".to_owned(),
21369 arguments: None,
21370 }),
21371 data: None,
21372 },
21373 ]))
21374 })
21375 .next()
21376 .await;
21377
21378 let actions = actions.await.unwrap();
21379 assert_eq!(
21380 actions.len(),
21381 1,
21382 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
21383 );
21384 let action = actions[0].clone();
21385 let apply = project.update(cx, |project, cx| {
21386 project.apply_code_action(buffer.clone(), action, true, cx)
21387 });
21388
21389 // Resolving the code action does not populate its edits. In absence of
21390 // edits, we must execute the given command.
21391 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
21392 |mut lens, _| async move {
21393 let lens_command = lens.command.as_mut().expect("should have a command");
21394 assert_eq!(lens_command.title, "Code lens command");
21395 lens_command.arguments = Some(vec![json!("the-argument")]);
21396 Ok(lens)
21397 },
21398 );
21399
21400 // While executing the command, the language server sends the editor
21401 // a `workspaceEdit` request.
21402 fake_server
21403 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
21404 let fake = fake_server.clone();
21405 move |params, _| {
21406 assert_eq!(params.command, "_the/command");
21407 let fake = fake.clone();
21408 async move {
21409 fake.server
21410 .request::<lsp::request::ApplyWorkspaceEdit>(
21411 lsp::ApplyWorkspaceEditParams {
21412 label: None,
21413 edit: lsp::WorkspaceEdit {
21414 changes: Some(
21415 [(
21416 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
21417 vec![lsp::TextEdit {
21418 range: lsp::Range::new(
21419 lsp::Position::new(0, 0),
21420 lsp::Position::new(0, 0),
21421 ),
21422 new_text: "X".into(),
21423 }],
21424 )]
21425 .into_iter()
21426 .collect(),
21427 ),
21428 ..lsp::WorkspaceEdit::default()
21429 },
21430 },
21431 )
21432 .await
21433 .into_response()
21434 .unwrap();
21435 Ok(Some(json!(null)))
21436 }
21437 }
21438 })
21439 .next()
21440 .await;
21441
21442 // Applying the code lens command returns a project transaction containing the edits
21443 // sent by the language server in its `workspaceEdit` request.
21444 let transaction = apply.await.unwrap();
21445 assert!(transaction.0.contains_key(&buffer));
21446 buffer.update(cx, |buffer, cx| {
21447 assert_eq!(buffer.text(), "Xa");
21448 buffer.undo(cx);
21449 assert_eq!(buffer.text(), "a");
21450 });
21451
21452 let actions_after_edits = cx
21453 .update_window(*workspace, |_, window, cx| {
21454 project.code_actions(&buffer, anchor..anchor, window, cx)
21455 })
21456 .unwrap()
21457 .await
21458 .unwrap();
21459 assert_eq!(
21460 actions, actions_after_edits,
21461 "For the same selection, same code lens actions should be returned"
21462 );
21463
21464 let _responses =
21465 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21466 panic!("No more code lens requests are expected");
21467 });
21468 editor.update_in(cx, |editor, window, cx| {
21469 editor.select_all(&SelectAll, window, cx);
21470 });
21471 cx.executor().run_until_parked();
21472 let new_actions = cx
21473 .update_window(*workspace, |_, window, cx| {
21474 project.code_actions(&buffer, anchor..anchor, window, cx)
21475 })
21476 .unwrap()
21477 .await
21478 .unwrap();
21479 assert_eq!(
21480 actions, new_actions,
21481 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
21482 );
21483}
21484
21485#[gpui::test]
21486async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
21487 init_test(cx, |_| {});
21488
21489 let fs = FakeFs::new(cx.executor());
21490 let main_text = r#"fn main() {
21491println!("1");
21492println!("2");
21493println!("3");
21494println!("4");
21495println!("5");
21496}"#;
21497 let lib_text = "mod foo {}";
21498 fs.insert_tree(
21499 path!("/a"),
21500 json!({
21501 "lib.rs": lib_text,
21502 "main.rs": main_text,
21503 }),
21504 )
21505 .await;
21506
21507 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21508 let (workspace, cx) =
21509 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21510 let worktree_id = workspace.update(cx, |workspace, cx| {
21511 workspace.project().update(cx, |project, cx| {
21512 project.worktrees(cx).next().unwrap().read(cx).id()
21513 })
21514 });
21515
21516 let expected_ranges = vec![
21517 Point::new(0, 0)..Point::new(0, 0),
21518 Point::new(1, 0)..Point::new(1, 1),
21519 Point::new(2, 0)..Point::new(2, 2),
21520 Point::new(3, 0)..Point::new(3, 3),
21521 ];
21522
21523 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21524 let editor_1 = workspace
21525 .update_in(cx, |workspace, window, cx| {
21526 workspace.open_path(
21527 (worktree_id, "main.rs"),
21528 Some(pane_1.downgrade()),
21529 true,
21530 window,
21531 cx,
21532 )
21533 })
21534 .unwrap()
21535 .await
21536 .downcast::<Editor>()
21537 .unwrap();
21538 pane_1.update(cx, |pane, cx| {
21539 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21540 open_editor.update(cx, |editor, cx| {
21541 assert_eq!(
21542 editor.display_text(cx),
21543 main_text,
21544 "Original main.rs text on initial open",
21545 );
21546 assert_eq!(
21547 editor
21548 .selections
21549 .all::<Point>(cx)
21550 .into_iter()
21551 .map(|s| s.range())
21552 .collect::<Vec<_>>(),
21553 vec![Point::zero()..Point::zero()],
21554 "Default selections on initial open",
21555 );
21556 })
21557 });
21558 editor_1.update_in(cx, |editor, window, cx| {
21559 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21560 s.select_ranges(expected_ranges.clone());
21561 });
21562 });
21563
21564 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
21565 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
21566 });
21567 let editor_2 = workspace
21568 .update_in(cx, |workspace, window, cx| {
21569 workspace.open_path(
21570 (worktree_id, "main.rs"),
21571 Some(pane_2.downgrade()),
21572 true,
21573 window,
21574 cx,
21575 )
21576 })
21577 .unwrap()
21578 .await
21579 .downcast::<Editor>()
21580 .unwrap();
21581 pane_2.update(cx, |pane, cx| {
21582 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21583 open_editor.update(cx, |editor, cx| {
21584 assert_eq!(
21585 editor.display_text(cx),
21586 main_text,
21587 "Original main.rs text on initial open in another panel",
21588 );
21589 assert_eq!(
21590 editor
21591 .selections
21592 .all::<Point>(cx)
21593 .into_iter()
21594 .map(|s| s.range())
21595 .collect::<Vec<_>>(),
21596 vec![Point::zero()..Point::zero()],
21597 "Default selections on initial open in another panel",
21598 );
21599 })
21600 });
21601
21602 editor_2.update_in(cx, |editor, window, cx| {
21603 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
21604 });
21605
21606 let _other_editor_1 = workspace
21607 .update_in(cx, |workspace, window, cx| {
21608 workspace.open_path(
21609 (worktree_id, "lib.rs"),
21610 Some(pane_1.downgrade()),
21611 true,
21612 window,
21613 cx,
21614 )
21615 })
21616 .unwrap()
21617 .await
21618 .downcast::<Editor>()
21619 .unwrap();
21620 pane_1
21621 .update_in(cx, |pane, window, cx| {
21622 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
21623 })
21624 .await
21625 .unwrap();
21626 drop(editor_1);
21627 pane_1.update(cx, |pane, cx| {
21628 pane.active_item()
21629 .unwrap()
21630 .downcast::<Editor>()
21631 .unwrap()
21632 .update(cx, |editor, cx| {
21633 assert_eq!(
21634 editor.display_text(cx),
21635 lib_text,
21636 "Other file should be open and active",
21637 );
21638 });
21639 assert_eq!(pane.items().count(), 1, "No other editors should be open");
21640 });
21641
21642 let _other_editor_2 = workspace
21643 .update_in(cx, |workspace, window, cx| {
21644 workspace.open_path(
21645 (worktree_id, "lib.rs"),
21646 Some(pane_2.downgrade()),
21647 true,
21648 window,
21649 cx,
21650 )
21651 })
21652 .unwrap()
21653 .await
21654 .downcast::<Editor>()
21655 .unwrap();
21656 pane_2
21657 .update_in(cx, |pane, window, cx| {
21658 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
21659 })
21660 .await
21661 .unwrap();
21662 drop(editor_2);
21663 pane_2.update(cx, |pane, cx| {
21664 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21665 open_editor.update(cx, |editor, cx| {
21666 assert_eq!(
21667 editor.display_text(cx),
21668 lib_text,
21669 "Other file should be open and active in another panel too",
21670 );
21671 });
21672 assert_eq!(
21673 pane.items().count(),
21674 1,
21675 "No other editors should be open in another pane",
21676 );
21677 });
21678
21679 let _editor_1_reopened = workspace
21680 .update_in(cx, |workspace, window, cx| {
21681 workspace.open_path(
21682 (worktree_id, "main.rs"),
21683 Some(pane_1.downgrade()),
21684 true,
21685 window,
21686 cx,
21687 )
21688 })
21689 .unwrap()
21690 .await
21691 .downcast::<Editor>()
21692 .unwrap();
21693 let _editor_2_reopened = workspace
21694 .update_in(cx, |workspace, window, cx| {
21695 workspace.open_path(
21696 (worktree_id, "main.rs"),
21697 Some(pane_2.downgrade()),
21698 true,
21699 window,
21700 cx,
21701 )
21702 })
21703 .unwrap()
21704 .await
21705 .downcast::<Editor>()
21706 .unwrap();
21707 pane_1.update(cx, |pane, cx| {
21708 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21709 open_editor.update(cx, |editor, cx| {
21710 assert_eq!(
21711 editor.display_text(cx),
21712 main_text,
21713 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
21714 );
21715 assert_eq!(
21716 editor
21717 .selections
21718 .all::<Point>(cx)
21719 .into_iter()
21720 .map(|s| s.range())
21721 .collect::<Vec<_>>(),
21722 expected_ranges,
21723 "Previous editor in the 1st panel had selections and should get them restored on reopen",
21724 );
21725 })
21726 });
21727 pane_2.update(cx, |pane, cx| {
21728 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21729 open_editor.update(cx, |editor, cx| {
21730 assert_eq!(
21731 editor.display_text(cx),
21732 r#"fn main() {
21733⋯rintln!("1");
21734⋯intln!("2");
21735⋯ntln!("3");
21736println!("4");
21737println!("5");
21738}"#,
21739 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
21740 );
21741 assert_eq!(
21742 editor
21743 .selections
21744 .all::<Point>(cx)
21745 .into_iter()
21746 .map(|s| s.range())
21747 .collect::<Vec<_>>(),
21748 vec![Point::zero()..Point::zero()],
21749 "Previous editor in the 2nd pane had no selections changed hence should restore none",
21750 );
21751 })
21752 });
21753}
21754
21755#[gpui::test]
21756async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
21757 init_test(cx, |_| {});
21758
21759 let fs = FakeFs::new(cx.executor());
21760 let main_text = r#"fn main() {
21761println!("1");
21762println!("2");
21763println!("3");
21764println!("4");
21765println!("5");
21766}"#;
21767 let lib_text = "mod foo {}";
21768 fs.insert_tree(
21769 path!("/a"),
21770 json!({
21771 "lib.rs": lib_text,
21772 "main.rs": main_text,
21773 }),
21774 )
21775 .await;
21776
21777 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21778 let (workspace, cx) =
21779 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21780 let worktree_id = workspace.update(cx, |workspace, cx| {
21781 workspace.project().update(cx, |project, cx| {
21782 project.worktrees(cx).next().unwrap().read(cx).id()
21783 })
21784 });
21785
21786 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21787 let editor = workspace
21788 .update_in(cx, |workspace, window, cx| {
21789 workspace.open_path(
21790 (worktree_id, "main.rs"),
21791 Some(pane.downgrade()),
21792 true,
21793 window,
21794 cx,
21795 )
21796 })
21797 .unwrap()
21798 .await
21799 .downcast::<Editor>()
21800 .unwrap();
21801 pane.update(cx, |pane, cx| {
21802 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21803 open_editor.update(cx, |editor, cx| {
21804 assert_eq!(
21805 editor.display_text(cx),
21806 main_text,
21807 "Original main.rs text on initial open",
21808 );
21809 })
21810 });
21811 editor.update_in(cx, |editor, window, cx| {
21812 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
21813 });
21814
21815 cx.update_global(|store: &mut SettingsStore, cx| {
21816 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21817 s.restore_on_file_reopen = Some(false);
21818 });
21819 });
21820 editor.update_in(cx, |editor, window, cx| {
21821 editor.fold_ranges(
21822 vec![
21823 Point::new(1, 0)..Point::new(1, 1),
21824 Point::new(2, 0)..Point::new(2, 2),
21825 Point::new(3, 0)..Point::new(3, 3),
21826 ],
21827 false,
21828 window,
21829 cx,
21830 );
21831 });
21832 pane.update_in(cx, |pane, window, cx| {
21833 pane.close_all_items(&CloseAllItems::default(), window, cx)
21834 })
21835 .await
21836 .unwrap();
21837 pane.update(cx, |pane, _| {
21838 assert!(pane.active_item().is_none());
21839 });
21840 cx.update_global(|store: &mut SettingsStore, cx| {
21841 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21842 s.restore_on_file_reopen = Some(true);
21843 });
21844 });
21845
21846 let _editor_reopened = workspace
21847 .update_in(cx, |workspace, window, cx| {
21848 workspace.open_path(
21849 (worktree_id, "main.rs"),
21850 Some(pane.downgrade()),
21851 true,
21852 window,
21853 cx,
21854 )
21855 })
21856 .unwrap()
21857 .await
21858 .downcast::<Editor>()
21859 .unwrap();
21860 pane.update(cx, |pane, cx| {
21861 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21862 open_editor.update(cx, |editor, cx| {
21863 assert_eq!(
21864 editor.display_text(cx),
21865 main_text,
21866 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
21867 );
21868 })
21869 });
21870}
21871
21872#[gpui::test]
21873async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
21874 struct EmptyModalView {
21875 focus_handle: gpui::FocusHandle,
21876 }
21877 impl EventEmitter<DismissEvent> for EmptyModalView {}
21878 impl Render for EmptyModalView {
21879 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
21880 div()
21881 }
21882 }
21883 impl Focusable for EmptyModalView {
21884 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
21885 self.focus_handle.clone()
21886 }
21887 }
21888 impl workspace::ModalView for EmptyModalView {}
21889 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
21890 EmptyModalView {
21891 focus_handle: cx.focus_handle(),
21892 }
21893 }
21894
21895 init_test(cx, |_| {});
21896
21897 let fs = FakeFs::new(cx.executor());
21898 let project = Project::test(fs, [], cx).await;
21899 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21900 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
21901 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21902 let editor = cx.new_window_entity(|window, cx| {
21903 Editor::new(
21904 EditorMode::full(),
21905 buffer,
21906 Some(project.clone()),
21907 window,
21908 cx,
21909 )
21910 });
21911 workspace
21912 .update(cx, |workspace, window, cx| {
21913 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
21914 })
21915 .unwrap();
21916 editor.update_in(cx, |editor, window, cx| {
21917 editor.open_context_menu(&OpenContextMenu, window, cx);
21918 assert!(editor.mouse_context_menu.is_some());
21919 });
21920 workspace
21921 .update(cx, |workspace, window, cx| {
21922 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
21923 })
21924 .unwrap();
21925 cx.read(|cx| {
21926 assert!(editor.read(cx).mouse_context_menu.is_none());
21927 });
21928}
21929
21930#[gpui::test]
21931async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
21932 init_test(cx, |_| {});
21933
21934 let fs = FakeFs::new(cx.executor());
21935 fs.insert_file(path!("/file.html"), Default::default())
21936 .await;
21937
21938 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
21939
21940 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21941 let html_language = Arc::new(Language::new(
21942 LanguageConfig {
21943 name: "HTML".into(),
21944 matcher: LanguageMatcher {
21945 path_suffixes: vec!["html".to_string()],
21946 ..LanguageMatcher::default()
21947 },
21948 brackets: BracketPairConfig {
21949 pairs: vec![BracketPair {
21950 start: "<".into(),
21951 end: ">".into(),
21952 close: true,
21953 ..Default::default()
21954 }],
21955 ..Default::default()
21956 },
21957 ..Default::default()
21958 },
21959 Some(tree_sitter_html::LANGUAGE.into()),
21960 ));
21961 language_registry.add(html_language);
21962 let mut fake_servers = language_registry.register_fake_lsp(
21963 "HTML",
21964 FakeLspAdapter {
21965 capabilities: lsp::ServerCapabilities {
21966 completion_provider: Some(lsp::CompletionOptions {
21967 resolve_provider: Some(true),
21968 ..Default::default()
21969 }),
21970 ..Default::default()
21971 },
21972 ..Default::default()
21973 },
21974 );
21975
21976 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21977 let cx = &mut VisualTestContext::from_window(*workspace, cx);
21978
21979 let worktree_id = workspace
21980 .update(cx, |workspace, _window, cx| {
21981 workspace.project().update(cx, |project, cx| {
21982 project.worktrees(cx).next().unwrap().read(cx).id()
21983 })
21984 })
21985 .unwrap();
21986 project
21987 .update(cx, |project, cx| {
21988 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
21989 })
21990 .await
21991 .unwrap();
21992 let editor = workspace
21993 .update(cx, |workspace, window, cx| {
21994 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
21995 })
21996 .unwrap()
21997 .await
21998 .unwrap()
21999 .downcast::<Editor>()
22000 .unwrap();
22001
22002 let fake_server = fake_servers.next().await.unwrap();
22003 editor.update_in(cx, |editor, window, cx| {
22004 editor.set_text("<ad></ad>", window, cx);
22005 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22006 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
22007 });
22008 let Some((buffer, _)) = editor
22009 .buffer
22010 .read(cx)
22011 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
22012 else {
22013 panic!("Failed to get buffer for selection position");
22014 };
22015 let buffer = buffer.read(cx);
22016 let buffer_id = buffer.remote_id();
22017 let opening_range =
22018 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
22019 let closing_range =
22020 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
22021 let mut linked_ranges = HashMap::default();
22022 linked_ranges.insert(
22023 buffer_id,
22024 vec![(opening_range.clone(), vec![closing_range.clone()])],
22025 );
22026 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
22027 });
22028 let mut completion_handle =
22029 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
22030 Ok(Some(lsp::CompletionResponse::Array(vec![
22031 lsp::CompletionItem {
22032 label: "head".to_string(),
22033 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22034 lsp::InsertReplaceEdit {
22035 new_text: "head".to_string(),
22036 insert: lsp::Range::new(
22037 lsp::Position::new(0, 1),
22038 lsp::Position::new(0, 3),
22039 ),
22040 replace: lsp::Range::new(
22041 lsp::Position::new(0, 1),
22042 lsp::Position::new(0, 3),
22043 ),
22044 },
22045 )),
22046 ..Default::default()
22047 },
22048 ])))
22049 });
22050 editor.update_in(cx, |editor, window, cx| {
22051 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
22052 });
22053 cx.run_until_parked();
22054 completion_handle.next().await.unwrap();
22055 editor.update(cx, |editor, _| {
22056 assert!(
22057 editor.context_menu_visible(),
22058 "Completion menu should be visible"
22059 );
22060 });
22061 editor.update_in(cx, |editor, window, cx| {
22062 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
22063 });
22064 cx.executor().run_until_parked();
22065 editor.update(cx, |editor, cx| {
22066 assert_eq!(editor.text(cx), "<head></head>");
22067 });
22068}
22069
22070#[gpui::test]
22071async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
22072 init_test(cx, |_| {});
22073
22074 let fs = FakeFs::new(cx.executor());
22075 fs.insert_tree(
22076 path!("/root"),
22077 json!({
22078 "a": {
22079 "main.rs": "fn main() {}",
22080 },
22081 "foo": {
22082 "bar": {
22083 "external_file.rs": "pub mod external {}",
22084 }
22085 }
22086 }),
22087 )
22088 .await;
22089
22090 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
22091 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22092 language_registry.add(rust_lang());
22093 let _fake_servers = language_registry.register_fake_lsp(
22094 "Rust",
22095 FakeLspAdapter {
22096 ..FakeLspAdapter::default()
22097 },
22098 );
22099 let (workspace, cx) =
22100 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22101 let worktree_id = workspace.update(cx, |workspace, cx| {
22102 workspace.project().update(cx, |project, cx| {
22103 project.worktrees(cx).next().unwrap().read(cx).id()
22104 })
22105 });
22106
22107 let assert_language_servers_count =
22108 |expected: usize, context: &str, cx: &mut VisualTestContext| {
22109 project.update(cx, |project, cx| {
22110 let current = project
22111 .lsp_store()
22112 .read(cx)
22113 .as_local()
22114 .unwrap()
22115 .language_servers
22116 .len();
22117 assert_eq!(expected, current, "{context}");
22118 });
22119 };
22120
22121 assert_language_servers_count(
22122 0,
22123 "No servers should be running before any file is open",
22124 cx,
22125 );
22126 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22127 let main_editor = workspace
22128 .update_in(cx, |workspace, window, cx| {
22129 workspace.open_path(
22130 (worktree_id, "main.rs"),
22131 Some(pane.downgrade()),
22132 true,
22133 window,
22134 cx,
22135 )
22136 })
22137 .unwrap()
22138 .await
22139 .downcast::<Editor>()
22140 .unwrap();
22141 pane.update(cx, |pane, cx| {
22142 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22143 open_editor.update(cx, |editor, cx| {
22144 assert_eq!(
22145 editor.display_text(cx),
22146 "fn main() {}",
22147 "Original main.rs text on initial open",
22148 );
22149 });
22150 assert_eq!(open_editor, main_editor);
22151 });
22152 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
22153
22154 let external_editor = workspace
22155 .update_in(cx, |workspace, window, cx| {
22156 workspace.open_abs_path(
22157 PathBuf::from("/root/foo/bar/external_file.rs"),
22158 OpenOptions::default(),
22159 window,
22160 cx,
22161 )
22162 })
22163 .await
22164 .expect("opening external file")
22165 .downcast::<Editor>()
22166 .expect("downcasted external file's open element to editor");
22167 pane.update(cx, |pane, cx| {
22168 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22169 open_editor.update(cx, |editor, cx| {
22170 assert_eq!(
22171 editor.display_text(cx),
22172 "pub mod external {}",
22173 "External file is open now",
22174 );
22175 });
22176 assert_eq!(open_editor, external_editor);
22177 });
22178 assert_language_servers_count(
22179 1,
22180 "Second, external, *.rs file should join the existing server",
22181 cx,
22182 );
22183
22184 pane.update_in(cx, |pane, window, cx| {
22185 pane.close_active_item(&CloseActiveItem::default(), window, cx)
22186 })
22187 .await
22188 .unwrap();
22189 pane.update_in(cx, |pane, window, cx| {
22190 pane.navigate_backward(window, cx);
22191 });
22192 cx.run_until_parked();
22193 pane.update(cx, |pane, cx| {
22194 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22195 open_editor.update(cx, |editor, cx| {
22196 assert_eq!(
22197 editor.display_text(cx),
22198 "pub mod external {}",
22199 "External file is open now",
22200 );
22201 });
22202 });
22203 assert_language_servers_count(
22204 1,
22205 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
22206 cx,
22207 );
22208
22209 cx.update(|_, cx| {
22210 workspace::reload(&workspace::Reload::default(), cx);
22211 });
22212 assert_language_servers_count(
22213 1,
22214 "After reloading the worktree with local and external files opened, only one project should be started",
22215 cx,
22216 );
22217}
22218
22219#[gpui::test]
22220async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
22221 init_test(cx, |_| {});
22222
22223 let mut cx = EditorTestContext::new(cx).await;
22224 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22225 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22226
22227 // test cursor move to start of each line on tab
22228 // for `if`, `elif`, `else`, `while`, `with` and `for`
22229 cx.set_state(indoc! {"
22230 def main():
22231 ˇ for item in items:
22232 ˇ while item.active:
22233 ˇ if item.value > 10:
22234 ˇ continue
22235 ˇ elif item.value < 0:
22236 ˇ break
22237 ˇ else:
22238 ˇ with item.context() as ctx:
22239 ˇ yield count
22240 ˇ else:
22241 ˇ log('while else')
22242 ˇ else:
22243 ˇ log('for else')
22244 "});
22245 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22246 cx.assert_editor_state(indoc! {"
22247 def main():
22248 ˇfor item in items:
22249 ˇwhile item.active:
22250 ˇif item.value > 10:
22251 ˇcontinue
22252 ˇelif item.value < 0:
22253 ˇbreak
22254 ˇelse:
22255 ˇwith item.context() as ctx:
22256 ˇyield count
22257 ˇelse:
22258 ˇlog('while else')
22259 ˇelse:
22260 ˇlog('for else')
22261 "});
22262 // test relative indent is preserved when tab
22263 // for `if`, `elif`, `else`, `while`, `with` and `for`
22264 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22265 cx.assert_editor_state(indoc! {"
22266 def main():
22267 ˇfor item in items:
22268 ˇwhile item.active:
22269 ˇif item.value > 10:
22270 ˇcontinue
22271 ˇelif item.value < 0:
22272 ˇbreak
22273 ˇelse:
22274 ˇwith item.context() as ctx:
22275 ˇyield count
22276 ˇelse:
22277 ˇlog('while else')
22278 ˇelse:
22279 ˇlog('for else')
22280 "});
22281
22282 // test cursor move to start of each line on tab
22283 // for `try`, `except`, `else`, `finally`, `match` and `def`
22284 cx.set_state(indoc! {"
22285 def main():
22286 ˇ try:
22287 ˇ fetch()
22288 ˇ except ValueError:
22289 ˇ handle_error()
22290 ˇ else:
22291 ˇ match value:
22292 ˇ case _:
22293 ˇ finally:
22294 ˇ def status():
22295 ˇ return 0
22296 "});
22297 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22298 cx.assert_editor_state(indoc! {"
22299 def main():
22300 ˇtry:
22301 ˇfetch()
22302 ˇexcept ValueError:
22303 ˇhandle_error()
22304 ˇelse:
22305 ˇmatch value:
22306 ˇcase _:
22307 ˇfinally:
22308 ˇdef status():
22309 ˇreturn 0
22310 "});
22311 // test relative indent is preserved when tab
22312 // for `try`, `except`, `else`, `finally`, `match` and `def`
22313 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22314 cx.assert_editor_state(indoc! {"
22315 def main():
22316 ˇtry:
22317 ˇfetch()
22318 ˇexcept ValueError:
22319 ˇhandle_error()
22320 ˇelse:
22321 ˇmatch value:
22322 ˇcase _:
22323 ˇfinally:
22324 ˇdef status():
22325 ˇreturn 0
22326 "});
22327}
22328
22329#[gpui::test]
22330async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
22331 init_test(cx, |_| {});
22332
22333 let mut cx = EditorTestContext::new(cx).await;
22334 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22335 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22336
22337 // test `else` auto outdents when typed inside `if` block
22338 cx.set_state(indoc! {"
22339 def main():
22340 if i == 2:
22341 return
22342 ˇ
22343 "});
22344 cx.update_editor(|editor, window, cx| {
22345 editor.handle_input("else:", window, cx);
22346 });
22347 cx.assert_editor_state(indoc! {"
22348 def main():
22349 if i == 2:
22350 return
22351 else:ˇ
22352 "});
22353
22354 // test `except` auto outdents when typed inside `try` block
22355 cx.set_state(indoc! {"
22356 def main():
22357 try:
22358 i = 2
22359 ˇ
22360 "});
22361 cx.update_editor(|editor, window, cx| {
22362 editor.handle_input("except:", window, cx);
22363 });
22364 cx.assert_editor_state(indoc! {"
22365 def main():
22366 try:
22367 i = 2
22368 except:ˇ
22369 "});
22370
22371 // test `else` auto outdents when typed inside `except` block
22372 cx.set_state(indoc! {"
22373 def main():
22374 try:
22375 i = 2
22376 except:
22377 j = 2
22378 ˇ
22379 "});
22380 cx.update_editor(|editor, window, cx| {
22381 editor.handle_input("else:", window, cx);
22382 });
22383 cx.assert_editor_state(indoc! {"
22384 def main():
22385 try:
22386 i = 2
22387 except:
22388 j = 2
22389 else:ˇ
22390 "});
22391
22392 // test `finally` auto outdents when typed inside `else` block
22393 cx.set_state(indoc! {"
22394 def main():
22395 try:
22396 i = 2
22397 except:
22398 j = 2
22399 else:
22400 k = 2
22401 ˇ
22402 "});
22403 cx.update_editor(|editor, window, cx| {
22404 editor.handle_input("finally:", window, cx);
22405 });
22406 cx.assert_editor_state(indoc! {"
22407 def main():
22408 try:
22409 i = 2
22410 except:
22411 j = 2
22412 else:
22413 k = 2
22414 finally:ˇ
22415 "});
22416
22417 // test `else` does not outdents when typed inside `except` block right after for block
22418 cx.set_state(indoc! {"
22419 def main():
22420 try:
22421 i = 2
22422 except:
22423 for i in range(n):
22424 pass
22425 ˇ
22426 "});
22427 cx.update_editor(|editor, window, cx| {
22428 editor.handle_input("else:", window, cx);
22429 });
22430 cx.assert_editor_state(indoc! {"
22431 def main():
22432 try:
22433 i = 2
22434 except:
22435 for i in range(n):
22436 pass
22437 else:ˇ
22438 "});
22439
22440 // test `finally` auto outdents when typed inside `else` block right after for block
22441 cx.set_state(indoc! {"
22442 def main():
22443 try:
22444 i = 2
22445 except:
22446 j = 2
22447 else:
22448 for i in range(n):
22449 pass
22450 ˇ
22451 "});
22452 cx.update_editor(|editor, window, cx| {
22453 editor.handle_input("finally:", window, cx);
22454 });
22455 cx.assert_editor_state(indoc! {"
22456 def main():
22457 try:
22458 i = 2
22459 except:
22460 j = 2
22461 else:
22462 for i in range(n):
22463 pass
22464 finally:ˇ
22465 "});
22466
22467 // test `except` outdents to inner "try" block
22468 cx.set_state(indoc! {"
22469 def main():
22470 try:
22471 i = 2
22472 if i == 2:
22473 try:
22474 i = 3
22475 ˇ
22476 "});
22477 cx.update_editor(|editor, window, cx| {
22478 editor.handle_input("except:", window, cx);
22479 });
22480 cx.assert_editor_state(indoc! {"
22481 def main():
22482 try:
22483 i = 2
22484 if i == 2:
22485 try:
22486 i = 3
22487 except:ˇ
22488 "});
22489
22490 // test `except` outdents to outer "try" block
22491 cx.set_state(indoc! {"
22492 def main():
22493 try:
22494 i = 2
22495 if i == 2:
22496 try:
22497 i = 3
22498 ˇ
22499 "});
22500 cx.update_editor(|editor, window, cx| {
22501 editor.handle_input("except:", window, cx);
22502 });
22503 cx.assert_editor_state(indoc! {"
22504 def main():
22505 try:
22506 i = 2
22507 if i == 2:
22508 try:
22509 i = 3
22510 except:ˇ
22511 "});
22512
22513 // test `else` stays at correct indent when typed after `for` block
22514 cx.set_state(indoc! {"
22515 def main():
22516 for i in range(10):
22517 if i == 3:
22518 break
22519 ˇ
22520 "});
22521 cx.update_editor(|editor, window, cx| {
22522 editor.handle_input("else:", window, cx);
22523 });
22524 cx.assert_editor_state(indoc! {"
22525 def main():
22526 for i in range(10):
22527 if i == 3:
22528 break
22529 else:ˇ
22530 "});
22531
22532 // test does not outdent on typing after line with square brackets
22533 cx.set_state(indoc! {"
22534 def f() -> list[str]:
22535 ˇ
22536 "});
22537 cx.update_editor(|editor, window, cx| {
22538 editor.handle_input("a", window, cx);
22539 });
22540 cx.assert_editor_state(indoc! {"
22541 def f() -> list[str]:
22542 aˇ
22543 "});
22544
22545 // test does not outdent on typing : after case keyword
22546 cx.set_state(indoc! {"
22547 match 1:
22548 caseˇ
22549 "});
22550 cx.update_editor(|editor, window, cx| {
22551 editor.handle_input(":", window, cx);
22552 });
22553 cx.assert_editor_state(indoc! {"
22554 match 1:
22555 case:ˇ
22556 "});
22557}
22558
22559#[gpui::test]
22560async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
22561 init_test(cx, |_| {});
22562 update_test_language_settings(cx, |settings| {
22563 settings.defaults.extend_comment_on_newline = Some(false);
22564 });
22565 let mut cx = EditorTestContext::new(cx).await;
22566 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22567 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22568
22569 // test correct indent after newline on comment
22570 cx.set_state(indoc! {"
22571 # COMMENT:ˇ
22572 "});
22573 cx.update_editor(|editor, window, cx| {
22574 editor.newline(&Newline, window, cx);
22575 });
22576 cx.assert_editor_state(indoc! {"
22577 # COMMENT:
22578 ˇ
22579 "});
22580
22581 // test correct indent after newline in brackets
22582 cx.set_state(indoc! {"
22583 {ˇ}
22584 "});
22585 cx.update_editor(|editor, window, cx| {
22586 editor.newline(&Newline, window, cx);
22587 });
22588 cx.run_until_parked();
22589 cx.assert_editor_state(indoc! {"
22590 {
22591 ˇ
22592 }
22593 "});
22594
22595 cx.set_state(indoc! {"
22596 (ˇ)
22597 "});
22598 cx.update_editor(|editor, window, cx| {
22599 editor.newline(&Newline, window, cx);
22600 });
22601 cx.run_until_parked();
22602 cx.assert_editor_state(indoc! {"
22603 (
22604 ˇ
22605 )
22606 "});
22607
22608 // do not indent after empty lists or dictionaries
22609 cx.set_state(indoc! {"
22610 a = []ˇ
22611 "});
22612 cx.update_editor(|editor, window, cx| {
22613 editor.newline(&Newline, window, cx);
22614 });
22615 cx.run_until_parked();
22616 cx.assert_editor_state(indoc! {"
22617 a = []
22618 ˇ
22619 "});
22620}
22621
22622fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
22623 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
22624 point..point
22625}
22626
22627#[track_caller]
22628fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
22629 let (text, ranges) = marked_text_ranges(marked_text, true);
22630 assert_eq!(editor.text(cx), text);
22631 assert_eq!(
22632 editor.selections.ranges(cx),
22633 ranges,
22634 "Assert selections are {}",
22635 marked_text
22636 );
22637}
22638
22639pub fn handle_signature_help_request(
22640 cx: &mut EditorLspTestContext,
22641 mocked_response: lsp::SignatureHelp,
22642) -> impl Future<Output = ()> + use<> {
22643 let mut request =
22644 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
22645 let mocked_response = mocked_response.clone();
22646 async move { Ok(Some(mocked_response)) }
22647 });
22648
22649 async move {
22650 request.next().await;
22651 }
22652}
22653
22654#[track_caller]
22655pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
22656 cx.update_editor(|editor, _, _| {
22657 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
22658 let entries = menu.entries.borrow();
22659 let entries = entries
22660 .iter()
22661 .map(|entry| entry.string.as_str())
22662 .collect::<Vec<_>>();
22663 assert_eq!(entries, expected);
22664 } else {
22665 panic!("Expected completions menu");
22666 }
22667 });
22668}
22669
22670/// Handle completion request passing a marked string specifying where the completion
22671/// should be triggered from using '|' character, what range should be replaced, and what completions
22672/// should be returned using '<' and '>' to delimit the range.
22673///
22674/// Also see `handle_completion_request_with_insert_and_replace`.
22675#[track_caller]
22676pub fn handle_completion_request(
22677 marked_string: &str,
22678 completions: Vec<&'static str>,
22679 is_incomplete: bool,
22680 counter: Arc<AtomicUsize>,
22681 cx: &mut EditorLspTestContext,
22682) -> impl Future<Output = ()> {
22683 let complete_from_marker: TextRangeMarker = '|'.into();
22684 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22685 let (_, mut marked_ranges) = marked_text_ranges_by(
22686 marked_string,
22687 vec![complete_from_marker.clone(), replace_range_marker.clone()],
22688 );
22689
22690 let complete_from_position =
22691 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22692 let replace_range =
22693 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22694
22695 let mut request =
22696 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22697 let completions = completions.clone();
22698 counter.fetch_add(1, atomic::Ordering::Release);
22699 async move {
22700 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22701 assert_eq!(
22702 params.text_document_position.position,
22703 complete_from_position
22704 );
22705 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
22706 is_incomplete: is_incomplete,
22707 item_defaults: None,
22708 items: completions
22709 .iter()
22710 .map(|completion_text| lsp::CompletionItem {
22711 label: completion_text.to_string(),
22712 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
22713 range: replace_range,
22714 new_text: completion_text.to_string(),
22715 })),
22716 ..Default::default()
22717 })
22718 .collect(),
22719 })))
22720 }
22721 });
22722
22723 async move {
22724 request.next().await;
22725 }
22726}
22727
22728/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
22729/// given instead, which also contains an `insert` range.
22730///
22731/// This function uses markers to define ranges:
22732/// - `|` marks the cursor position
22733/// - `<>` marks the replace range
22734/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
22735pub fn handle_completion_request_with_insert_and_replace(
22736 cx: &mut EditorLspTestContext,
22737 marked_string: &str,
22738 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
22739 counter: Arc<AtomicUsize>,
22740) -> impl Future<Output = ()> {
22741 let complete_from_marker: TextRangeMarker = '|'.into();
22742 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22743 let insert_range_marker: TextRangeMarker = ('{', '}').into();
22744
22745 let (_, mut marked_ranges) = marked_text_ranges_by(
22746 marked_string,
22747 vec![
22748 complete_from_marker.clone(),
22749 replace_range_marker.clone(),
22750 insert_range_marker.clone(),
22751 ],
22752 );
22753
22754 let complete_from_position =
22755 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22756 let replace_range =
22757 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22758
22759 let insert_range = match marked_ranges.remove(&insert_range_marker) {
22760 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
22761 _ => lsp::Range {
22762 start: replace_range.start,
22763 end: complete_from_position,
22764 },
22765 };
22766
22767 let mut request =
22768 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22769 let completions = completions.clone();
22770 counter.fetch_add(1, atomic::Ordering::Release);
22771 async move {
22772 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22773 assert_eq!(
22774 params.text_document_position.position, complete_from_position,
22775 "marker `|` position doesn't match",
22776 );
22777 Ok(Some(lsp::CompletionResponse::Array(
22778 completions
22779 .iter()
22780 .map(|(label, new_text)| lsp::CompletionItem {
22781 label: label.to_string(),
22782 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22783 lsp::InsertReplaceEdit {
22784 insert: insert_range,
22785 replace: replace_range,
22786 new_text: new_text.to_string(),
22787 },
22788 )),
22789 ..Default::default()
22790 })
22791 .collect(),
22792 )))
22793 }
22794 });
22795
22796 async move {
22797 request.next().await;
22798 }
22799}
22800
22801fn handle_resolve_completion_request(
22802 cx: &mut EditorLspTestContext,
22803 edits: Option<Vec<(&'static str, &'static str)>>,
22804) -> impl Future<Output = ()> {
22805 let edits = edits.map(|edits| {
22806 edits
22807 .iter()
22808 .map(|(marked_string, new_text)| {
22809 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
22810 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
22811 lsp::TextEdit::new(replace_range, new_text.to_string())
22812 })
22813 .collect::<Vec<_>>()
22814 });
22815
22816 let mut request =
22817 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
22818 let edits = edits.clone();
22819 async move {
22820 Ok(lsp::CompletionItem {
22821 additional_text_edits: edits,
22822 ..Default::default()
22823 })
22824 }
22825 });
22826
22827 async move {
22828 request.next().await;
22829 }
22830}
22831
22832pub(crate) fn update_test_language_settings(
22833 cx: &mut TestAppContext,
22834 f: impl Fn(&mut AllLanguageSettingsContent),
22835) {
22836 cx.update(|cx| {
22837 SettingsStore::update_global(cx, |store, cx| {
22838 store.update_user_settings::<AllLanguageSettings>(cx, f);
22839 });
22840 });
22841}
22842
22843pub(crate) fn update_test_project_settings(
22844 cx: &mut TestAppContext,
22845 f: impl Fn(&mut ProjectSettings),
22846) {
22847 cx.update(|cx| {
22848 SettingsStore::update_global(cx, |store, cx| {
22849 store.update_user_settings::<ProjectSettings>(cx, f);
22850 });
22851 });
22852}
22853
22854pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
22855 cx.update(|cx| {
22856 assets::Assets.load_test_fonts(cx);
22857 let store = SettingsStore::test(cx);
22858 cx.set_global(store);
22859 theme::init(theme::LoadThemes::JustBase, cx);
22860 release_channel::init(SemanticVersion::default(), cx);
22861 client::init_settings(cx);
22862 language::init(cx);
22863 Project::init_settings(cx);
22864 workspace::init_settings(cx);
22865 crate::init(cx);
22866 });
22867 zlog::init_test();
22868 update_test_language_settings(cx, f);
22869}
22870
22871#[track_caller]
22872fn assert_hunk_revert(
22873 not_reverted_text_with_selections: &str,
22874 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
22875 expected_reverted_text_with_selections: &str,
22876 base_text: &str,
22877 cx: &mut EditorLspTestContext,
22878) {
22879 cx.set_state(not_reverted_text_with_selections);
22880 cx.set_head_text(base_text);
22881 cx.executor().run_until_parked();
22882
22883 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
22884 let snapshot = editor.snapshot(window, cx);
22885 let reverted_hunk_statuses = snapshot
22886 .buffer_snapshot
22887 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
22888 .map(|hunk| hunk.status().kind)
22889 .collect::<Vec<_>>();
22890
22891 editor.git_restore(&Default::default(), window, cx);
22892 reverted_hunk_statuses
22893 });
22894 cx.executor().run_until_parked();
22895 cx.assert_editor_state(expected_reverted_text_with_selections);
22896 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
22897}
22898
22899#[gpui::test(iterations = 10)]
22900async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
22901 init_test(cx, |_| {});
22902
22903 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
22904 let counter = diagnostic_requests.clone();
22905
22906 let fs = FakeFs::new(cx.executor());
22907 fs.insert_tree(
22908 path!("/a"),
22909 json!({
22910 "first.rs": "fn main() { let a = 5; }",
22911 "second.rs": "// Test file",
22912 }),
22913 )
22914 .await;
22915
22916 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22917 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22918 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22919
22920 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22921 language_registry.add(rust_lang());
22922 let mut fake_servers = language_registry.register_fake_lsp(
22923 "Rust",
22924 FakeLspAdapter {
22925 capabilities: lsp::ServerCapabilities {
22926 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
22927 lsp::DiagnosticOptions {
22928 identifier: None,
22929 inter_file_dependencies: true,
22930 workspace_diagnostics: true,
22931 work_done_progress_options: Default::default(),
22932 },
22933 )),
22934 ..Default::default()
22935 },
22936 ..Default::default()
22937 },
22938 );
22939
22940 let editor = workspace
22941 .update(cx, |workspace, window, cx| {
22942 workspace.open_abs_path(
22943 PathBuf::from(path!("/a/first.rs")),
22944 OpenOptions::default(),
22945 window,
22946 cx,
22947 )
22948 })
22949 .unwrap()
22950 .await
22951 .unwrap()
22952 .downcast::<Editor>()
22953 .unwrap();
22954 let fake_server = fake_servers.next().await.unwrap();
22955 let server_id = fake_server.server.server_id();
22956 let mut first_request = fake_server
22957 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
22958 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
22959 let result_id = Some(new_result_id.to_string());
22960 assert_eq!(
22961 params.text_document.uri,
22962 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
22963 );
22964 async move {
22965 Ok(lsp::DocumentDiagnosticReportResult::Report(
22966 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
22967 related_documents: None,
22968 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
22969 items: Vec::new(),
22970 result_id,
22971 },
22972 }),
22973 ))
22974 }
22975 });
22976
22977 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
22978 project.update(cx, |project, cx| {
22979 let buffer_id = editor
22980 .read(cx)
22981 .buffer()
22982 .read(cx)
22983 .as_singleton()
22984 .expect("created a singleton buffer")
22985 .read(cx)
22986 .remote_id();
22987 let buffer_result_id = project
22988 .lsp_store()
22989 .read(cx)
22990 .result_id(server_id, buffer_id, cx);
22991 assert_eq!(expected, buffer_result_id);
22992 });
22993 };
22994
22995 ensure_result_id(None, cx);
22996 cx.executor().advance_clock(Duration::from_millis(60));
22997 cx.executor().run_until_parked();
22998 assert_eq!(
22999 diagnostic_requests.load(atomic::Ordering::Acquire),
23000 1,
23001 "Opening file should trigger diagnostic request"
23002 );
23003 first_request
23004 .next()
23005 .await
23006 .expect("should have sent the first diagnostics pull request");
23007 ensure_result_id(Some("1".to_string()), cx);
23008
23009 // Editing should trigger diagnostics
23010 editor.update_in(cx, |editor, window, cx| {
23011 editor.handle_input("2", window, cx)
23012 });
23013 cx.executor().advance_clock(Duration::from_millis(60));
23014 cx.executor().run_until_parked();
23015 assert_eq!(
23016 diagnostic_requests.load(atomic::Ordering::Acquire),
23017 2,
23018 "Editing should trigger diagnostic request"
23019 );
23020 ensure_result_id(Some("2".to_string()), cx);
23021
23022 // Moving cursor should not trigger diagnostic request
23023 editor.update_in(cx, |editor, window, cx| {
23024 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23025 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
23026 });
23027 });
23028 cx.executor().advance_clock(Duration::from_millis(60));
23029 cx.executor().run_until_parked();
23030 assert_eq!(
23031 diagnostic_requests.load(atomic::Ordering::Acquire),
23032 2,
23033 "Cursor movement should not trigger diagnostic request"
23034 );
23035 ensure_result_id(Some("2".to_string()), cx);
23036 // Multiple rapid edits should be debounced
23037 for _ in 0..5 {
23038 editor.update_in(cx, |editor, window, cx| {
23039 editor.handle_input("x", window, cx)
23040 });
23041 }
23042 cx.executor().advance_clock(Duration::from_millis(60));
23043 cx.executor().run_until_parked();
23044
23045 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
23046 assert!(
23047 final_requests <= 4,
23048 "Multiple rapid edits should be debounced (got {final_requests} requests)",
23049 );
23050 ensure_result_id(Some(final_requests.to_string()), cx);
23051}
23052
23053#[gpui::test]
23054async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
23055 // Regression test for issue #11671
23056 // Previously, adding a cursor after moving multiple cursors would reset
23057 // the cursor count instead of adding to the existing cursors.
23058 init_test(cx, |_| {});
23059 let mut cx = EditorTestContext::new(cx).await;
23060
23061 // Create a simple buffer with cursor at start
23062 cx.set_state(indoc! {"
23063 ˇaaaa
23064 bbbb
23065 cccc
23066 dddd
23067 eeee
23068 ffff
23069 gggg
23070 hhhh"});
23071
23072 // Add 2 cursors below (so we have 3 total)
23073 cx.update_editor(|editor, window, cx| {
23074 editor.add_selection_below(&Default::default(), window, cx);
23075 editor.add_selection_below(&Default::default(), window, cx);
23076 });
23077
23078 // Verify we have 3 cursors
23079 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
23080 assert_eq!(
23081 initial_count, 3,
23082 "Should have 3 cursors after adding 2 below"
23083 );
23084
23085 // Move down one line
23086 cx.update_editor(|editor, window, cx| {
23087 editor.move_down(&MoveDown, window, cx);
23088 });
23089
23090 // Add another cursor below
23091 cx.update_editor(|editor, window, cx| {
23092 editor.add_selection_below(&Default::default(), window, cx);
23093 });
23094
23095 // Should now have 4 cursors (3 original + 1 new)
23096 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
23097 assert_eq!(
23098 final_count, 4,
23099 "Should have 4 cursors after moving and adding another"
23100 );
23101}
23102
23103#[gpui::test(iterations = 10)]
23104async fn test_document_colors(cx: &mut TestAppContext) {
23105 let expected_color = Rgba {
23106 r: 0.33,
23107 g: 0.33,
23108 b: 0.33,
23109 a: 0.33,
23110 };
23111
23112 init_test(cx, |_| {});
23113
23114 let fs = FakeFs::new(cx.executor());
23115 fs.insert_tree(
23116 path!("/a"),
23117 json!({
23118 "first.rs": "fn main() { let a = 5; }",
23119 }),
23120 )
23121 .await;
23122
23123 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23124 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23125 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23126
23127 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23128 language_registry.add(rust_lang());
23129 let mut fake_servers = language_registry.register_fake_lsp(
23130 "Rust",
23131 FakeLspAdapter {
23132 capabilities: lsp::ServerCapabilities {
23133 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
23134 ..lsp::ServerCapabilities::default()
23135 },
23136 name: "rust-analyzer",
23137 ..FakeLspAdapter::default()
23138 },
23139 );
23140 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
23141 "Rust",
23142 FakeLspAdapter {
23143 capabilities: lsp::ServerCapabilities {
23144 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
23145 ..lsp::ServerCapabilities::default()
23146 },
23147 name: "not-rust-analyzer",
23148 ..FakeLspAdapter::default()
23149 },
23150 );
23151
23152 let editor = workspace
23153 .update(cx, |workspace, window, cx| {
23154 workspace.open_abs_path(
23155 PathBuf::from(path!("/a/first.rs")),
23156 OpenOptions::default(),
23157 window,
23158 cx,
23159 )
23160 })
23161 .unwrap()
23162 .await
23163 .unwrap()
23164 .downcast::<Editor>()
23165 .unwrap();
23166 let fake_language_server = fake_servers.next().await.unwrap();
23167 let fake_language_server_without_capabilities =
23168 fake_servers_without_capabilities.next().await.unwrap();
23169 let requests_made = Arc::new(AtomicUsize::new(0));
23170 let closure_requests_made = Arc::clone(&requests_made);
23171 let mut color_request_handle = fake_language_server
23172 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
23173 let requests_made = Arc::clone(&closure_requests_made);
23174 async move {
23175 assert_eq!(
23176 params.text_document.uri,
23177 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
23178 );
23179 requests_made.fetch_add(1, atomic::Ordering::Release);
23180 Ok(vec![
23181 lsp::ColorInformation {
23182 range: lsp::Range {
23183 start: lsp::Position {
23184 line: 0,
23185 character: 0,
23186 },
23187 end: lsp::Position {
23188 line: 0,
23189 character: 1,
23190 },
23191 },
23192 color: lsp::Color {
23193 red: 0.33,
23194 green: 0.33,
23195 blue: 0.33,
23196 alpha: 0.33,
23197 },
23198 },
23199 lsp::ColorInformation {
23200 range: lsp::Range {
23201 start: lsp::Position {
23202 line: 0,
23203 character: 0,
23204 },
23205 end: lsp::Position {
23206 line: 0,
23207 character: 1,
23208 },
23209 },
23210 color: lsp::Color {
23211 red: 0.33,
23212 green: 0.33,
23213 blue: 0.33,
23214 alpha: 0.33,
23215 },
23216 },
23217 ])
23218 }
23219 });
23220
23221 let _handle = fake_language_server_without_capabilities
23222 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
23223 panic!("Should not be called");
23224 });
23225 cx.executor().advance_clock(Duration::from_millis(100));
23226 color_request_handle.next().await.unwrap();
23227 cx.run_until_parked();
23228 assert_eq!(
23229 1,
23230 requests_made.load(atomic::Ordering::Acquire),
23231 "Should query for colors once per editor open"
23232 );
23233 editor.update_in(cx, |editor, _, cx| {
23234 assert_eq!(
23235 vec![expected_color],
23236 extract_color_inlays(editor, cx),
23237 "Should have an initial inlay"
23238 );
23239 });
23240
23241 // opening another file in a split should not influence the LSP query counter
23242 workspace
23243 .update(cx, |workspace, window, cx| {
23244 assert_eq!(
23245 workspace.panes().len(),
23246 1,
23247 "Should have one pane with one editor"
23248 );
23249 workspace.move_item_to_pane_in_direction(
23250 &MoveItemToPaneInDirection {
23251 direction: SplitDirection::Right,
23252 focus: false,
23253 clone: true,
23254 },
23255 window,
23256 cx,
23257 );
23258 })
23259 .unwrap();
23260 cx.run_until_parked();
23261 workspace
23262 .update(cx, |workspace, _, cx| {
23263 let panes = workspace.panes();
23264 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
23265 for pane in panes {
23266 let editor = pane
23267 .read(cx)
23268 .active_item()
23269 .and_then(|item| item.downcast::<Editor>())
23270 .expect("Should have opened an editor in each split");
23271 let editor_file = editor
23272 .read(cx)
23273 .buffer()
23274 .read(cx)
23275 .as_singleton()
23276 .expect("test deals with singleton buffers")
23277 .read(cx)
23278 .file()
23279 .expect("test buffese should have a file")
23280 .path();
23281 assert_eq!(
23282 editor_file.as_ref(),
23283 Path::new("first.rs"),
23284 "Both editors should be opened for the same file"
23285 )
23286 }
23287 })
23288 .unwrap();
23289
23290 cx.executor().advance_clock(Duration::from_millis(500));
23291 let save = editor.update_in(cx, |editor, window, cx| {
23292 editor.move_to_end(&MoveToEnd, window, cx);
23293 editor.handle_input("dirty", window, cx);
23294 editor.save(
23295 SaveOptions {
23296 format: true,
23297 autosave: true,
23298 },
23299 project.clone(),
23300 window,
23301 cx,
23302 )
23303 });
23304 save.await.unwrap();
23305
23306 color_request_handle.next().await.unwrap();
23307 cx.run_until_parked();
23308 assert_eq!(
23309 3,
23310 requests_made.load(atomic::Ordering::Acquire),
23311 "Should query for colors once per save and once per formatting after save"
23312 );
23313
23314 drop(editor);
23315 let close = workspace
23316 .update(cx, |workspace, window, cx| {
23317 workspace.active_pane().update(cx, |pane, cx| {
23318 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23319 })
23320 })
23321 .unwrap();
23322 close.await.unwrap();
23323 let close = workspace
23324 .update(cx, |workspace, window, cx| {
23325 workspace.active_pane().update(cx, |pane, cx| {
23326 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23327 })
23328 })
23329 .unwrap();
23330 close.await.unwrap();
23331 assert_eq!(
23332 3,
23333 requests_made.load(atomic::Ordering::Acquire),
23334 "After saving and closing all editors, no extra requests should be made"
23335 );
23336 workspace
23337 .update(cx, |workspace, _, cx| {
23338 assert!(
23339 workspace.active_item(cx).is_none(),
23340 "Should close all editors"
23341 )
23342 })
23343 .unwrap();
23344
23345 workspace
23346 .update(cx, |workspace, window, cx| {
23347 workspace.active_pane().update(cx, |pane, cx| {
23348 pane.navigate_backward(window, cx);
23349 })
23350 })
23351 .unwrap();
23352 cx.executor().advance_clock(Duration::from_millis(100));
23353 cx.run_until_parked();
23354 let editor = workspace
23355 .update(cx, |workspace, _, cx| {
23356 workspace
23357 .active_item(cx)
23358 .expect("Should have reopened the editor again after navigating back")
23359 .downcast::<Editor>()
23360 .expect("Should be an editor")
23361 })
23362 .unwrap();
23363 color_request_handle.next().await.unwrap();
23364 assert_eq!(
23365 3,
23366 requests_made.load(atomic::Ordering::Acquire),
23367 "Cache should be reused on buffer close and reopen"
23368 );
23369 editor.update(cx, |editor, cx| {
23370 assert_eq!(
23371 vec![expected_color],
23372 extract_color_inlays(editor, cx),
23373 "Should have an initial inlay"
23374 );
23375 });
23376}
23377
23378#[gpui::test]
23379async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
23380 init_test(cx, |_| {});
23381 let (editor, cx) = cx.add_window_view(Editor::single_line);
23382 editor.update_in(cx, |editor, window, cx| {
23383 editor.set_text("oops\n\nwow\n", window, cx)
23384 });
23385 cx.run_until_parked();
23386 editor.update(cx, |editor, cx| {
23387 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
23388 });
23389 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
23390 cx.run_until_parked();
23391 editor.update(cx, |editor, cx| {
23392 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
23393 });
23394}
23395
23396#[track_caller]
23397fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
23398 editor
23399 .all_inlays(cx)
23400 .into_iter()
23401 .filter_map(|inlay| inlay.get_color())
23402 .map(Rgba::from)
23403 .collect()
23404}