1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 inline_completion_tests::FakeInlineCompletionProvider,
6 linked_editing_ranges::LinkedEditingRanges,
7 scroll::scroll_amount::ScrollAmount,
8 test::{
9 assert_text_with_selections, build_editor,
10 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
11 editor_test_context::EditorTestContext,
12 select_ranges,
13 },
14};
15use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
16use futures::StreamExt;
17use gpui::{
18 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
19 VisualTestContext, WindowBounds, WindowOptions, div,
20};
21use indoc::indoc;
22use language::{
23 BracketPairConfig,
24 Capability::ReadWrite,
25 DiagnosticSourceKind, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher,
26 LanguageName, Override, Point,
27 language_settings::{
28 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings, FormatterList,
29 LanguageSettingsContent, LspInsertMode, PrettierSettings, SelectedFormatter,
30 },
31 tree_sitter_python,
32};
33use language_settings::{Formatter, IndentGuideSettings};
34use lsp::CompletionParams;
35use multi_buffer::{IndentGuide, PathKey};
36use parking_lot::Mutex;
37use pretty_assertions::{assert_eq, assert_ne};
38use project::{
39 FakeFs,
40 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
41 project_settings::{LspSettings, ProjectSettings},
42};
43use serde_json::{self, json};
44use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
45use std::{
46 iter,
47 sync::atomic::{self, AtomicUsize},
48};
49use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
50use text::ToPoint as _;
51use unindent::Unindent;
52use util::{
53 assert_set_eq, path,
54 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
55 uri,
56};
57use workspace::{
58 CloseActiveItem, CloseAllItems, CloseInactiveItems, MoveItemToPaneInDirection, NavigationEntry,
59 OpenOptions, ViewId,
60 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
61};
62
63#[gpui::test]
64fn test_edit_events(cx: &mut TestAppContext) {
65 init_test(cx, |_| {});
66
67 let buffer = cx.new(|cx| {
68 let mut buffer = language::Buffer::local("123456", cx);
69 buffer.set_group_interval(Duration::from_secs(1));
70 buffer
71 });
72
73 let events = Rc::new(RefCell::new(Vec::new()));
74 let editor1 = cx.add_window({
75 let events = events.clone();
76 |window, cx| {
77 let entity = cx.entity().clone();
78 cx.subscribe_in(
79 &entity,
80 window,
81 move |_, _, event: &EditorEvent, _, _| match event {
82 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
83 EditorEvent::BufferEdited => {
84 events.borrow_mut().push(("editor1", "buffer edited"))
85 }
86 _ => {}
87 },
88 )
89 .detach();
90 Editor::for_buffer(buffer.clone(), None, window, cx)
91 }
92 });
93
94 let editor2 = cx.add_window({
95 let events = events.clone();
96 |window, cx| {
97 cx.subscribe_in(
98 &cx.entity().clone(),
99 window,
100 move |_, _, event: &EditorEvent, _, _| match event {
101 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
102 EditorEvent::BufferEdited => {
103 events.borrow_mut().push(("editor2", "buffer edited"))
104 }
105 _ => {}
106 },
107 )
108 .detach();
109 Editor::for_buffer(buffer.clone(), None, window, cx)
110 }
111 });
112
113 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
114
115 // Mutating editor 1 will emit an `Edited` event only for that editor.
116 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
117 assert_eq!(
118 mem::take(&mut *events.borrow_mut()),
119 [
120 ("editor1", "edited"),
121 ("editor1", "buffer edited"),
122 ("editor2", "buffer edited"),
123 ]
124 );
125
126 // Mutating editor 2 will emit an `Edited` event only for that editor.
127 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
128 assert_eq!(
129 mem::take(&mut *events.borrow_mut()),
130 [
131 ("editor2", "edited"),
132 ("editor1", "buffer edited"),
133 ("editor2", "buffer edited"),
134 ]
135 );
136
137 // Undoing on editor 1 will emit an `Edited` event only for that editor.
138 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
139 assert_eq!(
140 mem::take(&mut *events.borrow_mut()),
141 [
142 ("editor1", "edited"),
143 ("editor1", "buffer edited"),
144 ("editor2", "buffer edited"),
145 ]
146 );
147
148 // Redoing on editor 1 will emit an `Edited` event only for that editor.
149 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
150 assert_eq!(
151 mem::take(&mut *events.borrow_mut()),
152 [
153 ("editor1", "edited"),
154 ("editor1", "buffer edited"),
155 ("editor2", "buffer edited"),
156 ]
157 );
158
159 // Undoing on editor 2 will emit an `Edited` event only for that editor.
160 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
161 assert_eq!(
162 mem::take(&mut *events.borrow_mut()),
163 [
164 ("editor2", "edited"),
165 ("editor1", "buffer edited"),
166 ("editor2", "buffer edited"),
167 ]
168 );
169
170 // Redoing on editor 2 will emit an `Edited` event only for that editor.
171 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
172 assert_eq!(
173 mem::take(&mut *events.borrow_mut()),
174 [
175 ("editor2", "edited"),
176 ("editor1", "buffer edited"),
177 ("editor2", "buffer edited"),
178 ]
179 );
180
181 // No event is emitted when the mutation is a no-op.
182 _ = editor2.update(cx, |editor, window, cx| {
183 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
184 s.select_ranges([0..0])
185 });
186
187 editor.backspace(&Backspace, window, cx);
188 });
189 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
190}
191
192#[gpui::test]
193fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
194 init_test(cx, |_| {});
195
196 let mut now = Instant::now();
197 let group_interval = Duration::from_millis(1);
198 let buffer = cx.new(|cx| {
199 let mut buf = language::Buffer::local("123456", cx);
200 buf.set_group_interval(group_interval);
201 buf
202 });
203 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
204 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
205
206 _ = editor.update(cx, |editor, window, cx| {
207 editor.start_transaction_at(now, window, cx);
208 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
209 s.select_ranges([2..4])
210 });
211
212 editor.insert("cd", window, cx);
213 editor.end_transaction_at(now, cx);
214 assert_eq!(editor.text(cx), "12cd56");
215 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
216
217 editor.start_transaction_at(now, window, cx);
218 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
219 s.select_ranges([4..5])
220 });
221 editor.insert("e", window, cx);
222 editor.end_transaction_at(now, cx);
223 assert_eq!(editor.text(cx), "12cde6");
224 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
225
226 now += group_interval + Duration::from_millis(1);
227 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
228 s.select_ranges([2..2])
229 });
230
231 // Simulate an edit in another editor
232 buffer.update(cx, |buffer, cx| {
233 buffer.start_transaction_at(now, cx);
234 buffer.edit([(0..1, "a")], None, cx);
235 buffer.edit([(1..1, "b")], None, cx);
236 buffer.end_transaction_at(now, cx);
237 });
238
239 assert_eq!(editor.text(cx), "ab2cde6");
240 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
241
242 // Last transaction happened past the group interval in a different editor.
243 // Undo it individually and don't restore selections.
244 editor.undo(&Undo, window, cx);
245 assert_eq!(editor.text(cx), "12cde6");
246 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
247
248 // First two transactions happened within the group interval in this editor.
249 // Undo them together and restore selections.
250 editor.undo(&Undo, window, cx);
251 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
252 assert_eq!(editor.text(cx), "123456");
253 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
254
255 // Redo the first two transactions together.
256 editor.redo(&Redo, window, cx);
257 assert_eq!(editor.text(cx), "12cde6");
258 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
259
260 // Redo the last transaction on its own.
261 editor.redo(&Redo, window, cx);
262 assert_eq!(editor.text(cx), "ab2cde6");
263 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
264
265 // Test empty transactions.
266 editor.start_transaction_at(now, window, cx);
267 editor.end_transaction_at(now, cx);
268 editor.undo(&Undo, window, cx);
269 assert_eq!(editor.text(cx), "12cde6");
270 });
271}
272
273#[gpui::test]
274fn test_ime_composition(cx: &mut TestAppContext) {
275 init_test(cx, |_| {});
276
277 let buffer = cx.new(|cx| {
278 let mut buffer = language::Buffer::local("abcde", cx);
279 // Ensure automatic grouping doesn't occur.
280 buffer.set_group_interval(Duration::ZERO);
281 buffer
282 });
283
284 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
285 cx.add_window(|window, cx| {
286 let mut editor = build_editor(buffer.clone(), window, cx);
287
288 // Start a new IME composition.
289 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
290 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
291 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
292 assert_eq!(editor.text(cx), "äbcde");
293 assert_eq!(
294 editor.marked_text_ranges(cx),
295 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
296 );
297
298 // Finalize IME composition.
299 editor.replace_text_in_range(None, "ā", window, cx);
300 assert_eq!(editor.text(cx), "ābcde");
301 assert_eq!(editor.marked_text_ranges(cx), None);
302
303 // IME composition edits are grouped and are undone/redone at once.
304 editor.undo(&Default::default(), window, cx);
305 assert_eq!(editor.text(cx), "abcde");
306 assert_eq!(editor.marked_text_ranges(cx), None);
307 editor.redo(&Default::default(), window, cx);
308 assert_eq!(editor.text(cx), "ābcde");
309 assert_eq!(editor.marked_text_ranges(cx), None);
310
311 // Start a new IME composition.
312 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
313 assert_eq!(
314 editor.marked_text_ranges(cx),
315 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
316 );
317
318 // Undoing during an IME composition cancels it.
319 editor.undo(&Default::default(), window, cx);
320 assert_eq!(editor.text(cx), "ābcde");
321 assert_eq!(editor.marked_text_ranges(cx), None);
322
323 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
324 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
325 assert_eq!(editor.text(cx), "ābcdè");
326 assert_eq!(
327 editor.marked_text_ranges(cx),
328 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
329 );
330
331 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
332 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
333 assert_eq!(editor.text(cx), "ābcdę");
334 assert_eq!(editor.marked_text_ranges(cx), None);
335
336 // Start a new IME composition with multiple cursors.
337 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
338 s.select_ranges([
339 OffsetUtf16(1)..OffsetUtf16(1),
340 OffsetUtf16(3)..OffsetUtf16(3),
341 OffsetUtf16(5)..OffsetUtf16(5),
342 ])
343 });
344 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
345 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
346 assert_eq!(
347 editor.marked_text_ranges(cx),
348 Some(vec![
349 OffsetUtf16(0)..OffsetUtf16(3),
350 OffsetUtf16(4)..OffsetUtf16(7),
351 OffsetUtf16(8)..OffsetUtf16(11)
352 ])
353 );
354
355 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
356 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
357 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
358 assert_eq!(
359 editor.marked_text_ranges(cx),
360 Some(vec![
361 OffsetUtf16(1)..OffsetUtf16(2),
362 OffsetUtf16(5)..OffsetUtf16(6),
363 OffsetUtf16(9)..OffsetUtf16(10)
364 ])
365 );
366
367 // Finalize IME composition with multiple cursors.
368 editor.replace_text_in_range(Some(9..10), "2", window, cx);
369 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
370 assert_eq!(editor.marked_text_ranges(cx), None);
371
372 editor
373 });
374}
375
376#[gpui::test]
377fn test_selection_with_mouse(cx: &mut TestAppContext) {
378 init_test(cx, |_| {});
379
380 let editor = cx.add_window(|window, cx| {
381 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
382 build_editor(buffer, window, cx)
383 });
384
385 _ = editor.update(cx, |editor, window, cx| {
386 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
387 });
388 assert_eq!(
389 editor
390 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
391 .unwrap(),
392 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
393 );
394
395 _ = editor.update(cx, |editor, window, cx| {
396 editor.update_selection(
397 DisplayPoint::new(DisplayRow(3), 3),
398 0,
399 gpui::Point::<f32>::default(),
400 window,
401 cx,
402 );
403 });
404
405 assert_eq!(
406 editor
407 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
408 .unwrap(),
409 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
410 );
411
412 _ = editor.update(cx, |editor, window, cx| {
413 editor.update_selection(
414 DisplayPoint::new(DisplayRow(1), 1),
415 0,
416 gpui::Point::<f32>::default(),
417 window,
418 cx,
419 );
420 });
421
422 assert_eq!(
423 editor
424 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
425 .unwrap(),
426 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
427 );
428
429 _ = editor.update(cx, |editor, window, cx| {
430 editor.end_selection(window, cx);
431 editor.update_selection(
432 DisplayPoint::new(DisplayRow(3), 3),
433 0,
434 gpui::Point::<f32>::default(),
435 window,
436 cx,
437 );
438 });
439
440 assert_eq!(
441 editor
442 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
443 .unwrap(),
444 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
445 );
446
447 _ = editor.update(cx, |editor, window, cx| {
448 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
449 editor.update_selection(
450 DisplayPoint::new(DisplayRow(0), 0),
451 0,
452 gpui::Point::<f32>::default(),
453 window,
454 cx,
455 );
456 });
457
458 assert_eq!(
459 editor
460 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
461 .unwrap(),
462 [
463 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
464 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
465 ]
466 );
467
468 _ = editor.update(cx, |editor, window, cx| {
469 editor.end_selection(window, cx);
470 });
471
472 assert_eq!(
473 editor
474 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
475 .unwrap(),
476 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
477 );
478}
479
480#[gpui::test]
481fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
482 init_test(cx, |_| {});
483
484 let editor = cx.add_window(|window, cx| {
485 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
486 build_editor(buffer, window, cx)
487 });
488
489 _ = editor.update(cx, |editor, window, cx| {
490 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
491 });
492
493 _ = editor.update(cx, |editor, window, cx| {
494 editor.end_selection(window, cx);
495 });
496
497 _ = editor.update(cx, |editor, window, cx| {
498 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
499 });
500
501 _ = editor.update(cx, |editor, window, cx| {
502 editor.end_selection(window, cx);
503 });
504
505 assert_eq!(
506 editor
507 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
508 .unwrap(),
509 [
510 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
511 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
512 ]
513 );
514
515 _ = editor.update(cx, |editor, window, cx| {
516 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
517 });
518
519 _ = editor.update(cx, |editor, window, cx| {
520 editor.end_selection(window, cx);
521 });
522
523 assert_eq!(
524 editor
525 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
526 .unwrap(),
527 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
528 );
529}
530
531#[gpui::test]
532fn test_canceling_pending_selection(cx: &mut TestAppContext) {
533 init_test(cx, |_| {});
534
535 let editor = cx.add_window(|window, cx| {
536 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
537 build_editor(buffer, window, cx)
538 });
539
540 _ = editor.update(cx, |editor, window, cx| {
541 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
542 assert_eq!(
543 editor.selections.display_ranges(cx),
544 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
545 );
546 });
547
548 _ = editor.update(cx, |editor, window, cx| {
549 editor.update_selection(
550 DisplayPoint::new(DisplayRow(3), 3),
551 0,
552 gpui::Point::<f32>::default(),
553 window,
554 cx,
555 );
556 assert_eq!(
557 editor.selections.display_ranges(cx),
558 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
559 );
560 });
561
562 _ = editor.update(cx, |editor, window, cx| {
563 editor.cancel(&Cancel, window, cx);
564 editor.update_selection(
565 DisplayPoint::new(DisplayRow(1), 1),
566 0,
567 gpui::Point::<f32>::default(),
568 window,
569 cx,
570 );
571 assert_eq!(
572 editor.selections.display_ranges(cx),
573 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
574 );
575 });
576}
577
578#[gpui::test]
579fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
580 init_test(cx, |_| {});
581
582 let editor = cx.add_window(|window, cx| {
583 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
584 build_editor(buffer, window, cx)
585 });
586
587 _ = editor.update(cx, |editor, window, cx| {
588 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
589 assert_eq!(
590 editor.selections.display_ranges(cx),
591 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
592 );
593
594 editor.move_down(&Default::default(), window, cx);
595 assert_eq!(
596 editor.selections.display_ranges(cx),
597 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
598 );
599
600 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
601 assert_eq!(
602 editor.selections.display_ranges(cx),
603 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
604 );
605
606 editor.move_up(&Default::default(), window, cx);
607 assert_eq!(
608 editor.selections.display_ranges(cx),
609 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
610 );
611 });
612}
613
614#[gpui::test]
615fn test_clone(cx: &mut TestAppContext) {
616 init_test(cx, |_| {});
617
618 let (text, selection_ranges) = marked_text_ranges(
619 indoc! {"
620 one
621 two
622 threeˇ
623 four
624 fiveˇ
625 "},
626 true,
627 );
628
629 let editor = cx.add_window(|window, cx| {
630 let buffer = MultiBuffer::build_simple(&text, cx);
631 build_editor(buffer, window, cx)
632 });
633
634 _ = editor.update(cx, |editor, window, cx| {
635 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
636 s.select_ranges(selection_ranges.clone())
637 });
638 editor.fold_creases(
639 vec![
640 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
641 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
642 ],
643 true,
644 window,
645 cx,
646 );
647 });
648
649 let cloned_editor = editor
650 .update(cx, |editor, _, cx| {
651 cx.open_window(Default::default(), |window, cx| {
652 cx.new(|cx| editor.clone(window, cx))
653 })
654 })
655 .unwrap()
656 .unwrap();
657
658 let snapshot = editor
659 .update(cx, |e, window, cx| e.snapshot(window, cx))
660 .unwrap();
661 let cloned_snapshot = cloned_editor
662 .update(cx, |e, window, cx| e.snapshot(window, cx))
663 .unwrap();
664
665 assert_eq!(
666 cloned_editor
667 .update(cx, |e, _, cx| e.display_text(cx))
668 .unwrap(),
669 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
670 );
671 assert_eq!(
672 cloned_snapshot
673 .folds_in_range(0..text.len())
674 .collect::<Vec<_>>(),
675 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
676 );
677 assert_set_eq!(
678 cloned_editor
679 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
680 .unwrap(),
681 editor
682 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
683 .unwrap()
684 );
685 assert_set_eq!(
686 cloned_editor
687 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
688 .unwrap(),
689 editor
690 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
691 .unwrap()
692 );
693}
694
695#[gpui::test]
696async fn test_navigation_history(cx: &mut TestAppContext) {
697 init_test(cx, |_| {});
698
699 use workspace::item::Item;
700
701 let fs = FakeFs::new(cx.executor());
702 let project = Project::test(fs, [], cx).await;
703 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
704 let pane = workspace
705 .update(cx, |workspace, _, _| workspace.active_pane().clone())
706 .unwrap();
707
708 _ = workspace.update(cx, |_v, window, cx| {
709 cx.new(|cx| {
710 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
711 let mut editor = build_editor(buffer.clone(), window, cx);
712 let handle = cx.entity();
713 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
714
715 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
716 editor.nav_history.as_mut().unwrap().pop_backward(cx)
717 }
718
719 // Move the cursor a small distance.
720 // Nothing is added to the navigation history.
721 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
722 s.select_display_ranges([
723 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
724 ])
725 });
726 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
727 s.select_display_ranges([
728 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
729 ])
730 });
731 assert!(pop_history(&mut editor, cx).is_none());
732
733 // Move the cursor a large distance.
734 // The history can jump back to the previous position.
735 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
736 s.select_display_ranges([
737 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
738 ])
739 });
740 let nav_entry = pop_history(&mut editor, cx).unwrap();
741 editor.navigate(nav_entry.data.unwrap(), window, cx);
742 assert_eq!(nav_entry.item.id(), cx.entity_id());
743 assert_eq!(
744 editor.selections.display_ranges(cx),
745 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
746 );
747 assert!(pop_history(&mut editor, cx).is_none());
748
749 // Move the cursor a small distance via the mouse.
750 // Nothing is added to the navigation history.
751 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
752 editor.end_selection(window, cx);
753 assert_eq!(
754 editor.selections.display_ranges(cx),
755 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
756 );
757 assert!(pop_history(&mut editor, cx).is_none());
758
759 // Move the cursor a large distance via the mouse.
760 // The history can jump back to the previous position.
761 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
762 editor.end_selection(window, cx);
763 assert_eq!(
764 editor.selections.display_ranges(cx),
765 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
766 );
767 let nav_entry = pop_history(&mut editor, cx).unwrap();
768 editor.navigate(nav_entry.data.unwrap(), window, cx);
769 assert_eq!(nav_entry.item.id(), cx.entity_id());
770 assert_eq!(
771 editor.selections.display_ranges(cx),
772 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
773 );
774 assert!(pop_history(&mut editor, cx).is_none());
775
776 // Set scroll position to check later
777 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
778 let original_scroll_position = editor.scroll_manager.anchor();
779
780 // Jump to the end of the document and adjust scroll
781 editor.move_to_end(&MoveToEnd, window, cx);
782 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
783 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
784
785 let nav_entry = pop_history(&mut editor, cx).unwrap();
786 editor.navigate(nav_entry.data.unwrap(), window, cx);
787 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
788
789 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
790 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
791 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
792 let invalid_point = Point::new(9999, 0);
793 editor.navigate(
794 Box::new(NavigationData {
795 cursor_anchor: invalid_anchor,
796 cursor_position: invalid_point,
797 scroll_anchor: ScrollAnchor {
798 anchor: invalid_anchor,
799 offset: Default::default(),
800 },
801 scroll_top_row: invalid_point.row,
802 }),
803 window,
804 cx,
805 );
806 assert_eq!(
807 editor.selections.display_ranges(cx),
808 &[editor.max_point(cx)..editor.max_point(cx)]
809 );
810 assert_eq!(
811 editor.scroll_position(cx),
812 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
813 );
814
815 editor
816 })
817 });
818}
819
820#[gpui::test]
821fn test_cancel(cx: &mut TestAppContext) {
822 init_test(cx, |_| {});
823
824 let editor = cx.add_window(|window, cx| {
825 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
826 build_editor(buffer, window, cx)
827 });
828
829 _ = editor.update(cx, |editor, window, cx| {
830 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
831 editor.update_selection(
832 DisplayPoint::new(DisplayRow(1), 1),
833 0,
834 gpui::Point::<f32>::default(),
835 window,
836 cx,
837 );
838 editor.end_selection(window, cx);
839
840 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
841 editor.update_selection(
842 DisplayPoint::new(DisplayRow(0), 3),
843 0,
844 gpui::Point::<f32>::default(),
845 window,
846 cx,
847 );
848 editor.end_selection(window, cx);
849 assert_eq!(
850 editor.selections.display_ranges(cx),
851 [
852 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
853 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
854 ]
855 );
856 });
857
858 _ = editor.update(cx, |editor, window, cx| {
859 editor.cancel(&Cancel, window, cx);
860 assert_eq!(
861 editor.selections.display_ranges(cx),
862 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
863 );
864 });
865
866 _ = editor.update(cx, |editor, window, cx| {
867 editor.cancel(&Cancel, window, cx);
868 assert_eq!(
869 editor.selections.display_ranges(cx),
870 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
871 );
872 });
873}
874
875#[gpui::test]
876fn test_fold_action(cx: &mut TestAppContext) {
877 init_test(cx, |_| {});
878
879 let editor = cx.add_window(|window, cx| {
880 let buffer = MultiBuffer::build_simple(
881 &"
882 impl Foo {
883 // Hello!
884
885 fn a() {
886 1
887 }
888
889 fn b() {
890 2
891 }
892
893 fn c() {
894 3
895 }
896 }
897 "
898 .unindent(),
899 cx,
900 );
901 build_editor(buffer.clone(), window, cx)
902 });
903
904 _ = editor.update(cx, |editor, window, cx| {
905 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
906 s.select_display_ranges([
907 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
908 ]);
909 });
910 editor.fold(&Fold, window, cx);
911 assert_eq!(
912 editor.display_text(cx),
913 "
914 impl Foo {
915 // Hello!
916
917 fn a() {
918 1
919 }
920
921 fn b() {⋯
922 }
923
924 fn c() {⋯
925 }
926 }
927 "
928 .unindent(),
929 );
930
931 editor.fold(&Fold, window, cx);
932 assert_eq!(
933 editor.display_text(cx),
934 "
935 impl Foo {⋯
936 }
937 "
938 .unindent(),
939 );
940
941 editor.unfold_lines(&UnfoldLines, window, cx);
942 assert_eq!(
943 editor.display_text(cx),
944 "
945 impl Foo {
946 // Hello!
947
948 fn a() {
949 1
950 }
951
952 fn b() {⋯
953 }
954
955 fn c() {⋯
956 }
957 }
958 "
959 .unindent(),
960 );
961
962 editor.unfold_lines(&UnfoldLines, window, cx);
963 assert_eq!(
964 editor.display_text(cx),
965 editor.buffer.read(cx).read(cx).text()
966 );
967 });
968}
969
970#[gpui::test]
971fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
972 init_test(cx, |_| {});
973
974 let editor = cx.add_window(|window, cx| {
975 let buffer = MultiBuffer::build_simple(
976 &"
977 class Foo:
978 # Hello!
979
980 def a():
981 print(1)
982
983 def b():
984 print(2)
985
986 def c():
987 print(3)
988 "
989 .unindent(),
990 cx,
991 );
992 build_editor(buffer.clone(), window, cx)
993 });
994
995 _ = editor.update(cx, |editor, window, cx| {
996 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
997 s.select_display_ranges([
998 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
999 ]);
1000 });
1001 editor.fold(&Fold, window, cx);
1002 assert_eq!(
1003 editor.display_text(cx),
1004 "
1005 class Foo:
1006 # Hello!
1007
1008 def a():
1009 print(1)
1010
1011 def b():⋯
1012
1013 def c():⋯
1014 "
1015 .unindent(),
1016 );
1017
1018 editor.fold(&Fold, window, cx);
1019 assert_eq!(
1020 editor.display_text(cx),
1021 "
1022 class Foo:⋯
1023 "
1024 .unindent(),
1025 );
1026
1027 editor.unfold_lines(&UnfoldLines, window, cx);
1028 assert_eq!(
1029 editor.display_text(cx),
1030 "
1031 class Foo:
1032 # Hello!
1033
1034 def a():
1035 print(1)
1036
1037 def b():⋯
1038
1039 def c():⋯
1040 "
1041 .unindent(),
1042 );
1043
1044 editor.unfold_lines(&UnfoldLines, window, cx);
1045 assert_eq!(
1046 editor.display_text(cx),
1047 editor.buffer.read(cx).read(cx).text()
1048 );
1049 });
1050}
1051
1052#[gpui::test]
1053fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1054 init_test(cx, |_| {});
1055
1056 let editor = cx.add_window(|window, cx| {
1057 let buffer = MultiBuffer::build_simple(
1058 &"
1059 class Foo:
1060 # Hello!
1061
1062 def a():
1063 print(1)
1064
1065 def b():
1066 print(2)
1067
1068
1069 def c():
1070 print(3)
1071
1072
1073 "
1074 .unindent(),
1075 cx,
1076 );
1077 build_editor(buffer.clone(), window, cx)
1078 });
1079
1080 _ = editor.update(cx, |editor, window, cx| {
1081 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1082 s.select_display_ranges([
1083 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1084 ]);
1085 });
1086 editor.fold(&Fold, window, cx);
1087 assert_eq!(
1088 editor.display_text(cx),
1089 "
1090 class Foo:
1091 # Hello!
1092
1093 def a():
1094 print(1)
1095
1096 def b():⋯
1097
1098
1099 def c():⋯
1100
1101
1102 "
1103 .unindent(),
1104 );
1105
1106 editor.fold(&Fold, window, cx);
1107 assert_eq!(
1108 editor.display_text(cx),
1109 "
1110 class Foo:⋯
1111
1112
1113 "
1114 .unindent(),
1115 );
1116
1117 editor.unfold_lines(&UnfoldLines, window, cx);
1118 assert_eq!(
1119 editor.display_text(cx),
1120 "
1121 class Foo:
1122 # Hello!
1123
1124 def a():
1125 print(1)
1126
1127 def b():⋯
1128
1129
1130 def c():⋯
1131
1132
1133 "
1134 .unindent(),
1135 );
1136
1137 editor.unfold_lines(&UnfoldLines, window, cx);
1138 assert_eq!(
1139 editor.display_text(cx),
1140 editor.buffer.read(cx).read(cx).text()
1141 );
1142 });
1143}
1144
1145#[gpui::test]
1146fn test_fold_at_level(cx: &mut TestAppContext) {
1147 init_test(cx, |_| {});
1148
1149 let editor = cx.add_window(|window, cx| {
1150 let buffer = MultiBuffer::build_simple(
1151 &"
1152 class Foo:
1153 # Hello!
1154
1155 def a():
1156 print(1)
1157
1158 def b():
1159 print(2)
1160
1161
1162 class Bar:
1163 # World!
1164
1165 def a():
1166 print(1)
1167
1168 def b():
1169 print(2)
1170
1171
1172 "
1173 .unindent(),
1174 cx,
1175 );
1176 build_editor(buffer.clone(), window, cx)
1177 });
1178
1179 _ = editor.update(cx, |editor, window, cx| {
1180 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1181 assert_eq!(
1182 editor.display_text(cx),
1183 "
1184 class Foo:
1185 # Hello!
1186
1187 def a():⋯
1188
1189 def b():⋯
1190
1191
1192 class Bar:
1193 # World!
1194
1195 def a():⋯
1196
1197 def b():⋯
1198
1199
1200 "
1201 .unindent(),
1202 );
1203
1204 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1205 assert_eq!(
1206 editor.display_text(cx),
1207 "
1208 class Foo:⋯
1209
1210
1211 class Bar:⋯
1212
1213
1214 "
1215 .unindent(),
1216 );
1217
1218 editor.unfold_all(&UnfoldAll, window, cx);
1219 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1220 assert_eq!(
1221 editor.display_text(cx),
1222 "
1223 class Foo:
1224 # Hello!
1225
1226 def a():
1227 print(1)
1228
1229 def b():
1230 print(2)
1231
1232
1233 class Bar:
1234 # World!
1235
1236 def a():
1237 print(1)
1238
1239 def b():
1240 print(2)
1241
1242
1243 "
1244 .unindent(),
1245 );
1246
1247 assert_eq!(
1248 editor.display_text(cx),
1249 editor.buffer.read(cx).read(cx).text()
1250 );
1251 });
1252}
1253
1254#[gpui::test]
1255fn test_move_cursor(cx: &mut TestAppContext) {
1256 init_test(cx, |_| {});
1257
1258 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1259 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1260
1261 buffer.update(cx, |buffer, cx| {
1262 buffer.edit(
1263 vec![
1264 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1265 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1266 ],
1267 None,
1268 cx,
1269 );
1270 });
1271 _ = editor.update(cx, |editor, window, cx| {
1272 assert_eq!(
1273 editor.selections.display_ranges(cx),
1274 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1275 );
1276
1277 editor.move_down(&MoveDown, window, cx);
1278 assert_eq!(
1279 editor.selections.display_ranges(cx),
1280 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1281 );
1282
1283 editor.move_right(&MoveRight, window, cx);
1284 assert_eq!(
1285 editor.selections.display_ranges(cx),
1286 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1287 );
1288
1289 editor.move_left(&MoveLeft, window, cx);
1290 assert_eq!(
1291 editor.selections.display_ranges(cx),
1292 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1293 );
1294
1295 editor.move_up(&MoveUp, window, cx);
1296 assert_eq!(
1297 editor.selections.display_ranges(cx),
1298 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1299 );
1300
1301 editor.move_to_end(&MoveToEnd, window, cx);
1302 assert_eq!(
1303 editor.selections.display_ranges(cx),
1304 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1305 );
1306
1307 editor.move_to_beginning(&MoveToBeginning, window, cx);
1308 assert_eq!(
1309 editor.selections.display_ranges(cx),
1310 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1311 );
1312
1313 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1314 s.select_display_ranges([
1315 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1316 ]);
1317 });
1318 editor.select_to_beginning(&SelectToBeginning, window, cx);
1319 assert_eq!(
1320 editor.selections.display_ranges(cx),
1321 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1322 );
1323
1324 editor.select_to_end(&SelectToEnd, window, cx);
1325 assert_eq!(
1326 editor.selections.display_ranges(cx),
1327 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1328 );
1329 });
1330}
1331
1332#[gpui::test]
1333fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1334 init_test(cx, |_| {});
1335
1336 let editor = cx.add_window(|window, cx| {
1337 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1338 build_editor(buffer.clone(), window, cx)
1339 });
1340
1341 assert_eq!('🟥'.len_utf8(), 4);
1342 assert_eq!('α'.len_utf8(), 2);
1343
1344 _ = editor.update(cx, |editor, window, cx| {
1345 editor.fold_creases(
1346 vec![
1347 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1348 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1349 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1350 ],
1351 true,
1352 window,
1353 cx,
1354 );
1355 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1356
1357 editor.move_right(&MoveRight, window, cx);
1358 assert_eq!(
1359 editor.selections.display_ranges(cx),
1360 &[empty_range(0, "🟥".len())]
1361 );
1362 editor.move_right(&MoveRight, window, cx);
1363 assert_eq!(
1364 editor.selections.display_ranges(cx),
1365 &[empty_range(0, "🟥🟧".len())]
1366 );
1367 editor.move_right(&MoveRight, window, cx);
1368 assert_eq!(
1369 editor.selections.display_ranges(cx),
1370 &[empty_range(0, "🟥🟧⋯".len())]
1371 );
1372
1373 editor.move_down(&MoveDown, window, cx);
1374 assert_eq!(
1375 editor.selections.display_ranges(cx),
1376 &[empty_range(1, "ab⋯e".len())]
1377 );
1378 editor.move_left(&MoveLeft, window, cx);
1379 assert_eq!(
1380 editor.selections.display_ranges(cx),
1381 &[empty_range(1, "ab⋯".len())]
1382 );
1383 editor.move_left(&MoveLeft, window, cx);
1384 assert_eq!(
1385 editor.selections.display_ranges(cx),
1386 &[empty_range(1, "ab".len())]
1387 );
1388 editor.move_left(&MoveLeft, window, cx);
1389 assert_eq!(
1390 editor.selections.display_ranges(cx),
1391 &[empty_range(1, "a".len())]
1392 );
1393
1394 editor.move_down(&MoveDown, window, cx);
1395 assert_eq!(
1396 editor.selections.display_ranges(cx),
1397 &[empty_range(2, "α".len())]
1398 );
1399 editor.move_right(&MoveRight, window, cx);
1400 assert_eq!(
1401 editor.selections.display_ranges(cx),
1402 &[empty_range(2, "αβ".len())]
1403 );
1404 editor.move_right(&MoveRight, window, cx);
1405 assert_eq!(
1406 editor.selections.display_ranges(cx),
1407 &[empty_range(2, "αβ⋯".len())]
1408 );
1409 editor.move_right(&MoveRight, window, cx);
1410 assert_eq!(
1411 editor.selections.display_ranges(cx),
1412 &[empty_range(2, "αβ⋯ε".len())]
1413 );
1414
1415 editor.move_up(&MoveUp, window, cx);
1416 assert_eq!(
1417 editor.selections.display_ranges(cx),
1418 &[empty_range(1, "ab⋯e".len())]
1419 );
1420 editor.move_down(&MoveDown, window, cx);
1421 assert_eq!(
1422 editor.selections.display_ranges(cx),
1423 &[empty_range(2, "αβ⋯ε".len())]
1424 );
1425 editor.move_up(&MoveUp, window, cx);
1426 assert_eq!(
1427 editor.selections.display_ranges(cx),
1428 &[empty_range(1, "ab⋯e".len())]
1429 );
1430
1431 editor.move_up(&MoveUp, window, cx);
1432 assert_eq!(
1433 editor.selections.display_ranges(cx),
1434 &[empty_range(0, "🟥🟧".len())]
1435 );
1436 editor.move_left(&MoveLeft, window, cx);
1437 assert_eq!(
1438 editor.selections.display_ranges(cx),
1439 &[empty_range(0, "🟥".len())]
1440 );
1441 editor.move_left(&MoveLeft, window, cx);
1442 assert_eq!(
1443 editor.selections.display_ranges(cx),
1444 &[empty_range(0, "".len())]
1445 );
1446 });
1447}
1448
1449#[gpui::test]
1450fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1451 init_test(cx, |_| {});
1452
1453 let editor = cx.add_window(|window, cx| {
1454 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1455 build_editor(buffer.clone(), window, cx)
1456 });
1457 _ = editor.update(cx, |editor, window, cx| {
1458 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1459 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1460 });
1461
1462 // moving above start of document should move selection to start of document,
1463 // but the next move down should still be at the original goal_x
1464 editor.move_up(&MoveUp, window, cx);
1465 assert_eq!(
1466 editor.selections.display_ranges(cx),
1467 &[empty_range(0, "".len())]
1468 );
1469
1470 editor.move_down(&MoveDown, window, cx);
1471 assert_eq!(
1472 editor.selections.display_ranges(cx),
1473 &[empty_range(1, "abcd".len())]
1474 );
1475
1476 editor.move_down(&MoveDown, window, cx);
1477 assert_eq!(
1478 editor.selections.display_ranges(cx),
1479 &[empty_range(2, "αβγ".len())]
1480 );
1481
1482 editor.move_down(&MoveDown, window, cx);
1483 assert_eq!(
1484 editor.selections.display_ranges(cx),
1485 &[empty_range(3, "abcd".len())]
1486 );
1487
1488 editor.move_down(&MoveDown, window, cx);
1489 assert_eq!(
1490 editor.selections.display_ranges(cx),
1491 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1492 );
1493
1494 // moving past end of document should not change goal_x
1495 editor.move_down(&MoveDown, window, cx);
1496 assert_eq!(
1497 editor.selections.display_ranges(cx),
1498 &[empty_range(5, "".len())]
1499 );
1500
1501 editor.move_down(&MoveDown, window, cx);
1502 assert_eq!(
1503 editor.selections.display_ranges(cx),
1504 &[empty_range(5, "".len())]
1505 );
1506
1507 editor.move_up(&MoveUp, window, cx);
1508 assert_eq!(
1509 editor.selections.display_ranges(cx),
1510 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1511 );
1512
1513 editor.move_up(&MoveUp, window, cx);
1514 assert_eq!(
1515 editor.selections.display_ranges(cx),
1516 &[empty_range(3, "abcd".len())]
1517 );
1518
1519 editor.move_up(&MoveUp, window, cx);
1520 assert_eq!(
1521 editor.selections.display_ranges(cx),
1522 &[empty_range(2, "αβγ".len())]
1523 );
1524 });
1525}
1526
1527#[gpui::test]
1528fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1529 init_test(cx, |_| {});
1530 let move_to_beg = MoveToBeginningOfLine {
1531 stop_at_soft_wraps: true,
1532 stop_at_indent: true,
1533 };
1534
1535 let delete_to_beg = DeleteToBeginningOfLine {
1536 stop_at_indent: false,
1537 };
1538
1539 let move_to_end = MoveToEndOfLine {
1540 stop_at_soft_wraps: true,
1541 };
1542
1543 let editor = cx.add_window(|window, cx| {
1544 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1545 build_editor(buffer, window, cx)
1546 });
1547 _ = editor.update(cx, |editor, window, cx| {
1548 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1549 s.select_display_ranges([
1550 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1551 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1552 ]);
1553 });
1554 });
1555
1556 _ = editor.update(cx, |editor, window, cx| {
1557 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1558 assert_eq!(
1559 editor.selections.display_ranges(cx),
1560 &[
1561 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1562 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1563 ]
1564 );
1565 });
1566
1567 _ = editor.update(cx, |editor, window, cx| {
1568 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1569 assert_eq!(
1570 editor.selections.display_ranges(cx),
1571 &[
1572 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1573 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1574 ]
1575 );
1576 });
1577
1578 _ = editor.update(cx, |editor, window, cx| {
1579 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1580 assert_eq!(
1581 editor.selections.display_ranges(cx),
1582 &[
1583 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1584 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1585 ]
1586 );
1587 });
1588
1589 _ = editor.update(cx, |editor, window, cx| {
1590 editor.move_to_end_of_line(&move_to_end, window, cx);
1591 assert_eq!(
1592 editor.selections.display_ranges(cx),
1593 &[
1594 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1595 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1596 ]
1597 );
1598 });
1599
1600 // Moving to the end of line again is a no-op.
1601 _ = editor.update(cx, |editor, window, cx| {
1602 editor.move_to_end_of_line(&move_to_end, window, cx);
1603 assert_eq!(
1604 editor.selections.display_ranges(cx),
1605 &[
1606 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1607 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1608 ]
1609 );
1610 });
1611
1612 _ = editor.update(cx, |editor, window, cx| {
1613 editor.move_left(&MoveLeft, window, cx);
1614 editor.select_to_beginning_of_line(
1615 &SelectToBeginningOfLine {
1616 stop_at_soft_wraps: true,
1617 stop_at_indent: true,
1618 },
1619 window,
1620 cx,
1621 );
1622 assert_eq!(
1623 editor.selections.display_ranges(cx),
1624 &[
1625 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1626 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1627 ]
1628 );
1629 });
1630
1631 _ = editor.update(cx, |editor, window, cx| {
1632 editor.select_to_beginning_of_line(
1633 &SelectToBeginningOfLine {
1634 stop_at_soft_wraps: true,
1635 stop_at_indent: true,
1636 },
1637 window,
1638 cx,
1639 );
1640 assert_eq!(
1641 editor.selections.display_ranges(cx),
1642 &[
1643 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1644 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1645 ]
1646 );
1647 });
1648
1649 _ = editor.update(cx, |editor, window, cx| {
1650 editor.select_to_beginning_of_line(
1651 &SelectToBeginningOfLine {
1652 stop_at_soft_wraps: true,
1653 stop_at_indent: true,
1654 },
1655 window,
1656 cx,
1657 );
1658 assert_eq!(
1659 editor.selections.display_ranges(cx),
1660 &[
1661 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1662 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1663 ]
1664 );
1665 });
1666
1667 _ = editor.update(cx, |editor, window, cx| {
1668 editor.select_to_end_of_line(
1669 &SelectToEndOfLine {
1670 stop_at_soft_wraps: true,
1671 },
1672 window,
1673 cx,
1674 );
1675 assert_eq!(
1676 editor.selections.display_ranges(cx),
1677 &[
1678 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1679 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1680 ]
1681 );
1682 });
1683
1684 _ = editor.update(cx, |editor, window, cx| {
1685 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1686 assert_eq!(editor.display_text(cx), "ab\n de");
1687 assert_eq!(
1688 editor.selections.display_ranges(cx),
1689 &[
1690 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1691 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1692 ]
1693 );
1694 });
1695
1696 _ = editor.update(cx, |editor, window, cx| {
1697 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1698 assert_eq!(editor.display_text(cx), "\n");
1699 assert_eq!(
1700 editor.selections.display_ranges(cx),
1701 &[
1702 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1703 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1704 ]
1705 );
1706 });
1707}
1708
1709#[gpui::test]
1710fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1711 init_test(cx, |_| {});
1712 let move_to_beg = MoveToBeginningOfLine {
1713 stop_at_soft_wraps: false,
1714 stop_at_indent: false,
1715 };
1716
1717 let move_to_end = MoveToEndOfLine {
1718 stop_at_soft_wraps: false,
1719 };
1720
1721 let editor = cx.add_window(|window, cx| {
1722 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1723 build_editor(buffer, window, cx)
1724 });
1725
1726 _ = editor.update(cx, |editor, window, cx| {
1727 editor.set_wrap_width(Some(140.0.into()), cx);
1728
1729 // We expect the following lines after wrapping
1730 // ```
1731 // thequickbrownfox
1732 // jumpedoverthelazydo
1733 // gs
1734 // ```
1735 // The final `gs` was soft-wrapped onto a new line.
1736 assert_eq!(
1737 "thequickbrownfox\njumpedoverthelaz\nydogs",
1738 editor.display_text(cx),
1739 );
1740
1741 // First, let's assert behavior on the first line, that was not soft-wrapped.
1742 // Start the cursor at the `k` on the first line
1743 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1744 s.select_display_ranges([
1745 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1746 ]);
1747 });
1748
1749 // Moving to the beginning of the line should put us at the beginning of the line.
1750 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1751 assert_eq!(
1752 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1753 editor.selections.display_ranges(cx)
1754 );
1755
1756 // Moving to the end of the line should put us at the end of the line.
1757 editor.move_to_end_of_line(&move_to_end, window, cx);
1758 assert_eq!(
1759 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1760 editor.selections.display_ranges(cx)
1761 );
1762
1763 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1764 // Start the cursor at the last line (`y` that was wrapped to a new line)
1765 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1766 s.select_display_ranges([
1767 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1768 ]);
1769 });
1770
1771 // Moving to the beginning of the line should put us at the start of the second line of
1772 // display text, i.e., the `j`.
1773 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1774 assert_eq!(
1775 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1776 editor.selections.display_ranges(cx)
1777 );
1778
1779 // Moving to the beginning of the line again should be a no-op.
1780 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1781 assert_eq!(
1782 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1783 editor.selections.display_ranges(cx)
1784 );
1785
1786 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1787 // next display line.
1788 editor.move_to_end_of_line(&move_to_end, window, cx);
1789 assert_eq!(
1790 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1791 editor.selections.display_ranges(cx)
1792 );
1793
1794 // Moving to the end of the line again should be a no-op.
1795 editor.move_to_end_of_line(&move_to_end, window, cx);
1796 assert_eq!(
1797 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1798 editor.selections.display_ranges(cx)
1799 );
1800 });
1801}
1802
1803#[gpui::test]
1804fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1805 init_test(cx, |_| {});
1806
1807 let move_to_beg = MoveToBeginningOfLine {
1808 stop_at_soft_wraps: true,
1809 stop_at_indent: true,
1810 };
1811
1812 let select_to_beg = SelectToBeginningOfLine {
1813 stop_at_soft_wraps: true,
1814 stop_at_indent: true,
1815 };
1816
1817 let delete_to_beg = DeleteToBeginningOfLine {
1818 stop_at_indent: true,
1819 };
1820
1821 let move_to_end = MoveToEndOfLine {
1822 stop_at_soft_wraps: false,
1823 };
1824
1825 let editor = cx.add_window(|window, cx| {
1826 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1827 build_editor(buffer, window, cx)
1828 });
1829
1830 _ = editor.update(cx, |editor, window, cx| {
1831 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1832 s.select_display_ranges([
1833 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1834 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1835 ]);
1836 });
1837
1838 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1839 // and the second cursor at the first non-whitespace character in the line.
1840 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1841 assert_eq!(
1842 editor.selections.display_ranges(cx),
1843 &[
1844 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1845 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1846 ]
1847 );
1848
1849 // Moving to the beginning of the line again should be a no-op for the first cursor,
1850 // and should move the second cursor to the beginning of the line.
1851 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1852 assert_eq!(
1853 editor.selections.display_ranges(cx),
1854 &[
1855 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1856 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1857 ]
1858 );
1859
1860 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1861 // and should move the second cursor back to the first non-whitespace character in the line.
1862 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1863 assert_eq!(
1864 editor.selections.display_ranges(cx),
1865 &[
1866 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1867 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1868 ]
1869 );
1870
1871 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1872 // and to the first non-whitespace character in the line for the second cursor.
1873 editor.move_to_end_of_line(&move_to_end, window, cx);
1874 editor.move_left(&MoveLeft, window, cx);
1875 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1876 assert_eq!(
1877 editor.selections.display_ranges(cx),
1878 &[
1879 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1880 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1881 ]
1882 );
1883
1884 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1885 // and should select to the beginning of the line for the second cursor.
1886 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1887 assert_eq!(
1888 editor.selections.display_ranges(cx),
1889 &[
1890 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1891 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1892 ]
1893 );
1894
1895 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1896 // and should delete to the first non-whitespace character in the line for the second cursor.
1897 editor.move_to_end_of_line(&move_to_end, window, cx);
1898 editor.move_left(&MoveLeft, window, cx);
1899 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1900 assert_eq!(editor.text(cx), "c\n f");
1901 });
1902}
1903
1904#[gpui::test]
1905fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1906 init_test(cx, |_| {});
1907
1908 let editor = cx.add_window(|window, cx| {
1909 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1910 build_editor(buffer, window, cx)
1911 });
1912 _ = editor.update(cx, |editor, window, cx| {
1913 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1914 s.select_display_ranges([
1915 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1916 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1917 ])
1918 });
1919 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1920 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1921
1922 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1923 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1924
1925 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1926 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1927
1928 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1929 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1930
1931 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1932 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
1933
1934 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1935 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1936
1937 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1938 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1939
1940 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1941 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1942
1943 editor.move_right(&MoveRight, window, cx);
1944 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1945 assert_selection_ranges(
1946 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
1947 editor,
1948 cx,
1949 );
1950
1951 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1952 assert_selection_ranges(
1953 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
1954 editor,
1955 cx,
1956 );
1957
1958 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1959 assert_selection_ranges(
1960 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
1961 editor,
1962 cx,
1963 );
1964 });
1965}
1966
1967#[gpui::test]
1968fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1969 init_test(cx, |_| {});
1970
1971 let editor = cx.add_window(|window, cx| {
1972 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1973 build_editor(buffer, window, cx)
1974 });
1975
1976 _ = editor.update(cx, |editor, window, cx| {
1977 editor.set_wrap_width(Some(140.0.into()), cx);
1978 assert_eq!(
1979 editor.display_text(cx),
1980 "use one::{\n two::three::\n four::five\n};"
1981 );
1982
1983 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1984 s.select_display_ranges([
1985 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1986 ]);
1987 });
1988
1989 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1990 assert_eq!(
1991 editor.selections.display_ranges(cx),
1992 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1993 );
1994
1995 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1996 assert_eq!(
1997 editor.selections.display_ranges(cx),
1998 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1999 );
2000
2001 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2002 assert_eq!(
2003 editor.selections.display_ranges(cx),
2004 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2005 );
2006
2007 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2008 assert_eq!(
2009 editor.selections.display_ranges(cx),
2010 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2011 );
2012
2013 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2014 assert_eq!(
2015 editor.selections.display_ranges(cx),
2016 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2017 );
2018
2019 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2020 assert_eq!(
2021 editor.selections.display_ranges(cx),
2022 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2023 );
2024 });
2025}
2026
2027#[gpui::test]
2028async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2029 init_test(cx, |_| {});
2030 let mut cx = EditorTestContext::new(cx).await;
2031
2032 let line_height = cx.editor(|editor, window, _| {
2033 editor
2034 .style()
2035 .unwrap()
2036 .text
2037 .line_height_in_pixels(window.rem_size())
2038 });
2039 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2040
2041 cx.set_state(
2042 &r#"ˇone
2043 two
2044
2045 three
2046 fourˇ
2047 five
2048
2049 six"#
2050 .unindent(),
2051 );
2052
2053 cx.update_editor(|editor, window, cx| {
2054 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2055 });
2056 cx.assert_editor_state(
2057 &r#"one
2058 two
2059 ˇ
2060 three
2061 four
2062 five
2063 ˇ
2064 six"#
2065 .unindent(),
2066 );
2067
2068 cx.update_editor(|editor, window, cx| {
2069 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2070 });
2071 cx.assert_editor_state(
2072 &r#"one
2073 two
2074
2075 three
2076 four
2077 five
2078 ˇ
2079 sixˇ"#
2080 .unindent(),
2081 );
2082
2083 cx.update_editor(|editor, window, cx| {
2084 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2085 });
2086 cx.assert_editor_state(
2087 &r#"one
2088 two
2089
2090 three
2091 four
2092 five
2093
2094 sixˇ"#
2095 .unindent(),
2096 );
2097
2098 cx.update_editor(|editor, window, cx| {
2099 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2100 });
2101 cx.assert_editor_state(
2102 &r#"one
2103 two
2104
2105 three
2106 four
2107 five
2108 ˇ
2109 six"#
2110 .unindent(),
2111 );
2112
2113 cx.update_editor(|editor, window, cx| {
2114 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2115 });
2116 cx.assert_editor_state(
2117 &r#"one
2118 two
2119 ˇ
2120 three
2121 four
2122 five
2123
2124 six"#
2125 .unindent(),
2126 );
2127
2128 cx.update_editor(|editor, window, cx| {
2129 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2130 });
2131 cx.assert_editor_state(
2132 &r#"ˇone
2133 two
2134
2135 three
2136 four
2137 five
2138
2139 six"#
2140 .unindent(),
2141 );
2142}
2143
2144#[gpui::test]
2145async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2146 init_test(cx, |_| {});
2147 let mut cx = EditorTestContext::new(cx).await;
2148 let line_height = cx.editor(|editor, window, _| {
2149 editor
2150 .style()
2151 .unwrap()
2152 .text
2153 .line_height_in_pixels(window.rem_size())
2154 });
2155 let window = cx.window;
2156 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2157
2158 cx.set_state(
2159 r#"ˇone
2160 two
2161 three
2162 four
2163 five
2164 six
2165 seven
2166 eight
2167 nine
2168 ten
2169 "#,
2170 );
2171
2172 cx.update_editor(|editor, window, cx| {
2173 assert_eq!(
2174 editor.snapshot(window, cx).scroll_position(),
2175 gpui::Point::new(0., 0.)
2176 );
2177 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2178 assert_eq!(
2179 editor.snapshot(window, cx).scroll_position(),
2180 gpui::Point::new(0., 3.)
2181 );
2182 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2183 assert_eq!(
2184 editor.snapshot(window, cx).scroll_position(),
2185 gpui::Point::new(0., 6.)
2186 );
2187 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2188 assert_eq!(
2189 editor.snapshot(window, cx).scroll_position(),
2190 gpui::Point::new(0., 3.)
2191 );
2192
2193 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2194 assert_eq!(
2195 editor.snapshot(window, cx).scroll_position(),
2196 gpui::Point::new(0., 1.)
2197 );
2198 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2199 assert_eq!(
2200 editor.snapshot(window, cx).scroll_position(),
2201 gpui::Point::new(0., 3.)
2202 );
2203 });
2204}
2205
2206#[gpui::test]
2207async fn test_autoscroll(cx: &mut TestAppContext) {
2208 init_test(cx, |_| {});
2209 let mut cx = EditorTestContext::new(cx).await;
2210
2211 let line_height = cx.update_editor(|editor, window, cx| {
2212 editor.set_vertical_scroll_margin(2, cx);
2213 editor
2214 .style()
2215 .unwrap()
2216 .text
2217 .line_height_in_pixels(window.rem_size())
2218 });
2219 let window = cx.window;
2220 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2221
2222 cx.set_state(
2223 r#"ˇone
2224 two
2225 three
2226 four
2227 five
2228 six
2229 seven
2230 eight
2231 nine
2232 ten
2233 "#,
2234 );
2235 cx.update_editor(|editor, window, cx| {
2236 assert_eq!(
2237 editor.snapshot(window, cx).scroll_position(),
2238 gpui::Point::new(0., 0.0)
2239 );
2240 });
2241
2242 // Add a cursor below the visible area. Since both cursors cannot fit
2243 // on screen, the editor autoscrolls to reveal the newest cursor, and
2244 // allows the vertical scroll margin below that cursor.
2245 cx.update_editor(|editor, window, cx| {
2246 editor.change_selections(Default::default(), window, cx, |selections| {
2247 selections.select_ranges([
2248 Point::new(0, 0)..Point::new(0, 0),
2249 Point::new(6, 0)..Point::new(6, 0),
2250 ]);
2251 })
2252 });
2253 cx.update_editor(|editor, window, cx| {
2254 assert_eq!(
2255 editor.snapshot(window, cx).scroll_position(),
2256 gpui::Point::new(0., 3.0)
2257 );
2258 });
2259
2260 // Move down. The editor cursor scrolls down to track the newest cursor.
2261 cx.update_editor(|editor, window, cx| {
2262 editor.move_down(&Default::default(), window, cx);
2263 });
2264 cx.update_editor(|editor, window, cx| {
2265 assert_eq!(
2266 editor.snapshot(window, cx).scroll_position(),
2267 gpui::Point::new(0., 4.0)
2268 );
2269 });
2270
2271 // Add a cursor above the visible area. Since both cursors fit on screen,
2272 // the editor scrolls to show both.
2273 cx.update_editor(|editor, window, cx| {
2274 editor.change_selections(Default::default(), window, cx, |selections| {
2275 selections.select_ranges([
2276 Point::new(1, 0)..Point::new(1, 0),
2277 Point::new(6, 0)..Point::new(6, 0),
2278 ]);
2279 })
2280 });
2281 cx.update_editor(|editor, window, cx| {
2282 assert_eq!(
2283 editor.snapshot(window, cx).scroll_position(),
2284 gpui::Point::new(0., 1.0)
2285 );
2286 });
2287}
2288
2289#[gpui::test]
2290async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2291 init_test(cx, |_| {});
2292 let mut cx = EditorTestContext::new(cx).await;
2293
2294 let line_height = cx.editor(|editor, window, _cx| {
2295 editor
2296 .style()
2297 .unwrap()
2298 .text
2299 .line_height_in_pixels(window.rem_size())
2300 });
2301 let window = cx.window;
2302 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2303 cx.set_state(
2304 &r#"
2305 ˇone
2306 two
2307 threeˇ
2308 four
2309 five
2310 six
2311 seven
2312 eight
2313 nine
2314 ten
2315 "#
2316 .unindent(),
2317 );
2318
2319 cx.update_editor(|editor, window, cx| {
2320 editor.move_page_down(&MovePageDown::default(), window, cx)
2321 });
2322 cx.assert_editor_state(
2323 &r#"
2324 one
2325 two
2326 three
2327 ˇfour
2328 five
2329 sixˇ
2330 seven
2331 eight
2332 nine
2333 ten
2334 "#
2335 .unindent(),
2336 );
2337
2338 cx.update_editor(|editor, window, cx| {
2339 editor.move_page_down(&MovePageDown::default(), window, cx)
2340 });
2341 cx.assert_editor_state(
2342 &r#"
2343 one
2344 two
2345 three
2346 four
2347 five
2348 six
2349 ˇseven
2350 eight
2351 nineˇ
2352 ten
2353 "#
2354 .unindent(),
2355 );
2356
2357 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2358 cx.assert_editor_state(
2359 &r#"
2360 one
2361 two
2362 three
2363 ˇfour
2364 five
2365 sixˇ
2366 seven
2367 eight
2368 nine
2369 ten
2370 "#
2371 .unindent(),
2372 );
2373
2374 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2375 cx.assert_editor_state(
2376 &r#"
2377 ˇone
2378 two
2379 threeˇ
2380 four
2381 five
2382 six
2383 seven
2384 eight
2385 nine
2386 ten
2387 "#
2388 .unindent(),
2389 );
2390
2391 // Test select collapsing
2392 cx.update_editor(|editor, window, cx| {
2393 editor.move_page_down(&MovePageDown::default(), window, cx);
2394 editor.move_page_down(&MovePageDown::default(), window, cx);
2395 editor.move_page_down(&MovePageDown::default(), window, cx);
2396 });
2397 cx.assert_editor_state(
2398 &r#"
2399 one
2400 two
2401 three
2402 four
2403 five
2404 six
2405 seven
2406 eight
2407 nine
2408 ˇten
2409 ˇ"#
2410 .unindent(),
2411 );
2412}
2413
2414#[gpui::test]
2415async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2416 init_test(cx, |_| {});
2417 let mut cx = EditorTestContext::new(cx).await;
2418 cx.set_state("one «two threeˇ» four");
2419 cx.update_editor(|editor, window, cx| {
2420 editor.delete_to_beginning_of_line(
2421 &DeleteToBeginningOfLine {
2422 stop_at_indent: false,
2423 },
2424 window,
2425 cx,
2426 );
2427 assert_eq!(editor.text(cx), " four");
2428 });
2429}
2430
2431#[gpui::test]
2432fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2433 init_test(cx, |_| {});
2434
2435 let editor = cx.add_window(|window, cx| {
2436 let buffer = MultiBuffer::build_simple("one two three four", cx);
2437 build_editor(buffer.clone(), window, cx)
2438 });
2439
2440 _ = editor.update(cx, |editor, window, cx| {
2441 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2442 s.select_display_ranges([
2443 // an empty selection - the preceding word fragment is deleted
2444 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2445 // characters selected - they are deleted
2446 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2447 ])
2448 });
2449 editor.delete_to_previous_word_start(
2450 &DeleteToPreviousWordStart {
2451 ignore_newlines: false,
2452 },
2453 window,
2454 cx,
2455 );
2456 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2457 });
2458
2459 _ = editor.update(cx, |editor, window, cx| {
2460 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2461 s.select_display_ranges([
2462 // an empty selection - the following word fragment is deleted
2463 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2464 // characters selected - they are deleted
2465 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2466 ])
2467 });
2468 editor.delete_to_next_word_end(
2469 &DeleteToNextWordEnd {
2470 ignore_newlines: false,
2471 },
2472 window,
2473 cx,
2474 );
2475 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2476 });
2477}
2478
2479#[gpui::test]
2480fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2481 init_test(cx, |_| {});
2482
2483 let editor = cx.add_window(|window, cx| {
2484 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2485 build_editor(buffer.clone(), window, cx)
2486 });
2487 let del_to_prev_word_start = DeleteToPreviousWordStart {
2488 ignore_newlines: false,
2489 };
2490 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2491 ignore_newlines: true,
2492 };
2493
2494 _ = editor.update(cx, |editor, window, cx| {
2495 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2496 s.select_display_ranges([
2497 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2498 ])
2499 });
2500 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2501 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2502 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2503 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2504 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2505 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2506 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2507 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2508 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2509 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2510 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2511 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2512 });
2513}
2514
2515#[gpui::test]
2516fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2517 init_test(cx, |_| {});
2518
2519 let editor = cx.add_window(|window, cx| {
2520 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2521 build_editor(buffer.clone(), window, cx)
2522 });
2523 let del_to_next_word_end = DeleteToNextWordEnd {
2524 ignore_newlines: false,
2525 };
2526 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2527 ignore_newlines: true,
2528 };
2529
2530 _ = editor.update(cx, |editor, window, cx| {
2531 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2532 s.select_display_ranges([
2533 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2534 ])
2535 });
2536 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2537 assert_eq!(
2538 editor.buffer.read(cx).read(cx).text(),
2539 "one\n two\nthree\n four"
2540 );
2541 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2542 assert_eq!(
2543 editor.buffer.read(cx).read(cx).text(),
2544 "\n two\nthree\n four"
2545 );
2546 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2547 assert_eq!(
2548 editor.buffer.read(cx).read(cx).text(),
2549 "two\nthree\n four"
2550 );
2551 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2552 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2553 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2554 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2555 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2556 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2557 });
2558}
2559
2560#[gpui::test]
2561fn test_newline(cx: &mut TestAppContext) {
2562 init_test(cx, |_| {});
2563
2564 let editor = cx.add_window(|window, cx| {
2565 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2566 build_editor(buffer.clone(), window, cx)
2567 });
2568
2569 _ = editor.update(cx, |editor, window, cx| {
2570 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2571 s.select_display_ranges([
2572 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2573 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2574 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2575 ])
2576 });
2577
2578 editor.newline(&Newline, window, cx);
2579 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2580 });
2581}
2582
2583#[gpui::test]
2584fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2585 init_test(cx, |_| {});
2586
2587 let editor = cx.add_window(|window, cx| {
2588 let buffer = MultiBuffer::build_simple(
2589 "
2590 a
2591 b(
2592 X
2593 )
2594 c(
2595 X
2596 )
2597 "
2598 .unindent()
2599 .as_str(),
2600 cx,
2601 );
2602 let mut editor = build_editor(buffer.clone(), window, cx);
2603 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2604 s.select_ranges([
2605 Point::new(2, 4)..Point::new(2, 5),
2606 Point::new(5, 4)..Point::new(5, 5),
2607 ])
2608 });
2609 editor
2610 });
2611
2612 _ = editor.update(cx, |editor, window, cx| {
2613 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2614 editor.buffer.update(cx, |buffer, cx| {
2615 buffer.edit(
2616 [
2617 (Point::new(1, 2)..Point::new(3, 0), ""),
2618 (Point::new(4, 2)..Point::new(6, 0), ""),
2619 ],
2620 None,
2621 cx,
2622 );
2623 assert_eq!(
2624 buffer.read(cx).text(),
2625 "
2626 a
2627 b()
2628 c()
2629 "
2630 .unindent()
2631 );
2632 });
2633 assert_eq!(
2634 editor.selections.ranges(cx),
2635 &[
2636 Point::new(1, 2)..Point::new(1, 2),
2637 Point::new(2, 2)..Point::new(2, 2),
2638 ],
2639 );
2640
2641 editor.newline(&Newline, window, cx);
2642 assert_eq!(
2643 editor.text(cx),
2644 "
2645 a
2646 b(
2647 )
2648 c(
2649 )
2650 "
2651 .unindent()
2652 );
2653
2654 // The selections are moved after the inserted newlines
2655 assert_eq!(
2656 editor.selections.ranges(cx),
2657 &[
2658 Point::new(2, 0)..Point::new(2, 0),
2659 Point::new(4, 0)..Point::new(4, 0),
2660 ],
2661 );
2662 });
2663}
2664
2665#[gpui::test]
2666async fn test_newline_above(cx: &mut TestAppContext) {
2667 init_test(cx, |settings| {
2668 settings.defaults.tab_size = NonZeroU32::new(4)
2669 });
2670
2671 let language = Arc::new(
2672 Language::new(
2673 LanguageConfig::default(),
2674 Some(tree_sitter_rust::LANGUAGE.into()),
2675 )
2676 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2677 .unwrap(),
2678 );
2679
2680 let mut cx = EditorTestContext::new(cx).await;
2681 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2682 cx.set_state(indoc! {"
2683 const a: ˇA = (
2684 (ˇ
2685 «const_functionˇ»(ˇ),
2686 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2687 )ˇ
2688 ˇ);ˇ
2689 "});
2690
2691 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2692 cx.assert_editor_state(indoc! {"
2693 ˇ
2694 const a: A = (
2695 ˇ
2696 (
2697 ˇ
2698 ˇ
2699 const_function(),
2700 ˇ
2701 ˇ
2702 ˇ
2703 ˇ
2704 something_else,
2705 ˇ
2706 )
2707 ˇ
2708 ˇ
2709 );
2710 "});
2711}
2712
2713#[gpui::test]
2714async fn test_newline_below(cx: &mut TestAppContext) {
2715 init_test(cx, |settings| {
2716 settings.defaults.tab_size = NonZeroU32::new(4)
2717 });
2718
2719 let language = Arc::new(
2720 Language::new(
2721 LanguageConfig::default(),
2722 Some(tree_sitter_rust::LANGUAGE.into()),
2723 )
2724 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2725 .unwrap(),
2726 );
2727
2728 let mut cx = EditorTestContext::new(cx).await;
2729 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2730 cx.set_state(indoc! {"
2731 const a: ˇA = (
2732 (ˇ
2733 «const_functionˇ»(ˇ),
2734 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2735 )ˇ
2736 ˇ);ˇ
2737 "});
2738
2739 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2740 cx.assert_editor_state(indoc! {"
2741 const a: A = (
2742 ˇ
2743 (
2744 ˇ
2745 const_function(),
2746 ˇ
2747 ˇ
2748 something_else,
2749 ˇ
2750 ˇ
2751 ˇ
2752 ˇ
2753 )
2754 ˇ
2755 );
2756 ˇ
2757 ˇ
2758 "});
2759}
2760
2761#[gpui::test]
2762async fn test_newline_comments(cx: &mut TestAppContext) {
2763 init_test(cx, |settings| {
2764 settings.defaults.tab_size = NonZeroU32::new(4)
2765 });
2766
2767 let language = Arc::new(Language::new(
2768 LanguageConfig {
2769 line_comments: vec!["// ".into()],
2770 ..LanguageConfig::default()
2771 },
2772 None,
2773 ));
2774 {
2775 let mut cx = EditorTestContext::new(cx).await;
2776 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2777 cx.set_state(indoc! {"
2778 // Fooˇ
2779 "});
2780
2781 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2782 cx.assert_editor_state(indoc! {"
2783 // Foo
2784 // ˇ
2785 "});
2786 // Ensure that we add comment prefix when existing line contains space
2787 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2788 cx.assert_editor_state(
2789 indoc! {"
2790 // Foo
2791 //s
2792 // ˇ
2793 "}
2794 .replace("s", " ") // s is used as space placeholder to prevent format on save
2795 .as_str(),
2796 );
2797 // Ensure that we add comment prefix when existing line does not contain space
2798 cx.set_state(indoc! {"
2799 // Foo
2800 //ˇ
2801 "});
2802 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2803 cx.assert_editor_state(indoc! {"
2804 // Foo
2805 //
2806 // ˇ
2807 "});
2808 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2809 cx.set_state(indoc! {"
2810 ˇ// Foo
2811 "});
2812 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2813 cx.assert_editor_state(indoc! {"
2814
2815 ˇ// Foo
2816 "});
2817 }
2818 // Ensure that comment continuations can be disabled.
2819 update_test_language_settings(cx, |settings| {
2820 settings.defaults.extend_comment_on_newline = Some(false);
2821 });
2822 let mut cx = EditorTestContext::new(cx).await;
2823 cx.set_state(indoc! {"
2824 // Fooˇ
2825 "});
2826 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2827 cx.assert_editor_state(indoc! {"
2828 // Foo
2829 ˇ
2830 "});
2831}
2832
2833#[gpui::test]
2834async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
2835 init_test(cx, |settings| {
2836 settings.defaults.tab_size = NonZeroU32::new(4)
2837 });
2838
2839 let language = Arc::new(Language::new(
2840 LanguageConfig {
2841 line_comments: vec!["// ".into(), "/// ".into()],
2842 ..LanguageConfig::default()
2843 },
2844 None,
2845 ));
2846 {
2847 let mut cx = EditorTestContext::new(cx).await;
2848 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2849 cx.set_state(indoc! {"
2850 //ˇ
2851 "});
2852 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2853 cx.assert_editor_state(indoc! {"
2854 //
2855 // ˇ
2856 "});
2857
2858 cx.set_state(indoc! {"
2859 ///ˇ
2860 "});
2861 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2862 cx.assert_editor_state(indoc! {"
2863 ///
2864 /// ˇ
2865 "});
2866 }
2867}
2868
2869#[gpui::test]
2870async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
2871 init_test(cx, |settings| {
2872 settings.defaults.tab_size = NonZeroU32::new(4)
2873 });
2874
2875 let language = Arc::new(
2876 Language::new(
2877 LanguageConfig {
2878 documentation: Some(language::DocumentationConfig {
2879 start: "/**".into(),
2880 end: "*/".into(),
2881 prefix: "* ".into(),
2882 tab_size: NonZeroU32::new(1).unwrap(),
2883 }),
2884
2885 ..LanguageConfig::default()
2886 },
2887 Some(tree_sitter_rust::LANGUAGE.into()),
2888 )
2889 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
2890 .unwrap(),
2891 );
2892
2893 {
2894 let mut cx = EditorTestContext::new(cx).await;
2895 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2896 cx.set_state(indoc! {"
2897 /**ˇ
2898 "});
2899
2900 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2901 cx.assert_editor_state(indoc! {"
2902 /**
2903 * ˇ
2904 "});
2905 // Ensure that if cursor is before the comment start,
2906 // we do not actually insert a comment prefix.
2907 cx.set_state(indoc! {"
2908 ˇ/**
2909 "});
2910 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2911 cx.assert_editor_state(indoc! {"
2912
2913 ˇ/**
2914 "});
2915 // Ensure that if cursor is between it doesn't add comment prefix.
2916 cx.set_state(indoc! {"
2917 /*ˇ*
2918 "});
2919 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2920 cx.assert_editor_state(indoc! {"
2921 /*
2922 ˇ*
2923 "});
2924 // Ensure that if suffix exists on same line after cursor it adds new line.
2925 cx.set_state(indoc! {"
2926 /**ˇ*/
2927 "});
2928 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2929 cx.assert_editor_state(indoc! {"
2930 /**
2931 * ˇ
2932 */
2933 "});
2934 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2935 cx.set_state(indoc! {"
2936 /**ˇ */
2937 "});
2938 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2939 cx.assert_editor_state(indoc! {"
2940 /**
2941 * ˇ
2942 */
2943 "});
2944 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2945 cx.set_state(indoc! {"
2946 /** ˇ*/
2947 "});
2948 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2949 cx.assert_editor_state(
2950 indoc! {"
2951 /**s
2952 * ˇ
2953 */
2954 "}
2955 .replace("s", " ") // s is used as space placeholder to prevent format on save
2956 .as_str(),
2957 );
2958 // Ensure that delimiter space is preserved when newline on already
2959 // spaced delimiter.
2960 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2961 cx.assert_editor_state(
2962 indoc! {"
2963 /**s
2964 *s
2965 * ˇ
2966 */
2967 "}
2968 .replace("s", " ") // s is used as space placeholder to prevent format on save
2969 .as_str(),
2970 );
2971 // Ensure that delimiter space is preserved when space is not
2972 // on existing delimiter.
2973 cx.set_state(indoc! {"
2974 /**
2975 *ˇ
2976 */
2977 "});
2978 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2979 cx.assert_editor_state(indoc! {"
2980 /**
2981 *
2982 * ˇ
2983 */
2984 "});
2985 // Ensure that if suffix exists on same line after cursor it
2986 // doesn't add extra new line if prefix is not on same line.
2987 cx.set_state(indoc! {"
2988 /**
2989 ˇ*/
2990 "});
2991 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2992 cx.assert_editor_state(indoc! {"
2993 /**
2994
2995 ˇ*/
2996 "});
2997 // Ensure that it detects suffix after existing prefix.
2998 cx.set_state(indoc! {"
2999 /**ˇ/
3000 "});
3001 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3002 cx.assert_editor_state(indoc! {"
3003 /**
3004 ˇ/
3005 "});
3006 // Ensure that if suffix exists on same line before
3007 // cursor it does not add comment prefix.
3008 cx.set_state(indoc! {"
3009 /** */ˇ
3010 "});
3011 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3012 cx.assert_editor_state(indoc! {"
3013 /** */
3014 ˇ
3015 "});
3016 // Ensure that if suffix exists on same line before
3017 // cursor it does not add comment prefix.
3018 cx.set_state(indoc! {"
3019 /**
3020 *
3021 */ˇ
3022 "});
3023 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3024 cx.assert_editor_state(indoc! {"
3025 /**
3026 *
3027 */
3028 ˇ
3029 "});
3030
3031 // Ensure that inline comment followed by code
3032 // doesn't add comment prefix on newline
3033 cx.set_state(indoc! {"
3034 /** */ textˇ
3035 "});
3036 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3037 cx.assert_editor_state(indoc! {"
3038 /** */ text
3039 ˇ
3040 "});
3041
3042 // Ensure that text after comment end tag
3043 // doesn't add comment prefix on newline
3044 cx.set_state(indoc! {"
3045 /**
3046 *
3047 */ˇtext
3048 "});
3049 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3050 cx.assert_editor_state(indoc! {"
3051 /**
3052 *
3053 */
3054 ˇtext
3055 "});
3056
3057 // Ensure if not comment block it doesn't
3058 // add comment prefix on newline
3059 cx.set_state(indoc! {"
3060 * textˇ
3061 "});
3062 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3063 cx.assert_editor_state(indoc! {"
3064 * text
3065 ˇ
3066 "});
3067 }
3068 // Ensure that comment continuations can be disabled.
3069 update_test_language_settings(cx, |settings| {
3070 settings.defaults.extend_comment_on_newline = Some(false);
3071 });
3072 let mut cx = EditorTestContext::new(cx).await;
3073 cx.set_state(indoc! {"
3074 /**ˇ
3075 "});
3076 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3077 cx.assert_editor_state(indoc! {"
3078 /**
3079 ˇ
3080 "});
3081}
3082
3083#[gpui::test]
3084fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3085 init_test(cx, |_| {});
3086
3087 let editor = cx.add_window(|window, cx| {
3088 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3089 let mut editor = build_editor(buffer.clone(), window, cx);
3090 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3091 s.select_ranges([3..4, 11..12, 19..20])
3092 });
3093 editor
3094 });
3095
3096 _ = editor.update(cx, |editor, window, cx| {
3097 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3098 editor.buffer.update(cx, |buffer, cx| {
3099 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3100 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3101 });
3102 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3103
3104 editor.insert("Z", window, cx);
3105 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3106
3107 // The selections are moved after the inserted characters
3108 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3109 });
3110}
3111
3112#[gpui::test]
3113async fn test_tab(cx: &mut TestAppContext) {
3114 init_test(cx, |settings| {
3115 settings.defaults.tab_size = NonZeroU32::new(3)
3116 });
3117
3118 let mut cx = EditorTestContext::new(cx).await;
3119 cx.set_state(indoc! {"
3120 ˇabˇc
3121 ˇ🏀ˇ🏀ˇefg
3122 dˇ
3123 "});
3124 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3125 cx.assert_editor_state(indoc! {"
3126 ˇab ˇc
3127 ˇ🏀 ˇ🏀 ˇefg
3128 d ˇ
3129 "});
3130
3131 cx.set_state(indoc! {"
3132 a
3133 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3134 "});
3135 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3136 cx.assert_editor_state(indoc! {"
3137 a
3138 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3139 "});
3140}
3141
3142#[gpui::test]
3143async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3144 init_test(cx, |_| {});
3145
3146 let mut cx = EditorTestContext::new(cx).await;
3147 let language = Arc::new(
3148 Language::new(
3149 LanguageConfig::default(),
3150 Some(tree_sitter_rust::LANGUAGE.into()),
3151 )
3152 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3153 .unwrap(),
3154 );
3155 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3156
3157 // test when all cursors are not at suggested indent
3158 // then simply move to their suggested indent location
3159 cx.set_state(indoc! {"
3160 const a: B = (
3161 c(
3162 ˇ
3163 ˇ )
3164 );
3165 "});
3166 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3167 cx.assert_editor_state(indoc! {"
3168 const a: B = (
3169 c(
3170 ˇ
3171 ˇ)
3172 );
3173 "});
3174
3175 // test cursor already at suggested indent not moving when
3176 // other cursors are yet to reach their suggested indents
3177 cx.set_state(indoc! {"
3178 ˇ
3179 const a: B = (
3180 c(
3181 d(
3182 ˇ
3183 )
3184 ˇ
3185 ˇ )
3186 );
3187 "});
3188 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3189 cx.assert_editor_state(indoc! {"
3190 ˇ
3191 const a: B = (
3192 c(
3193 d(
3194 ˇ
3195 )
3196 ˇ
3197 ˇ)
3198 );
3199 "});
3200 // test when all cursors are at suggested indent then tab is inserted
3201 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3202 cx.assert_editor_state(indoc! {"
3203 ˇ
3204 const a: B = (
3205 c(
3206 d(
3207 ˇ
3208 )
3209 ˇ
3210 ˇ)
3211 );
3212 "});
3213
3214 // test when current indent is less than suggested indent,
3215 // we adjust line to match suggested indent and move cursor to it
3216 //
3217 // when no other cursor is at word boundary, all of them should move
3218 cx.set_state(indoc! {"
3219 const a: B = (
3220 c(
3221 d(
3222 ˇ
3223 ˇ )
3224 ˇ )
3225 );
3226 "});
3227 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3228 cx.assert_editor_state(indoc! {"
3229 const a: B = (
3230 c(
3231 d(
3232 ˇ
3233 ˇ)
3234 ˇ)
3235 );
3236 "});
3237
3238 // test when current indent is less than suggested indent,
3239 // we adjust line to match suggested indent and move cursor to it
3240 //
3241 // when some other cursor is at word boundary, it should not move
3242 cx.set_state(indoc! {"
3243 const a: B = (
3244 c(
3245 d(
3246 ˇ
3247 ˇ )
3248 ˇ)
3249 );
3250 "});
3251 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3252 cx.assert_editor_state(indoc! {"
3253 const a: B = (
3254 c(
3255 d(
3256 ˇ
3257 ˇ)
3258 ˇ)
3259 );
3260 "});
3261
3262 // test when current indent is more than suggested indent,
3263 // we just move cursor to current indent instead of suggested indent
3264 //
3265 // when no other cursor is at word boundary, all of them should move
3266 cx.set_state(indoc! {"
3267 const a: B = (
3268 c(
3269 d(
3270 ˇ
3271 ˇ )
3272 ˇ )
3273 );
3274 "});
3275 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3276 cx.assert_editor_state(indoc! {"
3277 const a: B = (
3278 c(
3279 d(
3280 ˇ
3281 ˇ)
3282 ˇ)
3283 );
3284 "});
3285 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3286 cx.assert_editor_state(indoc! {"
3287 const a: B = (
3288 c(
3289 d(
3290 ˇ
3291 ˇ)
3292 ˇ)
3293 );
3294 "});
3295
3296 // test when current indent is more than suggested indent,
3297 // we just move cursor to current indent instead of suggested indent
3298 //
3299 // when some other cursor is at word boundary, it doesn't move
3300 cx.set_state(indoc! {"
3301 const a: B = (
3302 c(
3303 d(
3304 ˇ
3305 ˇ )
3306 ˇ)
3307 );
3308 "});
3309 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3310 cx.assert_editor_state(indoc! {"
3311 const a: B = (
3312 c(
3313 d(
3314 ˇ
3315 ˇ)
3316 ˇ)
3317 );
3318 "});
3319
3320 // handle auto-indent when there are multiple cursors on the same line
3321 cx.set_state(indoc! {"
3322 const a: B = (
3323 c(
3324 ˇ ˇ
3325 ˇ )
3326 );
3327 "});
3328 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3329 cx.assert_editor_state(indoc! {"
3330 const a: B = (
3331 c(
3332 ˇ
3333 ˇ)
3334 );
3335 "});
3336}
3337
3338#[gpui::test]
3339async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3340 init_test(cx, |settings| {
3341 settings.defaults.tab_size = NonZeroU32::new(3)
3342 });
3343
3344 let mut cx = EditorTestContext::new(cx).await;
3345 cx.set_state(indoc! {"
3346 ˇ
3347 \t ˇ
3348 \t ˇ
3349 \t ˇ
3350 \t \t\t \t \t\t \t\t \t \t ˇ
3351 "});
3352
3353 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3354 cx.assert_editor_state(indoc! {"
3355 ˇ
3356 \t ˇ
3357 \t ˇ
3358 \t ˇ
3359 \t \t\t \t \t\t \t\t \t \t ˇ
3360 "});
3361}
3362
3363#[gpui::test]
3364async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3365 init_test(cx, |settings| {
3366 settings.defaults.tab_size = NonZeroU32::new(4)
3367 });
3368
3369 let language = Arc::new(
3370 Language::new(
3371 LanguageConfig::default(),
3372 Some(tree_sitter_rust::LANGUAGE.into()),
3373 )
3374 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3375 .unwrap(),
3376 );
3377
3378 let mut cx = EditorTestContext::new(cx).await;
3379 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3380 cx.set_state(indoc! {"
3381 fn a() {
3382 if b {
3383 \t ˇc
3384 }
3385 }
3386 "});
3387
3388 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3389 cx.assert_editor_state(indoc! {"
3390 fn a() {
3391 if b {
3392 ˇc
3393 }
3394 }
3395 "});
3396}
3397
3398#[gpui::test]
3399async fn test_indent_outdent(cx: &mut TestAppContext) {
3400 init_test(cx, |settings| {
3401 settings.defaults.tab_size = NonZeroU32::new(4);
3402 });
3403
3404 let mut cx = EditorTestContext::new(cx).await;
3405
3406 cx.set_state(indoc! {"
3407 «oneˇ» «twoˇ»
3408 three
3409 four
3410 "});
3411 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3412 cx.assert_editor_state(indoc! {"
3413 «oneˇ» «twoˇ»
3414 three
3415 four
3416 "});
3417
3418 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3419 cx.assert_editor_state(indoc! {"
3420 «oneˇ» «twoˇ»
3421 three
3422 four
3423 "});
3424
3425 // select across line ending
3426 cx.set_state(indoc! {"
3427 one two
3428 t«hree
3429 ˇ» four
3430 "});
3431 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3432 cx.assert_editor_state(indoc! {"
3433 one two
3434 t«hree
3435 ˇ» four
3436 "});
3437
3438 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3439 cx.assert_editor_state(indoc! {"
3440 one two
3441 t«hree
3442 ˇ» four
3443 "});
3444
3445 // Ensure that indenting/outdenting works when the cursor is at column 0.
3446 cx.set_state(indoc! {"
3447 one two
3448 ˇthree
3449 four
3450 "});
3451 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3452 cx.assert_editor_state(indoc! {"
3453 one two
3454 ˇthree
3455 four
3456 "});
3457
3458 cx.set_state(indoc! {"
3459 one two
3460 ˇ three
3461 four
3462 "});
3463 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3464 cx.assert_editor_state(indoc! {"
3465 one two
3466 ˇthree
3467 four
3468 "});
3469}
3470
3471#[gpui::test]
3472async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3473 // This is a regression test for issue #33761
3474 init_test(cx, |_| {});
3475
3476 let mut cx = EditorTestContext::new(cx).await;
3477 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3478 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3479
3480 cx.set_state(
3481 r#"ˇ# ingress:
3482ˇ# api:
3483ˇ# enabled: false
3484ˇ# pathType: Prefix
3485ˇ# console:
3486ˇ# enabled: false
3487ˇ# pathType: Prefix
3488"#,
3489 );
3490
3491 // Press tab to indent all lines
3492 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3493
3494 cx.assert_editor_state(
3495 r#" ˇ# ingress:
3496 ˇ# api:
3497 ˇ# enabled: false
3498 ˇ# pathType: Prefix
3499 ˇ# console:
3500 ˇ# enabled: false
3501 ˇ# pathType: Prefix
3502"#,
3503 );
3504}
3505
3506#[gpui::test]
3507async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3508 // This is a test to make sure our fix for issue #33761 didn't break anything
3509 init_test(cx, |_| {});
3510
3511 let mut cx = EditorTestContext::new(cx).await;
3512 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3513 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3514
3515 cx.set_state(
3516 r#"ˇingress:
3517ˇ api:
3518ˇ enabled: false
3519ˇ pathType: Prefix
3520"#,
3521 );
3522
3523 // Press tab to indent all lines
3524 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3525
3526 cx.assert_editor_state(
3527 r#"ˇingress:
3528 ˇapi:
3529 ˇenabled: false
3530 ˇpathType: Prefix
3531"#,
3532 );
3533}
3534
3535#[gpui::test]
3536async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3537 init_test(cx, |settings| {
3538 settings.defaults.hard_tabs = Some(true);
3539 });
3540
3541 let mut cx = EditorTestContext::new(cx).await;
3542
3543 // select two ranges on one line
3544 cx.set_state(indoc! {"
3545 «oneˇ» «twoˇ»
3546 three
3547 four
3548 "});
3549 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3550 cx.assert_editor_state(indoc! {"
3551 \t«oneˇ» «twoˇ»
3552 three
3553 four
3554 "});
3555 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3556 cx.assert_editor_state(indoc! {"
3557 \t\t«oneˇ» «twoˇ»
3558 three
3559 four
3560 "});
3561 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3562 cx.assert_editor_state(indoc! {"
3563 \t«oneˇ» «twoˇ»
3564 three
3565 four
3566 "});
3567 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3568 cx.assert_editor_state(indoc! {"
3569 «oneˇ» «twoˇ»
3570 three
3571 four
3572 "});
3573
3574 // select across a line ending
3575 cx.set_state(indoc! {"
3576 one two
3577 t«hree
3578 ˇ»four
3579 "});
3580 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3581 cx.assert_editor_state(indoc! {"
3582 one two
3583 \tt«hree
3584 ˇ»four
3585 "});
3586 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3587 cx.assert_editor_state(indoc! {"
3588 one two
3589 \t\tt«hree
3590 ˇ»four
3591 "});
3592 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3593 cx.assert_editor_state(indoc! {"
3594 one two
3595 \tt«hree
3596 ˇ»four
3597 "});
3598 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3599 cx.assert_editor_state(indoc! {"
3600 one two
3601 t«hree
3602 ˇ»four
3603 "});
3604
3605 // Ensure that indenting/outdenting works when the cursor is at column 0.
3606 cx.set_state(indoc! {"
3607 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 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3618 cx.assert_editor_state(indoc! {"
3619 one two
3620 \tˇthree
3621 four
3622 "});
3623 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3624 cx.assert_editor_state(indoc! {"
3625 one two
3626 ˇthree
3627 four
3628 "});
3629}
3630
3631#[gpui::test]
3632fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3633 init_test(cx, |settings| {
3634 settings.languages.0.extend([
3635 (
3636 "TOML".into(),
3637 LanguageSettingsContent {
3638 tab_size: NonZeroU32::new(2),
3639 ..Default::default()
3640 },
3641 ),
3642 (
3643 "Rust".into(),
3644 LanguageSettingsContent {
3645 tab_size: NonZeroU32::new(4),
3646 ..Default::default()
3647 },
3648 ),
3649 ]);
3650 });
3651
3652 let toml_language = Arc::new(Language::new(
3653 LanguageConfig {
3654 name: "TOML".into(),
3655 ..Default::default()
3656 },
3657 None,
3658 ));
3659 let rust_language = Arc::new(Language::new(
3660 LanguageConfig {
3661 name: "Rust".into(),
3662 ..Default::default()
3663 },
3664 None,
3665 ));
3666
3667 let toml_buffer =
3668 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3669 let rust_buffer =
3670 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3671 let multibuffer = cx.new(|cx| {
3672 let mut multibuffer = MultiBuffer::new(ReadWrite);
3673 multibuffer.push_excerpts(
3674 toml_buffer.clone(),
3675 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3676 cx,
3677 );
3678 multibuffer.push_excerpts(
3679 rust_buffer.clone(),
3680 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3681 cx,
3682 );
3683 multibuffer
3684 });
3685
3686 cx.add_window(|window, cx| {
3687 let mut editor = build_editor(multibuffer, window, cx);
3688
3689 assert_eq!(
3690 editor.text(cx),
3691 indoc! {"
3692 a = 1
3693 b = 2
3694
3695 const c: usize = 3;
3696 "}
3697 );
3698
3699 select_ranges(
3700 &mut editor,
3701 indoc! {"
3702 «aˇ» = 1
3703 b = 2
3704
3705 «const c:ˇ» usize = 3;
3706 "},
3707 window,
3708 cx,
3709 );
3710
3711 editor.tab(&Tab, window, cx);
3712 assert_text_with_selections(
3713 &mut editor,
3714 indoc! {"
3715 «aˇ» = 1
3716 b = 2
3717
3718 «const c:ˇ» usize = 3;
3719 "},
3720 cx,
3721 );
3722 editor.backtab(&Backtab, window, cx);
3723 assert_text_with_selections(
3724 &mut editor,
3725 indoc! {"
3726 «aˇ» = 1
3727 b = 2
3728
3729 «const c:ˇ» usize = 3;
3730 "},
3731 cx,
3732 );
3733
3734 editor
3735 });
3736}
3737
3738#[gpui::test]
3739async fn test_backspace(cx: &mut TestAppContext) {
3740 init_test(cx, |_| {});
3741
3742 let mut cx = EditorTestContext::new(cx).await;
3743
3744 // Basic backspace
3745 cx.set_state(indoc! {"
3746 onˇe two three
3747 fou«rˇ» five six
3748 seven «ˇeight nine
3749 »ten
3750 "});
3751 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3752 cx.assert_editor_state(indoc! {"
3753 oˇe two three
3754 fouˇ five six
3755 seven ˇten
3756 "});
3757
3758 // Test backspace inside and around indents
3759 cx.set_state(indoc! {"
3760 zero
3761 ˇone
3762 ˇtwo
3763 ˇ ˇ ˇ three
3764 ˇ ˇ four
3765 "});
3766 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3767 cx.assert_editor_state(indoc! {"
3768 zero
3769 ˇone
3770 ˇtwo
3771 ˇ threeˇ four
3772 "});
3773}
3774
3775#[gpui::test]
3776async fn test_delete(cx: &mut TestAppContext) {
3777 init_test(cx, |_| {});
3778
3779 let mut cx = EditorTestContext::new(cx).await;
3780 cx.set_state(indoc! {"
3781 onˇe two three
3782 fou«rˇ» five six
3783 seven «ˇeight nine
3784 »ten
3785 "});
3786 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3787 cx.assert_editor_state(indoc! {"
3788 onˇ two three
3789 fouˇ five six
3790 seven ˇten
3791 "});
3792}
3793
3794#[gpui::test]
3795fn test_delete_line(cx: &mut TestAppContext) {
3796 init_test(cx, |_| {});
3797
3798 let editor = cx.add_window(|window, cx| {
3799 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3800 build_editor(buffer, window, cx)
3801 });
3802 _ = editor.update(cx, |editor, window, cx| {
3803 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3804 s.select_display_ranges([
3805 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3806 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3807 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3808 ])
3809 });
3810 editor.delete_line(&DeleteLine, window, cx);
3811 assert_eq!(editor.display_text(cx), "ghi");
3812 assert_eq!(
3813 editor.selections.display_ranges(cx),
3814 vec![
3815 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3816 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3817 ]
3818 );
3819 });
3820
3821 let editor = cx.add_window(|window, cx| {
3822 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3823 build_editor(buffer, window, cx)
3824 });
3825 _ = editor.update(cx, |editor, window, cx| {
3826 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3827 s.select_display_ranges([
3828 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3829 ])
3830 });
3831 editor.delete_line(&DeleteLine, window, cx);
3832 assert_eq!(editor.display_text(cx), "ghi\n");
3833 assert_eq!(
3834 editor.selections.display_ranges(cx),
3835 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3836 );
3837 });
3838}
3839
3840#[gpui::test]
3841fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3842 init_test(cx, |_| {});
3843
3844 cx.add_window(|window, cx| {
3845 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3846 let mut editor = build_editor(buffer.clone(), window, cx);
3847 let buffer = buffer.read(cx).as_singleton().unwrap();
3848
3849 assert_eq!(
3850 editor.selections.ranges::<Point>(cx),
3851 &[Point::new(0, 0)..Point::new(0, 0)]
3852 );
3853
3854 // When on single line, replace newline at end by space
3855 editor.join_lines(&JoinLines, window, cx);
3856 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3857 assert_eq!(
3858 editor.selections.ranges::<Point>(cx),
3859 &[Point::new(0, 3)..Point::new(0, 3)]
3860 );
3861
3862 // When multiple lines are selected, remove newlines that are spanned by the selection
3863 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3864 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3865 });
3866 editor.join_lines(&JoinLines, window, cx);
3867 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3868 assert_eq!(
3869 editor.selections.ranges::<Point>(cx),
3870 &[Point::new(0, 11)..Point::new(0, 11)]
3871 );
3872
3873 // Undo should be transactional
3874 editor.undo(&Undo, window, cx);
3875 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3876 assert_eq!(
3877 editor.selections.ranges::<Point>(cx),
3878 &[Point::new(0, 5)..Point::new(2, 2)]
3879 );
3880
3881 // When joining an empty line don't insert a space
3882 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3883 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3884 });
3885 editor.join_lines(&JoinLines, window, cx);
3886 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3887 assert_eq!(
3888 editor.selections.ranges::<Point>(cx),
3889 [Point::new(2, 3)..Point::new(2, 3)]
3890 );
3891
3892 // We can remove trailing newlines
3893 editor.join_lines(&JoinLines, window, cx);
3894 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3895 assert_eq!(
3896 editor.selections.ranges::<Point>(cx),
3897 [Point::new(2, 3)..Point::new(2, 3)]
3898 );
3899
3900 // We don't blow up on the last line
3901 editor.join_lines(&JoinLines, window, cx);
3902 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3903 assert_eq!(
3904 editor.selections.ranges::<Point>(cx),
3905 [Point::new(2, 3)..Point::new(2, 3)]
3906 );
3907
3908 // reset to test indentation
3909 editor.buffer.update(cx, |buffer, cx| {
3910 buffer.edit(
3911 [
3912 (Point::new(1, 0)..Point::new(1, 2), " "),
3913 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3914 ],
3915 None,
3916 cx,
3917 )
3918 });
3919
3920 // We remove any leading spaces
3921 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3922 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3923 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3924 });
3925 editor.join_lines(&JoinLines, window, cx);
3926 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3927
3928 // We don't insert a space for a line containing only spaces
3929 editor.join_lines(&JoinLines, window, cx);
3930 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3931
3932 // We ignore any leading tabs
3933 editor.join_lines(&JoinLines, window, cx);
3934 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3935
3936 editor
3937 });
3938}
3939
3940#[gpui::test]
3941fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3942 init_test(cx, |_| {});
3943
3944 cx.add_window(|window, cx| {
3945 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3946 let mut editor = build_editor(buffer.clone(), window, cx);
3947 let buffer = buffer.read(cx).as_singleton().unwrap();
3948
3949 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3950 s.select_ranges([
3951 Point::new(0, 2)..Point::new(1, 1),
3952 Point::new(1, 2)..Point::new(1, 2),
3953 Point::new(3, 1)..Point::new(3, 2),
3954 ])
3955 });
3956
3957 editor.join_lines(&JoinLines, window, cx);
3958 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3959
3960 assert_eq!(
3961 editor.selections.ranges::<Point>(cx),
3962 [
3963 Point::new(0, 7)..Point::new(0, 7),
3964 Point::new(1, 3)..Point::new(1, 3)
3965 ]
3966 );
3967 editor
3968 });
3969}
3970
3971#[gpui::test]
3972async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3973 init_test(cx, |_| {});
3974
3975 let mut cx = EditorTestContext::new(cx).await;
3976
3977 let diff_base = r#"
3978 Line 0
3979 Line 1
3980 Line 2
3981 Line 3
3982 "#
3983 .unindent();
3984
3985 cx.set_state(
3986 &r#"
3987 ˇLine 0
3988 Line 1
3989 Line 2
3990 Line 3
3991 "#
3992 .unindent(),
3993 );
3994
3995 cx.set_head_text(&diff_base);
3996 executor.run_until_parked();
3997
3998 // Join lines
3999 cx.update_editor(|editor, window, cx| {
4000 editor.join_lines(&JoinLines, window, cx);
4001 });
4002 executor.run_until_parked();
4003
4004 cx.assert_editor_state(
4005 &r#"
4006 Line 0ˇ Line 1
4007 Line 2
4008 Line 3
4009 "#
4010 .unindent(),
4011 );
4012 // Join again
4013 cx.update_editor(|editor, window, cx| {
4014 editor.join_lines(&JoinLines, window, cx);
4015 });
4016 executor.run_until_parked();
4017
4018 cx.assert_editor_state(
4019 &r#"
4020 Line 0 Line 1ˇ Line 2
4021 Line 3
4022 "#
4023 .unindent(),
4024 );
4025}
4026
4027#[gpui::test]
4028async fn test_custom_newlines_cause_no_false_positive_diffs(
4029 executor: BackgroundExecutor,
4030 cx: &mut TestAppContext,
4031) {
4032 init_test(cx, |_| {});
4033 let mut cx = EditorTestContext::new(cx).await;
4034 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4035 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4036 executor.run_until_parked();
4037
4038 cx.update_editor(|editor, window, cx| {
4039 let snapshot = editor.snapshot(window, cx);
4040 assert_eq!(
4041 snapshot
4042 .buffer_snapshot
4043 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
4044 .collect::<Vec<_>>(),
4045 Vec::new(),
4046 "Should not have any diffs for files with custom newlines"
4047 );
4048 });
4049}
4050
4051#[gpui::test]
4052async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4053 init_test(cx, |_| {});
4054
4055 let mut cx = EditorTestContext::new(cx).await;
4056
4057 // Test sort_lines_case_insensitive()
4058 cx.set_state(indoc! {"
4059 «z
4060 y
4061 x
4062 Z
4063 Y
4064 Xˇ»
4065 "});
4066 cx.update_editor(|e, window, cx| {
4067 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4068 });
4069 cx.assert_editor_state(indoc! {"
4070 «x
4071 X
4072 y
4073 Y
4074 z
4075 Zˇ»
4076 "});
4077
4078 // Test reverse_lines()
4079 cx.set_state(indoc! {"
4080 «5
4081 4
4082 3
4083 2
4084 1ˇ»
4085 "});
4086 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4087 cx.assert_editor_state(indoc! {"
4088 «1
4089 2
4090 3
4091 4
4092 5ˇ»
4093 "});
4094
4095 // Skip testing shuffle_line()
4096
4097 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4098 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4099
4100 // Don't manipulate when cursor is on single line, but expand the selection
4101 cx.set_state(indoc! {"
4102 ddˇdd
4103 ccc
4104 bb
4105 a
4106 "});
4107 cx.update_editor(|e, window, cx| {
4108 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4109 });
4110 cx.assert_editor_state(indoc! {"
4111 «ddddˇ»
4112 ccc
4113 bb
4114 a
4115 "});
4116
4117 // Basic manipulate case
4118 // Start selection moves to column 0
4119 // End of selection shrinks to fit shorter line
4120 cx.set_state(indoc! {"
4121 dd«d
4122 ccc
4123 bb
4124 aaaaaˇ»
4125 "});
4126 cx.update_editor(|e, window, cx| {
4127 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4128 });
4129 cx.assert_editor_state(indoc! {"
4130 «aaaaa
4131 bb
4132 ccc
4133 dddˇ»
4134 "});
4135
4136 // Manipulate case with newlines
4137 cx.set_state(indoc! {"
4138 dd«d
4139 ccc
4140
4141 bb
4142 aaaaa
4143
4144 ˇ»
4145 "});
4146 cx.update_editor(|e, window, cx| {
4147 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4148 });
4149 cx.assert_editor_state(indoc! {"
4150 «
4151
4152 aaaaa
4153 bb
4154 ccc
4155 dddˇ»
4156
4157 "});
4158
4159 // Adding new line
4160 cx.set_state(indoc! {"
4161 aa«a
4162 bbˇ»b
4163 "});
4164 cx.update_editor(|e, window, cx| {
4165 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4166 });
4167 cx.assert_editor_state(indoc! {"
4168 «aaa
4169 bbb
4170 added_lineˇ»
4171 "});
4172
4173 // Removing line
4174 cx.set_state(indoc! {"
4175 aa«a
4176 bbbˇ»
4177 "});
4178 cx.update_editor(|e, window, cx| {
4179 e.manipulate_immutable_lines(window, cx, |lines| {
4180 lines.pop();
4181 })
4182 });
4183 cx.assert_editor_state(indoc! {"
4184 «aaaˇ»
4185 "});
4186
4187 // Removing all lines
4188 cx.set_state(indoc! {"
4189 aa«a
4190 bbbˇ»
4191 "});
4192 cx.update_editor(|e, window, cx| {
4193 e.manipulate_immutable_lines(window, cx, |lines| {
4194 lines.drain(..);
4195 })
4196 });
4197 cx.assert_editor_state(indoc! {"
4198 ˇ
4199 "});
4200}
4201
4202#[gpui::test]
4203async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4204 init_test(cx, |_| {});
4205
4206 let mut cx = EditorTestContext::new(cx).await;
4207
4208 // Consider continuous selection as single selection
4209 cx.set_state(indoc! {"
4210 Aaa«aa
4211 cˇ»c«c
4212 bb
4213 aaaˇ»aa
4214 "});
4215 cx.update_editor(|e, window, cx| {
4216 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4217 });
4218 cx.assert_editor_state(indoc! {"
4219 «Aaaaa
4220 ccc
4221 bb
4222 aaaaaˇ»
4223 "});
4224
4225 cx.set_state(indoc! {"
4226 Aaa«aa
4227 cˇ»c«c
4228 bb
4229 aaaˇ»aa
4230 "});
4231 cx.update_editor(|e, window, cx| {
4232 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4233 });
4234 cx.assert_editor_state(indoc! {"
4235 «Aaaaa
4236 ccc
4237 bbˇ»
4238 "});
4239
4240 // Consider non continuous selection as distinct dedup operations
4241 cx.set_state(indoc! {"
4242 «aaaaa
4243 bb
4244 aaaaa
4245 aaaaaˇ»
4246
4247 aaa«aaˇ»
4248 "});
4249 cx.update_editor(|e, window, cx| {
4250 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4251 });
4252 cx.assert_editor_state(indoc! {"
4253 «aaaaa
4254 bbˇ»
4255
4256 «aaaaaˇ»
4257 "});
4258}
4259
4260#[gpui::test]
4261async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4262 init_test(cx, |_| {});
4263
4264 let mut cx = EditorTestContext::new(cx).await;
4265
4266 cx.set_state(indoc! {"
4267 «Aaa
4268 aAa
4269 Aaaˇ»
4270 "});
4271 cx.update_editor(|e, window, cx| {
4272 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4273 });
4274 cx.assert_editor_state(indoc! {"
4275 «Aaa
4276 aAaˇ»
4277 "});
4278
4279 cx.set_state(indoc! {"
4280 «Aaa
4281 aAa
4282 aaAˇ»
4283 "});
4284 cx.update_editor(|e, window, cx| {
4285 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4286 });
4287 cx.assert_editor_state(indoc! {"
4288 «Aaaˇ»
4289 "});
4290}
4291
4292#[gpui::test]
4293async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4294 init_test(cx, |_| {});
4295
4296 let mut cx = EditorTestContext::new(cx).await;
4297
4298 // Manipulate with multiple selections on a single line
4299 cx.set_state(indoc! {"
4300 dd«dd
4301 cˇ»c«c
4302 bb
4303 aaaˇ»aa
4304 "});
4305 cx.update_editor(|e, window, cx| {
4306 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4307 });
4308 cx.assert_editor_state(indoc! {"
4309 «aaaaa
4310 bb
4311 ccc
4312 ddddˇ»
4313 "});
4314
4315 // Manipulate with multiple disjoin selections
4316 cx.set_state(indoc! {"
4317 5«
4318 4
4319 3
4320 2
4321 1ˇ»
4322
4323 dd«dd
4324 ccc
4325 bb
4326 aaaˇ»aa
4327 "});
4328 cx.update_editor(|e, window, cx| {
4329 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4330 });
4331 cx.assert_editor_state(indoc! {"
4332 «1
4333 2
4334 3
4335 4
4336 5ˇ»
4337
4338 «aaaaa
4339 bb
4340 ccc
4341 ddddˇ»
4342 "});
4343
4344 // Adding lines on each selection
4345 cx.set_state(indoc! {"
4346 2«
4347 1ˇ»
4348
4349 bb«bb
4350 aaaˇ»aa
4351 "});
4352 cx.update_editor(|e, window, cx| {
4353 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4354 });
4355 cx.assert_editor_state(indoc! {"
4356 «2
4357 1
4358 added lineˇ»
4359
4360 «bbbb
4361 aaaaa
4362 added lineˇ»
4363 "});
4364
4365 // Removing lines on each selection
4366 cx.set_state(indoc! {"
4367 2«
4368 1ˇ»
4369
4370 bb«bb
4371 aaaˇ»aa
4372 "});
4373 cx.update_editor(|e, window, cx| {
4374 e.manipulate_immutable_lines(window, cx, |lines| {
4375 lines.pop();
4376 })
4377 });
4378 cx.assert_editor_state(indoc! {"
4379 «2ˇ»
4380
4381 «bbbbˇ»
4382 "});
4383}
4384
4385#[gpui::test]
4386async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4387 init_test(cx, |settings| {
4388 settings.defaults.tab_size = NonZeroU32::new(3)
4389 });
4390
4391 let mut cx = EditorTestContext::new(cx).await;
4392
4393 // MULTI SELECTION
4394 // Ln.1 "«" tests empty lines
4395 // Ln.9 tests just leading whitespace
4396 cx.set_state(indoc! {"
4397 «
4398 abc // No indentationˇ»
4399 «\tabc // 1 tabˇ»
4400 \t\tabc « ˇ» // 2 tabs
4401 \t ab«c // Tab followed by space
4402 \tabc // Space followed by tab (3 spaces should be the result)
4403 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4404 abˇ»ˇc ˇ ˇ // Already space indented«
4405 \t
4406 \tabc\tdef // Only the leading tab is manipulatedˇ»
4407 "});
4408 cx.update_editor(|e, window, cx| {
4409 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4410 });
4411 cx.assert_editor_state(
4412 indoc! {"
4413 «
4414 abc // No indentation
4415 abc // 1 tab
4416 abc // 2 tabs
4417 abc // Tab followed by space
4418 abc // Space followed by tab (3 spaces should be the result)
4419 abc // Mixed indentation (tab conversion depends on the column)
4420 abc // Already space indented
4421 ·
4422 abc\tdef // Only the leading tab is manipulatedˇ»
4423 "}
4424 .replace("·", "")
4425 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4426 );
4427
4428 // Test on just a few lines, the others should remain unchanged
4429 // Only lines (3, 5, 10, 11) should change
4430 cx.set_state(
4431 indoc! {"
4432 ·
4433 abc // No indentation
4434 \tabcˇ // 1 tab
4435 \t\tabc // 2 tabs
4436 \t abcˇ // Tab followed by space
4437 \tabc // Space followed by tab (3 spaces should be the result)
4438 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4439 abc // Already space indented
4440 «\t
4441 \tabc\tdef // Only the leading tab is manipulatedˇ»
4442 "}
4443 .replace("·", "")
4444 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4445 );
4446 cx.update_editor(|e, window, cx| {
4447 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4448 });
4449 cx.assert_editor_state(
4450 indoc! {"
4451 ·
4452 abc // No indentation
4453 « abc // 1 tabˇ»
4454 \t\tabc // 2 tabs
4455 « abc // Tab followed by spaceˇ»
4456 \tabc // Space followed by tab (3 spaces should be the result)
4457 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4458 abc // Already space indented
4459 « ·
4460 abc\tdef // Only the leading tab is manipulatedˇ»
4461 "}
4462 .replace("·", "")
4463 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4464 );
4465
4466 // SINGLE SELECTION
4467 // Ln.1 "«" tests empty lines
4468 // Ln.9 tests just leading whitespace
4469 cx.set_state(indoc! {"
4470 «
4471 abc // No indentation
4472 \tabc // 1 tab
4473 \t\tabc // 2 tabs
4474 \t abc // Tab followed by space
4475 \tabc // Space followed by tab (3 spaces should be the result)
4476 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4477 abc // Already space indented
4478 \t
4479 \tabc\tdef // Only the leading tab is manipulatedˇ»
4480 "});
4481 cx.update_editor(|e, window, cx| {
4482 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4483 });
4484 cx.assert_editor_state(
4485 indoc! {"
4486 «
4487 abc // No indentation
4488 abc // 1 tab
4489 abc // 2 tabs
4490 abc // Tab followed by space
4491 abc // Space followed by tab (3 spaces should be the result)
4492 abc // Mixed indentation (tab conversion depends on the column)
4493 abc // Already space indented
4494 ·
4495 abc\tdef // Only the leading tab is manipulatedˇ»
4496 "}
4497 .replace("·", "")
4498 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4499 );
4500}
4501
4502#[gpui::test]
4503async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
4504 init_test(cx, |settings| {
4505 settings.defaults.tab_size = NonZeroU32::new(3)
4506 });
4507
4508 let mut cx = EditorTestContext::new(cx).await;
4509
4510 // MULTI SELECTION
4511 // Ln.1 "«" tests empty lines
4512 // Ln.11 tests just leading whitespace
4513 cx.set_state(indoc! {"
4514 «
4515 abˇ»ˇc // No indentation
4516 abc ˇ ˇ // 1 space (< 3 so dont convert)
4517 abc « // 2 spaces (< 3 so dont convert)
4518 abc // 3 spaces (convert)
4519 abc ˇ» // 5 spaces (1 tab + 2 spaces)
4520 «\tˇ»\t«\tˇ»abc // Already tab indented
4521 «\t abc // Tab followed by space
4522 \tabc // Space followed by tab (should be consumed due to tab)
4523 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4524 \tˇ» «\t
4525 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
4526 "});
4527 cx.update_editor(|e, window, cx| {
4528 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4529 });
4530 cx.assert_editor_state(indoc! {"
4531 «
4532 abc // No indentation
4533 abc // 1 space (< 3 so dont convert)
4534 abc // 2 spaces (< 3 so dont convert)
4535 \tabc // 3 spaces (convert)
4536 \t abc // 5 spaces (1 tab + 2 spaces)
4537 \t\t\tabc // Already tab indented
4538 \t abc // Tab followed by space
4539 \tabc // Space followed by tab (should be consumed due to tab)
4540 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4541 \t\t\t
4542 \tabc \t // Only the leading spaces should be convertedˇ»
4543 "});
4544
4545 // Test on just a few lines, the other should remain unchanged
4546 // Only lines (4, 8, 11, 12) should change
4547 cx.set_state(
4548 indoc! {"
4549 ·
4550 abc // No indentation
4551 abc // 1 space (< 3 so dont convert)
4552 abc // 2 spaces (< 3 so dont convert)
4553 « abc // 3 spaces (convert)ˇ»
4554 abc // 5 spaces (1 tab + 2 spaces)
4555 \t\t\tabc // Already tab indented
4556 \t abc // Tab followed by space
4557 \tabc ˇ // Space followed by tab (should be consumed due to tab)
4558 \t\t \tabc // Mixed indentation
4559 \t \t \t \tabc // Mixed indentation
4560 \t \tˇ
4561 « abc \t // Only the leading spaces should be convertedˇ»
4562 "}
4563 .replace("·", "")
4564 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4565 );
4566 cx.update_editor(|e, window, cx| {
4567 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4568 });
4569 cx.assert_editor_state(
4570 indoc! {"
4571 ·
4572 abc // No indentation
4573 abc // 1 space (< 3 so dont convert)
4574 abc // 2 spaces (< 3 so dont convert)
4575 «\tabc // 3 spaces (convert)ˇ»
4576 abc // 5 spaces (1 tab + 2 spaces)
4577 \t\t\tabc // Already tab indented
4578 \t abc // Tab followed by space
4579 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
4580 \t\t \tabc // Mixed indentation
4581 \t \t \t \tabc // Mixed indentation
4582 «\t\t\t
4583 \tabc \t // Only the leading spaces should be convertedˇ»
4584 "}
4585 .replace("·", "")
4586 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4587 );
4588
4589 // SINGLE SELECTION
4590 // Ln.1 "«" tests empty lines
4591 // Ln.11 tests just leading whitespace
4592 cx.set_state(indoc! {"
4593 «
4594 abc // No indentation
4595 abc // 1 space (< 3 so dont convert)
4596 abc // 2 spaces (< 3 so dont convert)
4597 abc // 3 spaces (convert)
4598 abc // 5 spaces (1 tab + 2 spaces)
4599 \t\t\tabc // Already tab indented
4600 \t abc // Tab followed by space
4601 \tabc // Space followed by tab (should be consumed due to tab)
4602 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4603 \t \t
4604 abc \t // Only the leading spaces should be convertedˇ»
4605 "});
4606 cx.update_editor(|e, window, cx| {
4607 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4608 });
4609 cx.assert_editor_state(indoc! {"
4610 «
4611 abc // No indentation
4612 abc // 1 space (< 3 so dont convert)
4613 abc // 2 spaces (< 3 so dont convert)
4614 \tabc // 3 spaces (convert)
4615 \t abc // 5 spaces (1 tab + 2 spaces)
4616 \t\t\tabc // Already tab indented
4617 \t abc // Tab followed by space
4618 \tabc // Space followed by tab (should be consumed due to tab)
4619 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4620 \t\t\t
4621 \tabc \t // Only the leading spaces should be convertedˇ»
4622 "});
4623}
4624
4625#[gpui::test]
4626async fn test_toggle_case(cx: &mut TestAppContext) {
4627 init_test(cx, |_| {});
4628
4629 let mut cx = EditorTestContext::new(cx).await;
4630
4631 // If all lower case -> upper case
4632 cx.set_state(indoc! {"
4633 «hello worldˇ»
4634 "});
4635 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4636 cx.assert_editor_state(indoc! {"
4637 «HELLO WORLDˇ»
4638 "});
4639
4640 // If all upper case -> lower case
4641 cx.set_state(indoc! {"
4642 «HELLO WORLDˇ»
4643 "});
4644 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4645 cx.assert_editor_state(indoc! {"
4646 «hello worldˇ»
4647 "});
4648
4649 // If any upper case characters are identified -> lower case
4650 // This matches JetBrains IDEs
4651 cx.set_state(indoc! {"
4652 «hEllo worldˇ»
4653 "});
4654 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4655 cx.assert_editor_state(indoc! {"
4656 «hello worldˇ»
4657 "});
4658}
4659
4660#[gpui::test]
4661async fn test_manipulate_text(cx: &mut TestAppContext) {
4662 init_test(cx, |_| {});
4663
4664 let mut cx = EditorTestContext::new(cx).await;
4665
4666 // Test convert_to_upper_case()
4667 cx.set_state(indoc! {"
4668 «hello worldˇ»
4669 "});
4670 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4671 cx.assert_editor_state(indoc! {"
4672 «HELLO WORLDˇ»
4673 "});
4674
4675 // Test convert_to_lower_case()
4676 cx.set_state(indoc! {"
4677 «HELLO WORLDˇ»
4678 "});
4679 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4680 cx.assert_editor_state(indoc! {"
4681 «hello worldˇ»
4682 "});
4683
4684 // Test multiple line, single selection case
4685 cx.set_state(indoc! {"
4686 «The quick brown
4687 fox jumps over
4688 the lazy dogˇ»
4689 "});
4690 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4691 cx.assert_editor_state(indoc! {"
4692 «The Quick Brown
4693 Fox Jumps Over
4694 The Lazy Dogˇ»
4695 "});
4696
4697 // Test multiple line, single selection case
4698 cx.set_state(indoc! {"
4699 «The quick brown
4700 fox jumps over
4701 the lazy dogˇ»
4702 "});
4703 cx.update_editor(|e, window, cx| {
4704 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4705 });
4706 cx.assert_editor_state(indoc! {"
4707 «TheQuickBrown
4708 FoxJumpsOver
4709 TheLazyDogˇ»
4710 "});
4711
4712 // From here on out, test more complex cases of manipulate_text()
4713
4714 // Test no selection case - should affect words cursors are in
4715 // Cursor at beginning, middle, and end of word
4716 cx.set_state(indoc! {"
4717 ˇhello big beauˇtiful worldˇ
4718 "});
4719 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4720 cx.assert_editor_state(indoc! {"
4721 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4722 "});
4723
4724 // Test multiple selections on a single line and across multiple lines
4725 cx.set_state(indoc! {"
4726 «Theˇ» quick «brown
4727 foxˇ» jumps «overˇ»
4728 the «lazyˇ» dog
4729 "});
4730 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4731 cx.assert_editor_state(indoc! {"
4732 «THEˇ» quick «BROWN
4733 FOXˇ» jumps «OVERˇ»
4734 the «LAZYˇ» dog
4735 "});
4736
4737 // Test case where text length grows
4738 cx.set_state(indoc! {"
4739 «tschüߡ»
4740 "});
4741 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4742 cx.assert_editor_state(indoc! {"
4743 «TSCHÜSSˇ»
4744 "});
4745
4746 // Test to make sure we don't crash when text shrinks
4747 cx.set_state(indoc! {"
4748 aaa_bbbˇ
4749 "});
4750 cx.update_editor(|e, window, cx| {
4751 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4752 });
4753 cx.assert_editor_state(indoc! {"
4754 «aaaBbbˇ»
4755 "});
4756
4757 // Test to make sure we all aware of the fact that each word can grow and shrink
4758 // Final selections should be aware of this fact
4759 cx.set_state(indoc! {"
4760 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4761 "});
4762 cx.update_editor(|e, window, cx| {
4763 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4764 });
4765 cx.assert_editor_state(indoc! {"
4766 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4767 "});
4768
4769 cx.set_state(indoc! {"
4770 «hElLo, WoRld!ˇ»
4771 "});
4772 cx.update_editor(|e, window, cx| {
4773 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4774 });
4775 cx.assert_editor_state(indoc! {"
4776 «HeLlO, wOrLD!ˇ»
4777 "});
4778}
4779
4780#[gpui::test]
4781fn test_duplicate_line(cx: &mut TestAppContext) {
4782 init_test(cx, |_| {});
4783
4784 let editor = cx.add_window(|window, cx| {
4785 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4786 build_editor(buffer, window, cx)
4787 });
4788 _ = editor.update(cx, |editor, window, cx| {
4789 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4790 s.select_display_ranges([
4791 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4792 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4793 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4794 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4795 ])
4796 });
4797 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4798 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4799 assert_eq!(
4800 editor.selections.display_ranges(cx),
4801 vec![
4802 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4803 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4804 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4805 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4806 ]
4807 );
4808 });
4809
4810 let editor = cx.add_window(|window, cx| {
4811 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4812 build_editor(buffer, window, cx)
4813 });
4814 _ = editor.update(cx, |editor, window, cx| {
4815 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4816 s.select_display_ranges([
4817 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4818 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4819 ])
4820 });
4821 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4822 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4823 assert_eq!(
4824 editor.selections.display_ranges(cx),
4825 vec![
4826 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4827 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4828 ]
4829 );
4830 });
4831
4832 // With `move_upwards` the selections stay in place, except for
4833 // the lines inserted above them
4834 let editor = cx.add_window(|window, cx| {
4835 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4836 build_editor(buffer, window, cx)
4837 });
4838 _ = editor.update(cx, |editor, window, cx| {
4839 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4840 s.select_display_ranges([
4841 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4842 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4843 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4844 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4845 ])
4846 });
4847 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4848 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4849 assert_eq!(
4850 editor.selections.display_ranges(cx),
4851 vec![
4852 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4853 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4854 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4855 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4856 ]
4857 );
4858 });
4859
4860 let editor = cx.add_window(|window, cx| {
4861 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4862 build_editor(buffer, window, cx)
4863 });
4864 _ = editor.update(cx, |editor, window, cx| {
4865 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4866 s.select_display_ranges([
4867 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4868 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4869 ])
4870 });
4871 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4872 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4873 assert_eq!(
4874 editor.selections.display_ranges(cx),
4875 vec![
4876 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4877 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4878 ]
4879 );
4880 });
4881
4882 let editor = cx.add_window(|window, cx| {
4883 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4884 build_editor(buffer, window, cx)
4885 });
4886 _ = editor.update(cx, |editor, window, cx| {
4887 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4888 s.select_display_ranges([
4889 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4890 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4891 ])
4892 });
4893 editor.duplicate_selection(&DuplicateSelection, window, cx);
4894 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4895 assert_eq!(
4896 editor.selections.display_ranges(cx),
4897 vec![
4898 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4899 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4900 ]
4901 );
4902 });
4903}
4904
4905#[gpui::test]
4906fn test_move_line_up_down(cx: &mut TestAppContext) {
4907 init_test(cx, |_| {});
4908
4909 let editor = cx.add_window(|window, cx| {
4910 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4911 build_editor(buffer, window, cx)
4912 });
4913 _ = editor.update(cx, |editor, window, cx| {
4914 editor.fold_creases(
4915 vec![
4916 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4917 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4918 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4919 ],
4920 true,
4921 window,
4922 cx,
4923 );
4924 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4925 s.select_display_ranges([
4926 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4927 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4928 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4929 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4930 ])
4931 });
4932 assert_eq!(
4933 editor.display_text(cx),
4934 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4935 );
4936
4937 editor.move_line_up(&MoveLineUp, window, cx);
4938 assert_eq!(
4939 editor.display_text(cx),
4940 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4941 );
4942 assert_eq!(
4943 editor.selections.display_ranges(cx),
4944 vec![
4945 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4946 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4947 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4948 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4949 ]
4950 );
4951 });
4952
4953 _ = editor.update(cx, |editor, window, cx| {
4954 editor.move_line_down(&MoveLineDown, window, cx);
4955 assert_eq!(
4956 editor.display_text(cx),
4957 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4958 );
4959 assert_eq!(
4960 editor.selections.display_ranges(cx),
4961 vec![
4962 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4963 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4964 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4965 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4966 ]
4967 );
4968 });
4969
4970 _ = editor.update(cx, |editor, window, cx| {
4971 editor.move_line_down(&MoveLineDown, window, cx);
4972 assert_eq!(
4973 editor.display_text(cx),
4974 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4975 );
4976 assert_eq!(
4977 editor.selections.display_ranges(cx),
4978 vec![
4979 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4980 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4981 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4982 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4983 ]
4984 );
4985 });
4986
4987 _ = editor.update(cx, |editor, window, cx| {
4988 editor.move_line_up(&MoveLineUp, window, cx);
4989 assert_eq!(
4990 editor.display_text(cx),
4991 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4992 );
4993 assert_eq!(
4994 editor.selections.display_ranges(cx),
4995 vec![
4996 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4997 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4998 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4999 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5000 ]
5001 );
5002 });
5003}
5004
5005#[gpui::test]
5006fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5007 init_test(cx, |_| {});
5008
5009 let editor = cx.add_window(|window, cx| {
5010 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5011 build_editor(buffer, window, cx)
5012 });
5013 _ = editor.update(cx, |editor, window, cx| {
5014 let snapshot = editor.buffer.read(cx).snapshot(cx);
5015 editor.insert_blocks(
5016 [BlockProperties {
5017 style: BlockStyle::Fixed,
5018 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5019 height: Some(1),
5020 render: Arc::new(|_| div().into_any()),
5021 priority: 0,
5022 render_in_minimap: true,
5023 }],
5024 Some(Autoscroll::fit()),
5025 cx,
5026 );
5027 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5028 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5029 });
5030 editor.move_line_down(&MoveLineDown, window, cx);
5031 });
5032}
5033
5034#[gpui::test]
5035async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5036 init_test(cx, |_| {});
5037
5038 let mut cx = EditorTestContext::new(cx).await;
5039 cx.set_state(
5040 &"
5041 ˇzero
5042 one
5043 two
5044 three
5045 four
5046 five
5047 "
5048 .unindent(),
5049 );
5050
5051 // Create a four-line block that replaces three lines of text.
5052 cx.update_editor(|editor, window, cx| {
5053 let snapshot = editor.snapshot(window, cx);
5054 let snapshot = &snapshot.buffer_snapshot;
5055 let placement = BlockPlacement::Replace(
5056 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5057 );
5058 editor.insert_blocks(
5059 [BlockProperties {
5060 placement,
5061 height: Some(4),
5062 style: BlockStyle::Sticky,
5063 render: Arc::new(|_| gpui::div().into_any_element()),
5064 priority: 0,
5065 render_in_minimap: true,
5066 }],
5067 None,
5068 cx,
5069 );
5070 });
5071
5072 // Move down so that the cursor touches the block.
5073 cx.update_editor(|editor, window, cx| {
5074 editor.move_down(&Default::default(), window, cx);
5075 });
5076 cx.assert_editor_state(
5077 &"
5078 zero
5079 «one
5080 two
5081 threeˇ»
5082 four
5083 five
5084 "
5085 .unindent(),
5086 );
5087
5088 // Move down past the block.
5089 cx.update_editor(|editor, window, cx| {
5090 editor.move_down(&Default::default(), window, cx);
5091 });
5092 cx.assert_editor_state(
5093 &"
5094 zero
5095 one
5096 two
5097 three
5098 ˇfour
5099 five
5100 "
5101 .unindent(),
5102 );
5103}
5104
5105#[gpui::test]
5106fn test_transpose(cx: &mut TestAppContext) {
5107 init_test(cx, |_| {});
5108
5109 _ = cx.add_window(|window, cx| {
5110 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5111 editor.set_style(EditorStyle::default(), window, cx);
5112 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5113 s.select_ranges([1..1])
5114 });
5115 editor.transpose(&Default::default(), window, cx);
5116 assert_eq!(editor.text(cx), "bac");
5117 assert_eq!(editor.selections.ranges(cx), [2..2]);
5118
5119 editor.transpose(&Default::default(), window, cx);
5120 assert_eq!(editor.text(cx), "bca");
5121 assert_eq!(editor.selections.ranges(cx), [3..3]);
5122
5123 editor.transpose(&Default::default(), window, cx);
5124 assert_eq!(editor.text(cx), "bac");
5125 assert_eq!(editor.selections.ranges(cx), [3..3]);
5126
5127 editor
5128 });
5129
5130 _ = cx.add_window(|window, cx| {
5131 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5132 editor.set_style(EditorStyle::default(), window, cx);
5133 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5134 s.select_ranges([3..3])
5135 });
5136 editor.transpose(&Default::default(), window, cx);
5137 assert_eq!(editor.text(cx), "acb\nde");
5138 assert_eq!(editor.selections.ranges(cx), [3..3]);
5139
5140 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5141 s.select_ranges([4..4])
5142 });
5143 editor.transpose(&Default::default(), window, cx);
5144 assert_eq!(editor.text(cx), "acbd\ne");
5145 assert_eq!(editor.selections.ranges(cx), [5..5]);
5146
5147 editor.transpose(&Default::default(), window, cx);
5148 assert_eq!(editor.text(cx), "acbde\n");
5149 assert_eq!(editor.selections.ranges(cx), [6..6]);
5150
5151 editor.transpose(&Default::default(), window, cx);
5152 assert_eq!(editor.text(cx), "acbd\ne");
5153 assert_eq!(editor.selections.ranges(cx), [6..6]);
5154
5155 editor
5156 });
5157
5158 _ = cx.add_window(|window, cx| {
5159 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5160 editor.set_style(EditorStyle::default(), window, cx);
5161 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5162 s.select_ranges([1..1, 2..2, 4..4])
5163 });
5164 editor.transpose(&Default::default(), window, cx);
5165 assert_eq!(editor.text(cx), "bacd\ne");
5166 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5167
5168 editor.transpose(&Default::default(), window, cx);
5169 assert_eq!(editor.text(cx), "bcade\n");
5170 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5171
5172 editor.transpose(&Default::default(), window, cx);
5173 assert_eq!(editor.text(cx), "bcda\ne");
5174 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5175
5176 editor.transpose(&Default::default(), window, cx);
5177 assert_eq!(editor.text(cx), "bcade\n");
5178 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5179
5180 editor.transpose(&Default::default(), window, cx);
5181 assert_eq!(editor.text(cx), "bcaed\n");
5182 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5183
5184 editor
5185 });
5186
5187 _ = cx.add_window(|window, cx| {
5188 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5189 editor.set_style(EditorStyle::default(), window, cx);
5190 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5191 s.select_ranges([4..4])
5192 });
5193 editor.transpose(&Default::default(), window, cx);
5194 assert_eq!(editor.text(cx), "🏀🍐✋");
5195 assert_eq!(editor.selections.ranges(cx), [8..8]);
5196
5197 editor.transpose(&Default::default(), window, cx);
5198 assert_eq!(editor.text(cx), "🏀✋🍐");
5199 assert_eq!(editor.selections.ranges(cx), [11..11]);
5200
5201 editor.transpose(&Default::default(), window, cx);
5202 assert_eq!(editor.text(cx), "🏀🍐✋");
5203 assert_eq!(editor.selections.ranges(cx), [11..11]);
5204
5205 editor
5206 });
5207}
5208
5209#[gpui::test]
5210async fn test_rewrap(cx: &mut TestAppContext) {
5211 init_test(cx, |settings| {
5212 settings.languages.0.extend([
5213 (
5214 "Markdown".into(),
5215 LanguageSettingsContent {
5216 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5217 preferred_line_length: Some(40),
5218 ..Default::default()
5219 },
5220 ),
5221 (
5222 "Plain Text".into(),
5223 LanguageSettingsContent {
5224 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5225 preferred_line_length: Some(40),
5226 ..Default::default()
5227 },
5228 ),
5229 (
5230 "C++".into(),
5231 LanguageSettingsContent {
5232 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5233 preferred_line_length: Some(40),
5234 ..Default::default()
5235 },
5236 ),
5237 (
5238 "Python".into(),
5239 LanguageSettingsContent {
5240 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5241 preferred_line_length: Some(40),
5242 ..Default::default()
5243 },
5244 ),
5245 (
5246 "Rust".into(),
5247 LanguageSettingsContent {
5248 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5249 preferred_line_length: Some(40),
5250 ..Default::default()
5251 },
5252 ),
5253 ])
5254 });
5255
5256 let mut cx = EditorTestContext::new(cx).await;
5257
5258 let cpp_language = Arc::new(Language::new(
5259 LanguageConfig {
5260 name: "C++".into(),
5261 line_comments: vec!["// ".into()],
5262 ..LanguageConfig::default()
5263 },
5264 None,
5265 ));
5266 let python_language = Arc::new(Language::new(
5267 LanguageConfig {
5268 name: "Python".into(),
5269 line_comments: vec!["# ".into()],
5270 ..LanguageConfig::default()
5271 },
5272 None,
5273 ));
5274 let markdown_language = Arc::new(Language::new(
5275 LanguageConfig {
5276 name: "Markdown".into(),
5277 rewrap_prefixes: vec![
5278 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5279 regex::Regex::new("[-*+]\\s+").unwrap(),
5280 ],
5281 ..LanguageConfig::default()
5282 },
5283 None,
5284 ));
5285 let rust_language = Arc::new(Language::new(
5286 LanguageConfig {
5287 name: "Rust".into(),
5288 line_comments: vec!["// ".into(), "/// ".into()],
5289 ..LanguageConfig::default()
5290 },
5291 Some(tree_sitter_rust::LANGUAGE.into()),
5292 ));
5293
5294 let plaintext_language = Arc::new(Language::new(
5295 LanguageConfig {
5296 name: "Plain Text".into(),
5297 ..LanguageConfig::default()
5298 },
5299 None,
5300 ));
5301
5302 // Test basic rewrapping of a long line with a cursor
5303 assert_rewrap(
5304 indoc! {"
5305 // ˇThis is a long comment that needs to be wrapped.
5306 "},
5307 indoc! {"
5308 // ˇThis is a long comment that needs to
5309 // be wrapped.
5310 "},
5311 cpp_language.clone(),
5312 &mut cx,
5313 );
5314
5315 // Test rewrapping a full selection
5316 assert_rewrap(
5317 indoc! {"
5318 «// This selected long comment needs to be wrapped.ˇ»"
5319 },
5320 indoc! {"
5321 «// This selected long comment needs to
5322 // be wrapped.ˇ»"
5323 },
5324 cpp_language.clone(),
5325 &mut cx,
5326 );
5327
5328 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5329 assert_rewrap(
5330 indoc! {"
5331 // ˇThis is the first line.
5332 // Thisˇ is the second line.
5333 // This is the thirdˇ line, all part of one paragraph.
5334 "},
5335 indoc! {"
5336 // ˇThis is the first line. Thisˇ is the
5337 // second line. This is the thirdˇ line,
5338 // all part of one paragraph.
5339 "},
5340 cpp_language.clone(),
5341 &mut cx,
5342 );
5343
5344 // Test multiple cursors in different paragraphs trigger separate rewraps
5345 assert_rewrap(
5346 indoc! {"
5347 // ˇThis is the first paragraph, first line.
5348 // ˇThis is the first paragraph, second line.
5349
5350 // ˇThis is the second paragraph, first line.
5351 // ˇThis is the second paragraph, second line.
5352 "},
5353 indoc! {"
5354 // ˇThis is the first paragraph, first
5355 // line. ˇThis is the first paragraph,
5356 // second line.
5357
5358 // ˇThis is the second paragraph, first
5359 // line. ˇThis is the second paragraph,
5360 // second line.
5361 "},
5362 cpp_language.clone(),
5363 &mut cx,
5364 );
5365
5366 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
5367 assert_rewrap(
5368 indoc! {"
5369 «// A regular long long comment to be wrapped.
5370 /// A documentation long comment to be wrapped.ˇ»
5371 "},
5372 indoc! {"
5373 «// A regular long long comment to be
5374 // wrapped.
5375 /// A documentation long comment to be
5376 /// wrapped.ˇ»
5377 "},
5378 rust_language.clone(),
5379 &mut cx,
5380 );
5381
5382 // Test that change in indentation level trigger seperate rewraps
5383 assert_rewrap(
5384 indoc! {"
5385 fn foo() {
5386 «// This is a long comment at the base indent.
5387 // This is a long comment at the next indent.ˇ»
5388 }
5389 "},
5390 indoc! {"
5391 fn foo() {
5392 «// This is a long comment at the
5393 // base indent.
5394 // This is a long comment at the
5395 // next indent.ˇ»
5396 }
5397 "},
5398 rust_language.clone(),
5399 &mut cx,
5400 );
5401
5402 // Test that different comment prefix characters (e.g., '#') are handled correctly
5403 assert_rewrap(
5404 indoc! {"
5405 # ˇThis is a long comment using a pound sign.
5406 "},
5407 indoc! {"
5408 # ˇThis is a long comment using a pound
5409 # sign.
5410 "},
5411 python_language.clone(),
5412 &mut cx,
5413 );
5414
5415 // Test rewrapping only affects comments, not code even when selected
5416 assert_rewrap(
5417 indoc! {"
5418 «/// This doc comment is long and should be wrapped.
5419 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5420 "},
5421 indoc! {"
5422 «/// This doc comment is long and should
5423 /// be wrapped.
5424 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5425 "},
5426 rust_language.clone(),
5427 &mut cx,
5428 );
5429
5430 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
5431 assert_rewrap(
5432 indoc! {"
5433 # Header
5434
5435 A long long long line of markdown text to wrap.ˇ
5436 "},
5437 indoc! {"
5438 # Header
5439
5440 A long long long line of markdown text
5441 to wrap.ˇ
5442 "},
5443 markdown_language.clone(),
5444 &mut cx,
5445 );
5446
5447 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
5448 assert_rewrap(
5449 indoc! {"
5450 «1. This is a numbered list item that is very long and needs to be wrapped properly.
5451 2. This is a numbered list item that is very long and needs to be wrapped properly.
5452 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
5453 "},
5454 indoc! {"
5455 «1. This is a numbered list item that is
5456 very long and needs to be wrapped
5457 properly.
5458 2. This is a numbered list item that is
5459 very long and needs to be wrapped
5460 properly.
5461 - This is an unordered list item that is
5462 also very long and should not merge
5463 with the numbered item.ˇ»
5464 "},
5465 markdown_language.clone(),
5466 &mut cx,
5467 );
5468
5469 // Test that rewrapping add indents for rewrapping boundary if not exists already.
5470 assert_rewrap(
5471 indoc! {"
5472 «1. This is a numbered list item that is
5473 very long and needs to be wrapped
5474 properly.
5475 2. This is a numbered list item that is
5476 very long and needs to be wrapped
5477 properly.
5478 - This is an unordered list item that is
5479 also very long and should not merge with
5480 the numbered item.ˇ»
5481 "},
5482 indoc! {"
5483 «1. This is a numbered list item that is
5484 very long and needs to be wrapped
5485 properly.
5486 2. This is a numbered list item that is
5487 very long and needs to be wrapped
5488 properly.
5489 - This is an unordered list item that is
5490 also very long and should not merge
5491 with the numbered item.ˇ»
5492 "},
5493 markdown_language.clone(),
5494 &mut cx,
5495 );
5496
5497 // Test that rewrapping maintain indents even when they already exists.
5498 assert_rewrap(
5499 indoc! {"
5500 «1. This is a numbered list
5501 item that is very long and needs to be wrapped properly.
5502 2. This is a numbered list
5503 item that is very long and needs to be wrapped properly.
5504 - This is an unordered list item that is also very long and
5505 should not merge with the numbered item.ˇ»
5506 "},
5507 indoc! {"
5508 «1. This is a numbered list item that is
5509 very long and needs to be wrapped
5510 properly.
5511 2. This is a numbered list item that is
5512 very long and needs to be wrapped
5513 properly.
5514 - This is an unordered list item that is
5515 also very long and should not merge
5516 with the numbered item.ˇ»
5517 "},
5518 markdown_language.clone(),
5519 &mut cx,
5520 );
5521
5522 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
5523 assert_rewrap(
5524 indoc! {"
5525 ˇThis is a very long line of plain text that will be wrapped.
5526 "},
5527 indoc! {"
5528 ˇThis is a very long line of plain text
5529 that will be wrapped.
5530 "},
5531 plaintext_language.clone(),
5532 &mut cx,
5533 );
5534
5535 // Test that non-commented code acts as a paragraph boundary within a selection
5536 assert_rewrap(
5537 indoc! {"
5538 «// This is the first long comment block to be wrapped.
5539 fn my_func(a: u32);
5540 // This is the second long comment block to be wrapped.ˇ»
5541 "},
5542 indoc! {"
5543 «// This is the first long comment block
5544 // to be wrapped.
5545 fn my_func(a: u32);
5546 // This is the second long comment block
5547 // to be wrapped.ˇ»
5548 "},
5549 rust_language.clone(),
5550 &mut cx,
5551 );
5552
5553 // Test rewrapping multiple selections, including ones with blank lines or tabs
5554 assert_rewrap(
5555 indoc! {"
5556 «ˇThis is a very long line that will be wrapped.
5557
5558 This is another paragraph in the same selection.»
5559
5560 «\tThis is a very long indented line that will be wrapped.ˇ»
5561 "},
5562 indoc! {"
5563 «ˇThis is a very long line that will be
5564 wrapped.
5565
5566 This is another paragraph in the same
5567 selection.»
5568
5569 «\tThis is a very long indented line
5570 \tthat will be wrapped.ˇ»
5571 "},
5572 plaintext_language.clone(),
5573 &mut cx,
5574 );
5575
5576 // Test that an empty comment line acts as a paragraph boundary
5577 assert_rewrap(
5578 indoc! {"
5579 // ˇThis is a long comment that will be wrapped.
5580 //
5581 // And this is another long comment that will also be wrapped.ˇ
5582 "},
5583 indoc! {"
5584 // ˇThis is a long comment that will be
5585 // wrapped.
5586 //
5587 // And this is another long comment that
5588 // will also be wrapped.ˇ
5589 "},
5590 cpp_language,
5591 &mut cx,
5592 );
5593
5594 #[track_caller]
5595 fn assert_rewrap(
5596 unwrapped_text: &str,
5597 wrapped_text: &str,
5598 language: Arc<Language>,
5599 cx: &mut EditorTestContext,
5600 ) {
5601 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5602 cx.set_state(unwrapped_text);
5603 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5604 cx.assert_editor_state(wrapped_text);
5605 }
5606}
5607
5608#[gpui::test]
5609async fn test_hard_wrap(cx: &mut TestAppContext) {
5610 init_test(cx, |_| {});
5611 let mut cx = EditorTestContext::new(cx).await;
5612
5613 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5614 cx.update_editor(|editor, _, cx| {
5615 editor.set_hard_wrap(Some(14), cx);
5616 });
5617
5618 cx.set_state(indoc!(
5619 "
5620 one two three ˇ
5621 "
5622 ));
5623 cx.simulate_input("four");
5624 cx.run_until_parked();
5625
5626 cx.assert_editor_state(indoc!(
5627 "
5628 one two three
5629 fourˇ
5630 "
5631 ));
5632
5633 cx.update_editor(|editor, window, cx| {
5634 editor.newline(&Default::default(), window, cx);
5635 });
5636 cx.run_until_parked();
5637 cx.assert_editor_state(indoc!(
5638 "
5639 one two three
5640 four
5641 ˇ
5642 "
5643 ));
5644
5645 cx.simulate_input("five");
5646 cx.run_until_parked();
5647 cx.assert_editor_state(indoc!(
5648 "
5649 one two three
5650 four
5651 fiveˇ
5652 "
5653 ));
5654
5655 cx.update_editor(|editor, window, cx| {
5656 editor.newline(&Default::default(), window, cx);
5657 });
5658 cx.run_until_parked();
5659 cx.simulate_input("# ");
5660 cx.run_until_parked();
5661 cx.assert_editor_state(indoc!(
5662 "
5663 one two three
5664 four
5665 five
5666 # ˇ
5667 "
5668 ));
5669
5670 cx.update_editor(|editor, window, cx| {
5671 editor.newline(&Default::default(), window, cx);
5672 });
5673 cx.run_until_parked();
5674 cx.assert_editor_state(indoc!(
5675 "
5676 one two three
5677 four
5678 five
5679 #\x20
5680 #ˇ
5681 "
5682 ));
5683
5684 cx.simulate_input(" 6");
5685 cx.run_until_parked();
5686 cx.assert_editor_state(indoc!(
5687 "
5688 one two three
5689 four
5690 five
5691 #
5692 # 6ˇ
5693 "
5694 ));
5695}
5696
5697#[gpui::test]
5698async fn test_clipboard(cx: &mut TestAppContext) {
5699 init_test(cx, |_| {});
5700
5701 let mut cx = EditorTestContext::new(cx).await;
5702
5703 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5704 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5705 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5706
5707 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5708 cx.set_state("two ˇfour ˇsix ˇ");
5709 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5710 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5711
5712 // Paste again but with only two cursors. Since the number of cursors doesn't
5713 // match the number of slices in the clipboard, the entire clipboard text
5714 // is pasted at each cursor.
5715 cx.set_state("ˇtwo one✅ four three six five ˇ");
5716 cx.update_editor(|e, window, cx| {
5717 e.handle_input("( ", window, cx);
5718 e.paste(&Paste, window, cx);
5719 e.handle_input(") ", window, cx);
5720 });
5721 cx.assert_editor_state(
5722 &([
5723 "( one✅ ",
5724 "three ",
5725 "five ) ˇtwo one✅ four three six five ( one✅ ",
5726 "three ",
5727 "five ) ˇ",
5728 ]
5729 .join("\n")),
5730 );
5731
5732 // Cut with three selections, one of which is full-line.
5733 cx.set_state(indoc! {"
5734 1«2ˇ»3
5735 4ˇ567
5736 «8ˇ»9"});
5737 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5738 cx.assert_editor_state(indoc! {"
5739 1ˇ3
5740 ˇ9"});
5741
5742 // Paste with three selections, noticing how the copied selection that was full-line
5743 // gets inserted before the second cursor.
5744 cx.set_state(indoc! {"
5745 1ˇ3
5746 9ˇ
5747 «oˇ»ne"});
5748 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5749 cx.assert_editor_state(indoc! {"
5750 12ˇ3
5751 4567
5752 9ˇ
5753 8ˇne"});
5754
5755 // Copy with a single cursor only, which writes the whole line into the clipboard.
5756 cx.set_state(indoc! {"
5757 The quick brown
5758 fox juˇmps over
5759 the lazy dog"});
5760 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5761 assert_eq!(
5762 cx.read_from_clipboard()
5763 .and_then(|item| item.text().as_deref().map(str::to_string)),
5764 Some("fox jumps over\n".to_string())
5765 );
5766
5767 // Paste with three selections, noticing how the copied full-line selection is inserted
5768 // before the empty selections but replaces the selection that is non-empty.
5769 cx.set_state(indoc! {"
5770 Tˇhe quick brown
5771 «foˇ»x jumps over
5772 tˇhe lazy dog"});
5773 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5774 cx.assert_editor_state(indoc! {"
5775 fox jumps over
5776 Tˇhe quick brown
5777 fox jumps over
5778 ˇx jumps over
5779 fox jumps over
5780 tˇhe lazy dog"});
5781}
5782
5783#[gpui::test]
5784async fn test_copy_trim(cx: &mut TestAppContext) {
5785 init_test(cx, |_| {});
5786
5787 let mut cx = EditorTestContext::new(cx).await;
5788 cx.set_state(
5789 r#" «for selection in selections.iter() {
5790 let mut start = selection.start;
5791 let mut end = selection.end;
5792 let is_entire_line = selection.is_empty();
5793 if is_entire_line {
5794 start = Point::new(start.row, 0);ˇ»
5795 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5796 }
5797 "#,
5798 );
5799 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5800 assert_eq!(
5801 cx.read_from_clipboard()
5802 .and_then(|item| item.text().as_deref().map(str::to_string)),
5803 Some(
5804 "for selection in selections.iter() {
5805 let mut start = selection.start;
5806 let mut end = selection.end;
5807 let is_entire_line = selection.is_empty();
5808 if is_entire_line {
5809 start = Point::new(start.row, 0);"
5810 .to_string()
5811 ),
5812 "Regular copying preserves all indentation selected",
5813 );
5814 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5815 assert_eq!(
5816 cx.read_from_clipboard()
5817 .and_then(|item| item.text().as_deref().map(str::to_string)),
5818 Some(
5819 "for selection in selections.iter() {
5820let mut start = selection.start;
5821let mut end = selection.end;
5822let is_entire_line = selection.is_empty();
5823if is_entire_line {
5824 start = Point::new(start.row, 0);"
5825 .to_string()
5826 ),
5827 "Copying with stripping should strip all leading whitespaces"
5828 );
5829
5830 cx.set_state(
5831 r#" « for selection in selections.iter() {
5832 let mut start = selection.start;
5833 let mut end = selection.end;
5834 let is_entire_line = selection.is_empty();
5835 if is_entire_line {
5836 start = Point::new(start.row, 0);ˇ»
5837 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5838 }
5839 "#,
5840 );
5841 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5842 assert_eq!(
5843 cx.read_from_clipboard()
5844 .and_then(|item| item.text().as_deref().map(str::to_string)),
5845 Some(
5846 " for selection in selections.iter() {
5847 let mut start = selection.start;
5848 let mut end = selection.end;
5849 let is_entire_line = selection.is_empty();
5850 if is_entire_line {
5851 start = Point::new(start.row, 0);"
5852 .to_string()
5853 ),
5854 "Regular copying preserves all indentation selected",
5855 );
5856 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5857 assert_eq!(
5858 cx.read_from_clipboard()
5859 .and_then(|item| item.text().as_deref().map(str::to_string)),
5860 Some(
5861 "for selection in selections.iter() {
5862let mut start = selection.start;
5863let mut end = selection.end;
5864let is_entire_line = selection.is_empty();
5865if is_entire_line {
5866 start = Point::new(start.row, 0);"
5867 .to_string()
5868 ),
5869 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5870 );
5871
5872 cx.set_state(
5873 r#" «ˇ for selection in selections.iter() {
5874 let mut start = selection.start;
5875 let mut end = selection.end;
5876 let is_entire_line = selection.is_empty();
5877 if is_entire_line {
5878 start = Point::new(start.row, 0);»
5879 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5880 }
5881 "#,
5882 );
5883 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5884 assert_eq!(
5885 cx.read_from_clipboard()
5886 .and_then(|item| item.text().as_deref().map(str::to_string)),
5887 Some(
5888 " for selection in selections.iter() {
5889 let mut start = selection.start;
5890 let mut end = selection.end;
5891 let is_entire_line = selection.is_empty();
5892 if is_entire_line {
5893 start = Point::new(start.row, 0);"
5894 .to_string()
5895 ),
5896 "Regular copying for reverse selection works the same",
5897 );
5898 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5899 assert_eq!(
5900 cx.read_from_clipboard()
5901 .and_then(|item| item.text().as_deref().map(str::to_string)),
5902 Some(
5903 "for selection in selections.iter() {
5904let mut start = selection.start;
5905let mut end = selection.end;
5906let is_entire_line = selection.is_empty();
5907if is_entire_line {
5908 start = Point::new(start.row, 0);"
5909 .to_string()
5910 ),
5911 "Copying with stripping for reverse selection works the same"
5912 );
5913
5914 cx.set_state(
5915 r#" for selection «in selections.iter() {
5916 let mut start = selection.start;
5917 let mut end = selection.end;
5918 let is_entire_line = selection.is_empty();
5919 if is_entire_line {
5920 start = Point::new(start.row, 0);ˇ»
5921 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5922 }
5923 "#,
5924 );
5925 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5926 assert_eq!(
5927 cx.read_from_clipboard()
5928 .and_then(|item| item.text().as_deref().map(str::to_string)),
5929 Some(
5930 "in selections.iter() {
5931 let mut start = selection.start;
5932 let mut end = selection.end;
5933 let is_entire_line = selection.is_empty();
5934 if is_entire_line {
5935 start = Point::new(start.row, 0);"
5936 .to_string()
5937 ),
5938 "When selecting past the indent, the copying works as usual",
5939 );
5940 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5941 assert_eq!(
5942 cx.read_from_clipboard()
5943 .and_then(|item| item.text().as_deref().map(str::to_string)),
5944 Some(
5945 "in selections.iter() {
5946 let mut start = selection.start;
5947 let mut end = selection.end;
5948 let is_entire_line = selection.is_empty();
5949 if is_entire_line {
5950 start = Point::new(start.row, 0);"
5951 .to_string()
5952 ),
5953 "When selecting past the indent, nothing is trimmed"
5954 );
5955
5956 cx.set_state(
5957 r#" «for selection in selections.iter() {
5958 let mut start = selection.start;
5959
5960 let mut end = selection.end;
5961 let is_entire_line = selection.is_empty();
5962 if is_entire_line {
5963 start = Point::new(start.row, 0);
5964ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5965 }
5966 "#,
5967 );
5968 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5969 assert_eq!(
5970 cx.read_from_clipboard()
5971 .and_then(|item| item.text().as_deref().map(str::to_string)),
5972 Some(
5973 "for selection in selections.iter() {
5974let mut start = selection.start;
5975
5976let mut end = selection.end;
5977let is_entire_line = selection.is_empty();
5978if is_entire_line {
5979 start = Point::new(start.row, 0);
5980"
5981 .to_string()
5982 ),
5983 "Copying with stripping should ignore empty lines"
5984 );
5985}
5986
5987#[gpui::test]
5988async fn test_paste_multiline(cx: &mut TestAppContext) {
5989 init_test(cx, |_| {});
5990
5991 let mut cx = EditorTestContext::new(cx).await;
5992 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5993
5994 // Cut an indented block, without the leading whitespace.
5995 cx.set_state(indoc! {"
5996 const a: B = (
5997 c(),
5998 «d(
5999 e,
6000 f
6001 )ˇ»
6002 );
6003 "});
6004 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6005 cx.assert_editor_state(indoc! {"
6006 const a: B = (
6007 c(),
6008 ˇ
6009 );
6010 "});
6011
6012 // Paste it at the same position.
6013 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6014 cx.assert_editor_state(indoc! {"
6015 const a: B = (
6016 c(),
6017 d(
6018 e,
6019 f
6020 )ˇ
6021 );
6022 "});
6023
6024 // Paste it at a line with a lower indent level.
6025 cx.set_state(indoc! {"
6026 ˇ
6027 const a: B = (
6028 c(),
6029 );
6030 "});
6031 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6032 cx.assert_editor_state(indoc! {"
6033 d(
6034 e,
6035 f
6036 )ˇ
6037 const a: B = (
6038 c(),
6039 );
6040 "});
6041
6042 // Cut an indented block, with the leading whitespace.
6043 cx.set_state(indoc! {"
6044 const a: B = (
6045 c(),
6046 « d(
6047 e,
6048 f
6049 )
6050 ˇ»);
6051 "});
6052 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6053 cx.assert_editor_state(indoc! {"
6054 const a: B = (
6055 c(),
6056 ˇ);
6057 "});
6058
6059 // Paste it at the same position.
6060 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6061 cx.assert_editor_state(indoc! {"
6062 const a: B = (
6063 c(),
6064 d(
6065 e,
6066 f
6067 )
6068 ˇ);
6069 "});
6070
6071 // Paste it at a line with a higher indent level.
6072 cx.set_state(indoc! {"
6073 const a: B = (
6074 c(),
6075 d(
6076 e,
6077 fˇ
6078 )
6079 );
6080 "});
6081 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6082 cx.assert_editor_state(indoc! {"
6083 const a: B = (
6084 c(),
6085 d(
6086 e,
6087 f d(
6088 e,
6089 f
6090 )
6091 ˇ
6092 )
6093 );
6094 "});
6095
6096 // Copy an indented block, starting mid-line
6097 cx.set_state(indoc! {"
6098 const a: B = (
6099 c(),
6100 somethin«g(
6101 e,
6102 f
6103 )ˇ»
6104 );
6105 "});
6106 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6107
6108 // Paste it on a line with a lower indent level
6109 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
6110 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6111 cx.assert_editor_state(indoc! {"
6112 const a: B = (
6113 c(),
6114 something(
6115 e,
6116 f
6117 )
6118 );
6119 g(
6120 e,
6121 f
6122 )ˇ"});
6123}
6124
6125#[gpui::test]
6126async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
6127 init_test(cx, |_| {});
6128
6129 cx.write_to_clipboard(ClipboardItem::new_string(
6130 " d(\n e\n );\n".into(),
6131 ));
6132
6133 let mut cx = EditorTestContext::new(cx).await;
6134 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6135
6136 cx.set_state(indoc! {"
6137 fn a() {
6138 b();
6139 if c() {
6140 ˇ
6141 }
6142 }
6143 "});
6144
6145 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6146 cx.assert_editor_state(indoc! {"
6147 fn a() {
6148 b();
6149 if c() {
6150 d(
6151 e
6152 );
6153 ˇ
6154 }
6155 }
6156 "});
6157
6158 cx.set_state(indoc! {"
6159 fn a() {
6160 b();
6161 ˇ
6162 }
6163 "});
6164
6165 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6166 cx.assert_editor_state(indoc! {"
6167 fn a() {
6168 b();
6169 d(
6170 e
6171 );
6172 ˇ
6173 }
6174 "});
6175}
6176
6177#[gpui::test]
6178fn test_select_all(cx: &mut TestAppContext) {
6179 init_test(cx, |_| {});
6180
6181 let editor = cx.add_window(|window, cx| {
6182 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
6183 build_editor(buffer, window, cx)
6184 });
6185 _ = editor.update(cx, |editor, window, cx| {
6186 editor.select_all(&SelectAll, window, cx);
6187 assert_eq!(
6188 editor.selections.display_ranges(cx),
6189 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
6190 );
6191 });
6192}
6193
6194#[gpui::test]
6195fn test_select_line(cx: &mut TestAppContext) {
6196 init_test(cx, |_| {});
6197
6198 let editor = cx.add_window(|window, cx| {
6199 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
6200 build_editor(buffer, window, cx)
6201 });
6202 _ = editor.update(cx, |editor, window, cx| {
6203 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6204 s.select_display_ranges([
6205 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6206 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6207 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6208 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
6209 ])
6210 });
6211 editor.select_line(&SelectLine, window, cx);
6212 assert_eq!(
6213 editor.selections.display_ranges(cx),
6214 vec![
6215 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
6216 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
6217 ]
6218 );
6219 });
6220
6221 _ = editor.update(cx, |editor, window, cx| {
6222 editor.select_line(&SelectLine, window, cx);
6223 assert_eq!(
6224 editor.selections.display_ranges(cx),
6225 vec![
6226 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
6227 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
6228 ]
6229 );
6230 });
6231
6232 _ = editor.update(cx, |editor, window, cx| {
6233 editor.select_line(&SelectLine, window, cx);
6234 assert_eq!(
6235 editor.selections.display_ranges(cx),
6236 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
6237 );
6238 });
6239}
6240
6241#[gpui::test]
6242async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
6243 init_test(cx, |_| {});
6244 let mut cx = EditorTestContext::new(cx).await;
6245
6246 #[track_caller]
6247 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
6248 cx.set_state(initial_state);
6249 cx.update_editor(|e, window, cx| {
6250 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
6251 });
6252 cx.assert_editor_state(expected_state);
6253 }
6254
6255 // Selection starts and ends at the middle of lines, left-to-right
6256 test(
6257 &mut cx,
6258 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
6259 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6260 );
6261 // Same thing, right-to-left
6262 test(
6263 &mut cx,
6264 "aa\nb«b\ncc\ndd\neˇ»e\nff",
6265 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6266 );
6267
6268 // Whole buffer, left-to-right, last line *doesn't* end with newline
6269 test(
6270 &mut cx,
6271 "«ˇaa\nbb\ncc\ndd\nee\nff»",
6272 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6273 );
6274 // Same thing, right-to-left
6275 test(
6276 &mut cx,
6277 "«aa\nbb\ncc\ndd\nee\nffˇ»",
6278 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6279 );
6280
6281 // Whole buffer, left-to-right, last line ends with newline
6282 test(
6283 &mut cx,
6284 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
6285 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6286 );
6287 // Same thing, right-to-left
6288 test(
6289 &mut cx,
6290 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
6291 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6292 );
6293
6294 // Starts at the end of a line, ends at the start of another
6295 test(
6296 &mut cx,
6297 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
6298 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
6299 );
6300}
6301
6302#[gpui::test]
6303async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
6304 init_test(cx, |_| {});
6305
6306 let editor = cx.add_window(|window, cx| {
6307 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
6308 build_editor(buffer, window, cx)
6309 });
6310
6311 // setup
6312 _ = editor.update(cx, |editor, window, cx| {
6313 editor.fold_creases(
6314 vec![
6315 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
6316 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
6317 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
6318 ],
6319 true,
6320 window,
6321 cx,
6322 );
6323 assert_eq!(
6324 editor.display_text(cx),
6325 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6326 );
6327 });
6328
6329 _ = editor.update(cx, |editor, window, cx| {
6330 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6331 s.select_display_ranges([
6332 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6333 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6334 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6335 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
6336 ])
6337 });
6338 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6339 assert_eq!(
6340 editor.display_text(cx),
6341 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6342 );
6343 });
6344 EditorTestContext::for_editor(editor, cx)
6345 .await
6346 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
6347
6348 _ = editor.update(cx, |editor, window, cx| {
6349 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6350 s.select_display_ranges([
6351 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
6352 ])
6353 });
6354 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6355 assert_eq!(
6356 editor.display_text(cx),
6357 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
6358 );
6359 assert_eq!(
6360 editor.selections.display_ranges(cx),
6361 [
6362 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
6363 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
6364 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
6365 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
6366 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
6367 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
6368 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
6369 ]
6370 );
6371 });
6372 EditorTestContext::for_editor(editor, cx)
6373 .await
6374 .assert_editor_state(
6375 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
6376 );
6377}
6378
6379#[gpui::test]
6380async fn test_add_selection_above_below(cx: &mut TestAppContext) {
6381 init_test(cx, |_| {});
6382
6383 let mut cx = EditorTestContext::new(cx).await;
6384
6385 cx.set_state(indoc!(
6386 r#"abc
6387 defˇghi
6388
6389 jk
6390 nlmo
6391 "#
6392 ));
6393
6394 cx.update_editor(|editor, window, cx| {
6395 editor.add_selection_above(&Default::default(), window, cx);
6396 });
6397
6398 cx.assert_editor_state(indoc!(
6399 r#"abcˇ
6400 defˇghi
6401
6402 jk
6403 nlmo
6404 "#
6405 ));
6406
6407 cx.update_editor(|editor, window, cx| {
6408 editor.add_selection_above(&Default::default(), window, cx);
6409 });
6410
6411 cx.assert_editor_state(indoc!(
6412 r#"abcˇ
6413 defˇghi
6414
6415 jk
6416 nlmo
6417 "#
6418 ));
6419
6420 cx.update_editor(|editor, window, cx| {
6421 editor.add_selection_below(&Default::default(), window, cx);
6422 });
6423
6424 cx.assert_editor_state(indoc!(
6425 r#"abc
6426 defˇghi
6427
6428 jk
6429 nlmo
6430 "#
6431 ));
6432
6433 cx.update_editor(|editor, window, cx| {
6434 editor.undo_selection(&Default::default(), window, cx);
6435 });
6436
6437 cx.assert_editor_state(indoc!(
6438 r#"abcˇ
6439 defˇghi
6440
6441 jk
6442 nlmo
6443 "#
6444 ));
6445
6446 cx.update_editor(|editor, window, cx| {
6447 editor.redo_selection(&Default::default(), window, cx);
6448 });
6449
6450 cx.assert_editor_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_below(&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_below(&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 nlmˇo
6495 "#
6496 ));
6497
6498 cx.update_editor(|editor, window, cx| {
6499 editor.add_selection_below(&Default::default(), window, cx);
6500 });
6501
6502 cx.assert_editor_state(indoc!(
6503 r#"abc
6504 defˇghi
6505 ˇ
6506 jkˇ
6507 nlmˇo
6508 ˇ"#
6509 ));
6510
6511 // change selections
6512 cx.set_state(indoc!(
6513 r#"abc
6514 def«ˇg»hi
6515
6516 jk
6517 nlmo
6518 "#
6519 ));
6520
6521 cx.update_editor(|editor, window, cx| {
6522 editor.add_selection_below(&Default::default(), window, cx);
6523 });
6524
6525 cx.assert_editor_state(indoc!(
6526 r#"abc
6527 def«ˇg»hi
6528
6529 jk
6530 nlm«ˇo»
6531 "#
6532 ));
6533
6534 cx.update_editor(|editor, window, cx| {
6535 editor.add_selection_below(&Default::default(), window, cx);
6536 });
6537
6538 cx.assert_editor_state(indoc!(
6539 r#"abc
6540 def«ˇg»hi
6541
6542 jk
6543 nlm«ˇo»
6544 "#
6545 ));
6546
6547 cx.update_editor(|editor, window, cx| {
6548 editor.add_selection_above(&Default::default(), window, cx);
6549 });
6550
6551 cx.assert_editor_state(indoc!(
6552 r#"abc
6553 def«ˇg»hi
6554
6555 jk
6556 nlmo
6557 "#
6558 ));
6559
6560 cx.update_editor(|editor, window, cx| {
6561 editor.add_selection_above(&Default::default(), window, cx);
6562 });
6563
6564 cx.assert_editor_state(indoc!(
6565 r#"abc
6566 def«ˇg»hi
6567
6568 jk
6569 nlmo
6570 "#
6571 ));
6572
6573 // Change selections again
6574 cx.set_state(indoc!(
6575 r#"a«bc
6576 defgˇ»hi
6577
6578 jk
6579 nlmo
6580 "#
6581 ));
6582
6583 cx.update_editor(|editor, window, cx| {
6584 editor.add_selection_below(&Default::default(), window, cx);
6585 });
6586
6587 cx.assert_editor_state(indoc!(
6588 r#"a«bcˇ»
6589 d«efgˇ»hi
6590
6591 j«kˇ»
6592 nlmo
6593 "#
6594 ));
6595
6596 cx.update_editor(|editor, window, cx| {
6597 editor.add_selection_below(&Default::default(), window, cx);
6598 });
6599 cx.assert_editor_state(indoc!(
6600 r#"a«bcˇ»
6601 d«efgˇ»hi
6602
6603 j«kˇ»
6604 n«lmoˇ»
6605 "#
6606 ));
6607 cx.update_editor(|editor, window, cx| {
6608 editor.add_selection_above(&Default::default(), window, cx);
6609 });
6610
6611 cx.assert_editor_state(indoc!(
6612 r#"a«bcˇ»
6613 d«efgˇ»hi
6614
6615 j«kˇ»
6616 nlmo
6617 "#
6618 ));
6619
6620 // Change selections again
6621 cx.set_state(indoc!(
6622 r#"abc
6623 d«ˇefghi
6624
6625 jk
6626 nlm»o
6627 "#
6628 ));
6629
6630 cx.update_editor(|editor, window, cx| {
6631 editor.add_selection_above(&Default::default(), window, cx);
6632 });
6633
6634 cx.assert_editor_state(indoc!(
6635 r#"a«ˇbc»
6636 d«ˇef»ghi
6637
6638 j«ˇk»
6639 n«ˇlm»o
6640 "#
6641 ));
6642
6643 cx.update_editor(|editor, window, cx| {
6644 editor.add_selection_below(&Default::default(), window, cx);
6645 });
6646
6647 cx.assert_editor_state(indoc!(
6648 r#"abc
6649 d«ˇef»ghi
6650
6651 j«ˇk»
6652 n«ˇlm»o
6653 "#
6654 ));
6655}
6656
6657#[gpui::test]
6658async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
6659 init_test(cx, |_| {});
6660 let mut cx = EditorTestContext::new(cx).await;
6661
6662 cx.set_state(indoc!(
6663 r#"line onˇe
6664 liˇne two
6665 line three
6666 line four"#
6667 ));
6668
6669 cx.update_editor(|editor, window, cx| {
6670 editor.add_selection_below(&Default::default(), window, cx);
6671 });
6672
6673 // test multiple cursors expand in the same direction
6674 cx.assert_editor_state(indoc!(
6675 r#"line onˇe
6676 liˇne twˇo
6677 liˇne three
6678 line four"#
6679 ));
6680
6681 cx.update_editor(|editor, window, cx| {
6682 editor.add_selection_below(&Default::default(), window, cx);
6683 });
6684
6685 cx.update_editor(|editor, window, cx| {
6686 editor.add_selection_below(&Default::default(), window, cx);
6687 });
6688
6689 // test multiple cursors expand below overflow
6690 cx.assert_editor_state(indoc!(
6691 r#"line onˇe
6692 liˇne twˇo
6693 liˇne thˇree
6694 liˇne foˇur"#
6695 ));
6696
6697 cx.update_editor(|editor, window, cx| {
6698 editor.add_selection_above(&Default::default(), window, cx);
6699 });
6700
6701 // test multiple cursors retrieves back correctly
6702 cx.assert_editor_state(indoc!(
6703 r#"line onˇe
6704 liˇne twˇo
6705 liˇne thˇree
6706 line four"#
6707 ));
6708
6709 cx.update_editor(|editor, window, cx| {
6710 editor.add_selection_above(&Default::default(), window, cx);
6711 });
6712
6713 cx.update_editor(|editor, window, cx| {
6714 editor.add_selection_above(&Default::default(), window, cx);
6715 });
6716
6717 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
6718 cx.assert_editor_state(indoc!(
6719 r#"liˇne onˇe
6720 liˇne two
6721 line three
6722 line four"#
6723 ));
6724
6725 cx.update_editor(|editor, window, cx| {
6726 editor.undo_selection(&Default::default(), window, cx);
6727 });
6728
6729 // test undo
6730 cx.assert_editor_state(indoc!(
6731 r#"line onˇe
6732 liˇne twˇo
6733 line three
6734 line four"#
6735 ));
6736
6737 cx.update_editor(|editor, window, cx| {
6738 editor.redo_selection(&Default::default(), window, cx);
6739 });
6740
6741 // test redo
6742 cx.assert_editor_state(indoc!(
6743 r#"liˇne onˇe
6744 liˇne two
6745 line three
6746 line four"#
6747 ));
6748
6749 cx.set_state(indoc!(
6750 r#"abcd
6751 ef«ghˇ»
6752 ijkl
6753 «mˇ»nop"#
6754 ));
6755
6756 cx.update_editor(|editor, window, cx| {
6757 editor.add_selection_above(&Default::default(), window, cx);
6758 });
6759
6760 // test multiple selections expand in the same direction
6761 cx.assert_editor_state(indoc!(
6762 r#"ab«cdˇ»
6763 ef«ghˇ»
6764 «iˇ»jkl
6765 «mˇ»nop"#
6766 ));
6767
6768 cx.update_editor(|editor, window, cx| {
6769 editor.add_selection_above(&Default::default(), window, cx);
6770 });
6771
6772 // test multiple selection upward overflow
6773 cx.assert_editor_state(indoc!(
6774 r#"ab«cdˇ»
6775 «eˇ»f«ghˇ»
6776 «iˇ»jkl
6777 «mˇ»nop"#
6778 ));
6779
6780 cx.update_editor(|editor, window, cx| {
6781 editor.add_selection_below(&Default::default(), window, cx);
6782 });
6783
6784 // test multiple selection retrieves back correctly
6785 cx.assert_editor_state(indoc!(
6786 r#"abcd
6787 ef«ghˇ»
6788 «iˇ»jkl
6789 «mˇ»nop"#
6790 ));
6791
6792 cx.update_editor(|editor, window, cx| {
6793 editor.add_selection_below(&Default::default(), window, cx);
6794 });
6795
6796 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
6797 cx.assert_editor_state(indoc!(
6798 r#"abcd
6799 ef«ghˇ»
6800 ij«klˇ»
6801 «mˇ»nop"#
6802 ));
6803
6804 cx.update_editor(|editor, window, cx| {
6805 editor.undo_selection(&Default::default(), window, cx);
6806 });
6807
6808 // test undo
6809 cx.assert_editor_state(indoc!(
6810 r#"abcd
6811 ef«ghˇ»
6812 «iˇ»jkl
6813 «mˇ»nop"#
6814 ));
6815
6816 cx.update_editor(|editor, window, cx| {
6817 editor.redo_selection(&Default::default(), window, cx);
6818 });
6819
6820 // test redo
6821 cx.assert_editor_state(indoc!(
6822 r#"abcd
6823 ef«ghˇ»
6824 ij«klˇ»
6825 «mˇ»nop"#
6826 ));
6827}
6828
6829#[gpui::test]
6830async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
6831 init_test(cx, |_| {});
6832 let mut cx = EditorTestContext::new(cx).await;
6833
6834 cx.set_state(indoc!(
6835 r#"line onˇe
6836 liˇne two
6837 line three
6838 line four"#
6839 ));
6840
6841 cx.update_editor(|editor, window, cx| {
6842 editor.add_selection_below(&Default::default(), window, cx);
6843 editor.add_selection_below(&Default::default(), window, cx);
6844 editor.add_selection_below(&Default::default(), window, cx);
6845 });
6846
6847 // initial state with two multi cursor groups
6848 cx.assert_editor_state(indoc!(
6849 r#"line onˇe
6850 liˇne twˇo
6851 liˇne thˇree
6852 liˇne foˇur"#
6853 ));
6854
6855 // add single cursor in middle - simulate opt click
6856 cx.update_editor(|editor, window, cx| {
6857 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
6858 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6859 editor.end_selection(window, cx);
6860 });
6861
6862 cx.assert_editor_state(indoc!(
6863 r#"line onˇe
6864 liˇne twˇo
6865 liˇneˇ thˇree
6866 liˇne foˇur"#
6867 ));
6868
6869 cx.update_editor(|editor, window, cx| {
6870 editor.add_selection_above(&Default::default(), window, cx);
6871 });
6872
6873 // test new added selection expands above and existing selection shrinks
6874 cx.assert_editor_state(indoc!(
6875 r#"line onˇe
6876 liˇneˇ twˇo
6877 liˇneˇ thˇree
6878 line four"#
6879 ));
6880
6881 cx.update_editor(|editor, window, cx| {
6882 editor.add_selection_above(&Default::default(), window, cx);
6883 });
6884
6885 // test new added selection expands above and existing selection shrinks
6886 cx.assert_editor_state(indoc!(
6887 r#"lineˇ onˇe
6888 liˇneˇ twˇo
6889 lineˇ three
6890 line four"#
6891 ));
6892
6893 // intial state with two selection groups
6894 cx.set_state(indoc!(
6895 r#"abcd
6896 ef«ghˇ»
6897 ijkl
6898 «mˇ»nop"#
6899 ));
6900
6901 cx.update_editor(|editor, window, cx| {
6902 editor.add_selection_above(&Default::default(), window, cx);
6903 editor.add_selection_above(&Default::default(), window, cx);
6904 });
6905
6906 cx.assert_editor_state(indoc!(
6907 r#"ab«cdˇ»
6908 «eˇ»f«ghˇ»
6909 «iˇ»jkl
6910 «mˇ»nop"#
6911 ));
6912
6913 // add single selection in middle - simulate opt drag
6914 cx.update_editor(|editor, window, cx| {
6915 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
6916 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6917 editor.update_selection(
6918 DisplayPoint::new(DisplayRow(2), 4),
6919 0,
6920 gpui::Point::<f32>::default(),
6921 window,
6922 cx,
6923 );
6924 editor.end_selection(window, cx);
6925 });
6926
6927 cx.assert_editor_state(indoc!(
6928 r#"ab«cdˇ»
6929 «eˇ»f«ghˇ»
6930 «iˇ»jk«lˇ»
6931 «mˇ»nop"#
6932 ));
6933
6934 cx.update_editor(|editor, window, cx| {
6935 editor.add_selection_below(&Default::default(), window, cx);
6936 });
6937
6938 // test new added selection expands below, others shrinks from above
6939 cx.assert_editor_state(indoc!(
6940 r#"abcd
6941 ef«ghˇ»
6942 «iˇ»jk«lˇ»
6943 «mˇ»no«pˇ»"#
6944 ));
6945}
6946
6947#[gpui::test]
6948async fn test_select_next(cx: &mut TestAppContext) {
6949 init_test(cx, |_| {});
6950
6951 let mut cx = EditorTestContext::new(cx).await;
6952 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6953
6954 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6955 .unwrap();
6956 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6957
6958 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6959 .unwrap();
6960 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6961
6962 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6963 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6964
6965 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6966 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6967
6968 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6969 .unwrap();
6970 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6971
6972 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6973 .unwrap();
6974 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6975
6976 // Test selection direction should be preserved
6977 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6978
6979 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6980 .unwrap();
6981 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
6982}
6983
6984#[gpui::test]
6985async fn test_select_all_matches(cx: &mut TestAppContext) {
6986 init_test(cx, |_| {});
6987
6988 let mut cx = EditorTestContext::new(cx).await;
6989
6990 // Test caret-only selections
6991 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6992 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6993 .unwrap();
6994 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6995
6996 // Test left-to-right selections
6997 cx.set_state("abc\n«abcˇ»\nabc");
6998 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6999 .unwrap();
7000 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
7001
7002 // Test right-to-left selections
7003 cx.set_state("abc\n«ˇabc»\nabc");
7004 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7005 .unwrap();
7006 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
7007
7008 // Test selecting whitespace with caret selection
7009 cx.set_state("abc\nˇ abc\nabc");
7010 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7011 .unwrap();
7012 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7013
7014 // Test selecting whitespace with left-to-right selection
7015 cx.set_state("abc\n«ˇ »abc\nabc");
7016 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7017 .unwrap();
7018 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
7019
7020 // Test no matches with right-to-left selection
7021 cx.set_state("abc\n« ˇ»abc\nabc");
7022 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7023 .unwrap();
7024 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7025
7026 // Test with a single word and clip_at_line_ends=true (#29823)
7027 cx.set_state("aˇbc");
7028 cx.update_editor(|e, window, cx| {
7029 e.set_clip_at_line_ends(true, cx);
7030 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
7031 e.set_clip_at_line_ends(false, cx);
7032 });
7033 cx.assert_editor_state("«abcˇ»");
7034}
7035
7036#[gpui::test]
7037async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
7038 init_test(cx, |_| {});
7039
7040 let mut cx = EditorTestContext::new(cx).await;
7041
7042 let large_body_1 = "\nd".repeat(200);
7043 let large_body_2 = "\ne".repeat(200);
7044
7045 cx.set_state(&format!(
7046 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
7047 ));
7048 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
7049 let scroll_position = editor.scroll_position(cx);
7050 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
7051 scroll_position
7052 });
7053
7054 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7055 .unwrap();
7056 cx.assert_editor_state(&format!(
7057 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
7058 ));
7059 let scroll_position_after_selection =
7060 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
7061 assert_eq!(
7062 initial_scroll_position, scroll_position_after_selection,
7063 "Scroll position should not change after selecting all matches"
7064 );
7065}
7066
7067#[gpui::test]
7068async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
7069 init_test(cx, |_| {});
7070
7071 let mut cx = EditorLspTestContext::new_rust(
7072 lsp::ServerCapabilities {
7073 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7074 ..Default::default()
7075 },
7076 cx,
7077 )
7078 .await;
7079
7080 cx.set_state(indoc! {"
7081 line 1
7082 line 2
7083 linˇe 3
7084 line 4
7085 line 5
7086 "});
7087
7088 // Make an edit
7089 cx.update_editor(|editor, window, cx| {
7090 editor.handle_input("X", window, cx);
7091 });
7092
7093 // Move cursor to a different position
7094 cx.update_editor(|editor, window, cx| {
7095 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7096 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
7097 });
7098 });
7099
7100 cx.assert_editor_state(indoc! {"
7101 line 1
7102 line 2
7103 linXe 3
7104 line 4
7105 liˇne 5
7106 "});
7107
7108 cx.lsp
7109 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
7110 Ok(Some(vec![lsp::TextEdit::new(
7111 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
7112 "PREFIX ".to_string(),
7113 )]))
7114 });
7115
7116 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
7117 .unwrap()
7118 .await
7119 .unwrap();
7120
7121 cx.assert_editor_state(indoc! {"
7122 PREFIX line 1
7123 line 2
7124 linXe 3
7125 line 4
7126 liˇne 5
7127 "});
7128
7129 // Undo formatting
7130 cx.update_editor(|editor, window, cx| {
7131 editor.undo(&Default::default(), window, cx);
7132 });
7133
7134 // Verify cursor moved back to position after edit
7135 cx.assert_editor_state(indoc! {"
7136 line 1
7137 line 2
7138 linXˇe 3
7139 line 4
7140 line 5
7141 "});
7142}
7143
7144#[gpui::test]
7145async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) {
7146 init_test(cx, |_| {});
7147
7148 let mut cx = EditorTestContext::new(cx).await;
7149
7150 let provider = cx.new(|_| FakeInlineCompletionProvider::default());
7151 cx.update_editor(|editor, window, cx| {
7152 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
7153 });
7154
7155 cx.set_state(indoc! {"
7156 line 1
7157 line 2
7158 linˇe 3
7159 line 4
7160 line 5
7161 line 6
7162 line 7
7163 line 8
7164 line 9
7165 line 10
7166 "});
7167
7168 let snapshot = cx.buffer_snapshot();
7169 let edit_position = snapshot.anchor_after(Point::new(2, 4));
7170
7171 cx.update(|_, cx| {
7172 provider.update(cx, |provider, _| {
7173 provider.set_inline_completion(Some(inline_completion::InlineCompletion {
7174 id: None,
7175 edits: vec![(edit_position..edit_position, "X".into())],
7176 edit_preview: None,
7177 }))
7178 })
7179 });
7180
7181 cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
7182 cx.update_editor(|editor, window, cx| {
7183 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
7184 });
7185
7186 cx.assert_editor_state(indoc! {"
7187 line 1
7188 line 2
7189 lineXˇ 3
7190 line 4
7191 line 5
7192 line 6
7193 line 7
7194 line 8
7195 line 9
7196 line 10
7197 "});
7198
7199 cx.update_editor(|editor, window, cx| {
7200 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7201 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
7202 });
7203 });
7204
7205 cx.assert_editor_state(indoc! {"
7206 line 1
7207 line 2
7208 lineX 3
7209 line 4
7210 line 5
7211 line 6
7212 line 7
7213 line 8
7214 line 9
7215 liˇne 10
7216 "});
7217
7218 cx.update_editor(|editor, window, cx| {
7219 editor.undo(&Default::default(), window, cx);
7220 });
7221
7222 cx.assert_editor_state(indoc! {"
7223 line 1
7224 line 2
7225 lineˇ 3
7226 line 4
7227 line 5
7228 line 6
7229 line 7
7230 line 8
7231 line 9
7232 line 10
7233 "});
7234}
7235
7236#[gpui::test]
7237async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
7238 init_test(cx, |_| {});
7239
7240 let mut cx = EditorTestContext::new(cx).await;
7241 cx.set_state(
7242 r#"let foo = 2;
7243lˇet foo = 2;
7244let fooˇ = 2;
7245let foo = 2;
7246let foo = ˇ2;"#,
7247 );
7248
7249 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7250 .unwrap();
7251 cx.assert_editor_state(
7252 r#"let foo = 2;
7253«letˇ» foo = 2;
7254let «fooˇ» = 2;
7255let foo = 2;
7256let foo = «2ˇ»;"#,
7257 );
7258
7259 // noop for multiple selections with different contents
7260 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7261 .unwrap();
7262 cx.assert_editor_state(
7263 r#"let foo = 2;
7264«letˇ» foo = 2;
7265let «fooˇ» = 2;
7266let foo = 2;
7267let foo = «2ˇ»;"#,
7268 );
7269
7270 // Test last selection direction should be preserved
7271 cx.set_state(
7272 r#"let foo = 2;
7273let foo = 2;
7274let «fooˇ» = 2;
7275let «ˇfoo» = 2;
7276let foo = 2;"#,
7277 );
7278
7279 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7280 .unwrap();
7281 cx.assert_editor_state(
7282 r#"let foo = 2;
7283let foo = 2;
7284let «fooˇ» = 2;
7285let «ˇfoo» = 2;
7286let «ˇfoo» = 2;"#,
7287 );
7288}
7289
7290#[gpui::test]
7291async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
7292 init_test(cx, |_| {});
7293
7294 let mut cx =
7295 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
7296
7297 cx.assert_editor_state(indoc! {"
7298 ˇbbb
7299 ccc
7300
7301 bbb
7302 ccc
7303 "});
7304 cx.dispatch_action(SelectPrevious::default());
7305 cx.assert_editor_state(indoc! {"
7306 «bbbˇ»
7307 ccc
7308
7309 bbb
7310 ccc
7311 "});
7312 cx.dispatch_action(SelectPrevious::default());
7313 cx.assert_editor_state(indoc! {"
7314 «bbbˇ»
7315 ccc
7316
7317 «bbbˇ»
7318 ccc
7319 "});
7320}
7321
7322#[gpui::test]
7323async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
7324 init_test(cx, |_| {});
7325
7326 let mut cx = EditorTestContext::new(cx).await;
7327 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7328
7329 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7330 .unwrap();
7331 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7332
7333 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7334 .unwrap();
7335 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7336
7337 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7338 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7339
7340 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7341 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7342
7343 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7344 .unwrap();
7345 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
7346
7347 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7348 .unwrap();
7349 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7350}
7351
7352#[gpui::test]
7353async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
7354 init_test(cx, |_| {});
7355
7356 let mut cx = EditorTestContext::new(cx).await;
7357 cx.set_state("aˇ");
7358
7359 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7360 .unwrap();
7361 cx.assert_editor_state("«aˇ»");
7362 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7363 .unwrap();
7364 cx.assert_editor_state("«aˇ»");
7365}
7366
7367#[gpui::test]
7368async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
7369 init_test(cx, |_| {});
7370
7371 let mut cx = EditorTestContext::new(cx).await;
7372 cx.set_state(
7373 r#"let foo = 2;
7374lˇet foo = 2;
7375let fooˇ = 2;
7376let foo = 2;
7377let foo = ˇ2;"#,
7378 );
7379
7380 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7381 .unwrap();
7382 cx.assert_editor_state(
7383 r#"let foo = 2;
7384«letˇ» foo = 2;
7385let «fooˇ» = 2;
7386let foo = 2;
7387let foo = «2ˇ»;"#,
7388 );
7389
7390 // noop for multiple selections with different contents
7391 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7392 .unwrap();
7393 cx.assert_editor_state(
7394 r#"let foo = 2;
7395«letˇ» foo = 2;
7396let «fooˇ» = 2;
7397let foo = 2;
7398let foo = «2ˇ»;"#,
7399 );
7400}
7401
7402#[gpui::test]
7403async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
7404 init_test(cx, |_| {});
7405
7406 let mut cx = EditorTestContext::new(cx).await;
7407 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7408
7409 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7410 .unwrap();
7411 // selection direction is preserved
7412 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7413
7414 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7415 .unwrap();
7416 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7417
7418 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7419 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7420
7421 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7422 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7423
7424 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7425 .unwrap();
7426 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
7427
7428 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7429 .unwrap();
7430 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
7431}
7432
7433#[gpui::test]
7434async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
7435 init_test(cx, |_| {});
7436
7437 let language = Arc::new(Language::new(
7438 LanguageConfig::default(),
7439 Some(tree_sitter_rust::LANGUAGE.into()),
7440 ));
7441
7442 let text = r#"
7443 use mod1::mod2::{mod3, mod4};
7444
7445 fn fn_1(param1: bool, param2: &str) {
7446 let var1 = "text";
7447 }
7448 "#
7449 .unindent();
7450
7451 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7452 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7453 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7454
7455 editor
7456 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7457 .await;
7458
7459 editor.update_in(cx, |editor, window, cx| {
7460 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7461 s.select_display_ranges([
7462 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
7463 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
7464 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
7465 ]);
7466 });
7467 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7468 });
7469 editor.update(cx, |editor, cx| {
7470 assert_text_with_selections(
7471 editor,
7472 indoc! {r#"
7473 use mod1::mod2::{mod3, «mod4ˇ»};
7474
7475 fn fn_1«ˇ(param1: bool, param2: &str)» {
7476 let var1 = "«ˇtext»";
7477 }
7478 "#},
7479 cx,
7480 );
7481 });
7482
7483 editor.update_in(cx, |editor, window, cx| {
7484 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7485 });
7486 editor.update(cx, |editor, cx| {
7487 assert_text_with_selections(
7488 editor,
7489 indoc! {r#"
7490 use mod1::mod2::«{mod3, mod4}ˇ»;
7491
7492 «ˇfn fn_1(param1: bool, param2: &str) {
7493 let var1 = "text";
7494 }»
7495 "#},
7496 cx,
7497 );
7498 });
7499
7500 editor.update_in(cx, |editor, window, cx| {
7501 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7502 });
7503 assert_eq!(
7504 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7505 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7506 );
7507
7508 // Trying to expand the selected syntax node one more time has no effect.
7509 editor.update_in(cx, |editor, window, cx| {
7510 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7511 });
7512 assert_eq!(
7513 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7514 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7515 );
7516
7517 editor.update_in(cx, |editor, window, cx| {
7518 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7519 });
7520 editor.update(cx, |editor, cx| {
7521 assert_text_with_selections(
7522 editor,
7523 indoc! {r#"
7524 use mod1::mod2::«{mod3, mod4}ˇ»;
7525
7526 «ˇfn fn_1(param1: bool, param2: &str) {
7527 let var1 = "text";
7528 }»
7529 "#},
7530 cx,
7531 );
7532 });
7533
7534 editor.update_in(cx, |editor, window, cx| {
7535 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7536 });
7537 editor.update(cx, |editor, cx| {
7538 assert_text_with_selections(
7539 editor,
7540 indoc! {r#"
7541 use mod1::mod2::{mod3, «mod4ˇ»};
7542
7543 fn fn_1«ˇ(param1: bool, param2: &str)» {
7544 let var1 = "«ˇtext»";
7545 }
7546 "#},
7547 cx,
7548 );
7549 });
7550
7551 editor.update_in(cx, |editor, window, cx| {
7552 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7553 });
7554 editor.update(cx, |editor, cx| {
7555 assert_text_with_selections(
7556 editor,
7557 indoc! {r#"
7558 use mod1::mod2::{mod3, mo«ˇ»d4};
7559
7560 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7561 let var1 = "te«ˇ»xt";
7562 }
7563 "#},
7564 cx,
7565 );
7566 });
7567
7568 // Trying to shrink the selected syntax node one more time has no effect.
7569 editor.update_in(cx, |editor, window, cx| {
7570 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7571 });
7572 editor.update_in(cx, |editor, _, cx| {
7573 assert_text_with_selections(
7574 editor,
7575 indoc! {r#"
7576 use mod1::mod2::{mod3, mo«ˇ»d4};
7577
7578 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7579 let var1 = "te«ˇ»xt";
7580 }
7581 "#},
7582 cx,
7583 );
7584 });
7585
7586 // Ensure that we keep expanding the selection if the larger selection starts or ends within
7587 // a fold.
7588 editor.update_in(cx, |editor, window, cx| {
7589 editor.fold_creases(
7590 vec![
7591 Crease::simple(
7592 Point::new(0, 21)..Point::new(0, 24),
7593 FoldPlaceholder::test(),
7594 ),
7595 Crease::simple(
7596 Point::new(3, 20)..Point::new(3, 22),
7597 FoldPlaceholder::test(),
7598 ),
7599 ],
7600 true,
7601 window,
7602 cx,
7603 );
7604 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7605 });
7606 editor.update(cx, |editor, cx| {
7607 assert_text_with_selections(
7608 editor,
7609 indoc! {r#"
7610 use mod1::mod2::«{mod3, mod4}ˇ»;
7611
7612 fn fn_1«ˇ(param1: bool, param2: &str)» {
7613 let var1 = "«ˇtext»";
7614 }
7615 "#},
7616 cx,
7617 );
7618 });
7619}
7620
7621#[gpui::test]
7622async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
7623 init_test(cx, |_| {});
7624
7625 let language = Arc::new(Language::new(
7626 LanguageConfig::default(),
7627 Some(tree_sitter_rust::LANGUAGE.into()),
7628 ));
7629
7630 let text = "let a = 2;";
7631
7632 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7633 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7634 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7635
7636 editor
7637 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7638 .await;
7639
7640 // Test case 1: Cursor at end of word
7641 editor.update_in(cx, |editor, window, cx| {
7642 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7643 s.select_display_ranges([
7644 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
7645 ]);
7646 });
7647 });
7648 editor.update(cx, |editor, cx| {
7649 assert_text_with_selections(editor, "let aˇ = 2;", cx);
7650 });
7651 editor.update_in(cx, |editor, window, cx| {
7652 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7653 });
7654 editor.update(cx, |editor, cx| {
7655 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
7656 });
7657 editor.update_in(cx, |editor, window, cx| {
7658 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7659 });
7660 editor.update(cx, |editor, cx| {
7661 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7662 });
7663
7664 // Test case 2: Cursor at end of statement
7665 editor.update_in(cx, |editor, window, cx| {
7666 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7667 s.select_display_ranges([
7668 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
7669 ]);
7670 });
7671 });
7672 editor.update(cx, |editor, cx| {
7673 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
7674 });
7675 editor.update_in(cx, |editor, window, cx| {
7676 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7677 });
7678 editor.update(cx, |editor, cx| {
7679 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7680 });
7681}
7682
7683#[gpui::test]
7684async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
7685 init_test(cx, |_| {});
7686
7687 let language = Arc::new(Language::new(
7688 LanguageConfig::default(),
7689 Some(tree_sitter_rust::LANGUAGE.into()),
7690 ));
7691
7692 let text = r#"
7693 use mod1::mod2::{mod3, mod4};
7694
7695 fn fn_1(param1: bool, param2: &str) {
7696 let var1 = "hello world";
7697 }
7698 "#
7699 .unindent();
7700
7701 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7702 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7703 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7704
7705 editor
7706 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7707 .await;
7708
7709 // Test 1: Cursor on a letter of a string word
7710 editor.update_in(cx, |editor, window, cx| {
7711 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7712 s.select_display_ranges([
7713 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
7714 ]);
7715 });
7716 });
7717 editor.update_in(cx, |editor, window, cx| {
7718 assert_text_with_selections(
7719 editor,
7720 indoc! {r#"
7721 use mod1::mod2::{mod3, mod4};
7722
7723 fn fn_1(param1: bool, param2: &str) {
7724 let var1 = "hˇello world";
7725 }
7726 "#},
7727 cx,
7728 );
7729 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7730 assert_text_with_selections(
7731 editor,
7732 indoc! {r#"
7733 use mod1::mod2::{mod3, mod4};
7734
7735 fn fn_1(param1: bool, param2: &str) {
7736 let var1 = "«ˇhello» world";
7737 }
7738 "#},
7739 cx,
7740 );
7741 });
7742
7743 // Test 2: Partial selection within a word
7744 editor.update_in(cx, |editor, window, cx| {
7745 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7746 s.select_display_ranges([
7747 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7748 ]);
7749 });
7750 });
7751 editor.update_in(cx, |editor, window, cx| {
7752 assert_text_with_selections(
7753 editor,
7754 indoc! {r#"
7755 use mod1::mod2::{mod3, mod4};
7756
7757 fn fn_1(param1: bool, param2: &str) {
7758 let var1 = "h«elˇ»lo world";
7759 }
7760 "#},
7761 cx,
7762 );
7763 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7764 assert_text_with_selections(
7765 editor,
7766 indoc! {r#"
7767 use mod1::mod2::{mod3, mod4};
7768
7769 fn fn_1(param1: bool, param2: &str) {
7770 let var1 = "«ˇhello» world";
7771 }
7772 "#},
7773 cx,
7774 );
7775 });
7776
7777 // Test 3: Complete word already selected
7778 editor.update_in(cx, |editor, window, cx| {
7779 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7780 s.select_display_ranges([
7781 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7782 ]);
7783 });
7784 });
7785 editor.update_in(cx, |editor, window, cx| {
7786 assert_text_with_selections(
7787 editor,
7788 indoc! {r#"
7789 use mod1::mod2::{mod3, mod4};
7790
7791 fn fn_1(param1: bool, param2: &str) {
7792 let var1 = "«helloˇ» world";
7793 }
7794 "#},
7795 cx,
7796 );
7797 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7798 assert_text_with_selections(
7799 editor,
7800 indoc! {r#"
7801 use mod1::mod2::{mod3, mod4};
7802
7803 fn fn_1(param1: bool, param2: &str) {
7804 let var1 = "«hello worldˇ»";
7805 }
7806 "#},
7807 cx,
7808 );
7809 });
7810
7811 // Test 4: Selection spanning across words
7812 editor.update_in(cx, |editor, window, cx| {
7813 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7814 s.select_display_ranges([
7815 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7816 ]);
7817 });
7818 });
7819 editor.update_in(cx, |editor, window, cx| {
7820 assert_text_with_selections(
7821 editor,
7822 indoc! {r#"
7823 use mod1::mod2::{mod3, mod4};
7824
7825 fn fn_1(param1: bool, param2: &str) {
7826 let var1 = "hel«lo woˇ»rld";
7827 }
7828 "#},
7829 cx,
7830 );
7831 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7832 assert_text_with_selections(
7833 editor,
7834 indoc! {r#"
7835 use mod1::mod2::{mod3, mod4};
7836
7837 fn fn_1(param1: bool, param2: &str) {
7838 let var1 = "«ˇhello world»";
7839 }
7840 "#},
7841 cx,
7842 );
7843 });
7844
7845 // Test 5: Expansion beyond string
7846 editor.update_in(cx, |editor, window, cx| {
7847 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7848 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7849 assert_text_with_selections(
7850 editor,
7851 indoc! {r#"
7852 use mod1::mod2::{mod3, mod4};
7853
7854 fn fn_1(param1: bool, param2: &str) {
7855 «ˇlet var1 = "hello world";»
7856 }
7857 "#},
7858 cx,
7859 );
7860 });
7861}
7862
7863#[gpui::test]
7864async fn test_fold_function_bodies(cx: &mut TestAppContext) {
7865 init_test(cx, |_| {});
7866
7867 let base_text = r#"
7868 impl A {
7869 // this is an uncommitted comment
7870
7871 fn b() {
7872 c();
7873 }
7874
7875 // this is another uncommitted comment
7876
7877 fn d() {
7878 // e
7879 // f
7880 }
7881 }
7882
7883 fn g() {
7884 // h
7885 }
7886 "#
7887 .unindent();
7888
7889 let text = r#"
7890 ˇimpl A {
7891
7892 fn b() {
7893 c();
7894 }
7895
7896 fn d() {
7897 // e
7898 // f
7899 }
7900 }
7901
7902 fn g() {
7903 // h
7904 }
7905 "#
7906 .unindent();
7907
7908 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7909 cx.set_state(&text);
7910 cx.set_head_text(&base_text);
7911 cx.update_editor(|editor, window, cx| {
7912 editor.expand_all_diff_hunks(&Default::default(), window, cx);
7913 });
7914
7915 cx.assert_state_with_diff(
7916 "
7917 ˇimpl A {
7918 - // this is an uncommitted comment
7919
7920 fn b() {
7921 c();
7922 }
7923
7924 - // this is another uncommitted comment
7925 -
7926 fn d() {
7927 // e
7928 // f
7929 }
7930 }
7931
7932 fn g() {
7933 // h
7934 }
7935 "
7936 .unindent(),
7937 );
7938
7939 let expected_display_text = "
7940 impl A {
7941 // this is an uncommitted comment
7942
7943 fn b() {
7944 ⋯
7945 }
7946
7947 // this is another uncommitted comment
7948
7949 fn d() {
7950 ⋯
7951 }
7952 }
7953
7954 fn g() {
7955 ⋯
7956 }
7957 "
7958 .unindent();
7959
7960 cx.update_editor(|editor, window, cx| {
7961 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
7962 assert_eq!(editor.display_text(cx), expected_display_text);
7963 });
7964}
7965
7966#[gpui::test]
7967async fn test_autoindent(cx: &mut TestAppContext) {
7968 init_test(cx, |_| {});
7969
7970 let language = Arc::new(
7971 Language::new(
7972 LanguageConfig {
7973 brackets: BracketPairConfig {
7974 pairs: vec![
7975 BracketPair {
7976 start: "{".to_string(),
7977 end: "}".to_string(),
7978 close: false,
7979 surround: false,
7980 newline: true,
7981 },
7982 BracketPair {
7983 start: "(".to_string(),
7984 end: ")".to_string(),
7985 close: false,
7986 surround: false,
7987 newline: true,
7988 },
7989 ],
7990 ..Default::default()
7991 },
7992 ..Default::default()
7993 },
7994 Some(tree_sitter_rust::LANGUAGE.into()),
7995 )
7996 .with_indents_query(
7997 r#"
7998 (_ "(" ")" @end) @indent
7999 (_ "{" "}" @end) @indent
8000 "#,
8001 )
8002 .unwrap(),
8003 );
8004
8005 let text = "fn a() {}";
8006
8007 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8008 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8009 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8010 editor
8011 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8012 .await;
8013
8014 editor.update_in(cx, |editor, window, cx| {
8015 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8016 s.select_ranges([5..5, 8..8, 9..9])
8017 });
8018 editor.newline(&Newline, window, cx);
8019 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
8020 assert_eq!(
8021 editor.selections.ranges(cx),
8022 &[
8023 Point::new(1, 4)..Point::new(1, 4),
8024 Point::new(3, 4)..Point::new(3, 4),
8025 Point::new(5, 0)..Point::new(5, 0)
8026 ]
8027 );
8028 });
8029}
8030
8031#[gpui::test]
8032async fn test_autoindent_selections(cx: &mut TestAppContext) {
8033 init_test(cx, |_| {});
8034
8035 {
8036 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8037 cx.set_state(indoc! {"
8038 impl A {
8039
8040 fn b() {}
8041
8042 «fn c() {
8043
8044 }ˇ»
8045 }
8046 "});
8047
8048 cx.update_editor(|editor, window, cx| {
8049 editor.autoindent(&Default::default(), window, cx);
8050 });
8051
8052 cx.assert_editor_state(indoc! {"
8053 impl A {
8054
8055 fn b() {}
8056
8057 «fn c() {
8058
8059 }ˇ»
8060 }
8061 "});
8062 }
8063
8064 {
8065 let mut cx = EditorTestContext::new_multibuffer(
8066 cx,
8067 [indoc! { "
8068 impl A {
8069 «
8070 // a
8071 fn b(){}
8072 »
8073 «
8074 }
8075 fn c(){}
8076 »
8077 "}],
8078 );
8079
8080 let buffer = cx.update_editor(|editor, _, cx| {
8081 let buffer = editor.buffer().update(cx, |buffer, _| {
8082 buffer.all_buffers().iter().next().unwrap().clone()
8083 });
8084 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
8085 buffer
8086 });
8087
8088 cx.run_until_parked();
8089 cx.update_editor(|editor, window, cx| {
8090 editor.select_all(&Default::default(), window, cx);
8091 editor.autoindent(&Default::default(), window, cx)
8092 });
8093 cx.run_until_parked();
8094
8095 cx.update(|_, cx| {
8096 assert_eq!(
8097 buffer.read(cx).text(),
8098 indoc! { "
8099 impl A {
8100
8101 // a
8102 fn b(){}
8103
8104
8105 }
8106 fn c(){}
8107
8108 " }
8109 )
8110 });
8111 }
8112}
8113
8114#[gpui::test]
8115async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
8116 init_test(cx, |_| {});
8117
8118 let mut cx = EditorTestContext::new(cx).await;
8119
8120 let language = Arc::new(Language::new(
8121 LanguageConfig {
8122 brackets: BracketPairConfig {
8123 pairs: vec![
8124 BracketPair {
8125 start: "{".to_string(),
8126 end: "}".to_string(),
8127 close: true,
8128 surround: true,
8129 newline: true,
8130 },
8131 BracketPair {
8132 start: "(".to_string(),
8133 end: ")".to_string(),
8134 close: true,
8135 surround: true,
8136 newline: true,
8137 },
8138 BracketPair {
8139 start: "/*".to_string(),
8140 end: " */".to_string(),
8141 close: true,
8142 surround: true,
8143 newline: true,
8144 },
8145 BracketPair {
8146 start: "[".to_string(),
8147 end: "]".to_string(),
8148 close: false,
8149 surround: false,
8150 newline: true,
8151 },
8152 BracketPair {
8153 start: "\"".to_string(),
8154 end: "\"".to_string(),
8155 close: true,
8156 surround: true,
8157 newline: false,
8158 },
8159 BracketPair {
8160 start: "<".to_string(),
8161 end: ">".to_string(),
8162 close: false,
8163 surround: true,
8164 newline: true,
8165 },
8166 ],
8167 ..Default::default()
8168 },
8169 autoclose_before: "})]".to_string(),
8170 ..Default::default()
8171 },
8172 Some(tree_sitter_rust::LANGUAGE.into()),
8173 ));
8174
8175 cx.language_registry().add(language.clone());
8176 cx.update_buffer(|buffer, cx| {
8177 buffer.set_language(Some(language), cx);
8178 });
8179
8180 cx.set_state(
8181 &r#"
8182 🏀ˇ
8183 εˇ
8184 ❤️ˇ
8185 "#
8186 .unindent(),
8187 );
8188
8189 // autoclose multiple nested brackets at multiple cursors
8190 cx.update_editor(|editor, window, cx| {
8191 editor.handle_input("{", window, cx);
8192 editor.handle_input("{", window, cx);
8193 editor.handle_input("{", window, cx);
8194 });
8195 cx.assert_editor_state(
8196 &"
8197 🏀{{{ˇ}}}
8198 ε{{{ˇ}}}
8199 ❤️{{{ˇ}}}
8200 "
8201 .unindent(),
8202 );
8203
8204 // insert a different closing bracket
8205 cx.update_editor(|editor, window, cx| {
8206 editor.handle_input(")", window, cx);
8207 });
8208 cx.assert_editor_state(
8209 &"
8210 🏀{{{)ˇ}}}
8211 ε{{{)ˇ}}}
8212 ❤️{{{)ˇ}}}
8213 "
8214 .unindent(),
8215 );
8216
8217 // skip over the auto-closed brackets when typing a closing bracket
8218 cx.update_editor(|editor, window, cx| {
8219 editor.move_right(&MoveRight, window, cx);
8220 editor.handle_input("}", window, cx);
8221 editor.handle_input("}", window, cx);
8222 editor.handle_input("}", window, cx);
8223 });
8224 cx.assert_editor_state(
8225 &"
8226 🏀{{{)}}}}ˇ
8227 ε{{{)}}}}ˇ
8228 ❤️{{{)}}}}ˇ
8229 "
8230 .unindent(),
8231 );
8232
8233 // autoclose multi-character pairs
8234 cx.set_state(
8235 &"
8236 ˇ
8237 ˇ
8238 "
8239 .unindent(),
8240 );
8241 cx.update_editor(|editor, window, cx| {
8242 editor.handle_input("/", window, cx);
8243 editor.handle_input("*", window, cx);
8244 });
8245 cx.assert_editor_state(
8246 &"
8247 /*ˇ */
8248 /*ˇ */
8249 "
8250 .unindent(),
8251 );
8252
8253 // one cursor autocloses a multi-character pair, one cursor
8254 // does not autoclose.
8255 cx.set_state(
8256 &"
8257 /ˇ
8258 ˇ
8259 "
8260 .unindent(),
8261 );
8262 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
8263 cx.assert_editor_state(
8264 &"
8265 /*ˇ */
8266 *ˇ
8267 "
8268 .unindent(),
8269 );
8270
8271 // Don't autoclose if the next character isn't whitespace and isn't
8272 // listed in the language's "autoclose_before" section.
8273 cx.set_state("ˇa b");
8274 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8275 cx.assert_editor_state("{ˇa b");
8276
8277 // Don't autoclose if `close` is false for the bracket pair
8278 cx.set_state("ˇ");
8279 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
8280 cx.assert_editor_state("[ˇ");
8281
8282 // Surround with brackets if text is selected
8283 cx.set_state("«aˇ» b");
8284 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8285 cx.assert_editor_state("{«aˇ»} b");
8286
8287 // Autoclose when not immediately after a word character
8288 cx.set_state("a ˇ");
8289 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8290 cx.assert_editor_state("a \"ˇ\"");
8291
8292 // Autoclose pair where the start and end characters are the same
8293 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8294 cx.assert_editor_state("a \"\"ˇ");
8295
8296 // Don't autoclose when immediately after a word character
8297 cx.set_state("aˇ");
8298 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8299 cx.assert_editor_state("a\"ˇ");
8300
8301 // Do autoclose when after a non-word character
8302 cx.set_state("{ˇ");
8303 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8304 cx.assert_editor_state("{\"ˇ\"");
8305
8306 // Non identical pairs autoclose regardless of preceding character
8307 cx.set_state("aˇ");
8308 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8309 cx.assert_editor_state("a{ˇ}");
8310
8311 // Don't autoclose pair if autoclose is disabled
8312 cx.set_state("ˇ");
8313 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8314 cx.assert_editor_state("<ˇ");
8315
8316 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
8317 cx.set_state("«aˇ» b");
8318 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8319 cx.assert_editor_state("<«aˇ»> b");
8320}
8321
8322#[gpui::test]
8323async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
8324 init_test(cx, |settings| {
8325 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8326 });
8327
8328 let mut cx = EditorTestContext::new(cx).await;
8329
8330 let language = Arc::new(Language::new(
8331 LanguageConfig {
8332 brackets: BracketPairConfig {
8333 pairs: vec![
8334 BracketPair {
8335 start: "{".to_string(),
8336 end: "}".to_string(),
8337 close: true,
8338 surround: true,
8339 newline: true,
8340 },
8341 BracketPair {
8342 start: "(".to_string(),
8343 end: ")".to_string(),
8344 close: true,
8345 surround: true,
8346 newline: true,
8347 },
8348 BracketPair {
8349 start: "[".to_string(),
8350 end: "]".to_string(),
8351 close: false,
8352 surround: false,
8353 newline: true,
8354 },
8355 ],
8356 ..Default::default()
8357 },
8358 autoclose_before: "})]".to_string(),
8359 ..Default::default()
8360 },
8361 Some(tree_sitter_rust::LANGUAGE.into()),
8362 ));
8363
8364 cx.language_registry().add(language.clone());
8365 cx.update_buffer(|buffer, cx| {
8366 buffer.set_language(Some(language), cx);
8367 });
8368
8369 cx.set_state(
8370 &"
8371 ˇ
8372 ˇ
8373 ˇ
8374 "
8375 .unindent(),
8376 );
8377
8378 // ensure only matching closing brackets are skipped over
8379 cx.update_editor(|editor, window, cx| {
8380 editor.handle_input("}", window, cx);
8381 editor.move_left(&MoveLeft, window, cx);
8382 editor.handle_input(")", window, cx);
8383 editor.move_left(&MoveLeft, window, cx);
8384 });
8385 cx.assert_editor_state(
8386 &"
8387 ˇ)}
8388 ˇ)}
8389 ˇ)}
8390 "
8391 .unindent(),
8392 );
8393
8394 // skip-over closing brackets at multiple cursors
8395 cx.update_editor(|editor, window, cx| {
8396 editor.handle_input(")", window, cx);
8397 editor.handle_input("}", window, cx);
8398 });
8399 cx.assert_editor_state(
8400 &"
8401 )}ˇ
8402 )}ˇ
8403 )}ˇ
8404 "
8405 .unindent(),
8406 );
8407
8408 // ignore non-close brackets
8409 cx.update_editor(|editor, window, cx| {
8410 editor.handle_input("]", window, cx);
8411 editor.move_left(&MoveLeft, window, cx);
8412 editor.handle_input("]", window, cx);
8413 });
8414 cx.assert_editor_state(
8415 &"
8416 )}]ˇ]
8417 )}]ˇ]
8418 )}]ˇ]
8419 "
8420 .unindent(),
8421 );
8422}
8423
8424#[gpui::test]
8425async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8426 init_test(cx, |_| {});
8427
8428 let mut cx = EditorTestContext::new(cx).await;
8429
8430 let html_language = Arc::new(
8431 Language::new(
8432 LanguageConfig {
8433 name: "HTML".into(),
8434 brackets: BracketPairConfig {
8435 pairs: vec![
8436 BracketPair {
8437 start: "<".into(),
8438 end: ">".into(),
8439 close: true,
8440 ..Default::default()
8441 },
8442 BracketPair {
8443 start: "{".into(),
8444 end: "}".into(),
8445 close: true,
8446 ..Default::default()
8447 },
8448 BracketPair {
8449 start: "(".into(),
8450 end: ")".into(),
8451 close: true,
8452 ..Default::default()
8453 },
8454 ],
8455 ..Default::default()
8456 },
8457 autoclose_before: "})]>".into(),
8458 ..Default::default()
8459 },
8460 Some(tree_sitter_html::LANGUAGE.into()),
8461 )
8462 .with_injection_query(
8463 r#"
8464 (script_element
8465 (raw_text) @injection.content
8466 (#set! injection.language "javascript"))
8467 "#,
8468 )
8469 .unwrap(),
8470 );
8471
8472 let javascript_language = Arc::new(Language::new(
8473 LanguageConfig {
8474 name: "JavaScript".into(),
8475 brackets: BracketPairConfig {
8476 pairs: vec![
8477 BracketPair {
8478 start: "/*".into(),
8479 end: " */".into(),
8480 close: true,
8481 ..Default::default()
8482 },
8483 BracketPair {
8484 start: "{".into(),
8485 end: "}".into(),
8486 close: true,
8487 ..Default::default()
8488 },
8489 BracketPair {
8490 start: "(".into(),
8491 end: ")".into(),
8492 close: true,
8493 ..Default::default()
8494 },
8495 ],
8496 ..Default::default()
8497 },
8498 autoclose_before: "})]>".into(),
8499 ..Default::default()
8500 },
8501 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8502 ));
8503
8504 cx.language_registry().add(html_language.clone());
8505 cx.language_registry().add(javascript_language.clone());
8506
8507 cx.update_buffer(|buffer, cx| {
8508 buffer.set_language(Some(html_language), cx);
8509 });
8510
8511 cx.set_state(
8512 &r#"
8513 <body>ˇ
8514 <script>
8515 var x = 1;ˇ
8516 </script>
8517 </body>ˇ
8518 "#
8519 .unindent(),
8520 );
8521
8522 // Precondition: different languages are active at different locations.
8523 cx.update_editor(|editor, window, cx| {
8524 let snapshot = editor.snapshot(window, cx);
8525 let cursors = editor.selections.ranges::<usize>(cx);
8526 let languages = cursors
8527 .iter()
8528 .map(|c| snapshot.language_at(c.start).unwrap().name())
8529 .collect::<Vec<_>>();
8530 assert_eq!(
8531 languages,
8532 &["HTML".into(), "JavaScript".into(), "HTML".into()]
8533 );
8534 });
8535
8536 // Angle brackets autoclose in HTML, but not JavaScript.
8537 cx.update_editor(|editor, window, cx| {
8538 editor.handle_input("<", window, cx);
8539 editor.handle_input("a", window, cx);
8540 });
8541 cx.assert_editor_state(
8542 &r#"
8543 <body><aˇ>
8544 <script>
8545 var x = 1;<aˇ
8546 </script>
8547 </body><aˇ>
8548 "#
8549 .unindent(),
8550 );
8551
8552 // Curly braces and parens autoclose in both HTML and JavaScript.
8553 cx.update_editor(|editor, window, cx| {
8554 editor.handle_input(" b=", window, cx);
8555 editor.handle_input("{", window, cx);
8556 editor.handle_input("c", window, cx);
8557 editor.handle_input("(", window, cx);
8558 });
8559 cx.assert_editor_state(
8560 &r#"
8561 <body><a b={c(ˇ)}>
8562 <script>
8563 var x = 1;<a b={c(ˇ)}
8564 </script>
8565 </body><a b={c(ˇ)}>
8566 "#
8567 .unindent(),
8568 );
8569
8570 // Brackets that were already autoclosed are skipped.
8571 cx.update_editor(|editor, window, cx| {
8572 editor.handle_input(")", window, cx);
8573 editor.handle_input("d", window, cx);
8574 editor.handle_input("}", window, cx);
8575 });
8576 cx.assert_editor_state(
8577 &r#"
8578 <body><a b={c()d}ˇ>
8579 <script>
8580 var x = 1;<a b={c()d}ˇ
8581 </script>
8582 </body><a b={c()d}ˇ>
8583 "#
8584 .unindent(),
8585 );
8586 cx.update_editor(|editor, window, cx| {
8587 editor.handle_input(">", window, cx);
8588 });
8589 cx.assert_editor_state(
8590 &r#"
8591 <body><a b={c()d}>ˇ
8592 <script>
8593 var x = 1;<a b={c()d}>ˇ
8594 </script>
8595 </body><a b={c()d}>ˇ
8596 "#
8597 .unindent(),
8598 );
8599
8600 // Reset
8601 cx.set_state(
8602 &r#"
8603 <body>ˇ
8604 <script>
8605 var x = 1;ˇ
8606 </script>
8607 </body>ˇ
8608 "#
8609 .unindent(),
8610 );
8611
8612 cx.update_editor(|editor, window, cx| {
8613 editor.handle_input("<", window, cx);
8614 });
8615 cx.assert_editor_state(
8616 &r#"
8617 <body><ˇ>
8618 <script>
8619 var x = 1;<ˇ
8620 </script>
8621 </body><ˇ>
8622 "#
8623 .unindent(),
8624 );
8625
8626 // When backspacing, the closing angle brackets are removed.
8627 cx.update_editor(|editor, window, cx| {
8628 editor.backspace(&Backspace, window, cx);
8629 });
8630 cx.assert_editor_state(
8631 &r#"
8632 <body>ˇ
8633 <script>
8634 var x = 1;ˇ
8635 </script>
8636 </body>ˇ
8637 "#
8638 .unindent(),
8639 );
8640
8641 // Block comments autoclose in JavaScript, but not HTML.
8642 cx.update_editor(|editor, window, cx| {
8643 editor.handle_input("/", window, cx);
8644 editor.handle_input("*", window, cx);
8645 });
8646 cx.assert_editor_state(
8647 &r#"
8648 <body>/*ˇ
8649 <script>
8650 var x = 1;/*ˇ */
8651 </script>
8652 </body>/*ˇ
8653 "#
8654 .unindent(),
8655 );
8656}
8657
8658#[gpui::test]
8659async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
8660 init_test(cx, |_| {});
8661
8662 let mut cx = EditorTestContext::new(cx).await;
8663
8664 let rust_language = Arc::new(
8665 Language::new(
8666 LanguageConfig {
8667 name: "Rust".into(),
8668 brackets: serde_json::from_value(json!([
8669 { "start": "{", "end": "}", "close": true, "newline": true },
8670 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
8671 ]))
8672 .unwrap(),
8673 autoclose_before: "})]>".into(),
8674 ..Default::default()
8675 },
8676 Some(tree_sitter_rust::LANGUAGE.into()),
8677 )
8678 .with_override_query("(string_literal) @string")
8679 .unwrap(),
8680 );
8681
8682 cx.language_registry().add(rust_language.clone());
8683 cx.update_buffer(|buffer, cx| {
8684 buffer.set_language(Some(rust_language), cx);
8685 });
8686
8687 cx.set_state(
8688 &r#"
8689 let x = ˇ
8690 "#
8691 .unindent(),
8692 );
8693
8694 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
8695 cx.update_editor(|editor, window, cx| {
8696 editor.handle_input("\"", window, cx);
8697 });
8698 cx.assert_editor_state(
8699 &r#"
8700 let x = "ˇ"
8701 "#
8702 .unindent(),
8703 );
8704
8705 // Inserting another quotation mark. The cursor moves across the existing
8706 // automatically-inserted quotation mark.
8707 cx.update_editor(|editor, window, cx| {
8708 editor.handle_input("\"", window, cx);
8709 });
8710 cx.assert_editor_state(
8711 &r#"
8712 let x = ""ˇ
8713 "#
8714 .unindent(),
8715 );
8716
8717 // Reset
8718 cx.set_state(
8719 &r#"
8720 let x = ˇ
8721 "#
8722 .unindent(),
8723 );
8724
8725 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
8726 cx.update_editor(|editor, window, cx| {
8727 editor.handle_input("\"", window, cx);
8728 editor.handle_input(" ", window, cx);
8729 editor.move_left(&Default::default(), window, cx);
8730 editor.handle_input("\\", window, cx);
8731 editor.handle_input("\"", window, cx);
8732 });
8733 cx.assert_editor_state(
8734 &r#"
8735 let x = "\"ˇ "
8736 "#
8737 .unindent(),
8738 );
8739
8740 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8741 // mark. Nothing is inserted.
8742 cx.update_editor(|editor, window, cx| {
8743 editor.move_right(&Default::default(), window, cx);
8744 editor.handle_input("\"", window, cx);
8745 });
8746 cx.assert_editor_state(
8747 &r#"
8748 let x = "\" "ˇ
8749 "#
8750 .unindent(),
8751 );
8752}
8753
8754#[gpui::test]
8755async fn test_surround_with_pair(cx: &mut TestAppContext) {
8756 init_test(cx, |_| {});
8757
8758 let language = Arc::new(Language::new(
8759 LanguageConfig {
8760 brackets: BracketPairConfig {
8761 pairs: vec![
8762 BracketPair {
8763 start: "{".to_string(),
8764 end: "}".to_string(),
8765 close: true,
8766 surround: true,
8767 newline: true,
8768 },
8769 BracketPair {
8770 start: "/* ".to_string(),
8771 end: "*/".to_string(),
8772 close: true,
8773 surround: true,
8774 ..Default::default()
8775 },
8776 ],
8777 ..Default::default()
8778 },
8779 ..Default::default()
8780 },
8781 Some(tree_sitter_rust::LANGUAGE.into()),
8782 ));
8783
8784 let text = r#"
8785 a
8786 b
8787 c
8788 "#
8789 .unindent();
8790
8791 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8792 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8793 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8794 editor
8795 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8796 .await;
8797
8798 editor.update_in(cx, |editor, window, cx| {
8799 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8800 s.select_display_ranges([
8801 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8802 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8803 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8804 ])
8805 });
8806
8807 editor.handle_input("{", window, cx);
8808 editor.handle_input("{", window, cx);
8809 editor.handle_input("{", window, cx);
8810 assert_eq!(
8811 editor.text(cx),
8812 "
8813 {{{a}}}
8814 {{{b}}}
8815 {{{c}}}
8816 "
8817 .unindent()
8818 );
8819 assert_eq!(
8820 editor.selections.display_ranges(cx),
8821 [
8822 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
8823 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
8824 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
8825 ]
8826 );
8827
8828 editor.undo(&Undo, window, cx);
8829 editor.undo(&Undo, window, cx);
8830 editor.undo(&Undo, window, cx);
8831 assert_eq!(
8832 editor.text(cx),
8833 "
8834 a
8835 b
8836 c
8837 "
8838 .unindent()
8839 );
8840 assert_eq!(
8841 editor.selections.display_ranges(cx),
8842 [
8843 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8844 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8845 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8846 ]
8847 );
8848
8849 // Ensure inserting the first character of a multi-byte bracket pair
8850 // doesn't surround the selections with the bracket.
8851 editor.handle_input("/", window, cx);
8852 assert_eq!(
8853 editor.text(cx),
8854 "
8855 /
8856 /
8857 /
8858 "
8859 .unindent()
8860 );
8861 assert_eq!(
8862 editor.selections.display_ranges(cx),
8863 [
8864 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8865 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8866 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8867 ]
8868 );
8869
8870 editor.undo(&Undo, window, cx);
8871 assert_eq!(
8872 editor.text(cx),
8873 "
8874 a
8875 b
8876 c
8877 "
8878 .unindent()
8879 );
8880 assert_eq!(
8881 editor.selections.display_ranges(cx),
8882 [
8883 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8884 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8885 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8886 ]
8887 );
8888
8889 // Ensure inserting the last character of a multi-byte bracket pair
8890 // doesn't surround the selections with the bracket.
8891 editor.handle_input("*", window, cx);
8892 assert_eq!(
8893 editor.text(cx),
8894 "
8895 *
8896 *
8897 *
8898 "
8899 .unindent()
8900 );
8901 assert_eq!(
8902 editor.selections.display_ranges(cx),
8903 [
8904 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8905 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8906 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8907 ]
8908 );
8909 });
8910}
8911
8912#[gpui::test]
8913async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
8914 init_test(cx, |_| {});
8915
8916 let language = Arc::new(Language::new(
8917 LanguageConfig {
8918 brackets: BracketPairConfig {
8919 pairs: vec![BracketPair {
8920 start: "{".to_string(),
8921 end: "}".to_string(),
8922 close: true,
8923 surround: true,
8924 newline: true,
8925 }],
8926 ..Default::default()
8927 },
8928 autoclose_before: "}".to_string(),
8929 ..Default::default()
8930 },
8931 Some(tree_sitter_rust::LANGUAGE.into()),
8932 ));
8933
8934 let text = r#"
8935 a
8936 b
8937 c
8938 "#
8939 .unindent();
8940
8941 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8942 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8943 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8944 editor
8945 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8946 .await;
8947
8948 editor.update_in(cx, |editor, window, cx| {
8949 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8950 s.select_ranges([
8951 Point::new(0, 1)..Point::new(0, 1),
8952 Point::new(1, 1)..Point::new(1, 1),
8953 Point::new(2, 1)..Point::new(2, 1),
8954 ])
8955 });
8956
8957 editor.handle_input("{", window, cx);
8958 editor.handle_input("{", window, cx);
8959 editor.handle_input("_", window, cx);
8960 assert_eq!(
8961 editor.text(cx),
8962 "
8963 a{{_}}
8964 b{{_}}
8965 c{{_}}
8966 "
8967 .unindent()
8968 );
8969 assert_eq!(
8970 editor.selections.ranges::<Point>(cx),
8971 [
8972 Point::new(0, 4)..Point::new(0, 4),
8973 Point::new(1, 4)..Point::new(1, 4),
8974 Point::new(2, 4)..Point::new(2, 4)
8975 ]
8976 );
8977
8978 editor.backspace(&Default::default(), window, cx);
8979 editor.backspace(&Default::default(), window, cx);
8980 assert_eq!(
8981 editor.text(cx),
8982 "
8983 a{}
8984 b{}
8985 c{}
8986 "
8987 .unindent()
8988 );
8989 assert_eq!(
8990 editor.selections.ranges::<Point>(cx),
8991 [
8992 Point::new(0, 2)..Point::new(0, 2),
8993 Point::new(1, 2)..Point::new(1, 2),
8994 Point::new(2, 2)..Point::new(2, 2)
8995 ]
8996 );
8997
8998 editor.delete_to_previous_word_start(&Default::default(), window, cx);
8999 assert_eq!(
9000 editor.text(cx),
9001 "
9002 a
9003 b
9004 c
9005 "
9006 .unindent()
9007 );
9008 assert_eq!(
9009 editor.selections.ranges::<Point>(cx),
9010 [
9011 Point::new(0, 1)..Point::new(0, 1),
9012 Point::new(1, 1)..Point::new(1, 1),
9013 Point::new(2, 1)..Point::new(2, 1)
9014 ]
9015 );
9016 });
9017}
9018
9019#[gpui::test]
9020async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
9021 init_test(cx, |settings| {
9022 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9023 });
9024
9025 let mut cx = EditorTestContext::new(cx).await;
9026
9027 let language = Arc::new(Language::new(
9028 LanguageConfig {
9029 brackets: BracketPairConfig {
9030 pairs: vec![
9031 BracketPair {
9032 start: "{".to_string(),
9033 end: "}".to_string(),
9034 close: true,
9035 surround: true,
9036 newline: true,
9037 },
9038 BracketPair {
9039 start: "(".to_string(),
9040 end: ")".to_string(),
9041 close: true,
9042 surround: true,
9043 newline: true,
9044 },
9045 BracketPair {
9046 start: "[".to_string(),
9047 end: "]".to_string(),
9048 close: false,
9049 surround: true,
9050 newline: true,
9051 },
9052 ],
9053 ..Default::default()
9054 },
9055 autoclose_before: "})]".to_string(),
9056 ..Default::default()
9057 },
9058 Some(tree_sitter_rust::LANGUAGE.into()),
9059 ));
9060
9061 cx.language_registry().add(language.clone());
9062 cx.update_buffer(|buffer, cx| {
9063 buffer.set_language(Some(language), cx);
9064 });
9065
9066 cx.set_state(
9067 &"
9068 {(ˇ)}
9069 [[ˇ]]
9070 {(ˇ)}
9071 "
9072 .unindent(),
9073 );
9074
9075 cx.update_editor(|editor, window, cx| {
9076 editor.backspace(&Default::default(), window, cx);
9077 editor.backspace(&Default::default(), window, cx);
9078 });
9079
9080 cx.assert_editor_state(
9081 &"
9082 ˇ
9083 ˇ]]
9084 ˇ
9085 "
9086 .unindent(),
9087 );
9088
9089 cx.update_editor(|editor, window, cx| {
9090 editor.handle_input("{", window, cx);
9091 editor.handle_input("{", window, cx);
9092 editor.move_right(&MoveRight, window, cx);
9093 editor.move_right(&MoveRight, window, cx);
9094 editor.move_left(&MoveLeft, window, cx);
9095 editor.move_left(&MoveLeft, window, cx);
9096 editor.backspace(&Default::default(), window, cx);
9097 });
9098
9099 cx.assert_editor_state(
9100 &"
9101 {ˇ}
9102 {ˇ}]]
9103 {ˇ}
9104 "
9105 .unindent(),
9106 );
9107
9108 cx.update_editor(|editor, window, cx| {
9109 editor.backspace(&Default::default(), window, cx);
9110 });
9111
9112 cx.assert_editor_state(
9113 &"
9114 ˇ
9115 ˇ]]
9116 ˇ
9117 "
9118 .unindent(),
9119 );
9120}
9121
9122#[gpui::test]
9123async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
9124 init_test(cx, |_| {});
9125
9126 let language = Arc::new(Language::new(
9127 LanguageConfig::default(),
9128 Some(tree_sitter_rust::LANGUAGE.into()),
9129 ));
9130
9131 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
9132 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9133 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9134 editor
9135 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9136 .await;
9137
9138 editor.update_in(cx, |editor, window, cx| {
9139 editor.set_auto_replace_emoji_shortcode(true);
9140
9141 editor.handle_input("Hello ", window, cx);
9142 editor.handle_input(":wave", window, cx);
9143 assert_eq!(editor.text(cx), "Hello :wave".unindent());
9144
9145 editor.handle_input(":", window, cx);
9146 assert_eq!(editor.text(cx), "Hello 👋".unindent());
9147
9148 editor.handle_input(" :smile", window, cx);
9149 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
9150
9151 editor.handle_input(":", window, cx);
9152 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
9153
9154 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
9155 editor.handle_input(":wave", window, cx);
9156 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
9157
9158 editor.handle_input(":", window, cx);
9159 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
9160
9161 editor.handle_input(":1", window, cx);
9162 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
9163
9164 editor.handle_input(":", window, cx);
9165 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
9166
9167 // Ensure shortcode does not get replaced when it is part of a word
9168 editor.handle_input(" Test:wave", window, cx);
9169 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
9170
9171 editor.handle_input(":", window, cx);
9172 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
9173
9174 editor.set_auto_replace_emoji_shortcode(false);
9175
9176 // Ensure shortcode does not get replaced when auto replace is off
9177 editor.handle_input(" :wave", window, cx);
9178 assert_eq!(
9179 editor.text(cx),
9180 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
9181 );
9182
9183 editor.handle_input(":", window, cx);
9184 assert_eq!(
9185 editor.text(cx),
9186 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
9187 );
9188 });
9189}
9190
9191#[gpui::test]
9192async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
9193 init_test(cx, |_| {});
9194
9195 let (text, insertion_ranges) = marked_text_ranges(
9196 indoc! {"
9197 ˇ
9198 "},
9199 false,
9200 );
9201
9202 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
9203 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9204
9205 _ = editor.update_in(cx, |editor, window, cx| {
9206 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
9207
9208 editor
9209 .insert_snippet(&insertion_ranges, snippet, window, cx)
9210 .unwrap();
9211
9212 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
9213 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
9214 assert_eq!(editor.text(cx), expected_text);
9215 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
9216 }
9217
9218 assert(
9219 editor,
9220 cx,
9221 indoc! {"
9222 type «» =•
9223 "},
9224 );
9225
9226 assert!(editor.context_menu_visible(), "There should be a matches");
9227 });
9228}
9229
9230#[gpui::test]
9231async fn test_snippets(cx: &mut TestAppContext) {
9232 init_test(cx, |_| {});
9233
9234 let mut cx = EditorTestContext::new(cx).await;
9235
9236 cx.set_state(indoc! {"
9237 a.ˇ b
9238 a.ˇ b
9239 a.ˇ b
9240 "});
9241
9242 cx.update_editor(|editor, window, cx| {
9243 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
9244 let insertion_ranges = editor
9245 .selections
9246 .all(cx)
9247 .iter()
9248 .map(|s| s.range().clone())
9249 .collect::<Vec<_>>();
9250 editor
9251 .insert_snippet(&insertion_ranges, snippet, window, cx)
9252 .unwrap();
9253 });
9254
9255 cx.assert_editor_state(indoc! {"
9256 a.f(«oneˇ», two, «threeˇ») b
9257 a.f(«oneˇ», two, «threeˇ») b
9258 a.f(«oneˇ», two, «threeˇ») b
9259 "});
9260
9261 // Can't move earlier than the first tab stop
9262 cx.update_editor(|editor, window, cx| {
9263 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9264 });
9265 cx.assert_editor_state(indoc! {"
9266 a.f(«oneˇ», two, «threeˇ») b
9267 a.f(«oneˇ», two, «threeˇ») b
9268 a.f(«oneˇ», two, «threeˇ») b
9269 "});
9270
9271 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9272 cx.assert_editor_state(indoc! {"
9273 a.f(one, «twoˇ», three) b
9274 a.f(one, «twoˇ», three) b
9275 a.f(one, «twoˇ», three) b
9276 "});
9277
9278 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
9279 cx.assert_editor_state(indoc! {"
9280 a.f(«oneˇ», two, «threeˇ») b
9281 a.f(«oneˇ», two, «threeˇ») b
9282 a.f(«oneˇ», two, «threeˇ») b
9283 "});
9284
9285 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9286 cx.assert_editor_state(indoc! {"
9287 a.f(one, «twoˇ», three) b
9288 a.f(one, «twoˇ», three) b
9289 a.f(one, «twoˇ», three) b
9290 "});
9291 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9292 cx.assert_editor_state(indoc! {"
9293 a.f(one, two, three)ˇ b
9294 a.f(one, two, three)ˇ b
9295 a.f(one, two, three)ˇ b
9296 "});
9297
9298 // As soon as the last tab stop is reached, snippet state is gone
9299 cx.update_editor(|editor, window, cx| {
9300 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9301 });
9302 cx.assert_editor_state(indoc! {"
9303 a.f(one, two, three)ˇ b
9304 a.f(one, two, three)ˇ b
9305 a.f(one, two, three)ˇ b
9306 "});
9307}
9308
9309#[gpui::test]
9310async fn test_snippet_indentation(cx: &mut TestAppContext) {
9311 init_test(cx, |_| {});
9312
9313 let mut cx = EditorTestContext::new(cx).await;
9314
9315 cx.update_editor(|editor, window, cx| {
9316 let snippet = Snippet::parse(indoc! {"
9317 /*
9318 * Multiline comment with leading indentation
9319 *
9320 * $1
9321 */
9322 $0"})
9323 .unwrap();
9324 let insertion_ranges = editor
9325 .selections
9326 .all(cx)
9327 .iter()
9328 .map(|s| s.range().clone())
9329 .collect::<Vec<_>>();
9330 editor
9331 .insert_snippet(&insertion_ranges, snippet, window, cx)
9332 .unwrap();
9333 });
9334
9335 cx.assert_editor_state(indoc! {"
9336 /*
9337 * Multiline comment with leading indentation
9338 *
9339 * ˇ
9340 */
9341 "});
9342
9343 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9344 cx.assert_editor_state(indoc! {"
9345 /*
9346 * Multiline comment with leading indentation
9347 *
9348 *•
9349 */
9350 ˇ"});
9351}
9352
9353#[gpui::test]
9354async fn test_document_format_during_save(cx: &mut TestAppContext) {
9355 init_test(cx, |_| {});
9356
9357 let fs = FakeFs::new(cx.executor());
9358 fs.insert_file(path!("/file.rs"), Default::default()).await;
9359
9360 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
9361
9362 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9363 language_registry.add(rust_lang());
9364 let mut fake_servers = language_registry.register_fake_lsp(
9365 "Rust",
9366 FakeLspAdapter {
9367 capabilities: lsp::ServerCapabilities {
9368 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9369 ..Default::default()
9370 },
9371 ..Default::default()
9372 },
9373 );
9374
9375 let buffer = project
9376 .update(cx, |project, cx| {
9377 project.open_local_buffer(path!("/file.rs"), cx)
9378 })
9379 .await
9380 .unwrap();
9381
9382 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9383 let (editor, cx) = cx.add_window_view(|window, cx| {
9384 build_editor_with_project(project.clone(), buffer, window, cx)
9385 });
9386 editor.update_in(cx, |editor, window, cx| {
9387 editor.set_text("one\ntwo\nthree\n", window, cx)
9388 });
9389 assert!(cx.read(|cx| editor.is_dirty(cx)));
9390
9391 cx.executor().start_waiting();
9392 let fake_server = fake_servers.next().await.unwrap();
9393
9394 {
9395 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9396 move |params, _| async move {
9397 assert_eq!(
9398 params.text_document.uri,
9399 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9400 );
9401 assert_eq!(params.options.tab_size, 4);
9402 Ok(Some(vec![lsp::TextEdit::new(
9403 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9404 ", ".to_string(),
9405 )]))
9406 },
9407 );
9408 let save = editor
9409 .update_in(cx, |editor, window, cx| {
9410 editor.save(
9411 SaveOptions {
9412 format: true,
9413 autosave: false,
9414 },
9415 project.clone(),
9416 window,
9417 cx,
9418 )
9419 })
9420 .unwrap();
9421 cx.executor().start_waiting();
9422 save.await;
9423
9424 assert_eq!(
9425 editor.update(cx, |editor, cx| editor.text(cx)),
9426 "one, two\nthree\n"
9427 );
9428 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9429 }
9430
9431 {
9432 editor.update_in(cx, |editor, window, cx| {
9433 editor.set_text("one\ntwo\nthree\n", window, cx)
9434 });
9435 assert!(cx.read(|cx| editor.is_dirty(cx)));
9436
9437 // Ensure we can still save even if formatting hangs.
9438 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9439 move |params, _| async move {
9440 assert_eq!(
9441 params.text_document.uri,
9442 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9443 );
9444 futures::future::pending::<()>().await;
9445 unreachable!()
9446 },
9447 );
9448 let save = editor
9449 .update_in(cx, |editor, window, cx| {
9450 editor.save(
9451 SaveOptions {
9452 format: true,
9453 autosave: false,
9454 },
9455 project.clone(),
9456 window,
9457 cx,
9458 )
9459 })
9460 .unwrap();
9461 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9462 cx.executor().start_waiting();
9463 save.await;
9464 assert_eq!(
9465 editor.update(cx, |editor, cx| editor.text(cx)),
9466 "one\ntwo\nthree\n"
9467 );
9468 }
9469
9470 // Set rust language override and assert overridden tabsize is sent to language server
9471 update_test_language_settings(cx, |settings| {
9472 settings.languages.0.insert(
9473 "Rust".into(),
9474 LanguageSettingsContent {
9475 tab_size: NonZeroU32::new(8),
9476 ..Default::default()
9477 },
9478 );
9479 });
9480
9481 {
9482 editor.update_in(cx, |editor, window, cx| {
9483 editor.set_text("somehting_new\n", window, cx)
9484 });
9485 assert!(cx.read(|cx| editor.is_dirty(cx)));
9486 let _formatting_request_signal = fake_server
9487 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9488 assert_eq!(
9489 params.text_document.uri,
9490 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9491 );
9492 assert_eq!(params.options.tab_size, 8);
9493 Ok(Some(vec![]))
9494 });
9495 let save = editor
9496 .update_in(cx, |editor, window, cx| {
9497 editor.save(
9498 SaveOptions {
9499 format: true,
9500 autosave: false,
9501 },
9502 project.clone(),
9503 window,
9504 cx,
9505 )
9506 })
9507 .unwrap();
9508 cx.executor().start_waiting();
9509 save.await;
9510 }
9511}
9512
9513#[gpui::test]
9514async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9515 init_test(cx, |_| {});
9516
9517 let cols = 4;
9518 let rows = 10;
9519 let sample_text_1 = sample_text(rows, cols, 'a');
9520 assert_eq!(
9521 sample_text_1,
9522 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9523 );
9524 let sample_text_2 = sample_text(rows, cols, 'l');
9525 assert_eq!(
9526 sample_text_2,
9527 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9528 );
9529 let sample_text_3 = sample_text(rows, cols, 'v');
9530 assert_eq!(
9531 sample_text_3,
9532 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9533 );
9534
9535 let fs = FakeFs::new(cx.executor());
9536 fs.insert_tree(
9537 path!("/a"),
9538 json!({
9539 "main.rs": sample_text_1,
9540 "other.rs": sample_text_2,
9541 "lib.rs": sample_text_3,
9542 }),
9543 )
9544 .await;
9545
9546 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9547 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9548 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9549
9550 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9551 language_registry.add(rust_lang());
9552 let mut fake_servers = language_registry.register_fake_lsp(
9553 "Rust",
9554 FakeLspAdapter {
9555 capabilities: lsp::ServerCapabilities {
9556 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9557 ..Default::default()
9558 },
9559 ..Default::default()
9560 },
9561 );
9562
9563 let worktree = project.update(cx, |project, cx| {
9564 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
9565 assert_eq!(worktrees.len(), 1);
9566 worktrees.pop().unwrap()
9567 });
9568 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9569
9570 let buffer_1 = project
9571 .update(cx, |project, cx| {
9572 project.open_buffer((worktree_id, "main.rs"), cx)
9573 })
9574 .await
9575 .unwrap();
9576 let buffer_2 = project
9577 .update(cx, |project, cx| {
9578 project.open_buffer((worktree_id, "other.rs"), cx)
9579 })
9580 .await
9581 .unwrap();
9582 let buffer_3 = project
9583 .update(cx, |project, cx| {
9584 project.open_buffer((worktree_id, "lib.rs"), cx)
9585 })
9586 .await
9587 .unwrap();
9588
9589 let multi_buffer = cx.new(|cx| {
9590 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9591 multi_buffer.push_excerpts(
9592 buffer_1.clone(),
9593 [
9594 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9595 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9596 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9597 ],
9598 cx,
9599 );
9600 multi_buffer.push_excerpts(
9601 buffer_2.clone(),
9602 [
9603 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9604 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9605 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9606 ],
9607 cx,
9608 );
9609 multi_buffer.push_excerpts(
9610 buffer_3.clone(),
9611 [
9612 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9613 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9614 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9615 ],
9616 cx,
9617 );
9618 multi_buffer
9619 });
9620 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
9621 Editor::new(
9622 EditorMode::full(),
9623 multi_buffer,
9624 Some(project.clone()),
9625 window,
9626 cx,
9627 )
9628 });
9629
9630 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9631 editor.change_selections(
9632 SelectionEffects::scroll(Autoscroll::Next),
9633 window,
9634 cx,
9635 |s| s.select_ranges(Some(1..2)),
9636 );
9637 editor.insert("|one|two|three|", window, cx);
9638 });
9639 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9640 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9641 editor.change_selections(
9642 SelectionEffects::scroll(Autoscroll::Next),
9643 window,
9644 cx,
9645 |s| s.select_ranges(Some(60..70)),
9646 );
9647 editor.insert("|four|five|six|", window, cx);
9648 });
9649 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9650
9651 // First two buffers should be edited, but not the third one.
9652 assert_eq!(
9653 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9654 "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}",
9655 );
9656 buffer_1.update(cx, |buffer, _| {
9657 assert!(buffer.is_dirty());
9658 assert_eq!(
9659 buffer.text(),
9660 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
9661 )
9662 });
9663 buffer_2.update(cx, |buffer, _| {
9664 assert!(buffer.is_dirty());
9665 assert_eq!(
9666 buffer.text(),
9667 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
9668 )
9669 });
9670 buffer_3.update(cx, |buffer, _| {
9671 assert!(!buffer.is_dirty());
9672 assert_eq!(buffer.text(), sample_text_3,)
9673 });
9674 cx.executor().run_until_parked();
9675
9676 cx.executor().start_waiting();
9677 let save = multi_buffer_editor
9678 .update_in(cx, |editor, window, cx| {
9679 editor.save(
9680 SaveOptions {
9681 format: true,
9682 autosave: false,
9683 },
9684 project.clone(),
9685 window,
9686 cx,
9687 )
9688 })
9689 .unwrap();
9690
9691 let fake_server = fake_servers.next().await.unwrap();
9692 fake_server
9693 .server
9694 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
9695 Ok(Some(vec![lsp::TextEdit::new(
9696 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9697 format!("[{} formatted]", params.text_document.uri),
9698 )]))
9699 })
9700 .detach();
9701 save.await;
9702
9703 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
9704 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
9705 assert_eq!(
9706 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9707 uri!(
9708 "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}"
9709 ),
9710 );
9711 buffer_1.update(cx, |buffer, _| {
9712 assert!(!buffer.is_dirty());
9713 assert_eq!(
9714 buffer.text(),
9715 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
9716 )
9717 });
9718 buffer_2.update(cx, |buffer, _| {
9719 assert!(!buffer.is_dirty());
9720 assert_eq!(
9721 buffer.text(),
9722 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
9723 )
9724 });
9725 buffer_3.update(cx, |buffer, _| {
9726 assert!(!buffer.is_dirty());
9727 assert_eq!(buffer.text(), sample_text_3,)
9728 });
9729}
9730
9731#[gpui::test]
9732async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
9733 init_test(cx, |_| {});
9734
9735 let fs = FakeFs::new(cx.executor());
9736 fs.insert_tree(
9737 path!("/dir"),
9738 json!({
9739 "file1.rs": "fn main() { println!(\"hello\"); }",
9740 "file2.rs": "fn test() { println!(\"test\"); }",
9741 "file3.rs": "fn other() { println!(\"other\"); }\n",
9742 }),
9743 )
9744 .await;
9745
9746 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
9747 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9748 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9749
9750 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9751 language_registry.add(rust_lang());
9752
9753 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
9754 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9755
9756 // Open three buffers
9757 let buffer_1 = project
9758 .update(cx, |project, cx| {
9759 project.open_buffer((worktree_id, "file1.rs"), cx)
9760 })
9761 .await
9762 .unwrap();
9763 let buffer_2 = project
9764 .update(cx, |project, cx| {
9765 project.open_buffer((worktree_id, "file2.rs"), cx)
9766 })
9767 .await
9768 .unwrap();
9769 let buffer_3 = project
9770 .update(cx, |project, cx| {
9771 project.open_buffer((worktree_id, "file3.rs"), cx)
9772 })
9773 .await
9774 .unwrap();
9775
9776 // Create a multi-buffer with all three buffers
9777 let multi_buffer = cx.new(|cx| {
9778 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9779 multi_buffer.push_excerpts(
9780 buffer_1.clone(),
9781 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9782 cx,
9783 );
9784 multi_buffer.push_excerpts(
9785 buffer_2.clone(),
9786 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9787 cx,
9788 );
9789 multi_buffer.push_excerpts(
9790 buffer_3.clone(),
9791 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9792 cx,
9793 );
9794 multi_buffer
9795 });
9796
9797 let editor = cx.new_window_entity(|window, cx| {
9798 Editor::new(
9799 EditorMode::full(),
9800 multi_buffer,
9801 Some(project.clone()),
9802 window,
9803 cx,
9804 )
9805 });
9806
9807 // Edit only the first buffer
9808 editor.update_in(cx, |editor, window, cx| {
9809 editor.change_selections(
9810 SelectionEffects::scroll(Autoscroll::Next),
9811 window,
9812 cx,
9813 |s| s.select_ranges(Some(10..10)),
9814 );
9815 editor.insert("// edited", window, cx);
9816 });
9817
9818 // Verify that only buffer 1 is dirty
9819 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
9820 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9821 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9822
9823 // Get write counts after file creation (files were created with initial content)
9824 // We expect each file to have been written once during creation
9825 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
9826 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
9827 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
9828
9829 // Perform autosave
9830 let save_task = editor.update_in(cx, |editor, window, cx| {
9831 editor.save(
9832 SaveOptions {
9833 format: true,
9834 autosave: true,
9835 },
9836 project.clone(),
9837 window,
9838 cx,
9839 )
9840 });
9841 save_task.await.unwrap();
9842
9843 // Only the dirty buffer should have been saved
9844 assert_eq!(
9845 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9846 1,
9847 "Buffer 1 was dirty, so it should have been written once during autosave"
9848 );
9849 assert_eq!(
9850 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9851 0,
9852 "Buffer 2 was clean, so it should not have been written during autosave"
9853 );
9854 assert_eq!(
9855 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9856 0,
9857 "Buffer 3 was clean, so it should not have been written during autosave"
9858 );
9859
9860 // Verify buffer states after autosave
9861 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9862 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9863 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9864
9865 // Now perform a manual save (format = true)
9866 let save_task = editor.update_in(cx, |editor, window, cx| {
9867 editor.save(
9868 SaveOptions {
9869 format: true,
9870 autosave: false,
9871 },
9872 project.clone(),
9873 window,
9874 cx,
9875 )
9876 });
9877 save_task.await.unwrap();
9878
9879 // During manual save, clean buffers don't get written to disk
9880 // They just get did_save called for language server notifications
9881 assert_eq!(
9882 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9883 1,
9884 "Buffer 1 should only have been written once total (during autosave, not manual save)"
9885 );
9886 assert_eq!(
9887 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9888 0,
9889 "Buffer 2 should not have been written at all"
9890 );
9891 assert_eq!(
9892 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9893 0,
9894 "Buffer 3 should not have been written at all"
9895 );
9896}
9897
9898#[gpui::test]
9899async fn test_range_format_during_save(cx: &mut TestAppContext) {
9900 init_test(cx, |_| {});
9901
9902 let fs = FakeFs::new(cx.executor());
9903 fs.insert_file(path!("/file.rs"), Default::default()).await;
9904
9905 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9906
9907 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9908 language_registry.add(rust_lang());
9909 let mut fake_servers = language_registry.register_fake_lsp(
9910 "Rust",
9911 FakeLspAdapter {
9912 capabilities: lsp::ServerCapabilities {
9913 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
9914 ..Default::default()
9915 },
9916 ..Default::default()
9917 },
9918 );
9919
9920 let buffer = project
9921 .update(cx, |project, cx| {
9922 project.open_local_buffer(path!("/file.rs"), cx)
9923 })
9924 .await
9925 .unwrap();
9926
9927 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9928 let (editor, cx) = cx.add_window_view(|window, cx| {
9929 build_editor_with_project(project.clone(), buffer, window, cx)
9930 });
9931 editor.update_in(cx, |editor, window, cx| {
9932 editor.set_text("one\ntwo\nthree\n", window, cx)
9933 });
9934 assert!(cx.read(|cx| editor.is_dirty(cx)));
9935
9936 cx.executor().start_waiting();
9937 let fake_server = fake_servers.next().await.unwrap();
9938
9939 let save = editor
9940 .update_in(cx, |editor, window, cx| {
9941 editor.save(
9942 SaveOptions {
9943 format: true,
9944 autosave: false,
9945 },
9946 project.clone(),
9947 window,
9948 cx,
9949 )
9950 })
9951 .unwrap();
9952 fake_server
9953 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9954 assert_eq!(
9955 params.text_document.uri,
9956 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9957 );
9958 assert_eq!(params.options.tab_size, 4);
9959 Ok(Some(vec![lsp::TextEdit::new(
9960 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9961 ", ".to_string(),
9962 )]))
9963 })
9964 .next()
9965 .await;
9966 cx.executor().start_waiting();
9967 save.await;
9968 assert_eq!(
9969 editor.update(cx, |editor, cx| editor.text(cx)),
9970 "one, two\nthree\n"
9971 );
9972 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9973
9974 editor.update_in(cx, |editor, window, cx| {
9975 editor.set_text("one\ntwo\nthree\n", window, cx)
9976 });
9977 assert!(cx.read(|cx| editor.is_dirty(cx)));
9978
9979 // Ensure we can still save even if formatting hangs.
9980 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
9981 move |params, _| async move {
9982 assert_eq!(
9983 params.text_document.uri,
9984 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9985 );
9986 futures::future::pending::<()>().await;
9987 unreachable!()
9988 },
9989 );
9990 let save = editor
9991 .update_in(cx, |editor, window, cx| {
9992 editor.save(
9993 SaveOptions {
9994 format: true,
9995 autosave: false,
9996 },
9997 project.clone(),
9998 window,
9999 cx,
10000 )
10001 })
10002 .unwrap();
10003 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10004 cx.executor().start_waiting();
10005 save.await;
10006 assert_eq!(
10007 editor.update(cx, |editor, cx| editor.text(cx)),
10008 "one\ntwo\nthree\n"
10009 );
10010 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10011
10012 // For non-dirty buffer, no formatting request should be sent
10013 let save = editor
10014 .update_in(cx, |editor, window, cx| {
10015 editor.save(
10016 SaveOptions {
10017 format: false,
10018 autosave: false,
10019 },
10020 project.clone(),
10021 window,
10022 cx,
10023 )
10024 })
10025 .unwrap();
10026 let _pending_format_request = fake_server
10027 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
10028 panic!("Should not be invoked");
10029 })
10030 .next();
10031 cx.executor().start_waiting();
10032 save.await;
10033
10034 // Set Rust language override and assert overridden tabsize is sent to language server
10035 update_test_language_settings(cx, |settings| {
10036 settings.languages.0.insert(
10037 "Rust".into(),
10038 LanguageSettingsContent {
10039 tab_size: NonZeroU32::new(8),
10040 ..Default::default()
10041 },
10042 );
10043 });
10044
10045 editor.update_in(cx, |editor, window, cx| {
10046 editor.set_text("somehting_new\n", window, cx)
10047 });
10048 assert!(cx.read(|cx| editor.is_dirty(cx)));
10049 let save = editor
10050 .update_in(cx, |editor, window, cx| {
10051 editor.save(
10052 SaveOptions {
10053 format: true,
10054 autosave: false,
10055 },
10056 project.clone(),
10057 window,
10058 cx,
10059 )
10060 })
10061 .unwrap();
10062 fake_server
10063 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10064 assert_eq!(
10065 params.text_document.uri,
10066 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10067 );
10068 assert_eq!(params.options.tab_size, 8);
10069 Ok(Some(Vec::new()))
10070 })
10071 .next()
10072 .await;
10073 save.await;
10074}
10075
10076#[gpui::test]
10077async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
10078 init_test(cx, |settings| {
10079 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
10080 Formatter::LanguageServer { name: None },
10081 )))
10082 });
10083
10084 let fs = FakeFs::new(cx.executor());
10085 fs.insert_file(path!("/file.rs"), Default::default()).await;
10086
10087 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10088
10089 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10090 language_registry.add(Arc::new(Language::new(
10091 LanguageConfig {
10092 name: "Rust".into(),
10093 matcher: LanguageMatcher {
10094 path_suffixes: vec!["rs".to_string()],
10095 ..Default::default()
10096 },
10097 ..LanguageConfig::default()
10098 },
10099 Some(tree_sitter_rust::LANGUAGE.into()),
10100 )));
10101 update_test_language_settings(cx, |settings| {
10102 // Enable Prettier formatting for the same buffer, and ensure
10103 // LSP is called instead of Prettier.
10104 settings.defaults.prettier = Some(PrettierSettings {
10105 allowed: true,
10106 ..PrettierSettings::default()
10107 });
10108 });
10109 let mut fake_servers = language_registry.register_fake_lsp(
10110 "Rust",
10111 FakeLspAdapter {
10112 capabilities: lsp::ServerCapabilities {
10113 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10114 ..Default::default()
10115 },
10116 ..Default::default()
10117 },
10118 );
10119
10120 let buffer = project
10121 .update(cx, |project, cx| {
10122 project.open_local_buffer(path!("/file.rs"), cx)
10123 })
10124 .await
10125 .unwrap();
10126
10127 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10128 let (editor, cx) = cx.add_window_view(|window, cx| {
10129 build_editor_with_project(project.clone(), buffer, window, cx)
10130 });
10131 editor.update_in(cx, |editor, window, cx| {
10132 editor.set_text("one\ntwo\nthree\n", window, cx)
10133 });
10134
10135 cx.executor().start_waiting();
10136 let fake_server = fake_servers.next().await.unwrap();
10137
10138 let format = editor
10139 .update_in(cx, |editor, window, cx| {
10140 editor.perform_format(
10141 project.clone(),
10142 FormatTrigger::Manual,
10143 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10144 window,
10145 cx,
10146 )
10147 })
10148 .unwrap();
10149 fake_server
10150 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10151 assert_eq!(
10152 params.text_document.uri,
10153 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10154 );
10155 assert_eq!(params.options.tab_size, 4);
10156 Ok(Some(vec![lsp::TextEdit::new(
10157 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10158 ", ".to_string(),
10159 )]))
10160 })
10161 .next()
10162 .await;
10163 cx.executor().start_waiting();
10164 format.await;
10165 assert_eq!(
10166 editor.update(cx, |editor, cx| editor.text(cx)),
10167 "one, two\nthree\n"
10168 );
10169
10170 editor.update_in(cx, |editor, window, cx| {
10171 editor.set_text("one\ntwo\nthree\n", window, cx)
10172 });
10173 // Ensure we don't lock if formatting hangs.
10174 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10175 move |params, _| async move {
10176 assert_eq!(
10177 params.text_document.uri,
10178 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10179 );
10180 futures::future::pending::<()>().await;
10181 unreachable!()
10182 },
10183 );
10184 let format = editor
10185 .update_in(cx, |editor, window, cx| {
10186 editor.perform_format(
10187 project,
10188 FormatTrigger::Manual,
10189 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10190 window,
10191 cx,
10192 )
10193 })
10194 .unwrap();
10195 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10196 cx.executor().start_waiting();
10197 format.await;
10198 assert_eq!(
10199 editor.update(cx, |editor, cx| editor.text(cx)),
10200 "one\ntwo\nthree\n"
10201 );
10202}
10203
10204#[gpui::test]
10205async fn test_multiple_formatters(cx: &mut TestAppContext) {
10206 init_test(cx, |settings| {
10207 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
10208 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10209 Formatter::LanguageServer { name: None },
10210 Formatter::CodeActions(
10211 [
10212 ("code-action-1".into(), true),
10213 ("code-action-2".into(), true),
10214 ]
10215 .into_iter()
10216 .collect(),
10217 ),
10218 ])))
10219 });
10220
10221 let fs = FakeFs::new(cx.executor());
10222 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
10223 .await;
10224
10225 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10226 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10227 language_registry.add(rust_lang());
10228
10229 let mut fake_servers = language_registry.register_fake_lsp(
10230 "Rust",
10231 FakeLspAdapter {
10232 capabilities: lsp::ServerCapabilities {
10233 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10234 execute_command_provider: Some(lsp::ExecuteCommandOptions {
10235 commands: vec!["the-command-for-code-action-1".into()],
10236 ..Default::default()
10237 }),
10238 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10239 ..Default::default()
10240 },
10241 ..Default::default()
10242 },
10243 );
10244
10245 let buffer = project
10246 .update(cx, |project, cx| {
10247 project.open_local_buffer(path!("/file.rs"), cx)
10248 })
10249 .await
10250 .unwrap();
10251
10252 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10253 let (editor, cx) = cx.add_window_view(|window, cx| {
10254 build_editor_with_project(project.clone(), buffer, window, cx)
10255 });
10256
10257 cx.executor().start_waiting();
10258
10259 let fake_server = fake_servers.next().await.unwrap();
10260 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10261 move |_params, _| async move {
10262 Ok(Some(vec![lsp::TextEdit::new(
10263 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10264 "applied-formatting\n".to_string(),
10265 )]))
10266 },
10267 );
10268 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10269 move |params, _| async move {
10270 assert_eq!(
10271 params.context.only,
10272 Some(vec!["code-action-1".into(), "code-action-2".into()])
10273 );
10274 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
10275 Ok(Some(vec![
10276 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10277 kind: Some("code-action-1".into()),
10278 edit: Some(lsp::WorkspaceEdit::new(
10279 [(
10280 uri.clone(),
10281 vec![lsp::TextEdit::new(
10282 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10283 "applied-code-action-1-edit\n".to_string(),
10284 )],
10285 )]
10286 .into_iter()
10287 .collect(),
10288 )),
10289 command: Some(lsp::Command {
10290 command: "the-command-for-code-action-1".into(),
10291 ..Default::default()
10292 }),
10293 ..Default::default()
10294 }),
10295 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10296 kind: Some("code-action-2".into()),
10297 edit: Some(lsp::WorkspaceEdit::new(
10298 [(
10299 uri.clone(),
10300 vec![lsp::TextEdit::new(
10301 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10302 "applied-code-action-2-edit\n".to_string(),
10303 )],
10304 )]
10305 .into_iter()
10306 .collect(),
10307 )),
10308 ..Default::default()
10309 }),
10310 ]))
10311 },
10312 );
10313
10314 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
10315 move |params, _| async move { Ok(params) }
10316 });
10317
10318 let command_lock = Arc::new(futures::lock::Mutex::new(()));
10319 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
10320 let fake = fake_server.clone();
10321 let lock = command_lock.clone();
10322 move |params, _| {
10323 assert_eq!(params.command, "the-command-for-code-action-1");
10324 let fake = fake.clone();
10325 let lock = lock.clone();
10326 async move {
10327 lock.lock().await;
10328 fake.server
10329 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
10330 label: None,
10331 edit: lsp::WorkspaceEdit {
10332 changes: Some(
10333 [(
10334 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
10335 vec![lsp::TextEdit {
10336 range: lsp::Range::new(
10337 lsp::Position::new(0, 0),
10338 lsp::Position::new(0, 0),
10339 ),
10340 new_text: "applied-code-action-1-command\n".into(),
10341 }],
10342 )]
10343 .into_iter()
10344 .collect(),
10345 ),
10346 ..Default::default()
10347 },
10348 })
10349 .await
10350 .into_response()
10351 .unwrap();
10352 Ok(Some(json!(null)))
10353 }
10354 }
10355 });
10356
10357 cx.executor().start_waiting();
10358 editor
10359 .update_in(cx, |editor, window, cx| {
10360 editor.perform_format(
10361 project.clone(),
10362 FormatTrigger::Manual,
10363 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10364 window,
10365 cx,
10366 )
10367 })
10368 .unwrap()
10369 .await;
10370 editor.update(cx, |editor, cx| {
10371 assert_eq!(
10372 editor.text(cx),
10373 r#"
10374 applied-code-action-2-edit
10375 applied-code-action-1-command
10376 applied-code-action-1-edit
10377 applied-formatting
10378 one
10379 two
10380 three
10381 "#
10382 .unindent()
10383 );
10384 });
10385
10386 editor.update_in(cx, |editor, window, cx| {
10387 editor.undo(&Default::default(), window, cx);
10388 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10389 });
10390
10391 // Perform a manual edit while waiting for an LSP command
10392 // that's being run as part of a formatting code action.
10393 let lock_guard = command_lock.lock().await;
10394 let format = editor
10395 .update_in(cx, |editor, window, cx| {
10396 editor.perform_format(
10397 project.clone(),
10398 FormatTrigger::Manual,
10399 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10400 window,
10401 cx,
10402 )
10403 })
10404 .unwrap();
10405 cx.run_until_parked();
10406 editor.update(cx, |editor, cx| {
10407 assert_eq!(
10408 editor.text(cx),
10409 r#"
10410 applied-code-action-1-edit
10411 applied-formatting
10412 one
10413 two
10414 three
10415 "#
10416 .unindent()
10417 );
10418
10419 editor.buffer.update(cx, |buffer, cx| {
10420 let ix = buffer.len(cx);
10421 buffer.edit([(ix..ix, "edited\n")], None, cx);
10422 });
10423 });
10424
10425 // Allow the LSP command to proceed. Because the buffer was edited,
10426 // the second code action will not be run.
10427 drop(lock_guard);
10428 format.await;
10429 editor.update_in(cx, |editor, window, cx| {
10430 assert_eq!(
10431 editor.text(cx),
10432 r#"
10433 applied-code-action-1-command
10434 applied-code-action-1-edit
10435 applied-formatting
10436 one
10437 two
10438 three
10439 edited
10440 "#
10441 .unindent()
10442 );
10443
10444 // The manual edit is undone first, because it is the last thing the user did
10445 // (even though the command completed afterwards).
10446 editor.undo(&Default::default(), window, cx);
10447 assert_eq!(
10448 editor.text(cx),
10449 r#"
10450 applied-code-action-1-command
10451 applied-code-action-1-edit
10452 applied-formatting
10453 one
10454 two
10455 three
10456 "#
10457 .unindent()
10458 );
10459
10460 // All the formatting (including the command, which completed after the manual edit)
10461 // is undone together.
10462 editor.undo(&Default::default(), window, cx);
10463 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10464 });
10465}
10466
10467#[gpui::test]
10468async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
10469 init_test(cx, |settings| {
10470 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10471 Formatter::LanguageServer { name: None },
10472 ])))
10473 });
10474
10475 let fs = FakeFs::new(cx.executor());
10476 fs.insert_file(path!("/file.ts"), Default::default()).await;
10477
10478 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10479
10480 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10481 language_registry.add(Arc::new(Language::new(
10482 LanguageConfig {
10483 name: "TypeScript".into(),
10484 matcher: LanguageMatcher {
10485 path_suffixes: vec!["ts".to_string()],
10486 ..Default::default()
10487 },
10488 ..LanguageConfig::default()
10489 },
10490 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10491 )));
10492 update_test_language_settings(cx, |settings| {
10493 settings.defaults.prettier = Some(PrettierSettings {
10494 allowed: true,
10495 ..PrettierSettings::default()
10496 });
10497 });
10498 let mut fake_servers = language_registry.register_fake_lsp(
10499 "TypeScript",
10500 FakeLspAdapter {
10501 capabilities: lsp::ServerCapabilities {
10502 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10503 ..Default::default()
10504 },
10505 ..Default::default()
10506 },
10507 );
10508
10509 let buffer = project
10510 .update(cx, |project, cx| {
10511 project.open_local_buffer(path!("/file.ts"), cx)
10512 })
10513 .await
10514 .unwrap();
10515
10516 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10517 let (editor, cx) = cx.add_window_view(|window, cx| {
10518 build_editor_with_project(project.clone(), buffer, window, cx)
10519 });
10520 editor.update_in(cx, |editor, window, cx| {
10521 editor.set_text(
10522 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10523 window,
10524 cx,
10525 )
10526 });
10527
10528 cx.executor().start_waiting();
10529 let fake_server = fake_servers.next().await.unwrap();
10530
10531 let format = editor
10532 .update_in(cx, |editor, window, cx| {
10533 editor.perform_code_action_kind(
10534 project.clone(),
10535 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10536 window,
10537 cx,
10538 )
10539 })
10540 .unwrap();
10541 fake_server
10542 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
10543 assert_eq!(
10544 params.text_document.uri,
10545 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10546 );
10547 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
10548 lsp::CodeAction {
10549 title: "Organize Imports".to_string(),
10550 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
10551 edit: Some(lsp::WorkspaceEdit {
10552 changes: Some(
10553 [(
10554 params.text_document.uri.clone(),
10555 vec![lsp::TextEdit::new(
10556 lsp::Range::new(
10557 lsp::Position::new(1, 0),
10558 lsp::Position::new(2, 0),
10559 ),
10560 "".to_string(),
10561 )],
10562 )]
10563 .into_iter()
10564 .collect(),
10565 ),
10566 ..Default::default()
10567 }),
10568 ..Default::default()
10569 },
10570 )]))
10571 })
10572 .next()
10573 .await;
10574 cx.executor().start_waiting();
10575 format.await;
10576 assert_eq!(
10577 editor.update(cx, |editor, cx| editor.text(cx)),
10578 "import { a } from 'module';\n\nconst x = a;\n"
10579 );
10580
10581 editor.update_in(cx, |editor, window, cx| {
10582 editor.set_text(
10583 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10584 window,
10585 cx,
10586 )
10587 });
10588 // Ensure we don't lock if code action hangs.
10589 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10590 move |params, _| async move {
10591 assert_eq!(
10592 params.text_document.uri,
10593 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10594 );
10595 futures::future::pending::<()>().await;
10596 unreachable!()
10597 },
10598 );
10599 let format = editor
10600 .update_in(cx, |editor, window, cx| {
10601 editor.perform_code_action_kind(
10602 project,
10603 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10604 window,
10605 cx,
10606 )
10607 })
10608 .unwrap();
10609 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
10610 cx.executor().start_waiting();
10611 format.await;
10612 assert_eq!(
10613 editor.update(cx, |editor, cx| editor.text(cx)),
10614 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
10615 );
10616}
10617
10618#[gpui::test]
10619async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
10620 init_test(cx, |_| {});
10621
10622 let mut cx = EditorLspTestContext::new_rust(
10623 lsp::ServerCapabilities {
10624 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10625 ..Default::default()
10626 },
10627 cx,
10628 )
10629 .await;
10630
10631 cx.set_state(indoc! {"
10632 one.twoˇ
10633 "});
10634
10635 // The format request takes a long time. When it completes, it inserts
10636 // a newline and an indent before the `.`
10637 cx.lsp
10638 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
10639 let executor = cx.background_executor().clone();
10640 async move {
10641 executor.timer(Duration::from_millis(100)).await;
10642 Ok(Some(vec![lsp::TextEdit {
10643 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
10644 new_text: "\n ".into(),
10645 }]))
10646 }
10647 });
10648
10649 // Submit a format request.
10650 let format_1 = cx
10651 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10652 .unwrap();
10653 cx.executor().run_until_parked();
10654
10655 // Submit a second format request.
10656 let format_2 = cx
10657 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10658 .unwrap();
10659 cx.executor().run_until_parked();
10660
10661 // Wait for both format requests to complete
10662 cx.executor().advance_clock(Duration::from_millis(200));
10663 cx.executor().start_waiting();
10664 format_1.await.unwrap();
10665 cx.executor().start_waiting();
10666 format_2.await.unwrap();
10667
10668 // The formatting edits only happens once.
10669 cx.assert_editor_state(indoc! {"
10670 one
10671 .twoˇ
10672 "});
10673}
10674
10675#[gpui::test]
10676async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
10677 init_test(cx, |settings| {
10678 settings.defaults.formatter = Some(SelectedFormatter::Auto)
10679 });
10680
10681 let mut cx = EditorLspTestContext::new_rust(
10682 lsp::ServerCapabilities {
10683 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10684 ..Default::default()
10685 },
10686 cx,
10687 )
10688 .await;
10689
10690 // Set up a buffer white some trailing whitespace and no trailing newline.
10691 cx.set_state(
10692 &[
10693 "one ", //
10694 "twoˇ", //
10695 "three ", //
10696 "four", //
10697 ]
10698 .join("\n"),
10699 );
10700
10701 // Submit a format request.
10702 let format = cx
10703 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10704 .unwrap();
10705
10706 // Record which buffer changes have been sent to the language server
10707 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
10708 cx.lsp
10709 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
10710 let buffer_changes = buffer_changes.clone();
10711 move |params, _| {
10712 buffer_changes.lock().extend(
10713 params
10714 .content_changes
10715 .into_iter()
10716 .map(|e| (e.range.unwrap(), e.text)),
10717 );
10718 }
10719 });
10720
10721 // Handle formatting requests to the language server.
10722 cx.lsp
10723 .set_request_handler::<lsp::request::Formatting, _, _>({
10724 let buffer_changes = buffer_changes.clone();
10725 move |_, _| {
10726 // When formatting is requested, trailing whitespace has already been stripped,
10727 // and the trailing newline has already been added.
10728 assert_eq!(
10729 &buffer_changes.lock()[1..],
10730 &[
10731 (
10732 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
10733 "".into()
10734 ),
10735 (
10736 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
10737 "".into()
10738 ),
10739 (
10740 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
10741 "\n".into()
10742 ),
10743 ]
10744 );
10745
10746 // Insert blank lines between each line of the buffer.
10747 async move {
10748 Ok(Some(vec![
10749 lsp::TextEdit {
10750 range: lsp::Range::new(
10751 lsp::Position::new(1, 0),
10752 lsp::Position::new(1, 0),
10753 ),
10754 new_text: "\n".into(),
10755 },
10756 lsp::TextEdit {
10757 range: lsp::Range::new(
10758 lsp::Position::new(2, 0),
10759 lsp::Position::new(2, 0),
10760 ),
10761 new_text: "\n".into(),
10762 },
10763 ]))
10764 }
10765 }
10766 });
10767
10768 // After formatting the buffer, the trailing whitespace is stripped,
10769 // a newline is appended, and the edits provided by the language server
10770 // have been applied.
10771 format.await.unwrap();
10772 cx.assert_editor_state(
10773 &[
10774 "one", //
10775 "", //
10776 "twoˇ", //
10777 "", //
10778 "three", //
10779 "four", //
10780 "", //
10781 ]
10782 .join("\n"),
10783 );
10784
10785 // Undoing the formatting undoes the trailing whitespace removal, the
10786 // trailing newline, and the LSP edits.
10787 cx.update_buffer(|buffer, cx| buffer.undo(cx));
10788 cx.assert_editor_state(
10789 &[
10790 "one ", //
10791 "twoˇ", //
10792 "three ", //
10793 "four", //
10794 ]
10795 .join("\n"),
10796 );
10797}
10798
10799#[gpui::test]
10800async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
10801 cx: &mut TestAppContext,
10802) {
10803 init_test(cx, |_| {});
10804
10805 cx.update(|cx| {
10806 cx.update_global::<SettingsStore, _>(|settings, cx| {
10807 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10808 settings.auto_signature_help = Some(true);
10809 });
10810 });
10811 });
10812
10813 let mut cx = EditorLspTestContext::new_rust(
10814 lsp::ServerCapabilities {
10815 signature_help_provider: Some(lsp::SignatureHelpOptions {
10816 ..Default::default()
10817 }),
10818 ..Default::default()
10819 },
10820 cx,
10821 )
10822 .await;
10823
10824 let language = Language::new(
10825 LanguageConfig {
10826 name: "Rust".into(),
10827 brackets: BracketPairConfig {
10828 pairs: vec![
10829 BracketPair {
10830 start: "{".to_string(),
10831 end: "}".to_string(),
10832 close: true,
10833 surround: true,
10834 newline: true,
10835 },
10836 BracketPair {
10837 start: "(".to_string(),
10838 end: ")".to_string(),
10839 close: true,
10840 surround: true,
10841 newline: true,
10842 },
10843 BracketPair {
10844 start: "/*".to_string(),
10845 end: " */".to_string(),
10846 close: true,
10847 surround: true,
10848 newline: true,
10849 },
10850 BracketPair {
10851 start: "[".to_string(),
10852 end: "]".to_string(),
10853 close: false,
10854 surround: false,
10855 newline: true,
10856 },
10857 BracketPair {
10858 start: "\"".to_string(),
10859 end: "\"".to_string(),
10860 close: true,
10861 surround: true,
10862 newline: false,
10863 },
10864 BracketPair {
10865 start: "<".to_string(),
10866 end: ">".to_string(),
10867 close: false,
10868 surround: true,
10869 newline: true,
10870 },
10871 ],
10872 ..Default::default()
10873 },
10874 autoclose_before: "})]".to_string(),
10875 ..Default::default()
10876 },
10877 Some(tree_sitter_rust::LANGUAGE.into()),
10878 );
10879 let language = Arc::new(language);
10880
10881 cx.language_registry().add(language.clone());
10882 cx.update_buffer(|buffer, cx| {
10883 buffer.set_language(Some(language), cx);
10884 });
10885
10886 cx.set_state(
10887 &r#"
10888 fn main() {
10889 sampleˇ
10890 }
10891 "#
10892 .unindent(),
10893 );
10894
10895 cx.update_editor(|editor, window, cx| {
10896 editor.handle_input("(", window, cx);
10897 });
10898 cx.assert_editor_state(
10899 &"
10900 fn main() {
10901 sample(ˇ)
10902 }
10903 "
10904 .unindent(),
10905 );
10906
10907 let mocked_response = lsp::SignatureHelp {
10908 signatures: vec![lsp::SignatureInformation {
10909 label: "fn sample(param1: u8, param2: u8)".to_string(),
10910 documentation: None,
10911 parameters: Some(vec![
10912 lsp::ParameterInformation {
10913 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10914 documentation: None,
10915 },
10916 lsp::ParameterInformation {
10917 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10918 documentation: None,
10919 },
10920 ]),
10921 active_parameter: None,
10922 }],
10923 active_signature: Some(0),
10924 active_parameter: Some(0),
10925 };
10926 handle_signature_help_request(&mut cx, mocked_response).await;
10927
10928 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10929 .await;
10930
10931 cx.editor(|editor, _, _| {
10932 let signature_help_state = editor.signature_help_state.popover().cloned();
10933 let signature = signature_help_state.unwrap();
10934 assert_eq!(
10935 signature.signatures[signature.current_signature].label,
10936 "fn sample(param1: u8, param2: u8)"
10937 );
10938 });
10939}
10940
10941#[gpui::test]
10942async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
10943 init_test(cx, |_| {});
10944
10945 cx.update(|cx| {
10946 cx.update_global::<SettingsStore, _>(|settings, cx| {
10947 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10948 settings.auto_signature_help = Some(false);
10949 settings.show_signature_help_after_edits = Some(false);
10950 });
10951 });
10952 });
10953
10954 let mut cx = EditorLspTestContext::new_rust(
10955 lsp::ServerCapabilities {
10956 signature_help_provider: Some(lsp::SignatureHelpOptions {
10957 ..Default::default()
10958 }),
10959 ..Default::default()
10960 },
10961 cx,
10962 )
10963 .await;
10964
10965 let language = Language::new(
10966 LanguageConfig {
10967 name: "Rust".into(),
10968 brackets: BracketPairConfig {
10969 pairs: vec![
10970 BracketPair {
10971 start: "{".to_string(),
10972 end: "}".to_string(),
10973 close: true,
10974 surround: true,
10975 newline: true,
10976 },
10977 BracketPair {
10978 start: "(".to_string(),
10979 end: ")".to_string(),
10980 close: true,
10981 surround: true,
10982 newline: true,
10983 },
10984 BracketPair {
10985 start: "/*".to_string(),
10986 end: " */".to_string(),
10987 close: true,
10988 surround: true,
10989 newline: true,
10990 },
10991 BracketPair {
10992 start: "[".to_string(),
10993 end: "]".to_string(),
10994 close: false,
10995 surround: false,
10996 newline: true,
10997 },
10998 BracketPair {
10999 start: "\"".to_string(),
11000 end: "\"".to_string(),
11001 close: true,
11002 surround: true,
11003 newline: false,
11004 },
11005 BracketPair {
11006 start: "<".to_string(),
11007 end: ">".to_string(),
11008 close: false,
11009 surround: true,
11010 newline: true,
11011 },
11012 ],
11013 ..Default::default()
11014 },
11015 autoclose_before: "})]".to_string(),
11016 ..Default::default()
11017 },
11018 Some(tree_sitter_rust::LANGUAGE.into()),
11019 );
11020 let language = Arc::new(language);
11021
11022 cx.language_registry().add(language.clone());
11023 cx.update_buffer(|buffer, cx| {
11024 buffer.set_language(Some(language), cx);
11025 });
11026
11027 // Ensure that signature_help is not called when no signature help is enabled.
11028 cx.set_state(
11029 &r#"
11030 fn main() {
11031 sampleˇ
11032 }
11033 "#
11034 .unindent(),
11035 );
11036 cx.update_editor(|editor, window, cx| {
11037 editor.handle_input("(", window, cx);
11038 });
11039 cx.assert_editor_state(
11040 &"
11041 fn main() {
11042 sample(ˇ)
11043 }
11044 "
11045 .unindent(),
11046 );
11047 cx.editor(|editor, _, _| {
11048 assert!(editor.signature_help_state.task().is_none());
11049 });
11050
11051 let mocked_response = lsp::SignatureHelp {
11052 signatures: vec![lsp::SignatureInformation {
11053 label: "fn sample(param1: u8, param2: u8)".to_string(),
11054 documentation: None,
11055 parameters: Some(vec![
11056 lsp::ParameterInformation {
11057 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11058 documentation: None,
11059 },
11060 lsp::ParameterInformation {
11061 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11062 documentation: None,
11063 },
11064 ]),
11065 active_parameter: None,
11066 }],
11067 active_signature: Some(0),
11068 active_parameter: Some(0),
11069 };
11070
11071 // Ensure that signature_help is called when enabled afte edits
11072 cx.update(|_, cx| {
11073 cx.update_global::<SettingsStore, _>(|settings, cx| {
11074 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11075 settings.auto_signature_help = Some(false);
11076 settings.show_signature_help_after_edits = Some(true);
11077 });
11078 });
11079 });
11080 cx.set_state(
11081 &r#"
11082 fn main() {
11083 sampleˇ
11084 }
11085 "#
11086 .unindent(),
11087 );
11088 cx.update_editor(|editor, window, cx| {
11089 editor.handle_input("(", window, cx);
11090 });
11091 cx.assert_editor_state(
11092 &"
11093 fn main() {
11094 sample(ˇ)
11095 }
11096 "
11097 .unindent(),
11098 );
11099 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11100 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11101 .await;
11102 cx.update_editor(|editor, _, _| {
11103 let signature_help_state = editor.signature_help_state.popover().cloned();
11104 assert!(signature_help_state.is_some());
11105 let signature = signature_help_state.unwrap();
11106 assert_eq!(
11107 signature.signatures[signature.current_signature].label,
11108 "fn sample(param1: u8, param2: u8)"
11109 );
11110 editor.signature_help_state = SignatureHelpState::default();
11111 });
11112
11113 // Ensure that signature_help is called when auto signature help override is enabled
11114 cx.update(|_, cx| {
11115 cx.update_global::<SettingsStore, _>(|settings, cx| {
11116 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11117 settings.auto_signature_help = Some(true);
11118 settings.show_signature_help_after_edits = Some(false);
11119 });
11120 });
11121 });
11122 cx.set_state(
11123 &r#"
11124 fn main() {
11125 sampleˇ
11126 }
11127 "#
11128 .unindent(),
11129 );
11130 cx.update_editor(|editor, window, cx| {
11131 editor.handle_input("(", window, cx);
11132 });
11133 cx.assert_editor_state(
11134 &"
11135 fn main() {
11136 sample(ˇ)
11137 }
11138 "
11139 .unindent(),
11140 );
11141 handle_signature_help_request(&mut cx, mocked_response).await;
11142 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11143 .await;
11144 cx.editor(|editor, _, _| {
11145 let signature_help_state = editor.signature_help_state.popover().cloned();
11146 assert!(signature_help_state.is_some());
11147 let signature = signature_help_state.unwrap();
11148 assert_eq!(
11149 signature.signatures[signature.current_signature].label,
11150 "fn sample(param1: u8, param2: u8)"
11151 );
11152 });
11153}
11154
11155#[gpui::test]
11156async fn test_signature_help(cx: &mut TestAppContext) {
11157 init_test(cx, |_| {});
11158 cx.update(|cx| {
11159 cx.update_global::<SettingsStore, _>(|settings, cx| {
11160 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11161 settings.auto_signature_help = Some(true);
11162 });
11163 });
11164 });
11165
11166 let mut cx = EditorLspTestContext::new_rust(
11167 lsp::ServerCapabilities {
11168 signature_help_provider: Some(lsp::SignatureHelpOptions {
11169 ..Default::default()
11170 }),
11171 ..Default::default()
11172 },
11173 cx,
11174 )
11175 .await;
11176
11177 // A test that directly calls `show_signature_help`
11178 cx.update_editor(|editor, window, cx| {
11179 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11180 });
11181
11182 let mocked_response = lsp::SignatureHelp {
11183 signatures: vec![lsp::SignatureInformation {
11184 label: "fn sample(param1: u8, param2: u8)".to_string(),
11185 documentation: None,
11186 parameters: Some(vec![
11187 lsp::ParameterInformation {
11188 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11189 documentation: None,
11190 },
11191 lsp::ParameterInformation {
11192 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11193 documentation: None,
11194 },
11195 ]),
11196 active_parameter: None,
11197 }],
11198 active_signature: Some(0),
11199 active_parameter: Some(0),
11200 };
11201 handle_signature_help_request(&mut cx, mocked_response).await;
11202
11203 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11204 .await;
11205
11206 cx.editor(|editor, _, _| {
11207 let signature_help_state = editor.signature_help_state.popover().cloned();
11208 assert!(signature_help_state.is_some());
11209 let signature = signature_help_state.unwrap();
11210 assert_eq!(
11211 signature.signatures[signature.current_signature].label,
11212 "fn sample(param1: u8, param2: u8)"
11213 );
11214 });
11215
11216 // When exiting outside from inside the brackets, `signature_help` is closed.
11217 cx.set_state(indoc! {"
11218 fn main() {
11219 sample(ˇ);
11220 }
11221
11222 fn sample(param1: u8, param2: u8) {}
11223 "});
11224
11225 cx.update_editor(|editor, window, cx| {
11226 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11227 s.select_ranges([0..0])
11228 });
11229 });
11230
11231 let mocked_response = lsp::SignatureHelp {
11232 signatures: Vec::new(),
11233 active_signature: None,
11234 active_parameter: None,
11235 };
11236 handle_signature_help_request(&mut cx, mocked_response).await;
11237
11238 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11239 .await;
11240
11241 cx.editor(|editor, _, _| {
11242 assert!(!editor.signature_help_state.is_shown());
11243 });
11244
11245 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
11246 cx.set_state(indoc! {"
11247 fn main() {
11248 sample(ˇ);
11249 }
11250
11251 fn sample(param1: u8, param2: u8) {}
11252 "});
11253
11254 let mocked_response = lsp::SignatureHelp {
11255 signatures: vec![lsp::SignatureInformation {
11256 label: "fn sample(param1: u8, param2: u8)".to_string(),
11257 documentation: None,
11258 parameters: Some(vec![
11259 lsp::ParameterInformation {
11260 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11261 documentation: None,
11262 },
11263 lsp::ParameterInformation {
11264 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11265 documentation: None,
11266 },
11267 ]),
11268 active_parameter: None,
11269 }],
11270 active_signature: Some(0),
11271 active_parameter: Some(0),
11272 };
11273 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11274 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11275 .await;
11276 cx.editor(|editor, _, _| {
11277 assert!(editor.signature_help_state.is_shown());
11278 });
11279
11280 // Restore the popover with more parameter input
11281 cx.set_state(indoc! {"
11282 fn main() {
11283 sample(param1, param2ˇ);
11284 }
11285
11286 fn sample(param1: u8, param2: u8) {}
11287 "});
11288
11289 let mocked_response = lsp::SignatureHelp {
11290 signatures: vec![lsp::SignatureInformation {
11291 label: "fn sample(param1: u8, param2: u8)".to_string(),
11292 documentation: None,
11293 parameters: Some(vec![
11294 lsp::ParameterInformation {
11295 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11296 documentation: None,
11297 },
11298 lsp::ParameterInformation {
11299 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11300 documentation: None,
11301 },
11302 ]),
11303 active_parameter: None,
11304 }],
11305 active_signature: Some(0),
11306 active_parameter: Some(1),
11307 };
11308 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11309 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11310 .await;
11311
11312 // When selecting a range, the popover is gone.
11313 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
11314 cx.update_editor(|editor, window, cx| {
11315 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11316 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11317 })
11318 });
11319 cx.assert_editor_state(indoc! {"
11320 fn main() {
11321 sample(param1, «ˇparam2»);
11322 }
11323
11324 fn sample(param1: u8, param2: u8) {}
11325 "});
11326 cx.editor(|editor, _, _| {
11327 assert!(!editor.signature_help_state.is_shown());
11328 });
11329
11330 // When unselecting again, the popover is back if within the brackets.
11331 cx.update_editor(|editor, window, cx| {
11332 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11333 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11334 })
11335 });
11336 cx.assert_editor_state(indoc! {"
11337 fn main() {
11338 sample(param1, ˇparam2);
11339 }
11340
11341 fn sample(param1: u8, param2: u8) {}
11342 "});
11343 handle_signature_help_request(&mut cx, mocked_response).await;
11344 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11345 .await;
11346 cx.editor(|editor, _, _| {
11347 assert!(editor.signature_help_state.is_shown());
11348 });
11349
11350 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
11351 cx.update_editor(|editor, window, cx| {
11352 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11353 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
11354 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11355 })
11356 });
11357 cx.assert_editor_state(indoc! {"
11358 fn main() {
11359 sample(param1, ˇparam2);
11360 }
11361
11362 fn sample(param1: u8, param2: u8) {}
11363 "});
11364
11365 let mocked_response = lsp::SignatureHelp {
11366 signatures: vec![lsp::SignatureInformation {
11367 label: "fn sample(param1: u8, param2: u8)".to_string(),
11368 documentation: None,
11369 parameters: Some(vec![
11370 lsp::ParameterInformation {
11371 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11372 documentation: None,
11373 },
11374 lsp::ParameterInformation {
11375 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11376 documentation: None,
11377 },
11378 ]),
11379 active_parameter: None,
11380 }],
11381 active_signature: Some(0),
11382 active_parameter: Some(1),
11383 };
11384 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11385 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11386 .await;
11387 cx.update_editor(|editor, _, cx| {
11388 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
11389 });
11390 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11391 .await;
11392 cx.update_editor(|editor, window, cx| {
11393 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11394 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11395 })
11396 });
11397 cx.assert_editor_state(indoc! {"
11398 fn main() {
11399 sample(param1, «ˇparam2»);
11400 }
11401
11402 fn sample(param1: u8, param2: u8) {}
11403 "});
11404 cx.update_editor(|editor, window, cx| {
11405 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11406 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11407 })
11408 });
11409 cx.assert_editor_state(indoc! {"
11410 fn main() {
11411 sample(param1, ˇparam2);
11412 }
11413
11414 fn sample(param1: u8, param2: u8) {}
11415 "});
11416 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
11417 .await;
11418}
11419
11420#[gpui::test]
11421async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
11422 init_test(cx, |_| {});
11423
11424 let mut cx = EditorLspTestContext::new_rust(
11425 lsp::ServerCapabilities {
11426 signature_help_provider: Some(lsp::SignatureHelpOptions {
11427 ..Default::default()
11428 }),
11429 ..Default::default()
11430 },
11431 cx,
11432 )
11433 .await;
11434
11435 cx.set_state(indoc! {"
11436 fn main() {
11437 overloadedˇ
11438 }
11439 "});
11440
11441 cx.update_editor(|editor, window, cx| {
11442 editor.handle_input("(", window, cx);
11443 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11444 });
11445
11446 // Mock response with 3 signatures
11447 let mocked_response = lsp::SignatureHelp {
11448 signatures: vec![
11449 lsp::SignatureInformation {
11450 label: "fn overloaded(x: i32)".to_string(),
11451 documentation: None,
11452 parameters: Some(vec![lsp::ParameterInformation {
11453 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11454 documentation: None,
11455 }]),
11456 active_parameter: None,
11457 },
11458 lsp::SignatureInformation {
11459 label: "fn overloaded(x: i32, y: i32)".to_string(),
11460 documentation: None,
11461 parameters: Some(vec![
11462 lsp::ParameterInformation {
11463 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11464 documentation: None,
11465 },
11466 lsp::ParameterInformation {
11467 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11468 documentation: None,
11469 },
11470 ]),
11471 active_parameter: None,
11472 },
11473 lsp::SignatureInformation {
11474 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
11475 documentation: None,
11476 parameters: Some(vec![
11477 lsp::ParameterInformation {
11478 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11479 documentation: None,
11480 },
11481 lsp::ParameterInformation {
11482 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11483 documentation: None,
11484 },
11485 lsp::ParameterInformation {
11486 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
11487 documentation: None,
11488 },
11489 ]),
11490 active_parameter: None,
11491 },
11492 ],
11493 active_signature: Some(1),
11494 active_parameter: Some(0),
11495 };
11496 handle_signature_help_request(&mut cx, mocked_response).await;
11497
11498 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11499 .await;
11500
11501 // Verify we have multiple signatures and the right one is selected
11502 cx.editor(|editor, _, _| {
11503 let popover = editor.signature_help_state.popover().cloned().unwrap();
11504 assert_eq!(popover.signatures.len(), 3);
11505 // active_signature was 1, so that should be the current
11506 assert_eq!(popover.current_signature, 1);
11507 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
11508 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
11509 assert_eq!(
11510 popover.signatures[2].label,
11511 "fn overloaded(x: i32, y: i32, z: i32)"
11512 );
11513 });
11514
11515 // Test navigation functionality
11516 cx.update_editor(|editor, window, cx| {
11517 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11518 });
11519
11520 cx.editor(|editor, _, _| {
11521 let popover = editor.signature_help_state.popover().cloned().unwrap();
11522 assert_eq!(popover.current_signature, 2);
11523 });
11524
11525 // Test wrap around
11526 cx.update_editor(|editor, window, cx| {
11527 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11528 });
11529
11530 cx.editor(|editor, _, _| {
11531 let popover = editor.signature_help_state.popover().cloned().unwrap();
11532 assert_eq!(popover.current_signature, 0);
11533 });
11534
11535 // Test previous navigation
11536 cx.update_editor(|editor, window, cx| {
11537 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
11538 });
11539
11540 cx.editor(|editor, _, _| {
11541 let popover = editor.signature_help_state.popover().cloned().unwrap();
11542 assert_eq!(popover.current_signature, 2);
11543 });
11544}
11545
11546#[gpui::test]
11547async fn test_completion_mode(cx: &mut TestAppContext) {
11548 init_test(cx, |_| {});
11549 let mut cx = EditorLspTestContext::new_rust(
11550 lsp::ServerCapabilities {
11551 completion_provider: Some(lsp::CompletionOptions {
11552 resolve_provider: Some(true),
11553 ..Default::default()
11554 }),
11555 ..Default::default()
11556 },
11557 cx,
11558 )
11559 .await;
11560
11561 struct Run {
11562 run_description: &'static str,
11563 initial_state: String,
11564 buffer_marked_text: String,
11565 completion_label: &'static str,
11566 completion_text: &'static str,
11567 expected_with_insert_mode: String,
11568 expected_with_replace_mode: String,
11569 expected_with_replace_subsequence_mode: String,
11570 expected_with_replace_suffix_mode: String,
11571 }
11572
11573 let runs = [
11574 Run {
11575 run_description: "Start of word matches completion text",
11576 initial_state: "before ediˇ after".into(),
11577 buffer_marked_text: "before <edi|> after".into(),
11578 completion_label: "editor",
11579 completion_text: "editor",
11580 expected_with_insert_mode: "before editorˇ after".into(),
11581 expected_with_replace_mode: "before editorˇ after".into(),
11582 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11583 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11584 },
11585 Run {
11586 run_description: "Accept same text at the middle of the word",
11587 initial_state: "before ediˇtor after".into(),
11588 buffer_marked_text: "before <edi|tor> after".into(),
11589 completion_label: "editor",
11590 completion_text: "editor",
11591 expected_with_insert_mode: "before editorˇtor after".into(),
11592 expected_with_replace_mode: "before editorˇ after".into(),
11593 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11594 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11595 },
11596 Run {
11597 run_description: "End of word matches completion text -- cursor at end",
11598 initial_state: "before torˇ after".into(),
11599 buffer_marked_text: "before <tor|> after".into(),
11600 completion_label: "editor",
11601 completion_text: "editor",
11602 expected_with_insert_mode: "before editorˇ after".into(),
11603 expected_with_replace_mode: "before editorˇ after".into(),
11604 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11605 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11606 },
11607 Run {
11608 run_description: "End of word matches completion text -- cursor at start",
11609 initial_state: "before ˇtor after".into(),
11610 buffer_marked_text: "before <|tor> after".into(),
11611 completion_label: "editor",
11612 completion_text: "editor",
11613 expected_with_insert_mode: "before editorˇtor after".into(),
11614 expected_with_replace_mode: "before editorˇ after".into(),
11615 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11616 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11617 },
11618 Run {
11619 run_description: "Prepend text containing whitespace",
11620 initial_state: "pˇfield: bool".into(),
11621 buffer_marked_text: "<p|field>: bool".into(),
11622 completion_label: "pub ",
11623 completion_text: "pub ",
11624 expected_with_insert_mode: "pub ˇfield: bool".into(),
11625 expected_with_replace_mode: "pub ˇ: bool".into(),
11626 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
11627 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
11628 },
11629 Run {
11630 run_description: "Add element to start of list",
11631 initial_state: "[element_ˇelement_2]".into(),
11632 buffer_marked_text: "[<element_|element_2>]".into(),
11633 completion_label: "element_1",
11634 completion_text: "element_1",
11635 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
11636 expected_with_replace_mode: "[element_1ˇ]".into(),
11637 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
11638 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
11639 },
11640 Run {
11641 run_description: "Add element to start of list -- first and second elements are equal",
11642 initial_state: "[elˇelement]".into(),
11643 buffer_marked_text: "[<el|element>]".into(),
11644 completion_label: "element",
11645 completion_text: "element",
11646 expected_with_insert_mode: "[elementˇelement]".into(),
11647 expected_with_replace_mode: "[elementˇ]".into(),
11648 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
11649 expected_with_replace_suffix_mode: "[elementˇ]".into(),
11650 },
11651 Run {
11652 run_description: "Ends with matching suffix",
11653 initial_state: "SubˇError".into(),
11654 buffer_marked_text: "<Sub|Error>".into(),
11655 completion_label: "SubscriptionError",
11656 completion_text: "SubscriptionError",
11657 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
11658 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11659 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11660 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
11661 },
11662 Run {
11663 run_description: "Suffix is a subsequence -- contiguous",
11664 initial_state: "SubˇErr".into(),
11665 buffer_marked_text: "<Sub|Err>".into(),
11666 completion_label: "SubscriptionError",
11667 completion_text: "SubscriptionError",
11668 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
11669 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11670 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11671 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
11672 },
11673 Run {
11674 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
11675 initial_state: "Suˇscrirr".into(),
11676 buffer_marked_text: "<Su|scrirr>".into(),
11677 completion_label: "SubscriptionError",
11678 completion_text: "SubscriptionError",
11679 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
11680 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11681 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11682 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
11683 },
11684 Run {
11685 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
11686 initial_state: "foo(indˇix)".into(),
11687 buffer_marked_text: "foo(<ind|ix>)".into(),
11688 completion_label: "node_index",
11689 completion_text: "node_index",
11690 expected_with_insert_mode: "foo(node_indexˇix)".into(),
11691 expected_with_replace_mode: "foo(node_indexˇ)".into(),
11692 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
11693 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
11694 },
11695 Run {
11696 run_description: "Replace range ends before cursor - should extend to cursor",
11697 initial_state: "before editˇo after".into(),
11698 buffer_marked_text: "before <{ed}>it|o after".into(),
11699 completion_label: "editor",
11700 completion_text: "editor",
11701 expected_with_insert_mode: "before editorˇo after".into(),
11702 expected_with_replace_mode: "before editorˇo after".into(),
11703 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
11704 expected_with_replace_suffix_mode: "before editorˇo after".into(),
11705 },
11706 Run {
11707 run_description: "Uses label for suffix matching",
11708 initial_state: "before ediˇtor after".into(),
11709 buffer_marked_text: "before <edi|tor> after".into(),
11710 completion_label: "editor",
11711 completion_text: "editor()",
11712 expected_with_insert_mode: "before editor()ˇtor after".into(),
11713 expected_with_replace_mode: "before editor()ˇ after".into(),
11714 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
11715 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
11716 },
11717 Run {
11718 run_description: "Case insensitive subsequence and suffix matching",
11719 initial_state: "before EDiˇtoR after".into(),
11720 buffer_marked_text: "before <EDi|toR> after".into(),
11721 completion_label: "editor",
11722 completion_text: "editor",
11723 expected_with_insert_mode: "before editorˇtoR after".into(),
11724 expected_with_replace_mode: "before editorˇ after".into(),
11725 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11726 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11727 },
11728 ];
11729
11730 for run in runs {
11731 let run_variations = [
11732 (LspInsertMode::Insert, run.expected_with_insert_mode),
11733 (LspInsertMode::Replace, run.expected_with_replace_mode),
11734 (
11735 LspInsertMode::ReplaceSubsequence,
11736 run.expected_with_replace_subsequence_mode,
11737 ),
11738 (
11739 LspInsertMode::ReplaceSuffix,
11740 run.expected_with_replace_suffix_mode,
11741 ),
11742 ];
11743
11744 for (lsp_insert_mode, expected_text) in run_variations {
11745 eprintln!(
11746 "run = {:?}, mode = {lsp_insert_mode:.?}",
11747 run.run_description,
11748 );
11749
11750 update_test_language_settings(&mut cx, |settings| {
11751 settings.defaults.completions = Some(CompletionSettings {
11752 lsp_insert_mode,
11753 words: WordsCompletionMode::Disabled,
11754 lsp: true,
11755 lsp_fetch_timeout_ms: 0,
11756 });
11757 });
11758
11759 cx.set_state(&run.initial_state);
11760 cx.update_editor(|editor, window, cx| {
11761 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11762 });
11763
11764 let counter = Arc::new(AtomicUsize::new(0));
11765 handle_completion_request_with_insert_and_replace(
11766 &mut cx,
11767 &run.buffer_marked_text,
11768 vec![(run.completion_label, run.completion_text)],
11769 counter.clone(),
11770 )
11771 .await;
11772 cx.condition(|editor, _| editor.context_menu_visible())
11773 .await;
11774 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11775
11776 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11777 editor
11778 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11779 .unwrap()
11780 });
11781 cx.assert_editor_state(&expected_text);
11782 handle_resolve_completion_request(&mut cx, None).await;
11783 apply_additional_edits.await.unwrap();
11784 }
11785 }
11786}
11787
11788#[gpui::test]
11789async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
11790 init_test(cx, |_| {});
11791 let mut cx = EditorLspTestContext::new_rust(
11792 lsp::ServerCapabilities {
11793 completion_provider: Some(lsp::CompletionOptions {
11794 resolve_provider: Some(true),
11795 ..Default::default()
11796 }),
11797 ..Default::default()
11798 },
11799 cx,
11800 )
11801 .await;
11802
11803 let initial_state = "SubˇError";
11804 let buffer_marked_text = "<Sub|Error>";
11805 let completion_text = "SubscriptionError";
11806 let expected_with_insert_mode = "SubscriptionErrorˇError";
11807 let expected_with_replace_mode = "SubscriptionErrorˇ";
11808
11809 update_test_language_settings(&mut cx, |settings| {
11810 settings.defaults.completions = Some(CompletionSettings {
11811 words: WordsCompletionMode::Disabled,
11812 // set the opposite here to ensure that the action is overriding the default behavior
11813 lsp_insert_mode: LspInsertMode::Insert,
11814 lsp: true,
11815 lsp_fetch_timeout_ms: 0,
11816 });
11817 });
11818
11819 cx.set_state(initial_state);
11820 cx.update_editor(|editor, window, cx| {
11821 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11822 });
11823
11824 let counter = Arc::new(AtomicUsize::new(0));
11825 handle_completion_request_with_insert_and_replace(
11826 &mut cx,
11827 &buffer_marked_text,
11828 vec![(completion_text, completion_text)],
11829 counter.clone(),
11830 )
11831 .await;
11832 cx.condition(|editor, _| editor.context_menu_visible())
11833 .await;
11834 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11835
11836 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11837 editor
11838 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11839 .unwrap()
11840 });
11841 cx.assert_editor_state(&expected_with_replace_mode);
11842 handle_resolve_completion_request(&mut cx, None).await;
11843 apply_additional_edits.await.unwrap();
11844
11845 update_test_language_settings(&mut cx, |settings| {
11846 settings.defaults.completions = Some(CompletionSettings {
11847 words: WordsCompletionMode::Disabled,
11848 // set the opposite here to ensure that the action is overriding the default behavior
11849 lsp_insert_mode: LspInsertMode::Replace,
11850 lsp: true,
11851 lsp_fetch_timeout_ms: 0,
11852 });
11853 });
11854
11855 cx.set_state(initial_state);
11856 cx.update_editor(|editor, window, cx| {
11857 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11858 });
11859 handle_completion_request_with_insert_and_replace(
11860 &mut cx,
11861 &buffer_marked_text,
11862 vec![(completion_text, completion_text)],
11863 counter.clone(),
11864 )
11865 .await;
11866 cx.condition(|editor, _| editor.context_menu_visible())
11867 .await;
11868 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11869
11870 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11871 editor
11872 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
11873 .unwrap()
11874 });
11875 cx.assert_editor_state(&expected_with_insert_mode);
11876 handle_resolve_completion_request(&mut cx, None).await;
11877 apply_additional_edits.await.unwrap();
11878}
11879
11880#[gpui::test]
11881async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
11882 init_test(cx, |_| {});
11883 let mut cx = EditorLspTestContext::new_rust(
11884 lsp::ServerCapabilities {
11885 completion_provider: Some(lsp::CompletionOptions {
11886 resolve_provider: Some(true),
11887 ..Default::default()
11888 }),
11889 ..Default::default()
11890 },
11891 cx,
11892 )
11893 .await;
11894
11895 // scenario: surrounding text matches completion text
11896 let completion_text = "to_offset";
11897 let initial_state = indoc! {"
11898 1. buf.to_offˇsuffix
11899 2. buf.to_offˇsuf
11900 3. buf.to_offˇfix
11901 4. buf.to_offˇ
11902 5. into_offˇensive
11903 6. ˇsuffix
11904 7. let ˇ //
11905 8. aaˇzz
11906 9. buf.to_off«zzzzzˇ»suffix
11907 10. buf.«ˇzzzzz»suffix
11908 11. to_off«ˇzzzzz»
11909
11910 buf.to_offˇsuffix // newest cursor
11911 "};
11912 let completion_marked_buffer = indoc! {"
11913 1. buf.to_offsuffix
11914 2. buf.to_offsuf
11915 3. buf.to_offfix
11916 4. buf.to_off
11917 5. into_offensive
11918 6. suffix
11919 7. let //
11920 8. aazz
11921 9. buf.to_offzzzzzsuffix
11922 10. buf.zzzzzsuffix
11923 11. to_offzzzzz
11924
11925 buf.<to_off|suffix> // newest cursor
11926 "};
11927 let expected = indoc! {"
11928 1. buf.to_offsetˇ
11929 2. buf.to_offsetˇsuf
11930 3. buf.to_offsetˇfix
11931 4. buf.to_offsetˇ
11932 5. into_offsetˇensive
11933 6. to_offsetˇsuffix
11934 7. let to_offsetˇ //
11935 8. aato_offsetˇzz
11936 9. buf.to_offsetˇ
11937 10. buf.to_offsetˇsuffix
11938 11. to_offsetˇ
11939
11940 buf.to_offsetˇ // newest cursor
11941 "};
11942 cx.set_state(initial_state);
11943 cx.update_editor(|editor, window, cx| {
11944 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11945 });
11946 handle_completion_request_with_insert_and_replace(
11947 &mut cx,
11948 completion_marked_buffer,
11949 vec![(completion_text, completion_text)],
11950 Arc::new(AtomicUsize::new(0)),
11951 )
11952 .await;
11953 cx.condition(|editor, _| editor.context_menu_visible())
11954 .await;
11955 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11956 editor
11957 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11958 .unwrap()
11959 });
11960 cx.assert_editor_state(expected);
11961 handle_resolve_completion_request(&mut cx, None).await;
11962 apply_additional_edits.await.unwrap();
11963
11964 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
11965 let completion_text = "foo_and_bar";
11966 let initial_state = indoc! {"
11967 1. ooanbˇ
11968 2. zooanbˇ
11969 3. ooanbˇz
11970 4. zooanbˇz
11971 5. ooanˇ
11972 6. oanbˇ
11973
11974 ooanbˇ
11975 "};
11976 let completion_marked_buffer = indoc! {"
11977 1. ooanb
11978 2. zooanb
11979 3. ooanbz
11980 4. zooanbz
11981 5. ooan
11982 6. oanb
11983
11984 <ooanb|>
11985 "};
11986 let expected = indoc! {"
11987 1. foo_and_barˇ
11988 2. zfoo_and_barˇ
11989 3. foo_and_barˇz
11990 4. zfoo_and_barˇz
11991 5. ooanfoo_and_barˇ
11992 6. oanbfoo_and_barˇ
11993
11994 foo_and_barˇ
11995 "};
11996 cx.set_state(initial_state);
11997 cx.update_editor(|editor, window, cx| {
11998 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11999 });
12000 handle_completion_request_with_insert_and_replace(
12001 &mut cx,
12002 completion_marked_buffer,
12003 vec![(completion_text, completion_text)],
12004 Arc::new(AtomicUsize::new(0)),
12005 )
12006 .await;
12007 cx.condition(|editor, _| editor.context_menu_visible())
12008 .await;
12009 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12010 editor
12011 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12012 .unwrap()
12013 });
12014 cx.assert_editor_state(expected);
12015 handle_resolve_completion_request(&mut cx, None).await;
12016 apply_additional_edits.await.unwrap();
12017
12018 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
12019 // (expects the same as if it was inserted at the end)
12020 let completion_text = "foo_and_bar";
12021 let initial_state = indoc! {"
12022 1. ooˇanb
12023 2. zooˇanb
12024 3. ooˇanbz
12025 4. zooˇanbz
12026
12027 ooˇanb
12028 "};
12029 let completion_marked_buffer = indoc! {"
12030 1. ooanb
12031 2. zooanb
12032 3. ooanbz
12033 4. zooanbz
12034
12035 <oo|anb>
12036 "};
12037 let expected = indoc! {"
12038 1. foo_and_barˇ
12039 2. zfoo_and_barˇ
12040 3. foo_and_barˇz
12041 4. zfoo_and_barˇz
12042
12043 foo_and_barˇ
12044 "};
12045 cx.set_state(initial_state);
12046 cx.update_editor(|editor, window, cx| {
12047 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12048 });
12049 handle_completion_request_with_insert_and_replace(
12050 &mut cx,
12051 completion_marked_buffer,
12052 vec![(completion_text, completion_text)],
12053 Arc::new(AtomicUsize::new(0)),
12054 )
12055 .await;
12056 cx.condition(|editor, _| editor.context_menu_visible())
12057 .await;
12058 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12059 editor
12060 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12061 .unwrap()
12062 });
12063 cx.assert_editor_state(expected);
12064 handle_resolve_completion_request(&mut cx, None).await;
12065 apply_additional_edits.await.unwrap();
12066}
12067
12068// This used to crash
12069#[gpui::test]
12070async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
12071 init_test(cx, |_| {});
12072
12073 let buffer_text = indoc! {"
12074 fn main() {
12075 10.satu;
12076
12077 //
12078 // separate cursors so they open in different excerpts (manually reproducible)
12079 //
12080
12081 10.satu20;
12082 }
12083 "};
12084 let multibuffer_text_with_selections = indoc! {"
12085 fn main() {
12086 10.satuˇ;
12087
12088 //
12089
12090 //
12091
12092 10.satuˇ20;
12093 }
12094 "};
12095 let expected_multibuffer = indoc! {"
12096 fn main() {
12097 10.saturating_sub()ˇ;
12098
12099 //
12100
12101 //
12102
12103 10.saturating_sub()ˇ;
12104 }
12105 "};
12106
12107 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
12108 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
12109
12110 let fs = FakeFs::new(cx.executor());
12111 fs.insert_tree(
12112 path!("/a"),
12113 json!({
12114 "main.rs": buffer_text,
12115 }),
12116 )
12117 .await;
12118
12119 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12120 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12121 language_registry.add(rust_lang());
12122 let mut fake_servers = language_registry.register_fake_lsp(
12123 "Rust",
12124 FakeLspAdapter {
12125 capabilities: lsp::ServerCapabilities {
12126 completion_provider: Some(lsp::CompletionOptions {
12127 resolve_provider: None,
12128 ..lsp::CompletionOptions::default()
12129 }),
12130 ..lsp::ServerCapabilities::default()
12131 },
12132 ..FakeLspAdapter::default()
12133 },
12134 );
12135 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12136 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12137 let buffer = project
12138 .update(cx, |project, cx| {
12139 project.open_local_buffer(path!("/a/main.rs"), cx)
12140 })
12141 .await
12142 .unwrap();
12143
12144 let multi_buffer = cx.new(|cx| {
12145 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
12146 multi_buffer.push_excerpts(
12147 buffer.clone(),
12148 [ExcerptRange::new(0..first_excerpt_end)],
12149 cx,
12150 );
12151 multi_buffer.push_excerpts(
12152 buffer.clone(),
12153 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
12154 cx,
12155 );
12156 multi_buffer
12157 });
12158
12159 let editor = workspace
12160 .update(cx, |_, window, cx| {
12161 cx.new(|cx| {
12162 Editor::new(
12163 EditorMode::Full {
12164 scale_ui_elements_with_buffer_font_size: false,
12165 show_active_line_background: false,
12166 sized_by_content: false,
12167 },
12168 multi_buffer.clone(),
12169 Some(project.clone()),
12170 window,
12171 cx,
12172 )
12173 })
12174 })
12175 .unwrap();
12176
12177 let pane = workspace
12178 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12179 .unwrap();
12180 pane.update_in(cx, |pane, window, cx| {
12181 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
12182 });
12183
12184 let fake_server = fake_servers.next().await.unwrap();
12185
12186 editor.update_in(cx, |editor, window, cx| {
12187 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12188 s.select_ranges([
12189 Point::new(1, 11)..Point::new(1, 11),
12190 Point::new(7, 11)..Point::new(7, 11),
12191 ])
12192 });
12193
12194 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
12195 });
12196
12197 editor.update_in(cx, |editor, window, cx| {
12198 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12199 });
12200
12201 fake_server
12202 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12203 let completion_item = lsp::CompletionItem {
12204 label: "saturating_sub()".into(),
12205 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
12206 lsp::InsertReplaceEdit {
12207 new_text: "saturating_sub()".to_owned(),
12208 insert: lsp::Range::new(
12209 lsp::Position::new(7, 7),
12210 lsp::Position::new(7, 11),
12211 ),
12212 replace: lsp::Range::new(
12213 lsp::Position::new(7, 7),
12214 lsp::Position::new(7, 13),
12215 ),
12216 },
12217 )),
12218 ..lsp::CompletionItem::default()
12219 };
12220
12221 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
12222 })
12223 .next()
12224 .await
12225 .unwrap();
12226
12227 cx.condition(&editor, |editor, _| editor.context_menu_visible())
12228 .await;
12229
12230 editor
12231 .update_in(cx, |editor, window, cx| {
12232 editor
12233 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12234 .unwrap()
12235 })
12236 .await
12237 .unwrap();
12238
12239 editor.update(cx, |editor, cx| {
12240 assert_text_with_selections(editor, expected_multibuffer, cx);
12241 })
12242}
12243
12244#[gpui::test]
12245async fn test_completion(cx: &mut TestAppContext) {
12246 init_test(cx, |_| {});
12247
12248 let mut cx = EditorLspTestContext::new_rust(
12249 lsp::ServerCapabilities {
12250 completion_provider: Some(lsp::CompletionOptions {
12251 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12252 resolve_provider: Some(true),
12253 ..Default::default()
12254 }),
12255 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12256 ..Default::default()
12257 },
12258 cx,
12259 )
12260 .await;
12261 let counter = Arc::new(AtomicUsize::new(0));
12262
12263 cx.set_state(indoc! {"
12264 oneˇ
12265 two
12266 three
12267 "});
12268 cx.simulate_keystroke(".");
12269 handle_completion_request(
12270 indoc! {"
12271 one.|<>
12272 two
12273 three
12274 "},
12275 vec!["first_completion", "second_completion"],
12276 true,
12277 counter.clone(),
12278 &mut cx,
12279 )
12280 .await;
12281 cx.condition(|editor, _| editor.context_menu_visible())
12282 .await;
12283 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12284
12285 let _handler = handle_signature_help_request(
12286 &mut cx,
12287 lsp::SignatureHelp {
12288 signatures: vec![lsp::SignatureInformation {
12289 label: "test signature".to_string(),
12290 documentation: None,
12291 parameters: Some(vec![lsp::ParameterInformation {
12292 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
12293 documentation: None,
12294 }]),
12295 active_parameter: None,
12296 }],
12297 active_signature: None,
12298 active_parameter: None,
12299 },
12300 );
12301 cx.update_editor(|editor, window, cx| {
12302 assert!(
12303 !editor.signature_help_state.is_shown(),
12304 "No signature help was called for"
12305 );
12306 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12307 });
12308 cx.run_until_parked();
12309 cx.update_editor(|editor, _, _| {
12310 assert!(
12311 !editor.signature_help_state.is_shown(),
12312 "No signature help should be shown when completions menu is open"
12313 );
12314 });
12315
12316 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12317 editor.context_menu_next(&Default::default(), window, cx);
12318 editor
12319 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12320 .unwrap()
12321 });
12322 cx.assert_editor_state(indoc! {"
12323 one.second_completionˇ
12324 two
12325 three
12326 "});
12327
12328 handle_resolve_completion_request(
12329 &mut cx,
12330 Some(vec![
12331 (
12332 //This overlaps with the primary completion edit which is
12333 //misbehavior from the LSP spec, test that we filter it out
12334 indoc! {"
12335 one.second_ˇcompletion
12336 two
12337 threeˇ
12338 "},
12339 "overlapping additional edit",
12340 ),
12341 (
12342 indoc! {"
12343 one.second_completion
12344 two
12345 threeˇ
12346 "},
12347 "\nadditional edit",
12348 ),
12349 ]),
12350 )
12351 .await;
12352 apply_additional_edits.await.unwrap();
12353 cx.assert_editor_state(indoc! {"
12354 one.second_completionˇ
12355 two
12356 three
12357 additional edit
12358 "});
12359
12360 cx.set_state(indoc! {"
12361 one.second_completion
12362 twoˇ
12363 threeˇ
12364 additional edit
12365 "});
12366 cx.simulate_keystroke(" ");
12367 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12368 cx.simulate_keystroke("s");
12369 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12370
12371 cx.assert_editor_state(indoc! {"
12372 one.second_completion
12373 two sˇ
12374 three sˇ
12375 additional edit
12376 "});
12377 handle_completion_request(
12378 indoc! {"
12379 one.second_completion
12380 two s
12381 three <s|>
12382 additional edit
12383 "},
12384 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12385 true,
12386 counter.clone(),
12387 &mut cx,
12388 )
12389 .await;
12390 cx.condition(|editor, _| editor.context_menu_visible())
12391 .await;
12392 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12393
12394 cx.simulate_keystroke("i");
12395
12396 handle_completion_request(
12397 indoc! {"
12398 one.second_completion
12399 two si
12400 three <si|>
12401 additional edit
12402 "},
12403 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12404 true,
12405 counter.clone(),
12406 &mut cx,
12407 )
12408 .await;
12409 cx.condition(|editor, _| editor.context_menu_visible())
12410 .await;
12411 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12412
12413 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12414 editor
12415 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12416 .unwrap()
12417 });
12418 cx.assert_editor_state(indoc! {"
12419 one.second_completion
12420 two sixth_completionˇ
12421 three sixth_completionˇ
12422 additional edit
12423 "});
12424
12425 apply_additional_edits.await.unwrap();
12426
12427 update_test_language_settings(&mut cx, |settings| {
12428 settings.defaults.show_completions_on_input = Some(false);
12429 });
12430 cx.set_state("editorˇ");
12431 cx.simulate_keystroke(".");
12432 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12433 cx.simulate_keystrokes("c l o");
12434 cx.assert_editor_state("editor.cloˇ");
12435 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12436 cx.update_editor(|editor, window, cx| {
12437 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12438 });
12439 handle_completion_request(
12440 "editor.<clo|>",
12441 vec!["close", "clobber"],
12442 true,
12443 counter.clone(),
12444 &mut cx,
12445 )
12446 .await;
12447 cx.condition(|editor, _| editor.context_menu_visible())
12448 .await;
12449 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
12450
12451 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12452 editor
12453 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12454 .unwrap()
12455 });
12456 cx.assert_editor_state("editor.clobberˇ");
12457 handle_resolve_completion_request(&mut cx, None).await;
12458 apply_additional_edits.await.unwrap();
12459}
12460
12461#[gpui::test]
12462async fn test_completion_reuse(cx: &mut TestAppContext) {
12463 init_test(cx, |_| {});
12464
12465 let mut cx = EditorLspTestContext::new_rust(
12466 lsp::ServerCapabilities {
12467 completion_provider: Some(lsp::CompletionOptions {
12468 trigger_characters: Some(vec![".".to_string()]),
12469 ..Default::default()
12470 }),
12471 ..Default::default()
12472 },
12473 cx,
12474 )
12475 .await;
12476
12477 let counter = Arc::new(AtomicUsize::new(0));
12478 cx.set_state("objˇ");
12479 cx.simulate_keystroke(".");
12480
12481 // Initial completion request returns complete results
12482 let is_incomplete = false;
12483 handle_completion_request(
12484 "obj.|<>",
12485 vec!["a", "ab", "abc"],
12486 is_incomplete,
12487 counter.clone(),
12488 &mut cx,
12489 )
12490 .await;
12491 cx.run_until_parked();
12492 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12493 cx.assert_editor_state("obj.ˇ");
12494 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12495
12496 // Type "a" - filters existing completions
12497 cx.simulate_keystroke("a");
12498 cx.run_until_parked();
12499 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12500 cx.assert_editor_state("obj.aˇ");
12501 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12502
12503 // Type "b" - filters existing completions
12504 cx.simulate_keystroke("b");
12505 cx.run_until_parked();
12506 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12507 cx.assert_editor_state("obj.abˇ");
12508 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12509
12510 // Type "c" - filters existing completions
12511 cx.simulate_keystroke("c");
12512 cx.run_until_parked();
12513 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12514 cx.assert_editor_state("obj.abcˇ");
12515 check_displayed_completions(vec!["abc"], &mut cx);
12516
12517 // Backspace to delete "c" - filters existing completions
12518 cx.update_editor(|editor, window, cx| {
12519 editor.backspace(&Backspace, window, cx);
12520 });
12521 cx.run_until_parked();
12522 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12523 cx.assert_editor_state("obj.abˇ");
12524 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12525
12526 // Moving cursor to the left dismisses menu.
12527 cx.update_editor(|editor, window, cx| {
12528 editor.move_left(&MoveLeft, window, cx);
12529 });
12530 cx.run_until_parked();
12531 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12532 cx.assert_editor_state("obj.aˇb");
12533 cx.update_editor(|editor, _, _| {
12534 assert_eq!(editor.context_menu_visible(), false);
12535 });
12536
12537 // Type "b" - new request
12538 cx.simulate_keystroke("b");
12539 let is_incomplete = false;
12540 handle_completion_request(
12541 "obj.<ab|>a",
12542 vec!["ab", "abc"],
12543 is_incomplete,
12544 counter.clone(),
12545 &mut cx,
12546 )
12547 .await;
12548 cx.run_until_parked();
12549 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12550 cx.assert_editor_state("obj.abˇb");
12551 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12552
12553 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
12554 cx.update_editor(|editor, window, cx| {
12555 editor.backspace(&Backspace, window, cx);
12556 });
12557 let is_incomplete = false;
12558 handle_completion_request(
12559 "obj.<a|>b",
12560 vec!["a", "ab", "abc"],
12561 is_incomplete,
12562 counter.clone(),
12563 &mut cx,
12564 )
12565 .await;
12566 cx.run_until_parked();
12567 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12568 cx.assert_editor_state("obj.aˇb");
12569 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12570
12571 // Backspace to delete "a" - dismisses menu.
12572 cx.update_editor(|editor, window, cx| {
12573 editor.backspace(&Backspace, window, cx);
12574 });
12575 cx.run_until_parked();
12576 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12577 cx.assert_editor_state("obj.ˇb");
12578 cx.update_editor(|editor, _, _| {
12579 assert_eq!(editor.context_menu_visible(), false);
12580 });
12581}
12582
12583#[gpui::test]
12584async fn test_word_completion(cx: &mut TestAppContext) {
12585 let lsp_fetch_timeout_ms = 10;
12586 init_test(cx, |language_settings| {
12587 language_settings.defaults.completions = Some(CompletionSettings {
12588 words: WordsCompletionMode::Fallback,
12589 lsp: true,
12590 lsp_fetch_timeout_ms: 10,
12591 lsp_insert_mode: LspInsertMode::Insert,
12592 });
12593 });
12594
12595 let mut cx = EditorLspTestContext::new_rust(
12596 lsp::ServerCapabilities {
12597 completion_provider: Some(lsp::CompletionOptions {
12598 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12599 ..lsp::CompletionOptions::default()
12600 }),
12601 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12602 ..lsp::ServerCapabilities::default()
12603 },
12604 cx,
12605 )
12606 .await;
12607
12608 let throttle_completions = Arc::new(AtomicBool::new(false));
12609
12610 let lsp_throttle_completions = throttle_completions.clone();
12611 let _completion_requests_handler =
12612 cx.lsp
12613 .server
12614 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
12615 let lsp_throttle_completions = lsp_throttle_completions.clone();
12616 let cx = cx.clone();
12617 async move {
12618 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
12619 cx.background_executor()
12620 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
12621 .await;
12622 }
12623 Ok(Some(lsp::CompletionResponse::Array(vec![
12624 lsp::CompletionItem {
12625 label: "first".into(),
12626 ..lsp::CompletionItem::default()
12627 },
12628 lsp::CompletionItem {
12629 label: "last".into(),
12630 ..lsp::CompletionItem::default()
12631 },
12632 ])))
12633 }
12634 });
12635
12636 cx.set_state(indoc! {"
12637 oneˇ
12638 two
12639 three
12640 "});
12641 cx.simulate_keystroke(".");
12642 cx.executor().run_until_parked();
12643 cx.condition(|editor, _| editor.context_menu_visible())
12644 .await;
12645 cx.update_editor(|editor, window, cx| {
12646 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12647 {
12648 assert_eq!(
12649 completion_menu_entries(&menu),
12650 &["first", "last"],
12651 "When LSP server is fast to reply, no fallback word completions are used"
12652 );
12653 } else {
12654 panic!("expected completion menu to be open");
12655 }
12656 editor.cancel(&Cancel, window, cx);
12657 });
12658 cx.executor().run_until_parked();
12659 cx.condition(|editor, _| !editor.context_menu_visible())
12660 .await;
12661
12662 throttle_completions.store(true, atomic::Ordering::Release);
12663 cx.simulate_keystroke(".");
12664 cx.executor()
12665 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
12666 cx.executor().run_until_parked();
12667 cx.condition(|editor, _| editor.context_menu_visible())
12668 .await;
12669 cx.update_editor(|editor, _, _| {
12670 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12671 {
12672 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
12673 "When LSP server is slow, document words can be shown instead, if configured accordingly");
12674 } else {
12675 panic!("expected completion menu to be open");
12676 }
12677 });
12678}
12679
12680#[gpui::test]
12681async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
12682 init_test(cx, |language_settings| {
12683 language_settings.defaults.completions = Some(CompletionSettings {
12684 words: WordsCompletionMode::Enabled,
12685 lsp: true,
12686 lsp_fetch_timeout_ms: 0,
12687 lsp_insert_mode: LspInsertMode::Insert,
12688 });
12689 });
12690
12691 let mut cx = EditorLspTestContext::new_rust(
12692 lsp::ServerCapabilities {
12693 completion_provider: Some(lsp::CompletionOptions {
12694 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12695 ..lsp::CompletionOptions::default()
12696 }),
12697 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12698 ..lsp::ServerCapabilities::default()
12699 },
12700 cx,
12701 )
12702 .await;
12703
12704 let _completion_requests_handler =
12705 cx.lsp
12706 .server
12707 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12708 Ok(Some(lsp::CompletionResponse::Array(vec![
12709 lsp::CompletionItem {
12710 label: "first".into(),
12711 ..lsp::CompletionItem::default()
12712 },
12713 lsp::CompletionItem {
12714 label: "last".into(),
12715 ..lsp::CompletionItem::default()
12716 },
12717 ])))
12718 });
12719
12720 cx.set_state(indoc! {"ˇ
12721 first
12722 last
12723 second
12724 "});
12725 cx.simulate_keystroke(".");
12726 cx.executor().run_until_parked();
12727 cx.condition(|editor, _| editor.context_menu_visible())
12728 .await;
12729 cx.update_editor(|editor, _, _| {
12730 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12731 {
12732 assert_eq!(
12733 completion_menu_entries(&menu),
12734 &["first", "last", "second"],
12735 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
12736 );
12737 } else {
12738 panic!("expected completion menu to be open");
12739 }
12740 });
12741}
12742
12743#[gpui::test]
12744async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
12745 init_test(cx, |language_settings| {
12746 language_settings.defaults.completions = Some(CompletionSettings {
12747 words: WordsCompletionMode::Disabled,
12748 lsp: true,
12749 lsp_fetch_timeout_ms: 0,
12750 lsp_insert_mode: LspInsertMode::Insert,
12751 });
12752 });
12753
12754 let mut cx = EditorLspTestContext::new_rust(
12755 lsp::ServerCapabilities {
12756 completion_provider: Some(lsp::CompletionOptions {
12757 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12758 ..lsp::CompletionOptions::default()
12759 }),
12760 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12761 ..lsp::ServerCapabilities::default()
12762 },
12763 cx,
12764 )
12765 .await;
12766
12767 let _completion_requests_handler =
12768 cx.lsp
12769 .server
12770 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12771 panic!("LSP completions should not be queried when dealing with word completions")
12772 });
12773
12774 cx.set_state(indoc! {"ˇ
12775 first
12776 last
12777 second
12778 "});
12779 cx.update_editor(|editor, window, cx| {
12780 editor.show_word_completions(&ShowWordCompletions, window, cx);
12781 });
12782 cx.executor().run_until_parked();
12783 cx.condition(|editor, _| editor.context_menu_visible())
12784 .await;
12785 cx.update_editor(|editor, _, _| {
12786 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12787 {
12788 assert_eq!(
12789 completion_menu_entries(&menu),
12790 &["first", "last", "second"],
12791 "`ShowWordCompletions` action should show word completions"
12792 );
12793 } else {
12794 panic!("expected completion menu to be open");
12795 }
12796 });
12797
12798 cx.simulate_keystroke("l");
12799 cx.executor().run_until_parked();
12800 cx.condition(|editor, _| editor.context_menu_visible())
12801 .await;
12802 cx.update_editor(|editor, _, _| {
12803 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12804 {
12805 assert_eq!(
12806 completion_menu_entries(&menu),
12807 &["last"],
12808 "After showing word completions, further editing should filter them and not query the LSP"
12809 );
12810 } else {
12811 panic!("expected completion menu to be open");
12812 }
12813 });
12814}
12815
12816#[gpui::test]
12817async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
12818 init_test(cx, |language_settings| {
12819 language_settings.defaults.completions = Some(CompletionSettings {
12820 words: WordsCompletionMode::Fallback,
12821 lsp: false,
12822 lsp_fetch_timeout_ms: 0,
12823 lsp_insert_mode: LspInsertMode::Insert,
12824 });
12825 });
12826
12827 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12828
12829 cx.set_state(indoc! {"ˇ
12830 0_usize
12831 let
12832 33
12833 4.5f32
12834 "});
12835 cx.update_editor(|editor, window, cx| {
12836 editor.show_completions(&ShowCompletions::default(), window, cx);
12837 });
12838 cx.executor().run_until_parked();
12839 cx.condition(|editor, _| editor.context_menu_visible())
12840 .await;
12841 cx.update_editor(|editor, window, cx| {
12842 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12843 {
12844 assert_eq!(
12845 completion_menu_entries(&menu),
12846 &["let"],
12847 "With no digits in the completion query, no digits should be in the word completions"
12848 );
12849 } else {
12850 panic!("expected completion menu to be open");
12851 }
12852 editor.cancel(&Cancel, window, cx);
12853 });
12854
12855 cx.set_state(indoc! {"3ˇ
12856 0_usize
12857 let
12858 3
12859 33.35f32
12860 "});
12861 cx.update_editor(|editor, window, cx| {
12862 editor.show_completions(&ShowCompletions::default(), window, cx);
12863 });
12864 cx.executor().run_until_parked();
12865 cx.condition(|editor, _| editor.context_menu_visible())
12866 .await;
12867 cx.update_editor(|editor, _, _| {
12868 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12869 {
12870 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
12871 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
12872 } else {
12873 panic!("expected completion menu to be open");
12874 }
12875 });
12876}
12877
12878fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
12879 let position = || lsp::Position {
12880 line: params.text_document_position.position.line,
12881 character: params.text_document_position.position.character,
12882 };
12883 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12884 range: lsp::Range {
12885 start: position(),
12886 end: position(),
12887 },
12888 new_text: text.to_string(),
12889 }))
12890}
12891
12892#[gpui::test]
12893async fn test_multiline_completion(cx: &mut TestAppContext) {
12894 init_test(cx, |_| {});
12895
12896 let fs = FakeFs::new(cx.executor());
12897 fs.insert_tree(
12898 path!("/a"),
12899 json!({
12900 "main.ts": "a",
12901 }),
12902 )
12903 .await;
12904
12905 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12906 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12907 let typescript_language = Arc::new(Language::new(
12908 LanguageConfig {
12909 name: "TypeScript".into(),
12910 matcher: LanguageMatcher {
12911 path_suffixes: vec!["ts".to_string()],
12912 ..LanguageMatcher::default()
12913 },
12914 line_comments: vec!["// ".into()],
12915 ..LanguageConfig::default()
12916 },
12917 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12918 ));
12919 language_registry.add(typescript_language.clone());
12920 let mut fake_servers = language_registry.register_fake_lsp(
12921 "TypeScript",
12922 FakeLspAdapter {
12923 capabilities: lsp::ServerCapabilities {
12924 completion_provider: Some(lsp::CompletionOptions {
12925 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12926 ..lsp::CompletionOptions::default()
12927 }),
12928 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12929 ..lsp::ServerCapabilities::default()
12930 },
12931 // Emulate vtsls label generation
12932 label_for_completion: Some(Box::new(|item, _| {
12933 let text = if let Some(description) = item
12934 .label_details
12935 .as_ref()
12936 .and_then(|label_details| label_details.description.as_ref())
12937 {
12938 format!("{} {}", item.label, description)
12939 } else if let Some(detail) = &item.detail {
12940 format!("{} {}", item.label, detail)
12941 } else {
12942 item.label.clone()
12943 };
12944 let len = text.len();
12945 Some(language::CodeLabel {
12946 text,
12947 runs: Vec::new(),
12948 filter_range: 0..len,
12949 })
12950 })),
12951 ..FakeLspAdapter::default()
12952 },
12953 );
12954 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12955 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12956 let worktree_id = workspace
12957 .update(cx, |workspace, _window, cx| {
12958 workspace.project().update(cx, |project, cx| {
12959 project.worktrees(cx).next().unwrap().read(cx).id()
12960 })
12961 })
12962 .unwrap();
12963 let _buffer = project
12964 .update(cx, |project, cx| {
12965 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
12966 })
12967 .await
12968 .unwrap();
12969 let editor = workspace
12970 .update(cx, |workspace, window, cx| {
12971 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
12972 })
12973 .unwrap()
12974 .await
12975 .unwrap()
12976 .downcast::<Editor>()
12977 .unwrap();
12978 let fake_server = fake_servers.next().await.unwrap();
12979
12980 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
12981 let multiline_label_2 = "a\nb\nc\n";
12982 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
12983 let multiline_description = "d\ne\nf\n";
12984 let multiline_detail_2 = "g\nh\ni\n";
12985
12986 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
12987 move |params, _| async move {
12988 Ok(Some(lsp::CompletionResponse::Array(vec![
12989 lsp::CompletionItem {
12990 label: multiline_label.to_string(),
12991 text_edit: gen_text_edit(¶ms, "new_text_1"),
12992 ..lsp::CompletionItem::default()
12993 },
12994 lsp::CompletionItem {
12995 label: "single line label 1".to_string(),
12996 detail: Some(multiline_detail.to_string()),
12997 text_edit: gen_text_edit(¶ms, "new_text_2"),
12998 ..lsp::CompletionItem::default()
12999 },
13000 lsp::CompletionItem {
13001 label: "single line label 2".to_string(),
13002 label_details: Some(lsp::CompletionItemLabelDetails {
13003 description: Some(multiline_description.to_string()),
13004 detail: None,
13005 }),
13006 text_edit: gen_text_edit(¶ms, "new_text_2"),
13007 ..lsp::CompletionItem::default()
13008 },
13009 lsp::CompletionItem {
13010 label: multiline_label_2.to_string(),
13011 detail: Some(multiline_detail_2.to_string()),
13012 text_edit: gen_text_edit(¶ms, "new_text_3"),
13013 ..lsp::CompletionItem::default()
13014 },
13015 lsp::CompletionItem {
13016 label: "Label with many spaces and \t but without newlines".to_string(),
13017 detail: Some(
13018 "Details with many spaces and \t but without newlines".to_string(),
13019 ),
13020 text_edit: gen_text_edit(¶ms, "new_text_4"),
13021 ..lsp::CompletionItem::default()
13022 },
13023 ])))
13024 },
13025 );
13026
13027 editor.update_in(cx, |editor, window, cx| {
13028 cx.focus_self(window);
13029 editor.move_to_end(&MoveToEnd, window, cx);
13030 editor.handle_input(".", window, cx);
13031 });
13032 cx.run_until_parked();
13033 completion_handle.next().await.unwrap();
13034
13035 editor.update(cx, |editor, _| {
13036 assert!(editor.context_menu_visible());
13037 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13038 {
13039 let completion_labels = menu
13040 .completions
13041 .borrow()
13042 .iter()
13043 .map(|c| c.label.text.clone())
13044 .collect::<Vec<_>>();
13045 assert_eq!(
13046 completion_labels,
13047 &[
13048 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
13049 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
13050 "single line label 2 d e f ",
13051 "a b c g h i ",
13052 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
13053 ],
13054 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
13055 );
13056
13057 for completion in menu
13058 .completions
13059 .borrow()
13060 .iter() {
13061 assert_eq!(
13062 completion.label.filter_range,
13063 0..completion.label.text.len(),
13064 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
13065 );
13066 }
13067 } else {
13068 panic!("expected completion menu to be open");
13069 }
13070 });
13071}
13072
13073#[gpui::test]
13074async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
13075 init_test(cx, |_| {});
13076 let mut cx = EditorLspTestContext::new_rust(
13077 lsp::ServerCapabilities {
13078 completion_provider: Some(lsp::CompletionOptions {
13079 trigger_characters: Some(vec![".".to_string()]),
13080 ..Default::default()
13081 }),
13082 ..Default::default()
13083 },
13084 cx,
13085 )
13086 .await;
13087 cx.lsp
13088 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13089 Ok(Some(lsp::CompletionResponse::Array(vec![
13090 lsp::CompletionItem {
13091 label: "first".into(),
13092 ..Default::default()
13093 },
13094 lsp::CompletionItem {
13095 label: "last".into(),
13096 ..Default::default()
13097 },
13098 ])))
13099 });
13100 cx.set_state("variableˇ");
13101 cx.simulate_keystroke(".");
13102 cx.executor().run_until_parked();
13103
13104 cx.update_editor(|editor, _, _| {
13105 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13106 {
13107 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
13108 } else {
13109 panic!("expected completion menu to be open");
13110 }
13111 });
13112
13113 cx.update_editor(|editor, window, cx| {
13114 editor.move_page_down(&MovePageDown::default(), window, cx);
13115 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13116 {
13117 assert!(
13118 menu.selected_item == 1,
13119 "expected PageDown to select the last item from the context menu"
13120 );
13121 } else {
13122 panic!("expected completion menu to stay open after PageDown");
13123 }
13124 });
13125
13126 cx.update_editor(|editor, window, cx| {
13127 editor.move_page_up(&MovePageUp::default(), window, cx);
13128 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13129 {
13130 assert!(
13131 menu.selected_item == 0,
13132 "expected PageUp to select the first item from the context menu"
13133 );
13134 } else {
13135 panic!("expected completion menu to stay open after PageUp");
13136 }
13137 });
13138}
13139
13140#[gpui::test]
13141async fn test_as_is_completions(cx: &mut TestAppContext) {
13142 init_test(cx, |_| {});
13143 let mut cx = EditorLspTestContext::new_rust(
13144 lsp::ServerCapabilities {
13145 completion_provider: Some(lsp::CompletionOptions {
13146 ..Default::default()
13147 }),
13148 ..Default::default()
13149 },
13150 cx,
13151 )
13152 .await;
13153 cx.lsp
13154 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13155 Ok(Some(lsp::CompletionResponse::Array(vec![
13156 lsp::CompletionItem {
13157 label: "unsafe".into(),
13158 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13159 range: lsp::Range {
13160 start: lsp::Position {
13161 line: 1,
13162 character: 2,
13163 },
13164 end: lsp::Position {
13165 line: 1,
13166 character: 3,
13167 },
13168 },
13169 new_text: "unsafe".to_string(),
13170 })),
13171 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
13172 ..Default::default()
13173 },
13174 ])))
13175 });
13176 cx.set_state("fn a() {}\n nˇ");
13177 cx.executor().run_until_parked();
13178 cx.update_editor(|editor, window, cx| {
13179 editor.show_completions(
13180 &ShowCompletions {
13181 trigger: Some("\n".into()),
13182 },
13183 window,
13184 cx,
13185 );
13186 });
13187 cx.executor().run_until_parked();
13188
13189 cx.update_editor(|editor, window, cx| {
13190 editor.confirm_completion(&Default::default(), window, cx)
13191 });
13192 cx.executor().run_until_parked();
13193 cx.assert_editor_state("fn a() {}\n unsafeˇ");
13194}
13195
13196#[gpui::test]
13197async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
13198 init_test(cx, |_| {});
13199
13200 let mut cx = EditorLspTestContext::new_rust(
13201 lsp::ServerCapabilities {
13202 completion_provider: Some(lsp::CompletionOptions {
13203 trigger_characters: Some(vec![".".to_string()]),
13204 resolve_provider: Some(true),
13205 ..Default::default()
13206 }),
13207 ..Default::default()
13208 },
13209 cx,
13210 )
13211 .await;
13212
13213 cx.set_state("fn main() { let a = 2ˇ; }");
13214 cx.simulate_keystroke(".");
13215 let completion_item = lsp::CompletionItem {
13216 label: "Some".into(),
13217 kind: Some(lsp::CompletionItemKind::SNIPPET),
13218 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13219 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13220 kind: lsp::MarkupKind::Markdown,
13221 value: "```rust\nSome(2)\n```".to_string(),
13222 })),
13223 deprecated: Some(false),
13224 sort_text: Some("Some".to_string()),
13225 filter_text: Some("Some".to_string()),
13226 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13227 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13228 range: lsp::Range {
13229 start: lsp::Position {
13230 line: 0,
13231 character: 22,
13232 },
13233 end: lsp::Position {
13234 line: 0,
13235 character: 22,
13236 },
13237 },
13238 new_text: "Some(2)".to_string(),
13239 })),
13240 additional_text_edits: Some(vec![lsp::TextEdit {
13241 range: lsp::Range {
13242 start: lsp::Position {
13243 line: 0,
13244 character: 20,
13245 },
13246 end: lsp::Position {
13247 line: 0,
13248 character: 22,
13249 },
13250 },
13251 new_text: "".to_string(),
13252 }]),
13253 ..Default::default()
13254 };
13255
13256 let closure_completion_item = completion_item.clone();
13257 let counter = Arc::new(AtomicUsize::new(0));
13258 let counter_clone = counter.clone();
13259 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13260 let task_completion_item = closure_completion_item.clone();
13261 counter_clone.fetch_add(1, atomic::Ordering::Release);
13262 async move {
13263 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13264 is_incomplete: true,
13265 item_defaults: None,
13266 items: vec![task_completion_item],
13267 })))
13268 }
13269 });
13270
13271 cx.condition(|editor, _| editor.context_menu_visible())
13272 .await;
13273 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
13274 assert!(request.next().await.is_some());
13275 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13276
13277 cx.simulate_keystrokes("S o m");
13278 cx.condition(|editor, _| editor.context_menu_visible())
13279 .await;
13280 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
13281 assert!(request.next().await.is_some());
13282 assert!(request.next().await.is_some());
13283 assert!(request.next().await.is_some());
13284 request.close();
13285 assert!(request.next().await.is_none());
13286 assert_eq!(
13287 counter.load(atomic::Ordering::Acquire),
13288 4,
13289 "With the completions menu open, only one LSP request should happen per input"
13290 );
13291}
13292
13293#[gpui::test]
13294async fn test_toggle_comment(cx: &mut TestAppContext) {
13295 init_test(cx, |_| {});
13296 let mut cx = EditorTestContext::new(cx).await;
13297 let language = Arc::new(Language::new(
13298 LanguageConfig {
13299 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13300 ..Default::default()
13301 },
13302 Some(tree_sitter_rust::LANGUAGE.into()),
13303 ));
13304 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13305
13306 // If multiple selections intersect a line, the line is only toggled once.
13307 cx.set_state(indoc! {"
13308 fn a() {
13309 «//b();
13310 ˇ»// «c();
13311 //ˇ» d();
13312 }
13313 "});
13314
13315 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13316
13317 cx.assert_editor_state(indoc! {"
13318 fn a() {
13319 «b();
13320 c();
13321 ˇ» d();
13322 }
13323 "});
13324
13325 // The comment prefix is inserted at the same column for every line in a
13326 // selection.
13327 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13328
13329 cx.assert_editor_state(indoc! {"
13330 fn a() {
13331 // «b();
13332 // c();
13333 ˇ»// d();
13334 }
13335 "});
13336
13337 // If a selection ends at the beginning of a line, that line is not toggled.
13338 cx.set_selections_state(indoc! {"
13339 fn a() {
13340 // b();
13341 «// c();
13342 ˇ» // d();
13343 }
13344 "});
13345
13346 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13347
13348 cx.assert_editor_state(indoc! {"
13349 fn a() {
13350 // b();
13351 «c();
13352 ˇ» // d();
13353 }
13354 "});
13355
13356 // If a selection span a single line and is empty, the line is toggled.
13357 cx.set_state(indoc! {"
13358 fn a() {
13359 a();
13360 b();
13361 ˇ
13362 }
13363 "});
13364
13365 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13366
13367 cx.assert_editor_state(indoc! {"
13368 fn a() {
13369 a();
13370 b();
13371 //•ˇ
13372 }
13373 "});
13374
13375 // If a selection span multiple lines, empty lines are not toggled.
13376 cx.set_state(indoc! {"
13377 fn a() {
13378 «a();
13379
13380 c();ˇ»
13381 }
13382 "});
13383
13384 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13385
13386 cx.assert_editor_state(indoc! {"
13387 fn a() {
13388 // «a();
13389
13390 // c();ˇ»
13391 }
13392 "});
13393
13394 // If a selection includes multiple comment prefixes, all lines are uncommented.
13395 cx.set_state(indoc! {"
13396 fn a() {
13397 «// a();
13398 /// b();
13399 //! c();ˇ»
13400 }
13401 "});
13402
13403 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13404
13405 cx.assert_editor_state(indoc! {"
13406 fn a() {
13407 «a();
13408 b();
13409 c();ˇ»
13410 }
13411 "});
13412}
13413
13414#[gpui::test]
13415async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
13416 init_test(cx, |_| {});
13417 let mut cx = EditorTestContext::new(cx).await;
13418 let language = Arc::new(Language::new(
13419 LanguageConfig {
13420 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13421 ..Default::default()
13422 },
13423 Some(tree_sitter_rust::LANGUAGE.into()),
13424 ));
13425 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13426
13427 let toggle_comments = &ToggleComments {
13428 advance_downwards: false,
13429 ignore_indent: true,
13430 };
13431
13432 // If multiple selections intersect a line, the line is only toggled once.
13433 cx.set_state(indoc! {"
13434 fn a() {
13435 // «b();
13436 // c();
13437 // ˇ» d();
13438 }
13439 "});
13440
13441 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13442
13443 cx.assert_editor_state(indoc! {"
13444 fn a() {
13445 «b();
13446 c();
13447 ˇ» d();
13448 }
13449 "});
13450
13451 // The comment prefix is inserted at the beginning of each line
13452 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13453
13454 cx.assert_editor_state(indoc! {"
13455 fn a() {
13456 // «b();
13457 // c();
13458 // ˇ» d();
13459 }
13460 "});
13461
13462 // If a selection ends at the beginning of a line, that line is not toggled.
13463 cx.set_selections_state(indoc! {"
13464 fn a() {
13465 // b();
13466 // «c();
13467 ˇ»// d();
13468 }
13469 "});
13470
13471 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13472
13473 cx.assert_editor_state(indoc! {"
13474 fn a() {
13475 // b();
13476 «c();
13477 ˇ»// d();
13478 }
13479 "});
13480
13481 // If a selection span a single line and is empty, the line is toggled.
13482 cx.set_state(indoc! {"
13483 fn a() {
13484 a();
13485 b();
13486 ˇ
13487 }
13488 "});
13489
13490 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13491
13492 cx.assert_editor_state(indoc! {"
13493 fn a() {
13494 a();
13495 b();
13496 //ˇ
13497 }
13498 "});
13499
13500 // If a selection span multiple lines, empty lines are not toggled.
13501 cx.set_state(indoc! {"
13502 fn a() {
13503 «a();
13504
13505 c();ˇ»
13506 }
13507 "});
13508
13509 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13510
13511 cx.assert_editor_state(indoc! {"
13512 fn a() {
13513 // «a();
13514
13515 // c();ˇ»
13516 }
13517 "});
13518
13519 // If a selection includes multiple comment prefixes, all lines are uncommented.
13520 cx.set_state(indoc! {"
13521 fn a() {
13522 // «a();
13523 /// b();
13524 //! c();ˇ»
13525 }
13526 "});
13527
13528 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13529
13530 cx.assert_editor_state(indoc! {"
13531 fn a() {
13532 «a();
13533 b();
13534 c();ˇ»
13535 }
13536 "});
13537}
13538
13539#[gpui::test]
13540async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
13541 init_test(cx, |_| {});
13542
13543 let language = Arc::new(Language::new(
13544 LanguageConfig {
13545 line_comments: vec!["// ".into()],
13546 ..Default::default()
13547 },
13548 Some(tree_sitter_rust::LANGUAGE.into()),
13549 ));
13550
13551 let mut cx = EditorTestContext::new(cx).await;
13552
13553 cx.language_registry().add(language.clone());
13554 cx.update_buffer(|buffer, cx| {
13555 buffer.set_language(Some(language), cx);
13556 });
13557
13558 let toggle_comments = &ToggleComments {
13559 advance_downwards: true,
13560 ignore_indent: false,
13561 };
13562
13563 // Single cursor on one line -> advance
13564 // Cursor moves horizontally 3 characters as well on non-blank line
13565 cx.set_state(indoc!(
13566 "fn a() {
13567 ˇdog();
13568 cat();
13569 }"
13570 ));
13571 cx.update_editor(|editor, window, cx| {
13572 editor.toggle_comments(toggle_comments, window, cx);
13573 });
13574 cx.assert_editor_state(indoc!(
13575 "fn a() {
13576 // dog();
13577 catˇ();
13578 }"
13579 ));
13580
13581 // Single selection on one line -> don't advance
13582 cx.set_state(indoc!(
13583 "fn a() {
13584 «dog()ˇ»;
13585 cat();
13586 }"
13587 ));
13588 cx.update_editor(|editor, window, cx| {
13589 editor.toggle_comments(toggle_comments, window, cx);
13590 });
13591 cx.assert_editor_state(indoc!(
13592 "fn a() {
13593 // «dog()ˇ»;
13594 cat();
13595 }"
13596 ));
13597
13598 // Multiple cursors on one line -> advance
13599 cx.set_state(indoc!(
13600 "fn a() {
13601 ˇdˇog();
13602 cat();
13603 }"
13604 ));
13605 cx.update_editor(|editor, window, cx| {
13606 editor.toggle_comments(toggle_comments, window, cx);
13607 });
13608 cx.assert_editor_state(indoc!(
13609 "fn a() {
13610 // dog();
13611 catˇ(ˇ);
13612 }"
13613 ));
13614
13615 // Multiple cursors on one line, with selection -> don't advance
13616 cx.set_state(indoc!(
13617 "fn a() {
13618 ˇdˇog«()ˇ»;
13619 cat();
13620 }"
13621 ));
13622 cx.update_editor(|editor, window, cx| {
13623 editor.toggle_comments(toggle_comments, window, cx);
13624 });
13625 cx.assert_editor_state(indoc!(
13626 "fn a() {
13627 // ˇdˇog«()ˇ»;
13628 cat();
13629 }"
13630 ));
13631
13632 // Single cursor on one line -> advance
13633 // Cursor moves to column 0 on blank line
13634 cx.set_state(indoc!(
13635 "fn a() {
13636 ˇdog();
13637
13638 cat();
13639 }"
13640 ));
13641 cx.update_editor(|editor, window, cx| {
13642 editor.toggle_comments(toggle_comments, window, cx);
13643 });
13644 cx.assert_editor_state(indoc!(
13645 "fn a() {
13646 // dog();
13647 ˇ
13648 cat();
13649 }"
13650 ));
13651
13652 // Single cursor on one line -> advance
13653 // Cursor starts and ends at column 0
13654 cx.set_state(indoc!(
13655 "fn a() {
13656 ˇ dog();
13657 cat();
13658 }"
13659 ));
13660 cx.update_editor(|editor, window, cx| {
13661 editor.toggle_comments(toggle_comments, window, cx);
13662 });
13663 cx.assert_editor_state(indoc!(
13664 "fn a() {
13665 // dog();
13666 ˇ cat();
13667 }"
13668 ));
13669}
13670
13671#[gpui::test]
13672async fn test_toggle_block_comment(cx: &mut TestAppContext) {
13673 init_test(cx, |_| {});
13674
13675 let mut cx = EditorTestContext::new(cx).await;
13676
13677 let html_language = Arc::new(
13678 Language::new(
13679 LanguageConfig {
13680 name: "HTML".into(),
13681 block_comment: Some(("<!-- ".into(), " -->".into())),
13682 ..Default::default()
13683 },
13684 Some(tree_sitter_html::LANGUAGE.into()),
13685 )
13686 .with_injection_query(
13687 r#"
13688 (script_element
13689 (raw_text) @injection.content
13690 (#set! injection.language "javascript"))
13691 "#,
13692 )
13693 .unwrap(),
13694 );
13695
13696 let javascript_language = Arc::new(Language::new(
13697 LanguageConfig {
13698 name: "JavaScript".into(),
13699 line_comments: vec!["// ".into()],
13700 ..Default::default()
13701 },
13702 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13703 ));
13704
13705 cx.language_registry().add(html_language.clone());
13706 cx.language_registry().add(javascript_language.clone());
13707 cx.update_buffer(|buffer, cx| {
13708 buffer.set_language(Some(html_language), cx);
13709 });
13710
13711 // Toggle comments for empty selections
13712 cx.set_state(
13713 &r#"
13714 <p>A</p>ˇ
13715 <p>B</p>ˇ
13716 <p>C</p>ˇ
13717 "#
13718 .unindent(),
13719 );
13720 cx.update_editor(|editor, window, cx| {
13721 editor.toggle_comments(&ToggleComments::default(), window, cx)
13722 });
13723 cx.assert_editor_state(
13724 &r#"
13725 <!-- <p>A</p>ˇ -->
13726 <!-- <p>B</p>ˇ -->
13727 <!-- <p>C</p>ˇ -->
13728 "#
13729 .unindent(),
13730 );
13731 cx.update_editor(|editor, window, cx| {
13732 editor.toggle_comments(&ToggleComments::default(), window, cx)
13733 });
13734 cx.assert_editor_state(
13735 &r#"
13736 <p>A</p>ˇ
13737 <p>B</p>ˇ
13738 <p>C</p>ˇ
13739 "#
13740 .unindent(),
13741 );
13742
13743 // Toggle comments for mixture of empty and non-empty selections, where
13744 // multiple selections occupy a given line.
13745 cx.set_state(
13746 &r#"
13747 <p>A«</p>
13748 <p>ˇ»B</p>ˇ
13749 <p>C«</p>
13750 <p>ˇ»D</p>ˇ
13751 "#
13752 .unindent(),
13753 );
13754
13755 cx.update_editor(|editor, window, cx| {
13756 editor.toggle_comments(&ToggleComments::default(), window, cx)
13757 });
13758 cx.assert_editor_state(
13759 &r#"
13760 <!-- <p>A«</p>
13761 <p>ˇ»B</p>ˇ -->
13762 <!-- <p>C«</p>
13763 <p>ˇ»D</p>ˇ -->
13764 "#
13765 .unindent(),
13766 );
13767 cx.update_editor(|editor, window, cx| {
13768 editor.toggle_comments(&ToggleComments::default(), window, cx)
13769 });
13770 cx.assert_editor_state(
13771 &r#"
13772 <p>A«</p>
13773 <p>ˇ»B</p>ˇ
13774 <p>C«</p>
13775 <p>ˇ»D</p>ˇ
13776 "#
13777 .unindent(),
13778 );
13779
13780 // Toggle comments when different languages are active for different
13781 // selections.
13782 cx.set_state(
13783 &r#"
13784 ˇ<script>
13785 ˇvar x = new Y();
13786 ˇ</script>
13787 "#
13788 .unindent(),
13789 );
13790 cx.executor().run_until_parked();
13791 cx.update_editor(|editor, window, cx| {
13792 editor.toggle_comments(&ToggleComments::default(), window, cx)
13793 });
13794 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
13795 // Uncommenting and commenting from this position brings in even more wrong artifacts.
13796 cx.assert_editor_state(
13797 &r#"
13798 <!-- ˇ<script> -->
13799 // ˇvar x = new Y();
13800 <!-- ˇ</script> -->
13801 "#
13802 .unindent(),
13803 );
13804}
13805
13806#[gpui::test]
13807fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
13808 init_test(cx, |_| {});
13809
13810 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13811 let multibuffer = cx.new(|cx| {
13812 let mut multibuffer = MultiBuffer::new(ReadWrite);
13813 multibuffer.push_excerpts(
13814 buffer.clone(),
13815 [
13816 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
13817 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
13818 ],
13819 cx,
13820 );
13821 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
13822 multibuffer
13823 });
13824
13825 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13826 editor.update_in(cx, |editor, window, cx| {
13827 assert_eq!(editor.text(cx), "aaaa\nbbbb");
13828 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13829 s.select_ranges([
13830 Point::new(0, 0)..Point::new(0, 0),
13831 Point::new(1, 0)..Point::new(1, 0),
13832 ])
13833 });
13834
13835 editor.handle_input("X", window, cx);
13836 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
13837 assert_eq!(
13838 editor.selections.ranges(cx),
13839 [
13840 Point::new(0, 1)..Point::new(0, 1),
13841 Point::new(1, 1)..Point::new(1, 1),
13842 ]
13843 );
13844
13845 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
13846 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13847 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
13848 });
13849 editor.backspace(&Default::default(), window, cx);
13850 assert_eq!(editor.text(cx), "Xa\nbbb");
13851 assert_eq!(
13852 editor.selections.ranges(cx),
13853 [Point::new(1, 0)..Point::new(1, 0)]
13854 );
13855
13856 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13857 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
13858 });
13859 editor.backspace(&Default::default(), window, cx);
13860 assert_eq!(editor.text(cx), "X\nbb");
13861 assert_eq!(
13862 editor.selections.ranges(cx),
13863 [Point::new(0, 1)..Point::new(0, 1)]
13864 );
13865 });
13866}
13867
13868#[gpui::test]
13869fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
13870 init_test(cx, |_| {});
13871
13872 let markers = vec![('[', ']').into(), ('(', ')').into()];
13873 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
13874 indoc! {"
13875 [aaaa
13876 (bbbb]
13877 cccc)",
13878 },
13879 markers.clone(),
13880 );
13881 let excerpt_ranges = markers.into_iter().map(|marker| {
13882 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
13883 ExcerptRange::new(context.clone())
13884 });
13885 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
13886 let multibuffer = cx.new(|cx| {
13887 let mut multibuffer = MultiBuffer::new(ReadWrite);
13888 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
13889 multibuffer
13890 });
13891
13892 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13893 editor.update_in(cx, |editor, window, cx| {
13894 let (expected_text, selection_ranges) = marked_text_ranges(
13895 indoc! {"
13896 aaaa
13897 bˇbbb
13898 bˇbbˇb
13899 cccc"
13900 },
13901 true,
13902 );
13903 assert_eq!(editor.text(cx), expected_text);
13904 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13905 s.select_ranges(selection_ranges)
13906 });
13907
13908 editor.handle_input("X", window, cx);
13909
13910 let (expected_text, expected_selections) = marked_text_ranges(
13911 indoc! {"
13912 aaaa
13913 bXˇbbXb
13914 bXˇbbXˇb
13915 cccc"
13916 },
13917 false,
13918 );
13919 assert_eq!(editor.text(cx), expected_text);
13920 assert_eq!(editor.selections.ranges(cx), expected_selections);
13921
13922 editor.newline(&Newline, window, cx);
13923 let (expected_text, expected_selections) = marked_text_ranges(
13924 indoc! {"
13925 aaaa
13926 bX
13927 ˇbbX
13928 b
13929 bX
13930 ˇbbX
13931 ˇb
13932 cccc"
13933 },
13934 false,
13935 );
13936 assert_eq!(editor.text(cx), expected_text);
13937 assert_eq!(editor.selections.ranges(cx), expected_selections);
13938 });
13939}
13940
13941#[gpui::test]
13942fn test_refresh_selections(cx: &mut TestAppContext) {
13943 init_test(cx, |_| {});
13944
13945 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13946 let mut excerpt1_id = None;
13947 let multibuffer = cx.new(|cx| {
13948 let mut multibuffer = MultiBuffer::new(ReadWrite);
13949 excerpt1_id = multibuffer
13950 .push_excerpts(
13951 buffer.clone(),
13952 [
13953 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
13954 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
13955 ],
13956 cx,
13957 )
13958 .into_iter()
13959 .next();
13960 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
13961 multibuffer
13962 });
13963
13964 let editor = cx.add_window(|window, cx| {
13965 let mut editor = build_editor(multibuffer.clone(), window, cx);
13966 let snapshot = editor.snapshot(window, cx);
13967 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13968 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
13969 });
13970 editor.begin_selection(
13971 Point::new(2, 1).to_display_point(&snapshot),
13972 true,
13973 1,
13974 window,
13975 cx,
13976 );
13977 assert_eq!(
13978 editor.selections.ranges(cx),
13979 [
13980 Point::new(1, 3)..Point::new(1, 3),
13981 Point::new(2, 1)..Point::new(2, 1),
13982 ]
13983 );
13984 editor
13985 });
13986
13987 // Refreshing selections is a no-op when excerpts haven't changed.
13988 _ = editor.update(cx, |editor, window, cx| {
13989 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
13990 assert_eq!(
13991 editor.selections.ranges(cx),
13992 [
13993 Point::new(1, 3)..Point::new(1, 3),
13994 Point::new(2, 1)..Point::new(2, 1),
13995 ]
13996 );
13997 });
13998
13999 multibuffer.update(cx, |multibuffer, cx| {
14000 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14001 });
14002 _ = editor.update(cx, |editor, window, cx| {
14003 // Removing an excerpt causes the first selection to become degenerate.
14004 assert_eq!(
14005 editor.selections.ranges(cx),
14006 [
14007 Point::new(0, 0)..Point::new(0, 0),
14008 Point::new(0, 1)..Point::new(0, 1)
14009 ]
14010 );
14011
14012 // Refreshing selections will relocate the first selection to the original buffer
14013 // location.
14014 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14015 assert_eq!(
14016 editor.selections.ranges(cx),
14017 [
14018 Point::new(0, 1)..Point::new(0, 1),
14019 Point::new(0, 3)..Point::new(0, 3)
14020 ]
14021 );
14022 assert!(editor.selections.pending_anchor().is_some());
14023 });
14024}
14025
14026#[gpui::test]
14027fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
14028 init_test(cx, |_| {});
14029
14030 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14031 let mut excerpt1_id = None;
14032 let multibuffer = cx.new(|cx| {
14033 let mut multibuffer = MultiBuffer::new(ReadWrite);
14034 excerpt1_id = multibuffer
14035 .push_excerpts(
14036 buffer.clone(),
14037 [
14038 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14039 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14040 ],
14041 cx,
14042 )
14043 .into_iter()
14044 .next();
14045 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14046 multibuffer
14047 });
14048
14049 let editor = cx.add_window(|window, cx| {
14050 let mut editor = build_editor(multibuffer.clone(), window, cx);
14051 let snapshot = editor.snapshot(window, cx);
14052 editor.begin_selection(
14053 Point::new(1, 3).to_display_point(&snapshot),
14054 false,
14055 1,
14056 window,
14057 cx,
14058 );
14059 assert_eq!(
14060 editor.selections.ranges(cx),
14061 [Point::new(1, 3)..Point::new(1, 3)]
14062 );
14063 editor
14064 });
14065
14066 multibuffer.update(cx, |multibuffer, cx| {
14067 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14068 });
14069 _ = editor.update(cx, |editor, window, cx| {
14070 assert_eq!(
14071 editor.selections.ranges(cx),
14072 [Point::new(0, 0)..Point::new(0, 0)]
14073 );
14074
14075 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
14076 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14077 assert_eq!(
14078 editor.selections.ranges(cx),
14079 [Point::new(0, 3)..Point::new(0, 3)]
14080 );
14081 assert!(editor.selections.pending_anchor().is_some());
14082 });
14083}
14084
14085#[gpui::test]
14086async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
14087 init_test(cx, |_| {});
14088
14089 let language = Arc::new(
14090 Language::new(
14091 LanguageConfig {
14092 brackets: BracketPairConfig {
14093 pairs: vec![
14094 BracketPair {
14095 start: "{".to_string(),
14096 end: "}".to_string(),
14097 close: true,
14098 surround: true,
14099 newline: true,
14100 },
14101 BracketPair {
14102 start: "/* ".to_string(),
14103 end: " */".to_string(),
14104 close: true,
14105 surround: true,
14106 newline: true,
14107 },
14108 ],
14109 ..Default::default()
14110 },
14111 ..Default::default()
14112 },
14113 Some(tree_sitter_rust::LANGUAGE.into()),
14114 )
14115 .with_indents_query("")
14116 .unwrap(),
14117 );
14118
14119 let text = concat!(
14120 "{ }\n", //
14121 " x\n", //
14122 " /* */\n", //
14123 "x\n", //
14124 "{{} }\n", //
14125 );
14126
14127 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
14128 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14129 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14130 editor
14131 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
14132 .await;
14133
14134 editor.update_in(cx, |editor, window, cx| {
14135 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14136 s.select_display_ranges([
14137 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
14138 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
14139 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
14140 ])
14141 });
14142 editor.newline(&Newline, window, cx);
14143
14144 assert_eq!(
14145 editor.buffer().read(cx).read(cx).text(),
14146 concat!(
14147 "{ \n", // Suppress rustfmt
14148 "\n", //
14149 "}\n", //
14150 " x\n", //
14151 " /* \n", //
14152 " \n", //
14153 " */\n", //
14154 "x\n", //
14155 "{{} \n", //
14156 "}\n", //
14157 )
14158 );
14159 });
14160}
14161
14162#[gpui::test]
14163fn test_highlighted_ranges(cx: &mut TestAppContext) {
14164 init_test(cx, |_| {});
14165
14166 let editor = cx.add_window(|window, cx| {
14167 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
14168 build_editor(buffer.clone(), window, cx)
14169 });
14170
14171 _ = editor.update(cx, |editor, window, cx| {
14172 struct Type1;
14173 struct Type2;
14174
14175 let buffer = editor.buffer.read(cx).snapshot(cx);
14176
14177 let anchor_range =
14178 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
14179
14180 editor.highlight_background::<Type1>(
14181 &[
14182 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
14183 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
14184 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
14185 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
14186 ],
14187 |_| Hsla::red(),
14188 cx,
14189 );
14190 editor.highlight_background::<Type2>(
14191 &[
14192 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
14193 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
14194 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
14195 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
14196 ],
14197 |_| Hsla::green(),
14198 cx,
14199 );
14200
14201 let snapshot = editor.snapshot(window, cx);
14202 let mut highlighted_ranges = editor.background_highlights_in_range(
14203 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
14204 &snapshot,
14205 cx.theme(),
14206 );
14207 // Enforce a consistent ordering based on color without relying on the ordering of the
14208 // highlight's `TypeId` which is non-executor.
14209 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
14210 assert_eq!(
14211 highlighted_ranges,
14212 &[
14213 (
14214 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
14215 Hsla::red(),
14216 ),
14217 (
14218 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14219 Hsla::red(),
14220 ),
14221 (
14222 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
14223 Hsla::green(),
14224 ),
14225 (
14226 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
14227 Hsla::green(),
14228 ),
14229 ]
14230 );
14231 assert_eq!(
14232 editor.background_highlights_in_range(
14233 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
14234 &snapshot,
14235 cx.theme(),
14236 ),
14237 &[(
14238 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14239 Hsla::red(),
14240 )]
14241 );
14242 });
14243}
14244
14245#[gpui::test]
14246async fn test_following(cx: &mut TestAppContext) {
14247 init_test(cx, |_| {});
14248
14249 let fs = FakeFs::new(cx.executor());
14250 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14251
14252 let buffer = project.update(cx, |project, cx| {
14253 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
14254 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
14255 });
14256 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
14257 let follower = cx.update(|cx| {
14258 cx.open_window(
14259 WindowOptions {
14260 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
14261 gpui::Point::new(px(0.), px(0.)),
14262 gpui::Point::new(px(10.), px(80.)),
14263 ))),
14264 ..Default::default()
14265 },
14266 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
14267 )
14268 .unwrap()
14269 });
14270
14271 let is_still_following = Rc::new(RefCell::new(true));
14272 let follower_edit_event_count = Rc::new(RefCell::new(0));
14273 let pending_update = Rc::new(RefCell::new(None));
14274 let leader_entity = leader.root(cx).unwrap();
14275 let follower_entity = follower.root(cx).unwrap();
14276 _ = follower.update(cx, {
14277 let update = pending_update.clone();
14278 let is_still_following = is_still_following.clone();
14279 let follower_edit_event_count = follower_edit_event_count.clone();
14280 |_, window, cx| {
14281 cx.subscribe_in(
14282 &leader_entity,
14283 window,
14284 move |_, leader, event, window, cx| {
14285 leader.read(cx).add_event_to_update_proto(
14286 event,
14287 &mut update.borrow_mut(),
14288 window,
14289 cx,
14290 );
14291 },
14292 )
14293 .detach();
14294
14295 cx.subscribe_in(
14296 &follower_entity,
14297 window,
14298 move |_, _, event: &EditorEvent, _window, _cx| {
14299 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
14300 *is_still_following.borrow_mut() = false;
14301 }
14302
14303 if let EditorEvent::BufferEdited = event {
14304 *follower_edit_event_count.borrow_mut() += 1;
14305 }
14306 },
14307 )
14308 .detach();
14309 }
14310 });
14311
14312 // Update the selections only
14313 _ = leader.update(cx, |leader, window, cx| {
14314 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14315 s.select_ranges([1..1])
14316 });
14317 });
14318 follower
14319 .update(cx, |follower, window, cx| {
14320 follower.apply_update_proto(
14321 &project,
14322 pending_update.borrow_mut().take().unwrap(),
14323 window,
14324 cx,
14325 )
14326 })
14327 .unwrap()
14328 .await
14329 .unwrap();
14330 _ = follower.update(cx, |follower, _, cx| {
14331 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
14332 });
14333 assert!(*is_still_following.borrow());
14334 assert_eq!(*follower_edit_event_count.borrow(), 0);
14335
14336 // Update the scroll position only
14337 _ = leader.update(cx, |leader, window, cx| {
14338 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14339 });
14340 follower
14341 .update(cx, |follower, window, cx| {
14342 follower.apply_update_proto(
14343 &project,
14344 pending_update.borrow_mut().take().unwrap(),
14345 window,
14346 cx,
14347 )
14348 })
14349 .unwrap()
14350 .await
14351 .unwrap();
14352 assert_eq!(
14353 follower
14354 .update(cx, |follower, _, cx| follower.scroll_position(cx))
14355 .unwrap(),
14356 gpui::Point::new(1.5, 3.5)
14357 );
14358 assert!(*is_still_following.borrow());
14359 assert_eq!(*follower_edit_event_count.borrow(), 0);
14360
14361 // Update the selections and scroll position. The follower's scroll position is updated
14362 // via autoscroll, not via the leader's exact scroll position.
14363 _ = leader.update(cx, |leader, window, cx| {
14364 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14365 s.select_ranges([0..0])
14366 });
14367 leader.request_autoscroll(Autoscroll::newest(), cx);
14368 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14369 });
14370 follower
14371 .update(cx, |follower, window, cx| {
14372 follower.apply_update_proto(
14373 &project,
14374 pending_update.borrow_mut().take().unwrap(),
14375 window,
14376 cx,
14377 )
14378 })
14379 .unwrap()
14380 .await
14381 .unwrap();
14382 _ = follower.update(cx, |follower, _, cx| {
14383 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
14384 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
14385 });
14386 assert!(*is_still_following.borrow());
14387
14388 // Creating a pending selection that precedes another selection
14389 _ = leader.update(cx, |leader, window, cx| {
14390 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14391 s.select_ranges([1..1])
14392 });
14393 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
14394 });
14395 follower
14396 .update(cx, |follower, window, cx| {
14397 follower.apply_update_proto(
14398 &project,
14399 pending_update.borrow_mut().take().unwrap(),
14400 window,
14401 cx,
14402 )
14403 })
14404 .unwrap()
14405 .await
14406 .unwrap();
14407 _ = follower.update(cx, |follower, _, cx| {
14408 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
14409 });
14410 assert!(*is_still_following.borrow());
14411
14412 // Extend the pending selection so that it surrounds another selection
14413 _ = leader.update(cx, |leader, window, cx| {
14414 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
14415 });
14416 follower
14417 .update(cx, |follower, window, cx| {
14418 follower.apply_update_proto(
14419 &project,
14420 pending_update.borrow_mut().take().unwrap(),
14421 window,
14422 cx,
14423 )
14424 })
14425 .unwrap()
14426 .await
14427 .unwrap();
14428 _ = follower.update(cx, |follower, _, cx| {
14429 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
14430 });
14431
14432 // Scrolling locally breaks the follow
14433 _ = follower.update(cx, |follower, window, cx| {
14434 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
14435 follower.set_scroll_anchor(
14436 ScrollAnchor {
14437 anchor: top_anchor,
14438 offset: gpui::Point::new(0.0, 0.5),
14439 },
14440 window,
14441 cx,
14442 );
14443 });
14444 assert!(!(*is_still_following.borrow()));
14445}
14446
14447#[gpui::test]
14448async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
14449 init_test(cx, |_| {});
14450
14451 let fs = FakeFs::new(cx.executor());
14452 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14453 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14454 let pane = workspace
14455 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14456 .unwrap();
14457
14458 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14459
14460 let leader = pane.update_in(cx, |_, window, cx| {
14461 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
14462 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
14463 });
14464
14465 // Start following the editor when it has no excerpts.
14466 let mut state_message =
14467 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14468 let workspace_entity = workspace.root(cx).unwrap();
14469 let follower_1 = cx
14470 .update_window(*workspace.deref(), |_, window, cx| {
14471 Editor::from_state_proto(
14472 workspace_entity,
14473 ViewId {
14474 creator: CollaboratorId::PeerId(PeerId::default()),
14475 id: 0,
14476 },
14477 &mut state_message,
14478 window,
14479 cx,
14480 )
14481 })
14482 .unwrap()
14483 .unwrap()
14484 .await
14485 .unwrap();
14486
14487 let update_message = Rc::new(RefCell::new(None));
14488 follower_1.update_in(cx, {
14489 let update = update_message.clone();
14490 |_, window, cx| {
14491 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
14492 leader.read(cx).add_event_to_update_proto(
14493 event,
14494 &mut update.borrow_mut(),
14495 window,
14496 cx,
14497 );
14498 })
14499 .detach();
14500 }
14501 });
14502
14503 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
14504 (
14505 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
14506 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
14507 )
14508 });
14509
14510 // Insert some excerpts.
14511 leader.update(cx, |leader, cx| {
14512 leader.buffer.update(cx, |multibuffer, cx| {
14513 multibuffer.set_excerpts_for_path(
14514 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
14515 buffer_1.clone(),
14516 vec![
14517 Point::row_range(0..3),
14518 Point::row_range(1..6),
14519 Point::row_range(12..15),
14520 ],
14521 0,
14522 cx,
14523 );
14524 multibuffer.set_excerpts_for_path(
14525 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
14526 buffer_2.clone(),
14527 vec![Point::row_range(0..6), Point::row_range(8..12)],
14528 0,
14529 cx,
14530 );
14531 });
14532 });
14533
14534 // Apply the update of adding the excerpts.
14535 follower_1
14536 .update_in(cx, |follower, window, cx| {
14537 follower.apply_update_proto(
14538 &project,
14539 update_message.borrow().clone().unwrap(),
14540 window,
14541 cx,
14542 )
14543 })
14544 .await
14545 .unwrap();
14546 assert_eq!(
14547 follower_1.update(cx, |editor, cx| editor.text(cx)),
14548 leader.update(cx, |editor, cx| editor.text(cx))
14549 );
14550 update_message.borrow_mut().take();
14551
14552 // Start following separately after it already has excerpts.
14553 let mut state_message =
14554 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14555 let workspace_entity = workspace.root(cx).unwrap();
14556 let follower_2 = cx
14557 .update_window(*workspace.deref(), |_, window, cx| {
14558 Editor::from_state_proto(
14559 workspace_entity,
14560 ViewId {
14561 creator: CollaboratorId::PeerId(PeerId::default()),
14562 id: 0,
14563 },
14564 &mut state_message,
14565 window,
14566 cx,
14567 )
14568 })
14569 .unwrap()
14570 .unwrap()
14571 .await
14572 .unwrap();
14573 assert_eq!(
14574 follower_2.update(cx, |editor, cx| editor.text(cx)),
14575 leader.update(cx, |editor, cx| editor.text(cx))
14576 );
14577
14578 // Remove some excerpts.
14579 leader.update(cx, |leader, cx| {
14580 leader.buffer.update(cx, |multibuffer, cx| {
14581 let excerpt_ids = multibuffer.excerpt_ids();
14582 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
14583 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
14584 });
14585 });
14586
14587 // Apply the update of removing the excerpts.
14588 follower_1
14589 .update_in(cx, |follower, window, cx| {
14590 follower.apply_update_proto(
14591 &project,
14592 update_message.borrow().clone().unwrap(),
14593 window,
14594 cx,
14595 )
14596 })
14597 .await
14598 .unwrap();
14599 follower_2
14600 .update_in(cx, |follower, window, cx| {
14601 follower.apply_update_proto(
14602 &project,
14603 update_message.borrow().clone().unwrap(),
14604 window,
14605 cx,
14606 )
14607 })
14608 .await
14609 .unwrap();
14610 update_message.borrow_mut().take();
14611 assert_eq!(
14612 follower_1.update(cx, |editor, cx| editor.text(cx)),
14613 leader.update(cx, |editor, cx| editor.text(cx))
14614 );
14615}
14616
14617#[gpui::test]
14618async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14619 init_test(cx, |_| {});
14620
14621 let mut cx = EditorTestContext::new(cx).await;
14622 let lsp_store =
14623 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
14624
14625 cx.set_state(indoc! {"
14626 ˇfn func(abc def: i32) -> u32 {
14627 }
14628 "});
14629
14630 cx.update(|_, cx| {
14631 lsp_store.update(cx, |lsp_store, cx| {
14632 lsp_store
14633 .update_diagnostics(
14634 LanguageServerId(0),
14635 lsp::PublishDiagnosticsParams {
14636 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
14637 version: None,
14638 diagnostics: vec![
14639 lsp::Diagnostic {
14640 range: lsp::Range::new(
14641 lsp::Position::new(0, 11),
14642 lsp::Position::new(0, 12),
14643 ),
14644 severity: Some(lsp::DiagnosticSeverity::ERROR),
14645 ..Default::default()
14646 },
14647 lsp::Diagnostic {
14648 range: lsp::Range::new(
14649 lsp::Position::new(0, 12),
14650 lsp::Position::new(0, 15),
14651 ),
14652 severity: Some(lsp::DiagnosticSeverity::ERROR),
14653 ..Default::default()
14654 },
14655 lsp::Diagnostic {
14656 range: lsp::Range::new(
14657 lsp::Position::new(0, 25),
14658 lsp::Position::new(0, 28),
14659 ),
14660 severity: Some(lsp::DiagnosticSeverity::ERROR),
14661 ..Default::default()
14662 },
14663 ],
14664 },
14665 None,
14666 DiagnosticSourceKind::Pushed,
14667 &[],
14668 cx,
14669 )
14670 .unwrap()
14671 });
14672 });
14673
14674 executor.run_until_parked();
14675
14676 cx.update_editor(|editor, window, cx| {
14677 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14678 });
14679
14680 cx.assert_editor_state(indoc! {"
14681 fn func(abc def: i32) -> ˇu32 {
14682 }
14683 "});
14684
14685 cx.update_editor(|editor, window, cx| {
14686 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14687 });
14688
14689 cx.assert_editor_state(indoc! {"
14690 fn func(abc ˇdef: i32) -> u32 {
14691 }
14692 "});
14693
14694 cx.update_editor(|editor, window, cx| {
14695 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14696 });
14697
14698 cx.assert_editor_state(indoc! {"
14699 fn func(abcˇ def: i32) -> u32 {
14700 }
14701 "});
14702
14703 cx.update_editor(|editor, window, cx| {
14704 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14705 });
14706
14707 cx.assert_editor_state(indoc! {"
14708 fn func(abc def: i32) -> ˇu32 {
14709 }
14710 "});
14711}
14712
14713#[gpui::test]
14714async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14715 init_test(cx, |_| {});
14716
14717 let mut cx = EditorTestContext::new(cx).await;
14718
14719 let diff_base = r#"
14720 use some::mod;
14721
14722 const A: u32 = 42;
14723
14724 fn main() {
14725 println!("hello");
14726
14727 println!("world");
14728 }
14729 "#
14730 .unindent();
14731
14732 // Edits are modified, removed, modified, added
14733 cx.set_state(
14734 &r#"
14735 use some::modified;
14736
14737 ˇ
14738 fn main() {
14739 println!("hello there");
14740
14741 println!("around the");
14742 println!("world");
14743 }
14744 "#
14745 .unindent(),
14746 );
14747
14748 cx.set_head_text(&diff_base);
14749 executor.run_until_parked();
14750
14751 cx.update_editor(|editor, window, cx| {
14752 //Wrap around the bottom of the buffer
14753 for _ in 0..3 {
14754 editor.go_to_next_hunk(&GoToHunk, window, cx);
14755 }
14756 });
14757
14758 cx.assert_editor_state(
14759 &r#"
14760 ˇuse some::modified;
14761
14762
14763 fn main() {
14764 println!("hello there");
14765
14766 println!("around the");
14767 println!("world");
14768 }
14769 "#
14770 .unindent(),
14771 );
14772
14773 cx.update_editor(|editor, window, cx| {
14774 //Wrap around the top of the buffer
14775 for _ in 0..2 {
14776 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14777 }
14778 });
14779
14780 cx.assert_editor_state(
14781 &r#"
14782 use some::modified;
14783
14784
14785 fn main() {
14786 ˇ println!("hello there");
14787
14788 println!("around the");
14789 println!("world");
14790 }
14791 "#
14792 .unindent(),
14793 );
14794
14795 cx.update_editor(|editor, window, cx| {
14796 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14797 });
14798
14799 cx.assert_editor_state(
14800 &r#"
14801 use some::modified;
14802
14803 ˇ
14804 fn main() {
14805 println!("hello there");
14806
14807 println!("around the");
14808 println!("world");
14809 }
14810 "#
14811 .unindent(),
14812 );
14813
14814 cx.update_editor(|editor, window, cx| {
14815 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14816 });
14817
14818 cx.assert_editor_state(
14819 &r#"
14820 ˇuse some::modified;
14821
14822
14823 fn main() {
14824 println!("hello there");
14825
14826 println!("around the");
14827 println!("world");
14828 }
14829 "#
14830 .unindent(),
14831 );
14832
14833 cx.update_editor(|editor, window, cx| {
14834 for _ in 0..2 {
14835 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14836 }
14837 });
14838
14839 cx.assert_editor_state(
14840 &r#"
14841 use some::modified;
14842
14843
14844 fn main() {
14845 ˇ println!("hello there");
14846
14847 println!("around the");
14848 println!("world");
14849 }
14850 "#
14851 .unindent(),
14852 );
14853
14854 cx.update_editor(|editor, window, cx| {
14855 editor.fold(&Fold, window, cx);
14856 });
14857
14858 cx.update_editor(|editor, window, cx| {
14859 editor.go_to_next_hunk(&GoToHunk, window, cx);
14860 });
14861
14862 cx.assert_editor_state(
14863 &r#"
14864 ˇuse some::modified;
14865
14866
14867 fn main() {
14868 println!("hello there");
14869
14870 println!("around the");
14871 println!("world");
14872 }
14873 "#
14874 .unindent(),
14875 );
14876}
14877
14878#[test]
14879fn test_split_words() {
14880 fn split(text: &str) -> Vec<&str> {
14881 split_words(text).collect()
14882 }
14883
14884 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
14885 assert_eq!(split("hello_world"), &["hello_", "world"]);
14886 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
14887 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
14888 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
14889 assert_eq!(split("helloworld"), &["helloworld"]);
14890
14891 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
14892}
14893
14894#[gpui::test]
14895async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
14896 init_test(cx, |_| {});
14897
14898 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
14899 let mut assert = |before, after| {
14900 let _state_context = cx.set_state(before);
14901 cx.run_until_parked();
14902 cx.update_editor(|editor, window, cx| {
14903 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
14904 });
14905 cx.run_until_parked();
14906 cx.assert_editor_state(after);
14907 };
14908
14909 // Outside bracket jumps to outside of matching bracket
14910 assert("console.logˇ(var);", "console.log(var)ˇ;");
14911 assert("console.log(var)ˇ;", "console.logˇ(var);");
14912
14913 // Inside bracket jumps to inside of matching bracket
14914 assert("console.log(ˇvar);", "console.log(varˇ);");
14915 assert("console.log(varˇ);", "console.log(ˇvar);");
14916
14917 // When outside a bracket and inside, favor jumping to the inside bracket
14918 assert(
14919 "console.log('foo', [1, 2, 3]ˇ);",
14920 "console.log(ˇ'foo', [1, 2, 3]);",
14921 );
14922 assert(
14923 "console.log(ˇ'foo', [1, 2, 3]);",
14924 "console.log('foo', [1, 2, 3]ˇ);",
14925 );
14926
14927 // Bias forward if two options are equally likely
14928 assert(
14929 "let result = curried_fun()ˇ();",
14930 "let result = curried_fun()()ˇ;",
14931 );
14932
14933 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
14934 assert(
14935 indoc! {"
14936 function test() {
14937 console.log('test')ˇ
14938 }"},
14939 indoc! {"
14940 function test() {
14941 console.logˇ('test')
14942 }"},
14943 );
14944}
14945
14946#[gpui::test]
14947async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
14948 init_test(cx, |_| {});
14949
14950 let fs = FakeFs::new(cx.executor());
14951 fs.insert_tree(
14952 path!("/a"),
14953 json!({
14954 "main.rs": "fn main() { let a = 5; }",
14955 "other.rs": "// Test file",
14956 }),
14957 )
14958 .await;
14959 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14960
14961 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14962 language_registry.add(Arc::new(Language::new(
14963 LanguageConfig {
14964 name: "Rust".into(),
14965 matcher: LanguageMatcher {
14966 path_suffixes: vec!["rs".to_string()],
14967 ..Default::default()
14968 },
14969 brackets: BracketPairConfig {
14970 pairs: vec![BracketPair {
14971 start: "{".to_string(),
14972 end: "}".to_string(),
14973 close: true,
14974 surround: true,
14975 newline: true,
14976 }],
14977 disabled_scopes_by_bracket_ix: Vec::new(),
14978 },
14979 ..Default::default()
14980 },
14981 Some(tree_sitter_rust::LANGUAGE.into()),
14982 )));
14983 let mut fake_servers = language_registry.register_fake_lsp(
14984 "Rust",
14985 FakeLspAdapter {
14986 capabilities: lsp::ServerCapabilities {
14987 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
14988 first_trigger_character: "{".to_string(),
14989 more_trigger_character: None,
14990 }),
14991 ..Default::default()
14992 },
14993 ..Default::default()
14994 },
14995 );
14996
14997 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14998
14999 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15000
15001 let worktree_id = workspace
15002 .update(cx, |workspace, _, cx| {
15003 workspace.project().update(cx, |project, cx| {
15004 project.worktrees(cx).next().unwrap().read(cx).id()
15005 })
15006 })
15007 .unwrap();
15008
15009 let buffer = project
15010 .update(cx, |project, cx| {
15011 project.open_local_buffer(path!("/a/main.rs"), cx)
15012 })
15013 .await
15014 .unwrap();
15015 let editor_handle = workspace
15016 .update(cx, |workspace, window, cx| {
15017 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
15018 })
15019 .unwrap()
15020 .await
15021 .unwrap()
15022 .downcast::<Editor>()
15023 .unwrap();
15024
15025 cx.executor().start_waiting();
15026 let fake_server = fake_servers.next().await.unwrap();
15027
15028 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
15029 |params, _| async move {
15030 assert_eq!(
15031 params.text_document_position.text_document.uri,
15032 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
15033 );
15034 assert_eq!(
15035 params.text_document_position.position,
15036 lsp::Position::new(0, 21),
15037 );
15038
15039 Ok(Some(vec![lsp::TextEdit {
15040 new_text: "]".to_string(),
15041 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15042 }]))
15043 },
15044 );
15045
15046 editor_handle.update_in(cx, |editor, window, cx| {
15047 window.focus(&editor.focus_handle(cx));
15048 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15049 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
15050 });
15051 editor.handle_input("{", window, cx);
15052 });
15053
15054 cx.executor().run_until_parked();
15055
15056 buffer.update(cx, |buffer, _| {
15057 assert_eq!(
15058 buffer.text(),
15059 "fn main() { let a = {5}; }",
15060 "No extra braces from on type formatting should appear in the buffer"
15061 )
15062 });
15063}
15064
15065#[gpui::test(iterations = 20, seeds(31))]
15066async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
15067 init_test(cx, |_| {});
15068
15069 let mut cx = EditorLspTestContext::new_rust(
15070 lsp::ServerCapabilities {
15071 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15072 first_trigger_character: ".".to_string(),
15073 more_trigger_character: None,
15074 }),
15075 ..Default::default()
15076 },
15077 cx,
15078 )
15079 .await;
15080
15081 cx.update_buffer(|buffer, _| {
15082 // This causes autoindent to be async.
15083 buffer.set_sync_parse_timeout(Duration::ZERO)
15084 });
15085
15086 cx.set_state("fn c() {\n d()ˇ\n}\n");
15087 cx.simulate_keystroke("\n");
15088 cx.run_until_parked();
15089
15090 let buffer_cloned =
15091 cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap().clone());
15092 let mut request =
15093 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
15094 let buffer_cloned = buffer_cloned.clone();
15095 async move {
15096 buffer_cloned.update(&mut cx, |buffer, _| {
15097 assert_eq!(
15098 buffer.text(),
15099 "fn c() {\n d()\n .\n}\n",
15100 "OnTypeFormatting should triggered after autoindent applied"
15101 )
15102 })?;
15103
15104 Ok(Some(vec![]))
15105 }
15106 });
15107
15108 cx.simulate_keystroke(".");
15109 cx.run_until_parked();
15110
15111 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
15112 assert!(request.next().await.is_some());
15113 request.close();
15114 assert!(request.next().await.is_none());
15115}
15116
15117#[gpui::test]
15118async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
15119 init_test(cx, |_| {});
15120
15121 let fs = FakeFs::new(cx.executor());
15122 fs.insert_tree(
15123 path!("/a"),
15124 json!({
15125 "main.rs": "fn main() { let a = 5; }",
15126 "other.rs": "// Test file",
15127 }),
15128 )
15129 .await;
15130
15131 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15132
15133 let server_restarts = Arc::new(AtomicUsize::new(0));
15134 let closure_restarts = Arc::clone(&server_restarts);
15135 let language_server_name = "test language server";
15136 let language_name: LanguageName = "Rust".into();
15137
15138 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15139 language_registry.add(Arc::new(Language::new(
15140 LanguageConfig {
15141 name: language_name.clone(),
15142 matcher: LanguageMatcher {
15143 path_suffixes: vec!["rs".to_string()],
15144 ..Default::default()
15145 },
15146 ..Default::default()
15147 },
15148 Some(tree_sitter_rust::LANGUAGE.into()),
15149 )));
15150 let mut fake_servers = language_registry.register_fake_lsp(
15151 "Rust",
15152 FakeLspAdapter {
15153 name: language_server_name,
15154 initialization_options: Some(json!({
15155 "testOptionValue": true
15156 })),
15157 initializer: Some(Box::new(move |fake_server| {
15158 let task_restarts = Arc::clone(&closure_restarts);
15159 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
15160 task_restarts.fetch_add(1, atomic::Ordering::Release);
15161 futures::future::ready(Ok(()))
15162 });
15163 })),
15164 ..Default::default()
15165 },
15166 );
15167
15168 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15169 let _buffer = project
15170 .update(cx, |project, cx| {
15171 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
15172 })
15173 .await
15174 .unwrap();
15175 let _fake_server = fake_servers.next().await.unwrap();
15176 update_test_language_settings(cx, |language_settings| {
15177 language_settings.languages.0.insert(
15178 language_name.clone(),
15179 LanguageSettingsContent {
15180 tab_size: NonZeroU32::new(8),
15181 ..Default::default()
15182 },
15183 );
15184 });
15185 cx.executor().run_until_parked();
15186 assert_eq!(
15187 server_restarts.load(atomic::Ordering::Acquire),
15188 0,
15189 "Should not restart LSP server on an unrelated change"
15190 );
15191
15192 update_test_project_settings(cx, |project_settings| {
15193 project_settings.lsp.insert(
15194 "Some other server name".into(),
15195 LspSettings {
15196 binary: None,
15197 settings: None,
15198 initialization_options: Some(json!({
15199 "some other init value": false
15200 })),
15201 enable_lsp_tasks: false,
15202 },
15203 );
15204 });
15205 cx.executor().run_until_parked();
15206 assert_eq!(
15207 server_restarts.load(atomic::Ordering::Acquire),
15208 0,
15209 "Should not restart LSP server on an unrelated LSP settings change"
15210 );
15211
15212 update_test_project_settings(cx, |project_settings| {
15213 project_settings.lsp.insert(
15214 language_server_name.into(),
15215 LspSettings {
15216 binary: None,
15217 settings: None,
15218 initialization_options: Some(json!({
15219 "anotherInitValue": false
15220 })),
15221 enable_lsp_tasks: false,
15222 },
15223 );
15224 });
15225 cx.executor().run_until_parked();
15226 assert_eq!(
15227 server_restarts.load(atomic::Ordering::Acquire),
15228 1,
15229 "Should restart LSP server on a related LSP settings change"
15230 );
15231
15232 update_test_project_settings(cx, |project_settings| {
15233 project_settings.lsp.insert(
15234 language_server_name.into(),
15235 LspSettings {
15236 binary: None,
15237 settings: None,
15238 initialization_options: Some(json!({
15239 "anotherInitValue": false
15240 })),
15241 enable_lsp_tasks: false,
15242 },
15243 );
15244 });
15245 cx.executor().run_until_parked();
15246 assert_eq!(
15247 server_restarts.load(atomic::Ordering::Acquire),
15248 1,
15249 "Should not restart LSP server on a related LSP settings change that is the same"
15250 );
15251
15252 update_test_project_settings(cx, |project_settings| {
15253 project_settings.lsp.insert(
15254 language_server_name.into(),
15255 LspSettings {
15256 binary: None,
15257 settings: None,
15258 initialization_options: None,
15259 enable_lsp_tasks: false,
15260 },
15261 );
15262 });
15263 cx.executor().run_until_parked();
15264 assert_eq!(
15265 server_restarts.load(atomic::Ordering::Acquire),
15266 2,
15267 "Should restart LSP server on another related LSP settings change"
15268 );
15269}
15270
15271#[gpui::test]
15272async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
15273 init_test(cx, |_| {});
15274
15275 let mut cx = EditorLspTestContext::new_rust(
15276 lsp::ServerCapabilities {
15277 completion_provider: Some(lsp::CompletionOptions {
15278 trigger_characters: Some(vec![".".to_string()]),
15279 resolve_provider: Some(true),
15280 ..Default::default()
15281 }),
15282 ..Default::default()
15283 },
15284 cx,
15285 )
15286 .await;
15287
15288 cx.set_state("fn main() { let a = 2ˇ; }");
15289 cx.simulate_keystroke(".");
15290 let completion_item = lsp::CompletionItem {
15291 label: "some".into(),
15292 kind: Some(lsp::CompletionItemKind::SNIPPET),
15293 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15294 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15295 kind: lsp::MarkupKind::Markdown,
15296 value: "```rust\nSome(2)\n```".to_string(),
15297 })),
15298 deprecated: Some(false),
15299 sort_text: Some("fffffff2".to_string()),
15300 filter_text: Some("some".to_string()),
15301 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15302 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15303 range: lsp::Range {
15304 start: lsp::Position {
15305 line: 0,
15306 character: 22,
15307 },
15308 end: lsp::Position {
15309 line: 0,
15310 character: 22,
15311 },
15312 },
15313 new_text: "Some(2)".to_string(),
15314 })),
15315 additional_text_edits: Some(vec![lsp::TextEdit {
15316 range: lsp::Range {
15317 start: lsp::Position {
15318 line: 0,
15319 character: 20,
15320 },
15321 end: lsp::Position {
15322 line: 0,
15323 character: 22,
15324 },
15325 },
15326 new_text: "".to_string(),
15327 }]),
15328 ..Default::default()
15329 };
15330
15331 let closure_completion_item = completion_item.clone();
15332 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15333 let task_completion_item = closure_completion_item.clone();
15334 async move {
15335 Ok(Some(lsp::CompletionResponse::Array(vec![
15336 task_completion_item,
15337 ])))
15338 }
15339 });
15340
15341 request.next().await;
15342
15343 cx.condition(|editor, _| editor.context_menu_visible())
15344 .await;
15345 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15346 editor
15347 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15348 .unwrap()
15349 });
15350 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
15351
15352 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
15353 let task_completion_item = completion_item.clone();
15354 async move { Ok(task_completion_item) }
15355 })
15356 .next()
15357 .await
15358 .unwrap();
15359 apply_additional_edits.await.unwrap();
15360 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
15361}
15362
15363#[gpui::test]
15364async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
15365 init_test(cx, |_| {});
15366
15367 let mut cx = EditorLspTestContext::new_rust(
15368 lsp::ServerCapabilities {
15369 completion_provider: Some(lsp::CompletionOptions {
15370 trigger_characters: Some(vec![".".to_string()]),
15371 resolve_provider: Some(true),
15372 ..Default::default()
15373 }),
15374 ..Default::default()
15375 },
15376 cx,
15377 )
15378 .await;
15379
15380 cx.set_state("fn main() { let a = 2ˇ; }");
15381 cx.simulate_keystroke(".");
15382
15383 let item1 = lsp::CompletionItem {
15384 label: "method id()".to_string(),
15385 filter_text: Some("id".to_string()),
15386 detail: None,
15387 documentation: None,
15388 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15389 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15390 new_text: ".id".to_string(),
15391 })),
15392 ..lsp::CompletionItem::default()
15393 };
15394
15395 let item2 = lsp::CompletionItem {
15396 label: "other".to_string(),
15397 filter_text: Some("other".to_string()),
15398 detail: None,
15399 documentation: None,
15400 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15401 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15402 new_text: ".other".to_string(),
15403 })),
15404 ..lsp::CompletionItem::default()
15405 };
15406
15407 let item1 = item1.clone();
15408 cx.set_request_handler::<lsp::request::Completion, _, _>({
15409 let item1 = item1.clone();
15410 move |_, _, _| {
15411 let item1 = item1.clone();
15412 let item2 = item2.clone();
15413 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
15414 }
15415 })
15416 .next()
15417 .await;
15418
15419 cx.condition(|editor, _| editor.context_menu_visible())
15420 .await;
15421 cx.update_editor(|editor, _, _| {
15422 let context_menu = editor.context_menu.borrow_mut();
15423 let context_menu = context_menu
15424 .as_ref()
15425 .expect("Should have the context menu deployed");
15426 match context_menu {
15427 CodeContextMenu::Completions(completions_menu) => {
15428 let completions = completions_menu.completions.borrow_mut();
15429 assert_eq!(
15430 completions
15431 .iter()
15432 .map(|completion| &completion.label.text)
15433 .collect::<Vec<_>>(),
15434 vec!["method id()", "other"]
15435 )
15436 }
15437 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15438 }
15439 });
15440
15441 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
15442 let item1 = item1.clone();
15443 move |_, item_to_resolve, _| {
15444 let item1 = item1.clone();
15445 async move {
15446 if item1 == item_to_resolve {
15447 Ok(lsp::CompletionItem {
15448 label: "method id()".to_string(),
15449 filter_text: Some("id".to_string()),
15450 detail: Some("Now resolved!".to_string()),
15451 documentation: Some(lsp::Documentation::String("Docs".to_string())),
15452 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15453 range: lsp::Range::new(
15454 lsp::Position::new(0, 22),
15455 lsp::Position::new(0, 22),
15456 ),
15457 new_text: ".id".to_string(),
15458 })),
15459 ..lsp::CompletionItem::default()
15460 })
15461 } else {
15462 Ok(item_to_resolve)
15463 }
15464 }
15465 }
15466 })
15467 .next()
15468 .await
15469 .unwrap();
15470 cx.run_until_parked();
15471
15472 cx.update_editor(|editor, window, cx| {
15473 editor.context_menu_next(&Default::default(), window, cx);
15474 });
15475
15476 cx.update_editor(|editor, _, _| {
15477 let context_menu = editor.context_menu.borrow_mut();
15478 let context_menu = context_menu
15479 .as_ref()
15480 .expect("Should have the context menu deployed");
15481 match context_menu {
15482 CodeContextMenu::Completions(completions_menu) => {
15483 let completions = completions_menu.completions.borrow_mut();
15484 assert_eq!(
15485 completions
15486 .iter()
15487 .map(|completion| &completion.label.text)
15488 .collect::<Vec<_>>(),
15489 vec!["method id() Now resolved!", "other"],
15490 "Should update first completion label, but not second as the filter text did not match."
15491 );
15492 }
15493 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15494 }
15495 });
15496}
15497
15498#[gpui::test]
15499async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
15500 init_test(cx, |_| {});
15501 let mut cx = EditorLspTestContext::new_rust(
15502 lsp::ServerCapabilities {
15503 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
15504 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
15505 completion_provider: Some(lsp::CompletionOptions {
15506 resolve_provider: Some(true),
15507 ..Default::default()
15508 }),
15509 ..Default::default()
15510 },
15511 cx,
15512 )
15513 .await;
15514 cx.set_state(indoc! {"
15515 struct TestStruct {
15516 field: i32
15517 }
15518
15519 fn mainˇ() {
15520 let unused_var = 42;
15521 let test_struct = TestStruct { field: 42 };
15522 }
15523 "});
15524 let symbol_range = cx.lsp_range(indoc! {"
15525 struct TestStruct {
15526 field: i32
15527 }
15528
15529 «fn main»() {
15530 let unused_var = 42;
15531 let test_struct = TestStruct { field: 42 };
15532 }
15533 "});
15534 let mut hover_requests =
15535 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
15536 Ok(Some(lsp::Hover {
15537 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
15538 kind: lsp::MarkupKind::Markdown,
15539 value: "Function documentation".to_string(),
15540 }),
15541 range: Some(symbol_range),
15542 }))
15543 });
15544
15545 // Case 1: Test that code action menu hide hover popover
15546 cx.dispatch_action(Hover);
15547 hover_requests.next().await;
15548 cx.condition(|editor, _| editor.hover_state.visible()).await;
15549 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
15550 move |_, _, _| async move {
15551 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
15552 lsp::CodeAction {
15553 title: "Remove unused variable".to_string(),
15554 kind: Some(CodeActionKind::QUICKFIX),
15555 edit: Some(lsp::WorkspaceEdit {
15556 changes: Some(
15557 [(
15558 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
15559 vec![lsp::TextEdit {
15560 range: lsp::Range::new(
15561 lsp::Position::new(5, 4),
15562 lsp::Position::new(5, 27),
15563 ),
15564 new_text: "".to_string(),
15565 }],
15566 )]
15567 .into_iter()
15568 .collect(),
15569 ),
15570 ..Default::default()
15571 }),
15572 ..Default::default()
15573 },
15574 )]))
15575 },
15576 );
15577 cx.update_editor(|editor, window, cx| {
15578 editor.toggle_code_actions(
15579 &ToggleCodeActions {
15580 deployed_from: None,
15581 quick_launch: false,
15582 },
15583 window,
15584 cx,
15585 );
15586 });
15587 code_action_requests.next().await;
15588 cx.run_until_parked();
15589 cx.condition(|editor, _| editor.context_menu_visible())
15590 .await;
15591 cx.update_editor(|editor, _, _| {
15592 assert!(
15593 !editor.hover_state.visible(),
15594 "Hover popover should be hidden when code action menu is shown"
15595 );
15596 // Hide code actions
15597 editor.context_menu.take();
15598 });
15599
15600 // Case 2: Test that code completions hide hover popover
15601 cx.dispatch_action(Hover);
15602 hover_requests.next().await;
15603 cx.condition(|editor, _| editor.hover_state.visible()).await;
15604 let counter = Arc::new(AtomicUsize::new(0));
15605 let mut completion_requests =
15606 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15607 let counter = counter.clone();
15608 async move {
15609 counter.fetch_add(1, atomic::Ordering::Release);
15610 Ok(Some(lsp::CompletionResponse::Array(vec![
15611 lsp::CompletionItem {
15612 label: "main".into(),
15613 kind: Some(lsp::CompletionItemKind::FUNCTION),
15614 detail: Some("() -> ()".to_string()),
15615 ..Default::default()
15616 },
15617 lsp::CompletionItem {
15618 label: "TestStruct".into(),
15619 kind: Some(lsp::CompletionItemKind::STRUCT),
15620 detail: Some("struct TestStruct".to_string()),
15621 ..Default::default()
15622 },
15623 ])))
15624 }
15625 });
15626 cx.update_editor(|editor, window, cx| {
15627 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15628 });
15629 completion_requests.next().await;
15630 cx.condition(|editor, _| editor.context_menu_visible())
15631 .await;
15632 cx.update_editor(|editor, _, _| {
15633 assert!(
15634 !editor.hover_state.visible(),
15635 "Hover popover should be hidden when completion menu is shown"
15636 );
15637 });
15638}
15639
15640#[gpui::test]
15641async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
15642 init_test(cx, |_| {});
15643
15644 let mut cx = EditorLspTestContext::new_rust(
15645 lsp::ServerCapabilities {
15646 completion_provider: Some(lsp::CompletionOptions {
15647 trigger_characters: Some(vec![".".to_string()]),
15648 resolve_provider: Some(true),
15649 ..Default::default()
15650 }),
15651 ..Default::default()
15652 },
15653 cx,
15654 )
15655 .await;
15656
15657 cx.set_state("fn main() { let a = 2ˇ; }");
15658 cx.simulate_keystroke(".");
15659
15660 let unresolved_item_1 = lsp::CompletionItem {
15661 label: "id".to_string(),
15662 filter_text: Some("id".to_string()),
15663 detail: None,
15664 documentation: None,
15665 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15666 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15667 new_text: ".id".to_string(),
15668 })),
15669 ..lsp::CompletionItem::default()
15670 };
15671 let resolved_item_1 = lsp::CompletionItem {
15672 additional_text_edits: Some(vec![lsp::TextEdit {
15673 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15674 new_text: "!!".to_string(),
15675 }]),
15676 ..unresolved_item_1.clone()
15677 };
15678 let unresolved_item_2 = lsp::CompletionItem {
15679 label: "other".to_string(),
15680 filter_text: Some("other".to_string()),
15681 detail: None,
15682 documentation: None,
15683 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15684 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15685 new_text: ".other".to_string(),
15686 })),
15687 ..lsp::CompletionItem::default()
15688 };
15689 let resolved_item_2 = lsp::CompletionItem {
15690 additional_text_edits: Some(vec![lsp::TextEdit {
15691 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15692 new_text: "??".to_string(),
15693 }]),
15694 ..unresolved_item_2.clone()
15695 };
15696
15697 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
15698 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
15699 cx.lsp
15700 .server
15701 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15702 let unresolved_item_1 = unresolved_item_1.clone();
15703 let resolved_item_1 = resolved_item_1.clone();
15704 let unresolved_item_2 = unresolved_item_2.clone();
15705 let resolved_item_2 = resolved_item_2.clone();
15706 let resolve_requests_1 = resolve_requests_1.clone();
15707 let resolve_requests_2 = resolve_requests_2.clone();
15708 move |unresolved_request, _| {
15709 let unresolved_item_1 = unresolved_item_1.clone();
15710 let resolved_item_1 = resolved_item_1.clone();
15711 let unresolved_item_2 = unresolved_item_2.clone();
15712 let resolved_item_2 = resolved_item_2.clone();
15713 let resolve_requests_1 = resolve_requests_1.clone();
15714 let resolve_requests_2 = resolve_requests_2.clone();
15715 async move {
15716 if unresolved_request == unresolved_item_1 {
15717 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
15718 Ok(resolved_item_1.clone())
15719 } else if unresolved_request == unresolved_item_2 {
15720 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
15721 Ok(resolved_item_2.clone())
15722 } else {
15723 panic!("Unexpected completion item {unresolved_request:?}")
15724 }
15725 }
15726 }
15727 })
15728 .detach();
15729
15730 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15731 let unresolved_item_1 = unresolved_item_1.clone();
15732 let unresolved_item_2 = unresolved_item_2.clone();
15733 async move {
15734 Ok(Some(lsp::CompletionResponse::Array(vec![
15735 unresolved_item_1,
15736 unresolved_item_2,
15737 ])))
15738 }
15739 })
15740 .next()
15741 .await;
15742
15743 cx.condition(|editor, _| editor.context_menu_visible())
15744 .await;
15745 cx.update_editor(|editor, _, _| {
15746 let context_menu = editor.context_menu.borrow_mut();
15747 let context_menu = context_menu
15748 .as_ref()
15749 .expect("Should have the context menu deployed");
15750 match context_menu {
15751 CodeContextMenu::Completions(completions_menu) => {
15752 let completions = completions_menu.completions.borrow_mut();
15753 assert_eq!(
15754 completions
15755 .iter()
15756 .map(|completion| &completion.label.text)
15757 .collect::<Vec<_>>(),
15758 vec!["id", "other"]
15759 )
15760 }
15761 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15762 }
15763 });
15764 cx.run_until_parked();
15765
15766 cx.update_editor(|editor, window, cx| {
15767 editor.context_menu_next(&ContextMenuNext, window, cx);
15768 });
15769 cx.run_until_parked();
15770 cx.update_editor(|editor, window, cx| {
15771 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15772 });
15773 cx.run_until_parked();
15774 cx.update_editor(|editor, window, cx| {
15775 editor.context_menu_next(&ContextMenuNext, window, cx);
15776 });
15777 cx.run_until_parked();
15778 cx.update_editor(|editor, window, cx| {
15779 editor
15780 .compose_completion(&ComposeCompletion::default(), window, cx)
15781 .expect("No task returned")
15782 })
15783 .await
15784 .expect("Completion failed");
15785 cx.run_until_parked();
15786
15787 cx.update_editor(|editor, _, cx| {
15788 assert_eq!(
15789 resolve_requests_1.load(atomic::Ordering::Acquire),
15790 1,
15791 "Should always resolve once despite multiple selections"
15792 );
15793 assert_eq!(
15794 resolve_requests_2.load(atomic::Ordering::Acquire),
15795 1,
15796 "Should always resolve once after multiple selections and applying the completion"
15797 );
15798 assert_eq!(
15799 editor.text(cx),
15800 "fn main() { let a = ??.other; }",
15801 "Should use resolved data when applying the completion"
15802 );
15803 });
15804}
15805
15806#[gpui::test]
15807async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
15808 init_test(cx, |_| {});
15809
15810 let item_0 = lsp::CompletionItem {
15811 label: "abs".into(),
15812 insert_text: Some("abs".into()),
15813 data: Some(json!({ "very": "special"})),
15814 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
15815 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
15816 lsp::InsertReplaceEdit {
15817 new_text: "abs".to_string(),
15818 insert: lsp::Range::default(),
15819 replace: lsp::Range::default(),
15820 },
15821 )),
15822 ..lsp::CompletionItem::default()
15823 };
15824 let items = iter::once(item_0.clone())
15825 .chain((11..51).map(|i| lsp::CompletionItem {
15826 label: format!("item_{}", i),
15827 insert_text: Some(format!("item_{}", i)),
15828 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15829 ..lsp::CompletionItem::default()
15830 }))
15831 .collect::<Vec<_>>();
15832
15833 let default_commit_characters = vec!["?".to_string()];
15834 let default_data = json!({ "default": "data"});
15835 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
15836 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
15837 let default_edit_range = lsp::Range {
15838 start: lsp::Position {
15839 line: 0,
15840 character: 5,
15841 },
15842 end: lsp::Position {
15843 line: 0,
15844 character: 5,
15845 },
15846 };
15847
15848 let mut cx = EditorLspTestContext::new_rust(
15849 lsp::ServerCapabilities {
15850 completion_provider: Some(lsp::CompletionOptions {
15851 trigger_characters: Some(vec![".".to_string()]),
15852 resolve_provider: Some(true),
15853 ..Default::default()
15854 }),
15855 ..Default::default()
15856 },
15857 cx,
15858 )
15859 .await;
15860
15861 cx.set_state("fn main() { let a = 2ˇ; }");
15862 cx.simulate_keystroke(".");
15863
15864 let completion_data = default_data.clone();
15865 let completion_characters = default_commit_characters.clone();
15866 let completion_items = items.clone();
15867 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15868 let default_data = completion_data.clone();
15869 let default_commit_characters = completion_characters.clone();
15870 let items = completion_items.clone();
15871 async move {
15872 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15873 items,
15874 item_defaults: Some(lsp::CompletionListItemDefaults {
15875 data: Some(default_data.clone()),
15876 commit_characters: Some(default_commit_characters.clone()),
15877 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
15878 default_edit_range,
15879 )),
15880 insert_text_format: Some(default_insert_text_format),
15881 insert_text_mode: Some(default_insert_text_mode),
15882 }),
15883 ..lsp::CompletionList::default()
15884 })))
15885 }
15886 })
15887 .next()
15888 .await;
15889
15890 let resolved_items = Arc::new(Mutex::new(Vec::new()));
15891 cx.lsp
15892 .server
15893 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15894 let closure_resolved_items = resolved_items.clone();
15895 move |item_to_resolve, _| {
15896 let closure_resolved_items = closure_resolved_items.clone();
15897 async move {
15898 closure_resolved_items.lock().push(item_to_resolve.clone());
15899 Ok(item_to_resolve)
15900 }
15901 }
15902 })
15903 .detach();
15904
15905 cx.condition(|editor, _| editor.context_menu_visible())
15906 .await;
15907 cx.run_until_parked();
15908 cx.update_editor(|editor, _, _| {
15909 let menu = editor.context_menu.borrow_mut();
15910 match menu.as_ref().expect("should have the completions menu") {
15911 CodeContextMenu::Completions(completions_menu) => {
15912 assert_eq!(
15913 completions_menu
15914 .entries
15915 .borrow()
15916 .iter()
15917 .map(|mat| mat.string.clone())
15918 .collect::<Vec<String>>(),
15919 items
15920 .iter()
15921 .map(|completion| completion.label.clone())
15922 .collect::<Vec<String>>()
15923 );
15924 }
15925 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
15926 }
15927 });
15928 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
15929 // with 4 from the end.
15930 assert_eq!(
15931 *resolved_items.lock(),
15932 [&items[0..16], &items[items.len() - 4..items.len()]]
15933 .concat()
15934 .iter()
15935 .cloned()
15936 .map(|mut item| {
15937 if item.data.is_none() {
15938 item.data = Some(default_data.clone());
15939 }
15940 item
15941 })
15942 .collect::<Vec<lsp::CompletionItem>>(),
15943 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
15944 );
15945 resolved_items.lock().clear();
15946
15947 cx.update_editor(|editor, window, cx| {
15948 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15949 });
15950 cx.run_until_parked();
15951 // Completions that have already been resolved are skipped.
15952 assert_eq!(
15953 *resolved_items.lock(),
15954 items[items.len() - 17..items.len() - 4]
15955 .iter()
15956 .cloned()
15957 .map(|mut item| {
15958 if item.data.is_none() {
15959 item.data = Some(default_data.clone());
15960 }
15961 item
15962 })
15963 .collect::<Vec<lsp::CompletionItem>>()
15964 );
15965 resolved_items.lock().clear();
15966}
15967
15968#[gpui::test]
15969async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
15970 init_test(cx, |_| {});
15971
15972 let mut cx = EditorLspTestContext::new(
15973 Language::new(
15974 LanguageConfig {
15975 matcher: LanguageMatcher {
15976 path_suffixes: vec!["jsx".into()],
15977 ..Default::default()
15978 },
15979 overrides: [(
15980 "element".into(),
15981 LanguageConfigOverride {
15982 completion_query_characters: Override::Set(['-'].into_iter().collect()),
15983 ..Default::default()
15984 },
15985 )]
15986 .into_iter()
15987 .collect(),
15988 ..Default::default()
15989 },
15990 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15991 )
15992 .with_override_query("(jsx_self_closing_element) @element")
15993 .unwrap(),
15994 lsp::ServerCapabilities {
15995 completion_provider: Some(lsp::CompletionOptions {
15996 trigger_characters: Some(vec![":".to_string()]),
15997 ..Default::default()
15998 }),
15999 ..Default::default()
16000 },
16001 cx,
16002 )
16003 .await;
16004
16005 cx.lsp
16006 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16007 Ok(Some(lsp::CompletionResponse::Array(vec![
16008 lsp::CompletionItem {
16009 label: "bg-blue".into(),
16010 ..Default::default()
16011 },
16012 lsp::CompletionItem {
16013 label: "bg-red".into(),
16014 ..Default::default()
16015 },
16016 lsp::CompletionItem {
16017 label: "bg-yellow".into(),
16018 ..Default::default()
16019 },
16020 ])))
16021 });
16022
16023 cx.set_state(r#"<p class="bgˇ" />"#);
16024
16025 // Trigger completion when typing a dash, because the dash is an extra
16026 // word character in the 'element' scope, which contains the cursor.
16027 cx.simulate_keystroke("-");
16028 cx.executor().run_until_parked();
16029 cx.update_editor(|editor, _, _| {
16030 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16031 {
16032 assert_eq!(
16033 completion_menu_entries(&menu),
16034 &["bg-blue", "bg-red", "bg-yellow"]
16035 );
16036 } else {
16037 panic!("expected completion menu to be open");
16038 }
16039 });
16040
16041 cx.simulate_keystroke("l");
16042 cx.executor().run_until_parked();
16043 cx.update_editor(|editor, _, _| {
16044 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16045 {
16046 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
16047 } else {
16048 panic!("expected completion menu to be open");
16049 }
16050 });
16051
16052 // When filtering completions, consider the character after the '-' to
16053 // be the start of a subword.
16054 cx.set_state(r#"<p class="yelˇ" />"#);
16055 cx.simulate_keystroke("l");
16056 cx.executor().run_until_parked();
16057 cx.update_editor(|editor, _, _| {
16058 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16059 {
16060 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
16061 } else {
16062 panic!("expected completion menu to be open");
16063 }
16064 });
16065}
16066
16067fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
16068 let entries = menu.entries.borrow();
16069 entries.iter().map(|mat| mat.string.clone()).collect()
16070}
16071
16072#[gpui::test]
16073async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
16074 init_test(cx, |settings| {
16075 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
16076 Formatter::Prettier,
16077 )))
16078 });
16079
16080 let fs = FakeFs::new(cx.executor());
16081 fs.insert_file(path!("/file.ts"), Default::default()).await;
16082
16083 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
16084 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16085
16086 language_registry.add(Arc::new(Language::new(
16087 LanguageConfig {
16088 name: "TypeScript".into(),
16089 matcher: LanguageMatcher {
16090 path_suffixes: vec!["ts".to_string()],
16091 ..Default::default()
16092 },
16093 ..Default::default()
16094 },
16095 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
16096 )));
16097 update_test_language_settings(cx, |settings| {
16098 settings.defaults.prettier = Some(PrettierSettings {
16099 allowed: true,
16100 ..PrettierSettings::default()
16101 });
16102 });
16103
16104 let test_plugin = "test_plugin";
16105 let _ = language_registry.register_fake_lsp(
16106 "TypeScript",
16107 FakeLspAdapter {
16108 prettier_plugins: vec![test_plugin],
16109 ..Default::default()
16110 },
16111 );
16112
16113 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
16114 let buffer = project
16115 .update(cx, |project, cx| {
16116 project.open_local_buffer(path!("/file.ts"), cx)
16117 })
16118 .await
16119 .unwrap();
16120
16121 let buffer_text = "one\ntwo\nthree\n";
16122 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16123 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16124 editor.update_in(cx, |editor, window, cx| {
16125 editor.set_text(buffer_text, window, cx)
16126 });
16127
16128 editor
16129 .update_in(cx, |editor, window, cx| {
16130 editor.perform_format(
16131 project.clone(),
16132 FormatTrigger::Manual,
16133 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16134 window,
16135 cx,
16136 )
16137 })
16138 .unwrap()
16139 .await;
16140 assert_eq!(
16141 editor.update(cx, |editor, cx| editor.text(cx)),
16142 buffer_text.to_string() + prettier_format_suffix,
16143 "Test prettier formatting was not applied to the original buffer text",
16144 );
16145
16146 update_test_language_settings(cx, |settings| {
16147 settings.defaults.formatter = Some(SelectedFormatter::Auto)
16148 });
16149 let format = editor.update_in(cx, |editor, window, cx| {
16150 editor.perform_format(
16151 project.clone(),
16152 FormatTrigger::Manual,
16153 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16154 window,
16155 cx,
16156 )
16157 });
16158 format.await.unwrap();
16159 assert_eq!(
16160 editor.update(cx, |editor, cx| editor.text(cx)),
16161 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
16162 "Autoformatting (via test prettier) was not applied to the original buffer text",
16163 );
16164}
16165
16166#[gpui::test]
16167async fn test_addition_reverts(cx: &mut TestAppContext) {
16168 init_test(cx, |_| {});
16169 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16170 let base_text = indoc! {r#"
16171 struct Row;
16172 struct Row1;
16173 struct Row2;
16174
16175 struct Row4;
16176 struct Row5;
16177 struct Row6;
16178
16179 struct Row8;
16180 struct Row9;
16181 struct Row10;"#};
16182
16183 // When addition hunks are not adjacent to carets, no hunk revert is performed
16184 assert_hunk_revert(
16185 indoc! {r#"struct Row;
16186 struct Row1;
16187 struct Row1.1;
16188 struct Row1.2;
16189 struct Row2;ˇ
16190
16191 struct Row4;
16192 struct Row5;
16193 struct Row6;
16194
16195 struct Row8;
16196 ˇstruct Row9;
16197 struct Row9.1;
16198 struct Row9.2;
16199 struct Row9.3;
16200 struct Row10;"#},
16201 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16202 indoc! {r#"struct Row;
16203 struct Row1;
16204 struct Row1.1;
16205 struct Row1.2;
16206 struct Row2;ˇ
16207
16208 struct Row4;
16209 struct Row5;
16210 struct Row6;
16211
16212 struct Row8;
16213 ˇstruct Row9;
16214 struct Row9.1;
16215 struct Row9.2;
16216 struct Row9.3;
16217 struct Row10;"#},
16218 base_text,
16219 &mut cx,
16220 );
16221 // Same for selections
16222 assert_hunk_revert(
16223 indoc! {r#"struct Row;
16224 struct Row1;
16225 struct Row2;
16226 struct Row2.1;
16227 struct Row2.2;
16228 «ˇ
16229 struct Row4;
16230 struct» Row5;
16231 «struct Row6;
16232 ˇ»
16233 struct Row9.1;
16234 struct Row9.2;
16235 struct Row9.3;
16236 struct Row8;
16237 struct Row9;
16238 struct Row10;"#},
16239 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16240 indoc! {r#"struct Row;
16241 struct Row1;
16242 struct Row2;
16243 struct Row2.1;
16244 struct Row2.2;
16245 «ˇ
16246 struct Row4;
16247 struct» Row5;
16248 «struct Row6;
16249 ˇ»
16250 struct Row9.1;
16251 struct Row9.2;
16252 struct Row9.3;
16253 struct Row8;
16254 struct Row9;
16255 struct Row10;"#},
16256 base_text,
16257 &mut cx,
16258 );
16259
16260 // When carets and selections intersect the addition hunks, those are reverted.
16261 // Adjacent carets got merged.
16262 assert_hunk_revert(
16263 indoc! {r#"struct Row;
16264 ˇ// something on the top
16265 struct Row1;
16266 struct Row2;
16267 struct Roˇw3.1;
16268 struct Row2.2;
16269 struct Row2.3;ˇ
16270
16271 struct Row4;
16272 struct ˇRow5.1;
16273 struct Row5.2;
16274 struct «Rowˇ»5.3;
16275 struct Row5;
16276 struct Row6;
16277 ˇ
16278 struct Row9.1;
16279 struct «Rowˇ»9.2;
16280 struct «ˇRow»9.3;
16281 struct Row8;
16282 struct Row9;
16283 «ˇ// something on bottom»
16284 struct Row10;"#},
16285 vec![
16286 DiffHunkStatusKind::Added,
16287 DiffHunkStatusKind::Added,
16288 DiffHunkStatusKind::Added,
16289 DiffHunkStatusKind::Added,
16290 DiffHunkStatusKind::Added,
16291 ],
16292 indoc! {r#"struct Row;
16293 ˇstruct Row1;
16294 struct Row2;
16295 ˇ
16296 struct Row4;
16297 ˇstruct Row5;
16298 struct Row6;
16299 ˇ
16300 ˇstruct Row8;
16301 struct Row9;
16302 ˇstruct Row10;"#},
16303 base_text,
16304 &mut cx,
16305 );
16306}
16307
16308#[gpui::test]
16309async fn test_modification_reverts(cx: &mut TestAppContext) {
16310 init_test(cx, |_| {});
16311 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16312 let base_text = indoc! {r#"
16313 struct Row;
16314 struct Row1;
16315 struct Row2;
16316
16317 struct Row4;
16318 struct Row5;
16319 struct Row6;
16320
16321 struct Row8;
16322 struct Row9;
16323 struct Row10;"#};
16324
16325 // Modification hunks behave the same as the addition ones.
16326 assert_hunk_revert(
16327 indoc! {r#"struct Row;
16328 struct Row1;
16329 struct Row33;
16330 ˇ
16331 struct Row4;
16332 struct Row5;
16333 struct Row6;
16334 ˇ
16335 struct Row99;
16336 struct Row9;
16337 struct Row10;"#},
16338 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16339 indoc! {r#"struct Row;
16340 struct Row1;
16341 struct Row33;
16342 ˇ
16343 struct Row4;
16344 struct Row5;
16345 struct Row6;
16346 ˇ
16347 struct Row99;
16348 struct Row9;
16349 struct Row10;"#},
16350 base_text,
16351 &mut cx,
16352 );
16353 assert_hunk_revert(
16354 indoc! {r#"struct Row;
16355 struct Row1;
16356 struct Row33;
16357 «ˇ
16358 struct Row4;
16359 struct» Row5;
16360 «struct Row6;
16361 ˇ»
16362 struct Row99;
16363 struct Row9;
16364 struct Row10;"#},
16365 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16366 indoc! {r#"struct Row;
16367 struct Row1;
16368 struct Row33;
16369 «ˇ
16370 struct Row4;
16371 struct» Row5;
16372 «struct Row6;
16373 ˇ»
16374 struct Row99;
16375 struct Row9;
16376 struct Row10;"#},
16377 base_text,
16378 &mut cx,
16379 );
16380
16381 assert_hunk_revert(
16382 indoc! {r#"ˇstruct Row1.1;
16383 struct Row1;
16384 «ˇstr»uct Row22;
16385
16386 struct ˇRow44;
16387 struct Row5;
16388 struct «Rˇ»ow66;ˇ
16389
16390 «struˇ»ct Row88;
16391 struct Row9;
16392 struct Row1011;ˇ"#},
16393 vec![
16394 DiffHunkStatusKind::Modified,
16395 DiffHunkStatusKind::Modified,
16396 DiffHunkStatusKind::Modified,
16397 DiffHunkStatusKind::Modified,
16398 DiffHunkStatusKind::Modified,
16399 DiffHunkStatusKind::Modified,
16400 ],
16401 indoc! {r#"struct Row;
16402 ˇstruct Row1;
16403 struct Row2;
16404 ˇ
16405 struct Row4;
16406 ˇstruct Row5;
16407 struct Row6;
16408 ˇ
16409 struct Row8;
16410 ˇstruct Row9;
16411 struct Row10;ˇ"#},
16412 base_text,
16413 &mut cx,
16414 );
16415}
16416
16417#[gpui::test]
16418async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
16419 init_test(cx, |_| {});
16420 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16421 let base_text = indoc! {r#"
16422 one
16423
16424 two
16425 three
16426 "#};
16427
16428 cx.set_head_text(base_text);
16429 cx.set_state("\nˇ\n");
16430 cx.executor().run_until_parked();
16431 cx.update_editor(|editor, _window, cx| {
16432 editor.expand_selected_diff_hunks(cx);
16433 });
16434 cx.executor().run_until_parked();
16435 cx.update_editor(|editor, window, cx| {
16436 editor.backspace(&Default::default(), window, cx);
16437 });
16438 cx.run_until_parked();
16439 cx.assert_state_with_diff(
16440 indoc! {r#"
16441
16442 - two
16443 - threeˇ
16444 +
16445 "#}
16446 .to_string(),
16447 );
16448}
16449
16450#[gpui::test]
16451async fn test_deletion_reverts(cx: &mut TestAppContext) {
16452 init_test(cx, |_| {});
16453 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16454 let base_text = indoc! {r#"struct Row;
16455struct Row1;
16456struct Row2;
16457
16458struct Row4;
16459struct Row5;
16460struct Row6;
16461
16462struct Row8;
16463struct Row9;
16464struct Row10;"#};
16465
16466 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
16467 assert_hunk_revert(
16468 indoc! {r#"struct Row;
16469 struct Row2;
16470
16471 ˇstruct Row4;
16472 struct Row5;
16473 struct Row6;
16474 ˇ
16475 struct Row8;
16476 struct Row10;"#},
16477 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16478 indoc! {r#"struct Row;
16479 struct Row2;
16480
16481 ˇstruct Row4;
16482 struct Row5;
16483 struct Row6;
16484 ˇ
16485 struct Row8;
16486 struct Row10;"#},
16487 base_text,
16488 &mut cx,
16489 );
16490 assert_hunk_revert(
16491 indoc! {r#"struct Row;
16492 struct Row2;
16493
16494 «ˇstruct Row4;
16495 struct» Row5;
16496 «struct Row6;
16497 ˇ»
16498 struct Row8;
16499 struct Row10;"#},
16500 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16501 indoc! {r#"struct Row;
16502 struct Row2;
16503
16504 «ˇstruct Row4;
16505 struct» Row5;
16506 «struct Row6;
16507 ˇ»
16508 struct Row8;
16509 struct Row10;"#},
16510 base_text,
16511 &mut cx,
16512 );
16513
16514 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
16515 assert_hunk_revert(
16516 indoc! {r#"struct Row;
16517 ˇstruct Row2;
16518
16519 struct Row4;
16520 struct Row5;
16521 struct Row6;
16522
16523 struct Row8;ˇ
16524 struct Row10;"#},
16525 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16526 indoc! {r#"struct Row;
16527 struct Row1;
16528 ˇstruct Row2;
16529
16530 struct Row4;
16531 struct Row5;
16532 struct Row6;
16533
16534 struct Row8;ˇ
16535 struct Row9;
16536 struct Row10;"#},
16537 base_text,
16538 &mut cx,
16539 );
16540 assert_hunk_revert(
16541 indoc! {r#"struct Row;
16542 struct Row2«ˇ;
16543 struct Row4;
16544 struct» Row5;
16545 «struct Row6;
16546
16547 struct Row8;ˇ»
16548 struct Row10;"#},
16549 vec![
16550 DiffHunkStatusKind::Deleted,
16551 DiffHunkStatusKind::Deleted,
16552 DiffHunkStatusKind::Deleted,
16553 ],
16554 indoc! {r#"struct Row;
16555 struct Row1;
16556 struct Row2«ˇ;
16557
16558 struct Row4;
16559 struct» Row5;
16560 «struct Row6;
16561
16562 struct Row8;ˇ»
16563 struct Row9;
16564 struct Row10;"#},
16565 base_text,
16566 &mut cx,
16567 );
16568}
16569
16570#[gpui::test]
16571async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
16572 init_test(cx, |_| {});
16573
16574 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
16575 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
16576 let base_text_3 =
16577 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
16578
16579 let text_1 = edit_first_char_of_every_line(base_text_1);
16580 let text_2 = edit_first_char_of_every_line(base_text_2);
16581 let text_3 = edit_first_char_of_every_line(base_text_3);
16582
16583 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
16584 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
16585 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
16586
16587 let multibuffer = cx.new(|cx| {
16588 let mut multibuffer = MultiBuffer::new(ReadWrite);
16589 multibuffer.push_excerpts(
16590 buffer_1.clone(),
16591 [
16592 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16593 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16594 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16595 ],
16596 cx,
16597 );
16598 multibuffer.push_excerpts(
16599 buffer_2.clone(),
16600 [
16601 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16602 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16603 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16604 ],
16605 cx,
16606 );
16607 multibuffer.push_excerpts(
16608 buffer_3.clone(),
16609 [
16610 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16611 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16612 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16613 ],
16614 cx,
16615 );
16616 multibuffer
16617 });
16618
16619 let fs = FakeFs::new(cx.executor());
16620 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
16621 let (editor, cx) = cx
16622 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
16623 editor.update_in(cx, |editor, _window, cx| {
16624 for (buffer, diff_base) in [
16625 (buffer_1.clone(), base_text_1),
16626 (buffer_2.clone(), base_text_2),
16627 (buffer_3.clone(), base_text_3),
16628 ] {
16629 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16630 editor
16631 .buffer
16632 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16633 }
16634 });
16635 cx.executor().run_until_parked();
16636
16637 editor.update_in(cx, |editor, window, cx| {
16638 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}");
16639 editor.select_all(&SelectAll, window, cx);
16640 editor.git_restore(&Default::default(), window, cx);
16641 });
16642 cx.executor().run_until_parked();
16643
16644 // When all ranges are selected, all buffer hunks are reverted.
16645 editor.update(cx, |editor, cx| {
16646 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");
16647 });
16648 buffer_1.update(cx, |buffer, _| {
16649 assert_eq!(buffer.text(), base_text_1);
16650 });
16651 buffer_2.update(cx, |buffer, _| {
16652 assert_eq!(buffer.text(), base_text_2);
16653 });
16654 buffer_3.update(cx, |buffer, _| {
16655 assert_eq!(buffer.text(), base_text_3);
16656 });
16657
16658 editor.update_in(cx, |editor, window, cx| {
16659 editor.undo(&Default::default(), window, cx);
16660 });
16661
16662 editor.update_in(cx, |editor, window, cx| {
16663 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16664 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
16665 });
16666 editor.git_restore(&Default::default(), window, cx);
16667 });
16668
16669 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
16670 // but not affect buffer_2 and its related excerpts.
16671 editor.update(cx, |editor, cx| {
16672 assert_eq!(
16673 editor.text(cx),
16674 "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}"
16675 );
16676 });
16677 buffer_1.update(cx, |buffer, _| {
16678 assert_eq!(buffer.text(), base_text_1);
16679 });
16680 buffer_2.update(cx, |buffer, _| {
16681 assert_eq!(
16682 buffer.text(),
16683 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
16684 );
16685 });
16686 buffer_3.update(cx, |buffer, _| {
16687 assert_eq!(
16688 buffer.text(),
16689 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
16690 );
16691 });
16692
16693 fn edit_first_char_of_every_line(text: &str) -> String {
16694 text.split('\n')
16695 .map(|line| format!("X{}", &line[1..]))
16696 .collect::<Vec<_>>()
16697 .join("\n")
16698 }
16699}
16700
16701#[gpui::test]
16702async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
16703 init_test(cx, |_| {});
16704
16705 let cols = 4;
16706 let rows = 10;
16707 let sample_text_1 = sample_text(rows, cols, 'a');
16708 assert_eq!(
16709 sample_text_1,
16710 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
16711 );
16712 let sample_text_2 = sample_text(rows, cols, 'l');
16713 assert_eq!(
16714 sample_text_2,
16715 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
16716 );
16717 let sample_text_3 = sample_text(rows, cols, 'v');
16718 assert_eq!(
16719 sample_text_3,
16720 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
16721 );
16722
16723 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
16724 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
16725 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
16726
16727 let multi_buffer = cx.new(|cx| {
16728 let mut multibuffer = MultiBuffer::new(ReadWrite);
16729 multibuffer.push_excerpts(
16730 buffer_1.clone(),
16731 [
16732 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16733 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16734 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16735 ],
16736 cx,
16737 );
16738 multibuffer.push_excerpts(
16739 buffer_2.clone(),
16740 [
16741 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16742 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16743 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16744 ],
16745 cx,
16746 );
16747 multibuffer.push_excerpts(
16748 buffer_3.clone(),
16749 [
16750 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16751 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16752 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16753 ],
16754 cx,
16755 );
16756 multibuffer
16757 });
16758
16759 let fs = FakeFs::new(cx.executor());
16760 fs.insert_tree(
16761 "/a",
16762 json!({
16763 "main.rs": sample_text_1,
16764 "other.rs": sample_text_2,
16765 "lib.rs": sample_text_3,
16766 }),
16767 )
16768 .await;
16769 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16770 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16771 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16772 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16773 Editor::new(
16774 EditorMode::full(),
16775 multi_buffer,
16776 Some(project.clone()),
16777 window,
16778 cx,
16779 )
16780 });
16781 let multibuffer_item_id = workspace
16782 .update(cx, |workspace, window, cx| {
16783 assert!(
16784 workspace.active_item(cx).is_none(),
16785 "active item should be None before the first item is added"
16786 );
16787 workspace.add_item_to_active_pane(
16788 Box::new(multi_buffer_editor.clone()),
16789 None,
16790 true,
16791 window,
16792 cx,
16793 );
16794 let active_item = workspace
16795 .active_item(cx)
16796 .expect("should have an active item after adding the multi buffer");
16797 assert!(
16798 !active_item.is_singleton(cx),
16799 "A multi buffer was expected to active after adding"
16800 );
16801 active_item.item_id()
16802 })
16803 .unwrap();
16804 cx.executor().run_until_parked();
16805
16806 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16807 editor.change_selections(
16808 SelectionEffects::scroll(Autoscroll::Next),
16809 window,
16810 cx,
16811 |s| s.select_ranges(Some(1..2)),
16812 );
16813 editor.open_excerpts(&OpenExcerpts, window, cx);
16814 });
16815 cx.executor().run_until_parked();
16816 let first_item_id = workspace
16817 .update(cx, |workspace, window, cx| {
16818 let active_item = workspace
16819 .active_item(cx)
16820 .expect("should have an active item after navigating into the 1st buffer");
16821 let first_item_id = active_item.item_id();
16822 assert_ne!(
16823 first_item_id, multibuffer_item_id,
16824 "Should navigate into the 1st buffer and activate it"
16825 );
16826 assert!(
16827 active_item.is_singleton(cx),
16828 "New active item should be a singleton buffer"
16829 );
16830 assert_eq!(
16831 active_item
16832 .act_as::<Editor>(cx)
16833 .expect("should have navigated into an editor for the 1st buffer")
16834 .read(cx)
16835 .text(cx),
16836 sample_text_1
16837 );
16838
16839 workspace
16840 .go_back(workspace.active_pane().downgrade(), window, cx)
16841 .detach_and_log_err(cx);
16842
16843 first_item_id
16844 })
16845 .unwrap();
16846 cx.executor().run_until_parked();
16847 workspace
16848 .update(cx, |workspace, _, cx| {
16849 let active_item = workspace
16850 .active_item(cx)
16851 .expect("should have an active item after navigating back");
16852 assert_eq!(
16853 active_item.item_id(),
16854 multibuffer_item_id,
16855 "Should navigate back to the multi buffer"
16856 );
16857 assert!(!active_item.is_singleton(cx));
16858 })
16859 .unwrap();
16860
16861 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16862 editor.change_selections(
16863 SelectionEffects::scroll(Autoscroll::Next),
16864 window,
16865 cx,
16866 |s| s.select_ranges(Some(39..40)),
16867 );
16868 editor.open_excerpts(&OpenExcerpts, window, cx);
16869 });
16870 cx.executor().run_until_parked();
16871 let second_item_id = workspace
16872 .update(cx, |workspace, window, cx| {
16873 let active_item = workspace
16874 .active_item(cx)
16875 .expect("should have an active item after navigating into the 2nd buffer");
16876 let second_item_id = active_item.item_id();
16877 assert_ne!(
16878 second_item_id, multibuffer_item_id,
16879 "Should navigate away from the multibuffer"
16880 );
16881 assert_ne!(
16882 second_item_id, first_item_id,
16883 "Should navigate into the 2nd buffer and activate it"
16884 );
16885 assert!(
16886 active_item.is_singleton(cx),
16887 "New active item should be a singleton buffer"
16888 );
16889 assert_eq!(
16890 active_item
16891 .act_as::<Editor>(cx)
16892 .expect("should have navigated into an editor")
16893 .read(cx)
16894 .text(cx),
16895 sample_text_2
16896 );
16897
16898 workspace
16899 .go_back(workspace.active_pane().downgrade(), window, cx)
16900 .detach_and_log_err(cx);
16901
16902 second_item_id
16903 })
16904 .unwrap();
16905 cx.executor().run_until_parked();
16906 workspace
16907 .update(cx, |workspace, _, cx| {
16908 let active_item = workspace
16909 .active_item(cx)
16910 .expect("should have an active item after navigating back from the 2nd buffer");
16911 assert_eq!(
16912 active_item.item_id(),
16913 multibuffer_item_id,
16914 "Should navigate back from the 2nd buffer to the multi buffer"
16915 );
16916 assert!(!active_item.is_singleton(cx));
16917 })
16918 .unwrap();
16919
16920 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16921 editor.change_selections(
16922 SelectionEffects::scroll(Autoscroll::Next),
16923 window,
16924 cx,
16925 |s| s.select_ranges(Some(70..70)),
16926 );
16927 editor.open_excerpts(&OpenExcerpts, window, cx);
16928 });
16929 cx.executor().run_until_parked();
16930 workspace
16931 .update(cx, |workspace, window, cx| {
16932 let active_item = workspace
16933 .active_item(cx)
16934 .expect("should have an active item after navigating into the 3rd buffer");
16935 let third_item_id = active_item.item_id();
16936 assert_ne!(
16937 third_item_id, multibuffer_item_id,
16938 "Should navigate into the 3rd buffer and activate it"
16939 );
16940 assert_ne!(third_item_id, first_item_id);
16941 assert_ne!(third_item_id, second_item_id);
16942 assert!(
16943 active_item.is_singleton(cx),
16944 "New active item should be a singleton buffer"
16945 );
16946 assert_eq!(
16947 active_item
16948 .act_as::<Editor>(cx)
16949 .expect("should have navigated into an editor")
16950 .read(cx)
16951 .text(cx),
16952 sample_text_3
16953 );
16954
16955 workspace
16956 .go_back(workspace.active_pane().downgrade(), window, cx)
16957 .detach_and_log_err(cx);
16958 })
16959 .unwrap();
16960 cx.executor().run_until_parked();
16961 workspace
16962 .update(cx, |workspace, _, cx| {
16963 let active_item = workspace
16964 .active_item(cx)
16965 .expect("should have an active item after navigating back from the 3rd buffer");
16966 assert_eq!(
16967 active_item.item_id(),
16968 multibuffer_item_id,
16969 "Should navigate back from the 3rd buffer to the multi buffer"
16970 );
16971 assert!(!active_item.is_singleton(cx));
16972 })
16973 .unwrap();
16974}
16975
16976#[gpui::test]
16977async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16978 init_test(cx, |_| {});
16979
16980 let mut cx = EditorTestContext::new(cx).await;
16981
16982 let diff_base = r#"
16983 use some::mod;
16984
16985 const A: u32 = 42;
16986
16987 fn main() {
16988 println!("hello");
16989
16990 println!("world");
16991 }
16992 "#
16993 .unindent();
16994
16995 cx.set_state(
16996 &r#"
16997 use some::modified;
16998
16999 ˇ
17000 fn main() {
17001 println!("hello there");
17002
17003 println!("around the");
17004 println!("world");
17005 }
17006 "#
17007 .unindent(),
17008 );
17009
17010 cx.set_head_text(&diff_base);
17011 executor.run_until_parked();
17012
17013 cx.update_editor(|editor, window, cx| {
17014 editor.go_to_next_hunk(&GoToHunk, window, cx);
17015 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17016 });
17017 executor.run_until_parked();
17018 cx.assert_state_with_diff(
17019 r#"
17020 use some::modified;
17021
17022
17023 fn main() {
17024 - println!("hello");
17025 + ˇ println!("hello there");
17026
17027 println!("around the");
17028 println!("world");
17029 }
17030 "#
17031 .unindent(),
17032 );
17033
17034 cx.update_editor(|editor, window, cx| {
17035 for _ in 0..2 {
17036 editor.go_to_next_hunk(&GoToHunk, window, cx);
17037 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17038 }
17039 });
17040 executor.run_until_parked();
17041 cx.assert_state_with_diff(
17042 r#"
17043 - use some::mod;
17044 + ˇuse some::modified;
17045
17046
17047 fn main() {
17048 - println!("hello");
17049 + println!("hello there");
17050
17051 + println!("around the");
17052 println!("world");
17053 }
17054 "#
17055 .unindent(),
17056 );
17057
17058 cx.update_editor(|editor, window, cx| {
17059 editor.go_to_next_hunk(&GoToHunk, window, cx);
17060 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17061 });
17062 executor.run_until_parked();
17063 cx.assert_state_with_diff(
17064 r#"
17065 - use some::mod;
17066 + use some::modified;
17067
17068 - const A: u32 = 42;
17069 ˇ
17070 fn main() {
17071 - println!("hello");
17072 + println!("hello there");
17073
17074 + println!("around the");
17075 println!("world");
17076 }
17077 "#
17078 .unindent(),
17079 );
17080
17081 cx.update_editor(|editor, window, cx| {
17082 editor.cancel(&Cancel, window, cx);
17083 });
17084
17085 cx.assert_state_with_diff(
17086 r#"
17087 use some::modified;
17088
17089 ˇ
17090 fn main() {
17091 println!("hello there");
17092
17093 println!("around the");
17094 println!("world");
17095 }
17096 "#
17097 .unindent(),
17098 );
17099}
17100
17101#[gpui::test]
17102async fn test_diff_base_change_with_expanded_diff_hunks(
17103 executor: BackgroundExecutor,
17104 cx: &mut TestAppContext,
17105) {
17106 init_test(cx, |_| {});
17107
17108 let mut cx = EditorTestContext::new(cx).await;
17109
17110 let diff_base = r#"
17111 use some::mod1;
17112 use some::mod2;
17113
17114 const A: u32 = 42;
17115 const B: u32 = 42;
17116 const C: u32 = 42;
17117
17118 fn main() {
17119 println!("hello");
17120
17121 println!("world");
17122 }
17123 "#
17124 .unindent();
17125
17126 cx.set_state(
17127 &r#"
17128 use some::mod2;
17129
17130 const A: u32 = 42;
17131 const C: u32 = 42;
17132
17133 fn main(ˇ) {
17134 //println!("hello");
17135
17136 println!("world");
17137 //
17138 //
17139 }
17140 "#
17141 .unindent(),
17142 );
17143
17144 cx.set_head_text(&diff_base);
17145 executor.run_until_parked();
17146
17147 cx.update_editor(|editor, window, cx| {
17148 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17149 });
17150 executor.run_until_parked();
17151 cx.assert_state_with_diff(
17152 r#"
17153 - use some::mod1;
17154 use some::mod2;
17155
17156 const A: u32 = 42;
17157 - const B: u32 = 42;
17158 const C: u32 = 42;
17159
17160 fn main(ˇ) {
17161 - println!("hello");
17162 + //println!("hello");
17163
17164 println!("world");
17165 + //
17166 + //
17167 }
17168 "#
17169 .unindent(),
17170 );
17171
17172 cx.set_head_text("new diff base!");
17173 executor.run_until_parked();
17174 cx.assert_state_with_diff(
17175 r#"
17176 - new diff base!
17177 + use some::mod2;
17178 +
17179 + const A: u32 = 42;
17180 + const C: u32 = 42;
17181 +
17182 + fn main(ˇ) {
17183 + //println!("hello");
17184 +
17185 + println!("world");
17186 + //
17187 + //
17188 + }
17189 "#
17190 .unindent(),
17191 );
17192}
17193
17194#[gpui::test]
17195async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
17196 init_test(cx, |_| {});
17197
17198 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17199 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17200 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17201 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17202 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
17203 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
17204
17205 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
17206 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
17207 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
17208
17209 let multi_buffer = cx.new(|cx| {
17210 let mut multibuffer = MultiBuffer::new(ReadWrite);
17211 multibuffer.push_excerpts(
17212 buffer_1.clone(),
17213 [
17214 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17215 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17216 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17217 ],
17218 cx,
17219 );
17220 multibuffer.push_excerpts(
17221 buffer_2.clone(),
17222 [
17223 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17224 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17225 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17226 ],
17227 cx,
17228 );
17229 multibuffer.push_excerpts(
17230 buffer_3.clone(),
17231 [
17232 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17233 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17234 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17235 ],
17236 cx,
17237 );
17238 multibuffer
17239 });
17240
17241 let editor =
17242 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17243 editor
17244 .update(cx, |editor, _window, cx| {
17245 for (buffer, diff_base) in [
17246 (buffer_1.clone(), file_1_old),
17247 (buffer_2.clone(), file_2_old),
17248 (buffer_3.clone(), file_3_old),
17249 ] {
17250 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
17251 editor
17252 .buffer
17253 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
17254 }
17255 })
17256 .unwrap();
17257
17258 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17259 cx.run_until_parked();
17260
17261 cx.assert_editor_state(
17262 &"
17263 ˇaaa
17264 ccc
17265 ddd
17266
17267 ggg
17268 hhh
17269
17270
17271 lll
17272 mmm
17273 NNN
17274
17275 qqq
17276 rrr
17277
17278 uuu
17279 111
17280 222
17281 333
17282
17283 666
17284 777
17285
17286 000
17287 !!!"
17288 .unindent(),
17289 );
17290
17291 cx.update_editor(|editor, window, cx| {
17292 editor.select_all(&SelectAll, window, cx);
17293 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17294 });
17295 cx.executor().run_until_parked();
17296
17297 cx.assert_state_with_diff(
17298 "
17299 «aaa
17300 - bbb
17301 ccc
17302 ddd
17303
17304 ggg
17305 hhh
17306
17307
17308 lll
17309 mmm
17310 - nnn
17311 + NNN
17312
17313 qqq
17314 rrr
17315
17316 uuu
17317 111
17318 222
17319 333
17320
17321 + 666
17322 777
17323
17324 000
17325 !!!ˇ»"
17326 .unindent(),
17327 );
17328}
17329
17330#[gpui::test]
17331async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
17332 init_test(cx, |_| {});
17333
17334 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
17335 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
17336
17337 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
17338 let multi_buffer = cx.new(|cx| {
17339 let mut multibuffer = MultiBuffer::new(ReadWrite);
17340 multibuffer.push_excerpts(
17341 buffer.clone(),
17342 [
17343 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
17344 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
17345 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
17346 ],
17347 cx,
17348 );
17349 multibuffer
17350 });
17351
17352 let editor =
17353 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17354 editor
17355 .update(cx, |editor, _window, cx| {
17356 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
17357 editor
17358 .buffer
17359 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
17360 })
17361 .unwrap();
17362
17363 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17364 cx.run_until_parked();
17365
17366 cx.update_editor(|editor, window, cx| {
17367 editor.expand_all_diff_hunks(&Default::default(), window, cx)
17368 });
17369 cx.executor().run_until_parked();
17370
17371 // When the start of a hunk coincides with the start of its excerpt,
17372 // the hunk is expanded. When the start of a a hunk is earlier than
17373 // the start of its excerpt, the hunk is not expanded.
17374 cx.assert_state_with_diff(
17375 "
17376 ˇaaa
17377 - bbb
17378 + BBB
17379
17380 - ddd
17381 - eee
17382 + DDD
17383 + EEE
17384 fff
17385
17386 iii
17387 "
17388 .unindent(),
17389 );
17390}
17391
17392#[gpui::test]
17393async fn test_edits_around_expanded_insertion_hunks(
17394 executor: BackgroundExecutor,
17395 cx: &mut TestAppContext,
17396) {
17397 init_test(cx, |_| {});
17398
17399 let mut cx = EditorTestContext::new(cx).await;
17400
17401 let diff_base = r#"
17402 use some::mod1;
17403 use some::mod2;
17404
17405 const A: u32 = 42;
17406
17407 fn main() {
17408 println!("hello");
17409
17410 println!("world");
17411 }
17412 "#
17413 .unindent();
17414 executor.run_until_parked();
17415 cx.set_state(
17416 &r#"
17417 use some::mod1;
17418 use some::mod2;
17419
17420 const A: u32 = 42;
17421 const B: u32 = 42;
17422 const C: u32 = 42;
17423 ˇ
17424
17425 fn main() {
17426 println!("hello");
17427
17428 println!("world");
17429 }
17430 "#
17431 .unindent(),
17432 );
17433
17434 cx.set_head_text(&diff_base);
17435 executor.run_until_parked();
17436
17437 cx.update_editor(|editor, window, cx| {
17438 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17439 });
17440 executor.run_until_parked();
17441
17442 cx.assert_state_with_diff(
17443 r#"
17444 use some::mod1;
17445 use some::mod2;
17446
17447 const A: u32 = 42;
17448 + const B: u32 = 42;
17449 + const C: u32 = 42;
17450 + ˇ
17451
17452 fn main() {
17453 println!("hello");
17454
17455 println!("world");
17456 }
17457 "#
17458 .unindent(),
17459 );
17460
17461 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
17462 executor.run_until_parked();
17463
17464 cx.assert_state_with_diff(
17465 r#"
17466 use some::mod1;
17467 use some::mod2;
17468
17469 const A: u32 = 42;
17470 + const B: u32 = 42;
17471 + const C: u32 = 42;
17472 + const D: u32 = 42;
17473 + ˇ
17474
17475 fn main() {
17476 println!("hello");
17477
17478 println!("world");
17479 }
17480 "#
17481 .unindent(),
17482 );
17483
17484 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
17485 executor.run_until_parked();
17486
17487 cx.assert_state_with_diff(
17488 r#"
17489 use some::mod1;
17490 use some::mod2;
17491
17492 const A: u32 = 42;
17493 + const B: u32 = 42;
17494 + const C: u32 = 42;
17495 + const D: u32 = 42;
17496 + const E: u32 = 42;
17497 + ˇ
17498
17499 fn main() {
17500 println!("hello");
17501
17502 println!("world");
17503 }
17504 "#
17505 .unindent(),
17506 );
17507
17508 cx.update_editor(|editor, window, cx| {
17509 editor.delete_line(&DeleteLine, window, cx);
17510 });
17511 executor.run_until_parked();
17512
17513 cx.assert_state_with_diff(
17514 r#"
17515 use some::mod1;
17516 use some::mod2;
17517
17518 const A: u32 = 42;
17519 + const B: u32 = 42;
17520 + const C: u32 = 42;
17521 + const D: u32 = 42;
17522 + const E: u32 = 42;
17523 ˇ
17524 fn main() {
17525 println!("hello");
17526
17527 println!("world");
17528 }
17529 "#
17530 .unindent(),
17531 );
17532
17533 cx.update_editor(|editor, window, cx| {
17534 editor.move_up(&MoveUp, window, cx);
17535 editor.delete_line(&DeleteLine, window, cx);
17536 editor.move_up(&MoveUp, window, cx);
17537 editor.delete_line(&DeleteLine, window, cx);
17538 editor.move_up(&MoveUp, window, cx);
17539 editor.delete_line(&DeleteLine, window, cx);
17540 });
17541 executor.run_until_parked();
17542 cx.assert_state_with_diff(
17543 r#"
17544 use some::mod1;
17545 use some::mod2;
17546
17547 const A: u32 = 42;
17548 + const B: u32 = 42;
17549 ˇ
17550 fn main() {
17551 println!("hello");
17552
17553 println!("world");
17554 }
17555 "#
17556 .unindent(),
17557 );
17558
17559 cx.update_editor(|editor, window, cx| {
17560 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
17561 editor.delete_line(&DeleteLine, window, cx);
17562 });
17563 executor.run_until_parked();
17564 cx.assert_state_with_diff(
17565 r#"
17566 ˇ
17567 fn main() {
17568 println!("hello");
17569
17570 println!("world");
17571 }
17572 "#
17573 .unindent(),
17574 );
17575}
17576
17577#[gpui::test]
17578async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
17579 init_test(cx, |_| {});
17580
17581 let mut cx = EditorTestContext::new(cx).await;
17582 cx.set_head_text(indoc! { "
17583 one
17584 two
17585 three
17586 four
17587 five
17588 "
17589 });
17590 cx.set_state(indoc! { "
17591 one
17592 ˇthree
17593 five
17594 "});
17595 cx.run_until_parked();
17596 cx.update_editor(|editor, window, cx| {
17597 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17598 });
17599 cx.assert_state_with_diff(
17600 indoc! { "
17601 one
17602 - two
17603 ˇthree
17604 - four
17605 five
17606 "}
17607 .to_string(),
17608 );
17609 cx.update_editor(|editor, window, cx| {
17610 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17611 });
17612
17613 cx.assert_state_with_diff(
17614 indoc! { "
17615 one
17616 ˇthree
17617 five
17618 "}
17619 .to_string(),
17620 );
17621
17622 cx.set_state(indoc! { "
17623 one
17624 ˇTWO
17625 three
17626 four
17627 five
17628 "});
17629 cx.run_until_parked();
17630 cx.update_editor(|editor, window, cx| {
17631 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17632 });
17633
17634 cx.assert_state_with_diff(
17635 indoc! { "
17636 one
17637 - two
17638 + ˇTWO
17639 three
17640 four
17641 five
17642 "}
17643 .to_string(),
17644 );
17645 cx.update_editor(|editor, window, cx| {
17646 editor.move_up(&Default::default(), window, cx);
17647 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17648 });
17649 cx.assert_state_with_diff(
17650 indoc! { "
17651 one
17652 ˇTWO
17653 three
17654 four
17655 five
17656 "}
17657 .to_string(),
17658 );
17659}
17660
17661#[gpui::test]
17662async fn test_edits_around_expanded_deletion_hunks(
17663 executor: BackgroundExecutor,
17664 cx: &mut TestAppContext,
17665) {
17666 init_test(cx, |_| {});
17667
17668 let mut cx = EditorTestContext::new(cx).await;
17669
17670 let diff_base = r#"
17671 use some::mod1;
17672 use some::mod2;
17673
17674 const A: u32 = 42;
17675 const B: u32 = 42;
17676 const C: u32 = 42;
17677
17678
17679 fn main() {
17680 println!("hello");
17681
17682 println!("world");
17683 }
17684 "#
17685 .unindent();
17686 executor.run_until_parked();
17687 cx.set_state(
17688 &r#"
17689 use some::mod1;
17690 use some::mod2;
17691
17692 ˇconst B: u32 = 42;
17693 const C: u32 = 42;
17694
17695
17696 fn main() {
17697 println!("hello");
17698
17699 println!("world");
17700 }
17701 "#
17702 .unindent(),
17703 );
17704
17705 cx.set_head_text(&diff_base);
17706 executor.run_until_parked();
17707
17708 cx.update_editor(|editor, window, cx| {
17709 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17710 });
17711 executor.run_until_parked();
17712
17713 cx.assert_state_with_diff(
17714 r#"
17715 use some::mod1;
17716 use some::mod2;
17717
17718 - const A: u32 = 42;
17719 ˇconst B: u32 = 42;
17720 const C: u32 = 42;
17721
17722
17723 fn main() {
17724 println!("hello");
17725
17726 println!("world");
17727 }
17728 "#
17729 .unindent(),
17730 );
17731
17732 cx.update_editor(|editor, window, cx| {
17733 editor.delete_line(&DeleteLine, window, cx);
17734 });
17735 executor.run_until_parked();
17736 cx.assert_state_with_diff(
17737 r#"
17738 use some::mod1;
17739 use some::mod2;
17740
17741 - const A: u32 = 42;
17742 - const B: u32 = 42;
17743 ˇconst C: u32 = 42;
17744
17745
17746 fn main() {
17747 println!("hello");
17748
17749 println!("world");
17750 }
17751 "#
17752 .unindent(),
17753 );
17754
17755 cx.update_editor(|editor, window, cx| {
17756 editor.delete_line(&DeleteLine, window, cx);
17757 });
17758 executor.run_until_parked();
17759 cx.assert_state_with_diff(
17760 r#"
17761 use some::mod1;
17762 use some::mod2;
17763
17764 - const A: u32 = 42;
17765 - const B: u32 = 42;
17766 - const C: u32 = 42;
17767 ˇ
17768
17769 fn main() {
17770 println!("hello");
17771
17772 println!("world");
17773 }
17774 "#
17775 .unindent(),
17776 );
17777
17778 cx.update_editor(|editor, window, cx| {
17779 editor.handle_input("replacement", window, cx);
17780 });
17781 executor.run_until_parked();
17782 cx.assert_state_with_diff(
17783 r#"
17784 use some::mod1;
17785 use some::mod2;
17786
17787 - const A: u32 = 42;
17788 - const B: u32 = 42;
17789 - const C: u32 = 42;
17790 -
17791 + replacementˇ
17792
17793 fn main() {
17794 println!("hello");
17795
17796 println!("world");
17797 }
17798 "#
17799 .unindent(),
17800 );
17801}
17802
17803#[gpui::test]
17804async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17805 init_test(cx, |_| {});
17806
17807 let mut cx = EditorTestContext::new(cx).await;
17808
17809 let base_text = r#"
17810 one
17811 two
17812 three
17813 four
17814 five
17815 "#
17816 .unindent();
17817 executor.run_until_parked();
17818 cx.set_state(
17819 &r#"
17820 one
17821 two
17822 fˇour
17823 five
17824 "#
17825 .unindent(),
17826 );
17827
17828 cx.set_head_text(&base_text);
17829 executor.run_until_parked();
17830
17831 cx.update_editor(|editor, window, cx| {
17832 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17833 });
17834 executor.run_until_parked();
17835
17836 cx.assert_state_with_diff(
17837 r#"
17838 one
17839 two
17840 - three
17841 fˇour
17842 five
17843 "#
17844 .unindent(),
17845 );
17846
17847 cx.update_editor(|editor, window, cx| {
17848 editor.backspace(&Backspace, window, cx);
17849 editor.backspace(&Backspace, window, cx);
17850 });
17851 executor.run_until_parked();
17852 cx.assert_state_with_diff(
17853 r#"
17854 one
17855 two
17856 - threeˇ
17857 - four
17858 + our
17859 five
17860 "#
17861 .unindent(),
17862 );
17863}
17864
17865#[gpui::test]
17866async fn test_edit_after_expanded_modification_hunk(
17867 executor: BackgroundExecutor,
17868 cx: &mut TestAppContext,
17869) {
17870 init_test(cx, |_| {});
17871
17872 let mut cx = EditorTestContext::new(cx).await;
17873
17874 let diff_base = r#"
17875 use some::mod1;
17876 use some::mod2;
17877
17878 const A: u32 = 42;
17879 const B: u32 = 42;
17880 const C: u32 = 42;
17881 const D: u32 = 42;
17882
17883
17884 fn main() {
17885 println!("hello");
17886
17887 println!("world");
17888 }"#
17889 .unindent();
17890
17891 cx.set_state(
17892 &r#"
17893 use some::mod1;
17894 use some::mod2;
17895
17896 const A: u32 = 42;
17897 const B: u32 = 42;
17898 const C: u32 = 43ˇ
17899 const D: u32 = 42;
17900
17901
17902 fn main() {
17903 println!("hello");
17904
17905 println!("world");
17906 }"#
17907 .unindent(),
17908 );
17909
17910 cx.set_head_text(&diff_base);
17911 executor.run_until_parked();
17912 cx.update_editor(|editor, window, cx| {
17913 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17914 });
17915 executor.run_until_parked();
17916
17917 cx.assert_state_with_diff(
17918 r#"
17919 use some::mod1;
17920 use some::mod2;
17921
17922 const A: u32 = 42;
17923 const B: u32 = 42;
17924 - const C: u32 = 42;
17925 + const C: u32 = 43ˇ
17926 const D: u32 = 42;
17927
17928
17929 fn main() {
17930 println!("hello");
17931
17932 println!("world");
17933 }"#
17934 .unindent(),
17935 );
17936
17937 cx.update_editor(|editor, window, cx| {
17938 editor.handle_input("\nnew_line\n", window, cx);
17939 });
17940 executor.run_until_parked();
17941
17942 cx.assert_state_with_diff(
17943 r#"
17944 use some::mod1;
17945 use some::mod2;
17946
17947 const A: u32 = 42;
17948 const B: u32 = 42;
17949 - const C: u32 = 42;
17950 + const C: u32 = 43
17951 + new_line
17952 + ˇ
17953 const D: u32 = 42;
17954
17955
17956 fn main() {
17957 println!("hello");
17958
17959 println!("world");
17960 }"#
17961 .unindent(),
17962 );
17963}
17964
17965#[gpui::test]
17966async fn test_stage_and_unstage_added_file_hunk(
17967 executor: BackgroundExecutor,
17968 cx: &mut TestAppContext,
17969) {
17970 init_test(cx, |_| {});
17971
17972 let mut cx = EditorTestContext::new(cx).await;
17973 cx.update_editor(|editor, _, cx| {
17974 editor.set_expand_all_diff_hunks(cx);
17975 });
17976
17977 let working_copy = r#"
17978 ˇfn main() {
17979 println!("hello, world!");
17980 }
17981 "#
17982 .unindent();
17983
17984 cx.set_state(&working_copy);
17985 executor.run_until_parked();
17986
17987 cx.assert_state_with_diff(
17988 r#"
17989 + ˇfn main() {
17990 + println!("hello, world!");
17991 + }
17992 "#
17993 .unindent(),
17994 );
17995 cx.assert_index_text(None);
17996
17997 cx.update_editor(|editor, window, cx| {
17998 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17999 });
18000 executor.run_until_parked();
18001 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
18002 cx.assert_state_with_diff(
18003 r#"
18004 + ˇfn main() {
18005 + println!("hello, world!");
18006 + }
18007 "#
18008 .unindent(),
18009 );
18010
18011 cx.update_editor(|editor, window, cx| {
18012 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18013 });
18014 executor.run_until_parked();
18015 cx.assert_index_text(None);
18016}
18017
18018async fn setup_indent_guides_editor(
18019 text: &str,
18020 cx: &mut TestAppContext,
18021) -> (BufferId, EditorTestContext) {
18022 init_test(cx, |_| {});
18023
18024 let mut cx = EditorTestContext::new(cx).await;
18025
18026 let buffer_id = cx.update_editor(|editor, window, cx| {
18027 editor.set_text(text, window, cx);
18028 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
18029
18030 buffer_ids[0]
18031 });
18032
18033 (buffer_id, cx)
18034}
18035
18036fn assert_indent_guides(
18037 range: Range<u32>,
18038 expected: Vec<IndentGuide>,
18039 active_indices: Option<Vec<usize>>,
18040 cx: &mut EditorTestContext,
18041) {
18042 let indent_guides = cx.update_editor(|editor, window, cx| {
18043 let snapshot = editor.snapshot(window, cx).display_snapshot;
18044 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
18045 editor,
18046 MultiBufferRow(range.start)..MultiBufferRow(range.end),
18047 true,
18048 &snapshot,
18049 cx,
18050 );
18051
18052 indent_guides.sort_by(|a, b| {
18053 a.depth.cmp(&b.depth).then(
18054 a.start_row
18055 .cmp(&b.start_row)
18056 .then(a.end_row.cmp(&b.end_row)),
18057 )
18058 });
18059 indent_guides
18060 });
18061
18062 if let Some(expected) = active_indices {
18063 let active_indices = cx.update_editor(|editor, window, cx| {
18064 let snapshot = editor.snapshot(window, cx).display_snapshot;
18065 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
18066 });
18067
18068 assert_eq!(
18069 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
18070 expected,
18071 "Active indent guide indices do not match"
18072 );
18073 }
18074
18075 assert_eq!(indent_guides, expected, "Indent guides do not match");
18076}
18077
18078fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
18079 IndentGuide {
18080 buffer_id,
18081 start_row: MultiBufferRow(start_row),
18082 end_row: MultiBufferRow(end_row),
18083 depth,
18084 tab_size: 4,
18085 settings: IndentGuideSettings {
18086 enabled: true,
18087 line_width: 1,
18088 active_line_width: 1,
18089 ..Default::default()
18090 },
18091 }
18092}
18093
18094#[gpui::test]
18095async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
18096 let (buffer_id, mut cx) = setup_indent_guides_editor(
18097 &"
18098 fn main() {
18099 let a = 1;
18100 }"
18101 .unindent(),
18102 cx,
18103 )
18104 .await;
18105
18106 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18107}
18108
18109#[gpui::test]
18110async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
18111 let (buffer_id, mut cx) = setup_indent_guides_editor(
18112 &"
18113 fn main() {
18114 let a = 1;
18115 let b = 2;
18116 }"
18117 .unindent(),
18118 cx,
18119 )
18120 .await;
18121
18122 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
18123}
18124
18125#[gpui::test]
18126async fn test_indent_guide_nested(cx: &mut TestAppContext) {
18127 let (buffer_id, mut cx) = setup_indent_guides_editor(
18128 &"
18129 fn main() {
18130 let a = 1;
18131 if a == 3 {
18132 let b = 2;
18133 } else {
18134 let c = 3;
18135 }
18136 }"
18137 .unindent(),
18138 cx,
18139 )
18140 .await;
18141
18142 assert_indent_guides(
18143 0..8,
18144 vec![
18145 indent_guide(buffer_id, 1, 6, 0),
18146 indent_guide(buffer_id, 3, 3, 1),
18147 indent_guide(buffer_id, 5, 5, 1),
18148 ],
18149 None,
18150 &mut cx,
18151 );
18152}
18153
18154#[gpui::test]
18155async fn test_indent_guide_tab(cx: &mut TestAppContext) {
18156 let (buffer_id, mut cx) = setup_indent_guides_editor(
18157 &"
18158 fn main() {
18159 let a = 1;
18160 let b = 2;
18161 let c = 3;
18162 }"
18163 .unindent(),
18164 cx,
18165 )
18166 .await;
18167
18168 assert_indent_guides(
18169 0..5,
18170 vec![
18171 indent_guide(buffer_id, 1, 3, 0),
18172 indent_guide(buffer_id, 2, 2, 1),
18173 ],
18174 None,
18175 &mut cx,
18176 );
18177}
18178
18179#[gpui::test]
18180async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
18181 let (buffer_id, mut cx) = setup_indent_guides_editor(
18182 &"
18183 fn main() {
18184 let a = 1;
18185
18186 let c = 3;
18187 }"
18188 .unindent(),
18189 cx,
18190 )
18191 .await;
18192
18193 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
18194}
18195
18196#[gpui::test]
18197async fn test_indent_guide_complex(cx: &mut TestAppContext) {
18198 let (buffer_id, mut cx) = setup_indent_guides_editor(
18199 &"
18200 fn main() {
18201 let a = 1;
18202
18203 let c = 3;
18204
18205 if a == 3 {
18206 let b = 2;
18207 } else {
18208 let c = 3;
18209 }
18210 }"
18211 .unindent(),
18212 cx,
18213 )
18214 .await;
18215
18216 assert_indent_guides(
18217 0..11,
18218 vec![
18219 indent_guide(buffer_id, 1, 9, 0),
18220 indent_guide(buffer_id, 6, 6, 1),
18221 indent_guide(buffer_id, 8, 8, 1),
18222 ],
18223 None,
18224 &mut cx,
18225 );
18226}
18227
18228#[gpui::test]
18229async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
18230 let (buffer_id, mut cx) = setup_indent_guides_editor(
18231 &"
18232 fn main() {
18233 let a = 1;
18234
18235 let c = 3;
18236
18237 if a == 3 {
18238 let b = 2;
18239 } else {
18240 let c = 3;
18241 }
18242 }"
18243 .unindent(),
18244 cx,
18245 )
18246 .await;
18247
18248 assert_indent_guides(
18249 1..11,
18250 vec![
18251 indent_guide(buffer_id, 1, 9, 0),
18252 indent_guide(buffer_id, 6, 6, 1),
18253 indent_guide(buffer_id, 8, 8, 1),
18254 ],
18255 None,
18256 &mut cx,
18257 );
18258}
18259
18260#[gpui::test]
18261async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
18262 let (buffer_id, mut cx) = setup_indent_guides_editor(
18263 &"
18264 fn main() {
18265 let a = 1;
18266
18267 let c = 3;
18268
18269 if a == 3 {
18270 let b = 2;
18271 } else {
18272 let c = 3;
18273 }
18274 }"
18275 .unindent(),
18276 cx,
18277 )
18278 .await;
18279
18280 assert_indent_guides(
18281 1..10,
18282 vec![
18283 indent_guide(buffer_id, 1, 9, 0),
18284 indent_guide(buffer_id, 6, 6, 1),
18285 indent_guide(buffer_id, 8, 8, 1),
18286 ],
18287 None,
18288 &mut cx,
18289 );
18290}
18291
18292#[gpui::test]
18293async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
18294 let (buffer_id, mut cx) = setup_indent_guides_editor(
18295 &"
18296 fn main() {
18297 if a {
18298 b(
18299 c,
18300 d,
18301 )
18302 } else {
18303 e(
18304 f
18305 )
18306 }
18307 }"
18308 .unindent(),
18309 cx,
18310 )
18311 .await;
18312
18313 assert_indent_guides(
18314 0..11,
18315 vec![
18316 indent_guide(buffer_id, 1, 10, 0),
18317 indent_guide(buffer_id, 2, 5, 1),
18318 indent_guide(buffer_id, 7, 9, 1),
18319 indent_guide(buffer_id, 3, 4, 2),
18320 indent_guide(buffer_id, 8, 8, 2),
18321 ],
18322 None,
18323 &mut cx,
18324 );
18325
18326 cx.update_editor(|editor, window, cx| {
18327 editor.fold_at(MultiBufferRow(2), window, cx);
18328 assert_eq!(
18329 editor.display_text(cx),
18330 "
18331 fn main() {
18332 if a {
18333 b(⋯
18334 )
18335 } else {
18336 e(
18337 f
18338 )
18339 }
18340 }"
18341 .unindent()
18342 );
18343 });
18344
18345 assert_indent_guides(
18346 0..11,
18347 vec![
18348 indent_guide(buffer_id, 1, 10, 0),
18349 indent_guide(buffer_id, 2, 5, 1),
18350 indent_guide(buffer_id, 7, 9, 1),
18351 indent_guide(buffer_id, 8, 8, 2),
18352 ],
18353 None,
18354 &mut cx,
18355 );
18356}
18357
18358#[gpui::test]
18359async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
18360 let (buffer_id, mut cx) = setup_indent_guides_editor(
18361 &"
18362 block1
18363 block2
18364 block3
18365 block4
18366 block2
18367 block1
18368 block1"
18369 .unindent(),
18370 cx,
18371 )
18372 .await;
18373
18374 assert_indent_guides(
18375 1..10,
18376 vec![
18377 indent_guide(buffer_id, 1, 4, 0),
18378 indent_guide(buffer_id, 2, 3, 1),
18379 indent_guide(buffer_id, 3, 3, 2),
18380 ],
18381 None,
18382 &mut cx,
18383 );
18384}
18385
18386#[gpui::test]
18387async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
18388 let (buffer_id, mut cx) = setup_indent_guides_editor(
18389 &"
18390 block1
18391 block2
18392 block3
18393
18394 block1
18395 block1"
18396 .unindent(),
18397 cx,
18398 )
18399 .await;
18400
18401 assert_indent_guides(
18402 0..6,
18403 vec![
18404 indent_guide(buffer_id, 1, 2, 0),
18405 indent_guide(buffer_id, 2, 2, 1),
18406 ],
18407 None,
18408 &mut cx,
18409 );
18410}
18411
18412#[gpui::test]
18413async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
18414 let (buffer_id, mut cx) = setup_indent_guides_editor(
18415 &"
18416 function component() {
18417 \treturn (
18418 \t\t\t
18419 \t\t<div>
18420 \t\t\t<abc></abc>
18421 \t\t</div>
18422 \t)
18423 }"
18424 .unindent(),
18425 cx,
18426 )
18427 .await;
18428
18429 assert_indent_guides(
18430 0..8,
18431 vec![
18432 indent_guide(buffer_id, 1, 6, 0),
18433 indent_guide(buffer_id, 2, 5, 1),
18434 indent_guide(buffer_id, 4, 4, 2),
18435 ],
18436 None,
18437 &mut cx,
18438 );
18439}
18440
18441#[gpui::test]
18442async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
18443 let (buffer_id, mut cx) = setup_indent_guides_editor(
18444 &"
18445 function component() {
18446 \treturn (
18447 \t
18448 \t\t<div>
18449 \t\t\t<abc></abc>
18450 \t\t</div>
18451 \t)
18452 }"
18453 .unindent(),
18454 cx,
18455 )
18456 .await;
18457
18458 assert_indent_guides(
18459 0..8,
18460 vec![
18461 indent_guide(buffer_id, 1, 6, 0),
18462 indent_guide(buffer_id, 2, 5, 1),
18463 indent_guide(buffer_id, 4, 4, 2),
18464 ],
18465 None,
18466 &mut cx,
18467 );
18468}
18469
18470#[gpui::test]
18471async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
18472 let (buffer_id, mut cx) = setup_indent_guides_editor(
18473 &"
18474 block1
18475
18476
18477
18478 block2
18479 "
18480 .unindent(),
18481 cx,
18482 )
18483 .await;
18484
18485 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18486}
18487
18488#[gpui::test]
18489async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
18490 let (buffer_id, mut cx) = setup_indent_guides_editor(
18491 &"
18492 def a:
18493 \tb = 3
18494 \tif True:
18495 \t\tc = 4
18496 \t\td = 5
18497 \tprint(b)
18498 "
18499 .unindent(),
18500 cx,
18501 )
18502 .await;
18503
18504 assert_indent_guides(
18505 0..6,
18506 vec![
18507 indent_guide(buffer_id, 1, 5, 0),
18508 indent_guide(buffer_id, 3, 4, 1),
18509 ],
18510 None,
18511 &mut cx,
18512 );
18513}
18514
18515#[gpui::test]
18516async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
18517 let (buffer_id, mut cx) = setup_indent_guides_editor(
18518 &"
18519 fn main() {
18520 let a = 1;
18521 }"
18522 .unindent(),
18523 cx,
18524 )
18525 .await;
18526
18527 cx.update_editor(|editor, window, cx| {
18528 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18529 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18530 });
18531 });
18532
18533 assert_indent_guides(
18534 0..3,
18535 vec![indent_guide(buffer_id, 1, 1, 0)],
18536 Some(vec![0]),
18537 &mut cx,
18538 );
18539}
18540
18541#[gpui::test]
18542async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
18543 let (buffer_id, mut cx) = setup_indent_guides_editor(
18544 &"
18545 fn main() {
18546 if 1 == 2 {
18547 let a = 1;
18548 }
18549 }"
18550 .unindent(),
18551 cx,
18552 )
18553 .await;
18554
18555 cx.update_editor(|editor, window, cx| {
18556 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18557 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18558 });
18559 });
18560
18561 assert_indent_guides(
18562 0..4,
18563 vec![
18564 indent_guide(buffer_id, 1, 3, 0),
18565 indent_guide(buffer_id, 2, 2, 1),
18566 ],
18567 Some(vec![1]),
18568 &mut cx,
18569 );
18570
18571 cx.update_editor(|editor, window, cx| {
18572 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18573 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18574 });
18575 });
18576
18577 assert_indent_guides(
18578 0..4,
18579 vec![
18580 indent_guide(buffer_id, 1, 3, 0),
18581 indent_guide(buffer_id, 2, 2, 1),
18582 ],
18583 Some(vec![1]),
18584 &mut cx,
18585 );
18586
18587 cx.update_editor(|editor, window, cx| {
18588 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18589 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
18590 });
18591 });
18592
18593 assert_indent_guides(
18594 0..4,
18595 vec![
18596 indent_guide(buffer_id, 1, 3, 0),
18597 indent_guide(buffer_id, 2, 2, 1),
18598 ],
18599 Some(vec![0]),
18600 &mut cx,
18601 );
18602}
18603
18604#[gpui::test]
18605async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
18606 let (buffer_id, mut cx) = setup_indent_guides_editor(
18607 &"
18608 fn main() {
18609 let a = 1;
18610
18611 let b = 2;
18612 }"
18613 .unindent(),
18614 cx,
18615 )
18616 .await;
18617
18618 cx.update_editor(|editor, window, cx| {
18619 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18620 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18621 });
18622 });
18623
18624 assert_indent_guides(
18625 0..5,
18626 vec![indent_guide(buffer_id, 1, 3, 0)],
18627 Some(vec![0]),
18628 &mut cx,
18629 );
18630}
18631
18632#[gpui::test]
18633async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
18634 let (buffer_id, mut cx) = setup_indent_guides_editor(
18635 &"
18636 def m:
18637 a = 1
18638 pass"
18639 .unindent(),
18640 cx,
18641 )
18642 .await;
18643
18644 cx.update_editor(|editor, window, cx| {
18645 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18646 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18647 });
18648 });
18649
18650 assert_indent_guides(
18651 0..3,
18652 vec![indent_guide(buffer_id, 1, 2, 0)],
18653 Some(vec![0]),
18654 &mut cx,
18655 );
18656}
18657
18658#[gpui::test]
18659async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
18660 init_test(cx, |_| {});
18661 let mut cx = EditorTestContext::new(cx).await;
18662 let text = indoc! {
18663 "
18664 impl A {
18665 fn b() {
18666 0;
18667 3;
18668 5;
18669 6;
18670 7;
18671 }
18672 }
18673 "
18674 };
18675 let base_text = indoc! {
18676 "
18677 impl A {
18678 fn b() {
18679 0;
18680 1;
18681 2;
18682 3;
18683 4;
18684 }
18685 fn c() {
18686 5;
18687 6;
18688 7;
18689 }
18690 }
18691 "
18692 };
18693
18694 cx.update_editor(|editor, window, cx| {
18695 editor.set_text(text, window, cx);
18696
18697 editor.buffer().update(cx, |multibuffer, cx| {
18698 let buffer = multibuffer.as_singleton().unwrap();
18699 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
18700
18701 multibuffer.set_all_diff_hunks_expanded(cx);
18702 multibuffer.add_diff(diff, cx);
18703
18704 buffer.read(cx).remote_id()
18705 })
18706 });
18707 cx.run_until_parked();
18708
18709 cx.assert_state_with_diff(
18710 indoc! { "
18711 impl A {
18712 fn b() {
18713 0;
18714 - 1;
18715 - 2;
18716 3;
18717 - 4;
18718 - }
18719 - fn c() {
18720 5;
18721 6;
18722 7;
18723 }
18724 }
18725 ˇ"
18726 }
18727 .to_string(),
18728 );
18729
18730 let mut actual_guides = cx.update_editor(|editor, window, cx| {
18731 editor
18732 .snapshot(window, cx)
18733 .buffer_snapshot
18734 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
18735 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
18736 .collect::<Vec<_>>()
18737 });
18738 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
18739 assert_eq!(
18740 actual_guides,
18741 vec![
18742 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
18743 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
18744 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
18745 ]
18746 );
18747}
18748
18749#[gpui::test]
18750async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18751 init_test(cx, |_| {});
18752 let mut cx = EditorTestContext::new(cx).await;
18753
18754 let diff_base = r#"
18755 a
18756 b
18757 c
18758 "#
18759 .unindent();
18760
18761 cx.set_state(
18762 &r#"
18763 ˇA
18764 b
18765 C
18766 "#
18767 .unindent(),
18768 );
18769 cx.set_head_text(&diff_base);
18770 cx.update_editor(|editor, window, cx| {
18771 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18772 });
18773 executor.run_until_parked();
18774
18775 let both_hunks_expanded = r#"
18776 - a
18777 + ˇA
18778 b
18779 - c
18780 + C
18781 "#
18782 .unindent();
18783
18784 cx.assert_state_with_diff(both_hunks_expanded.clone());
18785
18786 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18787 let snapshot = editor.snapshot(window, cx);
18788 let hunks = editor
18789 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18790 .collect::<Vec<_>>();
18791 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18792 let buffer_id = hunks[0].buffer_id;
18793 hunks
18794 .into_iter()
18795 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18796 .collect::<Vec<_>>()
18797 });
18798 assert_eq!(hunk_ranges.len(), 2);
18799
18800 cx.update_editor(|editor, _, cx| {
18801 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18802 });
18803 executor.run_until_parked();
18804
18805 let second_hunk_expanded = r#"
18806 ˇA
18807 b
18808 - c
18809 + C
18810 "#
18811 .unindent();
18812
18813 cx.assert_state_with_diff(second_hunk_expanded);
18814
18815 cx.update_editor(|editor, _, cx| {
18816 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18817 });
18818 executor.run_until_parked();
18819
18820 cx.assert_state_with_diff(both_hunks_expanded.clone());
18821
18822 cx.update_editor(|editor, _, cx| {
18823 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18824 });
18825 executor.run_until_parked();
18826
18827 let first_hunk_expanded = r#"
18828 - a
18829 + ˇA
18830 b
18831 C
18832 "#
18833 .unindent();
18834
18835 cx.assert_state_with_diff(first_hunk_expanded);
18836
18837 cx.update_editor(|editor, _, cx| {
18838 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18839 });
18840 executor.run_until_parked();
18841
18842 cx.assert_state_with_diff(both_hunks_expanded);
18843
18844 cx.set_state(
18845 &r#"
18846 ˇA
18847 b
18848 "#
18849 .unindent(),
18850 );
18851 cx.run_until_parked();
18852
18853 // TODO this cursor position seems bad
18854 cx.assert_state_with_diff(
18855 r#"
18856 - ˇa
18857 + A
18858 b
18859 "#
18860 .unindent(),
18861 );
18862
18863 cx.update_editor(|editor, window, cx| {
18864 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18865 });
18866
18867 cx.assert_state_with_diff(
18868 r#"
18869 - ˇa
18870 + A
18871 b
18872 - c
18873 "#
18874 .unindent(),
18875 );
18876
18877 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18878 let snapshot = editor.snapshot(window, cx);
18879 let hunks = editor
18880 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18881 .collect::<Vec<_>>();
18882 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18883 let buffer_id = hunks[0].buffer_id;
18884 hunks
18885 .into_iter()
18886 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18887 .collect::<Vec<_>>()
18888 });
18889 assert_eq!(hunk_ranges.len(), 2);
18890
18891 cx.update_editor(|editor, _, cx| {
18892 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18893 });
18894 executor.run_until_parked();
18895
18896 cx.assert_state_with_diff(
18897 r#"
18898 - ˇa
18899 + A
18900 b
18901 "#
18902 .unindent(),
18903 );
18904}
18905
18906#[gpui::test]
18907async fn test_toggle_deletion_hunk_at_start_of_file(
18908 executor: BackgroundExecutor,
18909 cx: &mut TestAppContext,
18910) {
18911 init_test(cx, |_| {});
18912 let mut cx = EditorTestContext::new(cx).await;
18913
18914 let diff_base = r#"
18915 a
18916 b
18917 c
18918 "#
18919 .unindent();
18920
18921 cx.set_state(
18922 &r#"
18923 ˇb
18924 c
18925 "#
18926 .unindent(),
18927 );
18928 cx.set_head_text(&diff_base);
18929 cx.update_editor(|editor, window, cx| {
18930 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18931 });
18932 executor.run_until_parked();
18933
18934 let hunk_expanded = r#"
18935 - a
18936 ˇb
18937 c
18938 "#
18939 .unindent();
18940
18941 cx.assert_state_with_diff(hunk_expanded.clone());
18942
18943 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18944 let snapshot = editor.snapshot(window, cx);
18945 let hunks = editor
18946 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18947 .collect::<Vec<_>>();
18948 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18949 let buffer_id = hunks[0].buffer_id;
18950 hunks
18951 .into_iter()
18952 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18953 .collect::<Vec<_>>()
18954 });
18955 assert_eq!(hunk_ranges.len(), 1);
18956
18957 cx.update_editor(|editor, _, cx| {
18958 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18959 });
18960 executor.run_until_parked();
18961
18962 let hunk_collapsed = r#"
18963 ˇb
18964 c
18965 "#
18966 .unindent();
18967
18968 cx.assert_state_with_diff(hunk_collapsed);
18969
18970 cx.update_editor(|editor, _, cx| {
18971 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18972 });
18973 executor.run_until_parked();
18974
18975 cx.assert_state_with_diff(hunk_expanded.clone());
18976}
18977
18978#[gpui::test]
18979async fn test_display_diff_hunks(cx: &mut TestAppContext) {
18980 init_test(cx, |_| {});
18981
18982 let fs = FakeFs::new(cx.executor());
18983 fs.insert_tree(
18984 path!("/test"),
18985 json!({
18986 ".git": {},
18987 "file-1": "ONE\n",
18988 "file-2": "TWO\n",
18989 "file-3": "THREE\n",
18990 }),
18991 )
18992 .await;
18993
18994 fs.set_head_for_repo(
18995 path!("/test/.git").as_ref(),
18996 &[
18997 ("file-1".into(), "one\n".into()),
18998 ("file-2".into(), "two\n".into()),
18999 ("file-3".into(), "three\n".into()),
19000 ],
19001 "deadbeef",
19002 );
19003
19004 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
19005 let mut buffers = vec![];
19006 for i in 1..=3 {
19007 let buffer = project
19008 .update(cx, |project, cx| {
19009 let path = format!(path!("/test/file-{}"), i);
19010 project.open_local_buffer(path, cx)
19011 })
19012 .await
19013 .unwrap();
19014 buffers.push(buffer);
19015 }
19016
19017 let multibuffer = cx.new(|cx| {
19018 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
19019 multibuffer.set_all_diff_hunks_expanded(cx);
19020 for buffer in &buffers {
19021 let snapshot = buffer.read(cx).snapshot();
19022 multibuffer.set_excerpts_for_path(
19023 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
19024 buffer.clone(),
19025 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
19026 DEFAULT_MULTIBUFFER_CONTEXT,
19027 cx,
19028 );
19029 }
19030 multibuffer
19031 });
19032
19033 let editor = cx.add_window(|window, cx| {
19034 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
19035 });
19036 cx.run_until_parked();
19037
19038 let snapshot = editor
19039 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19040 .unwrap();
19041 let hunks = snapshot
19042 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
19043 .map(|hunk| match hunk {
19044 DisplayDiffHunk::Unfolded {
19045 display_row_range, ..
19046 } => display_row_range,
19047 DisplayDiffHunk::Folded { .. } => unreachable!(),
19048 })
19049 .collect::<Vec<_>>();
19050 assert_eq!(
19051 hunks,
19052 [
19053 DisplayRow(2)..DisplayRow(4),
19054 DisplayRow(7)..DisplayRow(9),
19055 DisplayRow(12)..DisplayRow(14),
19056 ]
19057 );
19058}
19059
19060#[gpui::test]
19061async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
19062 init_test(cx, |_| {});
19063
19064 let mut cx = EditorTestContext::new(cx).await;
19065 cx.set_head_text(indoc! { "
19066 one
19067 two
19068 three
19069 four
19070 five
19071 "
19072 });
19073 cx.set_index_text(indoc! { "
19074 one
19075 two
19076 three
19077 four
19078 five
19079 "
19080 });
19081 cx.set_state(indoc! {"
19082 one
19083 TWO
19084 ˇTHREE
19085 FOUR
19086 five
19087 "});
19088 cx.run_until_parked();
19089 cx.update_editor(|editor, window, cx| {
19090 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19091 });
19092 cx.run_until_parked();
19093 cx.assert_index_text(Some(indoc! {"
19094 one
19095 TWO
19096 THREE
19097 FOUR
19098 five
19099 "}));
19100 cx.set_state(indoc! { "
19101 one
19102 TWO
19103 ˇTHREE-HUNDRED
19104 FOUR
19105 five
19106 "});
19107 cx.run_until_parked();
19108 cx.update_editor(|editor, window, cx| {
19109 let snapshot = editor.snapshot(window, cx);
19110 let hunks = editor
19111 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19112 .collect::<Vec<_>>();
19113 assert_eq!(hunks.len(), 1);
19114 assert_eq!(
19115 hunks[0].status(),
19116 DiffHunkStatus {
19117 kind: DiffHunkStatusKind::Modified,
19118 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
19119 }
19120 );
19121
19122 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19123 });
19124 cx.run_until_parked();
19125 cx.assert_index_text(Some(indoc! {"
19126 one
19127 TWO
19128 THREE-HUNDRED
19129 FOUR
19130 five
19131 "}));
19132}
19133
19134#[gpui::test]
19135fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
19136 init_test(cx, |_| {});
19137
19138 let editor = cx.add_window(|window, cx| {
19139 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
19140 build_editor(buffer, window, cx)
19141 });
19142
19143 let render_args = Arc::new(Mutex::new(None));
19144 let snapshot = editor
19145 .update(cx, |editor, window, cx| {
19146 let snapshot = editor.buffer().read(cx).snapshot(cx);
19147 let range =
19148 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
19149
19150 struct RenderArgs {
19151 row: MultiBufferRow,
19152 folded: bool,
19153 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
19154 }
19155
19156 let crease = Crease::inline(
19157 range,
19158 FoldPlaceholder::test(),
19159 {
19160 let toggle_callback = render_args.clone();
19161 move |row, folded, callback, _window, _cx| {
19162 *toggle_callback.lock() = Some(RenderArgs {
19163 row,
19164 folded,
19165 callback,
19166 });
19167 div()
19168 }
19169 },
19170 |_row, _folded, _window, _cx| div(),
19171 );
19172
19173 editor.insert_creases(Some(crease), cx);
19174 let snapshot = editor.snapshot(window, cx);
19175 let _div = snapshot.render_crease_toggle(
19176 MultiBufferRow(1),
19177 false,
19178 cx.entity().clone(),
19179 window,
19180 cx,
19181 );
19182 snapshot
19183 })
19184 .unwrap();
19185
19186 let render_args = render_args.lock().take().unwrap();
19187 assert_eq!(render_args.row, MultiBufferRow(1));
19188 assert!(!render_args.folded);
19189 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19190
19191 cx.update_window(*editor, |_, window, cx| {
19192 (render_args.callback)(true, window, cx)
19193 })
19194 .unwrap();
19195 let snapshot = editor
19196 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19197 .unwrap();
19198 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
19199
19200 cx.update_window(*editor, |_, window, cx| {
19201 (render_args.callback)(false, window, cx)
19202 })
19203 .unwrap();
19204 let snapshot = editor
19205 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19206 .unwrap();
19207 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19208}
19209
19210#[gpui::test]
19211async fn test_input_text(cx: &mut TestAppContext) {
19212 init_test(cx, |_| {});
19213 let mut cx = EditorTestContext::new(cx).await;
19214
19215 cx.set_state(
19216 &r#"ˇone
19217 two
19218
19219 three
19220 fourˇ
19221 five
19222
19223 siˇx"#
19224 .unindent(),
19225 );
19226
19227 cx.dispatch_action(HandleInput(String::new()));
19228 cx.assert_editor_state(
19229 &r#"ˇone
19230 two
19231
19232 three
19233 fourˇ
19234 five
19235
19236 siˇx"#
19237 .unindent(),
19238 );
19239
19240 cx.dispatch_action(HandleInput("AAAA".to_string()));
19241 cx.assert_editor_state(
19242 &r#"AAAAˇone
19243 two
19244
19245 three
19246 fourAAAAˇ
19247 five
19248
19249 siAAAAˇx"#
19250 .unindent(),
19251 );
19252}
19253
19254#[gpui::test]
19255async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
19256 init_test(cx, |_| {});
19257
19258 let mut cx = EditorTestContext::new(cx).await;
19259 cx.set_state(
19260 r#"let foo = 1;
19261let foo = 2;
19262let foo = 3;
19263let fooˇ = 4;
19264let foo = 5;
19265let foo = 6;
19266let foo = 7;
19267let foo = 8;
19268let foo = 9;
19269let foo = 10;
19270let foo = 11;
19271let foo = 12;
19272let foo = 13;
19273let foo = 14;
19274let foo = 15;"#,
19275 );
19276
19277 cx.update_editor(|e, window, cx| {
19278 assert_eq!(
19279 e.next_scroll_position,
19280 NextScrollCursorCenterTopBottom::Center,
19281 "Default next scroll direction is center",
19282 );
19283
19284 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19285 assert_eq!(
19286 e.next_scroll_position,
19287 NextScrollCursorCenterTopBottom::Top,
19288 "After center, next scroll direction should be top",
19289 );
19290
19291 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19292 assert_eq!(
19293 e.next_scroll_position,
19294 NextScrollCursorCenterTopBottom::Bottom,
19295 "After top, next scroll direction should be bottom",
19296 );
19297
19298 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19299 assert_eq!(
19300 e.next_scroll_position,
19301 NextScrollCursorCenterTopBottom::Center,
19302 "After bottom, scrolling should start over",
19303 );
19304
19305 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19306 assert_eq!(
19307 e.next_scroll_position,
19308 NextScrollCursorCenterTopBottom::Top,
19309 "Scrolling continues if retriggered fast enough"
19310 );
19311 });
19312
19313 cx.executor()
19314 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
19315 cx.executor().run_until_parked();
19316 cx.update_editor(|e, _, _| {
19317 assert_eq!(
19318 e.next_scroll_position,
19319 NextScrollCursorCenterTopBottom::Center,
19320 "If scrolling is not triggered fast enough, it should reset"
19321 );
19322 });
19323}
19324
19325#[gpui::test]
19326async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
19327 init_test(cx, |_| {});
19328 let mut cx = EditorLspTestContext::new_rust(
19329 lsp::ServerCapabilities {
19330 definition_provider: Some(lsp::OneOf::Left(true)),
19331 references_provider: Some(lsp::OneOf::Left(true)),
19332 ..lsp::ServerCapabilities::default()
19333 },
19334 cx,
19335 )
19336 .await;
19337
19338 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
19339 let go_to_definition = cx
19340 .lsp
19341 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19342 move |params, _| async move {
19343 if empty_go_to_definition {
19344 Ok(None)
19345 } else {
19346 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
19347 uri: params.text_document_position_params.text_document.uri,
19348 range: lsp::Range::new(
19349 lsp::Position::new(4, 3),
19350 lsp::Position::new(4, 6),
19351 ),
19352 })))
19353 }
19354 },
19355 );
19356 let references = cx
19357 .lsp
19358 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
19359 Ok(Some(vec![lsp::Location {
19360 uri: params.text_document_position.text_document.uri,
19361 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
19362 }]))
19363 });
19364 (go_to_definition, references)
19365 };
19366
19367 cx.set_state(
19368 &r#"fn one() {
19369 let mut a = ˇtwo();
19370 }
19371
19372 fn two() {}"#
19373 .unindent(),
19374 );
19375 set_up_lsp_handlers(false, &mut cx);
19376 let navigated = cx
19377 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19378 .await
19379 .expect("Failed to navigate to definition");
19380 assert_eq!(
19381 navigated,
19382 Navigated::Yes,
19383 "Should have navigated to definition from the GetDefinition response"
19384 );
19385 cx.assert_editor_state(
19386 &r#"fn one() {
19387 let mut a = two();
19388 }
19389
19390 fn «twoˇ»() {}"#
19391 .unindent(),
19392 );
19393
19394 let editors = cx.update_workspace(|workspace, _, cx| {
19395 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19396 });
19397 cx.update_editor(|_, _, test_editor_cx| {
19398 assert_eq!(
19399 editors.len(),
19400 1,
19401 "Initially, only one, test, editor should be open in the workspace"
19402 );
19403 assert_eq!(
19404 test_editor_cx.entity(),
19405 editors.last().expect("Asserted len is 1").clone()
19406 );
19407 });
19408
19409 set_up_lsp_handlers(true, &mut cx);
19410 let navigated = cx
19411 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19412 .await
19413 .expect("Failed to navigate to lookup references");
19414 assert_eq!(
19415 navigated,
19416 Navigated::Yes,
19417 "Should have navigated to references as a fallback after empty GoToDefinition response"
19418 );
19419 // We should not change the selections in the existing file,
19420 // if opening another milti buffer with the references
19421 cx.assert_editor_state(
19422 &r#"fn one() {
19423 let mut a = two();
19424 }
19425
19426 fn «twoˇ»() {}"#
19427 .unindent(),
19428 );
19429 let editors = cx.update_workspace(|workspace, _, cx| {
19430 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19431 });
19432 cx.update_editor(|_, _, test_editor_cx| {
19433 assert_eq!(
19434 editors.len(),
19435 2,
19436 "After falling back to references search, we open a new editor with the results"
19437 );
19438 let references_fallback_text = editors
19439 .into_iter()
19440 .find(|new_editor| *new_editor != test_editor_cx.entity())
19441 .expect("Should have one non-test editor now")
19442 .read(test_editor_cx)
19443 .text(test_editor_cx);
19444 assert_eq!(
19445 references_fallback_text, "fn one() {\n let mut a = two();\n}",
19446 "Should use the range from the references response and not the GoToDefinition one"
19447 );
19448 });
19449}
19450
19451#[gpui::test]
19452async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
19453 init_test(cx, |_| {});
19454 cx.update(|cx| {
19455 let mut editor_settings = EditorSettings::get_global(cx).clone();
19456 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
19457 EditorSettings::override_global(editor_settings, cx);
19458 });
19459 let mut cx = EditorLspTestContext::new_rust(
19460 lsp::ServerCapabilities {
19461 definition_provider: Some(lsp::OneOf::Left(true)),
19462 references_provider: Some(lsp::OneOf::Left(true)),
19463 ..lsp::ServerCapabilities::default()
19464 },
19465 cx,
19466 )
19467 .await;
19468 let original_state = r#"fn one() {
19469 let mut a = ˇtwo();
19470 }
19471
19472 fn two() {}"#
19473 .unindent();
19474 cx.set_state(&original_state);
19475
19476 let mut go_to_definition = cx
19477 .lsp
19478 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19479 move |_, _| async move { Ok(None) },
19480 );
19481 let _references = cx
19482 .lsp
19483 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
19484 panic!("Should not call for references with no go to definition fallback")
19485 });
19486
19487 let navigated = cx
19488 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19489 .await
19490 .expect("Failed to navigate to lookup references");
19491 go_to_definition
19492 .next()
19493 .await
19494 .expect("Should have called the go_to_definition handler");
19495
19496 assert_eq!(
19497 navigated,
19498 Navigated::No,
19499 "Should have navigated to references as a fallback after empty GoToDefinition response"
19500 );
19501 cx.assert_editor_state(&original_state);
19502 let editors = cx.update_workspace(|workspace, _, cx| {
19503 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19504 });
19505 cx.update_editor(|_, _, _| {
19506 assert_eq!(
19507 editors.len(),
19508 1,
19509 "After unsuccessful fallback, no other editor should have been opened"
19510 );
19511 });
19512}
19513
19514#[gpui::test]
19515async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
19516 init_test(cx, |_| {});
19517
19518 let language = Arc::new(Language::new(
19519 LanguageConfig::default(),
19520 Some(tree_sitter_rust::LANGUAGE.into()),
19521 ));
19522
19523 let text = r#"
19524 #[cfg(test)]
19525 mod tests() {
19526 #[test]
19527 fn runnable_1() {
19528 let a = 1;
19529 }
19530
19531 #[test]
19532 fn runnable_2() {
19533 let a = 1;
19534 let b = 2;
19535 }
19536 }
19537 "#
19538 .unindent();
19539
19540 let fs = FakeFs::new(cx.executor());
19541 fs.insert_file("/file.rs", Default::default()).await;
19542
19543 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19544 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19545 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19546 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
19547 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
19548
19549 let editor = cx.new_window_entity(|window, cx| {
19550 Editor::new(
19551 EditorMode::full(),
19552 multi_buffer,
19553 Some(project.clone()),
19554 window,
19555 cx,
19556 )
19557 });
19558
19559 editor.update_in(cx, |editor, window, cx| {
19560 let snapshot = editor.buffer().read(cx).snapshot(cx);
19561 editor.tasks.insert(
19562 (buffer.read(cx).remote_id(), 3),
19563 RunnableTasks {
19564 templates: vec![],
19565 offset: snapshot.anchor_before(43),
19566 column: 0,
19567 extra_variables: HashMap::default(),
19568 context_range: BufferOffset(43)..BufferOffset(85),
19569 },
19570 );
19571 editor.tasks.insert(
19572 (buffer.read(cx).remote_id(), 8),
19573 RunnableTasks {
19574 templates: vec![],
19575 offset: snapshot.anchor_before(86),
19576 column: 0,
19577 extra_variables: HashMap::default(),
19578 context_range: BufferOffset(86)..BufferOffset(191),
19579 },
19580 );
19581
19582 // Test finding task when cursor is inside function body
19583 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19584 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
19585 });
19586 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19587 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
19588
19589 // Test finding task when cursor is on function name
19590 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19591 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
19592 });
19593 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19594 assert_eq!(row, 8, "Should find task when cursor is on function name");
19595 });
19596}
19597
19598#[gpui::test]
19599async fn test_folding_buffers(cx: &mut TestAppContext) {
19600 init_test(cx, |_| {});
19601
19602 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19603 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
19604 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
19605
19606 let fs = FakeFs::new(cx.executor());
19607 fs.insert_tree(
19608 path!("/a"),
19609 json!({
19610 "first.rs": sample_text_1,
19611 "second.rs": sample_text_2,
19612 "third.rs": sample_text_3,
19613 }),
19614 )
19615 .await;
19616 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19617 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19618 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19619 let worktree = project.update(cx, |project, cx| {
19620 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19621 assert_eq!(worktrees.len(), 1);
19622 worktrees.pop().unwrap()
19623 });
19624 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19625
19626 let buffer_1 = project
19627 .update(cx, |project, cx| {
19628 project.open_buffer((worktree_id, "first.rs"), cx)
19629 })
19630 .await
19631 .unwrap();
19632 let buffer_2 = project
19633 .update(cx, |project, cx| {
19634 project.open_buffer((worktree_id, "second.rs"), cx)
19635 })
19636 .await
19637 .unwrap();
19638 let buffer_3 = project
19639 .update(cx, |project, cx| {
19640 project.open_buffer((worktree_id, "third.rs"), cx)
19641 })
19642 .await
19643 .unwrap();
19644
19645 let multi_buffer = cx.new(|cx| {
19646 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19647 multi_buffer.push_excerpts(
19648 buffer_1.clone(),
19649 [
19650 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19651 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19652 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19653 ],
19654 cx,
19655 );
19656 multi_buffer.push_excerpts(
19657 buffer_2.clone(),
19658 [
19659 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19660 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19661 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19662 ],
19663 cx,
19664 );
19665 multi_buffer.push_excerpts(
19666 buffer_3.clone(),
19667 [
19668 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19669 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19670 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19671 ],
19672 cx,
19673 );
19674 multi_buffer
19675 });
19676 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19677 Editor::new(
19678 EditorMode::full(),
19679 multi_buffer.clone(),
19680 Some(project.clone()),
19681 window,
19682 cx,
19683 )
19684 });
19685
19686 assert_eq!(
19687 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19688 "\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",
19689 );
19690
19691 multi_buffer_editor.update(cx, |editor, cx| {
19692 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19693 });
19694 assert_eq!(
19695 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19696 "\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",
19697 "After folding the first buffer, its text should not be displayed"
19698 );
19699
19700 multi_buffer_editor.update(cx, |editor, cx| {
19701 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19702 });
19703 assert_eq!(
19704 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19705 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
19706 "After folding the second buffer, its text should not be displayed"
19707 );
19708
19709 multi_buffer_editor.update(cx, |editor, cx| {
19710 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19711 });
19712 assert_eq!(
19713 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19714 "\n\n\n\n\n",
19715 "After folding the third buffer, its text should not be displayed"
19716 );
19717
19718 // Emulate selection inside the fold logic, that should work
19719 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19720 editor
19721 .snapshot(window, cx)
19722 .next_line_boundary(Point::new(0, 4));
19723 });
19724
19725 multi_buffer_editor.update(cx, |editor, cx| {
19726 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19727 });
19728 assert_eq!(
19729 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19730 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19731 "After unfolding the second buffer, its text should be displayed"
19732 );
19733
19734 // Typing inside of buffer 1 causes that buffer to be unfolded.
19735 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19736 assert_eq!(
19737 multi_buffer
19738 .read(cx)
19739 .snapshot(cx)
19740 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
19741 .collect::<String>(),
19742 "bbbb"
19743 );
19744 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
19745 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
19746 });
19747 editor.handle_input("B", window, cx);
19748 });
19749
19750 assert_eq!(
19751 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19752 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19753 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
19754 );
19755
19756 multi_buffer_editor.update(cx, |editor, cx| {
19757 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19758 });
19759 assert_eq!(
19760 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19761 "\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",
19762 "After unfolding the all buffers, all original text should be displayed"
19763 );
19764}
19765
19766#[gpui::test]
19767async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
19768 init_test(cx, |_| {});
19769
19770 let sample_text_1 = "1111\n2222\n3333".to_string();
19771 let sample_text_2 = "4444\n5555\n6666".to_string();
19772 let sample_text_3 = "7777\n8888\n9999".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 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19818 cx,
19819 );
19820 multi_buffer.push_excerpts(
19821 buffer_2.clone(),
19822 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19823 cx,
19824 );
19825 multi_buffer.push_excerpts(
19826 buffer_3.clone(),
19827 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19828 cx,
19829 );
19830 multi_buffer
19831 });
19832
19833 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19834 Editor::new(
19835 EditorMode::full(),
19836 multi_buffer,
19837 Some(project.clone()),
19838 window,
19839 cx,
19840 )
19841 });
19842
19843 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
19844 assert_eq!(
19845 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19846 full_text,
19847 );
19848
19849 multi_buffer_editor.update(cx, |editor, cx| {
19850 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19851 });
19852 assert_eq!(
19853 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19854 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
19855 "After folding the first buffer, its text should not be displayed"
19856 );
19857
19858 multi_buffer_editor.update(cx, |editor, cx| {
19859 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19860 });
19861
19862 assert_eq!(
19863 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19864 "\n\n\n\n\n\n7777\n8888\n9999",
19865 "After folding the second buffer, its text should not be displayed"
19866 );
19867
19868 multi_buffer_editor.update(cx, |editor, cx| {
19869 editor.fold_buffer(buffer_3.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",
19874 "After folding the third buffer, its text should not be displayed"
19875 );
19876
19877 multi_buffer_editor.update(cx, |editor, cx| {
19878 editor.unfold_buffer(buffer_2.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\n4444\n5555\n6666\n\n",
19883 "After unfolding the second buffer, its text should be displayed"
19884 );
19885
19886 multi_buffer_editor.update(cx, |editor, cx| {
19887 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
19888 });
19889 assert_eq!(
19890 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19891 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
19892 "After unfolding the first buffer, its text should be displayed"
19893 );
19894
19895 multi_buffer_editor.update(cx, |editor, cx| {
19896 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19897 });
19898 assert_eq!(
19899 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19900 full_text,
19901 "After unfolding all buffers, all original text should be displayed"
19902 );
19903}
19904
19905#[gpui::test]
19906async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
19907 init_test(cx, |_| {});
19908
19909 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19910
19911 let fs = FakeFs::new(cx.executor());
19912 fs.insert_tree(
19913 path!("/a"),
19914 json!({
19915 "main.rs": sample_text,
19916 }),
19917 )
19918 .await;
19919 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19920 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19921 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19922 let worktree = project.update(cx, |project, cx| {
19923 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19924 assert_eq!(worktrees.len(), 1);
19925 worktrees.pop().unwrap()
19926 });
19927 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19928
19929 let buffer_1 = project
19930 .update(cx, |project, cx| {
19931 project.open_buffer((worktree_id, "main.rs"), cx)
19932 })
19933 .await
19934 .unwrap();
19935
19936 let multi_buffer = cx.new(|cx| {
19937 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19938 multi_buffer.push_excerpts(
19939 buffer_1.clone(),
19940 [ExcerptRange::new(
19941 Point::new(0, 0)
19942 ..Point::new(
19943 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
19944 0,
19945 ),
19946 )],
19947 cx,
19948 );
19949 multi_buffer
19950 });
19951 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19952 Editor::new(
19953 EditorMode::full(),
19954 multi_buffer,
19955 Some(project.clone()),
19956 window,
19957 cx,
19958 )
19959 });
19960
19961 let selection_range = Point::new(1, 0)..Point::new(2, 0);
19962 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19963 enum TestHighlight {}
19964 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
19965 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
19966 editor.highlight_text::<TestHighlight>(
19967 vec![highlight_range.clone()],
19968 HighlightStyle::color(Hsla::green()),
19969 cx,
19970 );
19971 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19972 s.select_ranges(Some(highlight_range))
19973 });
19974 });
19975
19976 let full_text = format!("\n\n{sample_text}");
19977 assert_eq!(
19978 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19979 full_text,
19980 );
19981}
19982
19983#[gpui::test]
19984async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
19985 init_test(cx, |_| {});
19986 cx.update(|cx| {
19987 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
19988 "keymaps/default-linux.json",
19989 cx,
19990 )
19991 .unwrap();
19992 cx.bind_keys(default_key_bindings);
19993 });
19994
19995 let (editor, cx) = cx.add_window_view(|window, cx| {
19996 let multi_buffer = MultiBuffer::build_multi(
19997 [
19998 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
19999 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
20000 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
20001 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
20002 ],
20003 cx,
20004 );
20005 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
20006
20007 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
20008 // fold all but the second buffer, so that we test navigating between two
20009 // adjacent folded buffers, as well as folded buffers at the start and
20010 // end the multibuffer
20011 editor.fold_buffer(buffer_ids[0], cx);
20012 editor.fold_buffer(buffer_ids[2], cx);
20013 editor.fold_buffer(buffer_ids[3], cx);
20014
20015 editor
20016 });
20017 cx.simulate_resize(size(px(1000.), px(1000.)));
20018
20019 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
20020 cx.assert_excerpts_with_selections(indoc! {"
20021 [EXCERPT]
20022 ˇ[FOLDED]
20023 [EXCERPT]
20024 a1
20025 b1
20026 [EXCERPT]
20027 [FOLDED]
20028 [EXCERPT]
20029 [FOLDED]
20030 "
20031 });
20032 cx.simulate_keystroke("down");
20033 cx.assert_excerpts_with_selections(indoc! {"
20034 [EXCERPT]
20035 [FOLDED]
20036 [EXCERPT]
20037 ˇa1
20038 b1
20039 [EXCERPT]
20040 [FOLDED]
20041 [EXCERPT]
20042 [FOLDED]
20043 "
20044 });
20045 cx.simulate_keystroke("down");
20046 cx.assert_excerpts_with_selections(indoc! {"
20047 [EXCERPT]
20048 [FOLDED]
20049 [EXCERPT]
20050 a1
20051 ˇb1
20052 [EXCERPT]
20053 [FOLDED]
20054 [EXCERPT]
20055 [FOLDED]
20056 "
20057 });
20058 cx.simulate_keystroke("down");
20059 cx.assert_excerpts_with_selections(indoc! {"
20060 [EXCERPT]
20061 [FOLDED]
20062 [EXCERPT]
20063 a1
20064 b1
20065 ˇ[EXCERPT]
20066 [FOLDED]
20067 [EXCERPT]
20068 [FOLDED]
20069 "
20070 });
20071 cx.simulate_keystroke("down");
20072 cx.assert_excerpts_with_selections(indoc! {"
20073 [EXCERPT]
20074 [FOLDED]
20075 [EXCERPT]
20076 a1
20077 b1
20078 [EXCERPT]
20079 ˇ[FOLDED]
20080 [EXCERPT]
20081 [FOLDED]
20082 "
20083 });
20084 for _ in 0..5 {
20085 cx.simulate_keystroke("down");
20086 cx.assert_excerpts_with_selections(indoc! {"
20087 [EXCERPT]
20088 [FOLDED]
20089 [EXCERPT]
20090 a1
20091 b1
20092 [EXCERPT]
20093 [FOLDED]
20094 [EXCERPT]
20095 ˇ[FOLDED]
20096 "
20097 });
20098 }
20099
20100 cx.simulate_keystroke("up");
20101 cx.assert_excerpts_with_selections(indoc! {"
20102 [EXCERPT]
20103 [FOLDED]
20104 [EXCERPT]
20105 a1
20106 b1
20107 [EXCERPT]
20108 ˇ[FOLDED]
20109 [EXCERPT]
20110 [FOLDED]
20111 "
20112 });
20113 cx.simulate_keystroke("up");
20114 cx.assert_excerpts_with_selections(indoc! {"
20115 [EXCERPT]
20116 [FOLDED]
20117 [EXCERPT]
20118 a1
20119 b1
20120 ˇ[EXCERPT]
20121 [FOLDED]
20122 [EXCERPT]
20123 [FOLDED]
20124 "
20125 });
20126 cx.simulate_keystroke("up");
20127 cx.assert_excerpts_with_selections(indoc! {"
20128 [EXCERPT]
20129 [FOLDED]
20130 [EXCERPT]
20131 a1
20132 ˇb1
20133 [EXCERPT]
20134 [FOLDED]
20135 [EXCERPT]
20136 [FOLDED]
20137 "
20138 });
20139 cx.simulate_keystroke("up");
20140 cx.assert_excerpts_with_selections(indoc! {"
20141 [EXCERPT]
20142 [FOLDED]
20143 [EXCERPT]
20144 ˇa1
20145 b1
20146 [EXCERPT]
20147 [FOLDED]
20148 [EXCERPT]
20149 [FOLDED]
20150 "
20151 });
20152 for _ in 0..5 {
20153 cx.simulate_keystroke("up");
20154 cx.assert_excerpts_with_selections(indoc! {"
20155 [EXCERPT]
20156 ˇ[FOLDED]
20157 [EXCERPT]
20158 a1
20159 b1
20160 [EXCERPT]
20161 [FOLDED]
20162 [EXCERPT]
20163 [FOLDED]
20164 "
20165 });
20166 }
20167}
20168
20169#[gpui::test]
20170async fn test_inline_completion_text(cx: &mut TestAppContext) {
20171 init_test(cx, |_| {});
20172
20173 // Simple insertion
20174 assert_highlighted_edits(
20175 "Hello, world!",
20176 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
20177 true,
20178 cx,
20179 |highlighted_edits, cx| {
20180 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
20181 assert_eq!(highlighted_edits.highlights.len(), 1);
20182 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
20183 assert_eq!(
20184 highlighted_edits.highlights[0].1.background_color,
20185 Some(cx.theme().status().created_background)
20186 );
20187 },
20188 )
20189 .await;
20190
20191 // Replacement
20192 assert_highlighted_edits(
20193 "This is a test.",
20194 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
20195 false,
20196 cx,
20197 |highlighted_edits, cx| {
20198 assert_eq!(highlighted_edits.text, "That is a test.");
20199 assert_eq!(highlighted_edits.highlights.len(), 1);
20200 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
20201 assert_eq!(
20202 highlighted_edits.highlights[0].1.background_color,
20203 Some(cx.theme().status().created_background)
20204 );
20205 },
20206 )
20207 .await;
20208
20209 // Multiple edits
20210 assert_highlighted_edits(
20211 "Hello, world!",
20212 vec![
20213 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
20214 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
20215 ],
20216 false,
20217 cx,
20218 |highlighted_edits, cx| {
20219 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
20220 assert_eq!(highlighted_edits.highlights.len(), 2);
20221 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
20222 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
20223 assert_eq!(
20224 highlighted_edits.highlights[0].1.background_color,
20225 Some(cx.theme().status().created_background)
20226 );
20227 assert_eq!(
20228 highlighted_edits.highlights[1].1.background_color,
20229 Some(cx.theme().status().created_background)
20230 );
20231 },
20232 )
20233 .await;
20234
20235 // Multiple lines with edits
20236 assert_highlighted_edits(
20237 "First line\nSecond line\nThird line\nFourth line",
20238 vec![
20239 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
20240 (
20241 Point::new(2, 0)..Point::new(2, 10),
20242 "New third line".to_string(),
20243 ),
20244 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
20245 ],
20246 false,
20247 cx,
20248 |highlighted_edits, cx| {
20249 assert_eq!(
20250 highlighted_edits.text,
20251 "Second modified\nNew third line\nFourth updated line"
20252 );
20253 assert_eq!(highlighted_edits.highlights.len(), 3);
20254 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
20255 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
20256 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
20257 for highlight in &highlighted_edits.highlights {
20258 assert_eq!(
20259 highlight.1.background_color,
20260 Some(cx.theme().status().created_background)
20261 );
20262 }
20263 },
20264 )
20265 .await;
20266}
20267
20268#[gpui::test]
20269async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
20270 init_test(cx, |_| {});
20271
20272 // Deletion
20273 assert_highlighted_edits(
20274 "Hello, world!",
20275 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
20276 true,
20277 cx,
20278 |highlighted_edits, cx| {
20279 assert_eq!(highlighted_edits.text, "Hello, world!");
20280 assert_eq!(highlighted_edits.highlights.len(), 1);
20281 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
20282 assert_eq!(
20283 highlighted_edits.highlights[0].1.background_color,
20284 Some(cx.theme().status().deleted_background)
20285 );
20286 },
20287 )
20288 .await;
20289
20290 // Insertion
20291 assert_highlighted_edits(
20292 "Hello, world!",
20293 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
20294 true,
20295 cx,
20296 |highlighted_edits, cx| {
20297 assert_eq!(highlighted_edits.highlights.len(), 1);
20298 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
20299 assert_eq!(
20300 highlighted_edits.highlights[0].1.background_color,
20301 Some(cx.theme().status().created_background)
20302 );
20303 },
20304 )
20305 .await;
20306}
20307
20308async fn assert_highlighted_edits(
20309 text: &str,
20310 edits: Vec<(Range<Point>, String)>,
20311 include_deletions: bool,
20312 cx: &mut TestAppContext,
20313 assertion_fn: impl Fn(HighlightedText, &App),
20314) {
20315 let window = cx.add_window(|window, cx| {
20316 let buffer = MultiBuffer::build_simple(text, cx);
20317 Editor::new(EditorMode::full(), buffer, None, window, cx)
20318 });
20319 let cx = &mut VisualTestContext::from_window(*window, cx);
20320
20321 let (buffer, snapshot) = window
20322 .update(cx, |editor, _window, cx| {
20323 (
20324 editor.buffer().clone(),
20325 editor.buffer().read(cx).snapshot(cx),
20326 )
20327 })
20328 .unwrap();
20329
20330 let edits = edits
20331 .into_iter()
20332 .map(|(range, edit)| {
20333 (
20334 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
20335 edit,
20336 )
20337 })
20338 .collect::<Vec<_>>();
20339
20340 let text_anchor_edits = edits
20341 .clone()
20342 .into_iter()
20343 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
20344 .collect::<Vec<_>>();
20345
20346 let edit_preview = window
20347 .update(cx, |_, _window, cx| {
20348 buffer
20349 .read(cx)
20350 .as_singleton()
20351 .unwrap()
20352 .read(cx)
20353 .preview_edits(text_anchor_edits.into(), cx)
20354 })
20355 .unwrap()
20356 .await;
20357
20358 cx.update(|_window, cx| {
20359 let highlighted_edits = inline_completion_edit_text(
20360 &snapshot.as_singleton().unwrap().2,
20361 &edits,
20362 &edit_preview,
20363 include_deletions,
20364 cx,
20365 );
20366 assertion_fn(highlighted_edits, cx)
20367 });
20368}
20369
20370#[track_caller]
20371fn assert_breakpoint(
20372 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
20373 path: &Arc<Path>,
20374 expected: Vec<(u32, Breakpoint)>,
20375) {
20376 if expected.len() == 0usize {
20377 assert!(!breakpoints.contains_key(path), "{}", path.display());
20378 } else {
20379 let mut breakpoint = breakpoints
20380 .get(path)
20381 .unwrap()
20382 .into_iter()
20383 .map(|breakpoint| {
20384 (
20385 breakpoint.row,
20386 Breakpoint {
20387 message: breakpoint.message.clone(),
20388 state: breakpoint.state,
20389 condition: breakpoint.condition.clone(),
20390 hit_condition: breakpoint.hit_condition.clone(),
20391 },
20392 )
20393 })
20394 .collect::<Vec<_>>();
20395
20396 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
20397
20398 assert_eq!(expected, breakpoint);
20399 }
20400}
20401
20402fn add_log_breakpoint_at_cursor(
20403 editor: &mut Editor,
20404 log_message: &str,
20405 window: &mut Window,
20406 cx: &mut Context<Editor>,
20407) {
20408 let (anchor, bp) = editor
20409 .breakpoints_at_cursors(window, cx)
20410 .first()
20411 .and_then(|(anchor, bp)| {
20412 if let Some(bp) = bp {
20413 Some((*anchor, bp.clone()))
20414 } else {
20415 None
20416 }
20417 })
20418 .unwrap_or_else(|| {
20419 let cursor_position: Point = editor.selections.newest(cx).head();
20420
20421 let breakpoint_position = editor
20422 .snapshot(window, cx)
20423 .display_snapshot
20424 .buffer_snapshot
20425 .anchor_before(Point::new(cursor_position.row, 0));
20426
20427 (breakpoint_position, Breakpoint::new_log(&log_message))
20428 });
20429
20430 editor.edit_breakpoint_at_anchor(
20431 anchor,
20432 bp,
20433 BreakpointEditAction::EditLogMessage(log_message.into()),
20434 cx,
20435 );
20436}
20437
20438#[gpui::test]
20439async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
20440 init_test(cx, |_| {});
20441
20442 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20443 let fs = FakeFs::new(cx.executor());
20444 fs.insert_tree(
20445 path!("/a"),
20446 json!({
20447 "main.rs": sample_text,
20448 }),
20449 )
20450 .await;
20451 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20452 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20453 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20454
20455 let fs = FakeFs::new(cx.executor());
20456 fs.insert_tree(
20457 path!("/a"),
20458 json!({
20459 "main.rs": sample_text,
20460 }),
20461 )
20462 .await;
20463 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20464 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20465 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20466 let worktree_id = workspace
20467 .update(cx, |workspace, _window, cx| {
20468 workspace.project().update(cx, |project, cx| {
20469 project.worktrees(cx).next().unwrap().read(cx).id()
20470 })
20471 })
20472 .unwrap();
20473
20474 let buffer = project
20475 .update(cx, |project, cx| {
20476 project.open_buffer((worktree_id, "main.rs"), cx)
20477 })
20478 .await
20479 .unwrap();
20480
20481 let (editor, cx) = cx.add_window_view(|window, cx| {
20482 Editor::new(
20483 EditorMode::full(),
20484 MultiBuffer::build_from_buffer(buffer, cx),
20485 Some(project.clone()),
20486 window,
20487 cx,
20488 )
20489 });
20490
20491 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20492 let abs_path = project.read_with(cx, |project, cx| {
20493 project
20494 .absolute_path(&project_path, cx)
20495 .map(|path_buf| Arc::from(path_buf.to_owned()))
20496 .unwrap()
20497 });
20498
20499 // assert we can add breakpoint on the first line
20500 editor.update_in(cx, |editor, window, cx| {
20501 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20502 editor.move_to_end(&MoveToEnd, window, cx);
20503 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20504 });
20505
20506 let breakpoints = editor.update(cx, |editor, cx| {
20507 editor
20508 .breakpoint_store()
20509 .as_ref()
20510 .unwrap()
20511 .read(cx)
20512 .all_source_breakpoints(cx)
20513 .clone()
20514 });
20515
20516 assert_eq!(1, breakpoints.len());
20517 assert_breakpoint(
20518 &breakpoints,
20519 &abs_path,
20520 vec![
20521 (0, Breakpoint::new_standard()),
20522 (3, Breakpoint::new_standard()),
20523 ],
20524 );
20525
20526 editor.update_in(cx, |editor, window, cx| {
20527 editor.move_to_beginning(&MoveToBeginning, window, cx);
20528 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20529 });
20530
20531 let breakpoints = editor.update(cx, |editor, cx| {
20532 editor
20533 .breakpoint_store()
20534 .as_ref()
20535 .unwrap()
20536 .read(cx)
20537 .all_source_breakpoints(cx)
20538 .clone()
20539 });
20540
20541 assert_eq!(1, breakpoints.len());
20542 assert_breakpoint(
20543 &breakpoints,
20544 &abs_path,
20545 vec![(3, Breakpoint::new_standard())],
20546 );
20547
20548 editor.update_in(cx, |editor, window, cx| {
20549 editor.move_to_end(&MoveToEnd, window, cx);
20550 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20551 });
20552
20553 let breakpoints = editor.update(cx, |editor, cx| {
20554 editor
20555 .breakpoint_store()
20556 .as_ref()
20557 .unwrap()
20558 .read(cx)
20559 .all_source_breakpoints(cx)
20560 .clone()
20561 });
20562
20563 assert_eq!(0, breakpoints.len());
20564 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20565}
20566
20567#[gpui::test]
20568async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
20569 init_test(cx, |_| {});
20570
20571 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20572
20573 let fs = FakeFs::new(cx.executor());
20574 fs.insert_tree(
20575 path!("/a"),
20576 json!({
20577 "main.rs": sample_text,
20578 }),
20579 )
20580 .await;
20581 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20582 let (workspace, cx) =
20583 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20584
20585 let worktree_id = workspace.update(cx, |workspace, cx| {
20586 workspace.project().update(cx, |project, cx| {
20587 project.worktrees(cx).next().unwrap().read(cx).id()
20588 })
20589 });
20590
20591 let buffer = project
20592 .update(cx, |project, cx| {
20593 project.open_buffer((worktree_id, "main.rs"), cx)
20594 })
20595 .await
20596 .unwrap();
20597
20598 let (editor, cx) = cx.add_window_view(|window, cx| {
20599 Editor::new(
20600 EditorMode::full(),
20601 MultiBuffer::build_from_buffer(buffer, cx),
20602 Some(project.clone()),
20603 window,
20604 cx,
20605 )
20606 });
20607
20608 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20609 let abs_path = project.read_with(cx, |project, cx| {
20610 project
20611 .absolute_path(&project_path, cx)
20612 .map(|path_buf| Arc::from(path_buf.to_owned()))
20613 .unwrap()
20614 });
20615
20616 editor.update_in(cx, |editor, window, cx| {
20617 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20618 });
20619
20620 let breakpoints = editor.update(cx, |editor, cx| {
20621 editor
20622 .breakpoint_store()
20623 .as_ref()
20624 .unwrap()
20625 .read(cx)
20626 .all_source_breakpoints(cx)
20627 .clone()
20628 });
20629
20630 assert_breakpoint(
20631 &breakpoints,
20632 &abs_path,
20633 vec![(0, Breakpoint::new_log("hello world"))],
20634 );
20635
20636 // Removing a log message from a log breakpoint should remove it
20637 editor.update_in(cx, |editor, window, cx| {
20638 add_log_breakpoint_at_cursor(editor, "", window, cx);
20639 });
20640
20641 let breakpoints = editor.update(cx, |editor, cx| {
20642 editor
20643 .breakpoint_store()
20644 .as_ref()
20645 .unwrap()
20646 .read(cx)
20647 .all_source_breakpoints(cx)
20648 .clone()
20649 });
20650
20651 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20652
20653 editor.update_in(cx, |editor, window, cx| {
20654 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20655 editor.move_to_end(&MoveToEnd, window, cx);
20656 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20657 // Not adding a log message to a standard breakpoint shouldn't remove it
20658 add_log_breakpoint_at_cursor(editor, "", window, cx);
20659 });
20660
20661 let breakpoints = editor.update(cx, |editor, cx| {
20662 editor
20663 .breakpoint_store()
20664 .as_ref()
20665 .unwrap()
20666 .read(cx)
20667 .all_source_breakpoints(cx)
20668 .clone()
20669 });
20670
20671 assert_breakpoint(
20672 &breakpoints,
20673 &abs_path,
20674 vec![
20675 (0, Breakpoint::new_standard()),
20676 (3, Breakpoint::new_standard()),
20677 ],
20678 );
20679
20680 editor.update_in(cx, |editor, window, cx| {
20681 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20682 });
20683
20684 let breakpoints = editor.update(cx, |editor, cx| {
20685 editor
20686 .breakpoint_store()
20687 .as_ref()
20688 .unwrap()
20689 .read(cx)
20690 .all_source_breakpoints(cx)
20691 .clone()
20692 });
20693
20694 assert_breakpoint(
20695 &breakpoints,
20696 &abs_path,
20697 vec![
20698 (0, Breakpoint::new_standard()),
20699 (3, Breakpoint::new_log("hello world")),
20700 ],
20701 );
20702
20703 editor.update_in(cx, |editor, window, cx| {
20704 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
20705 });
20706
20707 let breakpoints = editor.update(cx, |editor, cx| {
20708 editor
20709 .breakpoint_store()
20710 .as_ref()
20711 .unwrap()
20712 .read(cx)
20713 .all_source_breakpoints(cx)
20714 .clone()
20715 });
20716
20717 assert_breakpoint(
20718 &breakpoints,
20719 &abs_path,
20720 vec![
20721 (0, Breakpoint::new_standard()),
20722 (3, Breakpoint::new_log("hello Earth!!")),
20723 ],
20724 );
20725}
20726
20727/// This also tests that Editor::breakpoint_at_cursor_head is working properly
20728/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
20729/// or when breakpoints were placed out of order. This tests for a regression too
20730#[gpui::test]
20731async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
20732 init_test(cx, |_| {});
20733
20734 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20735 let fs = FakeFs::new(cx.executor());
20736 fs.insert_tree(
20737 path!("/a"),
20738 json!({
20739 "main.rs": sample_text,
20740 }),
20741 )
20742 .await;
20743 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20744 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20745 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20746
20747 let fs = FakeFs::new(cx.executor());
20748 fs.insert_tree(
20749 path!("/a"),
20750 json!({
20751 "main.rs": sample_text,
20752 }),
20753 )
20754 .await;
20755 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20756 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20757 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20758 let worktree_id = workspace
20759 .update(cx, |workspace, _window, cx| {
20760 workspace.project().update(cx, |project, cx| {
20761 project.worktrees(cx).next().unwrap().read(cx).id()
20762 })
20763 })
20764 .unwrap();
20765
20766 let buffer = project
20767 .update(cx, |project, cx| {
20768 project.open_buffer((worktree_id, "main.rs"), cx)
20769 })
20770 .await
20771 .unwrap();
20772
20773 let (editor, cx) = cx.add_window_view(|window, cx| {
20774 Editor::new(
20775 EditorMode::full(),
20776 MultiBuffer::build_from_buffer(buffer, cx),
20777 Some(project.clone()),
20778 window,
20779 cx,
20780 )
20781 });
20782
20783 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20784 let abs_path = project.read_with(cx, |project, cx| {
20785 project
20786 .absolute_path(&project_path, cx)
20787 .map(|path_buf| Arc::from(path_buf.to_owned()))
20788 .unwrap()
20789 });
20790
20791 // assert we can add breakpoint on the first line
20792 editor.update_in(cx, |editor, window, cx| {
20793 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20794 editor.move_to_end(&MoveToEnd, window, cx);
20795 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20796 editor.move_up(&MoveUp, window, cx);
20797 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20798 });
20799
20800 let breakpoints = editor.update(cx, |editor, cx| {
20801 editor
20802 .breakpoint_store()
20803 .as_ref()
20804 .unwrap()
20805 .read(cx)
20806 .all_source_breakpoints(cx)
20807 .clone()
20808 });
20809
20810 assert_eq!(1, breakpoints.len());
20811 assert_breakpoint(
20812 &breakpoints,
20813 &abs_path,
20814 vec![
20815 (0, Breakpoint::new_standard()),
20816 (2, Breakpoint::new_standard()),
20817 (3, Breakpoint::new_standard()),
20818 ],
20819 );
20820
20821 editor.update_in(cx, |editor, window, cx| {
20822 editor.move_to_beginning(&MoveToBeginning, window, cx);
20823 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20824 editor.move_to_end(&MoveToEnd, window, cx);
20825 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20826 // Disabling a breakpoint that doesn't exist should do nothing
20827 editor.move_up(&MoveUp, window, cx);
20828 editor.move_up(&MoveUp, window, cx);
20829 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20830 });
20831
20832 let breakpoints = editor.update(cx, |editor, cx| {
20833 editor
20834 .breakpoint_store()
20835 .as_ref()
20836 .unwrap()
20837 .read(cx)
20838 .all_source_breakpoints(cx)
20839 .clone()
20840 });
20841
20842 let disable_breakpoint = {
20843 let mut bp = Breakpoint::new_standard();
20844 bp.state = BreakpointState::Disabled;
20845 bp
20846 };
20847
20848 assert_eq!(1, breakpoints.len());
20849 assert_breakpoint(
20850 &breakpoints,
20851 &abs_path,
20852 vec![
20853 (0, disable_breakpoint.clone()),
20854 (2, Breakpoint::new_standard()),
20855 (3, disable_breakpoint.clone()),
20856 ],
20857 );
20858
20859 editor.update_in(cx, |editor, window, cx| {
20860 editor.move_to_beginning(&MoveToBeginning, window, cx);
20861 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20862 editor.move_to_end(&MoveToEnd, window, cx);
20863 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20864 editor.move_up(&MoveUp, window, cx);
20865 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20866 });
20867
20868 let breakpoints = editor.update(cx, |editor, cx| {
20869 editor
20870 .breakpoint_store()
20871 .as_ref()
20872 .unwrap()
20873 .read(cx)
20874 .all_source_breakpoints(cx)
20875 .clone()
20876 });
20877
20878 assert_eq!(1, breakpoints.len());
20879 assert_breakpoint(
20880 &breakpoints,
20881 &abs_path,
20882 vec![
20883 (0, Breakpoint::new_standard()),
20884 (2, disable_breakpoint),
20885 (3, Breakpoint::new_standard()),
20886 ],
20887 );
20888}
20889
20890#[gpui::test]
20891async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
20892 init_test(cx, |_| {});
20893 let capabilities = lsp::ServerCapabilities {
20894 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
20895 prepare_provider: Some(true),
20896 work_done_progress_options: Default::default(),
20897 })),
20898 ..Default::default()
20899 };
20900 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
20901
20902 cx.set_state(indoc! {"
20903 struct Fˇoo {}
20904 "});
20905
20906 cx.update_editor(|editor, _, cx| {
20907 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
20908 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
20909 editor.highlight_background::<DocumentHighlightRead>(
20910 &[highlight_range],
20911 |theme| theme.colors().editor_document_highlight_read_background,
20912 cx,
20913 );
20914 });
20915
20916 let mut prepare_rename_handler = cx
20917 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
20918 move |_, _, _| async move {
20919 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
20920 start: lsp::Position {
20921 line: 0,
20922 character: 7,
20923 },
20924 end: lsp::Position {
20925 line: 0,
20926 character: 10,
20927 },
20928 })))
20929 },
20930 );
20931 let prepare_rename_task = cx
20932 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
20933 .expect("Prepare rename was not started");
20934 prepare_rename_handler.next().await.unwrap();
20935 prepare_rename_task.await.expect("Prepare rename failed");
20936
20937 let mut rename_handler =
20938 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
20939 let edit = lsp::TextEdit {
20940 range: lsp::Range {
20941 start: lsp::Position {
20942 line: 0,
20943 character: 7,
20944 },
20945 end: lsp::Position {
20946 line: 0,
20947 character: 10,
20948 },
20949 },
20950 new_text: "FooRenamed".to_string(),
20951 };
20952 Ok(Some(lsp::WorkspaceEdit::new(
20953 // Specify the same edit twice
20954 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
20955 )))
20956 });
20957 let rename_task = cx
20958 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
20959 .expect("Confirm rename was not started");
20960 rename_handler.next().await.unwrap();
20961 rename_task.await.expect("Confirm rename failed");
20962 cx.run_until_parked();
20963
20964 // Despite two edits, only one is actually applied as those are identical
20965 cx.assert_editor_state(indoc! {"
20966 struct FooRenamedˇ {}
20967 "});
20968}
20969
20970#[gpui::test]
20971async fn test_rename_without_prepare(cx: &mut TestAppContext) {
20972 init_test(cx, |_| {});
20973 // These capabilities indicate that the server does not support prepare rename.
20974 let capabilities = lsp::ServerCapabilities {
20975 rename_provider: Some(lsp::OneOf::Left(true)),
20976 ..Default::default()
20977 };
20978 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
20979
20980 cx.set_state(indoc! {"
20981 struct Fˇoo {}
20982 "});
20983
20984 cx.update_editor(|editor, _window, cx| {
20985 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
20986 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
20987 editor.highlight_background::<DocumentHighlightRead>(
20988 &[highlight_range],
20989 |theme| theme.colors().editor_document_highlight_read_background,
20990 cx,
20991 );
20992 });
20993
20994 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
20995 .expect("Prepare rename was not started")
20996 .await
20997 .expect("Prepare rename failed");
20998
20999 let mut rename_handler =
21000 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21001 let edit = lsp::TextEdit {
21002 range: lsp::Range {
21003 start: lsp::Position {
21004 line: 0,
21005 character: 7,
21006 },
21007 end: lsp::Position {
21008 line: 0,
21009 character: 10,
21010 },
21011 },
21012 new_text: "FooRenamed".to_string(),
21013 };
21014 Ok(Some(lsp::WorkspaceEdit::new(
21015 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
21016 )))
21017 });
21018 let rename_task = cx
21019 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21020 .expect("Confirm rename was not started");
21021 rename_handler.next().await.unwrap();
21022 rename_task.await.expect("Confirm rename failed");
21023 cx.run_until_parked();
21024
21025 // Correct range is renamed, as `surrounding_word` is used to find it.
21026 cx.assert_editor_state(indoc! {"
21027 struct FooRenamedˇ {}
21028 "});
21029}
21030
21031#[gpui::test]
21032async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
21033 init_test(cx, |_| {});
21034 let mut cx = EditorTestContext::new(cx).await;
21035
21036 let language = Arc::new(
21037 Language::new(
21038 LanguageConfig::default(),
21039 Some(tree_sitter_html::LANGUAGE.into()),
21040 )
21041 .with_brackets_query(
21042 r#"
21043 ("<" @open "/>" @close)
21044 ("</" @open ">" @close)
21045 ("<" @open ">" @close)
21046 ("\"" @open "\"" @close)
21047 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
21048 "#,
21049 )
21050 .unwrap(),
21051 );
21052 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21053
21054 cx.set_state(indoc! {"
21055 <span>ˇ</span>
21056 "});
21057 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21058 cx.assert_editor_state(indoc! {"
21059 <span>
21060 ˇ
21061 </span>
21062 "});
21063
21064 cx.set_state(indoc! {"
21065 <span><span></span>ˇ</span>
21066 "});
21067 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21068 cx.assert_editor_state(indoc! {"
21069 <span><span></span>
21070 ˇ</span>
21071 "});
21072
21073 cx.set_state(indoc! {"
21074 <span>ˇ
21075 </span>
21076 "});
21077 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21078 cx.assert_editor_state(indoc! {"
21079 <span>
21080 ˇ
21081 </span>
21082 "});
21083}
21084
21085#[gpui::test(iterations = 10)]
21086async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
21087 init_test(cx, |_| {});
21088
21089 let fs = FakeFs::new(cx.executor());
21090 fs.insert_tree(
21091 path!("/dir"),
21092 json!({
21093 "a.ts": "a",
21094 }),
21095 )
21096 .await;
21097
21098 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
21099 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21100 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21101
21102 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21103 language_registry.add(Arc::new(Language::new(
21104 LanguageConfig {
21105 name: "TypeScript".into(),
21106 matcher: LanguageMatcher {
21107 path_suffixes: vec!["ts".to_string()],
21108 ..Default::default()
21109 },
21110 ..Default::default()
21111 },
21112 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
21113 )));
21114 let mut fake_language_servers = language_registry.register_fake_lsp(
21115 "TypeScript",
21116 FakeLspAdapter {
21117 capabilities: lsp::ServerCapabilities {
21118 code_lens_provider: Some(lsp::CodeLensOptions {
21119 resolve_provider: Some(true),
21120 }),
21121 execute_command_provider: Some(lsp::ExecuteCommandOptions {
21122 commands: vec!["_the/command".to_string()],
21123 ..lsp::ExecuteCommandOptions::default()
21124 }),
21125 ..lsp::ServerCapabilities::default()
21126 },
21127 ..FakeLspAdapter::default()
21128 },
21129 );
21130
21131 let (buffer, _handle) = project
21132 .update(cx, |p, cx| {
21133 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
21134 })
21135 .await
21136 .unwrap();
21137 cx.executor().run_until_parked();
21138
21139 let fake_server = fake_language_servers.next().await.unwrap();
21140
21141 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
21142 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
21143 drop(buffer_snapshot);
21144 let actions = cx
21145 .update_window(*workspace, |_, window, cx| {
21146 project.code_actions(&buffer, anchor..anchor, window, cx)
21147 })
21148 .unwrap();
21149
21150 fake_server
21151 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21152 Ok(Some(vec![
21153 lsp::CodeLens {
21154 range: lsp::Range::default(),
21155 command: Some(lsp::Command {
21156 title: "Code lens command".to_owned(),
21157 command: "_the/command".to_owned(),
21158 arguments: None,
21159 }),
21160 data: None,
21161 },
21162 lsp::CodeLens {
21163 range: lsp::Range::default(),
21164 command: Some(lsp::Command {
21165 title: "Command not in capabilities".to_owned(),
21166 command: "not in capabilities".to_owned(),
21167 arguments: None,
21168 }),
21169 data: None,
21170 },
21171 lsp::CodeLens {
21172 range: lsp::Range {
21173 start: lsp::Position {
21174 line: 1,
21175 character: 1,
21176 },
21177 end: lsp::Position {
21178 line: 1,
21179 character: 1,
21180 },
21181 },
21182 command: Some(lsp::Command {
21183 title: "Command not in range".to_owned(),
21184 command: "_the/command".to_owned(),
21185 arguments: None,
21186 }),
21187 data: None,
21188 },
21189 ]))
21190 })
21191 .next()
21192 .await;
21193
21194 let actions = actions.await.unwrap();
21195 assert_eq!(
21196 actions.len(),
21197 1,
21198 "Should have only one valid action for the 0..0 range"
21199 );
21200 let action = actions[0].clone();
21201 let apply = project.update(cx, |project, cx| {
21202 project.apply_code_action(buffer.clone(), action, true, cx)
21203 });
21204
21205 // Resolving the code action does not populate its edits. In absence of
21206 // edits, we must execute the given command.
21207 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
21208 |mut lens, _| async move {
21209 let lens_command = lens.command.as_mut().expect("should have a command");
21210 assert_eq!(lens_command.title, "Code lens command");
21211 lens_command.arguments = Some(vec![json!("the-argument")]);
21212 Ok(lens)
21213 },
21214 );
21215
21216 // While executing the command, the language server sends the editor
21217 // a `workspaceEdit` request.
21218 fake_server
21219 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
21220 let fake = fake_server.clone();
21221 move |params, _| {
21222 assert_eq!(params.command, "_the/command");
21223 let fake = fake.clone();
21224 async move {
21225 fake.server
21226 .request::<lsp::request::ApplyWorkspaceEdit>(
21227 lsp::ApplyWorkspaceEditParams {
21228 label: None,
21229 edit: lsp::WorkspaceEdit {
21230 changes: Some(
21231 [(
21232 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
21233 vec![lsp::TextEdit {
21234 range: lsp::Range::new(
21235 lsp::Position::new(0, 0),
21236 lsp::Position::new(0, 0),
21237 ),
21238 new_text: "X".into(),
21239 }],
21240 )]
21241 .into_iter()
21242 .collect(),
21243 ),
21244 ..Default::default()
21245 },
21246 },
21247 )
21248 .await
21249 .into_response()
21250 .unwrap();
21251 Ok(Some(json!(null)))
21252 }
21253 }
21254 })
21255 .next()
21256 .await;
21257
21258 // Applying the code lens command returns a project transaction containing the edits
21259 // sent by the language server in its `workspaceEdit` request.
21260 let transaction = apply.await.unwrap();
21261 assert!(transaction.0.contains_key(&buffer));
21262 buffer.update(cx, |buffer, cx| {
21263 assert_eq!(buffer.text(), "Xa");
21264 buffer.undo(cx);
21265 assert_eq!(buffer.text(), "a");
21266 });
21267}
21268
21269#[gpui::test]
21270async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
21271 init_test(cx, |_| {});
21272
21273 let fs = FakeFs::new(cx.executor());
21274 let main_text = r#"fn main() {
21275println!("1");
21276println!("2");
21277println!("3");
21278println!("4");
21279println!("5");
21280}"#;
21281 let lib_text = "mod foo {}";
21282 fs.insert_tree(
21283 path!("/a"),
21284 json!({
21285 "lib.rs": lib_text,
21286 "main.rs": main_text,
21287 }),
21288 )
21289 .await;
21290
21291 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21292 let (workspace, cx) =
21293 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21294 let worktree_id = workspace.update(cx, |workspace, cx| {
21295 workspace.project().update(cx, |project, cx| {
21296 project.worktrees(cx).next().unwrap().read(cx).id()
21297 })
21298 });
21299
21300 let expected_ranges = vec![
21301 Point::new(0, 0)..Point::new(0, 0),
21302 Point::new(1, 0)..Point::new(1, 1),
21303 Point::new(2, 0)..Point::new(2, 2),
21304 Point::new(3, 0)..Point::new(3, 3),
21305 ];
21306
21307 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21308 let editor_1 = workspace
21309 .update_in(cx, |workspace, window, cx| {
21310 workspace.open_path(
21311 (worktree_id, "main.rs"),
21312 Some(pane_1.downgrade()),
21313 true,
21314 window,
21315 cx,
21316 )
21317 })
21318 .unwrap()
21319 .await
21320 .downcast::<Editor>()
21321 .unwrap();
21322 pane_1.update(cx, |pane, cx| {
21323 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21324 open_editor.update(cx, |editor, cx| {
21325 assert_eq!(
21326 editor.display_text(cx),
21327 main_text,
21328 "Original main.rs text on initial open",
21329 );
21330 assert_eq!(
21331 editor
21332 .selections
21333 .all::<Point>(cx)
21334 .into_iter()
21335 .map(|s| s.range())
21336 .collect::<Vec<_>>(),
21337 vec![Point::zero()..Point::zero()],
21338 "Default selections on initial open",
21339 );
21340 })
21341 });
21342 editor_1.update_in(cx, |editor, window, cx| {
21343 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21344 s.select_ranges(expected_ranges.clone());
21345 });
21346 });
21347
21348 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
21349 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
21350 });
21351 let editor_2 = workspace
21352 .update_in(cx, |workspace, window, cx| {
21353 workspace.open_path(
21354 (worktree_id, "main.rs"),
21355 Some(pane_2.downgrade()),
21356 true,
21357 window,
21358 cx,
21359 )
21360 })
21361 .unwrap()
21362 .await
21363 .downcast::<Editor>()
21364 .unwrap();
21365 pane_2.update(cx, |pane, cx| {
21366 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21367 open_editor.update(cx, |editor, cx| {
21368 assert_eq!(
21369 editor.display_text(cx),
21370 main_text,
21371 "Original main.rs text on initial open in another panel",
21372 );
21373 assert_eq!(
21374 editor
21375 .selections
21376 .all::<Point>(cx)
21377 .into_iter()
21378 .map(|s| s.range())
21379 .collect::<Vec<_>>(),
21380 vec![Point::zero()..Point::zero()],
21381 "Default selections on initial open in another panel",
21382 );
21383 })
21384 });
21385
21386 editor_2.update_in(cx, |editor, window, cx| {
21387 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
21388 });
21389
21390 let _other_editor_1 = workspace
21391 .update_in(cx, |workspace, window, cx| {
21392 workspace.open_path(
21393 (worktree_id, "lib.rs"),
21394 Some(pane_1.downgrade()),
21395 true,
21396 window,
21397 cx,
21398 )
21399 })
21400 .unwrap()
21401 .await
21402 .downcast::<Editor>()
21403 .unwrap();
21404 pane_1
21405 .update_in(cx, |pane, window, cx| {
21406 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
21407 })
21408 .await
21409 .unwrap();
21410 drop(editor_1);
21411 pane_1.update(cx, |pane, cx| {
21412 pane.active_item()
21413 .unwrap()
21414 .downcast::<Editor>()
21415 .unwrap()
21416 .update(cx, |editor, cx| {
21417 assert_eq!(
21418 editor.display_text(cx),
21419 lib_text,
21420 "Other file should be open and active",
21421 );
21422 });
21423 assert_eq!(pane.items().count(), 1, "No other editors should be open");
21424 });
21425
21426 let _other_editor_2 = workspace
21427 .update_in(cx, |workspace, window, cx| {
21428 workspace.open_path(
21429 (worktree_id, "lib.rs"),
21430 Some(pane_2.downgrade()),
21431 true,
21432 window,
21433 cx,
21434 )
21435 })
21436 .unwrap()
21437 .await
21438 .downcast::<Editor>()
21439 .unwrap();
21440 pane_2
21441 .update_in(cx, |pane, window, cx| {
21442 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
21443 })
21444 .await
21445 .unwrap();
21446 drop(editor_2);
21447 pane_2.update(cx, |pane, cx| {
21448 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21449 open_editor.update(cx, |editor, cx| {
21450 assert_eq!(
21451 editor.display_text(cx),
21452 lib_text,
21453 "Other file should be open and active in another panel too",
21454 );
21455 });
21456 assert_eq!(
21457 pane.items().count(),
21458 1,
21459 "No other editors should be open in another pane",
21460 );
21461 });
21462
21463 let _editor_1_reopened = workspace
21464 .update_in(cx, |workspace, window, cx| {
21465 workspace.open_path(
21466 (worktree_id, "main.rs"),
21467 Some(pane_1.downgrade()),
21468 true,
21469 window,
21470 cx,
21471 )
21472 })
21473 .unwrap()
21474 .await
21475 .downcast::<Editor>()
21476 .unwrap();
21477 let _editor_2_reopened = workspace
21478 .update_in(cx, |workspace, window, cx| {
21479 workspace.open_path(
21480 (worktree_id, "main.rs"),
21481 Some(pane_2.downgrade()),
21482 true,
21483 window,
21484 cx,
21485 )
21486 })
21487 .unwrap()
21488 .await
21489 .downcast::<Editor>()
21490 .unwrap();
21491 pane_1.update(cx, |pane, cx| {
21492 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21493 open_editor.update(cx, |editor, cx| {
21494 assert_eq!(
21495 editor.display_text(cx),
21496 main_text,
21497 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
21498 );
21499 assert_eq!(
21500 editor
21501 .selections
21502 .all::<Point>(cx)
21503 .into_iter()
21504 .map(|s| s.range())
21505 .collect::<Vec<_>>(),
21506 expected_ranges,
21507 "Previous editor in the 1st panel had selections and should get them restored on reopen",
21508 );
21509 })
21510 });
21511 pane_2.update(cx, |pane, cx| {
21512 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21513 open_editor.update(cx, |editor, cx| {
21514 assert_eq!(
21515 editor.display_text(cx),
21516 r#"fn main() {
21517⋯rintln!("1");
21518⋯intln!("2");
21519⋯ntln!("3");
21520println!("4");
21521println!("5");
21522}"#,
21523 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
21524 );
21525 assert_eq!(
21526 editor
21527 .selections
21528 .all::<Point>(cx)
21529 .into_iter()
21530 .map(|s| s.range())
21531 .collect::<Vec<_>>(),
21532 vec![Point::zero()..Point::zero()],
21533 "Previous editor in the 2nd pane had no selections changed hence should restore none",
21534 );
21535 })
21536 });
21537}
21538
21539#[gpui::test]
21540async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
21541 init_test(cx, |_| {});
21542
21543 let fs = FakeFs::new(cx.executor());
21544 let main_text = r#"fn main() {
21545println!("1");
21546println!("2");
21547println!("3");
21548println!("4");
21549println!("5");
21550}"#;
21551 let lib_text = "mod foo {}";
21552 fs.insert_tree(
21553 path!("/a"),
21554 json!({
21555 "lib.rs": lib_text,
21556 "main.rs": main_text,
21557 }),
21558 )
21559 .await;
21560
21561 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21562 let (workspace, cx) =
21563 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21564 let worktree_id = workspace.update(cx, |workspace, cx| {
21565 workspace.project().update(cx, |project, cx| {
21566 project.worktrees(cx).next().unwrap().read(cx).id()
21567 })
21568 });
21569
21570 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21571 let editor = workspace
21572 .update_in(cx, |workspace, window, cx| {
21573 workspace.open_path(
21574 (worktree_id, "main.rs"),
21575 Some(pane.downgrade()),
21576 true,
21577 window,
21578 cx,
21579 )
21580 })
21581 .unwrap()
21582 .await
21583 .downcast::<Editor>()
21584 .unwrap();
21585 pane.update(cx, |pane, cx| {
21586 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21587 open_editor.update(cx, |editor, cx| {
21588 assert_eq!(
21589 editor.display_text(cx),
21590 main_text,
21591 "Original main.rs text on initial open",
21592 );
21593 })
21594 });
21595 editor.update_in(cx, |editor, window, cx| {
21596 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
21597 });
21598
21599 cx.update_global(|store: &mut SettingsStore, cx| {
21600 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21601 s.restore_on_file_reopen = Some(false);
21602 });
21603 });
21604 editor.update_in(cx, |editor, window, cx| {
21605 editor.fold_ranges(
21606 vec![
21607 Point::new(1, 0)..Point::new(1, 1),
21608 Point::new(2, 0)..Point::new(2, 2),
21609 Point::new(3, 0)..Point::new(3, 3),
21610 ],
21611 false,
21612 window,
21613 cx,
21614 );
21615 });
21616 pane.update_in(cx, |pane, window, cx| {
21617 pane.close_all_items(&CloseAllItems::default(), window, cx)
21618 })
21619 .await
21620 .unwrap();
21621 pane.update(cx, |pane, _| {
21622 assert!(pane.active_item().is_none());
21623 });
21624 cx.update_global(|store: &mut SettingsStore, cx| {
21625 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21626 s.restore_on_file_reopen = Some(true);
21627 });
21628 });
21629
21630 let _editor_reopened = workspace
21631 .update_in(cx, |workspace, window, cx| {
21632 workspace.open_path(
21633 (worktree_id, "main.rs"),
21634 Some(pane.downgrade()),
21635 true,
21636 window,
21637 cx,
21638 )
21639 })
21640 .unwrap()
21641 .await
21642 .downcast::<Editor>()
21643 .unwrap();
21644 pane.update(cx, |pane, cx| {
21645 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21646 open_editor.update(cx, |editor, cx| {
21647 assert_eq!(
21648 editor.display_text(cx),
21649 main_text,
21650 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
21651 );
21652 })
21653 });
21654}
21655
21656#[gpui::test]
21657async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
21658 struct EmptyModalView {
21659 focus_handle: gpui::FocusHandle,
21660 }
21661 impl EventEmitter<DismissEvent> for EmptyModalView {}
21662 impl Render for EmptyModalView {
21663 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
21664 div()
21665 }
21666 }
21667 impl Focusable for EmptyModalView {
21668 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
21669 self.focus_handle.clone()
21670 }
21671 }
21672 impl workspace::ModalView for EmptyModalView {}
21673 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
21674 EmptyModalView {
21675 focus_handle: cx.focus_handle(),
21676 }
21677 }
21678
21679 init_test(cx, |_| {});
21680
21681 let fs = FakeFs::new(cx.executor());
21682 let project = Project::test(fs, [], cx).await;
21683 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21684 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
21685 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21686 let editor = cx.new_window_entity(|window, cx| {
21687 Editor::new(
21688 EditorMode::full(),
21689 buffer,
21690 Some(project.clone()),
21691 window,
21692 cx,
21693 )
21694 });
21695 workspace
21696 .update(cx, |workspace, window, cx| {
21697 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
21698 })
21699 .unwrap();
21700 editor.update_in(cx, |editor, window, cx| {
21701 editor.open_context_menu(&OpenContextMenu, window, cx);
21702 assert!(editor.mouse_context_menu.is_some());
21703 });
21704 workspace
21705 .update(cx, |workspace, window, cx| {
21706 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
21707 })
21708 .unwrap();
21709 cx.read(|cx| {
21710 assert!(editor.read(cx).mouse_context_menu.is_none());
21711 });
21712}
21713
21714#[gpui::test]
21715async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
21716 init_test(cx, |_| {});
21717
21718 let fs = FakeFs::new(cx.executor());
21719 fs.insert_file(path!("/file.html"), Default::default())
21720 .await;
21721
21722 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
21723
21724 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21725 let html_language = Arc::new(Language::new(
21726 LanguageConfig {
21727 name: "HTML".into(),
21728 matcher: LanguageMatcher {
21729 path_suffixes: vec!["html".to_string()],
21730 ..LanguageMatcher::default()
21731 },
21732 brackets: BracketPairConfig {
21733 pairs: vec![BracketPair {
21734 start: "<".into(),
21735 end: ">".into(),
21736 close: true,
21737 ..Default::default()
21738 }],
21739 ..Default::default()
21740 },
21741 ..Default::default()
21742 },
21743 Some(tree_sitter_html::LANGUAGE.into()),
21744 ));
21745 language_registry.add(html_language);
21746 let mut fake_servers = language_registry.register_fake_lsp(
21747 "HTML",
21748 FakeLspAdapter {
21749 capabilities: lsp::ServerCapabilities {
21750 completion_provider: Some(lsp::CompletionOptions {
21751 resolve_provider: Some(true),
21752 ..Default::default()
21753 }),
21754 ..Default::default()
21755 },
21756 ..Default::default()
21757 },
21758 );
21759
21760 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21761 let cx = &mut VisualTestContext::from_window(*workspace, cx);
21762
21763 let worktree_id = workspace
21764 .update(cx, |workspace, _window, cx| {
21765 workspace.project().update(cx, |project, cx| {
21766 project.worktrees(cx).next().unwrap().read(cx).id()
21767 })
21768 })
21769 .unwrap();
21770 project
21771 .update(cx, |project, cx| {
21772 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
21773 })
21774 .await
21775 .unwrap();
21776 let editor = workspace
21777 .update(cx, |workspace, window, cx| {
21778 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
21779 })
21780 .unwrap()
21781 .await
21782 .unwrap()
21783 .downcast::<Editor>()
21784 .unwrap();
21785
21786 let fake_server = fake_servers.next().await.unwrap();
21787 editor.update_in(cx, |editor, window, cx| {
21788 editor.set_text("<ad></ad>", window, cx);
21789 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21790 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
21791 });
21792 let Some((buffer, _)) = editor
21793 .buffer
21794 .read(cx)
21795 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
21796 else {
21797 panic!("Failed to get buffer for selection position");
21798 };
21799 let buffer = buffer.read(cx);
21800 let buffer_id = buffer.remote_id();
21801 let opening_range =
21802 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
21803 let closing_range =
21804 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
21805 let mut linked_ranges = HashMap::default();
21806 linked_ranges.insert(
21807 buffer_id,
21808 vec![(opening_range.clone(), vec![closing_range.clone()])],
21809 );
21810 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
21811 });
21812 let mut completion_handle =
21813 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
21814 Ok(Some(lsp::CompletionResponse::Array(vec![
21815 lsp::CompletionItem {
21816 label: "head".to_string(),
21817 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21818 lsp::InsertReplaceEdit {
21819 new_text: "head".to_string(),
21820 insert: lsp::Range::new(
21821 lsp::Position::new(0, 1),
21822 lsp::Position::new(0, 3),
21823 ),
21824 replace: lsp::Range::new(
21825 lsp::Position::new(0, 1),
21826 lsp::Position::new(0, 3),
21827 ),
21828 },
21829 )),
21830 ..Default::default()
21831 },
21832 ])))
21833 });
21834 editor.update_in(cx, |editor, window, cx| {
21835 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
21836 });
21837 cx.run_until_parked();
21838 completion_handle.next().await.unwrap();
21839 editor.update(cx, |editor, _| {
21840 assert!(
21841 editor.context_menu_visible(),
21842 "Completion menu should be visible"
21843 );
21844 });
21845 editor.update_in(cx, |editor, window, cx| {
21846 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
21847 });
21848 cx.executor().run_until_parked();
21849 editor.update(cx, |editor, cx| {
21850 assert_eq!(editor.text(cx), "<head></head>");
21851 });
21852}
21853
21854#[gpui::test]
21855async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
21856 init_test(cx, |_| {});
21857
21858 let fs = FakeFs::new(cx.executor());
21859 fs.insert_tree(
21860 path!("/root"),
21861 json!({
21862 "a": {
21863 "main.rs": "fn main() {}",
21864 },
21865 "foo": {
21866 "bar": {
21867 "external_file.rs": "pub mod external {}",
21868 }
21869 }
21870 }),
21871 )
21872 .await;
21873
21874 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
21875 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21876 language_registry.add(rust_lang());
21877 let _fake_servers = language_registry.register_fake_lsp(
21878 "Rust",
21879 FakeLspAdapter {
21880 ..FakeLspAdapter::default()
21881 },
21882 );
21883 let (workspace, cx) =
21884 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21885 let worktree_id = workspace.update(cx, |workspace, cx| {
21886 workspace.project().update(cx, |project, cx| {
21887 project.worktrees(cx).next().unwrap().read(cx).id()
21888 })
21889 });
21890
21891 let assert_language_servers_count =
21892 |expected: usize, context: &str, cx: &mut VisualTestContext| {
21893 project.update(cx, |project, cx| {
21894 let current = project
21895 .lsp_store()
21896 .read(cx)
21897 .as_local()
21898 .unwrap()
21899 .language_servers
21900 .len();
21901 assert_eq!(expected, current, "{context}");
21902 });
21903 };
21904
21905 assert_language_servers_count(
21906 0,
21907 "No servers should be running before any file is open",
21908 cx,
21909 );
21910 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21911 let main_editor = workspace
21912 .update_in(cx, |workspace, window, cx| {
21913 workspace.open_path(
21914 (worktree_id, "main.rs"),
21915 Some(pane.downgrade()),
21916 true,
21917 window,
21918 cx,
21919 )
21920 })
21921 .unwrap()
21922 .await
21923 .downcast::<Editor>()
21924 .unwrap();
21925 pane.update(cx, |pane, cx| {
21926 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21927 open_editor.update(cx, |editor, cx| {
21928 assert_eq!(
21929 editor.display_text(cx),
21930 "fn main() {}",
21931 "Original main.rs text on initial open",
21932 );
21933 });
21934 assert_eq!(open_editor, main_editor);
21935 });
21936 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
21937
21938 let external_editor = workspace
21939 .update_in(cx, |workspace, window, cx| {
21940 workspace.open_abs_path(
21941 PathBuf::from("/root/foo/bar/external_file.rs"),
21942 OpenOptions::default(),
21943 window,
21944 cx,
21945 )
21946 })
21947 .await
21948 .expect("opening external file")
21949 .downcast::<Editor>()
21950 .expect("downcasted external file's open element to editor");
21951 pane.update(cx, |pane, cx| {
21952 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21953 open_editor.update(cx, |editor, cx| {
21954 assert_eq!(
21955 editor.display_text(cx),
21956 "pub mod external {}",
21957 "External file is open now",
21958 );
21959 });
21960 assert_eq!(open_editor, external_editor);
21961 });
21962 assert_language_servers_count(
21963 1,
21964 "Second, external, *.rs file should join the existing server",
21965 cx,
21966 );
21967
21968 pane.update_in(cx, |pane, window, cx| {
21969 pane.close_active_item(&CloseActiveItem::default(), window, cx)
21970 })
21971 .await
21972 .unwrap();
21973 pane.update_in(cx, |pane, window, cx| {
21974 pane.navigate_backward(window, cx);
21975 });
21976 cx.run_until_parked();
21977 pane.update(cx, |pane, cx| {
21978 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21979 open_editor.update(cx, |editor, cx| {
21980 assert_eq!(
21981 editor.display_text(cx),
21982 "pub mod external {}",
21983 "External file is open now",
21984 );
21985 });
21986 });
21987 assert_language_servers_count(
21988 1,
21989 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
21990 cx,
21991 );
21992
21993 cx.update(|_, cx| {
21994 workspace::reload(&workspace::Reload::default(), cx);
21995 });
21996 assert_language_servers_count(
21997 1,
21998 "After reloading the worktree with local and external files opened, only one project should be started",
21999 cx,
22000 );
22001}
22002
22003#[gpui::test]
22004async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
22005 init_test(cx, |_| {});
22006
22007 let mut cx = EditorTestContext::new(cx).await;
22008 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22009 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22010
22011 // test cursor move to start of each line on tab
22012 // for `if`, `elif`, `else`, `while`, `with` and `for`
22013 cx.set_state(indoc! {"
22014 def main():
22015 ˇ for item in items:
22016 ˇ while item.active:
22017 ˇ if item.value > 10:
22018 ˇ continue
22019 ˇ elif item.value < 0:
22020 ˇ break
22021 ˇ else:
22022 ˇ with item.context() as ctx:
22023 ˇ yield count
22024 ˇ else:
22025 ˇ log('while else')
22026 ˇ else:
22027 ˇ log('for else')
22028 "});
22029 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22030 cx.assert_editor_state(indoc! {"
22031 def main():
22032 ˇfor item in items:
22033 ˇwhile item.active:
22034 ˇif item.value > 10:
22035 ˇcontinue
22036 ˇelif item.value < 0:
22037 ˇbreak
22038 ˇelse:
22039 ˇwith item.context() as ctx:
22040 ˇyield count
22041 ˇelse:
22042 ˇlog('while else')
22043 ˇelse:
22044 ˇlog('for else')
22045 "});
22046 // test relative indent is preserved when tab
22047 // for `if`, `elif`, `else`, `while`, `with` and `for`
22048 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22049 cx.assert_editor_state(indoc! {"
22050 def main():
22051 ˇfor item in items:
22052 ˇwhile item.active:
22053 ˇif item.value > 10:
22054 ˇcontinue
22055 ˇelif item.value < 0:
22056 ˇbreak
22057 ˇelse:
22058 ˇwith item.context() as ctx:
22059 ˇyield count
22060 ˇelse:
22061 ˇlog('while else')
22062 ˇelse:
22063 ˇlog('for else')
22064 "});
22065
22066 // test cursor move to start of each line on tab
22067 // for `try`, `except`, `else`, `finally`, `match` and `def`
22068 cx.set_state(indoc! {"
22069 def main():
22070 ˇ try:
22071 ˇ fetch()
22072 ˇ except ValueError:
22073 ˇ handle_error()
22074 ˇ else:
22075 ˇ match value:
22076 ˇ case _:
22077 ˇ finally:
22078 ˇ def status():
22079 ˇ return 0
22080 "});
22081 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22082 cx.assert_editor_state(indoc! {"
22083 def main():
22084 ˇtry:
22085 ˇfetch()
22086 ˇexcept ValueError:
22087 ˇhandle_error()
22088 ˇelse:
22089 ˇmatch value:
22090 ˇcase _:
22091 ˇfinally:
22092 ˇdef status():
22093 ˇreturn 0
22094 "});
22095 // test relative indent is preserved when tab
22096 // for `try`, `except`, `else`, `finally`, `match` and `def`
22097 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22098 cx.assert_editor_state(indoc! {"
22099 def main():
22100 ˇtry:
22101 ˇfetch()
22102 ˇexcept ValueError:
22103 ˇhandle_error()
22104 ˇelse:
22105 ˇmatch value:
22106 ˇcase _:
22107 ˇfinally:
22108 ˇdef status():
22109 ˇreturn 0
22110 "});
22111}
22112
22113#[gpui::test]
22114async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
22115 init_test(cx, |_| {});
22116
22117 let mut cx = EditorTestContext::new(cx).await;
22118 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22119 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22120
22121 // test `else` auto outdents when typed inside `if` block
22122 cx.set_state(indoc! {"
22123 def main():
22124 if i == 2:
22125 return
22126 ˇ
22127 "});
22128 cx.update_editor(|editor, window, cx| {
22129 editor.handle_input("else:", window, cx);
22130 });
22131 cx.assert_editor_state(indoc! {"
22132 def main():
22133 if i == 2:
22134 return
22135 else:ˇ
22136 "});
22137
22138 // test `except` auto outdents when typed inside `try` block
22139 cx.set_state(indoc! {"
22140 def main():
22141 try:
22142 i = 2
22143 ˇ
22144 "});
22145 cx.update_editor(|editor, window, cx| {
22146 editor.handle_input("except:", window, cx);
22147 });
22148 cx.assert_editor_state(indoc! {"
22149 def main():
22150 try:
22151 i = 2
22152 except:ˇ
22153 "});
22154
22155 // test `else` auto outdents when typed inside `except` block
22156 cx.set_state(indoc! {"
22157 def main():
22158 try:
22159 i = 2
22160 except:
22161 j = 2
22162 ˇ
22163 "});
22164 cx.update_editor(|editor, window, cx| {
22165 editor.handle_input("else:", window, cx);
22166 });
22167 cx.assert_editor_state(indoc! {"
22168 def main():
22169 try:
22170 i = 2
22171 except:
22172 j = 2
22173 else:ˇ
22174 "});
22175
22176 // test `finally` auto outdents when typed inside `else` block
22177 cx.set_state(indoc! {"
22178 def main():
22179 try:
22180 i = 2
22181 except:
22182 j = 2
22183 else:
22184 k = 2
22185 ˇ
22186 "});
22187 cx.update_editor(|editor, window, cx| {
22188 editor.handle_input("finally:", window, cx);
22189 });
22190 cx.assert_editor_state(indoc! {"
22191 def main():
22192 try:
22193 i = 2
22194 except:
22195 j = 2
22196 else:
22197 k = 2
22198 finally:ˇ
22199 "});
22200
22201 // test `else` does not outdents when typed inside `except` block right after for block
22202 cx.set_state(indoc! {"
22203 def main():
22204 try:
22205 i = 2
22206 except:
22207 for i in range(n):
22208 pass
22209 ˇ
22210 "});
22211 cx.update_editor(|editor, window, cx| {
22212 editor.handle_input("else:", window, cx);
22213 });
22214 cx.assert_editor_state(indoc! {"
22215 def main():
22216 try:
22217 i = 2
22218 except:
22219 for i in range(n):
22220 pass
22221 else:ˇ
22222 "});
22223
22224 // test `finally` auto outdents when typed inside `else` block right after for block
22225 cx.set_state(indoc! {"
22226 def main():
22227 try:
22228 i = 2
22229 except:
22230 j = 2
22231 else:
22232 for i in range(n):
22233 pass
22234 ˇ
22235 "});
22236 cx.update_editor(|editor, window, cx| {
22237 editor.handle_input("finally:", window, cx);
22238 });
22239 cx.assert_editor_state(indoc! {"
22240 def main():
22241 try:
22242 i = 2
22243 except:
22244 j = 2
22245 else:
22246 for i in range(n):
22247 pass
22248 finally:ˇ
22249 "});
22250
22251 // test `except` outdents to inner "try" block
22252 cx.set_state(indoc! {"
22253 def main():
22254 try:
22255 i = 2
22256 if i == 2:
22257 try:
22258 i = 3
22259 ˇ
22260 "});
22261 cx.update_editor(|editor, window, cx| {
22262 editor.handle_input("except:", window, cx);
22263 });
22264 cx.assert_editor_state(indoc! {"
22265 def main():
22266 try:
22267 i = 2
22268 if i == 2:
22269 try:
22270 i = 3
22271 except:ˇ
22272 "});
22273
22274 // test `except` outdents to outer "try" block
22275 cx.set_state(indoc! {"
22276 def main():
22277 try:
22278 i = 2
22279 if i == 2:
22280 try:
22281 i = 3
22282 ˇ
22283 "});
22284 cx.update_editor(|editor, window, cx| {
22285 editor.handle_input("except:", window, cx);
22286 });
22287 cx.assert_editor_state(indoc! {"
22288 def main():
22289 try:
22290 i = 2
22291 if i == 2:
22292 try:
22293 i = 3
22294 except:ˇ
22295 "});
22296
22297 // test `else` stays at correct indent when typed after `for` block
22298 cx.set_state(indoc! {"
22299 def main():
22300 for i in range(10):
22301 if i == 3:
22302 break
22303 ˇ
22304 "});
22305 cx.update_editor(|editor, window, cx| {
22306 editor.handle_input("else:", window, cx);
22307 });
22308 cx.assert_editor_state(indoc! {"
22309 def main():
22310 for i in range(10):
22311 if i == 3:
22312 break
22313 else:ˇ
22314 "});
22315
22316 // test does not outdent on typing after line with square brackets
22317 cx.set_state(indoc! {"
22318 def f() -> list[str]:
22319 ˇ
22320 "});
22321 cx.update_editor(|editor, window, cx| {
22322 editor.handle_input("a", window, cx);
22323 });
22324 cx.assert_editor_state(indoc! {"
22325 def f() -> list[str]:
22326 aˇ
22327 "});
22328}
22329
22330#[gpui::test]
22331async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
22332 init_test(cx, |_| {});
22333 update_test_language_settings(cx, |settings| {
22334 settings.defaults.extend_comment_on_newline = Some(false);
22335 });
22336 let mut cx = EditorTestContext::new(cx).await;
22337 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22338 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22339
22340 // test correct indent after newline on comment
22341 cx.set_state(indoc! {"
22342 # COMMENT:ˇ
22343 "});
22344 cx.update_editor(|editor, window, cx| {
22345 editor.newline(&Newline, window, cx);
22346 });
22347 cx.assert_editor_state(indoc! {"
22348 # COMMENT:
22349 ˇ
22350 "});
22351
22352 // test correct indent after newline in brackets
22353 cx.set_state(indoc! {"
22354 {ˇ}
22355 "});
22356 cx.update_editor(|editor, window, cx| {
22357 editor.newline(&Newline, window, cx);
22358 });
22359 cx.run_until_parked();
22360 cx.assert_editor_state(indoc! {"
22361 {
22362 ˇ
22363 }
22364 "});
22365
22366 cx.set_state(indoc! {"
22367 (ˇ)
22368 "});
22369 cx.update_editor(|editor, window, cx| {
22370 editor.newline(&Newline, window, cx);
22371 });
22372 cx.run_until_parked();
22373 cx.assert_editor_state(indoc! {"
22374 (
22375 ˇ
22376 )
22377 "});
22378
22379 // do not indent after empty lists or dictionaries
22380 cx.set_state(indoc! {"
22381 a = []ˇ
22382 "});
22383 cx.update_editor(|editor, window, cx| {
22384 editor.newline(&Newline, window, cx);
22385 });
22386 cx.run_until_parked();
22387 cx.assert_editor_state(indoc! {"
22388 a = []
22389 ˇ
22390 "});
22391}
22392
22393fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
22394 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
22395 point..point
22396}
22397
22398#[track_caller]
22399fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
22400 let (text, ranges) = marked_text_ranges(marked_text, true);
22401 assert_eq!(editor.text(cx), text);
22402 assert_eq!(
22403 editor.selections.ranges(cx),
22404 ranges,
22405 "Assert selections are {}",
22406 marked_text
22407 );
22408}
22409
22410pub fn handle_signature_help_request(
22411 cx: &mut EditorLspTestContext,
22412 mocked_response: lsp::SignatureHelp,
22413) -> impl Future<Output = ()> + use<> {
22414 let mut request =
22415 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
22416 let mocked_response = mocked_response.clone();
22417 async move { Ok(Some(mocked_response)) }
22418 });
22419
22420 async move {
22421 request.next().await;
22422 }
22423}
22424
22425#[track_caller]
22426pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
22427 cx.update_editor(|editor, _, _| {
22428 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
22429 let entries = menu.entries.borrow();
22430 let entries = entries
22431 .iter()
22432 .map(|entry| entry.string.as_str())
22433 .collect::<Vec<_>>();
22434 assert_eq!(entries, expected);
22435 } else {
22436 panic!("Expected completions menu");
22437 }
22438 });
22439}
22440
22441/// Handle completion request passing a marked string specifying where the completion
22442/// should be triggered from using '|' character, what range should be replaced, and what completions
22443/// should be returned using '<' and '>' to delimit the range.
22444///
22445/// Also see `handle_completion_request_with_insert_and_replace`.
22446#[track_caller]
22447pub fn handle_completion_request(
22448 marked_string: &str,
22449 completions: Vec<&'static str>,
22450 is_incomplete: bool,
22451 counter: Arc<AtomicUsize>,
22452 cx: &mut EditorLspTestContext,
22453) -> impl Future<Output = ()> {
22454 let complete_from_marker: TextRangeMarker = '|'.into();
22455 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22456 let (_, mut marked_ranges) = marked_text_ranges_by(
22457 marked_string,
22458 vec![complete_from_marker.clone(), replace_range_marker.clone()],
22459 );
22460
22461 let complete_from_position =
22462 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22463 let replace_range =
22464 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22465
22466 let mut request =
22467 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22468 let completions = completions.clone();
22469 counter.fetch_add(1, atomic::Ordering::Release);
22470 async move {
22471 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22472 assert_eq!(
22473 params.text_document_position.position,
22474 complete_from_position
22475 );
22476 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
22477 is_incomplete: is_incomplete,
22478 item_defaults: None,
22479 items: completions
22480 .iter()
22481 .map(|completion_text| lsp::CompletionItem {
22482 label: completion_text.to_string(),
22483 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
22484 range: replace_range,
22485 new_text: completion_text.to_string(),
22486 })),
22487 ..Default::default()
22488 })
22489 .collect(),
22490 })))
22491 }
22492 });
22493
22494 async move {
22495 request.next().await;
22496 }
22497}
22498
22499/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
22500/// given instead, which also contains an `insert` range.
22501///
22502/// This function uses markers to define ranges:
22503/// - `|` marks the cursor position
22504/// - `<>` marks the replace range
22505/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
22506pub fn handle_completion_request_with_insert_and_replace(
22507 cx: &mut EditorLspTestContext,
22508 marked_string: &str,
22509 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
22510 counter: Arc<AtomicUsize>,
22511) -> impl Future<Output = ()> {
22512 let complete_from_marker: TextRangeMarker = '|'.into();
22513 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22514 let insert_range_marker: TextRangeMarker = ('{', '}').into();
22515
22516 let (_, mut marked_ranges) = marked_text_ranges_by(
22517 marked_string,
22518 vec![
22519 complete_from_marker.clone(),
22520 replace_range_marker.clone(),
22521 insert_range_marker.clone(),
22522 ],
22523 );
22524
22525 let complete_from_position =
22526 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22527 let replace_range =
22528 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22529
22530 let insert_range = match marked_ranges.remove(&insert_range_marker) {
22531 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
22532 _ => lsp::Range {
22533 start: replace_range.start,
22534 end: complete_from_position,
22535 },
22536 };
22537
22538 let mut request =
22539 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22540 let completions = completions.clone();
22541 counter.fetch_add(1, atomic::Ordering::Release);
22542 async move {
22543 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22544 assert_eq!(
22545 params.text_document_position.position, complete_from_position,
22546 "marker `|` position doesn't match",
22547 );
22548 Ok(Some(lsp::CompletionResponse::Array(
22549 completions
22550 .iter()
22551 .map(|(label, new_text)| lsp::CompletionItem {
22552 label: label.to_string(),
22553 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22554 lsp::InsertReplaceEdit {
22555 insert: insert_range,
22556 replace: replace_range,
22557 new_text: new_text.to_string(),
22558 },
22559 )),
22560 ..Default::default()
22561 })
22562 .collect(),
22563 )))
22564 }
22565 });
22566
22567 async move {
22568 request.next().await;
22569 }
22570}
22571
22572fn handle_resolve_completion_request(
22573 cx: &mut EditorLspTestContext,
22574 edits: Option<Vec<(&'static str, &'static str)>>,
22575) -> impl Future<Output = ()> {
22576 let edits = edits.map(|edits| {
22577 edits
22578 .iter()
22579 .map(|(marked_string, new_text)| {
22580 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
22581 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
22582 lsp::TextEdit::new(replace_range, new_text.to_string())
22583 })
22584 .collect::<Vec<_>>()
22585 });
22586
22587 let mut request =
22588 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
22589 let edits = edits.clone();
22590 async move {
22591 Ok(lsp::CompletionItem {
22592 additional_text_edits: edits,
22593 ..Default::default()
22594 })
22595 }
22596 });
22597
22598 async move {
22599 request.next().await;
22600 }
22601}
22602
22603pub(crate) fn update_test_language_settings(
22604 cx: &mut TestAppContext,
22605 f: impl Fn(&mut AllLanguageSettingsContent),
22606) {
22607 cx.update(|cx| {
22608 SettingsStore::update_global(cx, |store, cx| {
22609 store.update_user_settings::<AllLanguageSettings>(cx, f);
22610 });
22611 });
22612}
22613
22614pub(crate) fn update_test_project_settings(
22615 cx: &mut TestAppContext,
22616 f: impl Fn(&mut ProjectSettings),
22617) {
22618 cx.update(|cx| {
22619 SettingsStore::update_global(cx, |store, cx| {
22620 store.update_user_settings::<ProjectSettings>(cx, f);
22621 });
22622 });
22623}
22624
22625pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
22626 cx.update(|cx| {
22627 assets::Assets.load_test_fonts(cx);
22628 let store = SettingsStore::test(cx);
22629 cx.set_global(store);
22630 theme::init(theme::LoadThemes::JustBase, cx);
22631 release_channel::init(SemanticVersion::default(), cx);
22632 client::init_settings(cx);
22633 language::init(cx);
22634 Project::init_settings(cx);
22635 workspace::init_settings(cx);
22636 crate::init(cx);
22637 });
22638
22639 update_test_language_settings(cx, f);
22640}
22641
22642#[track_caller]
22643fn assert_hunk_revert(
22644 not_reverted_text_with_selections: &str,
22645 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
22646 expected_reverted_text_with_selections: &str,
22647 base_text: &str,
22648 cx: &mut EditorLspTestContext,
22649) {
22650 cx.set_state(not_reverted_text_with_selections);
22651 cx.set_head_text(base_text);
22652 cx.executor().run_until_parked();
22653
22654 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
22655 let snapshot = editor.snapshot(window, cx);
22656 let reverted_hunk_statuses = snapshot
22657 .buffer_snapshot
22658 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
22659 .map(|hunk| hunk.status().kind)
22660 .collect::<Vec<_>>();
22661
22662 editor.git_restore(&Default::default(), window, cx);
22663 reverted_hunk_statuses
22664 });
22665 cx.executor().run_until_parked();
22666 cx.assert_editor_state(expected_reverted_text_with_selections);
22667 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
22668}
22669
22670#[gpui::test(iterations = 10)]
22671async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
22672 init_test(cx, |_| {});
22673
22674 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
22675 let counter = diagnostic_requests.clone();
22676
22677 let fs = FakeFs::new(cx.executor());
22678 fs.insert_tree(
22679 path!("/a"),
22680 json!({
22681 "first.rs": "fn main() { let a = 5; }",
22682 "second.rs": "// Test file",
22683 }),
22684 )
22685 .await;
22686
22687 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22688 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22689 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22690
22691 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22692 language_registry.add(rust_lang());
22693 let mut fake_servers = language_registry.register_fake_lsp(
22694 "Rust",
22695 FakeLspAdapter {
22696 capabilities: lsp::ServerCapabilities {
22697 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
22698 lsp::DiagnosticOptions {
22699 identifier: None,
22700 inter_file_dependencies: true,
22701 workspace_diagnostics: true,
22702 work_done_progress_options: Default::default(),
22703 },
22704 )),
22705 ..Default::default()
22706 },
22707 ..Default::default()
22708 },
22709 );
22710
22711 let editor = workspace
22712 .update(cx, |workspace, window, cx| {
22713 workspace.open_abs_path(
22714 PathBuf::from(path!("/a/first.rs")),
22715 OpenOptions::default(),
22716 window,
22717 cx,
22718 )
22719 })
22720 .unwrap()
22721 .await
22722 .unwrap()
22723 .downcast::<Editor>()
22724 .unwrap();
22725 let fake_server = fake_servers.next().await.unwrap();
22726 let server_id = fake_server.server.server_id();
22727 let mut first_request = fake_server
22728 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
22729 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
22730 let result_id = Some(new_result_id.to_string());
22731 assert_eq!(
22732 params.text_document.uri,
22733 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
22734 );
22735 async move {
22736 Ok(lsp::DocumentDiagnosticReportResult::Report(
22737 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
22738 related_documents: None,
22739 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
22740 items: Vec::new(),
22741 result_id,
22742 },
22743 }),
22744 ))
22745 }
22746 });
22747
22748 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
22749 project.update(cx, |project, cx| {
22750 let buffer_id = editor
22751 .read(cx)
22752 .buffer()
22753 .read(cx)
22754 .as_singleton()
22755 .expect("created a singleton buffer")
22756 .read(cx)
22757 .remote_id();
22758 let buffer_result_id = project
22759 .lsp_store()
22760 .read(cx)
22761 .result_id(server_id, buffer_id, cx);
22762 assert_eq!(expected, buffer_result_id);
22763 });
22764 };
22765
22766 ensure_result_id(None, cx);
22767 cx.executor().advance_clock(Duration::from_millis(60));
22768 cx.executor().run_until_parked();
22769 assert_eq!(
22770 diagnostic_requests.load(atomic::Ordering::Acquire),
22771 1,
22772 "Opening file should trigger diagnostic request"
22773 );
22774 first_request
22775 .next()
22776 .await
22777 .expect("should have sent the first diagnostics pull request");
22778 ensure_result_id(Some("1".to_string()), cx);
22779
22780 // Editing should trigger diagnostics
22781 editor.update_in(cx, |editor, window, cx| {
22782 editor.handle_input("2", window, cx)
22783 });
22784 cx.executor().advance_clock(Duration::from_millis(60));
22785 cx.executor().run_until_parked();
22786 assert_eq!(
22787 diagnostic_requests.load(atomic::Ordering::Acquire),
22788 2,
22789 "Editing should trigger diagnostic request"
22790 );
22791 ensure_result_id(Some("2".to_string()), cx);
22792
22793 // Moving cursor should not trigger diagnostic request
22794 editor.update_in(cx, |editor, window, cx| {
22795 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22796 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
22797 });
22798 });
22799 cx.executor().advance_clock(Duration::from_millis(60));
22800 cx.executor().run_until_parked();
22801 assert_eq!(
22802 diagnostic_requests.load(atomic::Ordering::Acquire),
22803 2,
22804 "Cursor movement should not trigger diagnostic request"
22805 );
22806 ensure_result_id(Some("2".to_string()), cx);
22807 // Multiple rapid edits should be debounced
22808 for _ in 0..5 {
22809 editor.update_in(cx, |editor, window, cx| {
22810 editor.handle_input("x", window, cx)
22811 });
22812 }
22813 cx.executor().advance_clock(Duration::from_millis(60));
22814 cx.executor().run_until_parked();
22815
22816 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
22817 assert!(
22818 final_requests <= 4,
22819 "Multiple rapid edits should be debounced (got {final_requests} requests)",
22820 );
22821 ensure_result_id(Some(final_requests.to_string()), cx);
22822}
22823
22824#[gpui::test]
22825async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
22826 // Regression test for issue #11671
22827 // Previously, adding a cursor after moving multiple cursors would reset
22828 // the cursor count instead of adding to the existing cursors.
22829 init_test(cx, |_| {});
22830 let mut cx = EditorTestContext::new(cx).await;
22831
22832 // Create a simple buffer with cursor at start
22833 cx.set_state(indoc! {"
22834 ˇaaaa
22835 bbbb
22836 cccc
22837 dddd
22838 eeee
22839 ffff
22840 gggg
22841 hhhh"});
22842
22843 // Add 2 cursors below (so we have 3 total)
22844 cx.update_editor(|editor, window, cx| {
22845 editor.add_selection_below(&Default::default(), window, cx);
22846 editor.add_selection_below(&Default::default(), window, cx);
22847 });
22848
22849 // Verify we have 3 cursors
22850 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
22851 assert_eq!(
22852 initial_count, 3,
22853 "Should have 3 cursors after adding 2 below"
22854 );
22855
22856 // Move down one line
22857 cx.update_editor(|editor, window, cx| {
22858 editor.move_down(&MoveDown, window, cx);
22859 });
22860
22861 // Add another cursor below
22862 cx.update_editor(|editor, window, cx| {
22863 editor.add_selection_below(&Default::default(), window, cx);
22864 });
22865
22866 // Should now have 4 cursors (3 original + 1 new)
22867 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
22868 assert_eq!(
22869 final_count, 4,
22870 "Should have 4 cursors after moving and adding another"
22871 );
22872}
22873
22874#[gpui::test(iterations = 10)]
22875async fn test_document_colors(cx: &mut TestAppContext) {
22876 let expected_color = Rgba {
22877 r: 0.33,
22878 g: 0.33,
22879 b: 0.33,
22880 a: 0.33,
22881 };
22882
22883 init_test(cx, |_| {});
22884
22885 let fs = FakeFs::new(cx.executor());
22886 fs.insert_tree(
22887 path!("/a"),
22888 json!({
22889 "first.rs": "fn main() { let a = 5; }",
22890 }),
22891 )
22892 .await;
22893
22894 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22895 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22896 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22897
22898 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22899 language_registry.add(rust_lang());
22900 let mut fake_servers = language_registry.register_fake_lsp(
22901 "Rust",
22902 FakeLspAdapter {
22903 capabilities: lsp::ServerCapabilities {
22904 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
22905 ..lsp::ServerCapabilities::default()
22906 },
22907 name: "rust-analyzer",
22908 ..FakeLspAdapter::default()
22909 },
22910 );
22911 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
22912 "Rust",
22913 FakeLspAdapter {
22914 capabilities: lsp::ServerCapabilities {
22915 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
22916 ..lsp::ServerCapabilities::default()
22917 },
22918 name: "not-rust-analyzer",
22919 ..FakeLspAdapter::default()
22920 },
22921 );
22922
22923 let editor = workspace
22924 .update(cx, |workspace, window, cx| {
22925 workspace.open_abs_path(
22926 PathBuf::from(path!("/a/first.rs")),
22927 OpenOptions::default(),
22928 window,
22929 cx,
22930 )
22931 })
22932 .unwrap()
22933 .await
22934 .unwrap()
22935 .downcast::<Editor>()
22936 .unwrap();
22937 let fake_language_server = fake_servers.next().await.unwrap();
22938 let fake_language_server_without_capabilities =
22939 fake_servers_without_capabilities.next().await.unwrap();
22940 let requests_made = Arc::new(AtomicUsize::new(0));
22941 let closure_requests_made = Arc::clone(&requests_made);
22942 let mut color_request_handle = fake_language_server
22943 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
22944 let requests_made = Arc::clone(&closure_requests_made);
22945 async move {
22946 assert_eq!(
22947 params.text_document.uri,
22948 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
22949 );
22950 requests_made.fetch_add(1, atomic::Ordering::Release);
22951 Ok(vec![
22952 lsp::ColorInformation {
22953 range: lsp::Range {
22954 start: lsp::Position {
22955 line: 0,
22956 character: 0,
22957 },
22958 end: lsp::Position {
22959 line: 0,
22960 character: 1,
22961 },
22962 },
22963 color: lsp::Color {
22964 red: 0.33,
22965 green: 0.33,
22966 blue: 0.33,
22967 alpha: 0.33,
22968 },
22969 },
22970 lsp::ColorInformation {
22971 range: lsp::Range {
22972 start: lsp::Position {
22973 line: 0,
22974 character: 0,
22975 },
22976 end: lsp::Position {
22977 line: 0,
22978 character: 1,
22979 },
22980 },
22981 color: lsp::Color {
22982 red: 0.33,
22983 green: 0.33,
22984 blue: 0.33,
22985 alpha: 0.33,
22986 },
22987 },
22988 ])
22989 }
22990 });
22991
22992 let _handle = fake_language_server_without_capabilities
22993 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
22994 panic!("Should not be called");
22995 });
22996 cx.executor().advance_clock(Duration::from_millis(100));
22997 color_request_handle.next().await.unwrap();
22998 cx.run_until_parked();
22999 assert_eq!(
23000 1,
23001 requests_made.load(atomic::Ordering::Acquire),
23002 "Should query for colors once per editor open"
23003 );
23004 editor.update_in(cx, |editor, _, cx| {
23005 assert_eq!(
23006 vec![expected_color],
23007 extract_color_inlays(editor, cx),
23008 "Should have an initial inlay"
23009 );
23010 });
23011
23012 // opening another file in a split should not influence the LSP query counter
23013 workspace
23014 .update(cx, |workspace, window, cx| {
23015 assert_eq!(
23016 workspace.panes().len(),
23017 1,
23018 "Should have one pane with one editor"
23019 );
23020 workspace.move_item_to_pane_in_direction(
23021 &MoveItemToPaneInDirection {
23022 direction: SplitDirection::Right,
23023 focus: false,
23024 clone: true,
23025 },
23026 window,
23027 cx,
23028 );
23029 })
23030 .unwrap();
23031 cx.run_until_parked();
23032 workspace
23033 .update(cx, |workspace, _, cx| {
23034 let panes = workspace.panes();
23035 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
23036 for pane in panes {
23037 let editor = pane
23038 .read(cx)
23039 .active_item()
23040 .and_then(|item| item.downcast::<Editor>())
23041 .expect("Should have opened an editor in each split");
23042 let editor_file = editor
23043 .read(cx)
23044 .buffer()
23045 .read(cx)
23046 .as_singleton()
23047 .expect("test deals with singleton buffers")
23048 .read(cx)
23049 .file()
23050 .expect("test buffese should have a file")
23051 .path();
23052 assert_eq!(
23053 editor_file.as_ref(),
23054 Path::new("first.rs"),
23055 "Both editors should be opened for the same file"
23056 )
23057 }
23058 })
23059 .unwrap();
23060
23061 cx.executor().advance_clock(Duration::from_millis(500));
23062 let save = editor.update_in(cx, |editor, window, cx| {
23063 editor.move_to_end(&MoveToEnd, window, cx);
23064 editor.handle_input("dirty", window, cx);
23065 editor.save(
23066 SaveOptions {
23067 format: true,
23068 autosave: true,
23069 },
23070 project.clone(),
23071 window,
23072 cx,
23073 )
23074 });
23075 save.await.unwrap();
23076
23077 color_request_handle.next().await.unwrap();
23078 cx.run_until_parked();
23079 assert_eq!(
23080 3,
23081 requests_made.load(atomic::Ordering::Acquire),
23082 "Should query for colors once per save and once per formatting after save"
23083 );
23084
23085 drop(editor);
23086 let close = workspace
23087 .update(cx, |workspace, window, cx| {
23088 workspace.active_pane().update(cx, |pane, cx| {
23089 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23090 })
23091 })
23092 .unwrap();
23093 close.await.unwrap();
23094 let close = workspace
23095 .update(cx, |workspace, window, cx| {
23096 workspace.active_pane().update(cx, |pane, cx| {
23097 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23098 })
23099 })
23100 .unwrap();
23101 close.await.unwrap();
23102 assert_eq!(
23103 3,
23104 requests_made.load(atomic::Ordering::Acquire),
23105 "After saving and closing all editors, no extra requests should be made"
23106 );
23107 workspace
23108 .update(cx, |workspace, _, cx| {
23109 assert!(
23110 workspace.active_item(cx).is_none(),
23111 "Should close all editors"
23112 )
23113 })
23114 .unwrap();
23115
23116 workspace
23117 .update(cx, |workspace, window, cx| {
23118 workspace.active_pane().update(cx, |pane, cx| {
23119 pane.navigate_backward(window, cx);
23120 })
23121 })
23122 .unwrap();
23123 cx.executor().advance_clock(Duration::from_millis(100));
23124 cx.run_until_parked();
23125 let editor = workspace
23126 .update(cx, |workspace, _, cx| {
23127 workspace
23128 .active_item(cx)
23129 .expect("Should have reopened the editor again after navigating back")
23130 .downcast::<Editor>()
23131 .expect("Should be an editor")
23132 })
23133 .unwrap();
23134 color_request_handle.next().await.unwrap();
23135 assert_eq!(
23136 3,
23137 requests_made.load(atomic::Ordering::Acquire),
23138 "Cache should be reused on buffer close and reopen"
23139 );
23140 editor.update(cx, |editor, cx| {
23141 assert_eq!(
23142 vec![expected_color],
23143 extract_color_inlays(editor, cx),
23144 "Should have an initial inlay"
23145 );
23146 });
23147}
23148
23149#[gpui::test]
23150async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
23151 init_test(cx, |_| {});
23152 let (editor, cx) = cx.add_window_view(Editor::single_line);
23153 editor.update_in(cx, |editor, window, cx| {
23154 editor.set_text("oops\n\nwow\n", window, cx)
23155 });
23156 cx.run_until_parked();
23157 editor.update(cx, |editor, cx| {
23158 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
23159 });
23160 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
23161 cx.run_until_parked();
23162 editor.update(cx, |editor, cx| {
23163 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
23164 });
23165}
23166
23167#[track_caller]
23168fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
23169 editor
23170 .all_inlays(cx)
23171 .into_iter()
23172 .filter_map(|inlay| inlay.get_color())
23173 .map(Rgba::from)
23174 .collect()
23175}