1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 edit_prediction_tests::FakeEditPredictionProvider,
6 linked_editing_ranges::LinkedEditingRanges,
7 scroll::scroll_amount::ScrollAmount,
8 test::{
9 assert_text_with_selections, build_editor,
10 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
11 editor_test_context::EditorTestContext,
12 select_ranges,
13 },
14};
15use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
16use futures::StreamExt;
17use gpui::{
18 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
19 VisualTestContext, WindowBounds, WindowOptions, div,
20};
21use indoc::indoc;
22use language::{
23 BracketPairConfig,
24 Capability::ReadWrite,
25 DiagnosticSourceKind, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher,
26 LanguageName, Override, Point,
27 language_settings::{
28 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings, FormatterList,
29 LanguageSettingsContent, LspInsertMode, PrettierSettings, SelectedFormatter,
30 },
31 tree_sitter_python,
32};
33use language_settings::{Formatter, IndentGuideSettings};
34use lsp::CompletionParams;
35use multi_buffer::{IndentGuide, PathKey};
36use parking_lot::Mutex;
37use pretty_assertions::{assert_eq, assert_ne};
38use project::{
39 FakeFs,
40 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
41 project_settings::{LspSettings, ProjectSettings},
42};
43use serde_json::{self, json};
44use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
45use std::{
46 iter,
47 sync::atomic::{self, AtomicUsize},
48};
49use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
50use text::ToPoint as _;
51use unindent::Unindent;
52use util::{
53 assert_set_eq, path,
54 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
55 uri,
56};
57use workspace::{
58 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
59 OpenOptions, ViewId,
60 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
61};
62
63#[gpui::test]
64fn test_edit_events(cx: &mut TestAppContext) {
65 init_test(cx, |_| {});
66
67 let buffer = cx.new(|cx| {
68 let mut buffer = language::Buffer::local("123456", cx);
69 buffer.set_group_interval(Duration::from_secs(1));
70 buffer
71 });
72
73 let events = Rc::new(RefCell::new(Vec::new()));
74 let editor1 = cx.add_window({
75 let events = events.clone();
76 |window, cx| {
77 let entity = cx.entity();
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(),
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, 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, 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, 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, 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, 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, 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, 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_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
1906 init_test(cx, |_| {});
1907
1908 let move_to_beg = MoveToBeginningOfLine {
1909 stop_at_soft_wraps: true,
1910 stop_at_indent: true,
1911 };
1912
1913 let editor = cx.add_window(|window, cx| {
1914 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
1915 build_editor(buffer, window, cx)
1916 });
1917
1918 _ = editor.update(cx, |editor, window, cx| {
1919 // test cursor between line_start and indent_start
1920 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1921 s.select_display_ranges([
1922 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
1923 ]);
1924 });
1925
1926 // cursor should move to line_start
1927 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1928 assert_eq!(
1929 editor.selections.display_ranges(cx),
1930 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1931 );
1932
1933 // cursor should move to indent_start
1934 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1935 assert_eq!(
1936 editor.selections.display_ranges(cx),
1937 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
1938 );
1939
1940 // cursor should move to back to line_start
1941 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1942 assert_eq!(
1943 editor.selections.display_ranges(cx),
1944 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1945 );
1946 });
1947}
1948
1949#[gpui::test]
1950fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1951 init_test(cx, |_| {});
1952
1953 let editor = cx.add_window(|window, cx| {
1954 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1955 build_editor(buffer, window, cx)
1956 });
1957 _ = editor.update(cx, |editor, window, cx| {
1958 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1959 s.select_display_ranges([
1960 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1961 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1962 ])
1963 });
1964 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1965 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1966
1967 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1968 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1969
1970 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1971 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1972
1973 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1974 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1975
1976 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1977 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
1978
1979 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1980 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1981
1982 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1983 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1984
1985 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1986 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1987
1988 editor.move_right(&MoveRight, window, cx);
1989 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1990 assert_selection_ranges(
1991 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
1992 editor,
1993 cx,
1994 );
1995
1996 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1997 assert_selection_ranges(
1998 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
1999 editor,
2000 cx,
2001 );
2002
2003 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2004 assert_selection_ranges(
2005 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2006 editor,
2007 cx,
2008 );
2009 });
2010}
2011
2012#[gpui::test]
2013fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2014 init_test(cx, |_| {});
2015
2016 let editor = cx.add_window(|window, cx| {
2017 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2018 build_editor(buffer, window, cx)
2019 });
2020
2021 _ = editor.update(cx, |editor, window, cx| {
2022 editor.set_wrap_width(Some(140.0.into()), cx);
2023 assert_eq!(
2024 editor.display_text(cx),
2025 "use one::{\n two::three::\n four::five\n};"
2026 );
2027
2028 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2029 s.select_display_ranges([
2030 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2031 ]);
2032 });
2033
2034 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2035 assert_eq!(
2036 editor.selections.display_ranges(cx),
2037 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2038 );
2039
2040 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2041 assert_eq!(
2042 editor.selections.display_ranges(cx),
2043 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2044 );
2045
2046 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2047 assert_eq!(
2048 editor.selections.display_ranges(cx),
2049 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2050 );
2051
2052 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2053 assert_eq!(
2054 editor.selections.display_ranges(cx),
2055 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2056 );
2057
2058 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2059 assert_eq!(
2060 editor.selections.display_ranges(cx),
2061 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2062 );
2063
2064 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2065 assert_eq!(
2066 editor.selections.display_ranges(cx),
2067 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2068 );
2069 });
2070}
2071
2072#[gpui::test]
2073async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2074 init_test(cx, |_| {});
2075 let mut cx = EditorTestContext::new(cx).await;
2076
2077 let line_height = cx.editor(|editor, window, _| {
2078 editor
2079 .style()
2080 .unwrap()
2081 .text
2082 .line_height_in_pixels(window.rem_size())
2083 });
2084 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2085
2086 cx.set_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_end_of_paragraph(&MoveToEndOfParagraph, 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_end_of_paragraph(&MoveToEndOfParagraph, 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_end_of_paragraph(&MoveToEndOfParagraph, 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 cx.update_editor(|editor, window, cx| {
2144 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2145 });
2146 cx.assert_editor_state(
2147 &r#"one
2148 two
2149
2150 three
2151 four
2152 five
2153 ˇ
2154 six"#
2155 .unindent(),
2156 );
2157
2158 cx.update_editor(|editor, window, cx| {
2159 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2160 });
2161 cx.assert_editor_state(
2162 &r#"one
2163 two
2164 ˇ
2165 three
2166 four
2167 five
2168
2169 six"#
2170 .unindent(),
2171 );
2172
2173 cx.update_editor(|editor, window, cx| {
2174 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2175 });
2176 cx.assert_editor_state(
2177 &r#"ˇone
2178 two
2179
2180 three
2181 four
2182 five
2183
2184 six"#
2185 .unindent(),
2186 );
2187}
2188
2189#[gpui::test]
2190async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2191 init_test(cx, |_| {});
2192 let mut cx = EditorTestContext::new(cx).await;
2193 let line_height = cx.editor(|editor, window, _| {
2194 editor
2195 .style()
2196 .unwrap()
2197 .text
2198 .line_height_in_pixels(window.rem_size())
2199 });
2200 let window = cx.window;
2201 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2202
2203 cx.set_state(
2204 r#"ˇone
2205 two
2206 three
2207 four
2208 five
2209 six
2210 seven
2211 eight
2212 nine
2213 ten
2214 "#,
2215 );
2216
2217 cx.update_editor(|editor, window, cx| {
2218 assert_eq!(
2219 editor.snapshot(window, cx).scroll_position(),
2220 gpui::Point::new(0., 0.)
2221 );
2222 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2223 assert_eq!(
2224 editor.snapshot(window, cx).scroll_position(),
2225 gpui::Point::new(0., 3.)
2226 );
2227 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2228 assert_eq!(
2229 editor.snapshot(window, cx).scroll_position(),
2230 gpui::Point::new(0., 6.)
2231 );
2232 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2233 assert_eq!(
2234 editor.snapshot(window, cx).scroll_position(),
2235 gpui::Point::new(0., 3.)
2236 );
2237
2238 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2239 assert_eq!(
2240 editor.snapshot(window, cx).scroll_position(),
2241 gpui::Point::new(0., 1.)
2242 );
2243 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2244 assert_eq!(
2245 editor.snapshot(window, cx).scroll_position(),
2246 gpui::Point::new(0., 3.)
2247 );
2248 });
2249}
2250
2251#[gpui::test]
2252async fn test_autoscroll(cx: &mut TestAppContext) {
2253 init_test(cx, |_| {});
2254 let mut cx = EditorTestContext::new(cx).await;
2255
2256 let line_height = cx.update_editor(|editor, window, cx| {
2257 editor.set_vertical_scroll_margin(2, cx);
2258 editor
2259 .style()
2260 .unwrap()
2261 .text
2262 .line_height_in_pixels(window.rem_size())
2263 });
2264 let window = cx.window;
2265 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2266
2267 cx.set_state(
2268 r#"ˇone
2269 two
2270 three
2271 four
2272 five
2273 six
2274 seven
2275 eight
2276 nine
2277 ten
2278 "#,
2279 );
2280 cx.update_editor(|editor, window, cx| {
2281 assert_eq!(
2282 editor.snapshot(window, cx).scroll_position(),
2283 gpui::Point::new(0., 0.0)
2284 );
2285 });
2286
2287 // Add a cursor below the visible area. Since both cursors cannot fit
2288 // on screen, the editor autoscrolls to reveal the newest cursor, and
2289 // allows the vertical scroll margin below that cursor.
2290 cx.update_editor(|editor, window, cx| {
2291 editor.change_selections(Default::default(), window, cx, |selections| {
2292 selections.select_ranges([
2293 Point::new(0, 0)..Point::new(0, 0),
2294 Point::new(6, 0)..Point::new(6, 0),
2295 ]);
2296 })
2297 });
2298 cx.update_editor(|editor, window, cx| {
2299 assert_eq!(
2300 editor.snapshot(window, cx).scroll_position(),
2301 gpui::Point::new(0., 3.0)
2302 );
2303 });
2304
2305 // Move down. The editor cursor scrolls down to track the newest cursor.
2306 cx.update_editor(|editor, window, cx| {
2307 editor.move_down(&Default::default(), window, cx);
2308 });
2309 cx.update_editor(|editor, window, cx| {
2310 assert_eq!(
2311 editor.snapshot(window, cx).scroll_position(),
2312 gpui::Point::new(0., 4.0)
2313 );
2314 });
2315
2316 // Add a cursor above the visible area. Since both cursors fit on screen,
2317 // the editor scrolls to show both.
2318 cx.update_editor(|editor, window, cx| {
2319 editor.change_selections(Default::default(), window, cx, |selections| {
2320 selections.select_ranges([
2321 Point::new(1, 0)..Point::new(1, 0),
2322 Point::new(6, 0)..Point::new(6, 0),
2323 ]);
2324 })
2325 });
2326 cx.update_editor(|editor, window, cx| {
2327 assert_eq!(
2328 editor.snapshot(window, cx).scroll_position(),
2329 gpui::Point::new(0., 1.0)
2330 );
2331 });
2332}
2333
2334#[gpui::test]
2335async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2336 init_test(cx, |_| {});
2337 let mut cx = EditorTestContext::new(cx).await;
2338
2339 let line_height = cx.editor(|editor, window, _cx| {
2340 editor
2341 .style()
2342 .unwrap()
2343 .text
2344 .line_height_in_pixels(window.rem_size())
2345 });
2346 let window = cx.window;
2347 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2348 cx.set_state(
2349 &r#"
2350 ˇone
2351 two
2352 threeˇ
2353 four
2354 five
2355 six
2356 seven
2357 eight
2358 nine
2359 ten
2360 "#
2361 .unindent(),
2362 );
2363
2364 cx.update_editor(|editor, window, cx| {
2365 editor.move_page_down(&MovePageDown::default(), window, cx)
2366 });
2367 cx.assert_editor_state(
2368 &r#"
2369 one
2370 two
2371 three
2372 ˇfour
2373 five
2374 sixˇ
2375 seven
2376 eight
2377 nine
2378 ten
2379 "#
2380 .unindent(),
2381 );
2382
2383 cx.update_editor(|editor, window, cx| {
2384 editor.move_page_down(&MovePageDown::default(), window, cx)
2385 });
2386 cx.assert_editor_state(
2387 &r#"
2388 one
2389 two
2390 three
2391 four
2392 five
2393 six
2394 ˇseven
2395 eight
2396 nineˇ
2397 ten
2398 "#
2399 .unindent(),
2400 );
2401
2402 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2403 cx.assert_editor_state(
2404 &r#"
2405 one
2406 two
2407 three
2408 ˇfour
2409 five
2410 sixˇ
2411 seven
2412 eight
2413 nine
2414 ten
2415 "#
2416 .unindent(),
2417 );
2418
2419 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2420 cx.assert_editor_state(
2421 &r#"
2422 ˇone
2423 two
2424 threeˇ
2425 four
2426 five
2427 six
2428 seven
2429 eight
2430 nine
2431 ten
2432 "#
2433 .unindent(),
2434 );
2435
2436 // Test select collapsing
2437 cx.update_editor(|editor, window, cx| {
2438 editor.move_page_down(&MovePageDown::default(), window, cx);
2439 editor.move_page_down(&MovePageDown::default(), window, cx);
2440 editor.move_page_down(&MovePageDown::default(), window, cx);
2441 });
2442 cx.assert_editor_state(
2443 &r#"
2444 one
2445 two
2446 three
2447 four
2448 five
2449 six
2450 seven
2451 eight
2452 nine
2453 ˇten
2454 ˇ"#
2455 .unindent(),
2456 );
2457}
2458
2459#[gpui::test]
2460async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2461 init_test(cx, |_| {});
2462 let mut cx = EditorTestContext::new(cx).await;
2463 cx.set_state("one «two threeˇ» four");
2464 cx.update_editor(|editor, window, cx| {
2465 editor.delete_to_beginning_of_line(
2466 &DeleteToBeginningOfLine {
2467 stop_at_indent: false,
2468 },
2469 window,
2470 cx,
2471 );
2472 assert_eq!(editor.text(cx), " four");
2473 });
2474}
2475
2476#[gpui::test]
2477fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2478 init_test(cx, |_| {});
2479
2480 let editor = cx.add_window(|window, cx| {
2481 let buffer = MultiBuffer::build_simple("one two three four", cx);
2482 build_editor(buffer, window, cx)
2483 });
2484
2485 _ = editor.update(cx, |editor, window, cx| {
2486 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2487 s.select_display_ranges([
2488 // an empty selection - the preceding word fragment is deleted
2489 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2490 // characters selected - they are deleted
2491 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2492 ])
2493 });
2494 editor.delete_to_previous_word_start(
2495 &DeleteToPreviousWordStart {
2496 ignore_newlines: false,
2497 },
2498 window,
2499 cx,
2500 );
2501 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2502 });
2503
2504 _ = editor.update(cx, |editor, window, cx| {
2505 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2506 s.select_display_ranges([
2507 // an empty selection - the following word fragment is deleted
2508 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2509 // characters selected - they are deleted
2510 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2511 ])
2512 });
2513 editor.delete_to_next_word_end(
2514 &DeleteToNextWordEnd {
2515 ignore_newlines: false,
2516 },
2517 window,
2518 cx,
2519 );
2520 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2521 });
2522}
2523
2524#[gpui::test]
2525fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2526 init_test(cx, |_| {});
2527
2528 let editor = cx.add_window(|window, cx| {
2529 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2530 build_editor(buffer, window, cx)
2531 });
2532 let del_to_prev_word_start = DeleteToPreviousWordStart {
2533 ignore_newlines: false,
2534 };
2535 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2536 ignore_newlines: true,
2537 };
2538
2539 _ = editor.update(cx, |editor, window, cx| {
2540 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2541 s.select_display_ranges([
2542 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2543 ])
2544 });
2545 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2546 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2547 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2548 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2549 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2550 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2551 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2552 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2553 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2554 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2555 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2556 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2557 });
2558}
2559
2560#[gpui::test]
2561fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2562 init_test(cx, |_| {});
2563
2564 let editor = cx.add_window(|window, cx| {
2565 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2566 build_editor(buffer, window, cx)
2567 });
2568 let del_to_next_word_end = DeleteToNextWordEnd {
2569 ignore_newlines: false,
2570 };
2571 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2572 ignore_newlines: true,
2573 };
2574
2575 _ = editor.update(cx, |editor, window, cx| {
2576 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2577 s.select_display_ranges([
2578 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2579 ])
2580 });
2581 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2582 assert_eq!(
2583 editor.buffer.read(cx).read(cx).text(),
2584 "one\n two\nthree\n four"
2585 );
2586 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2587 assert_eq!(
2588 editor.buffer.read(cx).read(cx).text(),
2589 "\n two\nthree\n four"
2590 );
2591 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2592 assert_eq!(
2593 editor.buffer.read(cx).read(cx).text(),
2594 "two\nthree\n four"
2595 );
2596 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2597 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2598 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2599 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2600 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2601 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2602 });
2603}
2604
2605#[gpui::test]
2606fn test_newline(cx: &mut TestAppContext) {
2607 init_test(cx, |_| {});
2608
2609 let editor = cx.add_window(|window, cx| {
2610 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2611 build_editor(buffer, window, cx)
2612 });
2613
2614 _ = editor.update(cx, |editor, window, cx| {
2615 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2616 s.select_display_ranges([
2617 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2618 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2619 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2620 ])
2621 });
2622
2623 editor.newline(&Newline, window, cx);
2624 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2625 });
2626}
2627
2628#[gpui::test]
2629fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2630 init_test(cx, |_| {});
2631
2632 let editor = cx.add_window(|window, cx| {
2633 let buffer = MultiBuffer::build_simple(
2634 "
2635 a
2636 b(
2637 X
2638 )
2639 c(
2640 X
2641 )
2642 "
2643 .unindent()
2644 .as_str(),
2645 cx,
2646 );
2647 let mut editor = build_editor(buffer, window, cx);
2648 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2649 s.select_ranges([
2650 Point::new(2, 4)..Point::new(2, 5),
2651 Point::new(5, 4)..Point::new(5, 5),
2652 ])
2653 });
2654 editor
2655 });
2656
2657 _ = editor.update(cx, |editor, window, cx| {
2658 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2659 editor.buffer.update(cx, |buffer, cx| {
2660 buffer.edit(
2661 [
2662 (Point::new(1, 2)..Point::new(3, 0), ""),
2663 (Point::new(4, 2)..Point::new(6, 0), ""),
2664 ],
2665 None,
2666 cx,
2667 );
2668 assert_eq!(
2669 buffer.read(cx).text(),
2670 "
2671 a
2672 b()
2673 c()
2674 "
2675 .unindent()
2676 );
2677 });
2678 assert_eq!(
2679 editor.selections.ranges(cx),
2680 &[
2681 Point::new(1, 2)..Point::new(1, 2),
2682 Point::new(2, 2)..Point::new(2, 2),
2683 ],
2684 );
2685
2686 editor.newline(&Newline, window, cx);
2687 assert_eq!(
2688 editor.text(cx),
2689 "
2690 a
2691 b(
2692 )
2693 c(
2694 )
2695 "
2696 .unindent()
2697 );
2698
2699 // The selections are moved after the inserted newlines
2700 assert_eq!(
2701 editor.selections.ranges(cx),
2702 &[
2703 Point::new(2, 0)..Point::new(2, 0),
2704 Point::new(4, 0)..Point::new(4, 0),
2705 ],
2706 );
2707 });
2708}
2709
2710#[gpui::test]
2711async fn test_newline_above(cx: &mut TestAppContext) {
2712 init_test(cx, |settings| {
2713 settings.defaults.tab_size = NonZeroU32::new(4)
2714 });
2715
2716 let language = Arc::new(
2717 Language::new(
2718 LanguageConfig::default(),
2719 Some(tree_sitter_rust::LANGUAGE.into()),
2720 )
2721 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2722 .unwrap(),
2723 );
2724
2725 let mut cx = EditorTestContext::new(cx).await;
2726 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2727 cx.set_state(indoc! {"
2728 const a: ˇA = (
2729 (ˇ
2730 «const_functionˇ»(ˇ),
2731 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2732 )ˇ
2733 ˇ);ˇ
2734 "});
2735
2736 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2737 cx.assert_editor_state(indoc! {"
2738 ˇ
2739 const a: A = (
2740 ˇ
2741 (
2742 ˇ
2743 ˇ
2744 const_function(),
2745 ˇ
2746 ˇ
2747 ˇ
2748 ˇ
2749 something_else,
2750 ˇ
2751 )
2752 ˇ
2753 ˇ
2754 );
2755 "});
2756}
2757
2758#[gpui::test]
2759async fn test_newline_below(cx: &mut TestAppContext) {
2760 init_test(cx, |settings| {
2761 settings.defaults.tab_size = NonZeroU32::new(4)
2762 });
2763
2764 let language = Arc::new(
2765 Language::new(
2766 LanguageConfig::default(),
2767 Some(tree_sitter_rust::LANGUAGE.into()),
2768 )
2769 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2770 .unwrap(),
2771 );
2772
2773 let mut cx = EditorTestContext::new(cx).await;
2774 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2775 cx.set_state(indoc! {"
2776 const a: ˇA = (
2777 (ˇ
2778 «const_functionˇ»(ˇ),
2779 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2780 )ˇ
2781 ˇ);ˇ
2782 "});
2783
2784 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2785 cx.assert_editor_state(indoc! {"
2786 const a: A = (
2787 ˇ
2788 (
2789 ˇ
2790 const_function(),
2791 ˇ
2792 ˇ
2793 something_else,
2794 ˇ
2795 ˇ
2796 ˇ
2797 ˇ
2798 )
2799 ˇ
2800 );
2801 ˇ
2802 ˇ
2803 "});
2804}
2805
2806#[gpui::test]
2807async fn test_newline_comments(cx: &mut TestAppContext) {
2808 init_test(cx, |settings| {
2809 settings.defaults.tab_size = NonZeroU32::new(4)
2810 });
2811
2812 let language = Arc::new(Language::new(
2813 LanguageConfig {
2814 line_comments: vec!["// ".into()],
2815 ..LanguageConfig::default()
2816 },
2817 None,
2818 ));
2819 {
2820 let mut cx = EditorTestContext::new(cx).await;
2821 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2822 cx.set_state(indoc! {"
2823 // Fooˇ
2824 "});
2825
2826 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2827 cx.assert_editor_state(indoc! {"
2828 // Foo
2829 // ˇ
2830 "});
2831 // Ensure that we add comment prefix when existing line contains space
2832 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2833 cx.assert_editor_state(
2834 indoc! {"
2835 // Foo
2836 //s
2837 // ˇ
2838 "}
2839 .replace("s", " ") // s is used as space placeholder to prevent format on save
2840 .as_str(),
2841 );
2842 // Ensure that we add comment prefix when existing line does not contain space
2843 cx.set_state(indoc! {"
2844 // Foo
2845 //ˇ
2846 "});
2847 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2848 cx.assert_editor_state(indoc! {"
2849 // Foo
2850 //
2851 // ˇ
2852 "});
2853 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2854 cx.set_state(indoc! {"
2855 ˇ// Foo
2856 "});
2857 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2858 cx.assert_editor_state(indoc! {"
2859
2860 ˇ// Foo
2861 "});
2862 }
2863 // Ensure that comment continuations can be disabled.
2864 update_test_language_settings(cx, |settings| {
2865 settings.defaults.extend_comment_on_newline = Some(false);
2866 });
2867 let mut cx = EditorTestContext::new(cx).await;
2868 cx.set_state(indoc! {"
2869 // Fooˇ
2870 "});
2871 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2872 cx.assert_editor_state(indoc! {"
2873 // Foo
2874 ˇ
2875 "});
2876}
2877
2878#[gpui::test]
2879async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
2880 init_test(cx, |settings| {
2881 settings.defaults.tab_size = NonZeroU32::new(4)
2882 });
2883
2884 let language = Arc::new(Language::new(
2885 LanguageConfig {
2886 line_comments: vec!["// ".into(), "/// ".into()],
2887 ..LanguageConfig::default()
2888 },
2889 None,
2890 ));
2891 {
2892 let mut cx = EditorTestContext::new(cx).await;
2893 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2894 cx.set_state(indoc! {"
2895 //ˇ
2896 "});
2897 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2898 cx.assert_editor_state(indoc! {"
2899 //
2900 // ˇ
2901 "});
2902
2903 cx.set_state(indoc! {"
2904 ///ˇ
2905 "});
2906 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2907 cx.assert_editor_state(indoc! {"
2908 ///
2909 /// ˇ
2910 "});
2911 }
2912}
2913
2914#[gpui::test]
2915async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
2916 init_test(cx, |settings| {
2917 settings.defaults.tab_size = NonZeroU32::new(4)
2918 });
2919
2920 let language = Arc::new(
2921 Language::new(
2922 LanguageConfig {
2923 documentation_comment: Some(language::BlockCommentConfig {
2924 start: "/**".into(),
2925 end: "*/".into(),
2926 prefix: "* ".into(),
2927 tab_size: 1,
2928 }),
2929
2930 ..LanguageConfig::default()
2931 },
2932 Some(tree_sitter_rust::LANGUAGE.into()),
2933 )
2934 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
2935 .unwrap(),
2936 );
2937
2938 {
2939 let mut cx = EditorTestContext::new(cx).await;
2940 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2941 cx.set_state(indoc! {"
2942 /**ˇ
2943 "});
2944
2945 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2946 cx.assert_editor_state(indoc! {"
2947 /**
2948 * ˇ
2949 "});
2950 // Ensure that if cursor is before the comment start,
2951 // we do not actually insert a comment prefix.
2952 cx.set_state(indoc! {"
2953 ˇ/**
2954 "});
2955 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2956 cx.assert_editor_state(indoc! {"
2957
2958 ˇ/**
2959 "});
2960 // Ensure that if cursor is between it doesn't add comment prefix.
2961 cx.set_state(indoc! {"
2962 /*ˇ*
2963 "});
2964 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2965 cx.assert_editor_state(indoc! {"
2966 /*
2967 ˇ*
2968 "});
2969 // Ensure that if suffix exists on same line after cursor it adds new line.
2970 cx.set_state(indoc! {"
2971 /**ˇ*/
2972 "});
2973 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2974 cx.assert_editor_state(indoc! {"
2975 /**
2976 * ˇ
2977 */
2978 "});
2979 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2980 cx.set_state(indoc! {"
2981 /**ˇ */
2982 "});
2983 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2984 cx.assert_editor_state(indoc! {"
2985 /**
2986 * ˇ
2987 */
2988 "});
2989 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2990 cx.set_state(indoc! {"
2991 /** ˇ*/
2992 "});
2993 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2994 cx.assert_editor_state(
2995 indoc! {"
2996 /**s
2997 * ˇ
2998 */
2999 "}
3000 .replace("s", " ") // s is used as space placeholder to prevent format on save
3001 .as_str(),
3002 );
3003 // Ensure that delimiter space is preserved when newline on already
3004 // spaced delimiter.
3005 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3006 cx.assert_editor_state(
3007 indoc! {"
3008 /**s
3009 *s
3010 * ˇ
3011 */
3012 "}
3013 .replace("s", " ") // s is used as space placeholder to prevent format on save
3014 .as_str(),
3015 );
3016 // Ensure that delimiter space is preserved when space is not
3017 // on existing delimiter.
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 // Ensure that if suffix exists on same line after cursor it
3031 // doesn't add extra new line if prefix is not on same line.
3032 cx.set_state(indoc! {"
3033 /**
3034 ˇ*/
3035 "});
3036 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3037 cx.assert_editor_state(indoc! {"
3038 /**
3039
3040 ˇ*/
3041 "});
3042 // Ensure that it detects suffix after existing prefix.
3043 cx.set_state(indoc! {"
3044 /**ˇ/
3045 "});
3046 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3047 cx.assert_editor_state(indoc! {"
3048 /**
3049 ˇ/
3050 "});
3051 // Ensure that if suffix exists on same line before
3052 // cursor it does not add comment prefix.
3053 cx.set_state(indoc! {"
3054 /** */ˇ
3055 "});
3056 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3057 cx.assert_editor_state(indoc! {"
3058 /** */
3059 ˇ
3060 "});
3061 // Ensure that if suffix exists on same line before
3062 // cursor it does not add comment prefix.
3063 cx.set_state(indoc! {"
3064 /**
3065 *
3066 */ˇ
3067 "});
3068 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3069 cx.assert_editor_state(indoc! {"
3070 /**
3071 *
3072 */
3073 ˇ
3074 "});
3075
3076 // Ensure that inline comment followed by code
3077 // doesn't add comment prefix on newline
3078 cx.set_state(indoc! {"
3079 /** */ textˇ
3080 "});
3081 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3082 cx.assert_editor_state(indoc! {"
3083 /** */ text
3084 ˇ
3085 "});
3086
3087 // Ensure that text after comment end tag
3088 // doesn't add comment prefix on newline
3089 cx.set_state(indoc! {"
3090 /**
3091 *
3092 */ˇtext
3093 "});
3094 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3095 cx.assert_editor_state(indoc! {"
3096 /**
3097 *
3098 */
3099 ˇtext
3100 "});
3101
3102 // Ensure if not comment block it doesn't
3103 // add comment prefix on newline
3104 cx.set_state(indoc! {"
3105 * textˇ
3106 "});
3107 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3108 cx.assert_editor_state(indoc! {"
3109 * text
3110 ˇ
3111 "});
3112 }
3113 // Ensure that comment continuations can be disabled.
3114 update_test_language_settings(cx, |settings| {
3115 settings.defaults.extend_comment_on_newline = Some(false);
3116 });
3117 let mut cx = EditorTestContext::new(cx).await;
3118 cx.set_state(indoc! {"
3119 /**ˇ
3120 "});
3121 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3122 cx.assert_editor_state(indoc! {"
3123 /**
3124 ˇ
3125 "});
3126}
3127
3128#[gpui::test]
3129async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3130 init_test(cx, |settings| {
3131 settings.defaults.tab_size = NonZeroU32::new(4)
3132 });
3133
3134 let lua_language = Arc::new(Language::new(
3135 LanguageConfig {
3136 line_comments: vec!["--".into()],
3137 block_comment: Some(language::BlockCommentConfig {
3138 start: "--[[".into(),
3139 prefix: "".into(),
3140 end: "]]".into(),
3141 tab_size: 0,
3142 }),
3143 ..LanguageConfig::default()
3144 },
3145 None,
3146 ));
3147
3148 let mut cx = EditorTestContext::new(cx).await;
3149 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3150
3151 // Line with line comment should extend
3152 cx.set_state(indoc! {"
3153 --ˇ
3154 "});
3155 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3156 cx.assert_editor_state(indoc! {"
3157 --
3158 --ˇ
3159 "});
3160
3161 // Line with block comment that matches line comment should not extend
3162 cx.set_state(indoc! {"
3163 --[[ˇ
3164 "});
3165 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3166 cx.assert_editor_state(indoc! {"
3167 --[[
3168 ˇ
3169 "});
3170}
3171
3172#[gpui::test]
3173fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3174 init_test(cx, |_| {});
3175
3176 let editor = cx.add_window(|window, cx| {
3177 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3178 let mut editor = build_editor(buffer, window, cx);
3179 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3180 s.select_ranges([3..4, 11..12, 19..20])
3181 });
3182 editor
3183 });
3184
3185 _ = editor.update(cx, |editor, window, cx| {
3186 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3187 editor.buffer.update(cx, |buffer, cx| {
3188 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3189 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3190 });
3191 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3192
3193 editor.insert("Z", window, cx);
3194 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3195
3196 // The selections are moved after the inserted characters
3197 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3198 });
3199}
3200
3201#[gpui::test]
3202async fn test_tab(cx: &mut TestAppContext) {
3203 init_test(cx, |settings| {
3204 settings.defaults.tab_size = NonZeroU32::new(3)
3205 });
3206
3207 let mut cx = EditorTestContext::new(cx).await;
3208 cx.set_state(indoc! {"
3209 ˇabˇc
3210 ˇ🏀ˇ🏀ˇefg
3211 dˇ
3212 "});
3213 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3214 cx.assert_editor_state(indoc! {"
3215 ˇab ˇc
3216 ˇ🏀 ˇ🏀 ˇefg
3217 d ˇ
3218 "});
3219
3220 cx.set_state(indoc! {"
3221 a
3222 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3223 "});
3224 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3225 cx.assert_editor_state(indoc! {"
3226 a
3227 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3228 "});
3229}
3230
3231#[gpui::test]
3232async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3233 init_test(cx, |_| {});
3234
3235 let mut cx = EditorTestContext::new(cx).await;
3236 let language = Arc::new(
3237 Language::new(
3238 LanguageConfig::default(),
3239 Some(tree_sitter_rust::LANGUAGE.into()),
3240 )
3241 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3242 .unwrap(),
3243 );
3244 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3245
3246 // test when all cursors are not at suggested indent
3247 // then simply move to their suggested indent location
3248 cx.set_state(indoc! {"
3249 const a: B = (
3250 c(
3251 ˇ
3252 ˇ )
3253 );
3254 "});
3255 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3256 cx.assert_editor_state(indoc! {"
3257 const a: B = (
3258 c(
3259 ˇ
3260 ˇ)
3261 );
3262 "});
3263
3264 // test cursor already at suggested indent not moving when
3265 // other cursors are yet to reach their suggested indents
3266 cx.set_state(indoc! {"
3267 ˇ
3268 const a: B = (
3269 c(
3270 d(
3271 ˇ
3272 )
3273 ˇ
3274 ˇ )
3275 );
3276 "});
3277 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3278 cx.assert_editor_state(indoc! {"
3279 ˇ
3280 const a: B = (
3281 c(
3282 d(
3283 ˇ
3284 )
3285 ˇ
3286 ˇ)
3287 );
3288 "});
3289 // test when all cursors are at suggested indent then tab is inserted
3290 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3291 cx.assert_editor_state(indoc! {"
3292 ˇ
3293 const a: B = (
3294 c(
3295 d(
3296 ˇ
3297 )
3298 ˇ
3299 ˇ)
3300 );
3301 "});
3302
3303 // test when current indent is less than suggested indent,
3304 // we adjust line to match suggested indent and move cursor to it
3305 //
3306 // when no other cursor is at word boundary, all of them should move
3307 cx.set_state(indoc! {"
3308 const a: B = (
3309 c(
3310 d(
3311 ˇ
3312 ˇ )
3313 ˇ )
3314 );
3315 "});
3316 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3317 cx.assert_editor_state(indoc! {"
3318 const a: B = (
3319 c(
3320 d(
3321 ˇ
3322 ˇ)
3323 ˇ)
3324 );
3325 "});
3326
3327 // test when current indent is less than suggested indent,
3328 // we adjust line to match suggested indent and move cursor to it
3329 //
3330 // when some other cursor is at word boundary, it should not move
3331 cx.set_state(indoc! {"
3332 const a: B = (
3333 c(
3334 d(
3335 ˇ
3336 ˇ )
3337 ˇ)
3338 );
3339 "});
3340 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3341 cx.assert_editor_state(indoc! {"
3342 const a: B = (
3343 c(
3344 d(
3345 ˇ
3346 ˇ)
3347 ˇ)
3348 );
3349 "});
3350
3351 // test when current indent is more than suggested indent,
3352 // we just move cursor to current indent instead of suggested indent
3353 //
3354 // when no other cursor is at word boundary, all of them should move
3355 cx.set_state(indoc! {"
3356 const a: B = (
3357 c(
3358 d(
3359 ˇ
3360 ˇ )
3361 ˇ )
3362 );
3363 "});
3364 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3365 cx.assert_editor_state(indoc! {"
3366 const a: B = (
3367 c(
3368 d(
3369 ˇ
3370 ˇ)
3371 ˇ)
3372 );
3373 "});
3374 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3375 cx.assert_editor_state(indoc! {"
3376 const a: B = (
3377 c(
3378 d(
3379 ˇ
3380 ˇ)
3381 ˇ)
3382 );
3383 "});
3384
3385 // test when current indent is more than suggested indent,
3386 // we just move cursor to current indent instead of suggested indent
3387 //
3388 // when some other cursor is at word boundary, it doesn't move
3389 cx.set_state(indoc! {"
3390 const a: B = (
3391 c(
3392 d(
3393 ˇ
3394 ˇ )
3395 ˇ)
3396 );
3397 "});
3398 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3399 cx.assert_editor_state(indoc! {"
3400 const a: B = (
3401 c(
3402 d(
3403 ˇ
3404 ˇ)
3405 ˇ)
3406 );
3407 "});
3408
3409 // handle auto-indent when there are multiple cursors on the same line
3410 cx.set_state(indoc! {"
3411 const a: B = (
3412 c(
3413 ˇ ˇ
3414 ˇ )
3415 );
3416 "});
3417 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3418 cx.assert_editor_state(indoc! {"
3419 const a: B = (
3420 c(
3421 ˇ
3422 ˇ)
3423 );
3424 "});
3425}
3426
3427#[gpui::test]
3428async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3429 init_test(cx, |settings| {
3430 settings.defaults.tab_size = NonZeroU32::new(3)
3431 });
3432
3433 let mut cx = EditorTestContext::new(cx).await;
3434 cx.set_state(indoc! {"
3435 ˇ
3436 \t ˇ
3437 \t ˇ
3438 \t ˇ
3439 \t \t\t \t \t\t \t\t \t \t ˇ
3440 "});
3441
3442 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3443 cx.assert_editor_state(indoc! {"
3444 ˇ
3445 \t ˇ
3446 \t ˇ
3447 \t ˇ
3448 \t \t\t \t \t\t \t\t \t \t ˇ
3449 "});
3450}
3451
3452#[gpui::test]
3453async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3454 init_test(cx, |settings| {
3455 settings.defaults.tab_size = NonZeroU32::new(4)
3456 });
3457
3458 let language = Arc::new(
3459 Language::new(
3460 LanguageConfig::default(),
3461 Some(tree_sitter_rust::LANGUAGE.into()),
3462 )
3463 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3464 .unwrap(),
3465 );
3466
3467 let mut cx = EditorTestContext::new(cx).await;
3468 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3469 cx.set_state(indoc! {"
3470 fn a() {
3471 if b {
3472 \t ˇc
3473 }
3474 }
3475 "});
3476
3477 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3478 cx.assert_editor_state(indoc! {"
3479 fn a() {
3480 if b {
3481 ˇc
3482 }
3483 }
3484 "});
3485}
3486
3487#[gpui::test]
3488async fn test_indent_outdent(cx: &mut TestAppContext) {
3489 init_test(cx, |settings| {
3490 settings.defaults.tab_size = NonZeroU32::new(4);
3491 });
3492
3493 let mut cx = EditorTestContext::new(cx).await;
3494
3495 cx.set_state(indoc! {"
3496 «oneˇ» «twoˇ»
3497 three
3498 four
3499 "});
3500 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3501 cx.assert_editor_state(indoc! {"
3502 «oneˇ» «twoˇ»
3503 three
3504 four
3505 "});
3506
3507 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3508 cx.assert_editor_state(indoc! {"
3509 «oneˇ» «twoˇ»
3510 three
3511 four
3512 "});
3513
3514 // select across line ending
3515 cx.set_state(indoc! {"
3516 one two
3517 t«hree
3518 ˇ» four
3519 "});
3520 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3521 cx.assert_editor_state(indoc! {"
3522 one two
3523 t«hree
3524 ˇ» four
3525 "});
3526
3527 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3528 cx.assert_editor_state(indoc! {"
3529 one two
3530 t«hree
3531 ˇ» four
3532 "});
3533
3534 // Ensure that indenting/outdenting works when the cursor is at column 0.
3535 cx.set_state(indoc! {"
3536 one two
3537 ˇthree
3538 four
3539 "});
3540 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3541 cx.assert_editor_state(indoc! {"
3542 one two
3543 ˇthree
3544 four
3545 "});
3546
3547 cx.set_state(indoc! {"
3548 one two
3549 ˇ three
3550 four
3551 "});
3552 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3553 cx.assert_editor_state(indoc! {"
3554 one two
3555 ˇthree
3556 four
3557 "});
3558}
3559
3560#[gpui::test]
3561async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3562 // This is a regression test for issue #33761
3563 init_test(cx, |_| {});
3564
3565 let mut cx = EditorTestContext::new(cx).await;
3566 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3567 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3568
3569 cx.set_state(
3570 r#"ˇ# ingress:
3571ˇ# api:
3572ˇ# enabled: false
3573ˇ# pathType: Prefix
3574ˇ# console:
3575ˇ# enabled: false
3576ˇ# pathType: Prefix
3577"#,
3578 );
3579
3580 // Press tab to indent all lines
3581 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3582
3583 cx.assert_editor_state(
3584 r#" ˇ# ingress:
3585 ˇ# api:
3586 ˇ# enabled: false
3587 ˇ# pathType: Prefix
3588 ˇ# console:
3589 ˇ# enabled: false
3590 ˇ# pathType: Prefix
3591"#,
3592 );
3593}
3594
3595#[gpui::test]
3596async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3597 // This is a test to make sure our fix for issue #33761 didn't break anything
3598 init_test(cx, |_| {});
3599
3600 let mut cx = EditorTestContext::new(cx).await;
3601 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3602 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3603
3604 cx.set_state(
3605 r#"ˇingress:
3606ˇ api:
3607ˇ enabled: false
3608ˇ pathType: Prefix
3609"#,
3610 );
3611
3612 // Press tab to indent all lines
3613 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3614
3615 cx.assert_editor_state(
3616 r#"ˇingress:
3617 ˇapi:
3618 ˇenabled: false
3619 ˇpathType: Prefix
3620"#,
3621 );
3622}
3623
3624#[gpui::test]
3625async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3626 init_test(cx, |settings| {
3627 settings.defaults.hard_tabs = Some(true);
3628 });
3629
3630 let mut cx = EditorTestContext::new(cx).await;
3631
3632 // select two ranges on one line
3633 cx.set_state(indoc! {"
3634 «oneˇ» «twoˇ»
3635 three
3636 four
3637 "});
3638 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3639 cx.assert_editor_state(indoc! {"
3640 \t«oneˇ» «twoˇ»
3641 three
3642 four
3643 "});
3644 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3645 cx.assert_editor_state(indoc! {"
3646 \t\t«oneˇ» «twoˇ»
3647 three
3648 four
3649 "});
3650 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3651 cx.assert_editor_state(indoc! {"
3652 \t«oneˇ» «twoˇ»
3653 three
3654 four
3655 "});
3656 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3657 cx.assert_editor_state(indoc! {"
3658 «oneˇ» «twoˇ»
3659 three
3660 four
3661 "});
3662
3663 // select across a line ending
3664 cx.set_state(indoc! {"
3665 one two
3666 t«hree
3667 ˇ»four
3668 "});
3669 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3670 cx.assert_editor_state(indoc! {"
3671 one two
3672 \tt«hree
3673 ˇ»four
3674 "});
3675 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3676 cx.assert_editor_state(indoc! {"
3677 one two
3678 \t\tt«hree
3679 ˇ»four
3680 "});
3681 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3682 cx.assert_editor_state(indoc! {"
3683 one two
3684 \tt«hree
3685 ˇ»four
3686 "});
3687 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3688 cx.assert_editor_state(indoc! {"
3689 one two
3690 t«hree
3691 ˇ»four
3692 "});
3693
3694 // Ensure that indenting/outdenting works when the cursor is at column 0.
3695 cx.set_state(indoc! {"
3696 one two
3697 ˇthree
3698 four
3699 "});
3700 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3701 cx.assert_editor_state(indoc! {"
3702 one two
3703 ˇthree
3704 four
3705 "});
3706 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3707 cx.assert_editor_state(indoc! {"
3708 one two
3709 \tˇthree
3710 four
3711 "});
3712 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3713 cx.assert_editor_state(indoc! {"
3714 one two
3715 ˇthree
3716 four
3717 "});
3718}
3719
3720#[gpui::test]
3721fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3722 init_test(cx, |settings| {
3723 settings.languages.0.extend([
3724 (
3725 "TOML".into(),
3726 LanguageSettingsContent {
3727 tab_size: NonZeroU32::new(2),
3728 ..Default::default()
3729 },
3730 ),
3731 (
3732 "Rust".into(),
3733 LanguageSettingsContent {
3734 tab_size: NonZeroU32::new(4),
3735 ..Default::default()
3736 },
3737 ),
3738 ]);
3739 });
3740
3741 let toml_language = Arc::new(Language::new(
3742 LanguageConfig {
3743 name: "TOML".into(),
3744 ..Default::default()
3745 },
3746 None,
3747 ));
3748 let rust_language = Arc::new(Language::new(
3749 LanguageConfig {
3750 name: "Rust".into(),
3751 ..Default::default()
3752 },
3753 None,
3754 ));
3755
3756 let toml_buffer =
3757 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3758 let rust_buffer =
3759 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3760 let multibuffer = cx.new(|cx| {
3761 let mut multibuffer = MultiBuffer::new(ReadWrite);
3762 multibuffer.push_excerpts(
3763 toml_buffer.clone(),
3764 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3765 cx,
3766 );
3767 multibuffer.push_excerpts(
3768 rust_buffer.clone(),
3769 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3770 cx,
3771 );
3772 multibuffer
3773 });
3774
3775 cx.add_window(|window, cx| {
3776 let mut editor = build_editor(multibuffer, window, cx);
3777
3778 assert_eq!(
3779 editor.text(cx),
3780 indoc! {"
3781 a = 1
3782 b = 2
3783
3784 const c: usize = 3;
3785 "}
3786 );
3787
3788 select_ranges(
3789 &mut editor,
3790 indoc! {"
3791 «aˇ» = 1
3792 b = 2
3793
3794 «const c:ˇ» usize = 3;
3795 "},
3796 window,
3797 cx,
3798 );
3799
3800 editor.tab(&Tab, window, cx);
3801 assert_text_with_selections(
3802 &mut editor,
3803 indoc! {"
3804 «aˇ» = 1
3805 b = 2
3806
3807 «const c:ˇ» usize = 3;
3808 "},
3809 cx,
3810 );
3811 editor.backtab(&Backtab, window, cx);
3812 assert_text_with_selections(
3813 &mut editor,
3814 indoc! {"
3815 «aˇ» = 1
3816 b = 2
3817
3818 «const c:ˇ» usize = 3;
3819 "},
3820 cx,
3821 );
3822
3823 editor
3824 });
3825}
3826
3827#[gpui::test]
3828async fn test_backspace(cx: &mut TestAppContext) {
3829 init_test(cx, |_| {});
3830
3831 let mut cx = EditorTestContext::new(cx).await;
3832
3833 // Basic backspace
3834 cx.set_state(indoc! {"
3835 onˇe two three
3836 fou«rˇ» five six
3837 seven «ˇeight nine
3838 »ten
3839 "});
3840 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3841 cx.assert_editor_state(indoc! {"
3842 oˇe two three
3843 fouˇ five six
3844 seven ˇten
3845 "});
3846
3847 // Test backspace inside and around indents
3848 cx.set_state(indoc! {"
3849 zero
3850 ˇone
3851 ˇtwo
3852 ˇ ˇ ˇ three
3853 ˇ ˇ four
3854 "});
3855 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3856 cx.assert_editor_state(indoc! {"
3857 zero
3858 ˇone
3859 ˇtwo
3860 ˇ threeˇ four
3861 "});
3862}
3863
3864#[gpui::test]
3865async fn test_delete(cx: &mut TestAppContext) {
3866 init_test(cx, |_| {});
3867
3868 let mut cx = EditorTestContext::new(cx).await;
3869 cx.set_state(indoc! {"
3870 onˇe two three
3871 fou«rˇ» five six
3872 seven «ˇeight nine
3873 »ten
3874 "});
3875 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3876 cx.assert_editor_state(indoc! {"
3877 onˇ two three
3878 fouˇ five six
3879 seven ˇten
3880 "});
3881}
3882
3883#[gpui::test]
3884fn test_delete_line(cx: &mut TestAppContext) {
3885 init_test(cx, |_| {});
3886
3887 let editor = cx.add_window(|window, cx| {
3888 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3889 build_editor(buffer, window, cx)
3890 });
3891 _ = editor.update(cx, |editor, window, cx| {
3892 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3893 s.select_display_ranges([
3894 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3895 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3896 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3897 ])
3898 });
3899 editor.delete_line(&DeleteLine, window, cx);
3900 assert_eq!(editor.display_text(cx), "ghi");
3901 assert_eq!(
3902 editor.selections.display_ranges(cx),
3903 vec![
3904 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3905 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3906 ]
3907 );
3908 });
3909
3910 let editor = cx.add_window(|window, cx| {
3911 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3912 build_editor(buffer, window, cx)
3913 });
3914 _ = editor.update(cx, |editor, window, cx| {
3915 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3916 s.select_display_ranges([
3917 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3918 ])
3919 });
3920 editor.delete_line(&DeleteLine, window, cx);
3921 assert_eq!(editor.display_text(cx), "ghi\n");
3922 assert_eq!(
3923 editor.selections.display_ranges(cx),
3924 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3925 );
3926 });
3927}
3928
3929#[gpui::test]
3930fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3931 init_test(cx, |_| {});
3932
3933 cx.add_window(|window, cx| {
3934 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3935 let mut editor = build_editor(buffer.clone(), window, cx);
3936 let buffer = buffer.read(cx).as_singleton().unwrap();
3937
3938 assert_eq!(
3939 editor.selections.ranges::<Point>(cx),
3940 &[Point::new(0, 0)..Point::new(0, 0)]
3941 );
3942
3943 // When on single line, replace newline at end by space
3944 editor.join_lines(&JoinLines, window, cx);
3945 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3946 assert_eq!(
3947 editor.selections.ranges::<Point>(cx),
3948 &[Point::new(0, 3)..Point::new(0, 3)]
3949 );
3950
3951 // When multiple lines are selected, remove newlines that are spanned by the selection
3952 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3953 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3954 });
3955 editor.join_lines(&JoinLines, window, cx);
3956 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3957 assert_eq!(
3958 editor.selections.ranges::<Point>(cx),
3959 &[Point::new(0, 11)..Point::new(0, 11)]
3960 );
3961
3962 // Undo should be transactional
3963 editor.undo(&Undo, window, cx);
3964 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3965 assert_eq!(
3966 editor.selections.ranges::<Point>(cx),
3967 &[Point::new(0, 5)..Point::new(2, 2)]
3968 );
3969
3970 // When joining an empty line don't insert a space
3971 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3972 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3973 });
3974 editor.join_lines(&JoinLines, window, cx);
3975 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3976 assert_eq!(
3977 editor.selections.ranges::<Point>(cx),
3978 [Point::new(2, 3)..Point::new(2, 3)]
3979 );
3980
3981 // We can remove trailing newlines
3982 editor.join_lines(&JoinLines, window, cx);
3983 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3984 assert_eq!(
3985 editor.selections.ranges::<Point>(cx),
3986 [Point::new(2, 3)..Point::new(2, 3)]
3987 );
3988
3989 // We don't blow up on the last line
3990 editor.join_lines(&JoinLines, window, cx);
3991 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3992 assert_eq!(
3993 editor.selections.ranges::<Point>(cx),
3994 [Point::new(2, 3)..Point::new(2, 3)]
3995 );
3996
3997 // reset to test indentation
3998 editor.buffer.update(cx, |buffer, cx| {
3999 buffer.edit(
4000 [
4001 (Point::new(1, 0)..Point::new(1, 2), " "),
4002 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4003 ],
4004 None,
4005 cx,
4006 )
4007 });
4008
4009 // We remove any leading spaces
4010 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4011 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4012 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4013 });
4014 editor.join_lines(&JoinLines, window, cx);
4015 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4016
4017 // We don't insert a space for a line containing only spaces
4018 editor.join_lines(&JoinLines, window, cx);
4019 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4020
4021 // We ignore any leading tabs
4022 editor.join_lines(&JoinLines, window, cx);
4023 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4024
4025 editor
4026 });
4027}
4028
4029#[gpui::test]
4030fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4031 init_test(cx, |_| {});
4032
4033 cx.add_window(|window, cx| {
4034 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4035 let mut editor = build_editor(buffer.clone(), window, cx);
4036 let buffer = buffer.read(cx).as_singleton().unwrap();
4037
4038 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4039 s.select_ranges([
4040 Point::new(0, 2)..Point::new(1, 1),
4041 Point::new(1, 2)..Point::new(1, 2),
4042 Point::new(3, 1)..Point::new(3, 2),
4043 ])
4044 });
4045
4046 editor.join_lines(&JoinLines, window, cx);
4047 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4048
4049 assert_eq!(
4050 editor.selections.ranges::<Point>(cx),
4051 [
4052 Point::new(0, 7)..Point::new(0, 7),
4053 Point::new(1, 3)..Point::new(1, 3)
4054 ]
4055 );
4056 editor
4057 });
4058}
4059
4060#[gpui::test]
4061async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4062 init_test(cx, |_| {});
4063
4064 let mut cx = EditorTestContext::new(cx).await;
4065
4066 let diff_base = r#"
4067 Line 0
4068 Line 1
4069 Line 2
4070 Line 3
4071 "#
4072 .unindent();
4073
4074 cx.set_state(
4075 &r#"
4076 ˇLine 0
4077 Line 1
4078 Line 2
4079 Line 3
4080 "#
4081 .unindent(),
4082 );
4083
4084 cx.set_head_text(&diff_base);
4085 executor.run_until_parked();
4086
4087 // Join lines
4088 cx.update_editor(|editor, window, cx| {
4089 editor.join_lines(&JoinLines, window, cx);
4090 });
4091 executor.run_until_parked();
4092
4093 cx.assert_editor_state(
4094 &r#"
4095 Line 0ˇ Line 1
4096 Line 2
4097 Line 3
4098 "#
4099 .unindent(),
4100 );
4101 // Join again
4102 cx.update_editor(|editor, window, cx| {
4103 editor.join_lines(&JoinLines, window, cx);
4104 });
4105 executor.run_until_parked();
4106
4107 cx.assert_editor_state(
4108 &r#"
4109 Line 0 Line 1ˇ Line 2
4110 Line 3
4111 "#
4112 .unindent(),
4113 );
4114}
4115
4116#[gpui::test]
4117async fn test_custom_newlines_cause_no_false_positive_diffs(
4118 executor: BackgroundExecutor,
4119 cx: &mut TestAppContext,
4120) {
4121 init_test(cx, |_| {});
4122 let mut cx = EditorTestContext::new(cx).await;
4123 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4124 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4125 executor.run_until_parked();
4126
4127 cx.update_editor(|editor, window, cx| {
4128 let snapshot = editor.snapshot(window, cx);
4129 assert_eq!(
4130 snapshot
4131 .buffer_snapshot
4132 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
4133 .collect::<Vec<_>>(),
4134 Vec::new(),
4135 "Should not have any diffs for files with custom newlines"
4136 );
4137 });
4138}
4139
4140#[gpui::test]
4141async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4142 init_test(cx, |_| {});
4143
4144 let mut cx = EditorTestContext::new(cx).await;
4145
4146 // Test sort_lines_case_insensitive()
4147 cx.set_state(indoc! {"
4148 «z
4149 y
4150 x
4151 Z
4152 Y
4153 Xˇ»
4154 "});
4155 cx.update_editor(|e, window, cx| {
4156 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4157 });
4158 cx.assert_editor_state(indoc! {"
4159 «x
4160 X
4161 y
4162 Y
4163 z
4164 Zˇ»
4165 "});
4166
4167 // Test sort_lines_by_length()
4168 //
4169 // Demonstrates:
4170 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4171 // - sort is stable
4172 cx.set_state(indoc! {"
4173 «123
4174 æ
4175 12
4176 ∞
4177 1
4178 æˇ»
4179 "});
4180 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4181 cx.assert_editor_state(indoc! {"
4182 «æ
4183 ∞
4184 1
4185 æ
4186 12
4187 123ˇ»
4188 "});
4189
4190 // Test reverse_lines()
4191 cx.set_state(indoc! {"
4192 «5
4193 4
4194 3
4195 2
4196 1ˇ»
4197 "});
4198 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4199 cx.assert_editor_state(indoc! {"
4200 «1
4201 2
4202 3
4203 4
4204 5ˇ»
4205 "});
4206
4207 // Skip testing shuffle_line()
4208
4209 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4210 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4211
4212 // Don't manipulate when cursor is on single line, but expand the selection
4213 cx.set_state(indoc! {"
4214 ddˇdd
4215 ccc
4216 bb
4217 a
4218 "});
4219 cx.update_editor(|e, window, cx| {
4220 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4221 });
4222 cx.assert_editor_state(indoc! {"
4223 «ddddˇ»
4224 ccc
4225 bb
4226 a
4227 "});
4228
4229 // Basic manipulate case
4230 // Start selection moves to column 0
4231 // End of selection shrinks to fit shorter line
4232 cx.set_state(indoc! {"
4233 dd«d
4234 ccc
4235 bb
4236 aaaaaˇ»
4237 "});
4238 cx.update_editor(|e, window, cx| {
4239 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4240 });
4241 cx.assert_editor_state(indoc! {"
4242 «aaaaa
4243 bb
4244 ccc
4245 dddˇ»
4246 "});
4247
4248 // Manipulate case with newlines
4249 cx.set_state(indoc! {"
4250 dd«d
4251 ccc
4252
4253 bb
4254 aaaaa
4255
4256 ˇ»
4257 "});
4258 cx.update_editor(|e, window, cx| {
4259 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4260 });
4261 cx.assert_editor_state(indoc! {"
4262 «
4263
4264 aaaaa
4265 bb
4266 ccc
4267 dddˇ»
4268
4269 "});
4270
4271 // Adding new line
4272 cx.set_state(indoc! {"
4273 aa«a
4274 bbˇ»b
4275 "});
4276 cx.update_editor(|e, window, cx| {
4277 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4278 });
4279 cx.assert_editor_state(indoc! {"
4280 «aaa
4281 bbb
4282 added_lineˇ»
4283 "});
4284
4285 // Removing line
4286 cx.set_state(indoc! {"
4287 aa«a
4288 bbbˇ»
4289 "});
4290 cx.update_editor(|e, window, cx| {
4291 e.manipulate_immutable_lines(window, cx, |lines| {
4292 lines.pop();
4293 })
4294 });
4295 cx.assert_editor_state(indoc! {"
4296 «aaaˇ»
4297 "});
4298
4299 // Removing all lines
4300 cx.set_state(indoc! {"
4301 aa«a
4302 bbbˇ»
4303 "});
4304 cx.update_editor(|e, window, cx| {
4305 e.manipulate_immutable_lines(window, cx, |lines| {
4306 lines.drain(..);
4307 })
4308 });
4309 cx.assert_editor_state(indoc! {"
4310 ˇ
4311 "});
4312}
4313
4314#[gpui::test]
4315async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4316 init_test(cx, |_| {});
4317
4318 let mut cx = EditorTestContext::new(cx).await;
4319
4320 // Consider continuous selection as single selection
4321 cx.set_state(indoc! {"
4322 Aaa«aa
4323 cˇ»c«c
4324 bb
4325 aaaˇ»aa
4326 "});
4327 cx.update_editor(|e, window, cx| {
4328 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4329 });
4330 cx.assert_editor_state(indoc! {"
4331 «Aaaaa
4332 ccc
4333 bb
4334 aaaaaˇ»
4335 "});
4336
4337 cx.set_state(indoc! {"
4338 Aaa«aa
4339 cˇ»c«c
4340 bb
4341 aaaˇ»aa
4342 "});
4343 cx.update_editor(|e, window, cx| {
4344 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4345 });
4346 cx.assert_editor_state(indoc! {"
4347 «Aaaaa
4348 ccc
4349 bbˇ»
4350 "});
4351
4352 // Consider non continuous selection as distinct dedup operations
4353 cx.set_state(indoc! {"
4354 «aaaaa
4355 bb
4356 aaaaa
4357 aaaaaˇ»
4358
4359 aaa«aaˇ»
4360 "});
4361 cx.update_editor(|e, window, cx| {
4362 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4363 });
4364 cx.assert_editor_state(indoc! {"
4365 «aaaaa
4366 bbˇ»
4367
4368 «aaaaaˇ»
4369 "});
4370}
4371
4372#[gpui::test]
4373async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4374 init_test(cx, |_| {});
4375
4376 let mut cx = EditorTestContext::new(cx).await;
4377
4378 cx.set_state(indoc! {"
4379 «Aaa
4380 aAa
4381 Aaaˇ»
4382 "});
4383 cx.update_editor(|e, window, cx| {
4384 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4385 });
4386 cx.assert_editor_state(indoc! {"
4387 «Aaa
4388 aAaˇ»
4389 "});
4390
4391 cx.set_state(indoc! {"
4392 «Aaa
4393 aAa
4394 aaAˇ»
4395 "});
4396 cx.update_editor(|e, window, cx| {
4397 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4398 });
4399 cx.assert_editor_state(indoc! {"
4400 «Aaaˇ»
4401 "});
4402}
4403
4404#[gpui::test]
4405async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4406 init_test(cx, |_| {});
4407
4408 let mut cx = EditorTestContext::new(cx).await;
4409
4410 // Manipulate with multiple selections on a single line
4411 cx.set_state(indoc! {"
4412 dd«dd
4413 cˇ»c«c
4414 bb
4415 aaaˇ»aa
4416 "});
4417 cx.update_editor(|e, window, cx| {
4418 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4419 });
4420 cx.assert_editor_state(indoc! {"
4421 «aaaaa
4422 bb
4423 ccc
4424 ddddˇ»
4425 "});
4426
4427 // Manipulate with multiple disjoin selections
4428 cx.set_state(indoc! {"
4429 5«
4430 4
4431 3
4432 2
4433 1ˇ»
4434
4435 dd«dd
4436 ccc
4437 bb
4438 aaaˇ»aa
4439 "});
4440 cx.update_editor(|e, window, cx| {
4441 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4442 });
4443 cx.assert_editor_state(indoc! {"
4444 «1
4445 2
4446 3
4447 4
4448 5ˇ»
4449
4450 «aaaaa
4451 bb
4452 ccc
4453 ddddˇ»
4454 "});
4455
4456 // Adding lines on each selection
4457 cx.set_state(indoc! {"
4458 2«
4459 1ˇ»
4460
4461 bb«bb
4462 aaaˇ»aa
4463 "});
4464 cx.update_editor(|e, window, cx| {
4465 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4466 });
4467 cx.assert_editor_state(indoc! {"
4468 «2
4469 1
4470 added lineˇ»
4471
4472 «bbbb
4473 aaaaa
4474 added lineˇ»
4475 "});
4476
4477 // Removing lines on each selection
4478 cx.set_state(indoc! {"
4479 2«
4480 1ˇ»
4481
4482 bb«bb
4483 aaaˇ»aa
4484 "});
4485 cx.update_editor(|e, window, cx| {
4486 e.manipulate_immutable_lines(window, cx, |lines| {
4487 lines.pop();
4488 })
4489 });
4490 cx.assert_editor_state(indoc! {"
4491 «2ˇ»
4492
4493 «bbbbˇ»
4494 "});
4495}
4496
4497#[gpui::test]
4498async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4499 init_test(cx, |settings| {
4500 settings.defaults.tab_size = NonZeroU32::new(3)
4501 });
4502
4503 let mut cx = EditorTestContext::new(cx).await;
4504
4505 // MULTI SELECTION
4506 // Ln.1 "«" tests empty lines
4507 // Ln.9 tests just leading whitespace
4508 cx.set_state(indoc! {"
4509 «
4510 abc // No indentationˇ»
4511 «\tabc // 1 tabˇ»
4512 \t\tabc « ˇ» // 2 tabs
4513 \t ab«c // Tab followed by space
4514 \tabc // Space followed by tab (3 spaces should be the result)
4515 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4516 abˇ»ˇc ˇ ˇ // Already space indented«
4517 \t
4518 \tabc\tdef // Only the leading tab is manipulatedˇ»
4519 "});
4520 cx.update_editor(|e, window, cx| {
4521 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4522 });
4523 cx.assert_editor_state(
4524 indoc! {"
4525 «
4526 abc // No indentation
4527 abc // 1 tab
4528 abc // 2 tabs
4529 abc // Tab followed by space
4530 abc // Space followed by tab (3 spaces should be the result)
4531 abc // Mixed indentation (tab conversion depends on the column)
4532 abc // Already space indented
4533 ·
4534 abc\tdef // Only the leading tab is manipulatedˇ»
4535 "}
4536 .replace("·", "")
4537 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4538 );
4539
4540 // Test on just a few lines, the others should remain unchanged
4541 // Only lines (3, 5, 10, 11) should change
4542 cx.set_state(
4543 indoc! {"
4544 ·
4545 abc // No indentation
4546 \tabcˇ // 1 tab
4547 \t\tabc // 2 tabs
4548 \t abcˇ // Tab followed by space
4549 \tabc // Space followed by tab (3 spaces should be the result)
4550 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4551 abc // Already space indented
4552 «\t
4553 \tabc\tdef // Only the leading tab is manipulatedˇ»
4554 "}
4555 .replace("·", "")
4556 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4557 );
4558 cx.update_editor(|e, window, cx| {
4559 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4560 });
4561 cx.assert_editor_state(
4562 indoc! {"
4563 ·
4564 abc // No indentation
4565 « abc // 1 tabˇ»
4566 \t\tabc // 2 tabs
4567 « abc // Tab followed by spaceˇ»
4568 \tabc // Space followed by tab (3 spaces should be the result)
4569 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4570 abc // Already space indented
4571 « ·
4572 abc\tdef // Only the leading tab is manipulatedˇ»
4573 "}
4574 .replace("·", "")
4575 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4576 );
4577
4578 // SINGLE SELECTION
4579 // Ln.1 "«" tests empty lines
4580 // Ln.9 tests just leading whitespace
4581 cx.set_state(indoc! {"
4582 «
4583 abc // No indentation
4584 \tabc // 1 tab
4585 \t\tabc // 2 tabs
4586 \t abc // Tab followed by space
4587 \tabc // Space followed by tab (3 spaces should be the result)
4588 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4589 abc // Already space indented
4590 \t
4591 \tabc\tdef // Only the leading tab is manipulatedˇ»
4592 "});
4593 cx.update_editor(|e, window, cx| {
4594 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4595 });
4596 cx.assert_editor_state(
4597 indoc! {"
4598 «
4599 abc // No indentation
4600 abc // 1 tab
4601 abc // 2 tabs
4602 abc // Tab followed by space
4603 abc // Space followed by tab (3 spaces should be the result)
4604 abc // Mixed indentation (tab conversion depends on the column)
4605 abc // Already space indented
4606 ·
4607 abc\tdef // Only the leading tab is manipulatedˇ»
4608 "}
4609 .replace("·", "")
4610 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4611 );
4612}
4613
4614#[gpui::test]
4615async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
4616 init_test(cx, |settings| {
4617 settings.defaults.tab_size = NonZeroU32::new(3)
4618 });
4619
4620 let mut cx = EditorTestContext::new(cx).await;
4621
4622 // MULTI SELECTION
4623 // Ln.1 "«" tests empty lines
4624 // Ln.11 tests just leading whitespace
4625 cx.set_state(indoc! {"
4626 «
4627 abˇ»ˇc // No indentation
4628 abc ˇ ˇ // 1 space (< 3 so dont convert)
4629 abc « // 2 spaces (< 3 so dont convert)
4630 abc // 3 spaces (convert)
4631 abc ˇ» // 5 spaces (1 tab + 2 spaces)
4632 «\tˇ»\t«\tˇ»abc // Already tab indented
4633 «\t abc // Tab followed by space
4634 \tabc // Space followed by tab (should be consumed due to tab)
4635 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4636 \tˇ» «\t
4637 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
4638 "});
4639 cx.update_editor(|e, window, cx| {
4640 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4641 });
4642 cx.assert_editor_state(indoc! {"
4643 «
4644 abc // No indentation
4645 abc // 1 space (< 3 so dont convert)
4646 abc // 2 spaces (< 3 so dont convert)
4647 \tabc // 3 spaces (convert)
4648 \t abc // 5 spaces (1 tab + 2 spaces)
4649 \t\t\tabc // Already tab indented
4650 \t abc // Tab followed by space
4651 \tabc // Space followed by tab (should be consumed due to tab)
4652 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4653 \t\t\t
4654 \tabc \t // Only the leading spaces should be convertedˇ»
4655 "});
4656
4657 // Test on just a few lines, the other should remain unchanged
4658 // Only lines (4, 8, 11, 12) should change
4659 cx.set_state(
4660 indoc! {"
4661 ·
4662 abc // No indentation
4663 abc // 1 space (< 3 so dont convert)
4664 abc // 2 spaces (< 3 so dont convert)
4665 « abc // 3 spaces (convert)ˇ»
4666 abc // 5 spaces (1 tab + 2 spaces)
4667 \t\t\tabc // Already tab indented
4668 \t abc // Tab followed by space
4669 \tabc ˇ // Space followed by tab (should be consumed due to tab)
4670 \t\t \tabc // Mixed indentation
4671 \t \t \t \tabc // Mixed indentation
4672 \t \tˇ
4673 « abc \t // Only the leading spaces should be convertedˇ»
4674 "}
4675 .replace("·", "")
4676 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4677 );
4678 cx.update_editor(|e, window, cx| {
4679 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4680 });
4681 cx.assert_editor_state(
4682 indoc! {"
4683 ·
4684 abc // No indentation
4685 abc // 1 space (< 3 so dont convert)
4686 abc // 2 spaces (< 3 so dont convert)
4687 «\tabc // 3 spaces (convert)ˇ»
4688 abc // 5 spaces (1 tab + 2 spaces)
4689 \t\t\tabc // Already tab indented
4690 \t abc // Tab followed by space
4691 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
4692 \t\t \tabc // Mixed indentation
4693 \t \t \t \tabc // Mixed indentation
4694 «\t\t\t
4695 \tabc \t // Only the leading spaces should be convertedˇ»
4696 "}
4697 .replace("·", "")
4698 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4699 );
4700
4701 // SINGLE SELECTION
4702 // Ln.1 "«" tests empty lines
4703 // Ln.11 tests just leading whitespace
4704 cx.set_state(indoc! {"
4705 «
4706 abc // No indentation
4707 abc // 1 space (< 3 so dont convert)
4708 abc // 2 spaces (< 3 so dont convert)
4709 abc // 3 spaces (convert)
4710 abc // 5 spaces (1 tab + 2 spaces)
4711 \t\t\tabc // Already tab indented
4712 \t abc // Tab followed by space
4713 \tabc // Space followed by tab (should be consumed due to tab)
4714 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4715 \t \t
4716 abc \t // Only the leading spaces should be convertedˇ»
4717 "});
4718 cx.update_editor(|e, window, cx| {
4719 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4720 });
4721 cx.assert_editor_state(indoc! {"
4722 «
4723 abc // No indentation
4724 abc // 1 space (< 3 so dont convert)
4725 abc // 2 spaces (< 3 so dont convert)
4726 \tabc // 3 spaces (convert)
4727 \t abc // 5 spaces (1 tab + 2 spaces)
4728 \t\t\tabc // Already tab indented
4729 \t abc // Tab followed by space
4730 \tabc // Space followed by tab (should be consumed due to tab)
4731 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4732 \t\t\t
4733 \tabc \t // Only the leading spaces should be convertedˇ»
4734 "});
4735}
4736
4737#[gpui::test]
4738async fn test_toggle_case(cx: &mut TestAppContext) {
4739 init_test(cx, |_| {});
4740
4741 let mut cx = EditorTestContext::new(cx).await;
4742
4743 // If all lower case -> upper case
4744 cx.set_state(indoc! {"
4745 «hello worldˇ»
4746 "});
4747 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4748 cx.assert_editor_state(indoc! {"
4749 «HELLO WORLDˇ»
4750 "});
4751
4752 // If all upper case -> lower case
4753 cx.set_state(indoc! {"
4754 «HELLO WORLDˇ»
4755 "});
4756 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4757 cx.assert_editor_state(indoc! {"
4758 «hello worldˇ»
4759 "});
4760
4761 // If any upper case characters are identified -> lower case
4762 // This matches JetBrains IDEs
4763 cx.set_state(indoc! {"
4764 «hEllo worldˇ»
4765 "});
4766 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4767 cx.assert_editor_state(indoc! {"
4768 «hello worldˇ»
4769 "});
4770}
4771
4772#[gpui::test]
4773async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
4774 init_test(cx, |_| {});
4775
4776 let mut cx = EditorTestContext::new(cx).await;
4777
4778 cx.set_state(indoc! {"
4779 «implement-windows-supportˇ»
4780 "});
4781 cx.update_editor(|e, window, cx| {
4782 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
4783 });
4784 cx.assert_editor_state(indoc! {"
4785 «Implement windows supportˇ»
4786 "});
4787}
4788
4789#[gpui::test]
4790async fn test_manipulate_text(cx: &mut TestAppContext) {
4791 init_test(cx, |_| {});
4792
4793 let mut cx = EditorTestContext::new(cx).await;
4794
4795 // Test convert_to_upper_case()
4796 cx.set_state(indoc! {"
4797 «hello worldˇ»
4798 "});
4799 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4800 cx.assert_editor_state(indoc! {"
4801 «HELLO WORLDˇ»
4802 "});
4803
4804 // Test convert_to_lower_case()
4805 cx.set_state(indoc! {"
4806 «HELLO WORLDˇ»
4807 "});
4808 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4809 cx.assert_editor_state(indoc! {"
4810 «hello worldˇ»
4811 "});
4812
4813 // Test multiple line, single selection case
4814 cx.set_state(indoc! {"
4815 «The quick brown
4816 fox jumps over
4817 the lazy dogˇ»
4818 "});
4819 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4820 cx.assert_editor_state(indoc! {"
4821 «The Quick Brown
4822 Fox Jumps Over
4823 The Lazy Dogˇ»
4824 "});
4825
4826 // Test multiple line, single selection case
4827 cx.set_state(indoc! {"
4828 «The quick brown
4829 fox jumps over
4830 the lazy dogˇ»
4831 "});
4832 cx.update_editor(|e, window, cx| {
4833 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4834 });
4835 cx.assert_editor_state(indoc! {"
4836 «TheQuickBrown
4837 FoxJumpsOver
4838 TheLazyDogˇ»
4839 "});
4840
4841 // From here on out, test more complex cases of manipulate_text()
4842
4843 // Test no selection case - should affect words cursors are in
4844 // Cursor at beginning, middle, and end of word
4845 cx.set_state(indoc! {"
4846 ˇhello big beauˇtiful worldˇ
4847 "});
4848 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4849 cx.assert_editor_state(indoc! {"
4850 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4851 "});
4852
4853 // Test multiple selections on a single line and across multiple lines
4854 cx.set_state(indoc! {"
4855 «Theˇ» quick «brown
4856 foxˇ» jumps «overˇ»
4857 the «lazyˇ» dog
4858 "});
4859 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4860 cx.assert_editor_state(indoc! {"
4861 «THEˇ» quick «BROWN
4862 FOXˇ» jumps «OVERˇ»
4863 the «LAZYˇ» dog
4864 "});
4865
4866 // Test case where text length grows
4867 cx.set_state(indoc! {"
4868 «tschüߡ»
4869 "});
4870 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4871 cx.assert_editor_state(indoc! {"
4872 «TSCHÜSSˇ»
4873 "});
4874
4875 // Test to make sure we don't crash when text shrinks
4876 cx.set_state(indoc! {"
4877 aaa_bbbˇ
4878 "});
4879 cx.update_editor(|e, window, cx| {
4880 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4881 });
4882 cx.assert_editor_state(indoc! {"
4883 «aaaBbbˇ»
4884 "});
4885
4886 // Test to make sure we all aware of the fact that each word can grow and shrink
4887 // Final selections should be aware of this fact
4888 cx.set_state(indoc! {"
4889 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4890 "});
4891 cx.update_editor(|e, window, cx| {
4892 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4893 });
4894 cx.assert_editor_state(indoc! {"
4895 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4896 "});
4897
4898 cx.set_state(indoc! {"
4899 «hElLo, WoRld!ˇ»
4900 "});
4901 cx.update_editor(|e, window, cx| {
4902 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4903 });
4904 cx.assert_editor_state(indoc! {"
4905 «HeLlO, wOrLD!ˇ»
4906 "});
4907}
4908
4909#[gpui::test]
4910fn test_duplicate_line(cx: &mut TestAppContext) {
4911 init_test(cx, |_| {});
4912
4913 let editor = cx.add_window(|window, cx| {
4914 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4915 build_editor(buffer, window, cx)
4916 });
4917 _ = editor.update(cx, |editor, window, cx| {
4918 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4919 s.select_display_ranges([
4920 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4921 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4922 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4923 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4924 ])
4925 });
4926 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4927 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4928 assert_eq!(
4929 editor.selections.display_ranges(cx),
4930 vec![
4931 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4932 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4933 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4934 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4935 ]
4936 );
4937 });
4938
4939 let editor = cx.add_window(|window, cx| {
4940 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4941 build_editor(buffer, window, cx)
4942 });
4943 _ = editor.update(cx, |editor, window, cx| {
4944 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4945 s.select_display_ranges([
4946 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4947 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4948 ])
4949 });
4950 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4951 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4952 assert_eq!(
4953 editor.selections.display_ranges(cx),
4954 vec![
4955 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4956 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4957 ]
4958 );
4959 });
4960
4961 // With `move_upwards` the selections stay in place, except for
4962 // the lines inserted above them
4963 let editor = cx.add_window(|window, cx| {
4964 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4965 build_editor(buffer, window, cx)
4966 });
4967 _ = editor.update(cx, |editor, window, cx| {
4968 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4969 s.select_display_ranges([
4970 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4971 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4972 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4973 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4974 ])
4975 });
4976 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4977 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4978 assert_eq!(
4979 editor.selections.display_ranges(cx),
4980 vec![
4981 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4982 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4983 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4984 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4985 ]
4986 );
4987 });
4988
4989 let editor = cx.add_window(|window, cx| {
4990 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4991 build_editor(buffer, window, cx)
4992 });
4993 _ = editor.update(cx, |editor, window, cx| {
4994 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4995 s.select_display_ranges([
4996 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4997 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4998 ])
4999 });
5000 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5001 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5002 assert_eq!(
5003 editor.selections.display_ranges(cx),
5004 vec![
5005 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5006 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5007 ]
5008 );
5009 });
5010
5011 let editor = cx.add_window(|window, cx| {
5012 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5013 build_editor(buffer, window, cx)
5014 });
5015 _ = editor.update(cx, |editor, window, cx| {
5016 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5017 s.select_display_ranges([
5018 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5019 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5020 ])
5021 });
5022 editor.duplicate_selection(&DuplicateSelection, window, cx);
5023 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5024 assert_eq!(
5025 editor.selections.display_ranges(cx),
5026 vec![
5027 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5028 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5029 ]
5030 );
5031 });
5032}
5033
5034#[gpui::test]
5035fn test_move_line_up_down(cx: &mut TestAppContext) {
5036 init_test(cx, |_| {});
5037
5038 let editor = cx.add_window(|window, cx| {
5039 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5040 build_editor(buffer, window, cx)
5041 });
5042 _ = editor.update(cx, |editor, window, cx| {
5043 editor.fold_creases(
5044 vec![
5045 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5046 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5047 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5048 ],
5049 true,
5050 window,
5051 cx,
5052 );
5053 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5054 s.select_display_ranges([
5055 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5056 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5057 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5058 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5059 ])
5060 });
5061 assert_eq!(
5062 editor.display_text(cx),
5063 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5064 );
5065
5066 editor.move_line_up(&MoveLineUp, window, cx);
5067 assert_eq!(
5068 editor.display_text(cx),
5069 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5070 );
5071 assert_eq!(
5072 editor.selections.display_ranges(cx),
5073 vec![
5074 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5075 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5076 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5077 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5078 ]
5079 );
5080 });
5081
5082 _ = editor.update(cx, |editor, window, cx| {
5083 editor.move_line_down(&MoveLineDown, window, cx);
5084 assert_eq!(
5085 editor.display_text(cx),
5086 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5087 );
5088 assert_eq!(
5089 editor.selections.display_ranges(cx),
5090 vec![
5091 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5092 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5093 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5094 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5095 ]
5096 );
5097 });
5098
5099 _ = editor.update(cx, |editor, window, cx| {
5100 editor.move_line_down(&MoveLineDown, window, cx);
5101 assert_eq!(
5102 editor.display_text(cx),
5103 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5104 );
5105 assert_eq!(
5106 editor.selections.display_ranges(cx),
5107 vec![
5108 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5109 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5110 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5111 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5112 ]
5113 );
5114 });
5115
5116 _ = editor.update(cx, |editor, window, cx| {
5117 editor.move_line_up(&MoveLineUp, window, cx);
5118 assert_eq!(
5119 editor.display_text(cx),
5120 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5121 );
5122 assert_eq!(
5123 editor.selections.display_ranges(cx),
5124 vec![
5125 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5126 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5127 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5128 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5129 ]
5130 );
5131 });
5132}
5133
5134#[gpui::test]
5135fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5136 init_test(cx, |_| {});
5137 let editor = cx.add_window(|window, cx| {
5138 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5139 build_editor(buffer, window, cx)
5140 });
5141 _ = editor.update(cx, |editor, window, cx| {
5142 editor.fold_creases(
5143 vec![Crease::simple(
5144 Point::new(6, 4)..Point::new(7, 4),
5145 FoldPlaceholder::test(),
5146 )],
5147 true,
5148 window,
5149 cx,
5150 );
5151 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5152 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5153 });
5154 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5155 editor.move_line_up(&MoveLineUp, window, cx);
5156 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5157 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5158 });
5159}
5160
5161#[gpui::test]
5162fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5163 init_test(cx, |_| {});
5164
5165 let editor = cx.add_window(|window, cx| {
5166 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5167 build_editor(buffer, window, cx)
5168 });
5169 _ = editor.update(cx, |editor, window, cx| {
5170 let snapshot = editor.buffer.read(cx).snapshot(cx);
5171 editor.insert_blocks(
5172 [BlockProperties {
5173 style: BlockStyle::Fixed,
5174 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5175 height: Some(1),
5176 render: Arc::new(|_| div().into_any()),
5177 priority: 0,
5178 }],
5179 Some(Autoscroll::fit()),
5180 cx,
5181 );
5182 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5183 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5184 });
5185 editor.move_line_down(&MoveLineDown, window, cx);
5186 });
5187}
5188
5189#[gpui::test]
5190async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5191 init_test(cx, |_| {});
5192
5193 let mut cx = EditorTestContext::new(cx).await;
5194 cx.set_state(
5195 &"
5196 ˇzero
5197 one
5198 two
5199 three
5200 four
5201 five
5202 "
5203 .unindent(),
5204 );
5205
5206 // Create a four-line block that replaces three lines of text.
5207 cx.update_editor(|editor, window, cx| {
5208 let snapshot = editor.snapshot(window, cx);
5209 let snapshot = &snapshot.buffer_snapshot;
5210 let placement = BlockPlacement::Replace(
5211 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5212 );
5213 editor.insert_blocks(
5214 [BlockProperties {
5215 placement,
5216 height: Some(4),
5217 style: BlockStyle::Sticky,
5218 render: Arc::new(|_| gpui::div().into_any_element()),
5219 priority: 0,
5220 }],
5221 None,
5222 cx,
5223 );
5224 });
5225
5226 // Move down so that the cursor touches the block.
5227 cx.update_editor(|editor, window, cx| {
5228 editor.move_down(&Default::default(), window, cx);
5229 });
5230 cx.assert_editor_state(
5231 &"
5232 zero
5233 «one
5234 two
5235 threeˇ»
5236 four
5237 five
5238 "
5239 .unindent(),
5240 );
5241
5242 // Move down past the block.
5243 cx.update_editor(|editor, window, cx| {
5244 editor.move_down(&Default::default(), window, cx);
5245 });
5246 cx.assert_editor_state(
5247 &"
5248 zero
5249 one
5250 two
5251 three
5252 ˇfour
5253 five
5254 "
5255 .unindent(),
5256 );
5257}
5258
5259#[gpui::test]
5260fn test_transpose(cx: &mut TestAppContext) {
5261 init_test(cx, |_| {});
5262
5263 _ = cx.add_window(|window, cx| {
5264 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5265 editor.set_style(EditorStyle::default(), window, cx);
5266 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5267 s.select_ranges([1..1])
5268 });
5269 editor.transpose(&Default::default(), window, cx);
5270 assert_eq!(editor.text(cx), "bac");
5271 assert_eq!(editor.selections.ranges(cx), [2..2]);
5272
5273 editor.transpose(&Default::default(), window, cx);
5274 assert_eq!(editor.text(cx), "bca");
5275 assert_eq!(editor.selections.ranges(cx), [3..3]);
5276
5277 editor.transpose(&Default::default(), window, cx);
5278 assert_eq!(editor.text(cx), "bac");
5279 assert_eq!(editor.selections.ranges(cx), [3..3]);
5280
5281 editor
5282 });
5283
5284 _ = cx.add_window(|window, cx| {
5285 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5286 editor.set_style(EditorStyle::default(), window, cx);
5287 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5288 s.select_ranges([3..3])
5289 });
5290 editor.transpose(&Default::default(), window, cx);
5291 assert_eq!(editor.text(cx), "acb\nde");
5292 assert_eq!(editor.selections.ranges(cx), [3..3]);
5293
5294 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5295 s.select_ranges([4..4])
5296 });
5297 editor.transpose(&Default::default(), window, cx);
5298 assert_eq!(editor.text(cx), "acbd\ne");
5299 assert_eq!(editor.selections.ranges(cx), [5..5]);
5300
5301 editor.transpose(&Default::default(), window, cx);
5302 assert_eq!(editor.text(cx), "acbde\n");
5303 assert_eq!(editor.selections.ranges(cx), [6..6]);
5304
5305 editor.transpose(&Default::default(), window, cx);
5306 assert_eq!(editor.text(cx), "acbd\ne");
5307 assert_eq!(editor.selections.ranges(cx), [6..6]);
5308
5309 editor
5310 });
5311
5312 _ = cx.add_window(|window, cx| {
5313 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5314 editor.set_style(EditorStyle::default(), window, cx);
5315 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5316 s.select_ranges([1..1, 2..2, 4..4])
5317 });
5318 editor.transpose(&Default::default(), window, cx);
5319 assert_eq!(editor.text(cx), "bacd\ne");
5320 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5321
5322 editor.transpose(&Default::default(), window, cx);
5323 assert_eq!(editor.text(cx), "bcade\n");
5324 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5325
5326 editor.transpose(&Default::default(), window, cx);
5327 assert_eq!(editor.text(cx), "bcda\ne");
5328 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5329
5330 editor.transpose(&Default::default(), window, cx);
5331 assert_eq!(editor.text(cx), "bcade\n");
5332 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5333
5334 editor.transpose(&Default::default(), window, cx);
5335 assert_eq!(editor.text(cx), "bcaed\n");
5336 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5337
5338 editor
5339 });
5340
5341 _ = cx.add_window(|window, cx| {
5342 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5343 editor.set_style(EditorStyle::default(), window, cx);
5344 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5345 s.select_ranges([4..4])
5346 });
5347 editor.transpose(&Default::default(), window, cx);
5348 assert_eq!(editor.text(cx), "🏀🍐✋");
5349 assert_eq!(editor.selections.ranges(cx), [8..8]);
5350
5351 editor.transpose(&Default::default(), window, cx);
5352 assert_eq!(editor.text(cx), "🏀✋🍐");
5353 assert_eq!(editor.selections.ranges(cx), [11..11]);
5354
5355 editor.transpose(&Default::default(), window, cx);
5356 assert_eq!(editor.text(cx), "🏀🍐✋");
5357 assert_eq!(editor.selections.ranges(cx), [11..11]);
5358
5359 editor
5360 });
5361}
5362
5363#[gpui::test]
5364async fn test_rewrap(cx: &mut TestAppContext) {
5365 init_test(cx, |settings| {
5366 settings.languages.0.extend([
5367 (
5368 "Markdown".into(),
5369 LanguageSettingsContent {
5370 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5371 preferred_line_length: Some(40),
5372 ..Default::default()
5373 },
5374 ),
5375 (
5376 "Plain Text".into(),
5377 LanguageSettingsContent {
5378 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5379 preferred_line_length: Some(40),
5380 ..Default::default()
5381 },
5382 ),
5383 (
5384 "C++".into(),
5385 LanguageSettingsContent {
5386 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5387 preferred_line_length: Some(40),
5388 ..Default::default()
5389 },
5390 ),
5391 (
5392 "Python".into(),
5393 LanguageSettingsContent {
5394 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5395 preferred_line_length: Some(40),
5396 ..Default::default()
5397 },
5398 ),
5399 (
5400 "Rust".into(),
5401 LanguageSettingsContent {
5402 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5403 preferred_line_length: Some(40),
5404 ..Default::default()
5405 },
5406 ),
5407 ])
5408 });
5409
5410 let mut cx = EditorTestContext::new(cx).await;
5411
5412 let cpp_language = Arc::new(Language::new(
5413 LanguageConfig {
5414 name: "C++".into(),
5415 line_comments: vec!["// ".into()],
5416 ..LanguageConfig::default()
5417 },
5418 None,
5419 ));
5420 let python_language = Arc::new(Language::new(
5421 LanguageConfig {
5422 name: "Python".into(),
5423 line_comments: vec!["# ".into()],
5424 ..LanguageConfig::default()
5425 },
5426 None,
5427 ));
5428 let markdown_language = Arc::new(Language::new(
5429 LanguageConfig {
5430 name: "Markdown".into(),
5431 rewrap_prefixes: vec![
5432 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5433 regex::Regex::new("[-*+]\\s+").unwrap(),
5434 ],
5435 ..LanguageConfig::default()
5436 },
5437 None,
5438 ));
5439 let rust_language = Arc::new(Language::new(
5440 LanguageConfig {
5441 name: "Rust".into(),
5442 line_comments: vec!["// ".into(), "/// ".into()],
5443 ..LanguageConfig::default()
5444 },
5445 Some(tree_sitter_rust::LANGUAGE.into()),
5446 ));
5447
5448 let plaintext_language = Arc::new(Language::new(
5449 LanguageConfig {
5450 name: "Plain Text".into(),
5451 ..LanguageConfig::default()
5452 },
5453 None,
5454 ));
5455
5456 // Test basic rewrapping of a long line with a cursor
5457 assert_rewrap(
5458 indoc! {"
5459 // ˇThis is a long comment that needs to be wrapped.
5460 "},
5461 indoc! {"
5462 // ˇThis is a long comment that needs to
5463 // be wrapped.
5464 "},
5465 cpp_language.clone(),
5466 &mut cx,
5467 );
5468
5469 // Test rewrapping a full selection
5470 assert_rewrap(
5471 indoc! {"
5472 «// This selected long comment needs to be wrapped.ˇ»"
5473 },
5474 indoc! {"
5475 «// This selected long comment needs to
5476 // be wrapped.ˇ»"
5477 },
5478 cpp_language.clone(),
5479 &mut cx,
5480 );
5481
5482 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5483 assert_rewrap(
5484 indoc! {"
5485 // ˇThis is the first line.
5486 // Thisˇ is the second line.
5487 // This is the thirdˇ line, all part of one paragraph.
5488 "},
5489 indoc! {"
5490 // ˇThis is the first line. Thisˇ is the
5491 // second line. This is the thirdˇ line,
5492 // all part of one paragraph.
5493 "},
5494 cpp_language.clone(),
5495 &mut cx,
5496 );
5497
5498 // Test multiple cursors in different paragraphs trigger separate rewraps
5499 assert_rewrap(
5500 indoc! {"
5501 // ˇThis is the first paragraph, first line.
5502 // ˇThis is the first paragraph, second line.
5503
5504 // ˇThis is the second paragraph, first line.
5505 // ˇThis is the second paragraph, second line.
5506 "},
5507 indoc! {"
5508 // ˇThis is the first paragraph, first
5509 // line. ˇThis is the first paragraph,
5510 // second line.
5511
5512 // ˇThis is the second paragraph, first
5513 // line. ˇThis is the second paragraph,
5514 // second line.
5515 "},
5516 cpp_language.clone(),
5517 &mut cx,
5518 );
5519
5520 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
5521 assert_rewrap(
5522 indoc! {"
5523 «// A regular long long comment to be wrapped.
5524 /// A documentation long comment to be wrapped.ˇ»
5525 "},
5526 indoc! {"
5527 «// A regular long long comment to be
5528 // wrapped.
5529 /// A documentation long comment to be
5530 /// wrapped.ˇ»
5531 "},
5532 rust_language.clone(),
5533 &mut cx,
5534 );
5535
5536 // Test that change in indentation level trigger seperate rewraps
5537 assert_rewrap(
5538 indoc! {"
5539 fn foo() {
5540 «// This is a long comment at the base indent.
5541 // This is a long comment at the next indent.ˇ»
5542 }
5543 "},
5544 indoc! {"
5545 fn foo() {
5546 «// This is a long comment at the
5547 // base indent.
5548 // This is a long comment at the
5549 // next indent.ˇ»
5550 }
5551 "},
5552 rust_language.clone(),
5553 &mut cx,
5554 );
5555
5556 // Test that different comment prefix characters (e.g., '#') are handled correctly
5557 assert_rewrap(
5558 indoc! {"
5559 # ˇThis is a long comment using a pound sign.
5560 "},
5561 indoc! {"
5562 # ˇThis is a long comment using a pound
5563 # sign.
5564 "},
5565 python_language,
5566 &mut cx,
5567 );
5568
5569 // Test rewrapping only affects comments, not code even when selected
5570 assert_rewrap(
5571 indoc! {"
5572 «/// This doc comment is long and should be wrapped.
5573 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5574 "},
5575 indoc! {"
5576 «/// This doc comment is long and should
5577 /// be wrapped.
5578 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5579 "},
5580 rust_language.clone(),
5581 &mut cx,
5582 );
5583
5584 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
5585 assert_rewrap(
5586 indoc! {"
5587 # Header
5588
5589 A long long long line of markdown text to wrap.ˇ
5590 "},
5591 indoc! {"
5592 # Header
5593
5594 A long long long line of markdown text
5595 to wrap.ˇ
5596 "},
5597 markdown_language.clone(),
5598 &mut cx,
5599 );
5600
5601 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
5602 assert_rewrap(
5603 indoc! {"
5604 «1. This is a numbered list item that is very long and needs to be wrapped properly.
5605 2. This is a numbered list item that is very long and needs to be wrapped properly.
5606 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
5607 "},
5608 indoc! {"
5609 «1. This is a numbered list item that is
5610 very long and needs to be wrapped
5611 properly.
5612 2. This is a numbered list item that is
5613 very long and needs to be wrapped
5614 properly.
5615 - This is an unordered list item that is
5616 also very long and should not merge
5617 with the numbered item.ˇ»
5618 "},
5619 markdown_language.clone(),
5620 &mut cx,
5621 );
5622
5623 // Test that rewrapping add indents for rewrapping boundary if not exists already.
5624 assert_rewrap(
5625 indoc! {"
5626 «1. This is a numbered list item that is
5627 very long and needs to be wrapped
5628 properly.
5629 2. This is a numbered list item that is
5630 very long and needs to be wrapped
5631 properly.
5632 - This is an unordered list item that is
5633 also very long and should not merge with
5634 the numbered item.ˇ»
5635 "},
5636 indoc! {"
5637 «1. This is a numbered list item that is
5638 very long and needs to be wrapped
5639 properly.
5640 2. This is a numbered list item that is
5641 very long and needs to be wrapped
5642 properly.
5643 - This is an unordered list item that is
5644 also very long and should not merge
5645 with the numbered item.ˇ»
5646 "},
5647 markdown_language.clone(),
5648 &mut cx,
5649 );
5650
5651 // Test that rewrapping maintain indents even when they already exists.
5652 assert_rewrap(
5653 indoc! {"
5654 «1. This is a numbered list
5655 item that is very long and needs to be wrapped properly.
5656 2. This is a numbered list
5657 item that is very long and needs to be wrapped properly.
5658 - This is an unordered list item that is also very long and
5659 should not merge with the numbered item.ˇ»
5660 "},
5661 indoc! {"
5662 «1. This is a numbered list item that is
5663 very long and needs to be wrapped
5664 properly.
5665 2. This is a numbered list item that is
5666 very long and needs to be wrapped
5667 properly.
5668 - This is an unordered list item that is
5669 also very long and should not merge
5670 with the numbered item.ˇ»
5671 "},
5672 markdown_language,
5673 &mut cx,
5674 );
5675
5676 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
5677 assert_rewrap(
5678 indoc! {"
5679 ˇThis is a very long line of plain text that will be wrapped.
5680 "},
5681 indoc! {"
5682 ˇThis is a very long line of plain text
5683 that will be wrapped.
5684 "},
5685 plaintext_language.clone(),
5686 &mut cx,
5687 );
5688
5689 // Test that non-commented code acts as a paragraph boundary within a selection
5690 assert_rewrap(
5691 indoc! {"
5692 «// This is the first long comment block to be wrapped.
5693 fn my_func(a: u32);
5694 // This is the second long comment block to be wrapped.ˇ»
5695 "},
5696 indoc! {"
5697 «// This is the first long comment block
5698 // to be wrapped.
5699 fn my_func(a: u32);
5700 // This is the second long comment block
5701 // to be wrapped.ˇ»
5702 "},
5703 rust_language,
5704 &mut cx,
5705 );
5706
5707 // Test rewrapping multiple selections, including ones with blank lines or tabs
5708 assert_rewrap(
5709 indoc! {"
5710 «ˇThis is a very long line that will be wrapped.
5711
5712 This is another paragraph in the same selection.»
5713
5714 «\tThis is a very long indented line that will be wrapped.ˇ»
5715 "},
5716 indoc! {"
5717 «ˇThis is a very long line that will be
5718 wrapped.
5719
5720 This is another paragraph in the same
5721 selection.»
5722
5723 «\tThis is a very long indented line
5724 \tthat will be wrapped.ˇ»
5725 "},
5726 plaintext_language,
5727 &mut cx,
5728 );
5729
5730 // Test that an empty comment line acts as a paragraph boundary
5731 assert_rewrap(
5732 indoc! {"
5733 // ˇThis is a long comment that will be wrapped.
5734 //
5735 // And this is another long comment that will also be wrapped.ˇ
5736 "},
5737 indoc! {"
5738 // ˇThis is a long comment that will be
5739 // wrapped.
5740 //
5741 // And this is another long comment that
5742 // will also be wrapped.ˇ
5743 "},
5744 cpp_language,
5745 &mut cx,
5746 );
5747
5748 #[track_caller]
5749 fn assert_rewrap(
5750 unwrapped_text: &str,
5751 wrapped_text: &str,
5752 language: Arc<Language>,
5753 cx: &mut EditorTestContext,
5754 ) {
5755 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5756 cx.set_state(unwrapped_text);
5757 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5758 cx.assert_editor_state(wrapped_text);
5759 }
5760}
5761
5762#[gpui::test]
5763async fn test_hard_wrap(cx: &mut TestAppContext) {
5764 init_test(cx, |_| {});
5765 let mut cx = EditorTestContext::new(cx).await;
5766
5767 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5768 cx.update_editor(|editor, _, cx| {
5769 editor.set_hard_wrap(Some(14), cx);
5770 });
5771
5772 cx.set_state(indoc!(
5773 "
5774 one two three ˇ
5775 "
5776 ));
5777 cx.simulate_input("four");
5778 cx.run_until_parked();
5779
5780 cx.assert_editor_state(indoc!(
5781 "
5782 one two three
5783 fourˇ
5784 "
5785 ));
5786
5787 cx.update_editor(|editor, window, cx| {
5788 editor.newline(&Default::default(), window, cx);
5789 });
5790 cx.run_until_parked();
5791 cx.assert_editor_state(indoc!(
5792 "
5793 one two three
5794 four
5795 ˇ
5796 "
5797 ));
5798
5799 cx.simulate_input("five");
5800 cx.run_until_parked();
5801 cx.assert_editor_state(indoc!(
5802 "
5803 one two three
5804 four
5805 fiveˇ
5806 "
5807 ));
5808
5809 cx.update_editor(|editor, window, cx| {
5810 editor.newline(&Default::default(), window, cx);
5811 });
5812 cx.run_until_parked();
5813 cx.simulate_input("# ");
5814 cx.run_until_parked();
5815 cx.assert_editor_state(indoc!(
5816 "
5817 one two three
5818 four
5819 five
5820 # ˇ
5821 "
5822 ));
5823
5824 cx.update_editor(|editor, window, cx| {
5825 editor.newline(&Default::default(), window, cx);
5826 });
5827 cx.run_until_parked();
5828 cx.assert_editor_state(indoc!(
5829 "
5830 one two three
5831 four
5832 five
5833 #\x20
5834 #ˇ
5835 "
5836 ));
5837
5838 cx.simulate_input(" 6");
5839 cx.run_until_parked();
5840 cx.assert_editor_state(indoc!(
5841 "
5842 one two three
5843 four
5844 five
5845 #
5846 # 6ˇ
5847 "
5848 ));
5849}
5850
5851#[gpui::test]
5852async fn test_clipboard(cx: &mut TestAppContext) {
5853 init_test(cx, |_| {});
5854
5855 let mut cx = EditorTestContext::new(cx).await;
5856
5857 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5858 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5859 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5860
5861 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5862 cx.set_state("two ˇfour ˇsix ˇ");
5863 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5864 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5865
5866 // Paste again but with only two cursors. Since the number of cursors doesn't
5867 // match the number of slices in the clipboard, the entire clipboard text
5868 // is pasted at each cursor.
5869 cx.set_state("ˇtwo one✅ four three six five ˇ");
5870 cx.update_editor(|e, window, cx| {
5871 e.handle_input("( ", window, cx);
5872 e.paste(&Paste, window, cx);
5873 e.handle_input(") ", window, cx);
5874 });
5875 cx.assert_editor_state(
5876 &([
5877 "( one✅ ",
5878 "three ",
5879 "five ) ˇtwo one✅ four three six five ( one✅ ",
5880 "three ",
5881 "five ) ˇ",
5882 ]
5883 .join("\n")),
5884 );
5885
5886 // Cut with three selections, one of which is full-line.
5887 cx.set_state(indoc! {"
5888 1«2ˇ»3
5889 4ˇ567
5890 «8ˇ»9"});
5891 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5892 cx.assert_editor_state(indoc! {"
5893 1ˇ3
5894 ˇ9"});
5895
5896 // Paste with three selections, noticing how the copied selection that was full-line
5897 // gets inserted before the second cursor.
5898 cx.set_state(indoc! {"
5899 1ˇ3
5900 9ˇ
5901 «oˇ»ne"});
5902 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5903 cx.assert_editor_state(indoc! {"
5904 12ˇ3
5905 4567
5906 9ˇ
5907 8ˇne"});
5908
5909 // Copy with a single cursor only, which writes the whole line into the clipboard.
5910 cx.set_state(indoc! {"
5911 The quick brown
5912 fox juˇmps over
5913 the lazy dog"});
5914 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5915 assert_eq!(
5916 cx.read_from_clipboard()
5917 .and_then(|item| item.text().as_deref().map(str::to_string)),
5918 Some("fox jumps over\n".to_string())
5919 );
5920
5921 // Paste with three selections, noticing how the copied full-line selection is inserted
5922 // before the empty selections but replaces the selection that is non-empty.
5923 cx.set_state(indoc! {"
5924 Tˇhe quick brown
5925 «foˇ»x jumps over
5926 tˇhe lazy dog"});
5927 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5928 cx.assert_editor_state(indoc! {"
5929 fox jumps over
5930 Tˇhe quick brown
5931 fox jumps over
5932 ˇx jumps over
5933 fox jumps over
5934 tˇhe lazy dog"});
5935}
5936
5937#[gpui::test]
5938async fn test_copy_trim(cx: &mut TestAppContext) {
5939 init_test(cx, |_| {});
5940
5941 let mut cx = EditorTestContext::new(cx).await;
5942 cx.set_state(
5943 r#" «for selection in selections.iter() {
5944 let mut start = selection.start;
5945 let mut end = selection.end;
5946 let is_entire_line = selection.is_empty();
5947 if is_entire_line {
5948 start = Point::new(start.row, 0);ˇ»
5949 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5950 }
5951 "#,
5952 );
5953 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5954 assert_eq!(
5955 cx.read_from_clipboard()
5956 .and_then(|item| item.text().as_deref().map(str::to_string)),
5957 Some(
5958 "for selection in selections.iter() {
5959 let mut start = selection.start;
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 .to_string()
5965 ),
5966 "Regular copying preserves all indentation selected",
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;
5975let mut end = selection.end;
5976let is_entire_line = selection.is_empty();
5977if is_entire_line {
5978 start = Point::new(start.row, 0);"
5979 .to_string()
5980 ),
5981 "Copying with stripping should strip all leading whitespaces"
5982 );
5983
5984 cx.set_state(
5985 r#" « for selection in selections.iter() {
5986 let mut start = selection.start;
5987 let mut end = selection.end;
5988 let is_entire_line = selection.is_empty();
5989 if is_entire_line {
5990 start = Point::new(start.row, 0);ˇ»
5991 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5992 }
5993 "#,
5994 );
5995 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5996 assert_eq!(
5997 cx.read_from_clipboard()
5998 .and_then(|item| item.text().as_deref().map(str::to_string)),
5999 Some(
6000 " for selection in selections.iter() {
6001 let mut start = selection.start;
6002 let mut end = selection.end;
6003 let is_entire_line = selection.is_empty();
6004 if is_entire_line {
6005 start = Point::new(start.row, 0);"
6006 .to_string()
6007 ),
6008 "Regular copying preserves all indentation selected",
6009 );
6010 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6011 assert_eq!(
6012 cx.read_from_clipboard()
6013 .and_then(|item| item.text().as_deref().map(str::to_string)),
6014 Some(
6015 "for selection in selections.iter() {
6016let mut start = selection.start;
6017let mut end = selection.end;
6018let is_entire_line = selection.is_empty();
6019if is_entire_line {
6020 start = Point::new(start.row, 0);"
6021 .to_string()
6022 ),
6023 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
6024 );
6025
6026 cx.set_state(
6027 r#" «ˇ for selection in selections.iter() {
6028 let mut start = selection.start;
6029 let mut end = selection.end;
6030 let is_entire_line = selection.is_empty();
6031 if is_entire_line {
6032 start = Point::new(start.row, 0);»
6033 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6034 }
6035 "#,
6036 );
6037 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6038 assert_eq!(
6039 cx.read_from_clipboard()
6040 .and_then(|item| item.text().as_deref().map(str::to_string)),
6041 Some(
6042 " for selection in selections.iter() {
6043 let mut start = selection.start;
6044 let mut end = selection.end;
6045 let is_entire_line = selection.is_empty();
6046 if is_entire_line {
6047 start = Point::new(start.row, 0);"
6048 .to_string()
6049 ),
6050 "Regular copying for reverse selection works the same",
6051 );
6052 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6053 assert_eq!(
6054 cx.read_from_clipboard()
6055 .and_then(|item| item.text().as_deref().map(str::to_string)),
6056 Some(
6057 "for selection in selections.iter() {
6058let mut start = selection.start;
6059let mut end = selection.end;
6060let is_entire_line = selection.is_empty();
6061if is_entire_line {
6062 start = Point::new(start.row, 0);"
6063 .to_string()
6064 ),
6065 "Copying with stripping for reverse selection works the same"
6066 );
6067
6068 cx.set_state(
6069 r#" for selection «in selections.iter() {
6070 let mut start = selection.start;
6071 let mut end = selection.end;
6072 let is_entire_line = selection.is_empty();
6073 if is_entire_line {
6074 start = Point::new(start.row, 0);ˇ»
6075 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6076 }
6077 "#,
6078 );
6079 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6080 assert_eq!(
6081 cx.read_from_clipboard()
6082 .and_then(|item| item.text().as_deref().map(str::to_string)),
6083 Some(
6084 "in selections.iter() {
6085 let mut start = selection.start;
6086 let mut end = selection.end;
6087 let is_entire_line = selection.is_empty();
6088 if is_entire_line {
6089 start = Point::new(start.row, 0);"
6090 .to_string()
6091 ),
6092 "When selecting past the indent, the copying works as usual",
6093 );
6094 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6095 assert_eq!(
6096 cx.read_from_clipboard()
6097 .and_then(|item| item.text().as_deref().map(str::to_string)),
6098 Some(
6099 "in selections.iter() {
6100 let mut start = selection.start;
6101 let mut end = selection.end;
6102 let is_entire_line = selection.is_empty();
6103 if is_entire_line {
6104 start = Point::new(start.row, 0);"
6105 .to_string()
6106 ),
6107 "When selecting past the indent, nothing is trimmed"
6108 );
6109
6110 cx.set_state(
6111 r#" «for selection in selections.iter() {
6112 let mut start = selection.start;
6113
6114 let mut end = selection.end;
6115 let is_entire_line = selection.is_empty();
6116 if is_entire_line {
6117 start = Point::new(start.row, 0);
6118ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
6119 }
6120 "#,
6121 );
6122 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6123 assert_eq!(
6124 cx.read_from_clipboard()
6125 .and_then(|item| item.text().as_deref().map(str::to_string)),
6126 Some(
6127 "for selection in selections.iter() {
6128let mut start = selection.start;
6129
6130let mut end = selection.end;
6131let is_entire_line = selection.is_empty();
6132if is_entire_line {
6133 start = Point::new(start.row, 0);
6134"
6135 .to_string()
6136 ),
6137 "Copying with stripping should ignore empty lines"
6138 );
6139}
6140
6141#[gpui::test]
6142async fn test_paste_multiline(cx: &mut TestAppContext) {
6143 init_test(cx, |_| {});
6144
6145 let mut cx = EditorTestContext::new(cx).await;
6146 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6147
6148 // Cut an indented block, without the leading whitespace.
6149 cx.set_state(indoc! {"
6150 const a: B = (
6151 c(),
6152 «d(
6153 e,
6154 f
6155 )ˇ»
6156 );
6157 "});
6158 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6159 cx.assert_editor_state(indoc! {"
6160 const a: B = (
6161 c(),
6162 ˇ
6163 );
6164 "});
6165
6166 // Paste it at the same position.
6167 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6168 cx.assert_editor_state(indoc! {"
6169 const a: B = (
6170 c(),
6171 d(
6172 e,
6173 f
6174 )ˇ
6175 );
6176 "});
6177
6178 // Paste it at a line with a lower indent level.
6179 cx.set_state(indoc! {"
6180 ˇ
6181 const a: B = (
6182 c(),
6183 );
6184 "});
6185 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6186 cx.assert_editor_state(indoc! {"
6187 d(
6188 e,
6189 f
6190 )ˇ
6191 const a: B = (
6192 c(),
6193 );
6194 "});
6195
6196 // Cut an indented block, with the leading whitespace.
6197 cx.set_state(indoc! {"
6198 const a: B = (
6199 c(),
6200 « d(
6201 e,
6202 f
6203 )
6204 ˇ»);
6205 "});
6206 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6207 cx.assert_editor_state(indoc! {"
6208 const a: B = (
6209 c(),
6210 ˇ);
6211 "});
6212
6213 // Paste it at the same position.
6214 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6215 cx.assert_editor_state(indoc! {"
6216 const a: B = (
6217 c(),
6218 d(
6219 e,
6220 f
6221 )
6222 ˇ);
6223 "});
6224
6225 // Paste it at a line with a higher indent level.
6226 cx.set_state(indoc! {"
6227 const a: B = (
6228 c(),
6229 d(
6230 e,
6231 fˇ
6232 )
6233 );
6234 "});
6235 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6236 cx.assert_editor_state(indoc! {"
6237 const a: B = (
6238 c(),
6239 d(
6240 e,
6241 f d(
6242 e,
6243 f
6244 )
6245 ˇ
6246 )
6247 );
6248 "});
6249
6250 // Copy an indented block, starting mid-line
6251 cx.set_state(indoc! {"
6252 const a: B = (
6253 c(),
6254 somethin«g(
6255 e,
6256 f
6257 )ˇ»
6258 );
6259 "});
6260 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6261
6262 // Paste it on a line with a lower indent level
6263 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
6264 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6265 cx.assert_editor_state(indoc! {"
6266 const a: B = (
6267 c(),
6268 something(
6269 e,
6270 f
6271 )
6272 );
6273 g(
6274 e,
6275 f
6276 )ˇ"});
6277}
6278
6279#[gpui::test]
6280async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
6281 init_test(cx, |_| {});
6282
6283 cx.write_to_clipboard(ClipboardItem::new_string(
6284 " d(\n e\n );\n".into(),
6285 ));
6286
6287 let mut cx = EditorTestContext::new(cx).await;
6288 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6289
6290 cx.set_state(indoc! {"
6291 fn a() {
6292 b();
6293 if c() {
6294 ˇ
6295 }
6296 }
6297 "});
6298
6299 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6300 cx.assert_editor_state(indoc! {"
6301 fn a() {
6302 b();
6303 if c() {
6304 d(
6305 e
6306 );
6307 ˇ
6308 }
6309 }
6310 "});
6311
6312 cx.set_state(indoc! {"
6313 fn a() {
6314 b();
6315 ˇ
6316 }
6317 "});
6318
6319 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6320 cx.assert_editor_state(indoc! {"
6321 fn a() {
6322 b();
6323 d(
6324 e
6325 );
6326 ˇ
6327 }
6328 "});
6329}
6330
6331#[gpui::test]
6332fn test_select_all(cx: &mut TestAppContext) {
6333 init_test(cx, |_| {});
6334
6335 let editor = cx.add_window(|window, cx| {
6336 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
6337 build_editor(buffer, window, cx)
6338 });
6339 _ = editor.update(cx, |editor, window, cx| {
6340 editor.select_all(&SelectAll, window, cx);
6341 assert_eq!(
6342 editor.selections.display_ranges(cx),
6343 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
6344 );
6345 });
6346}
6347
6348#[gpui::test]
6349fn test_select_line(cx: &mut TestAppContext) {
6350 init_test(cx, |_| {});
6351
6352 let editor = cx.add_window(|window, cx| {
6353 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
6354 build_editor(buffer, window, cx)
6355 });
6356 _ = editor.update(cx, |editor, window, cx| {
6357 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6358 s.select_display_ranges([
6359 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6360 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6361 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6362 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
6363 ])
6364 });
6365 editor.select_line(&SelectLine, window, cx);
6366 assert_eq!(
6367 editor.selections.display_ranges(cx),
6368 vec![
6369 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
6370 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
6371 ]
6372 );
6373 });
6374
6375 _ = editor.update(cx, |editor, window, cx| {
6376 editor.select_line(&SelectLine, window, cx);
6377 assert_eq!(
6378 editor.selections.display_ranges(cx),
6379 vec![
6380 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
6381 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
6382 ]
6383 );
6384 });
6385
6386 _ = editor.update(cx, |editor, window, cx| {
6387 editor.select_line(&SelectLine, window, cx);
6388 assert_eq!(
6389 editor.selections.display_ranges(cx),
6390 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
6391 );
6392 });
6393}
6394
6395#[gpui::test]
6396async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
6397 init_test(cx, |_| {});
6398 let mut cx = EditorTestContext::new(cx).await;
6399
6400 #[track_caller]
6401 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
6402 cx.set_state(initial_state);
6403 cx.update_editor(|e, window, cx| {
6404 e.split_selection_into_lines(&Default::default(), window, cx)
6405 });
6406 cx.assert_editor_state(expected_state);
6407 }
6408
6409 // Selection starts and ends at the middle of lines, left-to-right
6410 test(
6411 &mut cx,
6412 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
6413 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6414 );
6415 // Same thing, right-to-left
6416 test(
6417 &mut cx,
6418 "aa\nb«b\ncc\ndd\neˇ»e\nff",
6419 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6420 );
6421
6422 // Whole buffer, left-to-right, last line *doesn't* end with newline
6423 test(
6424 &mut cx,
6425 "«ˇaa\nbb\ncc\ndd\nee\nff»",
6426 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6427 );
6428 // Same thing, right-to-left
6429 test(
6430 &mut cx,
6431 "«aa\nbb\ncc\ndd\nee\nffˇ»",
6432 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6433 );
6434
6435 // Whole buffer, left-to-right, last line ends with newline
6436 test(
6437 &mut cx,
6438 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
6439 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6440 );
6441 // Same thing, right-to-left
6442 test(
6443 &mut cx,
6444 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
6445 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6446 );
6447
6448 // Starts at the end of a line, ends at the start of another
6449 test(
6450 &mut cx,
6451 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
6452 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
6453 );
6454}
6455
6456#[gpui::test]
6457async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
6458 init_test(cx, |_| {});
6459
6460 let editor = cx.add_window(|window, cx| {
6461 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
6462 build_editor(buffer, window, cx)
6463 });
6464
6465 // setup
6466 _ = editor.update(cx, |editor, window, cx| {
6467 editor.fold_creases(
6468 vec![
6469 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
6470 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
6471 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
6472 ],
6473 true,
6474 window,
6475 cx,
6476 );
6477 assert_eq!(
6478 editor.display_text(cx),
6479 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6480 );
6481 });
6482
6483 _ = editor.update(cx, |editor, window, cx| {
6484 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6485 s.select_display_ranges([
6486 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6487 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6488 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6489 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
6490 ])
6491 });
6492 editor.split_selection_into_lines(&Default::default(), window, cx);
6493 assert_eq!(
6494 editor.display_text(cx),
6495 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6496 );
6497 });
6498 EditorTestContext::for_editor(editor, cx)
6499 .await
6500 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
6501
6502 _ = editor.update(cx, |editor, window, cx| {
6503 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6504 s.select_display_ranges([
6505 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
6506 ])
6507 });
6508 editor.split_selection_into_lines(&Default::default(), window, cx);
6509 assert_eq!(
6510 editor.display_text(cx),
6511 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
6512 );
6513 assert_eq!(
6514 editor.selections.display_ranges(cx),
6515 [
6516 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
6517 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
6518 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
6519 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
6520 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
6521 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
6522 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
6523 ]
6524 );
6525 });
6526 EditorTestContext::for_editor(editor, cx)
6527 .await
6528 .assert_editor_state(
6529 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
6530 );
6531}
6532
6533#[gpui::test]
6534async fn test_add_selection_above_below(cx: &mut TestAppContext) {
6535 init_test(cx, |_| {});
6536
6537 let mut cx = EditorTestContext::new(cx).await;
6538
6539 cx.set_state(indoc!(
6540 r#"abc
6541 defˇghi
6542
6543 jk
6544 nlmo
6545 "#
6546 ));
6547
6548 cx.update_editor(|editor, window, cx| {
6549 editor.add_selection_above(&Default::default(), window, cx);
6550 });
6551
6552 cx.assert_editor_state(indoc!(
6553 r#"abcˇ
6554 defˇghi
6555
6556 jk
6557 nlmo
6558 "#
6559 ));
6560
6561 cx.update_editor(|editor, window, cx| {
6562 editor.add_selection_above(&Default::default(), window, cx);
6563 });
6564
6565 cx.assert_editor_state(indoc!(
6566 r#"abcˇ
6567 defˇghi
6568
6569 jk
6570 nlmo
6571 "#
6572 ));
6573
6574 cx.update_editor(|editor, window, cx| {
6575 editor.add_selection_below(&Default::default(), window, cx);
6576 });
6577
6578 cx.assert_editor_state(indoc!(
6579 r#"abc
6580 defˇghi
6581
6582 jk
6583 nlmo
6584 "#
6585 ));
6586
6587 cx.update_editor(|editor, window, cx| {
6588 editor.undo_selection(&Default::default(), window, cx);
6589 });
6590
6591 cx.assert_editor_state(indoc!(
6592 r#"abcˇ
6593 defˇghi
6594
6595 jk
6596 nlmo
6597 "#
6598 ));
6599
6600 cx.update_editor(|editor, window, cx| {
6601 editor.redo_selection(&Default::default(), window, cx);
6602 });
6603
6604 cx.assert_editor_state(indoc!(
6605 r#"abc
6606 defˇghi
6607
6608 jk
6609 nlmo
6610 "#
6611 ));
6612
6613 cx.update_editor(|editor, window, cx| {
6614 editor.add_selection_below(&Default::default(), window, cx);
6615 });
6616
6617 cx.assert_editor_state(indoc!(
6618 r#"abc
6619 defˇghi
6620 ˇ
6621 jk
6622 nlmo
6623 "#
6624 ));
6625
6626 cx.update_editor(|editor, window, cx| {
6627 editor.add_selection_below(&Default::default(), window, cx);
6628 });
6629
6630 cx.assert_editor_state(indoc!(
6631 r#"abc
6632 defˇghi
6633 ˇ
6634 jkˇ
6635 nlmo
6636 "#
6637 ));
6638
6639 cx.update_editor(|editor, window, cx| {
6640 editor.add_selection_below(&Default::default(), window, cx);
6641 });
6642
6643 cx.assert_editor_state(indoc!(
6644 r#"abc
6645 defˇghi
6646 ˇ
6647 jkˇ
6648 nlmˇo
6649 "#
6650 ));
6651
6652 cx.update_editor(|editor, window, cx| {
6653 editor.add_selection_below(&Default::default(), window, cx);
6654 });
6655
6656 cx.assert_editor_state(indoc!(
6657 r#"abc
6658 defˇghi
6659 ˇ
6660 jkˇ
6661 nlmˇo
6662 ˇ"#
6663 ));
6664
6665 // change selections
6666 cx.set_state(indoc!(
6667 r#"abc
6668 def«ˇg»hi
6669
6670 jk
6671 nlmo
6672 "#
6673 ));
6674
6675 cx.update_editor(|editor, window, cx| {
6676 editor.add_selection_below(&Default::default(), window, cx);
6677 });
6678
6679 cx.assert_editor_state(indoc!(
6680 r#"abc
6681 def«ˇg»hi
6682
6683 jk
6684 nlm«ˇo»
6685 "#
6686 ));
6687
6688 cx.update_editor(|editor, window, cx| {
6689 editor.add_selection_below(&Default::default(), window, cx);
6690 });
6691
6692 cx.assert_editor_state(indoc!(
6693 r#"abc
6694 def«ˇg»hi
6695
6696 jk
6697 nlm«ˇo»
6698 "#
6699 ));
6700
6701 cx.update_editor(|editor, window, cx| {
6702 editor.add_selection_above(&Default::default(), window, cx);
6703 });
6704
6705 cx.assert_editor_state(indoc!(
6706 r#"abc
6707 def«ˇg»hi
6708
6709 jk
6710 nlmo
6711 "#
6712 ));
6713
6714 cx.update_editor(|editor, window, cx| {
6715 editor.add_selection_above(&Default::default(), window, cx);
6716 });
6717
6718 cx.assert_editor_state(indoc!(
6719 r#"abc
6720 def«ˇg»hi
6721
6722 jk
6723 nlmo
6724 "#
6725 ));
6726
6727 // Change selections again
6728 cx.set_state(indoc!(
6729 r#"a«bc
6730 defgˇ»hi
6731
6732 jk
6733 nlmo
6734 "#
6735 ));
6736
6737 cx.update_editor(|editor, window, cx| {
6738 editor.add_selection_below(&Default::default(), window, cx);
6739 });
6740
6741 cx.assert_editor_state(indoc!(
6742 r#"a«bcˇ»
6743 d«efgˇ»hi
6744
6745 j«kˇ»
6746 nlmo
6747 "#
6748 ));
6749
6750 cx.update_editor(|editor, window, cx| {
6751 editor.add_selection_below(&Default::default(), window, cx);
6752 });
6753 cx.assert_editor_state(indoc!(
6754 r#"a«bcˇ»
6755 d«efgˇ»hi
6756
6757 j«kˇ»
6758 n«lmoˇ»
6759 "#
6760 ));
6761 cx.update_editor(|editor, window, cx| {
6762 editor.add_selection_above(&Default::default(), window, cx);
6763 });
6764
6765 cx.assert_editor_state(indoc!(
6766 r#"a«bcˇ»
6767 d«efgˇ»hi
6768
6769 j«kˇ»
6770 nlmo
6771 "#
6772 ));
6773
6774 // Change selections again
6775 cx.set_state(indoc!(
6776 r#"abc
6777 d«ˇefghi
6778
6779 jk
6780 nlm»o
6781 "#
6782 ));
6783
6784 cx.update_editor(|editor, window, cx| {
6785 editor.add_selection_above(&Default::default(), window, cx);
6786 });
6787
6788 cx.assert_editor_state(indoc!(
6789 r#"a«ˇbc»
6790 d«ˇef»ghi
6791
6792 j«ˇk»
6793 n«ˇlm»o
6794 "#
6795 ));
6796
6797 cx.update_editor(|editor, window, cx| {
6798 editor.add_selection_below(&Default::default(), window, cx);
6799 });
6800
6801 cx.assert_editor_state(indoc!(
6802 r#"abc
6803 d«ˇef»ghi
6804
6805 j«ˇk»
6806 n«ˇlm»o
6807 "#
6808 ));
6809}
6810
6811#[gpui::test]
6812async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
6813 init_test(cx, |_| {});
6814 let mut cx = EditorTestContext::new(cx).await;
6815
6816 cx.set_state(indoc!(
6817 r#"line onˇe
6818 liˇne two
6819 line three
6820 line four"#
6821 ));
6822
6823 cx.update_editor(|editor, window, cx| {
6824 editor.add_selection_below(&Default::default(), window, cx);
6825 });
6826
6827 // test multiple cursors expand in the same direction
6828 cx.assert_editor_state(indoc!(
6829 r#"line onˇe
6830 liˇne twˇo
6831 liˇne three
6832 line four"#
6833 ));
6834
6835 cx.update_editor(|editor, window, cx| {
6836 editor.add_selection_below(&Default::default(), window, cx);
6837 });
6838
6839 cx.update_editor(|editor, window, cx| {
6840 editor.add_selection_below(&Default::default(), window, cx);
6841 });
6842
6843 // test multiple cursors expand below overflow
6844 cx.assert_editor_state(indoc!(
6845 r#"line onˇe
6846 liˇne twˇo
6847 liˇne thˇree
6848 liˇne foˇur"#
6849 ));
6850
6851 cx.update_editor(|editor, window, cx| {
6852 editor.add_selection_above(&Default::default(), window, cx);
6853 });
6854
6855 // test multiple cursors retrieves back correctly
6856 cx.assert_editor_state(indoc!(
6857 r#"line onˇe
6858 liˇne twˇo
6859 liˇne thˇree
6860 line four"#
6861 ));
6862
6863 cx.update_editor(|editor, window, cx| {
6864 editor.add_selection_above(&Default::default(), window, cx);
6865 });
6866
6867 cx.update_editor(|editor, window, cx| {
6868 editor.add_selection_above(&Default::default(), window, cx);
6869 });
6870
6871 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
6872 cx.assert_editor_state(indoc!(
6873 r#"liˇne onˇe
6874 liˇne two
6875 line three
6876 line four"#
6877 ));
6878
6879 cx.update_editor(|editor, window, cx| {
6880 editor.undo_selection(&Default::default(), window, cx);
6881 });
6882
6883 // test undo
6884 cx.assert_editor_state(indoc!(
6885 r#"line onˇe
6886 liˇne twˇo
6887 line three
6888 line four"#
6889 ));
6890
6891 cx.update_editor(|editor, window, cx| {
6892 editor.redo_selection(&Default::default(), window, cx);
6893 });
6894
6895 // test redo
6896 cx.assert_editor_state(indoc!(
6897 r#"liˇne onˇe
6898 liˇne two
6899 line three
6900 line four"#
6901 ));
6902
6903 cx.set_state(indoc!(
6904 r#"abcd
6905 ef«ghˇ»
6906 ijkl
6907 «mˇ»nop"#
6908 ));
6909
6910 cx.update_editor(|editor, window, cx| {
6911 editor.add_selection_above(&Default::default(), window, cx);
6912 });
6913
6914 // test multiple selections expand in the same direction
6915 cx.assert_editor_state(indoc!(
6916 r#"ab«cdˇ»
6917 ef«ghˇ»
6918 «iˇ»jkl
6919 «mˇ»nop"#
6920 ));
6921
6922 cx.update_editor(|editor, window, cx| {
6923 editor.add_selection_above(&Default::default(), window, cx);
6924 });
6925
6926 // test multiple selection upward overflow
6927 cx.assert_editor_state(indoc!(
6928 r#"ab«cdˇ»
6929 «eˇ»f«ghˇ»
6930 «iˇ»jkl
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 multiple selection retrieves back correctly
6939 cx.assert_editor_state(indoc!(
6940 r#"abcd
6941 ef«ghˇ»
6942 «iˇ»jkl
6943 «mˇ»nop"#
6944 ));
6945
6946 cx.update_editor(|editor, window, cx| {
6947 editor.add_selection_below(&Default::default(), window, cx);
6948 });
6949
6950 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
6951 cx.assert_editor_state(indoc!(
6952 r#"abcd
6953 ef«ghˇ»
6954 ij«klˇ»
6955 «mˇ»nop"#
6956 ));
6957
6958 cx.update_editor(|editor, window, cx| {
6959 editor.undo_selection(&Default::default(), window, cx);
6960 });
6961
6962 // test undo
6963 cx.assert_editor_state(indoc!(
6964 r#"abcd
6965 ef«ghˇ»
6966 «iˇ»jkl
6967 «mˇ»nop"#
6968 ));
6969
6970 cx.update_editor(|editor, window, cx| {
6971 editor.redo_selection(&Default::default(), window, cx);
6972 });
6973
6974 // test redo
6975 cx.assert_editor_state(indoc!(
6976 r#"abcd
6977 ef«ghˇ»
6978 ij«klˇ»
6979 «mˇ»nop"#
6980 ));
6981}
6982
6983#[gpui::test]
6984async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
6985 init_test(cx, |_| {});
6986 let mut cx = EditorTestContext::new(cx).await;
6987
6988 cx.set_state(indoc!(
6989 r#"line onˇe
6990 liˇne two
6991 line three
6992 line four"#
6993 ));
6994
6995 cx.update_editor(|editor, window, cx| {
6996 editor.add_selection_below(&Default::default(), window, cx);
6997 editor.add_selection_below(&Default::default(), window, cx);
6998 editor.add_selection_below(&Default::default(), window, cx);
6999 });
7000
7001 // initial state with two multi cursor groups
7002 cx.assert_editor_state(indoc!(
7003 r#"line onˇe
7004 liˇne twˇo
7005 liˇne thˇree
7006 liˇne foˇur"#
7007 ));
7008
7009 // add single cursor in middle - simulate opt click
7010 cx.update_editor(|editor, window, cx| {
7011 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
7012 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7013 editor.end_selection(window, cx);
7014 });
7015
7016 cx.assert_editor_state(indoc!(
7017 r#"line onˇe
7018 liˇne twˇo
7019 liˇneˇ thˇree
7020 liˇne foˇur"#
7021 ));
7022
7023 cx.update_editor(|editor, window, cx| {
7024 editor.add_selection_above(&Default::default(), window, cx);
7025 });
7026
7027 // test new added selection expands above and existing selection shrinks
7028 cx.assert_editor_state(indoc!(
7029 r#"line onˇe
7030 liˇneˇ twˇo
7031 liˇneˇ thˇree
7032 line four"#
7033 ));
7034
7035 cx.update_editor(|editor, window, cx| {
7036 editor.add_selection_above(&Default::default(), window, cx);
7037 });
7038
7039 // test new added selection expands above and existing selection shrinks
7040 cx.assert_editor_state(indoc!(
7041 r#"lineˇ onˇe
7042 liˇneˇ twˇo
7043 lineˇ three
7044 line four"#
7045 ));
7046
7047 // intial state with two selection groups
7048 cx.set_state(indoc!(
7049 r#"abcd
7050 ef«ghˇ»
7051 ijkl
7052 «mˇ»nop"#
7053 ));
7054
7055 cx.update_editor(|editor, window, cx| {
7056 editor.add_selection_above(&Default::default(), window, cx);
7057 editor.add_selection_above(&Default::default(), window, cx);
7058 });
7059
7060 cx.assert_editor_state(indoc!(
7061 r#"ab«cdˇ»
7062 «eˇ»f«ghˇ»
7063 «iˇ»jkl
7064 «mˇ»nop"#
7065 ));
7066
7067 // add single selection in middle - simulate opt drag
7068 cx.update_editor(|editor, window, cx| {
7069 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
7070 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7071 editor.update_selection(
7072 DisplayPoint::new(DisplayRow(2), 4),
7073 0,
7074 gpui::Point::<f32>::default(),
7075 window,
7076 cx,
7077 );
7078 editor.end_selection(window, cx);
7079 });
7080
7081 cx.assert_editor_state(indoc!(
7082 r#"ab«cdˇ»
7083 «eˇ»f«ghˇ»
7084 «iˇ»jk«lˇ»
7085 «mˇ»nop"#
7086 ));
7087
7088 cx.update_editor(|editor, window, cx| {
7089 editor.add_selection_below(&Default::default(), window, cx);
7090 });
7091
7092 // test new added selection expands below, others shrinks from above
7093 cx.assert_editor_state(indoc!(
7094 r#"abcd
7095 ef«ghˇ»
7096 «iˇ»jk«lˇ»
7097 «mˇ»no«pˇ»"#
7098 ));
7099}
7100
7101#[gpui::test]
7102async fn test_select_next(cx: &mut TestAppContext) {
7103 init_test(cx, |_| {});
7104
7105 let mut cx = EditorTestContext::new(cx).await;
7106 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7107
7108 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7109 .unwrap();
7110 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7111
7112 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7113 .unwrap();
7114 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7115
7116 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7117 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7118
7119 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7120 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7121
7122 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7123 .unwrap();
7124 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7125
7126 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7127 .unwrap();
7128 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7129
7130 // Test selection direction should be preserved
7131 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7132
7133 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7134 .unwrap();
7135 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
7136}
7137
7138#[gpui::test]
7139async fn test_select_all_matches(cx: &mut TestAppContext) {
7140 init_test(cx, |_| {});
7141
7142 let mut cx = EditorTestContext::new(cx).await;
7143
7144 // Test caret-only selections
7145 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7146 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7147 .unwrap();
7148 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7149
7150 // Test left-to-right selections
7151 cx.set_state("abc\n«abcˇ»\nabc");
7152 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7153 .unwrap();
7154 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
7155
7156 // Test right-to-left selections
7157 cx.set_state("abc\n«ˇabc»\nabc");
7158 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7159 .unwrap();
7160 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
7161
7162 // Test selecting whitespace with caret selection
7163 cx.set_state("abc\nˇ abc\nabc");
7164 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7165 .unwrap();
7166 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7167
7168 // Test selecting whitespace with left-to-right selection
7169 cx.set_state("abc\n«ˇ »abc\nabc");
7170 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7171 .unwrap();
7172 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
7173
7174 // Test no matches with right-to-left selection
7175 cx.set_state("abc\n« ˇ»abc\nabc");
7176 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7177 .unwrap();
7178 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7179
7180 // Test with a single word and clip_at_line_ends=true (#29823)
7181 cx.set_state("aˇbc");
7182 cx.update_editor(|e, window, cx| {
7183 e.set_clip_at_line_ends(true, cx);
7184 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
7185 e.set_clip_at_line_ends(false, cx);
7186 });
7187 cx.assert_editor_state("«abcˇ»");
7188}
7189
7190#[gpui::test]
7191async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
7192 init_test(cx, |_| {});
7193
7194 let mut cx = EditorTestContext::new(cx).await;
7195
7196 let large_body_1 = "\nd".repeat(200);
7197 let large_body_2 = "\ne".repeat(200);
7198
7199 cx.set_state(&format!(
7200 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
7201 ));
7202 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
7203 let scroll_position = editor.scroll_position(cx);
7204 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
7205 scroll_position
7206 });
7207
7208 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7209 .unwrap();
7210 cx.assert_editor_state(&format!(
7211 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
7212 ));
7213 let scroll_position_after_selection =
7214 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
7215 assert_eq!(
7216 initial_scroll_position, scroll_position_after_selection,
7217 "Scroll position should not change after selecting all matches"
7218 );
7219}
7220
7221#[gpui::test]
7222async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
7223 init_test(cx, |_| {});
7224
7225 let mut cx = EditorLspTestContext::new_rust(
7226 lsp::ServerCapabilities {
7227 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7228 ..Default::default()
7229 },
7230 cx,
7231 )
7232 .await;
7233
7234 cx.set_state(indoc! {"
7235 line 1
7236 line 2
7237 linˇe 3
7238 line 4
7239 line 5
7240 "});
7241
7242 // Make an edit
7243 cx.update_editor(|editor, window, cx| {
7244 editor.handle_input("X", window, cx);
7245 });
7246
7247 // Move cursor to a different position
7248 cx.update_editor(|editor, window, cx| {
7249 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7250 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
7251 });
7252 });
7253
7254 cx.assert_editor_state(indoc! {"
7255 line 1
7256 line 2
7257 linXe 3
7258 line 4
7259 liˇne 5
7260 "});
7261
7262 cx.lsp
7263 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
7264 Ok(Some(vec![lsp::TextEdit::new(
7265 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
7266 "PREFIX ".to_string(),
7267 )]))
7268 });
7269
7270 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
7271 .unwrap()
7272 .await
7273 .unwrap();
7274
7275 cx.assert_editor_state(indoc! {"
7276 PREFIX line 1
7277 line 2
7278 linXe 3
7279 line 4
7280 liˇne 5
7281 "});
7282
7283 // Undo formatting
7284 cx.update_editor(|editor, window, cx| {
7285 editor.undo(&Default::default(), window, cx);
7286 });
7287
7288 // Verify cursor moved back to position after edit
7289 cx.assert_editor_state(indoc! {"
7290 line 1
7291 line 2
7292 linXˇe 3
7293 line 4
7294 line 5
7295 "});
7296}
7297
7298#[gpui::test]
7299async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
7300 init_test(cx, |_| {});
7301
7302 let mut cx = EditorTestContext::new(cx).await;
7303
7304 let provider = cx.new(|_| FakeEditPredictionProvider::default());
7305 cx.update_editor(|editor, window, cx| {
7306 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
7307 });
7308
7309 cx.set_state(indoc! {"
7310 line 1
7311 line 2
7312 linˇe 3
7313 line 4
7314 line 5
7315 line 6
7316 line 7
7317 line 8
7318 line 9
7319 line 10
7320 "});
7321
7322 let snapshot = cx.buffer_snapshot();
7323 let edit_position = snapshot.anchor_after(Point::new(2, 4));
7324
7325 cx.update(|_, cx| {
7326 provider.update(cx, |provider, _| {
7327 provider.set_edit_prediction(Some(edit_prediction::EditPrediction {
7328 id: None,
7329 edits: vec![(edit_position..edit_position, "X".into())],
7330 edit_preview: None,
7331 }))
7332 })
7333 });
7334
7335 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
7336 cx.update_editor(|editor, window, cx| {
7337 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
7338 });
7339
7340 cx.assert_editor_state(indoc! {"
7341 line 1
7342 line 2
7343 lineXˇ 3
7344 line 4
7345 line 5
7346 line 6
7347 line 7
7348 line 8
7349 line 9
7350 line 10
7351 "});
7352
7353 cx.update_editor(|editor, window, cx| {
7354 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7355 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
7356 });
7357 });
7358
7359 cx.assert_editor_state(indoc! {"
7360 line 1
7361 line 2
7362 lineX 3
7363 line 4
7364 line 5
7365 line 6
7366 line 7
7367 line 8
7368 line 9
7369 liˇne 10
7370 "});
7371
7372 cx.update_editor(|editor, window, cx| {
7373 editor.undo(&Default::default(), window, cx);
7374 });
7375
7376 cx.assert_editor_state(indoc! {"
7377 line 1
7378 line 2
7379 lineˇ 3
7380 line 4
7381 line 5
7382 line 6
7383 line 7
7384 line 8
7385 line 9
7386 line 10
7387 "});
7388}
7389
7390#[gpui::test]
7391async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
7392 init_test(cx, |_| {});
7393
7394 let mut cx = EditorTestContext::new(cx).await;
7395 cx.set_state(
7396 r#"let foo = 2;
7397lˇet foo = 2;
7398let fooˇ = 2;
7399let foo = 2;
7400let foo = ˇ2;"#,
7401 );
7402
7403 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7404 .unwrap();
7405 cx.assert_editor_state(
7406 r#"let foo = 2;
7407«letˇ» foo = 2;
7408let «fooˇ» = 2;
7409let foo = 2;
7410let foo = «2ˇ»;"#,
7411 );
7412
7413 // noop for multiple selections with different contents
7414 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7415 .unwrap();
7416 cx.assert_editor_state(
7417 r#"let foo = 2;
7418«letˇ» foo = 2;
7419let «fooˇ» = 2;
7420let foo = 2;
7421let foo = «2ˇ»;"#,
7422 );
7423
7424 // Test last selection direction should be preserved
7425 cx.set_state(
7426 r#"let foo = 2;
7427let foo = 2;
7428let «fooˇ» = 2;
7429let «ˇfoo» = 2;
7430let foo = 2;"#,
7431 );
7432
7433 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7434 .unwrap();
7435 cx.assert_editor_state(
7436 r#"let foo = 2;
7437let foo = 2;
7438let «fooˇ» = 2;
7439let «ˇfoo» = 2;
7440let «ˇfoo» = 2;"#,
7441 );
7442}
7443
7444#[gpui::test]
7445async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
7446 init_test(cx, |_| {});
7447
7448 let mut cx =
7449 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
7450
7451 cx.assert_editor_state(indoc! {"
7452 ˇbbb
7453 ccc
7454
7455 bbb
7456 ccc
7457 "});
7458 cx.dispatch_action(SelectPrevious::default());
7459 cx.assert_editor_state(indoc! {"
7460 «bbbˇ»
7461 ccc
7462
7463 bbb
7464 ccc
7465 "});
7466 cx.dispatch_action(SelectPrevious::default());
7467 cx.assert_editor_state(indoc! {"
7468 «bbbˇ»
7469 ccc
7470
7471 «bbbˇ»
7472 ccc
7473 "});
7474}
7475
7476#[gpui::test]
7477async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
7478 init_test(cx, |_| {});
7479
7480 let mut cx = EditorTestContext::new(cx).await;
7481 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7482
7483 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7484 .unwrap();
7485 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7486
7487 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7488 .unwrap();
7489 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7490
7491 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7492 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7493
7494 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7495 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7496
7497 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7498 .unwrap();
7499 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
7500
7501 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7502 .unwrap();
7503 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7504}
7505
7506#[gpui::test]
7507async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
7508 init_test(cx, |_| {});
7509
7510 let mut cx = EditorTestContext::new(cx).await;
7511 cx.set_state("aˇ");
7512
7513 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7514 .unwrap();
7515 cx.assert_editor_state("«aˇ»");
7516 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7517 .unwrap();
7518 cx.assert_editor_state("«aˇ»");
7519}
7520
7521#[gpui::test]
7522async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
7523 init_test(cx, |_| {});
7524
7525 let mut cx = EditorTestContext::new(cx).await;
7526 cx.set_state(
7527 r#"let foo = 2;
7528lˇet foo = 2;
7529let fooˇ = 2;
7530let foo = 2;
7531let foo = ˇ2;"#,
7532 );
7533
7534 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7535 .unwrap();
7536 cx.assert_editor_state(
7537 r#"let foo = 2;
7538«letˇ» foo = 2;
7539let «fooˇ» = 2;
7540let foo = 2;
7541let foo = «2ˇ»;"#,
7542 );
7543
7544 // noop for multiple selections with different contents
7545 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7546 .unwrap();
7547 cx.assert_editor_state(
7548 r#"let foo = 2;
7549«letˇ» foo = 2;
7550let «fooˇ» = 2;
7551let foo = 2;
7552let foo = «2ˇ»;"#,
7553 );
7554}
7555
7556#[gpui::test]
7557async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
7558 init_test(cx, |_| {});
7559
7560 let mut cx = EditorTestContext::new(cx).await;
7561 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7562
7563 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7564 .unwrap();
7565 // selection direction is preserved
7566 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7567
7568 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7569 .unwrap();
7570 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7571
7572 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7573 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7574
7575 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7576 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7577
7578 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7579 .unwrap();
7580 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
7581
7582 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7583 .unwrap();
7584 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
7585}
7586
7587#[gpui::test]
7588async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
7589 init_test(cx, |_| {});
7590
7591 let language = Arc::new(Language::new(
7592 LanguageConfig::default(),
7593 Some(tree_sitter_rust::LANGUAGE.into()),
7594 ));
7595
7596 let text = r#"
7597 use mod1::mod2::{mod3, mod4};
7598
7599 fn fn_1(param1: bool, param2: &str) {
7600 let var1 = "text";
7601 }
7602 "#
7603 .unindent();
7604
7605 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7606 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7607 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7608
7609 editor
7610 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7611 .await;
7612
7613 editor.update_in(cx, |editor, window, cx| {
7614 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7615 s.select_display_ranges([
7616 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
7617 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
7618 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
7619 ]);
7620 });
7621 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7622 });
7623 editor.update(cx, |editor, cx| {
7624 assert_text_with_selections(
7625 editor,
7626 indoc! {r#"
7627 use mod1::mod2::{mod3, «mod4ˇ»};
7628
7629 fn fn_1«ˇ(param1: bool, param2: &str)» {
7630 let var1 = "«ˇtext»";
7631 }
7632 "#},
7633 cx,
7634 );
7635 });
7636
7637 editor.update_in(cx, |editor, window, cx| {
7638 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7639 });
7640 editor.update(cx, |editor, cx| {
7641 assert_text_with_selections(
7642 editor,
7643 indoc! {r#"
7644 use mod1::mod2::«{mod3, mod4}ˇ»;
7645
7646 «ˇfn fn_1(param1: bool, param2: &str) {
7647 let var1 = "text";
7648 }»
7649 "#},
7650 cx,
7651 );
7652 });
7653
7654 editor.update_in(cx, |editor, window, cx| {
7655 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7656 });
7657 assert_eq!(
7658 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7659 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7660 );
7661
7662 // Trying to expand the selected syntax node one more time has no effect.
7663 editor.update_in(cx, |editor, window, cx| {
7664 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7665 });
7666 assert_eq!(
7667 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7668 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7669 );
7670
7671 editor.update_in(cx, |editor, window, cx| {
7672 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7673 });
7674 editor.update(cx, |editor, cx| {
7675 assert_text_with_selections(
7676 editor,
7677 indoc! {r#"
7678 use mod1::mod2::«{mod3, mod4}ˇ»;
7679
7680 «ˇfn fn_1(param1: bool, param2: &str) {
7681 let var1 = "text";
7682 }»
7683 "#},
7684 cx,
7685 );
7686 });
7687
7688 editor.update_in(cx, |editor, window, cx| {
7689 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7690 });
7691 editor.update(cx, |editor, cx| {
7692 assert_text_with_selections(
7693 editor,
7694 indoc! {r#"
7695 use mod1::mod2::{mod3, «mod4ˇ»};
7696
7697 fn fn_1«ˇ(param1: bool, param2: &str)» {
7698 let var1 = "«ˇtext»";
7699 }
7700 "#},
7701 cx,
7702 );
7703 });
7704
7705 editor.update_in(cx, |editor, window, cx| {
7706 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7707 });
7708 editor.update(cx, |editor, cx| {
7709 assert_text_with_selections(
7710 editor,
7711 indoc! {r#"
7712 use mod1::mod2::{mod3, mo«ˇ»d4};
7713
7714 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7715 let var1 = "te«ˇ»xt";
7716 }
7717 "#},
7718 cx,
7719 );
7720 });
7721
7722 // Trying to shrink the selected syntax node one more time has no effect.
7723 editor.update_in(cx, |editor, window, cx| {
7724 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7725 });
7726 editor.update_in(cx, |editor, _, cx| {
7727 assert_text_with_selections(
7728 editor,
7729 indoc! {r#"
7730 use mod1::mod2::{mod3, mo«ˇ»d4};
7731
7732 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7733 let var1 = "te«ˇ»xt";
7734 }
7735 "#},
7736 cx,
7737 );
7738 });
7739
7740 // Ensure that we keep expanding the selection if the larger selection starts or ends within
7741 // a fold.
7742 editor.update_in(cx, |editor, window, cx| {
7743 editor.fold_creases(
7744 vec![
7745 Crease::simple(
7746 Point::new(0, 21)..Point::new(0, 24),
7747 FoldPlaceholder::test(),
7748 ),
7749 Crease::simple(
7750 Point::new(3, 20)..Point::new(3, 22),
7751 FoldPlaceholder::test(),
7752 ),
7753 ],
7754 true,
7755 window,
7756 cx,
7757 );
7758 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7759 });
7760 editor.update(cx, |editor, cx| {
7761 assert_text_with_selections(
7762 editor,
7763 indoc! {r#"
7764 use mod1::mod2::«{mod3, mod4}ˇ»;
7765
7766 fn fn_1«ˇ(param1: bool, param2: &str)» {
7767 let var1 = "«ˇtext»";
7768 }
7769 "#},
7770 cx,
7771 );
7772 });
7773}
7774
7775#[gpui::test]
7776async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
7777 init_test(cx, |_| {});
7778
7779 let language = Arc::new(Language::new(
7780 LanguageConfig::default(),
7781 Some(tree_sitter_rust::LANGUAGE.into()),
7782 ));
7783
7784 let text = "let a = 2;";
7785
7786 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7787 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7788 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7789
7790 editor
7791 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7792 .await;
7793
7794 // Test case 1: Cursor at end of word
7795 editor.update_in(cx, |editor, window, cx| {
7796 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7797 s.select_display_ranges([
7798 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
7799 ]);
7800 });
7801 });
7802 editor.update(cx, |editor, cx| {
7803 assert_text_with_selections(editor, "let aˇ = 2;", cx);
7804 });
7805 editor.update_in(cx, |editor, window, cx| {
7806 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7807 });
7808 editor.update(cx, |editor, cx| {
7809 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
7810 });
7811 editor.update_in(cx, |editor, window, cx| {
7812 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7813 });
7814 editor.update(cx, |editor, cx| {
7815 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7816 });
7817
7818 // Test case 2: Cursor at end of statement
7819 editor.update_in(cx, |editor, window, cx| {
7820 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7821 s.select_display_ranges([
7822 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
7823 ]);
7824 });
7825 });
7826 editor.update(cx, |editor, cx| {
7827 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
7828 });
7829 editor.update_in(cx, |editor, window, cx| {
7830 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7831 });
7832 editor.update(cx, |editor, cx| {
7833 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7834 });
7835}
7836
7837#[gpui::test]
7838async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
7839 init_test(cx, |_| {});
7840
7841 let language = Arc::new(Language::new(
7842 LanguageConfig::default(),
7843 Some(tree_sitter_rust::LANGUAGE.into()),
7844 ));
7845
7846 let text = r#"
7847 use mod1::mod2::{mod3, mod4};
7848
7849 fn fn_1(param1: bool, param2: &str) {
7850 let var1 = "hello world";
7851 }
7852 "#
7853 .unindent();
7854
7855 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7856 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7857 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7858
7859 editor
7860 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7861 .await;
7862
7863 // Test 1: Cursor on a letter of a string word
7864 editor.update_in(cx, |editor, window, cx| {
7865 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7866 s.select_display_ranges([
7867 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
7868 ]);
7869 });
7870 });
7871 editor.update_in(cx, |editor, window, cx| {
7872 assert_text_with_selections(
7873 editor,
7874 indoc! {r#"
7875 use mod1::mod2::{mod3, mod4};
7876
7877 fn fn_1(param1: bool, param2: &str) {
7878 let var1 = "hˇello world";
7879 }
7880 "#},
7881 cx,
7882 );
7883 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7884 assert_text_with_selections(
7885 editor,
7886 indoc! {r#"
7887 use mod1::mod2::{mod3, mod4};
7888
7889 fn fn_1(param1: bool, param2: &str) {
7890 let var1 = "«ˇhello» world";
7891 }
7892 "#},
7893 cx,
7894 );
7895 });
7896
7897 // Test 2: Partial selection within a word
7898 editor.update_in(cx, |editor, window, cx| {
7899 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7900 s.select_display_ranges([
7901 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7902 ]);
7903 });
7904 });
7905 editor.update_in(cx, |editor, window, cx| {
7906 assert_text_with_selections(
7907 editor,
7908 indoc! {r#"
7909 use mod1::mod2::{mod3, mod4};
7910
7911 fn fn_1(param1: bool, param2: &str) {
7912 let var1 = "h«elˇ»lo world";
7913 }
7914 "#},
7915 cx,
7916 );
7917 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7918 assert_text_with_selections(
7919 editor,
7920 indoc! {r#"
7921 use mod1::mod2::{mod3, mod4};
7922
7923 fn fn_1(param1: bool, param2: &str) {
7924 let var1 = "«ˇhello» world";
7925 }
7926 "#},
7927 cx,
7928 );
7929 });
7930
7931 // Test 3: Complete word already selected
7932 editor.update_in(cx, |editor, window, cx| {
7933 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7934 s.select_display_ranges([
7935 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7936 ]);
7937 });
7938 });
7939 editor.update_in(cx, |editor, window, cx| {
7940 assert_text_with_selections(
7941 editor,
7942 indoc! {r#"
7943 use mod1::mod2::{mod3, mod4};
7944
7945 fn fn_1(param1: bool, param2: &str) {
7946 let var1 = "«helloˇ» world";
7947 }
7948 "#},
7949 cx,
7950 );
7951 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7952 assert_text_with_selections(
7953 editor,
7954 indoc! {r#"
7955 use mod1::mod2::{mod3, mod4};
7956
7957 fn fn_1(param1: bool, param2: &str) {
7958 let var1 = "«hello worldˇ»";
7959 }
7960 "#},
7961 cx,
7962 );
7963 });
7964
7965 // Test 4: Selection spanning across words
7966 editor.update_in(cx, |editor, window, cx| {
7967 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7968 s.select_display_ranges([
7969 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7970 ]);
7971 });
7972 });
7973 editor.update_in(cx, |editor, window, cx| {
7974 assert_text_with_selections(
7975 editor,
7976 indoc! {r#"
7977 use mod1::mod2::{mod3, mod4};
7978
7979 fn fn_1(param1: bool, param2: &str) {
7980 let var1 = "hel«lo woˇ»rld";
7981 }
7982 "#},
7983 cx,
7984 );
7985 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7986 assert_text_with_selections(
7987 editor,
7988 indoc! {r#"
7989 use mod1::mod2::{mod3, mod4};
7990
7991 fn fn_1(param1: bool, param2: &str) {
7992 let var1 = "«ˇhello world»";
7993 }
7994 "#},
7995 cx,
7996 );
7997 });
7998
7999 // Test 5: Expansion beyond string
8000 editor.update_in(cx, |editor, window, cx| {
8001 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8002 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8003 assert_text_with_selections(
8004 editor,
8005 indoc! {r#"
8006 use mod1::mod2::{mod3, mod4};
8007
8008 fn fn_1(param1: bool, param2: &str) {
8009 «ˇlet var1 = "hello world";»
8010 }
8011 "#},
8012 cx,
8013 );
8014 });
8015}
8016
8017#[gpui::test]
8018async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
8019 init_test(cx, |_| {});
8020
8021 let mut cx = EditorTestContext::new(cx).await;
8022
8023 let language = Arc::new(Language::new(
8024 LanguageConfig::default(),
8025 Some(tree_sitter_rust::LANGUAGE.into()),
8026 ));
8027
8028 cx.update_buffer(|buffer, cx| {
8029 buffer.set_language(Some(language), cx);
8030 });
8031
8032 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
8033 cx.update_editor(|editor, window, cx| {
8034 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
8035 });
8036
8037 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
8038}
8039
8040#[gpui::test]
8041async fn test_fold_function_bodies(cx: &mut TestAppContext) {
8042 init_test(cx, |_| {});
8043
8044 let base_text = r#"
8045 impl A {
8046 // this is an uncommitted comment
8047
8048 fn b() {
8049 c();
8050 }
8051
8052 // this is another uncommitted comment
8053
8054 fn d() {
8055 // e
8056 // f
8057 }
8058 }
8059
8060 fn g() {
8061 // h
8062 }
8063 "#
8064 .unindent();
8065
8066 let text = r#"
8067 ˇimpl A {
8068
8069 fn b() {
8070 c();
8071 }
8072
8073 fn d() {
8074 // e
8075 // f
8076 }
8077 }
8078
8079 fn g() {
8080 // h
8081 }
8082 "#
8083 .unindent();
8084
8085 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8086 cx.set_state(&text);
8087 cx.set_head_text(&base_text);
8088 cx.update_editor(|editor, window, cx| {
8089 editor.expand_all_diff_hunks(&Default::default(), window, cx);
8090 });
8091
8092 cx.assert_state_with_diff(
8093 "
8094 ˇimpl A {
8095 - // this is an uncommitted comment
8096
8097 fn b() {
8098 c();
8099 }
8100
8101 - // this is another uncommitted comment
8102 -
8103 fn d() {
8104 // e
8105 // f
8106 }
8107 }
8108
8109 fn g() {
8110 // h
8111 }
8112 "
8113 .unindent(),
8114 );
8115
8116 let expected_display_text = "
8117 impl A {
8118 // this is an uncommitted comment
8119
8120 fn b() {
8121 ⋯
8122 }
8123
8124 // this is another uncommitted comment
8125
8126 fn d() {
8127 ⋯
8128 }
8129 }
8130
8131 fn g() {
8132 ⋯
8133 }
8134 "
8135 .unindent();
8136
8137 cx.update_editor(|editor, window, cx| {
8138 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
8139 assert_eq!(editor.display_text(cx), expected_display_text);
8140 });
8141}
8142
8143#[gpui::test]
8144async fn test_autoindent(cx: &mut TestAppContext) {
8145 init_test(cx, |_| {});
8146
8147 let language = Arc::new(
8148 Language::new(
8149 LanguageConfig {
8150 brackets: BracketPairConfig {
8151 pairs: vec![
8152 BracketPair {
8153 start: "{".to_string(),
8154 end: "}".to_string(),
8155 close: false,
8156 surround: false,
8157 newline: true,
8158 },
8159 BracketPair {
8160 start: "(".to_string(),
8161 end: ")".to_string(),
8162 close: false,
8163 surround: false,
8164 newline: true,
8165 },
8166 ],
8167 ..Default::default()
8168 },
8169 ..Default::default()
8170 },
8171 Some(tree_sitter_rust::LANGUAGE.into()),
8172 )
8173 .with_indents_query(
8174 r#"
8175 (_ "(" ")" @end) @indent
8176 (_ "{" "}" @end) @indent
8177 "#,
8178 )
8179 .unwrap(),
8180 );
8181
8182 let text = "fn a() {}";
8183
8184 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8185 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8186 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8187 editor
8188 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8189 .await;
8190
8191 editor.update_in(cx, |editor, window, cx| {
8192 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8193 s.select_ranges([5..5, 8..8, 9..9])
8194 });
8195 editor.newline(&Newline, window, cx);
8196 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
8197 assert_eq!(
8198 editor.selections.ranges(cx),
8199 &[
8200 Point::new(1, 4)..Point::new(1, 4),
8201 Point::new(3, 4)..Point::new(3, 4),
8202 Point::new(5, 0)..Point::new(5, 0)
8203 ]
8204 );
8205 });
8206}
8207
8208#[gpui::test]
8209async fn test_autoindent_disabled(cx: &mut TestAppContext) {
8210 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
8211
8212 let language = Arc::new(
8213 Language::new(
8214 LanguageConfig {
8215 brackets: BracketPairConfig {
8216 pairs: vec![
8217 BracketPair {
8218 start: "{".to_string(),
8219 end: "}".to_string(),
8220 close: false,
8221 surround: false,
8222 newline: true,
8223 },
8224 BracketPair {
8225 start: "(".to_string(),
8226 end: ")".to_string(),
8227 close: false,
8228 surround: false,
8229 newline: true,
8230 },
8231 ],
8232 ..Default::default()
8233 },
8234 ..Default::default()
8235 },
8236 Some(tree_sitter_rust::LANGUAGE.into()),
8237 )
8238 .with_indents_query(
8239 r#"
8240 (_ "(" ")" @end) @indent
8241 (_ "{" "}" @end) @indent
8242 "#,
8243 )
8244 .unwrap(),
8245 );
8246
8247 let text = "fn a() {}";
8248
8249 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8250 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8251 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8252 editor
8253 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8254 .await;
8255
8256 editor.update_in(cx, |editor, window, cx| {
8257 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8258 s.select_ranges([5..5, 8..8, 9..9])
8259 });
8260 editor.newline(&Newline, window, cx);
8261 assert_eq!(
8262 editor.text(cx),
8263 indoc!(
8264 "
8265 fn a(
8266
8267 ) {
8268
8269 }
8270 "
8271 )
8272 );
8273 assert_eq!(
8274 editor.selections.ranges(cx),
8275 &[
8276 Point::new(1, 0)..Point::new(1, 0),
8277 Point::new(3, 0)..Point::new(3, 0),
8278 Point::new(5, 0)..Point::new(5, 0)
8279 ]
8280 );
8281 });
8282}
8283
8284#[gpui::test]
8285async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
8286 init_test(cx, |settings| {
8287 settings.defaults.auto_indent = Some(true);
8288 settings.languages.0.insert(
8289 "python".into(),
8290 LanguageSettingsContent {
8291 auto_indent: Some(false),
8292 ..Default::default()
8293 },
8294 );
8295 });
8296
8297 let mut cx = EditorTestContext::new(cx).await;
8298
8299 let injected_language = Arc::new(
8300 Language::new(
8301 LanguageConfig {
8302 brackets: BracketPairConfig {
8303 pairs: vec![
8304 BracketPair {
8305 start: "{".to_string(),
8306 end: "}".to_string(),
8307 close: false,
8308 surround: false,
8309 newline: true,
8310 },
8311 BracketPair {
8312 start: "(".to_string(),
8313 end: ")".to_string(),
8314 close: true,
8315 surround: false,
8316 newline: true,
8317 },
8318 ],
8319 ..Default::default()
8320 },
8321 name: "python".into(),
8322 ..Default::default()
8323 },
8324 Some(tree_sitter_python::LANGUAGE.into()),
8325 )
8326 .with_indents_query(
8327 r#"
8328 (_ "(" ")" @end) @indent
8329 (_ "{" "}" @end) @indent
8330 "#,
8331 )
8332 .unwrap(),
8333 );
8334
8335 let language = Arc::new(
8336 Language::new(
8337 LanguageConfig {
8338 brackets: BracketPairConfig {
8339 pairs: vec![
8340 BracketPair {
8341 start: "{".to_string(),
8342 end: "}".to_string(),
8343 close: false,
8344 surround: false,
8345 newline: true,
8346 },
8347 BracketPair {
8348 start: "(".to_string(),
8349 end: ")".to_string(),
8350 close: true,
8351 surround: false,
8352 newline: true,
8353 },
8354 ],
8355 ..Default::default()
8356 },
8357 name: LanguageName::new("rust"),
8358 ..Default::default()
8359 },
8360 Some(tree_sitter_rust::LANGUAGE.into()),
8361 )
8362 .with_indents_query(
8363 r#"
8364 (_ "(" ")" @end) @indent
8365 (_ "{" "}" @end) @indent
8366 "#,
8367 )
8368 .unwrap()
8369 .with_injection_query(
8370 r#"
8371 (macro_invocation
8372 macro: (identifier) @_macro_name
8373 (token_tree) @injection.content
8374 (#set! injection.language "python"))
8375 "#,
8376 )
8377 .unwrap(),
8378 );
8379
8380 cx.language_registry().add(injected_language);
8381 cx.language_registry().add(language.clone());
8382
8383 cx.update_buffer(|buffer, cx| {
8384 buffer.set_language(Some(language), cx);
8385 });
8386
8387 cx.set_state(r#"struct A {ˇ}"#);
8388
8389 cx.update_editor(|editor, window, cx| {
8390 editor.newline(&Default::default(), window, cx);
8391 });
8392
8393 cx.assert_editor_state(indoc!(
8394 "struct A {
8395 ˇ
8396 }"
8397 ));
8398
8399 cx.set_state(r#"select_biased!(ˇ)"#);
8400
8401 cx.update_editor(|editor, window, cx| {
8402 editor.newline(&Default::default(), window, cx);
8403 editor.handle_input("def ", window, cx);
8404 editor.handle_input("(", window, cx);
8405 editor.newline(&Default::default(), window, cx);
8406 editor.handle_input("a", window, cx);
8407 });
8408
8409 cx.assert_editor_state(indoc!(
8410 "select_biased!(
8411 def (
8412 aˇ
8413 )
8414 )"
8415 ));
8416}
8417
8418#[gpui::test]
8419async fn test_autoindent_selections(cx: &mut TestAppContext) {
8420 init_test(cx, |_| {});
8421
8422 {
8423 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8424 cx.set_state(indoc! {"
8425 impl A {
8426
8427 fn b() {}
8428
8429 «fn c() {
8430
8431 }ˇ»
8432 }
8433 "});
8434
8435 cx.update_editor(|editor, window, cx| {
8436 editor.autoindent(&Default::default(), window, cx);
8437 });
8438
8439 cx.assert_editor_state(indoc! {"
8440 impl A {
8441
8442 fn b() {}
8443
8444 «fn c() {
8445
8446 }ˇ»
8447 }
8448 "});
8449 }
8450
8451 {
8452 let mut cx = EditorTestContext::new_multibuffer(
8453 cx,
8454 [indoc! { "
8455 impl A {
8456 «
8457 // a
8458 fn b(){}
8459 »
8460 «
8461 }
8462 fn c(){}
8463 »
8464 "}],
8465 );
8466
8467 let buffer = cx.update_editor(|editor, _, cx| {
8468 let buffer = editor.buffer().update(cx, |buffer, _| {
8469 buffer.all_buffers().iter().next().unwrap().clone()
8470 });
8471 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
8472 buffer
8473 });
8474
8475 cx.run_until_parked();
8476 cx.update_editor(|editor, window, cx| {
8477 editor.select_all(&Default::default(), window, cx);
8478 editor.autoindent(&Default::default(), window, cx)
8479 });
8480 cx.run_until_parked();
8481
8482 cx.update(|_, cx| {
8483 assert_eq!(
8484 buffer.read(cx).text(),
8485 indoc! { "
8486 impl A {
8487
8488 // a
8489 fn b(){}
8490
8491
8492 }
8493 fn c(){}
8494
8495 " }
8496 )
8497 });
8498 }
8499}
8500
8501#[gpui::test]
8502async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
8503 init_test(cx, |_| {});
8504
8505 let mut cx = EditorTestContext::new(cx).await;
8506
8507 let language = Arc::new(Language::new(
8508 LanguageConfig {
8509 brackets: BracketPairConfig {
8510 pairs: vec![
8511 BracketPair {
8512 start: "{".to_string(),
8513 end: "}".to_string(),
8514 close: true,
8515 surround: true,
8516 newline: true,
8517 },
8518 BracketPair {
8519 start: "(".to_string(),
8520 end: ")".to_string(),
8521 close: true,
8522 surround: true,
8523 newline: true,
8524 },
8525 BracketPair {
8526 start: "/*".to_string(),
8527 end: " */".to_string(),
8528 close: true,
8529 surround: true,
8530 newline: true,
8531 },
8532 BracketPair {
8533 start: "[".to_string(),
8534 end: "]".to_string(),
8535 close: false,
8536 surround: false,
8537 newline: true,
8538 },
8539 BracketPair {
8540 start: "\"".to_string(),
8541 end: "\"".to_string(),
8542 close: true,
8543 surround: true,
8544 newline: false,
8545 },
8546 BracketPair {
8547 start: "<".to_string(),
8548 end: ">".to_string(),
8549 close: false,
8550 surround: true,
8551 newline: true,
8552 },
8553 ],
8554 ..Default::default()
8555 },
8556 autoclose_before: "})]".to_string(),
8557 ..Default::default()
8558 },
8559 Some(tree_sitter_rust::LANGUAGE.into()),
8560 ));
8561
8562 cx.language_registry().add(language.clone());
8563 cx.update_buffer(|buffer, cx| {
8564 buffer.set_language(Some(language), cx);
8565 });
8566
8567 cx.set_state(
8568 &r#"
8569 🏀ˇ
8570 εˇ
8571 ❤️ˇ
8572 "#
8573 .unindent(),
8574 );
8575
8576 // autoclose multiple nested brackets at multiple cursors
8577 cx.update_editor(|editor, window, cx| {
8578 editor.handle_input("{", window, cx);
8579 editor.handle_input("{", window, cx);
8580 editor.handle_input("{", window, cx);
8581 });
8582 cx.assert_editor_state(
8583 &"
8584 🏀{{{ˇ}}}
8585 ε{{{ˇ}}}
8586 ❤️{{{ˇ}}}
8587 "
8588 .unindent(),
8589 );
8590
8591 // insert a different closing bracket
8592 cx.update_editor(|editor, window, cx| {
8593 editor.handle_input(")", window, cx);
8594 });
8595 cx.assert_editor_state(
8596 &"
8597 🏀{{{)ˇ}}}
8598 ε{{{)ˇ}}}
8599 ❤️{{{)ˇ}}}
8600 "
8601 .unindent(),
8602 );
8603
8604 // skip over the auto-closed brackets when typing a closing bracket
8605 cx.update_editor(|editor, window, cx| {
8606 editor.move_right(&MoveRight, window, cx);
8607 editor.handle_input("}", window, cx);
8608 editor.handle_input("}", window, cx);
8609 editor.handle_input("}", window, cx);
8610 });
8611 cx.assert_editor_state(
8612 &"
8613 🏀{{{)}}}}ˇ
8614 ε{{{)}}}}ˇ
8615 ❤️{{{)}}}}ˇ
8616 "
8617 .unindent(),
8618 );
8619
8620 // autoclose multi-character pairs
8621 cx.set_state(
8622 &"
8623 ˇ
8624 ˇ
8625 "
8626 .unindent(),
8627 );
8628 cx.update_editor(|editor, window, cx| {
8629 editor.handle_input("/", window, cx);
8630 editor.handle_input("*", window, cx);
8631 });
8632 cx.assert_editor_state(
8633 &"
8634 /*ˇ */
8635 /*ˇ */
8636 "
8637 .unindent(),
8638 );
8639
8640 // one cursor autocloses a multi-character pair, one cursor
8641 // does not autoclose.
8642 cx.set_state(
8643 &"
8644 /ˇ
8645 ˇ
8646 "
8647 .unindent(),
8648 );
8649 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
8650 cx.assert_editor_state(
8651 &"
8652 /*ˇ */
8653 *ˇ
8654 "
8655 .unindent(),
8656 );
8657
8658 // Don't autoclose if the next character isn't whitespace and isn't
8659 // listed in the language's "autoclose_before" section.
8660 cx.set_state("ˇa b");
8661 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8662 cx.assert_editor_state("{ˇa b");
8663
8664 // Don't autoclose if `close` is false for the bracket pair
8665 cx.set_state("ˇ");
8666 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
8667 cx.assert_editor_state("[ˇ");
8668
8669 // Surround with brackets if text is selected
8670 cx.set_state("«aˇ» b");
8671 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8672 cx.assert_editor_state("{«aˇ»} b");
8673
8674 // Autoclose when not immediately after a word character
8675 cx.set_state("a ˇ");
8676 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8677 cx.assert_editor_state("a \"ˇ\"");
8678
8679 // Autoclose pair where the start and end characters are the same
8680 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8681 cx.assert_editor_state("a \"\"ˇ");
8682
8683 // Don't autoclose when immediately after a word character
8684 cx.set_state("aˇ");
8685 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8686 cx.assert_editor_state("a\"ˇ");
8687
8688 // Do autoclose when after a non-word character
8689 cx.set_state("{ˇ");
8690 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8691 cx.assert_editor_state("{\"ˇ\"");
8692
8693 // Non identical pairs autoclose regardless of preceding character
8694 cx.set_state("aˇ");
8695 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8696 cx.assert_editor_state("a{ˇ}");
8697
8698 // Don't autoclose pair if autoclose is disabled
8699 cx.set_state("ˇ");
8700 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8701 cx.assert_editor_state("<ˇ");
8702
8703 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
8704 cx.set_state("«aˇ» b");
8705 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8706 cx.assert_editor_state("<«aˇ»> b");
8707}
8708
8709#[gpui::test]
8710async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
8711 init_test(cx, |settings| {
8712 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8713 });
8714
8715 let mut cx = EditorTestContext::new(cx).await;
8716
8717 let language = Arc::new(Language::new(
8718 LanguageConfig {
8719 brackets: BracketPairConfig {
8720 pairs: vec![
8721 BracketPair {
8722 start: "{".to_string(),
8723 end: "}".to_string(),
8724 close: true,
8725 surround: true,
8726 newline: true,
8727 },
8728 BracketPair {
8729 start: "(".to_string(),
8730 end: ")".to_string(),
8731 close: true,
8732 surround: true,
8733 newline: true,
8734 },
8735 BracketPair {
8736 start: "[".to_string(),
8737 end: "]".to_string(),
8738 close: false,
8739 surround: false,
8740 newline: true,
8741 },
8742 ],
8743 ..Default::default()
8744 },
8745 autoclose_before: "})]".to_string(),
8746 ..Default::default()
8747 },
8748 Some(tree_sitter_rust::LANGUAGE.into()),
8749 ));
8750
8751 cx.language_registry().add(language.clone());
8752 cx.update_buffer(|buffer, cx| {
8753 buffer.set_language(Some(language), cx);
8754 });
8755
8756 cx.set_state(
8757 &"
8758 ˇ
8759 ˇ
8760 ˇ
8761 "
8762 .unindent(),
8763 );
8764
8765 // ensure only matching closing brackets are skipped over
8766 cx.update_editor(|editor, window, cx| {
8767 editor.handle_input("}", window, cx);
8768 editor.move_left(&MoveLeft, window, cx);
8769 editor.handle_input(")", window, cx);
8770 editor.move_left(&MoveLeft, window, cx);
8771 });
8772 cx.assert_editor_state(
8773 &"
8774 ˇ)}
8775 ˇ)}
8776 ˇ)}
8777 "
8778 .unindent(),
8779 );
8780
8781 // skip-over closing brackets at multiple cursors
8782 cx.update_editor(|editor, window, cx| {
8783 editor.handle_input(")", window, cx);
8784 editor.handle_input("}", window, cx);
8785 });
8786 cx.assert_editor_state(
8787 &"
8788 )}ˇ
8789 )}ˇ
8790 )}ˇ
8791 "
8792 .unindent(),
8793 );
8794
8795 // ignore non-close brackets
8796 cx.update_editor(|editor, window, cx| {
8797 editor.handle_input("]", window, cx);
8798 editor.move_left(&MoveLeft, window, cx);
8799 editor.handle_input("]", window, cx);
8800 });
8801 cx.assert_editor_state(
8802 &"
8803 )}]ˇ]
8804 )}]ˇ]
8805 )}]ˇ]
8806 "
8807 .unindent(),
8808 );
8809}
8810
8811#[gpui::test]
8812async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8813 init_test(cx, |_| {});
8814
8815 let mut cx = EditorTestContext::new(cx).await;
8816
8817 let html_language = Arc::new(
8818 Language::new(
8819 LanguageConfig {
8820 name: "HTML".into(),
8821 brackets: BracketPairConfig {
8822 pairs: vec![
8823 BracketPair {
8824 start: "<".into(),
8825 end: ">".into(),
8826 close: true,
8827 ..Default::default()
8828 },
8829 BracketPair {
8830 start: "{".into(),
8831 end: "}".into(),
8832 close: true,
8833 ..Default::default()
8834 },
8835 BracketPair {
8836 start: "(".into(),
8837 end: ")".into(),
8838 close: true,
8839 ..Default::default()
8840 },
8841 ],
8842 ..Default::default()
8843 },
8844 autoclose_before: "})]>".into(),
8845 ..Default::default()
8846 },
8847 Some(tree_sitter_html::LANGUAGE.into()),
8848 )
8849 .with_injection_query(
8850 r#"
8851 (script_element
8852 (raw_text) @injection.content
8853 (#set! injection.language "javascript"))
8854 "#,
8855 )
8856 .unwrap(),
8857 );
8858
8859 let javascript_language = Arc::new(Language::new(
8860 LanguageConfig {
8861 name: "JavaScript".into(),
8862 brackets: BracketPairConfig {
8863 pairs: vec![
8864 BracketPair {
8865 start: "/*".into(),
8866 end: " */".into(),
8867 close: true,
8868 ..Default::default()
8869 },
8870 BracketPair {
8871 start: "{".into(),
8872 end: "}".into(),
8873 close: true,
8874 ..Default::default()
8875 },
8876 BracketPair {
8877 start: "(".into(),
8878 end: ")".into(),
8879 close: true,
8880 ..Default::default()
8881 },
8882 ],
8883 ..Default::default()
8884 },
8885 autoclose_before: "})]>".into(),
8886 ..Default::default()
8887 },
8888 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8889 ));
8890
8891 cx.language_registry().add(html_language.clone());
8892 cx.language_registry().add(javascript_language);
8893 cx.executor().run_until_parked();
8894
8895 cx.update_buffer(|buffer, cx| {
8896 buffer.set_language(Some(html_language), cx);
8897 });
8898
8899 cx.set_state(
8900 &r#"
8901 <body>ˇ
8902 <script>
8903 var x = 1;ˇ
8904 </script>
8905 </body>ˇ
8906 "#
8907 .unindent(),
8908 );
8909
8910 // Precondition: different languages are active at different locations.
8911 cx.update_editor(|editor, window, cx| {
8912 let snapshot = editor.snapshot(window, cx);
8913 let cursors = editor.selections.ranges::<usize>(cx);
8914 let languages = cursors
8915 .iter()
8916 .map(|c| snapshot.language_at(c.start).unwrap().name())
8917 .collect::<Vec<_>>();
8918 assert_eq!(
8919 languages,
8920 &["HTML".into(), "JavaScript".into(), "HTML".into()]
8921 );
8922 });
8923
8924 // Angle brackets autoclose in HTML, but not JavaScript.
8925 cx.update_editor(|editor, window, cx| {
8926 editor.handle_input("<", window, cx);
8927 editor.handle_input("a", window, cx);
8928 });
8929 cx.assert_editor_state(
8930 &r#"
8931 <body><aˇ>
8932 <script>
8933 var x = 1;<aˇ
8934 </script>
8935 </body><aˇ>
8936 "#
8937 .unindent(),
8938 );
8939
8940 // Curly braces and parens autoclose in both HTML and JavaScript.
8941 cx.update_editor(|editor, window, cx| {
8942 editor.handle_input(" b=", window, cx);
8943 editor.handle_input("{", window, cx);
8944 editor.handle_input("c", window, cx);
8945 editor.handle_input("(", window, cx);
8946 });
8947 cx.assert_editor_state(
8948 &r#"
8949 <body><a b={c(ˇ)}>
8950 <script>
8951 var x = 1;<a b={c(ˇ)}
8952 </script>
8953 </body><a b={c(ˇ)}>
8954 "#
8955 .unindent(),
8956 );
8957
8958 // Brackets that were already autoclosed are skipped.
8959 cx.update_editor(|editor, window, cx| {
8960 editor.handle_input(")", window, cx);
8961 editor.handle_input("d", window, cx);
8962 editor.handle_input("}", window, cx);
8963 });
8964 cx.assert_editor_state(
8965 &r#"
8966 <body><a b={c()d}ˇ>
8967 <script>
8968 var x = 1;<a b={c()d}ˇ
8969 </script>
8970 </body><a b={c()d}ˇ>
8971 "#
8972 .unindent(),
8973 );
8974 cx.update_editor(|editor, window, cx| {
8975 editor.handle_input(">", window, cx);
8976 });
8977 cx.assert_editor_state(
8978 &r#"
8979 <body><a b={c()d}>ˇ
8980 <script>
8981 var x = 1;<a b={c()d}>ˇ
8982 </script>
8983 </body><a b={c()d}>ˇ
8984 "#
8985 .unindent(),
8986 );
8987
8988 // Reset
8989 cx.set_state(
8990 &r#"
8991 <body>ˇ
8992 <script>
8993 var x = 1;ˇ
8994 </script>
8995 </body>ˇ
8996 "#
8997 .unindent(),
8998 );
8999
9000 cx.update_editor(|editor, window, cx| {
9001 editor.handle_input("<", window, cx);
9002 });
9003 cx.assert_editor_state(
9004 &r#"
9005 <body><ˇ>
9006 <script>
9007 var x = 1;<ˇ
9008 </script>
9009 </body><ˇ>
9010 "#
9011 .unindent(),
9012 );
9013
9014 // When backspacing, the closing angle brackets are removed.
9015 cx.update_editor(|editor, window, cx| {
9016 editor.backspace(&Backspace, window, cx);
9017 });
9018 cx.assert_editor_state(
9019 &r#"
9020 <body>ˇ
9021 <script>
9022 var x = 1;ˇ
9023 </script>
9024 </body>ˇ
9025 "#
9026 .unindent(),
9027 );
9028
9029 // Block comments autoclose in JavaScript, but not HTML.
9030 cx.update_editor(|editor, window, cx| {
9031 editor.handle_input("/", window, cx);
9032 editor.handle_input("*", window, cx);
9033 });
9034 cx.assert_editor_state(
9035 &r#"
9036 <body>/*ˇ
9037 <script>
9038 var x = 1;/*ˇ */
9039 </script>
9040 </body>/*ˇ
9041 "#
9042 .unindent(),
9043 );
9044}
9045
9046#[gpui::test]
9047async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
9048 init_test(cx, |_| {});
9049
9050 let mut cx = EditorTestContext::new(cx).await;
9051
9052 let rust_language = Arc::new(
9053 Language::new(
9054 LanguageConfig {
9055 name: "Rust".into(),
9056 brackets: serde_json::from_value(json!([
9057 { "start": "{", "end": "}", "close": true, "newline": true },
9058 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
9059 ]))
9060 .unwrap(),
9061 autoclose_before: "})]>".into(),
9062 ..Default::default()
9063 },
9064 Some(tree_sitter_rust::LANGUAGE.into()),
9065 )
9066 .with_override_query("(string_literal) @string")
9067 .unwrap(),
9068 );
9069
9070 cx.language_registry().add(rust_language.clone());
9071 cx.update_buffer(|buffer, cx| {
9072 buffer.set_language(Some(rust_language), cx);
9073 });
9074
9075 cx.set_state(
9076 &r#"
9077 let x = ˇ
9078 "#
9079 .unindent(),
9080 );
9081
9082 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
9083 cx.update_editor(|editor, window, cx| {
9084 editor.handle_input("\"", window, cx);
9085 });
9086 cx.assert_editor_state(
9087 &r#"
9088 let x = "ˇ"
9089 "#
9090 .unindent(),
9091 );
9092
9093 // Inserting another quotation mark. The cursor moves across the existing
9094 // automatically-inserted quotation mark.
9095 cx.update_editor(|editor, window, cx| {
9096 editor.handle_input("\"", window, cx);
9097 });
9098 cx.assert_editor_state(
9099 &r#"
9100 let x = ""ˇ
9101 "#
9102 .unindent(),
9103 );
9104
9105 // Reset
9106 cx.set_state(
9107 &r#"
9108 let x = ˇ
9109 "#
9110 .unindent(),
9111 );
9112
9113 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
9114 cx.update_editor(|editor, window, cx| {
9115 editor.handle_input("\"", window, cx);
9116 editor.handle_input(" ", window, cx);
9117 editor.move_left(&Default::default(), window, cx);
9118 editor.handle_input("\\", window, cx);
9119 editor.handle_input("\"", window, cx);
9120 });
9121 cx.assert_editor_state(
9122 &r#"
9123 let x = "\"ˇ "
9124 "#
9125 .unindent(),
9126 );
9127
9128 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
9129 // mark. Nothing is inserted.
9130 cx.update_editor(|editor, window, cx| {
9131 editor.move_right(&Default::default(), window, cx);
9132 editor.handle_input("\"", window, cx);
9133 });
9134 cx.assert_editor_state(
9135 &r#"
9136 let x = "\" "ˇ
9137 "#
9138 .unindent(),
9139 );
9140}
9141
9142#[gpui::test]
9143async fn test_surround_with_pair(cx: &mut TestAppContext) {
9144 init_test(cx, |_| {});
9145
9146 let language = Arc::new(Language::new(
9147 LanguageConfig {
9148 brackets: BracketPairConfig {
9149 pairs: vec![
9150 BracketPair {
9151 start: "{".to_string(),
9152 end: "}".to_string(),
9153 close: true,
9154 surround: true,
9155 newline: true,
9156 },
9157 BracketPair {
9158 start: "/* ".to_string(),
9159 end: "*/".to_string(),
9160 close: true,
9161 surround: true,
9162 ..Default::default()
9163 },
9164 ],
9165 ..Default::default()
9166 },
9167 ..Default::default()
9168 },
9169 Some(tree_sitter_rust::LANGUAGE.into()),
9170 ));
9171
9172 let text = r#"
9173 a
9174 b
9175 c
9176 "#
9177 .unindent();
9178
9179 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9180 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9181 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9182 editor
9183 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9184 .await;
9185
9186 editor.update_in(cx, |editor, window, cx| {
9187 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9188 s.select_display_ranges([
9189 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
9190 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
9191 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
9192 ])
9193 });
9194
9195 editor.handle_input("{", window, cx);
9196 editor.handle_input("{", window, cx);
9197 editor.handle_input("{", window, cx);
9198 assert_eq!(
9199 editor.text(cx),
9200 "
9201 {{{a}}}
9202 {{{b}}}
9203 {{{c}}}
9204 "
9205 .unindent()
9206 );
9207 assert_eq!(
9208 editor.selections.display_ranges(cx),
9209 [
9210 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
9211 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
9212 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
9213 ]
9214 );
9215
9216 editor.undo(&Undo, window, cx);
9217 editor.undo(&Undo, window, cx);
9218 editor.undo(&Undo, window, cx);
9219 assert_eq!(
9220 editor.text(cx),
9221 "
9222 a
9223 b
9224 c
9225 "
9226 .unindent()
9227 );
9228 assert_eq!(
9229 editor.selections.display_ranges(cx),
9230 [
9231 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
9232 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
9233 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
9234 ]
9235 );
9236
9237 // Ensure inserting the first character of a multi-byte bracket pair
9238 // doesn't surround the selections with the bracket.
9239 editor.handle_input("/", window, cx);
9240 assert_eq!(
9241 editor.text(cx),
9242 "
9243 /
9244 /
9245 /
9246 "
9247 .unindent()
9248 );
9249 assert_eq!(
9250 editor.selections.display_ranges(cx),
9251 [
9252 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
9253 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
9254 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
9255 ]
9256 );
9257
9258 editor.undo(&Undo, window, cx);
9259 assert_eq!(
9260 editor.text(cx),
9261 "
9262 a
9263 b
9264 c
9265 "
9266 .unindent()
9267 );
9268 assert_eq!(
9269 editor.selections.display_ranges(cx),
9270 [
9271 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
9272 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
9273 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
9274 ]
9275 );
9276
9277 // Ensure inserting the last character of a multi-byte bracket pair
9278 // doesn't surround the selections with the bracket.
9279 editor.handle_input("*", window, cx);
9280 assert_eq!(
9281 editor.text(cx),
9282 "
9283 *
9284 *
9285 *
9286 "
9287 .unindent()
9288 );
9289 assert_eq!(
9290 editor.selections.display_ranges(cx),
9291 [
9292 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
9293 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
9294 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
9295 ]
9296 );
9297 });
9298}
9299
9300#[gpui::test]
9301async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
9302 init_test(cx, |_| {});
9303
9304 let language = Arc::new(Language::new(
9305 LanguageConfig {
9306 brackets: BracketPairConfig {
9307 pairs: vec![BracketPair {
9308 start: "{".to_string(),
9309 end: "}".to_string(),
9310 close: true,
9311 surround: true,
9312 newline: true,
9313 }],
9314 ..Default::default()
9315 },
9316 autoclose_before: "}".to_string(),
9317 ..Default::default()
9318 },
9319 Some(tree_sitter_rust::LANGUAGE.into()),
9320 ));
9321
9322 let text = r#"
9323 a
9324 b
9325 c
9326 "#
9327 .unindent();
9328
9329 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9330 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9331 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9332 editor
9333 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9334 .await;
9335
9336 editor.update_in(cx, |editor, window, cx| {
9337 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9338 s.select_ranges([
9339 Point::new(0, 1)..Point::new(0, 1),
9340 Point::new(1, 1)..Point::new(1, 1),
9341 Point::new(2, 1)..Point::new(2, 1),
9342 ])
9343 });
9344
9345 editor.handle_input("{", window, cx);
9346 editor.handle_input("{", window, cx);
9347 editor.handle_input("_", window, cx);
9348 assert_eq!(
9349 editor.text(cx),
9350 "
9351 a{{_}}
9352 b{{_}}
9353 c{{_}}
9354 "
9355 .unindent()
9356 );
9357 assert_eq!(
9358 editor.selections.ranges::<Point>(cx),
9359 [
9360 Point::new(0, 4)..Point::new(0, 4),
9361 Point::new(1, 4)..Point::new(1, 4),
9362 Point::new(2, 4)..Point::new(2, 4)
9363 ]
9364 );
9365
9366 editor.backspace(&Default::default(), window, cx);
9367 editor.backspace(&Default::default(), window, cx);
9368 assert_eq!(
9369 editor.text(cx),
9370 "
9371 a{}
9372 b{}
9373 c{}
9374 "
9375 .unindent()
9376 );
9377 assert_eq!(
9378 editor.selections.ranges::<Point>(cx),
9379 [
9380 Point::new(0, 2)..Point::new(0, 2),
9381 Point::new(1, 2)..Point::new(1, 2),
9382 Point::new(2, 2)..Point::new(2, 2)
9383 ]
9384 );
9385
9386 editor.delete_to_previous_word_start(&Default::default(), window, cx);
9387 assert_eq!(
9388 editor.text(cx),
9389 "
9390 a
9391 b
9392 c
9393 "
9394 .unindent()
9395 );
9396 assert_eq!(
9397 editor.selections.ranges::<Point>(cx),
9398 [
9399 Point::new(0, 1)..Point::new(0, 1),
9400 Point::new(1, 1)..Point::new(1, 1),
9401 Point::new(2, 1)..Point::new(2, 1)
9402 ]
9403 );
9404 });
9405}
9406
9407#[gpui::test]
9408async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
9409 init_test(cx, |settings| {
9410 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9411 });
9412
9413 let mut cx = EditorTestContext::new(cx).await;
9414
9415 let language = Arc::new(Language::new(
9416 LanguageConfig {
9417 brackets: BracketPairConfig {
9418 pairs: vec![
9419 BracketPair {
9420 start: "{".to_string(),
9421 end: "}".to_string(),
9422 close: true,
9423 surround: true,
9424 newline: true,
9425 },
9426 BracketPair {
9427 start: "(".to_string(),
9428 end: ")".to_string(),
9429 close: true,
9430 surround: true,
9431 newline: true,
9432 },
9433 BracketPair {
9434 start: "[".to_string(),
9435 end: "]".to_string(),
9436 close: false,
9437 surround: true,
9438 newline: true,
9439 },
9440 ],
9441 ..Default::default()
9442 },
9443 autoclose_before: "})]".to_string(),
9444 ..Default::default()
9445 },
9446 Some(tree_sitter_rust::LANGUAGE.into()),
9447 ));
9448
9449 cx.language_registry().add(language.clone());
9450 cx.update_buffer(|buffer, cx| {
9451 buffer.set_language(Some(language), cx);
9452 });
9453
9454 cx.set_state(
9455 &"
9456 {(ˇ)}
9457 [[ˇ]]
9458 {(ˇ)}
9459 "
9460 .unindent(),
9461 );
9462
9463 cx.update_editor(|editor, window, cx| {
9464 editor.backspace(&Default::default(), window, cx);
9465 editor.backspace(&Default::default(), window, cx);
9466 });
9467
9468 cx.assert_editor_state(
9469 &"
9470 ˇ
9471 ˇ]]
9472 ˇ
9473 "
9474 .unindent(),
9475 );
9476
9477 cx.update_editor(|editor, window, cx| {
9478 editor.handle_input("{", window, cx);
9479 editor.handle_input("{", window, cx);
9480 editor.move_right(&MoveRight, window, cx);
9481 editor.move_right(&MoveRight, window, cx);
9482 editor.move_left(&MoveLeft, window, cx);
9483 editor.move_left(&MoveLeft, window, cx);
9484 editor.backspace(&Default::default(), window, cx);
9485 });
9486
9487 cx.assert_editor_state(
9488 &"
9489 {ˇ}
9490 {ˇ}]]
9491 {ˇ}
9492 "
9493 .unindent(),
9494 );
9495
9496 cx.update_editor(|editor, window, cx| {
9497 editor.backspace(&Default::default(), window, cx);
9498 });
9499
9500 cx.assert_editor_state(
9501 &"
9502 ˇ
9503 ˇ]]
9504 ˇ
9505 "
9506 .unindent(),
9507 );
9508}
9509
9510#[gpui::test]
9511async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
9512 init_test(cx, |_| {});
9513
9514 let language = Arc::new(Language::new(
9515 LanguageConfig::default(),
9516 Some(tree_sitter_rust::LANGUAGE.into()),
9517 ));
9518
9519 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
9520 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9521 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9522 editor
9523 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9524 .await;
9525
9526 editor.update_in(cx, |editor, window, cx| {
9527 editor.set_auto_replace_emoji_shortcode(true);
9528
9529 editor.handle_input("Hello ", window, cx);
9530 editor.handle_input(":wave", window, cx);
9531 assert_eq!(editor.text(cx), "Hello :wave".unindent());
9532
9533 editor.handle_input(":", window, cx);
9534 assert_eq!(editor.text(cx), "Hello 👋".unindent());
9535
9536 editor.handle_input(" :smile", window, cx);
9537 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
9538
9539 editor.handle_input(":", window, cx);
9540 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
9541
9542 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
9543 editor.handle_input(":wave", window, cx);
9544 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
9545
9546 editor.handle_input(":", window, cx);
9547 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
9548
9549 editor.handle_input(":1", window, cx);
9550 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
9551
9552 editor.handle_input(":", window, cx);
9553 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
9554
9555 // Ensure shortcode does not get replaced when it is part of a word
9556 editor.handle_input(" Test:wave", window, cx);
9557 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
9558
9559 editor.handle_input(":", window, cx);
9560 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
9561
9562 editor.set_auto_replace_emoji_shortcode(false);
9563
9564 // Ensure shortcode does not get replaced when auto replace is off
9565 editor.handle_input(" :wave", window, cx);
9566 assert_eq!(
9567 editor.text(cx),
9568 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
9569 );
9570
9571 editor.handle_input(":", window, cx);
9572 assert_eq!(
9573 editor.text(cx),
9574 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
9575 );
9576 });
9577}
9578
9579#[gpui::test]
9580async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
9581 init_test(cx, |_| {});
9582
9583 let (text, insertion_ranges) = marked_text_ranges(
9584 indoc! {"
9585 ˇ
9586 "},
9587 false,
9588 );
9589
9590 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
9591 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9592
9593 _ = editor.update_in(cx, |editor, window, cx| {
9594 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
9595
9596 editor
9597 .insert_snippet(&insertion_ranges, snippet, window, cx)
9598 .unwrap();
9599
9600 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
9601 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
9602 assert_eq!(editor.text(cx), expected_text);
9603 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
9604 }
9605
9606 assert(
9607 editor,
9608 cx,
9609 indoc! {"
9610 type «» =•
9611 "},
9612 );
9613
9614 assert!(editor.context_menu_visible(), "There should be a matches");
9615 });
9616}
9617
9618#[gpui::test]
9619async fn test_snippets(cx: &mut TestAppContext) {
9620 init_test(cx, |_| {});
9621
9622 let mut cx = EditorTestContext::new(cx).await;
9623
9624 cx.set_state(indoc! {"
9625 a.ˇ b
9626 a.ˇ b
9627 a.ˇ b
9628 "});
9629
9630 cx.update_editor(|editor, window, cx| {
9631 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
9632 let insertion_ranges = editor
9633 .selections
9634 .all(cx)
9635 .iter()
9636 .map(|s| s.range())
9637 .collect::<Vec<_>>();
9638 editor
9639 .insert_snippet(&insertion_ranges, snippet, window, cx)
9640 .unwrap();
9641 });
9642
9643 cx.assert_editor_state(indoc! {"
9644 a.f(«oneˇ», two, «threeˇ») b
9645 a.f(«oneˇ», two, «threeˇ») b
9646 a.f(«oneˇ», two, «threeˇ») b
9647 "});
9648
9649 // Can't move earlier than the first tab stop
9650 cx.update_editor(|editor, window, cx| {
9651 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9652 });
9653 cx.assert_editor_state(indoc! {"
9654 a.f(«oneˇ», two, «threeˇ») b
9655 a.f(«oneˇ», two, «threeˇ») b
9656 a.f(«oneˇ», two, «threeˇ») b
9657 "});
9658
9659 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9660 cx.assert_editor_state(indoc! {"
9661 a.f(one, «twoˇ», three) b
9662 a.f(one, «twoˇ», three) b
9663 a.f(one, «twoˇ», three) b
9664 "});
9665
9666 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
9667 cx.assert_editor_state(indoc! {"
9668 a.f(«oneˇ», two, «threeˇ») b
9669 a.f(«oneˇ», two, «threeˇ») b
9670 a.f(«oneˇ», two, «threeˇ») b
9671 "});
9672
9673 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9674 cx.assert_editor_state(indoc! {"
9675 a.f(one, «twoˇ», three) b
9676 a.f(one, «twoˇ», three) b
9677 a.f(one, «twoˇ», three) b
9678 "});
9679 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9680 cx.assert_editor_state(indoc! {"
9681 a.f(one, two, three)ˇ b
9682 a.f(one, two, three)ˇ b
9683 a.f(one, two, three)ˇ b
9684 "});
9685
9686 // As soon as the last tab stop is reached, snippet state is gone
9687 cx.update_editor(|editor, window, cx| {
9688 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9689 });
9690 cx.assert_editor_state(indoc! {"
9691 a.f(one, two, three)ˇ b
9692 a.f(one, two, three)ˇ b
9693 a.f(one, two, three)ˇ b
9694 "});
9695}
9696
9697#[gpui::test]
9698async fn test_snippet_indentation(cx: &mut TestAppContext) {
9699 init_test(cx, |_| {});
9700
9701 let mut cx = EditorTestContext::new(cx).await;
9702
9703 cx.update_editor(|editor, window, cx| {
9704 let snippet = Snippet::parse(indoc! {"
9705 /*
9706 * Multiline comment with leading indentation
9707 *
9708 * $1
9709 */
9710 $0"})
9711 .unwrap();
9712 let insertion_ranges = editor
9713 .selections
9714 .all(cx)
9715 .iter()
9716 .map(|s| s.range())
9717 .collect::<Vec<_>>();
9718 editor
9719 .insert_snippet(&insertion_ranges, snippet, window, cx)
9720 .unwrap();
9721 });
9722
9723 cx.assert_editor_state(indoc! {"
9724 /*
9725 * Multiline comment with leading indentation
9726 *
9727 * ˇ
9728 */
9729 "});
9730
9731 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9732 cx.assert_editor_state(indoc! {"
9733 /*
9734 * Multiline comment with leading indentation
9735 *
9736 *•
9737 */
9738 ˇ"});
9739}
9740
9741#[gpui::test]
9742async fn test_document_format_during_save(cx: &mut TestAppContext) {
9743 init_test(cx, |_| {});
9744
9745 let fs = FakeFs::new(cx.executor());
9746 fs.insert_file(path!("/file.rs"), Default::default()).await;
9747
9748 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
9749
9750 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9751 language_registry.add(rust_lang());
9752 let mut fake_servers = language_registry.register_fake_lsp(
9753 "Rust",
9754 FakeLspAdapter {
9755 capabilities: lsp::ServerCapabilities {
9756 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9757 ..Default::default()
9758 },
9759 ..Default::default()
9760 },
9761 );
9762
9763 let buffer = project
9764 .update(cx, |project, cx| {
9765 project.open_local_buffer(path!("/file.rs"), cx)
9766 })
9767 .await
9768 .unwrap();
9769
9770 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9771 let (editor, cx) = cx.add_window_view(|window, cx| {
9772 build_editor_with_project(project.clone(), buffer, window, cx)
9773 });
9774 editor.update_in(cx, |editor, window, cx| {
9775 editor.set_text("one\ntwo\nthree\n", window, cx)
9776 });
9777 assert!(cx.read(|cx| editor.is_dirty(cx)));
9778
9779 cx.executor().start_waiting();
9780 let fake_server = fake_servers.next().await.unwrap();
9781
9782 {
9783 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9784 move |params, _| async move {
9785 assert_eq!(
9786 params.text_document.uri,
9787 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9788 );
9789 assert_eq!(params.options.tab_size, 4);
9790 Ok(Some(vec![lsp::TextEdit::new(
9791 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9792 ", ".to_string(),
9793 )]))
9794 },
9795 );
9796 let save = editor
9797 .update_in(cx, |editor, window, cx| {
9798 editor.save(
9799 SaveOptions {
9800 format: true,
9801 autosave: false,
9802 },
9803 project.clone(),
9804 window,
9805 cx,
9806 )
9807 })
9808 .unwrap();
9809 cx.executor().start_waiting();
9810 save.await;
9811
9812 assert_eq!(
9813 editor.update(cx, |editor, cx| editor.text(cx)),
9814 "one, two\nthree\n"
9815 );
9816 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9817 }
9818
9819 {
9820 editor.update_in(cx, |editor, window, cx| {
9821 editor.set_text("one\ntwo\nthree\n", window, cx)
9822 });
9823 assert!(cx.read(|cx| editor.is_dirty(cx)));
9824
9825 // Ensure we can still save even if formatting hangs.
9826 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9827 move |params, _| async move {
9828 assert_eq!(
9829 params.text_document.uri,
9830 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9831 );
9832 futures::future::pending::<()>().await;
9833 unreachable!()
9834 },
9835 );
9836 let save = editor
9837 .update_in(cx, |editor, window, cx| {
9838 editor.save(
9839 SaveOptions {
9840 format: true,
9841 autosave: false,
9842 },
9843 project.clone(),
9844 window,
9845 cx,
9846 )
9847 })
9848 .unwrap();
9849 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9850 cx.executor().start_waiting();
9851 save.await;
9852 assert_eq!(
9853 editor.update(cx, |editor, cx| editor.text(cx)),
9854 "one\ntwo\nthree\n"
9855 );
9856 }
9857
9858 // Set rust language override and assert overridden tabsize is sent to language server
9859 update_test_language_settings(cx, |settings| {
9860 settings.languages.0.insert(
9861 "Rust".into(),
9862 LanguageSettingsContent {
9863 tab_size: NonZeroU32::new(8),
9864 ..Default::default()
9865 },
9866 );
9867 });
9868
9869 {
9870 editor.update_in(cx, |editor, window, cx| {
9871 editor.set_text("somehting_new\n", window, cx)
9872 });
9873 assert!(cx.read(|cx| editor.is_dirty(cx)));
9874 let _formatting_request_signal = fake_server
9875 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9876 assert_eq!(
9877 params.text_document.uri,
9878 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9879 );
9880 assert_eq!(params.options.tab_size, 8);
9881 Ok(Some(vec![]))
9882 });
9883 let save = editor
9884 .update_in(cx, |editor, window, cx| {
9885 editor.save(
9886 SaveOptions {
9887 format: true,
9888 autosave: false,
9889 },
9890 project.clone(),
9891 window,
9892 cx,
9893 )
9894 })
9895 .unwrap();
9896 cx.executor().start_waiting();
9897 save.await;
9898 }
9899}
9900
9901#[gpui::test]
9902async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
9903 init_test(cx, |settings| {
9904 settings.defaults.ensure_final_newline_on_save = Some(false);
9905 });
9906
9907 let fs = FakeFs::new(cx.executor());
9908 fs.insert_file(path!("/file.txt"), "foo".into()).await;
9909
9910 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
9911
9912 let buffer = project
9913 .update(cx, |project, cx| {
9914 project.open_local_buffer(path!("/file.txt"), cx)
9915 })
9916 .await
9917 .unwrap();
9918
9919 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9920 let (editor, cx) = cx.add_window_view(|window, cx| {
9921 build_editor_with_project(project.clone(), buffer, window, cx)
9922 });
9923 editor.update_in(cx, |editor, window, cx| {
9924 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9925 s.select_ranges([0..0])
9926 });
9927 });
9928 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9929
9930 editor.update_in(cx, |editor, window, cx| {
9931 editor.handle_input("\n", window, cx)
9932 });
9933 cx.run_until_parked();
9934 save(&editor, &project, cx).await;
9935 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9936
9937 editor.update_in(cx, |editor, window, cx| {
9938 editor.undo(&Default::default(), window, cx);
9939 });
9940 save(&editor, &project, cx).await;
9941 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9942
9943 editor.update_in(cx, |editor, window, cx| {
9944 editor.redo(&Default::default(), window, cx);
9945 });
9946 cx.run_until_parked();
9947 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9948
9949 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
9950 let save = editor
9951 .update_in(cx, |editor, window, cx| {
9952 editor.save(
9953 SaveOptions {
9954 format: true,
9955 autosave: false,
9956 },
9957 project.clone(),
9958 window,
9959 cx,
9960 )
9961 })
9962 .unwrap();
9963 cx.executor().start_waiting();
9964 save.await;
9965 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9966 }
9967}
9968
9969#[gpui::test]
9970async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9971 init_test(cx, |_| {});
9972
9973 let cols = 4;
9974 let rows = 10;
9975 let sample_text_1 = sample_text(rows, cols, 'a');
9976 assert_eq!(
9977 sample_text_1,
9978 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9979 );
9980 let sample_text_2 = sample_text(rows, cols, 'l');
9981 assert_eq!(
9982 sample_text_2,
9983 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9984 );
9985 let sample_text_3 = sample_text(rows, cols, 'v');
9986 assert_eq!(
9987 sample_text_3,
9988 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9989 );
9990
9991 let fs = FakeFs::new(cx.executor());
9992 fs.insert_tree(
9993 path!("/a"),
9994 json!({
9995 "main.rs": sample_text_1,
9996 "other.rs": sample_text_2,
9997 "lib.rs": sample_text_3,
9998 }),
9999 )
10000 .await;
10001
10002 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10003 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10004 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
10005
10006 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10007 language_registry.add(rust_lang());
10008 let mut fake_servers = language_registry.register_fake_lsp(
10009 "Rust",
10010 FakeLspAdapter {
10011 capabilities: lsp::ServerCapabilities {
10012 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10013 ..Default::default()
10014 },
10015 ..Default::default()
10016 },
10017 );
10018
10019 let worktree = project.update(cx, |project, cx| {
10020 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
10021 assert_eq!(worktrees.len(), 1);
10022 worktrees.pop().unwrap()
10023 });
10024 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
10025
10026 let buffer_1 = project
10027 .update(cx, |project, cx| {
10028 project.open_buffer((worktree_id, "main.rs"), cx)
10029 })
10030 .await
10031 .unwrap();
10032 let buffer_2 = project
10033 .update(cx, |project, cx| {
10034 project.open_buffer((worktree_id, "other.rs"), cx)
10035 })
10036 .await
10037 .unwrap();
10038 let buffer_3 = project
10039 .update(cx, |project, cx| {
10040 project.open_buffer((worktree_id, "lib.rs"), cx)
10041 })
10042 .await
10043 .unwrap();
10044
10045 let multi_buffer = cx.new(|cx| {
10046 let mut multi_buffer = MultiBuffer::new(ReadWrite);
10047 multi_buffer.push_excerpts(
10048 buffer_1.clone(),
10049 [
10050 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
10051 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
10052 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
10053 ],
10054 cx,
10055 );
10056 multi_buffer.push_excerpts(
10057 buffer_2.clone(),
10058 [
10059 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
10060 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
10061 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
10062 ],
10063 cx,
10064 );
10065 multi_buffer.push_excerpts(
10066 buffer_3.clone(),
10067 [
10068 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
10069 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
10070 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
10071 ],
10072 cx,
10073 );
10074 multi_buffer
10075 });
10076 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
10077 Editor::new(
10078 EditorMode::full(),
10079 multi_buffer,
10080 Some(project.clone()),
10081 window,
10082 cx,
10083 )
10084 });
10085
10086 multi_buffer_editor.update_in(cx, |editor, window, cx| {
10087 editor.change_selections(
10088 SelectionEffects::scroll(Autoscroll::Next),
10089 window,
10090 cx,
10091 |s| s.select_ranges(Some(1..2)),
10092 );
10093 editor.insert("|one|two|three|", window, cx);
10094 });
10095 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
10096 multi_buffer_editor.update_in(cx, |editor, window, cx| {
10097 editor.change_selections(
10098 SelectionEffects::scroll(Autoscroll::Next),
10099 window,
10100 cx,
10101 |s| s.select_ranges(Some(60..70)),
10102 );
10103 editor.insert("|four|five|six|", window, cx);
10104 });
10105 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
10106
10107 // First two buffers should be edited, but not the third one.
10108 assert_eq!(
10109 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
10110 "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}",
10111 );
10112 buffer_1.update(cx, |buffer, _| {
10113 assert!(buffer.is_dirty());
10114 assert_eq!(
10115 buffer.text(),
10116 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
10117 )
10118 });
10119 buffer_2.update(cx, |buffer, _| {
10120 assert!(buffer.is_dirty());
10121 assert_eq!(
10122 buffer.text(),
10123 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
10124 )
10125 });
10126 buffer_3.update(cx, |buffer, _| {
10127 assert!(!buffer.is_dirty());
10128 assert_eq!(buffer.text(), sample_text_3,)
10129 });
10130 cx.executor().run_until_parked();
10131
10132 cx.executor().start_waiting();
10133 let save = multi_buffer_editor
10134 .update_in(cx, |editor, window, cx| {
10135 editor.save(
10136 SaveOptions {
10137 format: true,
10138 autosave: false,
10139 },
10140 project.clone(),
10141 window,
10142 cx,
10143 )
10144 })
10145 .unwrap();
10146
10147 let fake_server = fake_servers.next().await.unwrap();
10148 fake_server
10149 .server
10150 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
10151 Ok(Some(vec![lsp::TextEdit::new(
10152 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10153 format!("[{} formatted]", params.text_document.uri),
10154 )]))
10155 })
10156 .detach();
10157 save.await;
10158
10159 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
10160 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
10161 assert_eq!(
10162 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
10163 uri!(
10164 "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}"
10165 ),
10166 );
10167 buffer_1.update(cx, |buffer, _| {
10168 assert!(!buffer.is_dirty());
10169 assert_eq!(
10170 buffer.text(),
10171 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
10172 )
10173 });
10174 buffer_2.update(cx, |buffer, _| {
10175 assert!(!buffer.is_dirty());
10176 assert_eq!(
10177 buffer.text(),
10178 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
10179 )
10180 });
10181 buffer_3.update(cx, |buffer, _| {
10182 assert!(!buffer.is_dirty());
10183 assert_eq!(buffer.text(), sample_text_3,)
10184 });
10185}
10186
10187#[gpui::test]
10188async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
10189 init_test(cx, |_| {});
10190
10191 let fs = FakeFs::new(cx.executor());
10192 fs.insert_tree(
10193 path!("/dir"),
10194 json!({
10195 "file1.rs": "fn main() { println!(\"hello\"); }",
10196 "file2.rs": "fn test() { println!(\"test\"); }",
10197 "file3.rs": "fn other() { println!(\"other\"); }\n",
10198 }),
10199 )
10200 .await;
10201
10202 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
10203 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10204 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
10205
10206 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10207 language_registry.add(rust_lang());
10208
10209 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
10210 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
10211
10212 // Open three buffers
10213 let buffer_1 = project
10214 .update(cx, |project, cx| {
10215 project.open_buffer((worktree_id, "file1.rs"), cx)
10216 })
10217 .await
10218 .unwrap();
10219 let buffer_2 = project
10220 .update(cx, |project, cx| {
10221 project.open_buffer((worktree_id, "file2.rs"), cx)
10222 })
10223 .await
10224 .unwrap();
10225 let buffer_3 = project
10226 .update(cx, |project, cx| {
10227 project.open_buffer((worktree_id, "file3.rs"), cx)
10228 })
10229 .await
10230 .unwrap();
10231
10232 // Create a multi-buffer with all three buffers
10233 let multi_buffer = cx.new(|cx| {
10234 let mut multi_buffer = MultiBuffer::new(ReadWrite);
10235 multi_buffer.push_excerpts(
10236 buffer_1.clone(),
10237 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
10238 cx,
10239 );
10240 multi_buffer.push_excerpts(
10241 buffer_2.clone(),
10242 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
10243 cx,
10244 );
10245 multi_buffer.push_excerpts(
10246 buffer_3.clone(),
10247 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
10248 cx,
10249 );
10250 multi_buffer
10251 });
10252
10253 let editor = cx.new_window_entity(|window, cx| {
10254 Editor::new(
10255 EditorMode::full(),
10256 multi_buffer,
10257 Some(project.clone()),
10258 window,
10259 cx,
10260 )
10261 });
10262
10263 // Edit only the first buffer
10264 editor.update_in(cx, |editor, window, cx| {
10265 editor.change_selections(
10266 SelectionEffects::scroll(Autoscroll::Next),
10267 window,
10268 cx,
10269 |s| s.select_ranges(Some(10..10)),
10270 );
10271 editor.insert("// edited", window, cx);
10272 });
10273
10274 // Verify that only buffer 1 is dirty
10275 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
10276 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10277 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10278
10279 // Get write counts after file creation (files were created with initial content)
10280 // We expect each file to have been written once during creation
10281 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
10282 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
10283 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
10284
10285 // Perform autosave
10286 let save_task = editor.update_in(cx, |editor, window, cx| {
10287 editor.save(
10288 SaveOptions {
10289 format: true,
10290 autosave: true,
10291 },
10292 project.clone(),
10293 window,
10294 cx,
10295 )
10296 });
10297 save_task.await.unwrap();
10298
10299 // Only the dirty buffer should have been saved
10300 assert_eq!(
10301 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
10302 1,
10303 "Buffer 1 was dirty, so it should have been written once during autosave"
10304 );
10305 assert_eq!(
10306 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
10307 0,
10308 "Buffer 2 was clean, so it should not have been written during autosave"
10309 );
10310 assert_eq!(
10311 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
10312 0,
10313 "Buffer 3 was clean, so it should not have been written during autosave"
10314 );
10315
10316 // Verify buffer states after autosave
10317 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10318 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10319 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10320
10321 // Now perform a manual save (format = true)
10322 let save_task = editor.update_in(cx, |editor, window, cx| {
10323 editor.save(
10324 SaveOptions {
10325 format: true,
10326 autosave: false,
10327 },
10328 project.clone(),
10329 window,
10330 cx,
10331 )
10332 });
10333 save_task.await.unwrap();
10334
10335 // During manual save, clean buffers don't get written to disk
10336 // They just get did_save called for language server notifications
10337 assert_eq!(
10338 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
10339 1,
10340 "Buffer 1 should only have been written once total (during autosave, not manual save)"
10341 );
10342 assert_eq!(
10343 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
10344 0,
10345 "Buffer 2 should not have been written at all"
10346 );
10347 assert_eq!(
10348 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
10349 0,
10350 "Buffer 3 should not have been written at all"
10351 );
10352}
10353
10354async fn setup_range_format_test(
10355 cx: &mut TestAppContext,
10356) -> (
10357 Entity<Project>,
10358 Entity<Editor>,
10359 &mut gpui::VisualTestContext,
10360 lsp::FakeLanguageServer,
10361) {
10362 init_test(cx, |_| {});
10363
10364 let fs = FakeFs::new(cx.executor());
10365 fs.insert_file(path!("/file.rs"), Default::default()).await;
10366
10367 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10368
10369 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10370 language_registry.add(rust_lang());
10371 let mut fake_servers = language_registry.register_fake_lsp(
10372 "Rust",
10373 FakeLspAdapter {
10374 capabilities: lsp::ServerCapabilities {
10375 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
10376 ..lsp::ServerCapabilities::default()
10377 },
10378 ..FakeLspAdapter::default()
10379 },
10380 );
10381
10382 let buffer = project
10383 .update(cx, |project, cx| {
10384 project.open_local_buffer(path!("/file.rs"), cx)
10385 })
10386 .await
10387 .unwrap();
10388
10389 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10390 let (editor, cx) = cx.add_window_view(|window, cx| {
10391 build_editor_with_project(project.clone(), buffer, window, cx)
10392 });
10393
10394 cx.executor().start_waiting();
10395 let fake_server = fake_servers.next().await.unwrap();
10396
10397 (project, editor, cx, fake_server)
10398}
10399
10400#[gpui::test]
10401async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
10402 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10403
10404 editor.update_in(cx, |editor, window, cx| {
10405 editor.set_text("one\ntwo\nthree\n", window, cx)
10406 });
10407 assert!(cx.read(|cx| editor.is_dirty(cx)));
10408
10409 let save = editor
10410 .update_in(cx, |editor, window, cx| {
10411 editor.save(
10412 SaveOptions {
10413 format: true,
10414 autosave: false,
10415 },
10416 project.clone(),
10417 window,
10418 cx,
10419 )
10420 })
10421 .unwrap();
10422 fake_server
10423 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10424 assert_eq!(
10425 params.text_document.uri,
10426 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10427 );
10428 assert_eq!(params.options.tab_size, 4);
10429 Ok(Some(vec![lsp::TextEdit::new(
10430 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10431 ", ".to_string(),
10432 )]))
10433 })
10434 .next()
10435 .await;
10436 cx.executor().start_waiting();
10437 save.await;
10438 assert_eq!(
10439 editor.update(cx, |editor, cx| editor.text(cx)),
10440 "one, two\nthree\n"
10441 );
10442 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10443}
10444
10445#[gpui::test]
10446async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
10447 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10448
10449 editor.update_in(cx, |editor, window, cx| {
10450 editor.set_text("one\ntwo\nthree\n", window, cx)
10451 });
10452 assert!(cx.read(|cx| editor.is_dirty(cx)));
10453
10454 // Test that save still works when formatting hangs
10455 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
10456 move |params, _| async move {
10457 assert_eq!(
10458 params.text_document.uri,
10459 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10460 );
10461 futures::future::pending::<()>().await;
10462 unreachable!()
10463 },
10464 );
10465 let save = editor
10466 .update_in(cx, |editor, window, cx| {
10467 editor.save(
10468 SaveOptions {
10469 format: true,
10470 autosave: false,
10471 },
10472 project.clone(),
10473 window,
10474 cx,
10475 )
10476 })
10477 .unwrap();
10478 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10479 cx.executor().start_waiting();
10480 save.await;
10481 assert_eq!(
10482 editor.update(cx, |editor, cx| editor.text(cx)),
10483 "one\ntwo\nthree\n"
10484 );
10485 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10486}
10487
10488#[gpui::test]
10489async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
10490 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10491
10492 // Buffer starts clean, no formatting should be requested
10493 let save = editor
10494 .update_in(cx, |editor, window, cx| {
10495 editor.save(
10496 SaveOptions {
10497 format: false,
10498 autosave: false,
10499 },
10500 project.clone(),
10501 window,
10502 cx,
10503 )
10504 })
10505 .unwrap();
10506 let _pending_format_request = fake_server
10507 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
10508 panic!("Should not be invoked");
10509 })
10510 .next();
10511 cx.executor().start_waiting();
10512 save.await;
10513 cx.run_until_parked();
10514}
10515
10516#[gpui::test]
10517async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
10518 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10519
10520 // Set Rust language override and assert overridden tabsize is sent to language server
10521 update_test_language_settings(cx, |settings| {
10522 settings.languages.0.insert(
10523 "Rust".into(),
10524 LanguageSettingsContent {
10525 tab_size: NonZeroU32::new(8),
10526 ..Default::default()
10527 },
10528 );
10529 });
10530
10531 editor.update_in(cx, |editor, window, cx| {
10532 editor.set_text("something_new\n", window, cx)
10533 });
10534 assert!(cx.read(|cx| editor.is_dirty(cx)));
10535 let save = editor
10536 .update_in(cx, |editor, window, cx| {
10537 editor.save(
10538 SaveOptions {
10539 format: true,
10540 autosave: false,
10541 },
10542 project.clone(),
10543 window,
10544 cx,
10545 )
10546 })
10547 .unwrap();
10548 fake_server
10549 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10550 assert_eq!(
10551 params.text_document.uri,
10552 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10553 );
10554 assert_eq!(params.options.tab_size, 8);
10555 Ok(Some(Vec::new()))
10556 })
10557 .next()
10558 .await;
10559 save.await;
10560}
10561
10562#[gpui::test]
10563async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
10564 init_test(cx, |settings| {
10565 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
10566 Formatter::LanguageServer { name: None },
10567 )))
10568 });
10569
10570 let fs = FakeFs::new(cx.executor());
10571 fs.insert_file(path!("/file.rs"), Default::default()).await;
10572
10573 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10574
10575 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10576 language_registry.add(Arc::new(Language::new(
10577 LanguageConfig {
10578 name: "Rust".into(),
10579 matcher: LanguageMatcher {
10580 path_suffixes: vec!["rs".to_string()],
10581 ..Default::default()
10582 },
10583 ..LanguageConfig::default()
10584 },
10585 Some(tree_sitter_rust::LANGUAGE.into()),
10586 )));
10587 update_test_language_settings(cx, |settings| {
10588 // Enable Prettier formatting for the same buffer, and ensure
10589 // LSP is called instead of Prettier.
10590 settings.defaults.prettier = Some(PrettierSettings {
10591 allowed: true,
10592 ..PrettierSettings::default()
10593 });
10594 });
10595 let mut fake_servers = language_registry.register_fake_lsp(
10596 "Rust",
10597 FakeLspAdapter {
10598 capabilities: lsp::ServerCapabilities {
10599 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10600 ..Default::default()
10601 },
10602 ..Default::default()
10603 },
10604 );
10605
10606 let buffer = project
10607 .update(cx, |project, cx| {
10608 project.open_local_buffer(path!("/file.rs"), cx)
10609 })
10610 .await
10611 .unwrap();
10612
10613 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10614 let (editor, cx) = cx.add_window_view(|window, cx| {
10615 build_editor_with_project(project.clone(), buffer, window, cx)
10616 });
10617 editor.update_in(cx, |editor, window, cx| {
10618 editor.set_text("one\ntwo\nthree\n", window, cx)
10619 });
10620
10621 cx.executor().start_waiting();
10622 let fake_server = fake_servers.next().await.unwrap();
10623
10624 let format = editor
10625 .update_in(cx, |editor, window, cx| {
10626 editor.perform_format(
10627 project.clone(),
10628 FormatTrigger::Manual,
10629 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10630 window,
10631 cx,
10632 )
10633 })
10634 .unwrap();
10635 fake_server
10636 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10637 assert_eq!(
10638 params.text_document.uri,
10639 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10640 );
10641 assert_eq!(params.options.tab_size, 4);
10642 Ok(Some(vec![lsp::TextEdit::new(
10643 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10644 ", ".to_string(),
10645 )]))
10646 })
10647 .next()
10648 .await;
10649 cx.executor().start_waiting();
10650 format.await;
10651 assert_eq!(
10652 editor.update(cx, |editor, cx| editor.text(cx)),
10653 "one, two\nthree\n"
10654 );
10655
10656 editor.update_in(cx, |editor, window, cx| {
10657 editor.set_text("one\ntwo\nthree\n", window, cx)
10658 });
10659 // Ensure we don't lock if formatting hangs.
10660 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10661 move |params, _| async move {
10662 assert_eq!(
10663 params.text_document.uri,
10664 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10665 );
10666 futures::future::pending::<()>().await;
10667 unreachable!()
10668 },
10669 );
10670 let format = editor
10671 .update_in(cx, |editor, window, cx| {
10672 editor.perform_format(
10673 project,
10674 FormatTrigger::Manual,
10675 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10676 window,
10677 cx,
10678 )
10679 })
10680 .unwrap();
10681 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10682 cx.executor().start_waiting();
10683 format.await;
10684 assert_eq!(
10685 editor.update(cx, |editor, cx| editor.text(cx)),
10686 "one\ntwo\nthree\n"
10687 );
10688}
10689
10690#[gpui::test]
10691async fn test_multiple_formatters(cx: &mut TestAppContext) {
10692 init_test(cx, |settings| {
10693 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
10694 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10695 Formatter::LanguageServer { name: None },
10696 Formatter::CodeActions(
10697 [
10698 ("code-action-1".into(), true),
10699 ("code-action-2".into(), true),
10700 ]
10701 .into_iter()
10702 .collect(),
10703 ),
10704 ])))
10705 });
10706
10707 let fs = FakeFs::new(cx.executor());
10708 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
10709 .await;
10710
10711 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10712 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10713 language_registry.add(rust_lang());
10714
10715 let mut fake_servers = language_registry.register_fake_lsp(
10716 "Rust",
10717 FakeLspAdapter {
10718 capabilities: lsp::ServerCapabilities {
10719 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10720 execute_command_provider: Some(lsp::ExecuteCommandOptions {
10721 commands: vec!["the-command-for-code-action-1".into()],
10722 ..Default::default()
10723 }),
10724 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10725 ..Default::default()
10726 },
10727 ..Default::default()
10728 },
10729 );
10730
10731 let buffer = project
10732 .update(cx, |project, cx| {
10733 project.open_local_buffer(path!("/file.rs"), cx)
10734 })
10735 .await
10736 .unwrap();
10737
10738 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10739 let (editor, cx) = cx.add_window_view(|window, cx| {
10740 build_editor_with_project(project.clone(), buffer, window, cx)
10741 });
10742
10743 cx.executor().start_waiting();
10744
10745 let fake_server = fake_servers.next().await.unwrap();
10746 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10747 move |_params, _| async move {
10748 Ok(Some(vec![lsp::TextEdit::new(
10749 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10750 "applied-formatting\n".to_string(),
10751 )]))
10752 },
10753 );
10754 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10755 move |params, _| async move {
10756 assert_eq!(
10757 params.context.only,
10758 Some(vec!["code-action-1".into(), "code-action-2".into()])
10759 );
10760 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
10761 Ok(Some(vec![
10762 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10763 kind: Some("code-action-1".into()),
10764 edit: Some(lsp::WorkspaceEdit::new(
10765 [(
10766 uri.clone(),
10767 vec![lsp::TextEdit::new(
10768 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10769 "applied-code-action-1-edit\n".to_string(),
10770 )],
10771 )]
10772 .into_iter()
10773 .collect(),
10774 )),
10775 command: Some(lsp::Command {
10776 command: "the-command-for-code-action-1".into(),
10777 ..Default::default()
10778 }),
10779 ..Default::default()
10780 }),
10781 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10782 kind: Some("code-action-2".into()),
10783 edit: Some(lsp::WorkspaceEdit::new(
10784 [(
10785 uri,
10786 vec![lsp::TextEdit::new(
10787 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10788 "applied-code-action-2-edit\n".to_string(),
10789 )],
10790 )]
10791 .into_iter()
10792 .collect(),
10793 )),
10794 ..Default::default()
10795 }),
10796 ]))
10797 },
10798 );
10799
10800 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
10801 move |params, _| async move { Ok(params) }
10802 });
10803
10804 let command_lock = Arc::new(futures::lock::Mutex::new(()));
10805 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
10806 let fake = fake_server.clone();
10807 let lock = command_lock.clone();
10808 move |params, _| {
10809 assert_eq!(params.command, "the-command-for-code-action-1");
10810 let fake = fake.clone();
10811 let lock = lock.clone();
10812 async move {
10813 lock.lock().await;
10814 fake.server
10815 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
10816 label: None,
10817 edit: lsp::WorkspaceEdit {
10818 changes: Some(
10819 [(
10820 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
10821 vec![lsp::TextEdit {
10822 range: lsp::Range::new(
10823 lsp::Position::new(0, 0),
10824 lsp::Position::new(0, 0),
10825 ),
10826 new_text: "applied-code-action-1-command\n".into(),
10827 }],
10828 )]
10829 .into_iter()
10830 .collect(),
10831 ),
10832 ..Default::default()
10833 },
10834 })
10835 .await
10836 .into_response()
10837 .unwrap();
10838 Ok(Some(json!(null)))
10839 }
10840 }
10841 });
10842
10843 cx.executor().start_waiting();
10844 editor
10845 .update_in(cx, |editor, window, cx| {
10846 editor.perform_format(
10847 project.clone(),
10848 FormatTrigger::Manual,
10849 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10850 window,
10851 cx,
10852 )
10853 })
10854 .unwrap()
10855 .await;
10856 editor.update(cx, |editor, cx| {
10857 assert_eq!(
10858 editor.text(cx),
10859 r#"
10860 applied-code-action-2-edit
10861 applied-code-action-1-command
10862 applied-code-action-1-edit
10863 applied-formatting
10864 one
10865 two
10866 three
10867 "#
10868 .unindent()
10869 );
10870 });
10871
10872 editor.update_in(cx, |editor, window, cx| {
10873 editor.undo(&Default::default(), window, cx);
10874 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10875 });
10876
10877 // Perform a manual edit while waiting for an LSP command
10878 // that's being run as part of a formatting code action.
10879 let lock_guard = command_lock.lock().await;
10880 let format = editor
10881 .update_in(cx, |editor, window, cx| {
10882 editor.perform_format(
10883 project.clone(),
10884 FormatTrigger::Manual,
10885 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10886 window,
10887 cx,
10888 )
10889 })
10890 .unwrap();
10891 cx.run_until_parked();
10892 editor.update(cx, |editor, cx| {
10893 assert_eq!(
10894 editor.text(cx),
10895 r#"
10896 applied-code-action-1-edit
10897 applied-formatting
10898 one
10899 two
10900 three
10901 "#
10902 .unindent()
10903 );
10904
10905 editor.buffer.update(cx, |buffer, cx| {
10906 let ix = buffer.len(cx);
10907 buffer.edit([(ix..ix, "edited\n")], None, cx);
10908 });
10909 });
10910
10911 // Allow the LSP command to proceed. Because the buffer was edited,
10912 // the second code action will not be run.
10913 drop(lock_guard);
10914 format.await;
10915 editor.update_in(cx, |editor, window, cx| {
10916 assert_eq!(
10917 editor.text(cx),
10918 r#"
10919 applied-code-action-1-command
10920 applied-code-action-1-edit
10921 applied-formatting
10922 one
10923 two
10924 three
10925 edited
10926 "#
10927 .unindent()
10928 );
10929
10930 // The manual edit is undone first, because it is the last thing the user did
10931 // (even though the command completed afterwards).
10932 editor.undo(&Default::default(), window, cx);
10933 assert_eq!(
10934 editor.text(cx),
10935 r#"
10936 applied-code-action-1-command
10937 applied-code-action-1-edit
10938 applied-formatting
10939 one
10940 two
10941 three
10942 "#
10943 .unindent()
10944 );
10945
10946 // All the formatting (including the command, which completed after the manual edit)
10947 // is undone together.
10948 editor.undo(&Default::default(), window, cx);
10949 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10950 });
10951}
10952
10953#[gpui::test]
10954async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
10955 init_test(cx, |settings| {
10956 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10957 Formatter::LanguageServer { name: None },
10958 ])))
10959 });
10960
10961 let fs = FakeFs::new(cx.executor());
10962 fs.insert_file(path!("/file.ts"), Default::default()).await;
10963
10964 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10965
10966 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10967 language_registry.add(Arc::new(Language::new(
10968 LanguageConfig {
10969 name: "TypeScript".into(),
10970 matcher: LanguageMatcher {
10971 path_suffixes: vec!["ts".to_string()],
10972 ..Default::default()
10973 },
10974 ..LanguageConfig::default()
10975 },
10976 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10977 )));
10978 update_test_language_settings(cx, |settings| {
10979 settings.defaults.prettier = Some(PrettierSettings {
10980 allowed: true,
10981 ..PrettierSettings::default()
10982 });
10983 });
10984 let mut fake_servers = language_registry.register_fake_lsp(
10985 "TypeScript",
10986 FakeLspAdapter {
10987 capabilities: lsp::ServerCapabilities {
10988 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10989 ..Default::default()
10990 },
10991 ..Default::default()
10992 },
10993 );
10994
10995 let buffer = project
10996 .update(cx, |project, cx| {
10997 project.open_local_buffer(path!("/file.ts"), cx)
10998 })
10999 .await
11000 .unwrap();
11001
11002 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11003 let (editor, cx) = cx.add_window_view(|window, cx| {
11004 build_editor_with_project(project.clone(), buffer, window, cx)
11005 });
11006 editor.update_in(cx, |editor, window, cx| {
11007 editor.set_text(
11008 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
11009 window,
11010 cx,
11011 )
11012 });
11013
11014 cx.executor().start_waiting();
11015 let fake_server = fake_servers.next().await.unwrap();
11016
11017 let format = editor
11018 .update_in(cx, |editor, window, cx| {
11019 editor.perform_code_action_kind(
11020 project.clone(),
11021 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
11022 window,
11023 cx,
11024 )
11025 })
11026 .unwrap();
11027 fake_server
11028 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
11029 assert_eq!(
11030 params.text_document.uri,
11031 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
11032 );
11033 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
11034 lsp::CodeAction {
11035 title: "Organize Imports".to_string(),
11036 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
11037 edit: Some(lsp::WorkspaceEdit {
11038 changes: Some(
11039 [(
11040 params.text_document.uri.clone(),
11041 vec![lsp::TextEdit::new(
11042 lsp::Range::new(
11043 lsp::Position::new(1, 0),
11044 lsp::Position::new(2, 0),
11045 ),
11046 "".to_string(),
11047 )],
11048 )]
11049 .into_iter()
11050 .collect(),
11051 ),
11052 ..Default::default()
11053 }),
11054 ..Default::default()
11055 },
11056 )]))
11057 })
11058 .next()
11059 .await;
11060 cx.executor().start_waiting();
11061 format.await;
11062 assert_eq!(
11063 editor.update(cx, |editor, cx| editor.text(cx)),
11064 "import { a } from 'module';\n\nconst x = a;\n"
11065 );
11066
11067 editor.update_in(cx, |editor, window, cx| {
11068 editor.set_text(
11069 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
11070 window,
11071 cx,
11072 )
11073 });
11074 // Ensure we don't lock if code action hangs.
11075 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
11076 move |params, _| async move {
11077 assert_eq!(
11078 params.text_document.uri,
11079 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
11080 );
11081 futures::future::pending::<()>().await;
11082 unreachable!()
11083 },
11084 );
11085 let format = editor
11086 .update_in(cx, |editor, window, cx| {
11087 editor.perform_code_action_kind(
11088 project,
11089 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
11090 window,
11091 cx,
11092 )
11093 })
11094 .unwrap();
11095 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
11096 cx.executor().start_waiting();
11097 format.await;
11098 assert_eq!(
11099 editor.update(cx, |editor, cx| editor.text(cx)),
11100 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
11101 );
11102}
11103
11104#[gpui::test]
11105async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
11106 init_test(cx, |_| {});
11107
11108 let mut cx = EditorLspTestContext::new_rust(
11109 lsp::ServerCapabilities {
11110 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11111 ..Default::default()
11112 },
11113 cx,
11114 )
11115 .await;
11116
11117 cx.set_state(indoc! {"
11118 one.twoˇ
11119 "});
11120
11121 // The format request takes a long time. When it completes, it inserts
11122 // a newline and an indent before the `.`
11123 cx.lsp
11124 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
11125 let executor = cx.background_executor().clone();
11126 async move {
11127 executor.timer(Duration::from_millis(100)).await;
11128 Ok(Some(vec![lsp::TextEdit {
11129 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
11130 new_text: "\n ".into(),
11131 }]))
11132 }
11133 });
11134
11135 // Submit a format request.
11136 let format_1 = cx
11137 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
11138 .unwrap();
11139 cx.executor().run_until_parked();
11140
11141 // Submit a second format request.
11142 let format_2 = cx
11143 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
11144 .unwrap();
11145 cx.executor().run_until_parked();
11146
11147 // Wait for both format requests to complete
11148 cx.executor().advance_clock(Duration::from_millis(200));
11149 cx.executor().start_waiting();
11150 format_1.await.unwrap();
11151 cx.executor().start_waiting();
11152 format_2.await.unwrap();
11153
11154 // The formatting edits only happens once.
11155 cx.assert_editor_state(indoc! {"
11156 one
11157 .twoˇ
11158 "});
11159}
11160
11161#[gpui::test]
11162async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
11163 init_test(cx, |settings| {
11164 settings.defaults.formatter = Some(SelectedFormatter::Auto)
11165 });
11166
11167 let mut cx = EditorLspTestContext::new_rust(
11168 lsp::ServerCapabilities {
11169 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11170 ..Default::default()
11171 },
11172 cx,
11173 )
11174 .await;
11175
11176 // Set up a buffer white some trailing whitespace and no trailing newline.
11177 cx.set_state(
11178 &[
11179 "one ", //
11180 "twoˇ", //
11181 "three ", //
11182 "four", //
11183 ]
11184 .join("\n"),
11185 );
11186
11187 // Submit a format request.
11188 let format = cx
11189 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
11190 .unwrap();
11191
11192 // Record which buffer changes have been sent to the language server
11193 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
11194 cx.lsp
11195 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
11196 let buffer_changes = buffer_changes.clone();
11197 move |params, _| {
11198 buffer_changes.lock().extend(
11199 params
11200 .content_changes
11201 .into_iter()
11202 .map(|e| (e.range.unwrap(), e.text)),
11203 );
11204 }
11205 });
11206
11207 // Handle formatting requests to the language server.
11208 cx.lsp
11209 .set_request_handler::<lsp::request::Formatting, _, _>({
11210 let buffer_changes = buffer_changes.clone();
11211 move |_, _| {
11212 // When formatting is requested, trailing whitespace has already been stripped,
11213 // and the trailing newline has already been added.
11214 assert_eq!(
11215 &buffer_changes.lock()[1..],
11216 &[
11217 (
11218 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
11219 "".into()
11220 ),
11221 (
11222 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
11223 "".into()
11224 ),
11225 (
11226 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
11227 "\n".into()
11228 ),
11229 ]
11230 );
11231
11232 // Insert blank lines between each line of the buffer.
11233 async move {
11234 Ok(Some(vec![
11235 lsp::TextEdit {
11236 range: lsp::Range::new(
11237 lsp::Position::new(1, 0),
11238 lsp::Position::new(1, 0),
11239 ),
11240 new_text: "\n".into(),
11241 },
11242 lsp::TextEdit {
11243 range: lsp::Range::new(
11244 lsp::Position::new(2, 0),
11245 lsp::Position::new(2, 0),
11246 ),
11247 new_text: "\n".into(),
11248 },
11249 ]))
11250 }
11251 }
11252 });
11253
11254 // After formatting the buffer, the trailing whitespace is stripped,
11255 // a newline is appended, and the edits provided by the language server
11256 // have been applied.
11257 format.await.unwrap();
11258 cx.assert_editor_state(
11259 &[
11260 "one", //
11261 "", //
11262 "twoˇ", //
11263 "", //
11264 "three", //
11265 "four", //
11266 "", //
11267 ]
11268 .join("\n"),
11269 );
11270
11271 // Undoing the formatting undoes the trailing whitespace removal, the
11272 // trailing newline, and the LSP edits.
11273 cx.update_buffer(|buffer, cx| buffer.undo(cx));
11274 cx.assert_editor_state(
11275 &[
11276 "one ", //
11277 "twoˇ", //
11278 "three ", //
11279 "four", //
11280 ]
11281 .join("\n"),
11282 );
11283}
11284
11285#[gpui::test]
11286async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
11287 cx: &mut TestAppContext,
11288) {
11289 init_test(cx, |_| {});
11290
11291 cx.update(|cx| {
11292 cx.update_global::<SettingsStore, _>(|settings, cx| {
11293 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11294 settings.auto_signature_help = Some(true);
11295 });
11296 });
11297 });
11298
11299 let mut cx = EditorLspTestContext::new_rust(
11300 lsp::ServerCapabilities {
11301 signature_help_provider: Some(lsp::SignatureHelpOptions {
11302 ..Default::default()
11303 }),
11304 ..Default::default()
11305 },
11306 cx,
11307 )
11308 .await;
11309
11310 let language = Language::new(
11311 LanguageConfig {
11312 name: "Rust".into(),
11313 brackets: BracketPairConfig {
11314 pairs: vec![
11315 BracketPair {
11316 start: "{".to_string(),
11317 end: "}".to_string(),
11318 close: true,
11319 surround: true,
11320 newline: true,
11321 },
11322 BracketPair {
11323 start: "(".to_string(),
11324 end: ")".to_string(),
11325 close: true,
11326 surround: true,
11327 newline: true,
11328 },
11329 BracketPair {
11330 start: "/*".to_string(),
11331 end: " */".to_string(),
11332 close: true,
11333 surround: true,
11334 newline: true,
11335 },
11336 BracketPair {
11337 start: "[".to_string(),
11338 end: "]".to_string(),
11339 close: false,
11340 surround: false,
11341 newline: true,
11342 },
11343 BracketPair {
11344 start: "\"".to_string(),
11345 end: "\"".to_string(),
11346 close: true,
11347 surround: true,
11348 newline: false,
11349 },
11350 BracketPair {
11351 start: "<".to_string(),
11352 end: ">".to_string(),
11353 close: false,
11354 surround: true,
11355 newline: true,
11356 },
11357 ],
11358 ..Default::default()
11359 },
11360 autoclose_before: "})]".to_string(),
11361 ..Default::default()
11362 },
11363 Some(tree_sitter_rust::LANGUAGE.into()),
11364 );
11365 let language = Arc::new(language);
11366
11367 cx.language_registry().add(language.clone());
11368 cx.update_buffer(|buffer, cx| {
11369 buffer.set_language(Some(language), cx);
11370 });
11371
11372 cx.set_state(
11373 &r#"
11374 fn main() {
11375 sampleˇ
11376 }
11377 "#
11378 .unindent(),
11379 );
11380
11381 cx.update_editor(|editor, window, cx| {
11382 editor.handle_input("(", window, cx);
11383 });
11384 cx.assert_editor_state(
11385 &"
11386 fn main() {
11387 sample(ˇ)
11388 }
11389 "
11390 .unindent(),
11391 );
11392
11393 let mocked_response = lsp::SignatureHelp {
11394 signatures: vec![lsp::SignatureInformation {
11395 label: "fn sample(param1: u8, param2: u8)".to_string(),
11396 documentation: None,
11397 parameters: Some(vec![
11398 lsp::ParameterInformation {
11399 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11400 documentation: None,
11401 },
11402 lsp::ParameterInformation {
11403 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11404 documentation: None,
11405 },
11406 ]),
11407 active_parameter: None,
11408 }],
11409 active_signature: Some(0),
11410 active_parameter: Some(0),
11411 };
11412 handle_signature_help_request(&mut cx, mocked_response).await;
11413
11414 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11415 .await;
11416
11417 cx.editor(|editor, _, _| {
11418 let signature_help_state = editor.signature_help_state.popover().cloned();
11419 let signature = signature_help_state.unwrap();
11420 assert_eq!(
11421 signature.signatures[signature.current_signature].label,
11422 "fn sample(param1: u8, param2: u8)"
11423 );
11424 });
11425}
11426
11427#[gpui::test]
11428async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
11429 init_test(cx, |_| {});
11430
11431 cx.update(|cx| {
11432 cx.update_global::<SettingsStore, _>(|settings, cx| {
11433 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11434 settings.auto_signature_help = Some(false);
11435 settings.show_signature_help_after_edits = Some(false);
11436 });
11437 });
11438 });
11439
11440 let mut cx = EditorLspTestContext::new_rust(
11441 lsp::ServerCapabilities {
11442 signature_help_provider: Some(lsp::SignatureHelpOptions {
11443 ..Default::default()
11444 }),
11445 ..Default::default()
11446 },
11447 cx,
11448 )
11449 .await;
11450
11451 let language = Language::new(
11452 LanguageConfig {
11453 name: "Rust".into(),
11454 brackets: BracketPairConfig {
11455 pairs: vec![
11456 BracketPair {
11457 start: "{".to_string(),
11458 end: "}".to_string(),
11459 close: true,
11460 surround: true,
11461 newline: true,
11462 },
11463 BracketPair {
11464 start: "(".to_string(),
11465 end: ")".to_string(),
11466 close: true,
11467 surround: true,
11468 newline: true,
11469 },
11470 BracketPair {
11471 start: "/*".to_string(),
11472 end: " */".to_string(),
11473 close: true,
11474 surround: true,
11475 newline: true,
11476 },
11477 BracketPair {
11478 start: "[".to_string(),
11479 end: "]".to_string(),
11480 close: false,
11481 surround: false,
11482 newline: true,
11483 },
11484 BracketPair {
11485 start: "\"".to_string(),
11486 end: "\"".to_string(),
11487 close: true,
11488 surround: true,
11489 newline: false,
11490 },
11491 BracketPair {
11492 start: "<".to_string(),
11493 end: ">".to_string(),
11494 close: false,
11495 surround: true,
11496 newline: true,
11497 },
11498 ],
11499 ..Default::default()
11500 },
11501 autoclose_before: "})]".to_string(),
11502 ..Default::default()
11503 },
11504 Some(tree_sitter_rust::LANGUAGE.into()),
11505 );
11506 let language = Arc::new(language);
11507
11508 cx.language_registry().add(language.clone());
11509 cx.update_buffer(|buffer, cx| {
11510 buffer.set_language(Some(language), cx);
11511 });
11512
11513 // Ensure that signature_help is not called when no signature help is enabled.
11514 cx.set_state(
11515 &r#"
11516 fn main() {
11517 sampleˇ
11518 }
11519 "#
11520 .unindent(),
11521 );
11522 cx.update_editor(|editor, window, cx| {
11523 editor.handle_input("(", window, cx);
11524 });
11525 cx.assert_editor_state(
11526 &"
11527 fn main() {
11528 sample(ˇ)
11529 }
11530 "
11531 .unindent(),
11532 );
11533 cx.editor(|editor, _, _| {
11534 assert!(editor.signature_help_state.task().is_none());
11535 });
11536
11537 let mocked_response = lsp::SignatureHelp {
11538 signatures: vec![lsp::SignatureInformation {
11539 label: "fn sample(param1: u8, param2: u8)".to_string(),
11540 documentation: None,
11541 parameters: Some(vec![
11542 lsp::ParameterInformation {
11543 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11544 documentation: None,
11545 },
11546 lsp::ParameterInformation {
11547 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11548 documentation: None,
11549 },
11550 ]),
11551 active_parameter: None,
11552 }],
11553 active_signature: Some(0),
11554 active_parameter: Some(0),
11555 };
11556
11557 // Ensure that signature_help is called when enabled afte edits
11558 cx.update(|_, cx| {
11559 cx.update_global::<SettingsStore, _>(|settings, cx| {
11560 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11561 settings.auto_signature_help = Some(false);
11562 settings.show_signature_help_after_edits = Some(true);
11563 });
11564 });
11565 });
11566 cx.set_state(
11567 &r#"
11568 fn main() {
11569 sampleˇ
11570 }
11571 "#
11572 .unindent(),
11573 );
11574 cx.update_editor(|editor, window, cx| {
11575 editor.handle_input("(", window, cx);
11576 });
11577 cx.assert_editor_state(
11578 &"
11579 fn main() {
11580 sample(ˇ)
11581 }
11582 "
11583 .unindent(),
11584 );
11585 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11586 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11587 .await;
11588 cx.update_editor(|editor, _, _| {
11589 let signature_help_state = editor.signature_help_state.popover().cloned();
11590 assert!(signature_help_state.is_some());
11591 let signature = signature_help_state.unwrap();
11592 assert_eq!(
11593 signature.signatures[signature.current_signature].label,
11594 "fn sample(param1: u8, param2: u8)"
11595 );
11596 editor.signature_help_state = SignatureHelpState::default();
11597 });
11598
11599 // Ensure that signature_help is called when auto signature help override is enabled
11600 cx.update(|_, cx| {
11601 cx.update_global::<SettingsStore, _>(|settings, cx| {
11602 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11603 settings.auto_signature_help = Some(true);
11604 settings.show_signature_help_after_edits = Some(false);
11605 });
11606 });
11607 });
11608 cx.set_state(
11609 &r#"
11610 fn main() {
11611 sampleˇ
11612 }
11613 "#
11614 .unindent(),
11615 );
11616 cx.update_editor(|editor, window, cx| {
11617 editor.handle_input("(", window, cx);
11618 });
11619 cx.assert_editor_state(
11620 &"
11621 fn main() {
11622 sample(ˇ)
11623 }
11624 "
11625 .unindent(),
11626 );
11627 handle_signature_help_request(&mut cx, mocked_response).await;
11628 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11629 .await;
11630 cx.editor(|editor, _, _| {
11631 let signature_help_state = editor.signature_help_state.popover().cloned();
11632 assert!(signature_help_state.is_some());
11633 let signature = signature_help_state.unwrap();
11634 assert_eq!(
11635 signature.signatures[signature.current_signature].label,
11636 "fn sample(param1: u8, param2: u8)"
11637 );
11638 });
11639}
11640
11641#[gpui::test]
11642async fn test_signature_help(cx: &mut TestAppContext) {
11643 init_test(cx, |_| {});
11644 cx.update(|cx| {
11645 cx.update_global::<SettingsStore, _>(|settings, cx| {
11646 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11647 settings.auto_signature_help = Some(true);
11648 });
11649 });
11650 });
11651
11652 let mut cx = EditorLspTestContext::new_rust(
11653 lsp::ServerCapabilities {
11654 signature_help_provider: Some(lsp::SignatureHelpOptions {
11655 ..Default::default()
11656 }),
11657 ..Default::default()
11658 },
11659 cx,
11660 )
11661 .await;
11662
11663 // A test that directly calls `show_signature_help`
11664 cx.update_editor(|editor, window, cx| {
11665 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11666 });
11667
11668 let mocked_response = lsp::SignatureHelp {
11669 signatures: vec![lsp::SignatureInformation {
11670 label: "fn sample(param1: u8, param2: u8)".to_string(),
11671 documentation: None,
11672 parameters: Some(vec![
11673 lsp::ParameterInformation {
11674 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11675 documentation: None,
11676 },
11677 lsp::ParameterInformation {
11678 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11679 documentation: None,
11680 },
11681 ]),
11682 active_parameter: None,
11683 }],
11684 active_signature: Some(0),
11685 active_parameter: Some(0),
11686 };
11687 handle_signature_help_request(&mut cx, mocked_response).await;
11688
11689 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11690 .await;
11691
11692 cx.editor(|editor, _, _| {
11693 let signature_help_state = editor.signature_help_state.popover().cloned();
11694 assert!(signature_help_state.is_some());
11695 let signature = signature_help_state.unwrap();
11696 assert_eq!(
11697 signature.signatures[signature.current_signature].label,
11698 "fn sample(param1: u8, param2: u8)"
11699 );
11700 });
11701
11702 // When exiting outside from inside the brackets, `signature_help` is closed.
11703 cx.set_state(indoc! {"
11704 fn main() {
11705 sample(ˇ);
11706 }
11707
11708 fn sample(param1: u8, param2: u8) {}
11709 "});
11710
11711 cx.update_editor(|editor, window, cx| {
11712 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11713 s.select_ranges([0..0])
11714 });
11715 });
11716
11717 let mocked_response = lsp::SignatureHelp {
11718 signatures: Vec::new(),
11719 active_signature: None,
11720 active_parameter: None,
11721 };
11722 handle_signature_help_request(&mut cx, mocked_response).await;
11723
11724 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11725 .await;
11726
11727 cx.editor(|editor, _, _| {
11728 assert!(!editor.signature_help_state.is_shown());
11729 });
11730
11731 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
11732 cx.set_state(indoc! {"
11733 fn main() {
11734 sample(ˇ);
11735 }
11736
11737 fn sample(param1: u8, param2: u8) {}
11738 "});
11739
11740 let mocked_response = lsp::SignatureHelp {
11741 signatures: vec![lsp::SignatureInformation {
11742 label: "fn sample(param1: u8, param2: u8)".to_string(),
11743 documentation: None,
11744 parameters: Some(vec![
11745 lsp::ParameterInformation {
11746 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11747 documentation: None,
11748 },
11749 lsp::ParameterInformation {
11750 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11751 documentation: None,
11752 },
11753 ]),
11754 active_parameter: None,
11755 }],
11756 active_signature: Some(0),
11757 active_parameter: Some(0),
11758 };
11759 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11760 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11761 .await;
11762 cx.editor(|editor, _, _| {
11763 assert!(editor.signature_help_state.is_shown());
11764 });
11765
11766 // Restore the popover with more parameter input
11767 cx.set_state(indoc! {"
11768 fn main() {
11769 sample(param1, param2ˇ);
11770 }
11771
11772 fn sample(param1: u8, param2: u8) {}
11773 "});
11774
11775 let mocked_response = lsp::SignatureHelp {
11776 signatures: vec![lsp::SignatureInformation {
11777 label: "fn sample(param1: u8, param2: u8)".to_string(),
11778 documentation: None,
11779 parameters: Some(vec![
11780 lsp::ParameterInformation {
11781 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11782 documentation: None,
11783 },
11784 lsp::ParameterInformation {
11785 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11786 documentation: None,
11787 },
11788 ]),
11789 active_parameter: None,
11790 }],
11791 active_signature: Some(0),
11792 active_parameter: Some(1),
11793 };
11794 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11795 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11796 .await;
11797
11798 // When selecting a range, the popover is gone.
11799 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
11800 cx.update_editor(|editor, window, cx| {
11801 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11802 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11803 })
11804 });
11805 cx.assert_editor_state(indoc! {"
11806 fn main() {
11807 sample(param1, «ˇparam2»);
11808 }
11809
11810 fn sample(param1: u8, param2: u8) {}
11811 "});
11812 cx.editor(|editor, _, _| {
11813 assert!(!editor.signature_help_state.is_shown());
11814 });
11815
11816 // When unselecting again, the popover is back if within the brackets.
11817 cx.update_editor(|editor, window, cx| {
11818 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11819 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11820 })
11821 });
11822 cx.assert_editor_state(indoc! {"
11823 fn main() {
11824 sample(param1, ˇparam2);
11825 }
11826
11827 fn sample(param1: u8, param2: u8) {}
11828 "});
11829 handle_signature_help_request(&mut cx, mocked_response).await;
11830 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11831 .await;
11832 cx.editor(|editor, _, _| {
11833 assert!(editor.signature_help_state.is_shown());
11834 });
11835
11836 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
11837 cx.update_editor(|editor, window, cx| {
11838 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11839 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
11840 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11841 })
11842 });
11843 cx.assert_editor_state(indoc! {"
11844 fn main() {
11845 sample(param1, ˇparam2);
11846 }
11847
11848 fn sample(param1: u8, param2: u8) {}
11849 "});
11850
11851 let mocked_response = lsp::SignatureHelp {
11852 signatures: vec![lsp::SignatureInformation {
11853 label: "fn sample(param1: u8, param2: u8)".to_string(),
11854 documentation: None,
11855 parameters: Some(vec![
11856 lsp::ParameterInformation {
11857 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11858 documentation: None,
11859 },
11860 lsp::ParameterInformation {
11861 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11862 documentation: None,
11863 },
11864 ]),
11865 active_parameter: None,
11866 }],
11867 active_signature: Some(0),
11868 active_parameter: Some(1),
11869 };
11870 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11871 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11872 .await;
11873 cx.update_editor(|editor, _, cx| {
11874 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
11875 });
11876 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11877 .await;
11878 cx.update_editor(|editor, window, cx| {
11879 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11880 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11881 })
11882 });
11883 cx.assert_editor_state(indoc! {"
11884 fn main() {
11885 sample(param1, «ˇparam2»);
11886 }
11887
11888 fn sample(param1: u8, param2: u8) {}
11889 "});
11890 cx.update_editor(|editor, window, cx| {
11891 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11892 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11893 })
11894 });
11895 cx.assert_editor_state(indoc! {"
11896 fn main() {
11897 sample(param1, ˇparam2);
11898 }
11899
11900 fn sample(param1: u8, param2: u8) {}
11901 "});
11902 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
11903 .await;
11904}
11905
11906#[gpui::test]
11907async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
11908 init_test(cx, |_| {});
11909
11910 let mut cx = EditorLspTestContext::new_rust(
11911 lsp::ServerCapabilities {
11912 signature_help_provider: Some(lsp::SignatureHelpOptions {
11913 ..Default::default()
11914 }),
11915 ..Default::default()
11916 },
11917 cx,
11918 )
11919 .await;
11920
11921 cx.set_state(indoc! {"
11922 fn main() {
11923 overloadedˇ
11924 }
11925 "});
11926
11927 cx.update_editor(|editor, window, cx| {
11928 editor.handle_input("(", window, cx);
11929 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11930 });
11931
11932 // Mock response with 3 signatures
11933 let mocked_response = lsp::SignatureHelp {
11934 signatures: vec![
11935 lsp::SignatureInformation {
11936 label: "fn overloaded(x: i32)".to_string(),
11937 documentation: None,
11938 parameters: Some(vec![lsp::ParameterInformation {
11939 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11940 documentation: None,
11941 }]),
11942 active_parameter: None,
11943 },
11944 lsp::SignatureInformation {
11945 label: "fn overloaded(x: i32, y: i32)".to_string(),
11946 documentation: None,
11947 parameters: Some(vec![
11948 lsp::ParameterInformation {
11949 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11950 documentation: None,
11951 },
11952 lsp::ParameterInformation {
11953 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11954 documentation: None,
11955 },
11956 ]),
11957 active_parameter: None,
11958 },
11959 lsp::SignatureInformation {
11960 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
11961 documentation: None,
11962 parameters: Some(vec![
11963 lsp::ParameterInformation {
11964 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11965 documentation: None,
11966 },
11967 lsp::ParameterInformation {
11968 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11969 documentation: None,
11970 },
11971 lsp::ParameterInformation {
11972 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
11973 documentation: None,
11974 },
11975 ]),
11976 active_parameter: None,
11977 },
11978 ],
11979 active_signature: Some(1),
11980 active_parameter: Some(0),
11981 };
11982 handle_signature_help_request(&mut cx, mocked_response).await;
11983
11984 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11985 .await;
11986
11987 // Verify we have multiple signatures and the right one is selected
11988 cx.editor(|editor, _, _| {
11989 let popover = editor.signature_help_state.popover().cloned().unwrap();
11990 assert_eq!(popover.signatures.len(), 3);
11991 // active_signature was 1, so that should be the current
11992 assert_eq!(popover.current_signature, 1);
11993 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
11994 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
11995 assert_eq!(
11996 popover.signatures[2].label,
11997 "fn overloaded(x: i32, y: i32, z: i32)"
11998 );
11999 });
12000
12001 // Test navigation functionality
12002 cx.update_editor(|editor, window, cx| {
12003 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
12004 });
12005
12006 cx.editor(|editor, _, _| {
12007 let popover = editor.signature_help_state.popover().cloned().unwrap();
12008 assert_eq!(popover.current_signature, 2);
12009 });
12010
12011 // Test wrap around
12012 cx.update_editor(|editor, window, cx| {
12013 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
12014 });
12015
12016 cx.editor(|editor, _, _| {
12017 let popover = editor.signature_help_state.popover().cloned().unwrap();
12018 assert_eq!(popover.current_signature, 0);
12019 });
12020
12021 // Test previous navigation
12022 cx.update_editor(|editor, window, cx| {
12023 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
12024 });
12025
12026 cx.editor(|editor, _, _| {
12027 let popover = editor.signature_help_state.popover().cloned().unwrap();
12028 assert_eq!(popover.current_signature, 2);
12029 });
12030}
12031
12032#[gpui::test]
12033async fn test_completion_mode(cx: &mut TestAppContext) {
12034 init_test(cx, |_| {});
12035 let mut cx = EditorLspTestContext::new_rust(
12036 lsp::ServerCapabilities {
12037 completion_provider: Some(lsp::CompletionOptions {
12038 resolve_provider: Some(true),
12039 ..Default::default()
12040 }),
12041 ..Default::default()
12042 },
12043 cx,
12044 )
12045 .await;
12046
12047 struct Run {
12048 run_description: &'static str,
12049 initial_state: String,
12050 buffer_marked_text: String,
12051 completion_label: &'static str,
12052 completion_text: &'static str,
12053 expected_with_insert_mode: String,
12054 expected_with_replace_mode: String,
12055 expected_with_replace_subsequence_mode: String,
12056 expected_with_replace_suffix_mode: String,
12057 }
12058
12059 let runs = [
12060 Run {
12061 run_description: "Start of word matches completion text",
12062 initial_state: "before ediˇ after".into(),
12063 buffer_marked_text: "before <edi|> after".into(),
12064 completion_label: "editor",
12065 completion_text: "editor",
12066 expected_with_insert_mode: "before editorˇ after".into(),
12067 expected_with_replace_mode: "before editorˇ after".into(),
12068 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12069 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12070 },
12071 Run {
12072 run_description: "Accept same text at the middle of the word",
12073 initial_state: "before ediˇtor after".into(),
12074 buffer_marked_text: "before <edi|tor> after".into(),
12075 completion_label: "editor",
12076 completion_text: "editor",
12077 expected_with_insert_mode: "before editorˇtor after".into(),
12078 expected_with_replace_mode: "before editorˇ after".into(),
12079 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12080 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12081 },
12082 Run {
12083 run_description: "End of word matches completion text -- cursor at end",
12084 initial_state: "before torˇ after".into(),
12085 buffer_marked_text: "before <tor|> after".into(),
12086 completion_label: "editor",
12087 completion_text: "editor",
12088 expected_with_insert_mode: "before editorˇ after".into(),
12089 expected_with_replace_mode: "before editorˇ after".into(),
12090 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12091 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12092 },
12093 Run {
12094 run_description: "End of word matches completion text -- cursor at start",
12095 initial_state: "before ˇtor after".into(),
12096 buffer_marked_text: "before <|tor> after".into(),
12097 completion_label: "editor",
12098 completion_text: "editor",
12099 expected_with_insert_mode: "before editorˇtor after".into(),
12100 expected_with_replace_mode: "before editorˇ after".into(),
12101 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12102 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12103 },
12104 Run {
12105 run_description: "Prepend text containing whitespace",
12106 initial_state: "pˇfield: bool".into(),
12107 buffer_marked_text: "<p|field>: bool".into(),
12108 completion_label: "pub ",
12109 completion_text: "pub ",
12110 expected_with_insert_mode: "pub ˇfield: bool".into(),
12111 expected_with_replace_mode: "pub ˇ: bool".into(),
12112 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
12113 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
12114 },
12115 Run {
12116 run_description: "Add element to start of list",
12117 initial_state: "[element_ˇelement_2]".into(),
12118 buffer_marked_text: "[<element_|element_2>]".into(),
12119 completion_label: "element_1",
12120 completion_text: "element_1",
12121 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
12122 expected_with_replace_mode: "[element_1ˇ]".into(),
12123 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
12124 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
12125 },
12126 Run {
12127 run_description: "Add element to start of list -- first and second elements are equal",
12128 initial_state: "[elˇelement]".into(),
12129 buffer_marked_text: "[<el|element>]".into(),
12130 completion_label: "element",
12131 completion_text: "element",
12132 expected_with_insert_mode: "[elementˇelement]".into(),
12133 expected_with_replace_mode: "[elementˇ]".into(),
12134 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
12135 expected_with_replace_suffix_mode: "[elementˇ]".into(),
12136 },
12137 Run {
12138 run_description: "Ends with matching suffix",
12139 initial_state: "SubˇError".into(),
12140 buffer_marked_text: "<Sub|Error>".into(),
12141 completion_label: "SubscriptionError",
12142 completion_text: "SubscriptionError",
12143 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
12144 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
12145 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
12146 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
12147 },
12148 Run {
12149 run_description: "Suffix is a subsequence -- contiguous",
12150 initial_state: "SubˇErr".into(),
12151 buffer_marked_text: "<Sub|Err>".into(),
12152 completion_label: "SubscriptionError",
12153 completion_text: "SubscriptionError",
12154 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
12155 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
12156 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
12157 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
12158 },
12159 Run {
12160 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
12161 initial_state: "Suˇscrirr".into(),
12162 buffer_marked_text: "<Su|scrirr>".into(),
12163 completion_label: "SubscriptionError",
12164 completion_text: "SubscriptionError",
12165 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
12166 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
12167 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
12168 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
12169 },
12170 Run {
12171 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
12172 initial_state: "foo(indˇix)".into(),
12173 buffer_marked_text: "foo(<ind|ix>)".into(),
12174 completion_label: "node_index",
12175 completion_text: "node_index",
12176 expected_with_insert_mode: "foo(node_indexˇix)".into(),
12177 expected_with_replace_mode: "foo(node_indexˇ)".into(),
12178 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
12179 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
12180 },
12181 Run {
12182 run_description: "Replace range ends before cursor - should extend to cursor",
12183 initial_state: "before editˇo after".into(),
12184 buffer_marked_text: "before <{ed}>it|o after".into(),
12185 completion_label: "editor",
12186 completion_text: "editor",
12187 expected_with_insert_mode: "before editorˇo after".into(),
12188 expected_with_replace_mode: "before editorˇo after".into(),
12189 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
12190 expected_with_replace_suffix_mode: "before editorˇo after".into(),
12191 },
12192 Run {
12193 run_description: "Uses label for suffix matching",
12194 initial_state: "before ediˇtor after".into(),
12195 buffer_marked_text: "before <edi|tor> after".into(),
12196 completion_label: "editor",
12197 completion_text: "editor()",
12198 expected_with_insert_mode: "before editor()ˇtor after".into(),
12199 expected_with_replace_mode: "before editor()ˇ after".into(),
12200 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
12201 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
12202 },
12203 Run {
12204 run_description: "Case insensitive subsequence and suffix matching",
12205 initial_state: "before EDiˇtoR after".into(),
12206 buffer_marked_text: "before <EDi|toR> after".into(),
12207 completion_label: "editor",
12208 completion_text: "editor",
12209 expected_with_insert_mode: "before editorˇtoR after".into(),
12210 expected_with_replace_mode: "before editorˇ after".into(),
12211 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
12212 expected_with_replace_suffix_mode: "before editorˇ after".into(),
12213 },
12214 ];
12215
12216 for run in runs {
12217 let run_variations = [
12218 (LspInsertMode::Insert, run.expected_with_insert_mode),
12219 (LspInsertMode::Replace, run.expected_with_replace_mode),
12220 (
12221 LspInsertMode::ReplaceSubsequence,
12222 run.expected_with_replace_subsequence_mode,
12223 ),
12224 (
12225 LspInsertMode::ReplaceSuffix,
12226 run.expected_with_replace_suffix_mode,
12227 ),
12228 ];
12229
12230 for (lsp_insert_mode, expected_text) in run_variations {
12231 eprintln!(
12232 "run = {:?}, mode = {lsp_insert_mode:.?}",
12233 run.run_description,
12234 );
12235
12236 update_test_language_settings(&mut cx, |settings| {
12237 settings.defaults.completions = Some(CompletionSettings {
12238 lsp_insert_mode,
12239 words: WordsCompletionMode::Disabled,
12240 lsp: true,
12241 lsp_fetch_timeout_ms: 0,
12242 });
12243 });
12244
12245 cx.set_state(&run.initial_state);
12246 cx.update_editor(|editor, window, cx| {
12247 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12248 });
12249
12250 let counter = Arc::new(AtomicUsize::new(0));
12251 handle_completion_request_with_insert_and_replace(
12252 &mut cx,
12253 &run.buffer_marked_text,
12254 vec![(run.completion_label, run.completion_text)],
12255 counter.clone(),
12256 )
12257 .await;
12258 cx.condition(|editor, _| editor.context_menu_visible())
12259 .await;
12260 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12261
12262 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12263 editor
12264 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12265 .unwrap()
12266 });
12267 cx.assert_editor_state(&expected_text);
12268 handle_resolve_completion_request(&mut cx, None).await;
12269 apply_additional_edits.await.unwrap();
12270 }
12271 }
12272}
12273
12274#[gpui::test]
12275async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
12276 init_test(cx, |_| {});
12277 let mut cx = EditorLspTestContext::new_rust(
12278 lsp::ServerCapabilities {
12279 completion_provider: Some(lsp::CompletionOptions {
12280 resolve_provider: Some(true),
12281 ..Default::default()
12282 }),
12283 ..Default::default()
12284 },
12285 cx,
12286 )
12287 .await;
12288
12289 let initial_state = "SubˇError";
12290 let buffer_marked_text = "<Sub|Error>";
12291 let completion_text = "SubscriptionError";
12292 let expected_with_insert_mode = "SubscriptionErrorˇError";
12293 let expected_with_replace_mode = "SubscriptionErrorˇ";
12294
12295 update_test_language_settings(&mut cx, |settings| {
12296 settings.defaults.completions = Some(CompletionSettings {
12297 words: WordsCompletionMode::Disabled,
12298 // set the opposite here to ensure that the action is overriding the default behavior
12299 lsp_insert_mode: LspInsertMode::Insert,
12300 lsp: true,
12301 lsp_fetch_timeout_ms: 0,
12302 });
12303 });
12304
12305 cx.set_state(initial_state);
12306 cx.update_editor(|editor, window, cx| {
12307 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12308 });
12309
12310 let counter = Arc::new(AtomicUsize::new(0));
12311 handle_completion_request_with_insert_and_replace(
12312 &mut cx,
12313 buffer_marked_text,
12314 vec![(completion_text, completion_text)],
12315 counter.clone(),
12316 )
12317 .await;
12318 cx.condition(|editor, _| editor.context_menu_visible())
12319 .await;
12320 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12321
12322 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12323 editor
12324 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12325 .unwrap()
12326 });
12327 cx.assert_editor_state(expected_with_replace_mode);
12328 handle_resolve_completion_request(&mut cx, None).await;
12329 apply_additional_edits.await.unwrap();
12330
12331 update_test_language_settings(&mut cx, |settings| {
12332 settings.defaults.completions = Some(CompletionSettings {
12333 words: WordsCompletionMode::Disabled,
12334 // set the opposite here to ensure that the action is overriding the default behavior
12335 lsp_insert_mode: LspInsertMode::Replace,
12336 lsp: true,
12337 lsp_fetch_timeout_ms: 0,
12338 });
12339 });
12340
12341 cx.set_state(initial_state);
12342 cx.update_editor(|editor, window, cx| {
12343 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12344 });
12345 handle_completion_request_with_insert_and_replace(
12346 &mut cx,
12347 buffer_marked_text,
12348 vec![(completion_text, completion_text)],
12349 counter.clone(),
12350 )
12351 .await;
12352 cx.condition(|editor, _| editor.context_menu_visible())
12353 .await;
12354 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12355
12356 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12357 editor
12358 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
12359 .unwrap()
12360 });
12361 cx.assert_editor_state(expected_with_insert_mode);
12362 handle_resolve_completion_request(&mut cx, None).await;
12363 apply_additional_edits.await.unwrap();
12364}
12365
12366#[gpui::test]
12367async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
12368 init_test(cx, |_| {});
12369 let mut cx = EditorLspTestContext::new_rust(
12370 lsp::ServerCapabilities {
12371 completion_provider: Some(lsp::CompletionOptions {
12372 resolve_provider: Some(true),
12373 ..Default::default()
12374 }),
12375 ..Default::default()
12376 },
12377 cx,
12378 )
12379 .await;
12380
12381 // scenario: surrounding text matches completion text
12382 let completion_text = "to_offset";
12383 let initial_state = indoc! {"
12384 1. buf.to_offˇsuffix
12385 2. buf.to_offˇsuf
12386 3. buf.to_offˇfix
12387 4. buf.to_offˇ
12388 5. into_offˇensive
12389 6. ˇsuffix
12390 7. let ˇ //
12391 8. aaˇzz
12392 9. buf.to_off«zzzzzˇ»suffix
12393 10. buf.«ˇzzzzz»suffix
12394 11. to_off«ˇzzzzz»
12395
12396 buf.to_offˇsuffix // newest cursor
12397 "};
12398 let completion_marked_buffer = indoc! {"
12399 1. buf.to_offsuffix
12400 2. buf.to_offsuf
12401 3. buf.to_offfix
12402 4. buf.to_off
12403 5. into_offensive
12404 6. suffix
12405 7. let //
12406 8. aazz
12407 9. buf.to_offzzzzzsuffix
12408 10. buf.zzzzzsuffix
12409 11. to_offzzzzz
12410
12411 buf.<to_off|suffix> // newest cursor
12412 "};
12413 let expected = indoc! {"
12414 1. buf.to_offsetˇ
12415 2. buf.to_offsetˇsuf
12416 3. buf.to_offsetˇfix
12417 4. buf.to_offsetˇ
12418 5. into_offsetˇensive
12419 6. to_offsetˇsuffix
12420 7. let to_offsetˇ //
12421 8. aato_offsetˇzz
12422 9. buf.to_offsetˇ
12423 10. buf.to_offsetˇsuffix
12424 11. to_offsetˇ
12425
12426 buf.to_offsetˇ // newest cursor
12427 "};
12428 cx.set_state(initial_state);
12429 cx.update_editor(|editor, window, cx| {
12430 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12431 });
12432 handle_completion_request_with_insert_and_replace(
12433 &mut cx,
12434 completion_marked_buffer,
12435 vec![(completion_text, completion_text)],
12436 Arc::new(AtomicUsize::new(0)),
12437 )
12438 .await;
12439 cx.condition(|editor, _| editor.context_menu_visible())
12440 .await;
12441 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12442 editor
12443 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12444 .unwrap()
12445 });
12446 cx.assert_editor_state(expected);
12447 handle_resolve_completion_request(&mut cx, None).await;
12448 apply_additional_edits.await.unwrap();
12449
12450 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
12451 let completion_text = "foo_and_bar";
12452 let initial_state = indoc! {"
12453 1. ooanbˇ
12454 2. zooanbˇ
12455 3. ooanbˇz
12456 4. zooanbˇz
12457 5. ooanˇ
12458 6. oanbˇ
12459
12460 ooanbˇ
12461 "};
12462 let completion_marked_buffer = indoc! {"
12463 1. ooanb
12464 2. zooanb
12465 3. ooanbz
12466 4. zooanbz
12467 5. ooan
12468 6. oanb
12469
12470 <ooanb|>
12471 "};
12472 let expected = indoc! {"
12473 1. foo_and_barˇ
12474 2. zfoo_and_barˇ
12475 3. foo_and_barˇz
12476 4. zfoo_and_barˇz
12477 5. ooanfoo_and_barˇ
12478 6. oanbfoo_and_barˇ
12479
12480 foo_and_barˇ
12481 "};
12482 cx.set_state(initial_state);
12483 cx.update_editor(|editor, window, cx| {
12484 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12485 });
12486 handle_completion_request_with_insert_and_replace(
12487 &mut cx,
12488 completion_marked_buffer,
12489 vec![(completion_text, completion_text)],
12490 Arc::new(AtomicUsize::new(0)),
12491 )
12492 .await;
12493 cx.condition(|editor, _| editor.context_menu_visible())
12494 .await;
12495 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12496 editor
12497 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12498 .unwrap()
12499 });
12500 cx.assert_editor_state(expected);
12501 handle_resolve_completion_request(&mut cx, None).await;
12502 apply_additional_edits.await.unwrap();
12503
12504 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
12505 // (expects the same as if it was inserted at the end)
12506 let completion_text = "foo_and_bar";
12507 let initial_state = indoc! {"
12508 1. ooˇanb
12509 2. zooˇanb
12510 3. ooˇanbz
12511 4. zooˇanbz
12512
12513 ooˇanb
12514 "};
12515 let completion_marked_buffer = indoc! {"
12516 1. ooanb
12517 2. zooanb
12518 3. ooanbz
12519 4. zooanbz
12520
12521 <oo|anb>
12522 "};
12523 let expected = indoc! {"
12524 1. foo_and_barˇ
12525 2. zfoo_and_barˇ
12526 3. foo_and_barˇz
12527 4. zfoo_and_barˇz
12528
12529 foo_and_barˇ
12530 "};
12531 cx.set_state(initial_state);
12532 cx.update_editor(|editor, window, cx| {
12533 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12534 });
12535 handle_completion_request_with_insert_and_replace(
12536 &mut cx,
12537 completion_marked_buffer,
12538 vec![(completion_text, completion_text)],
12539 Arc::new(AtomicUsize::new(0)),
12540 )
12541 .await;
12542 cx.condition(|editor, _| editor.context_menu_visible())
12543 .await;
12544 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12545 editor
12546 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12547 .unwrap()
12548 });
12549 cx.assert_editor_state(expected);
12550 handle_resolve_completion_request(&mut cx, None).await;
12551 apply_additional_edits.await.unwrap();
12552}
12553
12554// This used to crash
12555#[gpui::test]
12556async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
12557 init_test(cx, |_| {});
12558
12559 let buffer_text = indoc! {"
12560 fn main() {
12561 10.satu;
12562
12563 //
12564 // separate cursors so they open in different excerpts (manually reproducible)
12565 //
12566
12567 10.satu20;
12568 }
12569 "};
12570 let multibuffer_text_with_selections = indoc! {"
12571 fn main() {
12572 10.satuˇ;
12573
12574 //
12575
12576 //
12577
12578 10.satuˇ20;
12579 }
12580 "};
12581 let expected_multibuffer = indoc! {"
12582 fn main() {
12583 10.saturating_sub()ˇ;
12584
12585 //
12586
12587 //
12588
12589 10.saturating_sub()ˇ;
12590 }
12591 "};
12592
12593 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
12594 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
12595
12596 let fs = FakeFs::new(cx.executor());
12597 fs.insert_tree(
12598 path!("/a"),
12599 json!({
12600 "main.rs": buffer_text,
12601 }),
12602 )
12603 .await;
12604
12605 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12606 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12607 language_registry.add(rust_lang());
12608 let mut fake_servers = language_registry.register_fake_lsp(
12609 "Rust",
12610 FakeLspAdapter {
12611 capabilities: lsp::ServerCapabilities {
12612 completion_provider: Some(lsp::CompletionOptions {
12613 resolve_provider: None,
12614 ..lsp::CompletionOptions::default()
12615 }),
12616 ..lsp::ServerCapabilities::default()
12617 },
12618 ..FakeLspAdapter::default()
12619 },
12620 );
12621 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12622 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12623 let buffer = project
12624 .update(cx, |project, cx| {
12625 project.open_local_buffer(path!("/a/main.rs"), cx)
12626 })
12627 .await
12628 .unwrap();
12629
12630 let multi_buffer = cx.new(|cx| {
12631 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
12632 multi_buffer.push_excerpts(
12633 buffer.clone(),
12634 [ExcerptRange::new(0..first_excerpt_end)],
12635 cx,
12636 );
12637 multi_buffer.push_excerpts(
12638 buffer.clone(),
12639 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
12640 cx,
12641 );
12642 multi_buffer
12643 });
12644
12645 let editor = workspace
12646 .update(cx, |_, window, cx| {
12647 cx.new(|cx| {
12648 Editor::new(
12649 EditorMode::Full {
12650 scale_ui_elements_with_buffer_font_size: false,
12651 show_active_line_background: false,
12652 sized_by_content: false,
12653 },
12654 multi_buffer.clone(),
12655 Some(project.clone()),
12656 window,
12657 cx,
12658 )
12659 })
12660 })
12661 .unwrap();
12662
12663 let pane = workspace
12664 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12665 .unwrap();
12666 pane.update_in(cx, |pane, window, cx| {
12667 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
12668 });
12669
12670 let fake_server = fake_servers.next().await.unwrap();
12671
12672 editor.update_in(cx, |editor, window, cx| {
12673 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12674 s.select_ranges([
12675 Point::new(1, 11)..Point::new(1, 11),
12676 Point::new(7, 11)..Point::new(7, 11),
12677 ])
12678 });
12679
12680 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
12681 });
12682
12683 editor.update_in(cx, |editor, window, cx| {
12684 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12685 });
12686
12687 fake_server
12688 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12689 let completion_item = lsp::CompletionItem {
12690 label: "saturating_sub()".into(),
12691 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
12692 lsp::InsertReplaceEdit {
12693 new_text: "saturating_sub()".to_owned(),
12694 insert: lsp::Range::new(
12695 lsp::Position::new(7, 7),
12696 lsp::Position::new(7, 11),
12697 ),
12698 replace: lsp::Range::new(
12699 lsp::Position::new(7, 7),
12700 lsp::Position::new(7, 13),
12701 ),
12702 },
12703 )),
12704 ..lsp::CompletionItem::default()
12705 };
12706
12707 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
12708 })
12709 .next()
12710 .await
12711 .unwrap();
12712
12713 cx.condition(&editor, |editor, _| editor.context_menu_visible())
12714 .await;
12715
12716 editor
12717 .update_in(cx, |editor, window, cx| {
12718 editor
12719 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12720 .unwrap()
12721 })
12722 .await
12723 .unwrap();
12724
12725 editor.update(cx, |editor, cx| {
12726 assert_text_with_selections(editor, expected_multibuffer, cx);
12727 })
12728}
12729
12730#[gpui::test]
12731async fn test_completion(cx: &mut TestAppContext) {
12732 init_test(cx, |_| {});
12733
12734 let mut cx = EditorLspTestContext::new_rust(
12735 lsp::ServerCapabilities {
12736 completion_provider: Some(lsp::CompletionOptions {
12737 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12738 resolve_provider: Some(true),
12739 ..Default::default()
12740 }),
12741 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12742 ..Default::default()
12743 },
12744 cx,
12745 )
12746 .await;
12747 let counter = Arc::new(AtomicUsize::new(0));
12748
12749 cx.set_state(indoc! {"
12750 oneˇ
12751 two
12752 three
12753 "});
12754 cx.simulate_keystroke(".");
12755 handle_completion_request(
12756 indoc! {"
12757 one.|<>
12758 two
12759 three
12760 "},
12761 vec!["first_completion", "second_completion"],
12762 true,
12763 counter.clone(),
12764 &mut cx,
12765 )
12766 .await;
12767 cx.condition(|editor, _| editor.context_menu_visible())
12768 .await;
12769 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12770
12771 let _handler = handle_signature_help_request(
12772 &mut cx,
12773 lsp::SignatureHelp {
12774 signatures: vec![lsp::SignatureInformation {
12775 label: "test signature".to_string(),
12776 documentation: None,
12777 parameters: Some(vec![lsp::ParameterInformation {
12778 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
12779 documentation: None,
12780 }]),
12781 active_parameter: None,
12782 }],
12783 active_signature: None,
12784 active_parameter: None,
12785 },
12786 );
12787 cx.update_editor(|editor, window, cx| {
12788 assert!(
12789 !editor.signature_help_state.is_shown(),
12790 "No signature help was called for"
12791 );
12792 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12793 });
12794 cx.run_until_parked();
12795 cx.update_editor(|editor, _, _| {
12796 assert!(
12797 !editor.signature_help_state.is_shown(),
12798 "No signature help should be shown when completions menu is open"
12799 );
12800 });
12801
12802 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12803 editor.context_menu_next(&Default::default(), window, cx);
12804 editor
12805 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12806 .unwrap()
12807 });
12808 cx.assert_editor_state(indoc! {"
12809 one.second_completionˇ
12810 two
12811 three
12812 "});
12813
12814 handle_resolve_completion_request(
12815 &mut cx,
12816 Some(vec![
12817 (
12818 //This overlaps with the primary completion edit which is
12819 //misbehavior from the LSP spec, test that we filter it out
12820 indoc! {"
12821 one.second_ˇcompletion
12822 two
12823 threeˇ
12824 "},
12825 "overlapping additional edit",
12826 ),
12827 (
12828 indoc! {"
12829 one.second_completion
12830 two
12831 threeˇ
12832 "},
12833 "\nadditional edit",
12834 ),
12835 ]),
12836 )
12837 .await;
12838 apply_additional_edits.await.unwrap();
12839 cx.assert_editor_state(indoc! {"
12840 one.second_completionˇ
12841 two
12842 three
12843 additional edit
12844 "});
12845
12846 cx.set_state(indoc! {"
12847 one.second_completion
12848 twoˇ
12849 threeˇ
12850 additional edit
12851 "});
12852 cx.simulate_keystroke(" ");
12853 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12854 cx.simulate_keystroke("s");
12855 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12856
12857 cx.assert_editor_state(indoc! {"
12858 one.second_completion
12859 two sˇ
12860 three sˇ
12861 additional edit
12862 "});
12863 handle_completion_request(
12864 indoc! {"
12865 one.second_completion
12866 two s
12867 three <s|>
12868 additional edit
12869 "},
12870 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12871 true,
12872 counter.clone(),
12873 &mut cx,
12874 )
12875 .await;
12876 cx.condition(|editor, _| editor.context_menu_visible())
12877 .await;
12878 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12879
12880 cx.simulate_keystroke("i");
12881
12882 handle_completion_request(
12883 indoc! {"
12884 one.second_completion
12885 two si
12886 three <si|>
12887 additional edit
12888 "},
12889 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12890 true,
12891 counter.clone(),
12892 &mut cx,
12893 )
12894 .await;
12895 cx.condition(|editor, _| editor.context_menu_visible())
12896 .await;
12897 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12898
12899 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12900 editor
12901 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12902 .unwrap()
12903 });
12904 cx.assert_editor_state(indoc! {"
12905 one.second_completion
12906 two sixth_completionˇ
12907 three sixth_completionˇ
12908 additional edit
12909 "});
12910
12911 apply_additional_edits.await.unwrap();
12912
12913 update_test_language_settings(&mut cx, |settings| {
12914 settings.defaults.show_completions_on_input = Some(false);
12915 });
12916 cx.set_state("editorˇ");
12917 cx.simulate_keystroke(".");
12918 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12919 cx.simulate_keystrokes("c l o");
12920 cx.assert_editor_state("editor.cloˇ");
12921 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12922 cx.update_editor(|editor, window, cx| {
12923 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12924 });
12925 handle_completion_request(
12926 "editor.<clo|>",
12927 vec!["close", "clobber"],
12928 true,
12929 counter.clone(),
12930 &mut cx,
12931 )
12932 .await;
12933 cx.condition(|editor, _| editor.context_menu_visible())
12934 .await;
12935 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
12936
12937 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12938 editor
12939 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12940 .unwrap()
12941 });
12942 cx.assert_editor_state("editor.clobberˇ");
12943 handle_resolve_completion_request(&mut cx, None).await;
12944 apply_additional_edits.await.unwrap();
12945}
12946
12947#[gpui::test]
12948async fn test_completion_reuse(cx: &mut TestAppContext) {
12949 init_test(cx, |_| {});
12950
12951 let mut cx = EditorLspTestContext::new_rust(
12952 lsp::ServerCapabilities {
12953 completion_provider: Some(lsp::CompletionOptions {
12954 trigger_characters: Some(vec![".".to_string()]),
12955 ..Default::default()
12956 }),
12957 ..Default::default()
12958 },
12959 cx,
12960 )
12961 .await;
12962
12963 let counter = Arc::new(AtomicUsize::new(0));
12964 cx.set_state("objˇ");
12965 cx.simulate_keystroke(".");
12966
12967 // Initial completion request returns complete results
12968 let is_incomplete = false;
12969 handle_completion_request(
12970 "obj.|<>",
12971 vec!["a", "ab", "abc"],
12972 is_incomplete,
12973 counter.clone(),
12974 &mut cx,
12975 )
12976 .await;
12977 cx.run_until_parked();
12978 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12979 cx.assert_editor_state("obj.ˇ");
12980 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12981
12982 // Type "a" - filters existing completions
12983 cx.simulate_keystroke("a");
12984 cx.run_until_parked();
12985 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12986 cx.assert_editor_state("obj.aˇ");
12987 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12988
12989 // Type "b" - filters existing completions
12990 cx.simulate_keystroke("b");
12991 cx.run_until_parked();
12992 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12993 cx.assert_editor_state("obj.abˇ");
12994 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12995
12996 // Type "c" - filters existing completions
12997 cx.simulate_keystroke("c");
12998 cx.run_until_parked();
12999 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13000 cx.assert_editor_state("obj.abcˇ");
13001 check_displayed_completions(vec!["abc"], &mut cx);
13002
13003 // Backspace to delete "c" - filters existing completions
13004 cx.update_editor(|editor, window, cx| {
13005 editor.backspace(&Backspace, window, cx);
13006 });
13007 cx.run_until_parked();
13008 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13009 cx.assert_editor_state("obj.abˇ");
13010 check_displayed_completions(vec!["ab", "abc"], &mut cx);
13011
13012 // Moving cursor to the left dismisses menu.
13013 cx.update_editor(|editor, window, cx| {
13014 editor.move_left(&MoveLeft, window, cx);
13015 });
13016 cx.run_until_parked();
13017 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13018 cx.assert_editor_state("obj.aˇb");
13019 cx.update_editor(|editor, _, _| {
13020 assert_eq!(editor.context_menu_visible(), false);
13021 });
13022
13023 // Type "b" - new request
13024 cx.simulate_keystroke("b");
13025 let is_incomplete = false;
13026 handle_completion_request(
13027 "obj.<ab|>a",
13028 vec!["ab", "abc"],
13029 is_incomplete,
13030 counter.clone(),
13031 &mut cx,
13032 )
13033 .await;
13034 cx.run_until_parked();
13035 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13036 cx.assert_editor_state("obj.abˇb");
13037 check_displayed_completions(vec!["ab", "abc"], &mut cx);
13038
13039 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
13040 cx.update_editor(|editor, window, cx| {
13041 editor.backspace(&Backspace, window, cx);
13042 });
13043 let is_incomplete = false;
13044 handle_completion_request(
13045 "obj.<a|>b",
13046 vec!["a", "ab", "abc"],
13047 is_incomplete,
13048 counter.clone(),
13049 &mut cx,
13050 )
13051 .await;
13052 cx.run_until_parked();
13053 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
13054 cx.assert_editor_state("obj.aˇb");
13055 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
13056
13057 // Backspace to delete "a" - dismisses menu.
13058 cx.update_editor(|editor, window, cx| {
13059 editor.backspace(&Backspace, window, cx);
13060 });
13061 cx.run_until_parked();
13062 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
13063 cx.assert_editor_state("obj.ˇb");
13064 cx.update_editor(|editor, _, _| {
13065 assert_eq!(editor.context_menu_visible(), false);
13066 });
13067}
13068
13069#[gpui::test]
13070async fn test_word_completion(cx: &mut TestAppContext) {
13071 let lsp_fetch_timeout_ms = 10;
13072 init_test(cx, |language_settings| {
13073 language_settings.defaults.completions = Some(CompletionSettings {
13074 words: WordsCompletionMode::Fallback,
13075 lsp: true,
13076 lsp_fetch_timeout_ms: 10,
13077 lsp_insert_mode: LspInsertMode::Insert,
13078 });
13079 });
13080
13081 let mut cx = EditorLspTestContext::new_rust(
13082 lsp::ServerCapabilities {
13083 completion_provider: Some(lsp::CompletionOptions {
13084 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13085 ..lsp::CompletionOptions::default()
13086 }),
13087 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13088 ..lsp::ServerCapabilities::default()
13089 },
13090 cx,
13091 )
13092 .await;
13093
13094 let throttle_completions = Arc::new(AtomicBool::new(false));
13095
13096 let lsp_throttle_completions = throttle_completions.clone();
13097 let _completion_requests_handler =
13098 cx.lsp
13099 .server
13100 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
13101 let lsp_throttle_completions = lsp_throttle_completions.clone();
13102 let cx = cx.clone();
13103 async move {
13104 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
13105 cx.background_executor()
13106 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
13107 .await;
13108 }
13109 Ok(Some(lsp::CompletionResponse::Array(vec![
13110 lsp::CompletionItem {
13111 label: "first".into(),
13112 ..lsp::CompletionItem::default()
13113 },
13114 lsp::CompletionItem {
13115 label: "last".into(),
13116 ..lsp::CompletionItem::default()
13117 },
13118 ])))
13119 }
13120 });
13121
13122 cx.set_state(indoc! {"
13123 oneˇ
13124 two
13125 three
13126 "});
13127 cx.simulate_keystroke(".");
13128 cx.executor().run_until_parked();
13129 cx.condition(|editor, _| editor.context_menu_visible())
13130 .await;
13131 cx.update_editor(|editor, window, cx| {
13132 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13133 {
13134 assert_eq!(
13135 completion_menu_entries(menu),
13136 &["first", "last"],
13137 "When LSP server is fast to reply, no fallback word completions are used"
13138 );
13139 } else {
13140 panic!("expected completion menu to be open");
13141 }
13142 editor.cancel(&Cancel, window, cx);
13143 });
13144 cx.executor().run_until_parked();
13145 cx.condition(|editor, _| !editor.context_menu_visible())
13146 .await;
13147
13148 throttle_completions.store(true, atomic::Ordering::Release);
13149 cx.simulate_keystroke(".");
13150 cx.executor()
13151 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
13152 cx.executor().run_until_parked();
13153 cx.condition(|editor, _| editor.context_menu_visible())
13154 .await;
13155 cx.update_editor(|editor, _, _| {
13156 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13157 {
13158 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
13159 "When LSP server is slow, document words can be shown instead, if configured accordingly");
13160 } else {
13161 panic!("expected completion menu to be open");
13162 }
13163 });
13164}
13165
13166#[gpui::test]
13167async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
13168 init_test(cx, |language_settings| {
13169 language_settings.defaults.completions = Some(CompletionSettings {
13170 words: WordsCompletionMode::Enabled,
13171 lsp: true,
13172 lsp_fetch_timeout_ms: 0,
13173 lsp_insert_mode: LspInsertMode::Insert,
13174 });
13175 });
13176
13177 let mut cx = EditorLspTestContext::new_rust(
13178 lsp::ServerCapabilities {
13179 completion_provider: Some(lsp::CompletionOptions {
13180 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13181 ..lsp::CompletionOptions::default()
13182 }),
13183 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13184 ..lsp::ServerCapabilities::default()
13185 },
13186 cx,
13187 )
13188 .await;
13189
13190 let _completion_requests_handler =
13191 cx.lsp
13192 .server
13193 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
13194 Ok(Some(lsp::CompletionResponse::Array(vec![
13195 lsp::CompletionItem {
13196 label: "first".into(),
13197 ..lsp::CompletionItem::default()
13198 },
13199 lsp::CompletionItem {
13200 label: "last".into(),
13201 ..lsp::CompletionItem::default()
13202 },
13203 ])))
13204 });
13205
13206 cx.set_state(indoc! {"ˇ
13207 first
13208 last
13209 second
13210 "});
13211 cx.simulate_keystroke(".");
13212 cx.executor().run_until_parked();
13213 cx.condition(|editor, _| editor.context_menu_visible())
13214 .await;
13215 cx.update_editor(|editor, _, _| {
13216 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13217 {
13218 assert_eq!(
13219 completion_menu_entries(menu),
13220 &["first", "last", "second"],
13221 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
13222 );
13223 } else {
13224 panic!("expected completion menu to be open");
13225 }
13226 });
13227}
13228
13229#[gpui::test]
13230async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
13231 init_test(cx, |language_settings| {
13232 language_settings.defaults.completions = Some(CompletionSettings {
13233 words: WordsCompletionMode::Disabled,
13234 lsp: true,
13235 lsp_fetch_timeout_ms: 0,
13236 lsp_insert_mode: LspInsertMode::Insert,
13237 });
13238 });
13239
13240 let mut cx = EditorLspTestContext::new_rust(
13241 lsp::ServerCapabilities {
13242 completion_provider: Some(lsp::CompletionOptions {
13243 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13244 ..lsp::CompletionOptions::default()
13245 }),
13246 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13247 ..lsp::ServerCapabilities::default()
13248 },
13249 cx,
13250 )
13251 .await;
13252
13253 let _completion_requests_handler =
13254 cx.lsp
13255 .server
13256 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
13257 panic!("LSP completions should not be queried when dealing with word completions")
13258 });
13259
13260 cx.set_state(indoc! {"ˇ
13261 first
13262 last
13263 second
13264 "});
13265 cx.update_editor(|editor, window, cx| {
13266 editor.show_word_completions(&ShowWordCompletions, window, cx);
13267 });
13268 cx.executor().run_until_parked();
13269 cx.condition(|editor, _| editor.context_menu_visible())
13270 .await;
13271 cx.update_editor(|editor, _, _| {
13272 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13273 {
13274 assert_eq!(
13275 completion_menu_entries(menu),
13276 &["first", "last", "second"],
13277 "`ShowWordCompletions` action should show word completions"
13278 );
13279 } else {
13280 panic!("expected completion menu to be open");
13281 }
13282 });
13283
13284 cx.simulate_keystroke("l");
13285 cx.executor().run_until_parked();
13286 cx.condition(|editor, _| editor.context_menu_visible())
13287 .await;
13288 cx.update_editor(|editor, _, _| {
13289 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13290 {
13291 assert_eq!(
13292 completion_menu_entries(menu),
13293 &["last"],
13294 "After showing word completions, further editing should filter them and not query the LSP"
13295 );
13296 } else {
13297 panic!("expected completion menu to be open");
13298 }
13299 });
13300}
13301
13302#[gpui::test]
13303async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
13304 init_test(cx, |language_settings| {
13305 language_settings.defaults.completions = Some(CompletionSettings {
13306 words: WordsCompletionMode::Fallback,
13307 lsp: false,
13308 lsp_fetch_timeout_ms: 0,
13309 lsp_insert_mode: LspInsertMode::Insert,
13310 });
13311 });
13312
13313 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13314
13315 cx.set_state(indoc! {"ˇ
13316 0_usize
13317 let
13318 33
13319 4.5f32
13320 "});
13321 cx.update_editor(|editor, window, cx| {
13322 editor.show_completions(&ShowCompletions::default(), window, cx);
13323 });
13324 cx.executor().run_until_parked();
13325 cx.condition(|editor, _| editor.context_menu_visible())
13326 .await;
13327 cx.update_editor(|editor, window, cx| {
13328 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13329 {
13330 assert_eq!(
13331 completion_menu_entries(menu),
13332 &["let"],
13333 "With no digits in the completion query, no digits should be in the word completions"
13334 );
13335 } else {
13336 panic!("expected completion menu to be open");
13337 }
13338 editor.cancel(&Cancel, window, cx);
13339 });
13340
13341 cx.set_state(indoc! {"3ˇ
13342 0_usize
13343 let
13344 3
13345 33.35f32
13346 "});
13347 cx.update_editor(|editor, window, cx| {
13348 editor.show_completions(&ShowCompletions::default(), window, cx);
13349 });
13350 cx.executor().run_until_parked();
13351 cx.condition(|editor, _| editor.context_menu_visible())
13352 .await;
13353 cx.update_editor(|editor, _, _| {
13354 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13355 {
13356 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
13357 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
13358 } else {
13359 panic!("expected completion menu to be open");
13360 }
13361 });
13362}
13363
13364fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
13365 let position = || lsp::Position {
13366 line: params.text_document_position.position.line,
13367 character: params.text_document_position.position.character,
13368 };
13369 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13370 range: lsp::Range {
13371 start: position(),
13372 end: position(),
13373 },
13374 new_text: text.to_string(),
13375 }))
13376}
13377
13378#[gpui::test]
13379async fn test_multiline_completion(cx: &mut TestAppContext) {
13380 init_test(cx, |_| {});
13381
13382 let fs = FakeFs::new(cx.executor());
13383 fs.insert_tree(
13384 path!("/a"),
13385 json!({
13386 "main.ts": "a",
13387 }),
13388 )
13389 .await;
13390
13391 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13392 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13393 let typescript_language = Arc::new(Language::new(
13394 LanguageConfig {
13395 name: "TypeScript".into(),
13396 matcher: LanguageMatcher {
13397 path_suffixes: vec!["ts".to_string()],
13398 ..LanguageMatcher::default()
13399 },
13400 line_comments: vec!["// ".into()],
13401 ..LanguageConfig::default()
13402 },
13403 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13404 ));
13405 language_registry.add(typescript_language.clone());
13406 let mut fake_servers = language_registry.register_fake_lsp(
13407 "TypeScript",
13408 FakeLspAdapter {
13409 capabilities: lsp::ServerCapabilities {
13410 completion_provider: Some(lsp::CompletionOptions {
13411 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13412 ..lsp::CompletionOptions::default()
13413 }),
13414 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13415 ..lsp::ServerCapabilities::default()
13416 },
13417 // Emulate vtsls label generation
13418 label_for_completion: Some(Box::new(|item, _| {
13419 let text = if let Some(description) = item
13420 .label_details
13421 .as_ref()
13422 .and_then(|label_details| label_details.description.as_ref())
13423 {
13424 format!("{} {}", item.label, description)
13425 } else if let Some(detail) = &item.detail {
13426 format!("{} {}", item.label, detail)
13427 } else {
13428 item.label.clone()
13429 };
13430 let len = text.len();
13431 Some(language::CodeLabel {
13432 text,
13433 runs: Vec::new(),
13434 filter_range: 0..len,
13435 })
13436 })),
13437 ..FakeLspAdapter::default()
13438 },
13439 );
13440 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13441 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13442 let worktree_id = workspace
13443 .update(cx, |workspace, _window, cx| {
13444 workspace.project().update(cx, |project, cx| {
13445 project.worktrees(cx).next().unwrap().read(cx).id()
13446 })
13447 })
13448 .unwrap();
13449 let _buffer = project
13450 .update(cx, |project, cx| {
13451 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
13452 })
13453 .await
13454 .unwrap();
13455 let editor = workspace
13456 .update(cx, |workspace, window, cx| {
13457 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
13458 })
13459 .unwrap()
13460 .await
13461 .unwrap()
13462 .downcast::<Editor>()
13463 .unwrap();
13464 let fake_server = fake_servers.next().await.unwrap();
13465
13466 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
13467 let multiline_label_2 = "a\nb\nc\n";
13468 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
13469 let multiline_description = "d\ne\nf\n";
13470 let multiline_detail_2 = "g\nh\ni\n";
13471
13472 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
13473 move |params, _| async move {
13474 Ok(Some(lsp::CompletionResponse::Array(vec![
13475 lsp::CompletionItem {
13476 label: multiline_label.to_string(),
13477 text_edit: gen_text_edit(¶ms, "new_text_1"),
13478 ..lsp::CompletionItem::default()
13479 },
13480 lsp::CompletionItem {
13481 label: "single line label 1".to_string(),
13482 detail: Some(multiline_detail.to_string()),
13483 text_edit: gen_text_edit(¶ms, "new_text_2"),
13484 ..lsp::CompletionItem::default()
13485 },
13486 lsp::CompletionItem {
13487 label: "single line label 2".to_string(),
13488 label_details: Some(lsp::CompletionItemLabelDetails {
13489 description: Some(multiline_description.to_string()),
13490 detail: None,
13491 }),
13492 text_edit: gen_text_edit(¶ms, "new_text_2"),
13493 ..lsp::CompletionItem::default()
13494 },
13495 lsp::CompletionItem {
13496 label: multiline_label_2.to_string(),
13497 detail: Some(multiline_detail_2.to_string()),
13498 text_edit: gen_text_edit(¶ms, "new_text_3"),
13499 ..lsp::CompletionItem::default()
13500 },
13501 lsp::CompletionItem {
13502 label: "Label with many spaces and \t but without newlines".to_string(),
13503 detail: Some(
13504 "Details with many spaces and \t but without newlines".to_string(),
13505 ),
13506 text_edit: gen_text_edit(¶ms, "new_text_4"),
13507 ..lsp::CompletionItem::default()
13508 },
13509 ])))
13510 },
13511 );
13512
13513 editor.update_in(cx, |editor, window, cx| {
13514 cx.focus_self(window);
13515 editor.move_to_end(&MoveToEnd, window, cx);
13516 editor.handle_input(".", window, cx);
13517 });
13518 cx.run_until_parked();
13519 completion_handle.next().await.unwrap();
13520
13521 editor.update(cx, |editor, _| {
13522 assert!(editor.context_menu_visible());
13523 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13524 {
13525 let completion_labels = menu
13526 .completions
13527 .borrow()
13528 .iter()
13529 .map(|c| c.label.text.clone())
13530 .collect::<Vec<_>>();
13531 assert_eq!(
13532 completion_labels,
13533 &[
13534 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
13535 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
13536 "single line label 2 d e f ",
13537 "a b c g h i ",
13538 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
13539 ],
13540 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
13541 );
13542
13543 for completion in menu
13544 .completions
13545 .borrow()
13546 .iter() {
13547 assert_eq!(
13548 completion.label.filter_range,
13549 0..completion.label.text.len(),
13550 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
13551 );
13552 }
13553 } else {
13554 panic!("expected completion menu to be open");
13555 }
13556 });
13557}
13558
13559#[gpui::test]
13560async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
13561 init_test(cx, |_| {});
13562 let mut cx = EditorLspTestContext::new_rust(
13563 lsp::ServerCapabilities {
13564 completion_provider: Some(lsp::CompletionOptions {
13565 trigger_characters: Some(vec![".".to_string()]),
13566 ..Default::default()
13567 }),
13568 ..Default::default()
13569 },
13570 cx,
13571 )
13572 .await;
13573 cx.lsp
13574 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13575 Ok(Some(lsp::CompletionResponse::Array(vec![
13576 lsp::CompletionItem {
13577 label: "first".into(),
13578 ..Default::default()
13579 },
13580 lsp::CompletionItem {
13581 label: "last".into(),
13582 ..Default::default()
13583 },
13584 ])))
13585 });
13586 cx.set_state("variableˇ");
13587 cx.simulate_keystroke(".");
13588 cx.executor().run_until_parked();
13589
13590 cx.update_editor(|editor, _, _| {
13591 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13592 {
13593 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
13594 } else {
13595 panic!("expected completion menu to be open");
13596 }
13597 });
13598
13599 cx.update_editor(|editor, window, cx| {
13600 editor.move_page_down(&MovePageDown::default(), window, cx);
13601 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13602 {
13603 assert!(
13604 menu.selected_item == 1,
13605 "expected PageDown to select the last item from the context menu"
13606 );
13607 } else {
13608 panic!("expected completion menu to stay open after PageDown");
13609 }
13610 });
13611
13612 cx.update_editor(|editor, window, cx| {
13613 editor.move_page_up(&MovePageUp::default(), window, cx);
13614 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13615 {
13616 assert!(
13617 menu.selected_item == 0,
13618 "expected PageUp to select the first item from the context menu"
13619 );
13620 } else {
13621 panic!("expected completion menu to stay open after PageUp");
13622 }
13623 });
13624}
13625
13626#[gpui::test]
13627async fn test_as_is_completions(cx: &mut TestAppContext) {
13628 init_test(cx, |_| {});
13629 let mut cx = EditorLspTestContext::new_rust(
13630 lsp::ServerCapabilities {
13631 completion_provider: Some(lsp::CompletionOptions {
13632 ..Default::default()
13633 }),
13634 ..Default::default()
13635 },
13636 cx,
13637 )
13638 .await;
13639 cx.lsp
13640 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13641 Ok(Some(lsp::CompletionResponse::Array(vec![
13642 lsp::CompletionItem {
13643 label: "unsafe".into(),
13644 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13645 range: lsp::Range {
13646 start: lsp::Position {
13647 line: 1,
13648 character: 2,
13649 },
13650 end: lsp::Position {
13651 line: 1,
13652 character: 3,
13653 },
13654 },
13655 new_text: "unsafe".to_string(),
13656 })),
13657 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
13658 ..Default::default()
13659 },
13660 ])))
13661 });
13662 cx.set_state("fn a() {}\n nˇ");
13663 cx.executor().run_until_parked();
13664 cx.update_editor(|editor, window, cx| {
13665 editor.show_completions(
13666 &ShowCompletions {
13667 trigger: Some("\n".into()),
13668 },
13669 window,
13670 cx,
13671 );
13672 });
13673 cx.executor().run_until_parked();
13674
13675 cx.update_editor(|editor, window, cx| {
13676 editor.confirm_completion(&Default::default(), window, cx)
13677 });
13678 cx.executor().run_until_parked();
13679 cx.assert_editor_state("fn a() {}\n unsafeˇ");
13680}
13681
13682#[gpui::test]
13683async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
13684 init_test(cx, |_| {});
13685 let language =
13686 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
13687 let mut cx = EditorLspTestContext::new(
13688 language,
13689 lsp::ServerCapabilities {
13690 completion_provider: Some(lsp::CompletionOptions {
13691 ..lsp::CompletionOptions::default()
13692 }),
13693 ..lsp::ServerCapabilities::default()
13694 },
13695 cx,
13696 )
13697 .await;
13698
13699 cx.set_state(
13700 "#ifndef BAR_H
13701#define BAR_H
13702
13703#include <stdbool.h>
13704
13705int fn_branch(bool do_branch1, bool do_branch2);
13706
13707#endif // BAR_H
13708ˇ",
13709 );
13710 cx.executor().run_until_parked();
13711 cx.update_editor(|editor, window, cx| {
13712 editor.handle_input("#", window, cx);
13713 });
13714 cx.executor().run_until_parked();
13715 cx.update_editor(|editor, window, cx| {
13716 editor.handle_input("i", window, cx);
13717 });
13718 cx.executor().run_until_parked();
13719 cx.update_editor(|editor, window, cx| {
13720 editor.handle_input("n", window, cx);
13721 });
13722 cx.executor().run_until_parked();
13723 cx.assert_editor_state(
13724 "#ifndef BAR_H
13725#define BAR_H
13726
13727#include <stdbool.h>
13728
13729int fn_branch(bool do_branch1, bool do_branch2);
13730
13731#endif // BAR_H
13732#inˇ",
13733 );
13734
13735 cx.lsp
13736 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13737 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13738 is_incomplete: false,
13739 item_defaults: None,
13740 items: vec![lsp::CompletionItem {
13741 kind: Some(lsp::CompletionItemKind::SNIPPET),
13742 label_details: Some(lsp::CompletionItemLabelDetails {
13743 detail: Some("header".to_string()),
13744 description: None,
13745 }),
13746 label: " include".to_string(),
13747 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13748 range: lsp::Range {
13749 start: lsp::Position {
13750 line: 8,
13751 character: 1,
13752 },
13753 end: lsp::Position {
13754 line: 8,
13755 character: 1,
13756 },
13757 },
13758 new_text: "include \"$0\"".to_string(),
13759 })),
13760 sort_text: Some("40b67681include".to_string()),
13761 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13762 filter_text: Some("include".to_string()),
13763 insert_text: Some("include \"$0\"".to_string()),
13764 ..lsp::CompletionItem::default()
13765 }],
13766 })))
13767 });
13768 cx.update_editor(|editor, window, cx| {
13769 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13770 });
13771 cx.executor().run_until_parked();
13772 cx.update_editor(|editor, window, cx| {
13773 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
13774 });
13775 cx.executor().run_until_parked();
13776 cx.assert_editor_state(
13777 "#ifndef BAR_H
13778#define BAR_H
13779
13780#include <stdbool.h>
13781
13782int fn_branch(bool do_branch1, bool do_branch2);
13783
13784#endif // BAR_H
13785#include \"ˇ\"",
13786 );
13787
13788 cx.lsp
13789 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13790 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13791 is_incomplete: true,
13792 item_defaults: None,
13793 items: vec![lsp::CompletionItem {
13794 kind: Some(lsp::CompletionItemKind::FILE),
13795 label: "AGL/".to_string(),
13796 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13797 range: lsp::Range {
13798 start: lsp::Position {
13799 line: 8,
13800 character: 10,
13801 },
13802 end: lsp::Position {
13803 line: 8,
13804 character: 11,
13805 },
13806 },
13807 new_text: "AGL/".to_string(),
13808 })),
13809 sort_text: Some("40b67681AGL/".to_string()),
13810 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
13811 filter_text: Some("AGL/".to_string()),
13812 insert_text: Some("AGL/".to_string()),
13813 ..lsp::CompletionItem::default()
13814 }],
13815 })))
13816 });
13817 cx.update_editor(|editor, window, cx| {
13818 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13819 });
13820 cx.executor().run_until_parked();
13821 cx.update_editor(|editor, window, cx| {
13822 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
13823 });
13824 cx.executor().run_until_parked();
13825 cx.assert_editor_state(
13826 r##"#ifndef BAR_H
13827#define BAR_H
13828
13829#include <stdbool.h>
13830
13831int fn_branch(bool do_branch1, bool do_branch2);
13832
13833#endif // BAR_H
13834#include "AGL/ˇ"##,
13835 );
13836
13837 cx.update_editor(|editor, window, cx| {
13838 editor.handle_input("\"", window, cx);
13839 });
13840 cx.executor().run_until_parked();
13841 cx.assert_editor_state(
13842 r##"#ifndef BAR_H
13843#define BAR_H
13844
13845#include <stdbool.h>
13846
13847int fn_branch(bool do_branch1, bool do_branch2);
13848
13849#endif // BAR_H
13850#include "AGL/"ˇ"##,
13851 );
13852}
13853
13854#[gpui::test]
13855async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
13856 init_test(cx, |_| {});
13857
13858 let mut cx = EditorLspTestContext::new_rust(
13859 lsp::ServerCapabilities {
13860 completion_provider: Some(lsp::CompletionOptions {
13861 trigger_characters: Some(vec![".".to_string()]),
13862 resolve_provider: Some(true),
13863 ..Default::default()
13864 }),
13865 ..Default::default()
13866 },
13867 cx,
13868 )
13869 .await;
13870
13871 cx.set_state("fn main() { let a = 2ˇ; }");
13872 cx.simulate_keystroke(".");
13873 let completion_item = lsp::CompletionItem {
13874 label: "Some".into(),
13875 kind: Some(lsp::CompletionItemKind::SNIPPET),
13876 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13877 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13878 kind: lsp::MarkupKind::Markdown,
13879 value: "```rust\nSome(2)\n```".to_string(),
13880 })),
13881 deprecated: Some(false),
13882 sort_text: Some("Some".to_string()),
13883 filter_text: Some("Some".to_string()),
13884 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13885 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13886 range: lsp::Range {
13887 start: lsp::Position {
13888 line: 0,
13889 character: 22,
13890 },
13891 end: lsp::Position {
13892 line: 0,
13893 character: 22,
13894 },
13895 },
13896 new_text: "Some(2)".to_string(),
13897 })),
13898 additional_text_edits: Some(vec![lsp::TextEdit {
13899 range: lsp::Range {
13900 start: lsp::Position {
13901 line: 0,
13902 character: 20,
13903 },
13904 end: lsp::Position {
13905 line: 0,
13906 character: 22,
13907 },
13908 },
13909 new_text: "".to_string(),
13910 }]),
13911 ..Default::default()
13912 };
13913
13914 let closure_completion_item = completion_item.clone();
13915 let counter = Arc::new(AtomicUsize::new(0));
13916 let counter_clone = counter.clone();
13917 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13918 let task_completion_item = closure_completion_item.clone();
13919 counter_clone.fetch_add(1, atomic::Ordering::Release);
13920 async move {
13921 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13922 is_incomplete: true,
13923 item_defaults: None,
13924 items: vec![task_completion_item],
13925 })))
13926 }
13927 });
13928
13929 cx.condition(|editor, _| editor.context_menu_visible())
13930 .await;
13931 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
13932 assert!(request.next().await.is_some());
13933 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13934
13935 cx.simulate_keystrokes("S o m");
13936 cx.condition(|editor, _| editor.context_menu_visible())
13937 .await;
13938 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
13939 assert!(request.next().await.is_some());
13940 assert!(request.next().await.is_some());
13941 assert!(request.next().await.is_some());
13942 request.close();
13943 assert!(request.next().await.is_none());
13944 assert_eq!(
13945 counter.load(atomic::Ordering::Acquire),
13946 4,
13947 "With the completions menu open, only one LSP request should happen per input"
13948 );
13949}
13950
13951#[gpui::test]
13952async fn test_toggle_comment(cx: &mut TestAppContext) {
13953 init_test(cx, |_| {});
13954 let mut cx = EditorTestContext::new(cx).await;
13955 let language = Arc::new(Language::new(
13956 LanguageConfig {
13957 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13958 ..Default::default()
13959 },
13960 Some(tree_sitter_rust::LANGUAGE.into()),
13961 ));
13962 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13963
13964 // If multiple selections intersect a line, the line is only toggled once.
13965 cx.set_state(indoc! {"
13966 fn a() {
13967 «//b();
13968 ˇ»// «c();
13969 //ˇ» d();
13970 }
13971 "});
13972
13973 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13974
13975 cx.assert_editor_state(indoc! {"
13976 fn a() {
13977 «b();
13978 c();
13979 ˇ» d();
13980 }
13981 "});
13982
13983 // The comment prefix is inserted at the same column for every line in a
13984 // selection.
13985 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13986
13987 cx.assert_editor_state(indoc! {"
13988 fn a() {
13989 // «b();
13990 // c();
13991 ˇ»// d();
13992 }
13993 "});
13994
13995 // If a selection ends at the beginning of a line, that line is not toggled.
13996 cx.set_selections_state(indoc! {"
13997 fn a() {
13998 // b();
13999 «// c();
14000 ˇ» // d();
14001 }
14002 "});
14003
14004 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14005
14006 cx.assert_editor_state(indoc! {"
14007 fn a() {
14008 // b();
14009 «c();
14010 ˇ» // d();
14011 }
14012 "});
14013
14014 // If a selection span a single line and is empty, the line is toggled.
14015 cx.set_state(indoc! {"
14016 fn a() {
14017 a();
14018 b();
14019 ˇ
14020 }
14021 "});
14022
14023 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14024
14025 cx.assert_editor_state(indoc! {"
14026 fn a() {
14027 a();
14028 b();
14029 //•ˇ
14030 }
14031 "});
14032
14033 // If a selection span multiple lines, empty lines are not toggled.
14034 cx.set_state(indoc! {"
14035 fn a() {
14036 «a();
14037
14038 c();ˇ»
14039 }
14040 "});
14041
14042 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14043
14044 cx.assert_editor_state(indoc! {"
14045 fn a() {
14046 // «a();
14047
14048 // c();ˇ»
14049 }
14050 "});
14051
14052 // If a selection includes multiple comment prefixes, all lines are uncommented.
14053 cx.set_state(indoc! {"
14054 fn a() {
14055 «// a();
14056 /// b();
14057 //! c();ˇ»
14058 }
14059 "});
14060
14061 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
14062
14063 cx.assert_editor_state(indoc! {"
14064 fn a() {
14065 «a();
14066 b();
14067 c();ˇ»
14068 }
14069 "});
14070}
14071
14072#[gpui::test]
14073async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
14074 init_test(cx, |_| {});
14075 let mut cx = EditorTestContext::new(cx).await;
14076 let language = Arc::new(Language::new(
14077 LanguageConfig {
14078 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
14079 ..Default::default()
14080 },
14081 Some(tree_sitter_rust::LANGUAGE.into()),
14082 ));
14083 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
14084
14085 let toggle_comments = &ToggleComments {
14086 advance_downwards: false,
14087 ignore_indent: true,
14088 };
14089
14090 // If multiple selections intersect a line, the line is only toggled once.
14091 cx.set_state(indoc! {"
14092 fn a() {
14093 // «b();
14094 // c();
14095 // ˇ» d();
14096 }
14097 "});
14098
14099 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14100
14101 cx.assert_editor_state(indoc! {"
14102 fn a() {
14103 «b();
14104 c();
14105 ˇ» d();
14106 }
14107 "});
14108
14109 // The comment prefix is inserted at the beginning of each line
14110 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14111
14112 cx.assert_editor_state(indoc! {"
14113 fn a() {
14114 // «b();
14115 // c();
14116 // ˇ» d();
14117 }
14118 "});
14119
14120 // If a selection ends at the beginning of a line, that line is not toggled.
14121 cx.set_selections_state(indoc! {"
14122 fn a() {
14123 // b();
14124 // «c();
14125 ˇ»// d();
14126 }
14127 "});
14128
14129 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14130
14131 cx.assert_editor_state(indoc! {"
14132 fn a() {
14133 // b();
14134 «c();
14135 ˇ»// d();
14136 }
14137 "});
14138
14139 // If a selection span a single line and is empty, the line is toggled.
14140 cx.set_state(indoc! {"
14141 fn a() {
14142 a();
14143 b();
14144 ˇ
14145 }
14146 "});
14147
14148 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14149
14150 cx.assert_editor_state(indoc! {"
14151 fn a() {
14152 a();
14153 b();
14154 //ˇ
14155 }
14156 "});
14157
14158 // If a selection span multiple lines, empty lines are not toggled.
14159 cx.set_state(indoc! {"
14160 fn a() {
14161 «a();
14162
14163 c();ˇ»
14164 }
14165 "});
14166
14167 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14168
14169 cx.assert_editor_state(indoc! {"
14170 fn a() {
14171 // «a();
14172
14173 // c();ˇ»
14174 }
14175 "});
14176
14177 // If a selection includes multiple comment prefixes, all lines are uncommented.
14178 cx.set_state(indoc! {"
14179 fn a() {
14180 // «a();
14181 /// b();
14182 //! c();ˇ»
14183 }
14184 "});
14185
14186 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
14187
14188 cx.assert_editor_state(indoc! {"
14189 fn a() {
14190 «a();
14191 b();
14192 c();ˇ»
14193 }
14194 "});
14195}
14196
14197#[gpui::test]
14198async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
14199 init_test(cx, |_| {});
14200
14201 let language = Arc::new(Language::new(
14202 LanguageConfig {
14203 line_comments: vec!["// ".into()],
14204 ..Default::default()
14205 },
14206 Some(tree_sitter_rust::LANGUAGE.into()),
14207 ));
14208
14209 let mut cx = EditorTestContext::new(cx).await;
14210
14211 cx.language_registry().add(language.clone());
14212 cx.update_buffer(|buffer, cx| {
14213 buffer.set_language(Some(language), cx);
14214 });
14215
14216 let toggle_comments = &ToggleComments {
14217 advance_downwards: true,
14218 ignore_indent: false,
14219 };
14220
14221 // Single cursor on one line -> advance
14222 // Cursor moves horizontally 3 characters as well on non-blank line
14223 cx.set_state(indoc!(
14224 "fn a() {
14225 ˇdog();
14226 cat();
14227 }"
14228 ));
14229 cx.update_editor(|editor, window, cx| {
14230 editor.toggle_comments(toggle_comments, window, cx);
14231 });
14232 cx.assert_editor_state(indoc!(
14233 "fn a() {
14234 // dog();
14235 catˇ();
14236 }"
14237 ));
14238
14239 // Single selection on one line -> don't advance
14240 cx.set_state(indoc!(
14241 "fn a() {
14242 «dog()ˇ»;
14243 cat();
14244 }"
14245 ));
14246 cx.update_editor(|editor, window, cx| {
14247 editor.toggle_comments(toggle_comments, window, cx);
14248 });
14249 cx.assert_editor_state(indoc!(
14250 "fn a() {
14251 // «dog()ˇ»;
14252 cat();
14253 }"
14254 ));
14255
14256 // Multiple cursors on one line -> advance
14257 cx.set_state(indoc!(
14258 "fn a() {
14259 ˇdˇog();
14260 cat();
14261 }"
14262 ));
14263 cx.update_editor(|editor, window, cx| {
14264 editor.toggle_comments(toggle_comments, window, cx);
14265 });
14266 cx.assert_editor_state(indoc!(
14267 "fn a() {
14268 // dog();
14269 catˇ(ˇ);
14270 }"
14271 ));
14272
14273 // Multiple cursors on one line, with selection -> don't advance
14274 cx.set_state(indoc!(
14275 "fn a() {
14276 ˇdˇog«()ˇ»;
14277 cat();
14278 }"
14279 ));
14280 cx.update_editor(|editor, window, cx| {
14281 editor.toggle_comments(toggle_comments, window, cx);
14282 });
14283 cx.assert_editor_state(indoc!(
14284 "fn a() {
14285 // ˇdˇog«()ˇ»;
14286 cat();
14287 }"
14288 ));
14289
14290 // Single cursor on one line -> advance
14291 // Cursor moves to column 0 on blank line
14292 cx.set_state(indoc!(
14293 "fn a() {
14294 ˇdog();
14295
14296 cat();
14297 }"
14298 ));
14299 cx.update_editor(|editor, window, cx| {
14300 editor.toggle_comments(toggle_comments, window, cx);
14301 });
14302 cx.assert_editor_state(indoc!(
14303 "fn a() {
14304 // dog();
14305 ˇ
14306 cat();
14307 }"
14308 ));
14309
14310 // Single cursor on one line -> advance
14311 // Cursor starts and ends at column 0
14312 cx.set_state(indoc!(
14313 "fn a() {
14314 ˇ dog();
14315 cat();
14316 }"
14317 ));
14318 cx.update_editor(|editor, window, cx| {
14319 editor.toggle_comments(toggle_comments, window, cx);
14320 });
14321 cx.assert_editor_state(indoc!(
14322 "fn a() {
14323 // dog();
14324 ˇ cat();
14325 }"
14326 ));
14327}
14328
14329#[gpui::test]
14330async fn test_toggle_block_comment(cx: &mut TestAppContext) {
14331 init_test(cx, |_| {});
14332
14333 let mut cx = EditorTestContext::new(cx).await;
14334
14335 let html_language = Arc::new(
14336 Language::new(
14337 LanguageConfig {
14338 name: "HTML".into(),
14339 block_comment: Some(BlockCommentConfig {
14340 start: "<!-- ".into(),
14341 prefix: "".into(),
14342 end: " -->".into(),
14343 tab_size: 0,
14344 }),
14345 ..Default::default()
14346 },
14347 Some(tree_sitter_html::LANGUAGE.into()),
14348 )
14349 .with_injection_query(
14350 r#"
14351 (script_element
14352 (raw_text) @injection.content
14353 (#set! injection.language "javascript"))
14354 "#,
14355 )
14356 .unwrap(),
14357 );
14358
14359 let javascript_language = Arc::new(Language::new(
14360 LanguageConfig {
14361 name: "JavaScript".into(),
14362 line_comments: vec!["// ".into()],
14363 ..Default::default()
14364 },
14365 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
14366 ));
14367
14368 cx.language_registry().add(html_language.clone());
14369 cx.language_registry().add(javascript_language);
14370 cx.update_buffer(|buffer, cx| {
14371 buffer.set_language(Some(html_language), cx);
14372 });
14373
14374 // Toggle comments for empty selections
14375 cx.set_state(
14376 &r#"
14377 <p>A</p>ˇ
14378 <p>B</p>ˇ
14379 <p>C</p>ˇ
14380 "#
14381 .unindent(),
14382 );
14383 cx.update_editor(|editor, window, cx| {
14384 editor.toggle_comments(&ToggleComments::default(), window, cx)
14385 });
14386 cx.assert_editor_state(
14387 &r#"
14388 <!-- <p>A</p>ˇ -->
14389 <!-- <p>B</p>ˇ -->
14390 <!-- <p>C</p>ˇ -->
14391 "#
14392 .unindent(),
14393 );
14394 cx.update_editor(|editor, window, cx| {
14395 editor.toggle_comments(&ToggleComments::default(), window, cx)
14396 });
14397 cx.assert_editor_state(
14398 &r#"
14399 <p>A</p>ˇ
14400 <p>B</p>ˇ
14401 <p>C</p>ˇ
14402 "#
14403 .unindent(),
14404 );
14405
14406 // Toggle comments for mixture of empty and non-empty selections, where
14407 // multiple selections occupy a given line.
14408 cx.set_state(
14409 &r#"
14410 <p>A«</p>
14411 <p>ˇ»B</p>ˇ
14412 <p>C«</p>
14413 <p>ˇ»D</p>ˇ
14414 "#
14415 .unindent(),
14416 );
14417
14418 cx.update_editor(|editor, window, cx| {
14419 editor.toggle_comments(&ToggleComments::default(), window, cx)
14420 });
14421 cx.assert_editor_state(
14422 &r#"
14423 <!-- <p>A«</p>
14424 <p>ˇ»B</p>ˇ -->
14425 <!-- <p>C«</p>
14426 <p>ˇ»D</p>ˇ -->
14427 "#
14428 .unindent(),
14429 );
14430 cx.update_editor(|editor, window, cx| {
14431 editor.toggle_comments(&ToggleComments::default(), window, cx)
14432 });
14433 cx.assert_editor_state(
14434 &r#"
14435 <p>A«</p>
14436 <p>ˇ»B</p>ˇ
14437 <p>C«</p>
14438 <p>ˇ»D</p>ˇ
14439 "#
14440 .unindent(),
14441 );
14442
14443 // Toggle comments when different languages are active for different
14444 // selections.
14445 cx.set_state(
14446 &r#"
14447 ˇ<script>
14448 ˇvar x = new Y();
14449 ˇ</script>
14450 "#
14451 .unindent(),
14452 );
14453 cx.executor().run_until_parked();
14454 cx.update_editor(|editor, window, cx| {
14455 editor.toggle_comments(&ToggleComments::default(), window, cx)
14456 });
14457 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
14458 // Uncommenting and commenting from this position brings in even more wrong artifacts.
14459 cx.assert_editor_state(
14460 &r#"
14461 <!-- ˇ<script> -->
14462 // ˇvar x = new Y();
14463 <!-- ˇ</script> -->
14464 "#
14465 .unindent(),
14466 );
14467}
14468
14469#[gpui::test]
14470fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
14471 init_test(cx, |_| {});
14472
14473 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14474 let multibuffer = cx.new(|cx| {
14475 let mut multibuffer = MultiBuffer::new(ReadWrite);
14476 multibuffer.push_excerpts(
14477 buffer.clone(),
14478 [
14479 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
14480 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
14481 ],
14482 cx,
14483 );
14484 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
14485 multibuffer
14486 });
14487
14488 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
14489 editor.update_in(cx, |editor, window, cx| {
14490 assert_eq!(editor.text(cx), "aaaa\nbbbb");
14491 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14492 s.select_ranges([
14493 Point::new(0, 0)..Point::new(0, 0),
14494 Point::new(1, 0)..Point::new(1, 0),
14495 ])
14496 });
14497
14498 editor.handle_input("X", window, cx);
14499 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
14500 assert_eq!(
14501 editor.selections.ranges(cx),
14502 [
14503 Point::new(0, 1)..Point::new(0, 1),
14504 Point::new(1, 1)..Point::new(1, 1),
14505 ]
14506 );
14507
14508 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
14509 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14510 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
14511 });
14512 editor.backspace(&Default::default(), window, cx);
14513 assert_eq!(editor.text(cx), "Xa\nbbb");
14514 assert_eq!(
14515 editor.selections.ranges(cx),
14516 [Point::new(1, 0)..Point::new(1, 0)]
14517 );
14518
14519 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14520 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
14521 });
14522 editor.backspace(&Default::default(), window, cx);
14523 assert_eq!(editor.text(cx), "X\nbb");
14524 assert_eq!(
14525 editor.selections.ranges(cx),
14526 [Point::new(0, 1)..Point::new(0, 1)]
14527 );
14528 });
14529}
14530
14531#[gpui::test]
14532fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
14533 init_test(cx, |_| {});
14534
14535 let markers = vec![('[', ']').into(), ('(', ')').into()];
14536 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
14537 indoc! {"
14538 [aaaa
14539 (bbbb]
14540 cccc)",
14541 },
14542 markers.clone(),
14543 );
14544 let excerpt_ranges = markers.into_iter().map(|marker| {
14545 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
14546 ExcerptRange::new(context)
14547 });
14548 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
14549 let multibuffer = cx.new(|cx| {
14550 let mut multibuffer = MultiBuffer::new(ReadWrite);
14551 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
14552 multibuffer
14553 });
14554
14555 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
14556 editor.update_in(cx, |editor, window, cx| {
14557 let (expected_text, selection_ranges) = marked_text_ranges(
14558 indoc! {"
14559 aaaa
14560 bˇbbb
14561 bˇbbˇb
14562 cccc"
14563 },
14564 true,
14565 );
14566 assert_eq!(editor.text(cx), expected_text);
14567 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14568 s.select_ranges(selection_ranges)
14569 });
14570
14571 editor.handle_input("X", window, cx);
14572
14573 let (expected_text, expected_selections) = marked_text_ranges(
14574 indoc! {"
14575 aaaa
14576 bXˇbbXb
14577 bXˇbbXˇb
14578 cccc"
14579 },
14580 false,
14581 );
14582 assert_eq!(editor.text(cx), expected_text);
14583 assert_eq!(editor.selections.ranges(cx), expected_selections);
14584
14585 editor.newline(&Newline, window, cx);
14586 let (expected_text, expected_selections) = marked_text_ranges(
14587 indoc! {"
14588 aaaa
14589 bX
14590 ˇbbX
14591 b
14592 bX
14593 ˇbbX
14594 ˇb
14595 cccc"
14596 },
14597 false,
14598 );
14599 assert_eq!(editor.text(cx), expected_text);
14600 assert_eq!(editor.selections.ranges(cx), expected_selections);
14601 });
14602}
14603
14604#[gpui::test]
14605fn test_refresh_selections(cx: &mut TestAppContext) {
14606 init_test(cx, |_| {});
14607
14608 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14609 let mut excerpt1_id = None;
14610 let multibuffer = cx.new(|cx| {
14611 let mut multibuffer = MultiBuffer::new(ReadWrite);
14612 excerpt1_id = multibuffer
14613 .push_excerpts(
14614 buffer.clone(),
14615 [
14616 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14617 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14618 ],
14619 cx,
14620 )
14621 .into_iter()
14622 .next();
14623 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14624 multibuffer
14625 });
14626
14627 let editor = cx.add_window(|window, cx| {
14628 let mut editor = build_editor(multibuffer.clone(), window, cx);
14629 let snapshot = editor.snapshot(window, cx);
14630 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14631 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
14632 });
14633 editor.begin_selection(
14634 Point::new(2, 1).to_display_point(&snapshot),
14635 true,
14636 1,
14637 window,
14638 cx,
14639 );
14640 assert_eq!(
14641 editor.selections.ranges(cx),
14642 [
14643 Point::new(1, 3)..Point::new(1, 3),
14644 Point::new(2, 1)..Point::new(2, 1),
14645 ]
14646 );
14647 editor
14648 });
14649
14650 // Refreshing selections is a no-op when excerpts haven't changed.
14651 _ = editor.update(cx, |editor, window, cx| {
14652 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14653 assert_eq!(
14654 editor.selections.ranges(cx),
14655 [
14656 Point::new(1, 3)..Point::new(1, 3),
14657 Point::new(2, 1)..Point::new(2, 1),
14658 ]
14659 );
14660 });
14661
14662 multibuffer.update(cx, |multibuffer, cx| {
14663 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14664 });
14665 _ = editor.update(cx, |editor, window, cx| {
14666 // Removing an excerpt causes the first selection to become degenerate.
14667 assert_eq!(
14668 editor.selections.ranges(cx),
14669 [
14670 Point::new(0, 0)..Point::new(0, 0),
14671 Point::new(0, 1)..Point::new(0, 1)
14672 ]
14673 );
14674
14675 // Refreshing selections will relocate the first selection to the original buffer
14676 // location.
14677 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14678 assert_eq!(
14679 editor.selections.ranges(cx),
14680 [
14681 Point::new(0, 1)..Point::new(0, 1),
14682 Point::new(0, 3)..Point::new(0, 3)
14683 ]
14684 );
14685 assert!(editor.selections.pending_anchor().is_some());
14686 });
14687}
14688
14689#[gpui::test]
14690fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
14691 init_test(cx, |_| {});
14692
14693 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14694 let mut excerpt1_id = None;
14695 let multibuffer = cx.new(|cx| {
14696 let mut multibuffer = MultiBuffer::new(ReadWrite);
14697 excerpt1_id = multibuffer
14698 .push_excerpts(
14699 buffer.clone(),
14700 [
14701 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14702 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14703 ],
14704 cx,
14705 )
14706 .into_iter()
14707 .next();
14708 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14709 multibuffer
14710 });
14711
14712 let editor = cx.add_window(|window, cx| {
14713 let mut editor = build_editor(multibuffer.clone(), window, cx);
14714 let snapshot = editor.snapshot(window, cx);
14715 editor.begin_selection(
14716 Point::new(1, 3).to_display_point(&snapshot),
14717 false,
14718 1,
14719 window,
14720 cx,
14721 );
14722 assert_eq!(
14723 editor.selections.ranges(cx),
14724 [Point::new(1, 3)..Point::new(1, 3)]
14725 );
14726 editor
14727 });
14728
14729 multibuffer.update(cx, |multibuffer, cx| {
14730 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14731 });
14732 _ = editor.update(cx, |editor, window, cx| {
14733 assert_eq!(
14734 editor.selections.ranges(cx),
14735 [Point::new(0, 0)..Point::new(0, 0)]
14736 );
14737
14738 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
14739 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14740 assert_eq!(
14741 editor.selections.ranges(cx),
14742 [Point::new(0, 3)..Point::new(0, 3)]
14743 );
14744 assert!(editor.selections.pending_anchor().is_some());
14745 });
14746}
14747
14748#[gpui::test]
14749async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
14750 init_test(cx, |_| {});
14751
14752 let language = Arc::new(
14753 Language::new(
14754 LanguageConfig {
14755 brackets: BracketPairConfig {
14756 pairs: vec![
14757 BracketPair {
14758 start: "{".to_string(),
14759 end: "}".to_string(),
14760 close: true,
14761 surround: true,
14762 newline: true,
14763 },
14764 BracketPair {
14765 start: "/* ".to_string(),
14766 end: " */".to_string(),
14767 close: true,
14768 surround: true,
14769 newline: true,
14770 },
14771 ],
14772 ..Default::default()
14773 },
14774 ..Default::default()
14775 },
14776 Some(tree_sitter_rust::LANGUAGE.into()),
14777 )
14778 .with_indents_query("")
14779 .unwrap(),
14780 );
14781
14782 let text = concat!(
14783 "{ }\n", //
14784 " x\n", //
14785 " /* */\n", //
14786 "x\n", //
14787 "{{} }\n", //
14788 );
14789
14790 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
14791 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14792 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14793 editor
14794 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
14795 .await;
14796
14797 editor.update_in(cx, |editor, window, cx| {
14798 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14799 s.select_display_ranges([
14800 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
14801 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
14802 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
14803 ])
14804 });
14805 editor.newline(&Newline, window, cx);
14806
14807 assert_eq!(
14808 editor.buffer().read(cx).read(cx).text(),
14809 concat!(
14810 "{ \n", // Suppress rustfmt
14811 "\n", //
14812 "}\n", //
14813 " x\n", //
14814 " /* \n", //
14815 " \n", //
14816 " */\n", //
14817 "x\n", //
14818 "{{} \n", //
14819 "}\n", //
14820 )
14821 );
14822 });
14823}
14824
14825#[gpui::test]
14826fn test_highlighted_ranges(cx: &mut TestAppContext) {
14827 init_test(cx, |_| {});
14828
14829 let editor = cx.add_window(|window, cx| {
14830 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
14831 build_editor(buffer, window, cx)
14832 });
14833
14834 _ = editor.update(cx, |editor, window, cx| {
14835 struct Type1;
14836 struct Type2;
14837
14838 let buffer = editor.buffer.read(cx).snapshot(cx);
14839
14840 let anchor_range =
14841 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
14842
14843 editor.highlight_background::<Type1>(
14844 &[
14845 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
14846 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
14847 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
14848 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
14849 ],
14850 |_| Hsla::red(),
14851 cx,
14852 );
14853 editor.highlight_background::<Type2>(
14854 &[
14855 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
14856 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
14857 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
14858 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
14859 ],
14860 |_| Hsla::green(),
14861 cx,
14862 );
14863
14864 let snapshot = editor.snapshot(window, cx);
14865 let mut highlighted_ranges = editor.background_highlights_in_range(
14866 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
14867 &snapshot,
14868 cx.theme(),
14869 );
14870 // Enforce a consistent ordering based on color without relying on the ordering of the
14871 // highlight's `TypeId` which is non-executor.
14872 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
14873 assert_eq!(
14874 highlighted_ranges,
14875 &[
14876 (
14877 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
14878 Hsla::red(),
14879 ),
14880 (
14881 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14882 Hsla::red(),
14883 ),
14884 (
14885 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
14886 Hsla::green(),
14887 ),
14888 (
14889 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
14890 Hsla::green(),
14891 ),
14892 ]
14893 );
14894 assert_eq!(
14895 editor.background_highlights_in_range(
14896 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
14897 &snapshot,
14898 cx.theme(),
14899 ),
14900 &[(
14901 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14902 Hsla::red(),
14903 )]
14904 );
14905 });
14906}
14907
14908#[gpui::test]
14909async fn test_following(cx: &mut TestAppContext) {
14910 init_test(cx, |_| {});
14911
14912 let fs = FakeFs::new(cx.executor());
14913 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14914
14915 let buffer = project.update(cx, |project, cx| {
14916 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
14917 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
14918 });
14919 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
14920 let follower = cx.update(|cx| {
14921 cx.open_window(
14922 WindowOptions {
14923 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
14924 gpui::Point::new(px(0.), px(0.)),
14925 gpui::Point::new(px(10.), px(80.)),
14926 ))),
14927 ..Default::default()
14928 },
14929 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
14930 )
14931 .unwrap()
14932 });
14933
14934 let is_still_following = Rc::new(RefCell::new(true));
14935 let follower_edit_event_count = Rc::new(RefCell::new(0));
14936 let pending_update = Rc::new(RefCell::new(None));
14937 let leader_entity = leader.root(cx).unwrap();
14938 let follower_entity = follower.root(cx).unwrap();
14939 _ = follower.update(cx, {
14940 let update = pending_update.clone();
14941 let is_still_following = is_still_following.clone();
14942 let follower_edit_event_count = follower_edit_event_count.clone();
14943 |_, window, cx| {
14944 cx.subscribe_in(
14945 &leader_entity,
14946 window,
14947 move |_, leader, event, window, cx| {
14948 leader.read(cx).add_event_to_update_proto(
14949 event,
14950 &mut update.borrow_mut(),
14951 window,
14952 cx,
14953 );
14954 },
14955 )
14956 .detach();
14957
14958 cx.subscribe_in(
14959 &follower_entity,
14960 window,
14961 move |_, _, event: &EditorEvent, _window, _cx| {
14962 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
14963 *is_still_following.borrow_mut() = false;
14964 }
14965
14966 if let EditorEvent::BufferEdited = event {
14967 *follower_edit_event_count.borrow_mut() += 1;
14968 }
14969 },
14970 )
14971 .detach();
14972 }
14973 });
14974
14975 // Update the selections only
14976 _ = leader.update(cx, |leader, window, cx| {
14977 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14978 s.select_ranges([1..1])
14979 });
14980 });
14981 follower
14982 .update(cx, |follower, window, cx| {
14983 follower.apply_update_proto(
14984 &project,
14985 pending_update.borrow_mut().take().unwrap(),
14986 window,
14987 cx,
14988 )
14989 })
14990 .unwrap()
14991 .await
14992 .unwrap();
14993 _ = follower.update(cx, |follower, _, cx| {
14994 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
14995 });
14996 assert!(*is_still_following.borrow());
14997 assert_eq!(*follower_edit_event_count.borrow(), 0);
14998
14999 // Update the scroll position only
15000 _ = leader.update(cx, |leader, window, cx| {
15001 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
15002 });
15003 follower
15004 .update(cx, |follower, window, cx| {
15005 follower.apply_update_proto(
15006 &project,
15007 pending_update.borrow_mut().take().unwrap(),
15008 window,
15009 cx,
15010 )
15011 })
15012 .unwrap()
15013 .await
15014 .unwrap();
15015 assert_eq!(
15016 follower
15017 .update(cx, |follower, _, cx| follower.scroll_position(cx))
15018 .unwrap(),
15019 gpui::Point::new(1.5, 3.5)
15020 );
15021 assert!(*is_still_following.borrow());
15022 assert_eq!(*follower_edit_event_count.borrow(), 0);
15023
15024 // Update the selections and scroll position. The follower's scroll position is updated
15025 // via autoscroll, not via the leader's exact scroll position.
15026 _ = leader.update(cx, |leader, window, cx| {
15027 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15028 s.select_ranges([0..0])
15029 });
15030 leader.request_autoscroll(Autoscroll::newest(), cx);
15031 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
15032 });
15033 follower
15034 .update(cx, |follower, window, cx| {
15035 follower.apply_update_proto(
15036 &project,
15037 pending_update.borrow_mut().take().unwrap(),
15038 window,
15039 cx,
15040 )
15041 })
15042 .unwrap()
15043 .await
15044 .unwrap();
15045 _ = follower.update(cx, |follower, _, cx| {
15046 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
15047 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
15048 });
15049 assert!(*is_still_following.borrow());
15050
15051 // Creating a pending selection that precedes another selection
15052 _ = leader.update(cx, |leader, window, cx| {
15053 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15054 s.select_ranges([1..1])
15055 });
15056 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
15057 });
15058 follower
15059 .update(cx, |follower, window, cx| {
15060 follower.apply_update_proto(
15061 &project,
15062 pending_update.borrow_mut().take().unwrap(),
15063 window,
15064 cx,
15065 )
15066 })
15067 .unwrap()
15068 .await
15069 .unwrap();
15070 _ = follower.update(cx, |follower, _, cx| {
15071 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
15072 });
15073 assert!(*is_still_following.borrow());
15074
15075 // Extend the pending selection so that it surrounds another selection
15076 _ = leader.update(cx, |leader, window, cx| {
15077 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
15078 });
15079 follower
15080 .update(cx, |follower, window, cx| {
15081 follower.apply_update_proto(
15082 &project,
15083 pending_update.borrow_mut().take().unwrap(),
15084 window,
15085 cx,
15086 )
15087 })
15088 .unwrap()
15089 .await
15090 .unwrap();
15091 _ = follower.update(cx, |follower, _, cx| {
15092 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
15093 });
15094
15095 // Scrolling locally breaks the follow
15096 _ = follower.update(cx, |follower, window, cx| {
15097 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
15098 follower.set_scroll_anchor(
15099 ScrollAnchor {
15100 anchor: top_anchor,
15101 offset: gpui::Point::new(0.0, 0.5),
15102 },
15103 window,
15104 cx,
15105 );
15106 });
15107 assert!(!(*is_still_following.borrow()));
15108}
15109
15110#[gpui::test]
15111async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
15112 init_test(cx, |_| {});
15113
15114 let fs = FakeFs::new(cx.executor());
15115 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
15116 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15117 let pane = workspace
15118 .update(cx, |workspace, _, _| workspace.active_pane().clone())
15119 .unwrap();
15120
15121 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15122
15123 let leader = pane.update_in(cx, |_, window, cx| {
15124 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
15125 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
15126 });
15127
15128 // Start following the editor when it has no excerpts.
15129 let mut state_message =
15130 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
15131 let workspace_entity = workspace.root(cx).unwrap();
15132 let follower_1 = cx
15133 .update_window(*workspace.deref(), |_, window, cx| {
15134 Editor::from_state_proto(
15135 workspace_entity,
15136 ViewId {
15137 creator: CollaboratorId::PeerId(PeerId::default()),
15138 id: 0,
15139 },
15140 &mut state_message,
15141 window,
15142 cx,
15143 )
15144 })
15145 .unwrap()
15146 .unwrap()
15147 .await
15148 .unwrap();
15149
15150 let update_message = Rc::new(RefCell::new(None));
15151 follower_1.update_in(cx, {
15152 let update = update_message.clone();
15153 |_, window, cx| {
15154 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
15155 leader.read(cx).add_event_to_update_proto(
15156 event,
15157 &mut update.borrow_mut(),
15158 window,
15159 cx,
15160 );
15161 })
15162 .detach();
15163 }
15164 });
15165
15166 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
15167 (
15168 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
15169 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
15170 )
15171 });
15172
15173 // Insert some excerpts.
15174 leader.update(cx, |leader, cx| {
15175 leader.buffer.update(cx, |multibuffer, cx| {
15176 multibuffer.set_excerpts_for_path(
15177 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
15178 buffer_1.clone(),
15179 vec![
15180 Point::row_range(0..3),
15181 Point::row_range(1..6),
15182 Point::row_range(12..15),
15183 ],
15184 0,
15185 cx,
15186 );
15187 multibuffer.set_excerpts_for_path(
15188 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
15189 buffer_2.clone(),
15190 vec![Point::row_range(0..6), Point::row_range(8..12)],
15191 0,
15192 cx,
15193 );
15194 });
15195 });
15196
15197 // Apply the update of adding the excerpts.
15198 follower_1
15199 .update_in(cx, |follower, window, cx| {
15200 follower.apply_update_proto(
15201 &project,
15202 update_message.borrow().clone().unwrap(),
15203 window,
15204 cx,
15205 )
15206 })
15207 .await
15208 .unwrap();
15209 assert_eq!(
15210 follower_1.update(cx, |editor, cx| editor.text(cx)),
15211 leader.update(cx, |editor, cx| editor.text(cx))
15212 );
15213 update_message.borrow_mut().take();
15214
15215 // Start following separately after it already has excerpts.
15216 let mut state_message =
15217 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
15218 let workspace_entity = workspace.root(cx).unwrap();
15219 let follower_2 = cx
15220 .update_window(*workspace.deref(), |_, window, cx| {
15221 Editor::from_state_proto(
15222 workspace_entity,
15223 ViewId {
15224 creator: CollaboratorId::PeerId(PeerId::default()),
15225 id: 0,
15226 },
15227 &mut state_message,
15228 window,
15229 cx,
15230 )
15231 })
15232 .unwrap()
15233 .unwrap()
15234 .await
15235 .unwrap();
15236 assert_eq!(
15237 follower_2.update(cx, |editor, cx| editor.text(cx)),
15238 leader.update(cx, |editor, cx| editor.text(cx))
15239 );
15240
15241 // Remove some excerpts.
15242 leader.update(cx, |leader, cx| {
15243 leader.buffer.update(cx, |multibuffer, cx| {
15244 let excerpt_ids = multibuffer.excerpt_ids();
15245 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
15246 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
15247 });
15248 });
15249
15250 // Apply the update of removing the excerpts.
15251 follower_1
15252 .update_in(cx, |follower, window, cx| {
15253 follower.apply_update_proto(
15254 &project,
15255 update_message.borrow().clone().unwrap(),
15256 window,
15257 cx,
15258 )
15259 })
15260 .await
15261 .unwrap();
15262 follower_2
15263 .update_in(cx, |follower, window, cx| {
15264 follower.apply_update_proto(
15265 &project,
15266 update_message.borrow().clone().unwrap(),
15267 window,
15268 cx,
15269 )
15270 })
15271 .await
15272 .unwrap();
15273 update_message.borrow_mut().take();
15274 assert_eq!(
15275 follower_1.update(cx, |editor, cx| editor.text(cx)),
15276 leader.update(cx, |editor, cx| editor.text(cx))
15277 );
15278}
15279
15280#[gpui::test]
15281async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15282 init_test(cx, |_| {});
15283
15284 let mut cx = EditorTestContext::new(cx).await;
15285 let lsp_store =
15286 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
15287
15288 cx.set_state(indoc! {"
15289 ˇfn func(abc def: i32) -> u32 {
15290 }
15291 "});
15292
15293 cx.update(|_, cx| {
15294 lsp_store.update(cx, |lsp_store, cx| {
15295 lsp_store
15296 .update_diagnostics(
15297 LanguageServerId(0),
15298 lsp::PublishDiagnosticsParams {
15299 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
15300 version: None,
15301 diagnostics: vec![
15302 lsp::Diagnostic {
15303 range: lsp::Range::new(
15304 lsp::Position::new(0, 11),
15305 lsp::Position::new(0, 12),
15306 ),
15307 severity: Some(lsp::DiagnosticSeverity::ERROR),
15308 ..Default::default()
15309 },
15310 lsp::Diagnostic {
15311 range: lsp::Range::new(
15312 lsp::Position::new(0, 12),
15313 lsp::Position::new(0, 15),
15314 ),
15315 severity: Some(lsp::DiagnosticSeverity::ERROR),
15316 ..Default::default()
15317 },
15318 lsp::Diagnostic {
15319 range: lsp::Range::new(
15320 lsp::Position::new(0, 25),
15321 lsp::Position::new(0, 28),
15322 ),
15323 severity: Some(lsp::DiagnosticSeverity::ERROR),
15324 ..Default::default()
15325 },
15326 ],
15327 },
15328 None,
15329 DiagnosticSourceKind::Pushed,
15330 &[],
15331 cx,
15332 )
15333 .unwrap()
15334 });
15335 });
15336
15337 executor.run_until_parked();
15338
15339 cx.update_editor(|editor, window, cx| {
15340 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15341 });
15342
15343 cx.assert_editor_state(indoc! {"
15344 fn func(abc def: i32) -> ˇu32 {
15345 }
15346 "});
15347
15348 cx.update_editor(|editor, window, cx| {
15349 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15350 });
15351
15352 cx.assert_editor_state(indoc! {"
15353 fn func(abc ˇdef: i32) -> u32 {
15354 }
15355 "});
15356
15357 cx.update_editor(|editor, window, cx| {
15358 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15359 });
15360
15361 cx.assert_editor_state(indoc! {"
15362 fn func(abcˇ def: i32) -> u32 {
15363 }
15364 "});
15365
15366 cx.update_editor(|editor, window, cx| {
15367 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15368 });
15369
15370 cx.assert_editor_state(indoc! {"
15371 fn func(abc def: i32) -> ˇu32 {
15372 }
15373 "});
15374}
15375
15376#[gpui::test]
15377async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15378 init_test(cx, |_| {});
15379
15380 let mut cx = EditorTestContext::new(cx).await;
15381
15382 let diff_base = r#"
15383 use some::mod;
15384
15385 const A: u32 = 42;
15386
15387 fn main() {
15388 println!("hello");
15389
15390 println!("world");
15391 }
15392 "#
15393 .unindent();
15394
15395 // Edits are modified, removed, modified, added
15396 cx.set_state(
15397 &r#"
15398 use some::modified;
15399
15400 ˇ
15401 fn main() {
15402 println!("hello there");
15403
15404 println!("around the");
15405 println!("world");
15406 }
15407 "#
15408 .unindent(),
15409 );
15410
15411 cx.set_head_text(&diff_base);
15412 executor.run_until_parked();
15413
15414 cx.update_editor(|editor, window, cx| {
15415 //Wrap around the bottom of the buffer
15416 for _ in 0..3 {
15417 editor.go_to_next_hunk(&GoToHunk, window, cx);
15418 }
15419 });
15420
15421 cx.assert_editor_state(
15422 &r#"
15423 ˇuse some::modified;
15424
15425
15426 fn main() {
15427 println!("hello there");
15428
15429 println!("around the");
15430 println!("world");
15431 }
15432 "#
15433 .unindent(),
15434 );
15435
15436 cx.update_editor(|editor, window, cx| {
15437 //Wrap around the top of the buffer
15438 for _ in 0..2 {
15439 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15440 }
15441 });
15442
15443 cx.assert_editor_state(
15444 &r#"
15445 use some::modified;
15446
15447
15448 fn main() {
15449 ˇ println!("hello there");
15450
15451 println!("around the");
15452 println!("world");
15453 }
15454 "#
15455 .unindent(),
15456 );
15457
15458 cx.update_editor(|editor, window, cx| {
15459 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15460 });
15461
15462 cx.assert_editor_state(
15463 &r#"
15464 use some::modified;
15465
15466 ˇ
15467 fn main() {
15468 println!("hello there");
15469
15470 println!("around the");
15471 println!("world");
15472 }
15473 "#
15474 .unindent(),
15475 );
15476
15477 cx.update_editor(|editor, window, cx| {
15478 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15479 });
15480
15481 cx.assert_editor_state(
15482 &r#"
15483 ˇuse some::modified;
15484
15485
15486 fn main() {
15487 println!("hello there");
15488
15489 println!("around the");
15490 println!("world");
15491 }
15492 "#
15493 .unindent(),
15494 );
15495
15496 cx.update_editor(|editor, window, cx| {
15497 for _ in 0..2 {
15498 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15499 }
15500 });
15501
15502 cx.assert_editor_state(
15503 &r#"
15504 use some::modified;
15505
15506
15507 fn main() {
15508 ˇ println!("hello there");
15509
15510 println!("around the");
15511 println!("world");
15512 }
15513 "#
15514 .unindent(),
15515 );
15516
15517 cx.update_editor(|editor, window, cx| {
15518 editor.fold(&Fold, window, cx);
15519 });
15520
15521 cx.update_editor(|editor, window, cx| {
15522 editor.go_to_next_hunk(&GoToHunk, window, cx);
15523 });
15524
15525 cx.assert_editor_state(
15526 &r#"
15527 ˇuse some::modified;
15528
15529
15530 fn main() {
15531 println!("hello there");
15532
15533 println!("around the");
15534 println!("world");
15535 }
15536 "#
15537 .unindent(),
15538 );
15539}
15540
15541#[test]
15542fn test_split_words() {
15543 fn split(text: &str) -> Vec<&str> {
15544 split_words(text).collect()
15545 }
15546
15547 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
15548 assert_eq!(split("hello_world"), &["hello_", "world"]);
15549 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
15550 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
15551 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
15552 assert_eq!(split("helloworld"), &["helloworld"]);
15553
15554 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
15555}
15556
15557#[gpui::test]
15558async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
15559 init_test(cx, |_| {});
15560
15561 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
15562 let mut assert = |before, after| {
15563 let _state_context = cx.set_state(before);
15564 cx.run_until_parked();
15565 cx.update_editor(|editor, window, cx| {
15566 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
15567 });
15568 cx.run_until_parked();
15569 cx.assert_editor_state(after);
15570 };
15571
15572 // Outside bracket jumps to outside of matching bracket
15573 assert("console.logˇ(var);", "console.log(var)ˇ;");
15574 assert("console.log(var)ˇ;", "console.logˇ(var);");
15575
15576 // Inside bracket jumps to inside of matching bracket
15577 assert("console.log(ˇvar);", "console.log(varˇ);");
15578 assert("console.log(varˇ);", "console.log(ˇvar);");
15579
15580 // When outside a bracket and inside, favor jumping to the inside bracket
15581 assert(
15582 "console.log('foo', [1, 2, 3]ˇ);",
15583 "console.log(ˇ'foo', [1, 2, 3]);",
15584 );
15585 assert(
15586 "console.log(ˇ'foo', [1, 2, 3]);",
15587 "console.log('foo', [1, 2, 3]ˇ);",
15588 );
15589
15590 // Bias forward if two options are equally likely
15591 assert(
15592 "let result = curried_fun()ˇ();",
15593 "let result = curried_fun()()ˇ;",
15594 );
15595
15596 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
15597 assert(
15598 indoc! {"
15599 function test() {
15600 console.log('test')ˇ
15601 }"},
15602 indoc! {"
15603 function test() {
15604 console.logˇ('test')
15605 }"},
15606 );
15607}
15608
15609#[gpui::test]
15610async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
15611 init_test(cx, |_| {});
15612
15613 let fs = FakeFs::new(cx.executor());
15614 fs.insert_tree(
15615 path!("/a"),
15616 json!({
15617 "main.rs": "fn main() { let a = 5; }",
15618 "other.rs": "// Test file",
15619 }),
15620 )
15621 .await;
15622 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15623
15624 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15625 language_registry.add(Arc::new(Language::new(
15626 LanguageConfig {
15627 name: "Rust".into(),
15628 matcher: LanguageMatcher {
15629 path_suffixes: vec!["rs".to_string()],
15630 ..Default::default()
15631 },
15632 brackets: BracketPairConfig {
15633 pairs: vec![BracketPair {
15634 start: "{".to_string(),
15635 end: "}".to_string(),
15636 close: true,
15637 surround: true,
15638 newline: true,
15639 }],
15640 disabled_scopes_by_bracket_ix: Vec::new(),
15641 },
15642 ..Default::default()
15643 },
15644 Some(tree_sitter_rust::LANGUAGE.into()),
15645 )));
15646 let mut fake_servers = language_registry.register_fake_lsp(
15647 "Rust",
15648 FakeLspAdapter {
15649 capabilities: lsp::ServerCapabilities {
15650 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15651 first_trigger_character: "{".to_string(),
15652 more_trigger_character: None,
15653 }),
15654 ..Default::default()
15655 },
15656 ..Default::default()
15657 },
15658 );
15659
15660 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15661
15662 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15663
15664 let worktree_id = workspace
15665 .update(cx, |workspace, _, cx| {
15666 workspace.project().update(cx, |project, cx| {
15667 project.worktrees(cx).next().unwrap().read(cx).id()
15668 })
15669 })
15670 .unwrap();
15671
15672 let buffer = project
15673 .update(cx, |project, cx| {
15674 project.open_local_buffer(path!("/a/main.rs"), cx)
15675 })
15676 .await
15677 .unwrap();
15678 let editor_handle = workspace
15679 .update(cx, |workspace, window, cx| {
15680 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
15681 })
15682 .unwrap()
15683 .await
15684 .unwrap()
15685 .downcast::<Editor>()
15686 .unwrap();
15687
15688 cx.executor().start_waiting();
15689 let fake_server = fake_servers.next().await.unwrap();
15690
15691 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
15692 |params, _| async move {
15693 assert_eq!(
15694 params.text_document_position.text_document.uri,
15695 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
15696 );
15697 assert_eq!(
15698 params.text_document_position.position,
15699 lsp::Position::new(0, 21),
15700 );
15701
15702 Ok(Some(vec![lsp::TextEdit {
15703 new_text: "]".to_string(),
15704 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15705 }]))
15706 },
15707 );
15708
15709 editor_handle.update_in(cx, |editor, window, cx| {
15710 window.focus(&editor.focus_handle(cx));
15711 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15712 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
15713 });
15714 editor.handle_input("{", window, cx);
15715 });
15716
15717 cx.executor().run_until_parked();
15718
15719 buffer.update(cx, |buffer, _| {
15720 assert_eq!(
15721 buffer.text(),
15722 "fn main() { let a = {5}; }",
15723 "No extra braces from on type formatting should appear in the buffer"
15724 )
15725 });
15726}
15727
15728#[gpui::test(iterations = 20, seeds(31))]
15729async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
15730 init_test(cx, |_| {});
15731
15732 let mut cx = EditorLspTestContext::new_rust(
15733 lsp::ServerCapabilities {
15734 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15735 first_trigger_character: ".".to_string(),
15736 more_trigger_character: None,
15737 }),
15738 ..Default::default()
15739 },
15740 cx,
15741 )
15742 .await;
15743
15744 cx.update_buffer(|buffer, _| {
15745 // This causes autoindent to be async.
15746 buffer.set_sync_parse_timeout(Duration::ZERO)
15747 });
15748
15749 cx.set_state("fn c() {\n d()ˇ\n}\n");
15750 cx.simulate_keystroke("\n");
15751 cx.run_until_parked();
15752
15753 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
15754 let mut request =
15755 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
15756 let buffer_cloned = buffer_cloned.clone();
15757 async move {
15758 buffer_cloned.update(&mut cx, |buffer, _| {
15759 assert_eq!(
15760 buffer.text(),
15761 "fn c() {\n d()\n .\n}\n",
15762 "OnTypeFormatting should triggered after autoindent applied"
15763 )
15764 })?;
15765
15766 Ok(Some(vec![]))
15767 }
15768 });
15769
15770 cx.simulate_keystroke(".");
15771 cx.run_until_parked();
15772
15773 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
15774 assert!(request.next().await.is_some());
15775 request.close();
15776 assert!(request.next().await.is_none());
15777}
15778
15779#[gpui::test]
15780async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
15781 init_test(cx, |_| {});
15782
15783 let fs = FakeFs::new(cx.executor());
15784 fs.insert_tree(
15785 path!("/a"),
15786 json!({
15787 "main.rs": "fn main() { let a = 5; }",
15788 "other.rs": "// Test file",
15789 }),
15790 )
15791 .await;
15792
15793 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15794
15795 let server_restarts = Arc::new(AtomicUsize::new(0));
15796 let closure_restarts = Arc::clone(&server_restarts);
15797 let language_server_name = "test language server";
15798 let language_name: LanguageName = "Rust".into();
15799
15800 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15801 language_registry.add(Arc::new(Language::new(
15802 LanguageConfig {
15803 name: language_name.clone(),
15804 matcher: LanguageMatcher {
15805 path_suffixes: vec!["rs".to_string()],
15806 ..Default::default()
15807 },
15808 ..Default::default()
15809 },
15810 Some(tree_sitter_rust::LANGUAGE.into()),
15811 )));
15812 let mut fake_servers = language_registry.register_fake_lsp(
15813 "Rust",
15814 FakeLspAdapter {
15815 name: language_server_name,
15816 initialization_options: Some(json!({
15817 "testOptionValue": true
15818 })),
15819 initializer: Some(Box::new(move |fake_server| {
15820 let task_restarts = Arc::clone(&closure_restarts);
15821 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
15822 task_restarts.fetch_add(1, atomic::Ordering::Release);
15823 futures::future::ready(Ok(()))
15824 });
15825 })),
15826 ..Default::default()
15827 },
15828 );
15829
15830 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15831 let _buffer = project
15832 .update(cx, |project, cx| {
15833 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
15834 })
15835 .await
15836 .unwrap();
15837 let _fake_server = fake_servers.next().await.unwrap();
15838 update_test_language_settings(cx, |language_settings| {
15839 language_settings.languages.0.insert(
15840 language_name.clone(),
15841 LanguageSettingsContent {
15842 tab_size: NonZeroU32::new(8),
15843 ..Default::default()
15844 },
15845 );
15846 });
15847 cx.executor().run_until_parked();
15848 assert_eq!(
15849 server_restarts.load(atomic::Ordering::Acquire),
15850 0,
15851 "Should not restart LSP server on an unrelated change"
15852 );
15853
15854 update_test_project_settings(cx, |project_settings| {
15855 project_settings.lsp.insert(
15856 "Some other server name".into(),
15857 LspSettings {
15858 binary: None,
15859 settings: None,
15860 initialization_options: Some(json!({
15861 "some other init value": false
15862 })),
15863 enable_lsp_tasks: false,
15864 },
15865 );
15866 });
15867 cx.executor().run_until_parked();
15868 assert_eq!(
15869 server_restarts.load(atomic::Ordering::Acquire),
15870 0,
15871 "Should not restart LSP server on an unrelated LSP settings change"
15872 );
15873
15874 update_test_project_settings(cx, |project_settings| {
15875 project_settings.lsp.insert(
15876 language_server_name.into(),
15877 LspSettings {
15878 binary: None,
15879 settings: None,
15880 initialization_options: Some(json!({
15881 "anotherInitValue": false
15882 })),
15883 enable_lsp_tasks: false,
15884 },
15885 );
15886 });
15887 cx.executor().run_until_parked();
15888 assert_eq!(
15889 server_restarts.load(atomic::Ordering::Acquire),
15890 1,
15891 "Should restart LSP server on a related LSP settings change"
15892 );
15893
15894 update_test_project_settings(cx, |project_settings| {
15895 project_settings.lsp.insert(
15896 language_server_name.into(),
15897 LspSettings {
15898 binary: None,
15899 settings: None,
15900 initialization_options: Some(json!({
15901 "anotherInitValue": false
15902 })),
15903 enable_lsp_tasks: false,
15904 },
15905 );
15906 });
15907 cx.executor().run_until_parked();
15908 assert_eq!(
15909 server_restarts.load(atomic::Ordering::Acquire),
15910 1,
15911 "Should not restart LSP server on a related LSP settings change that is the same"
15912 );
15913
15914 update_test_project_settings(cx, |project_settings| {
15915 project_settings.lsp.insert(
15916 language_server_name.into(),
15917 LspSettings {
15918 binary: None,
15919 settings: None,
15920 initialization_options: None,
15921 enable_lsp_tasks: false,
15922 },
15923 );
15924 });
15925 cx.executor().run_until_parked();
15926 assert_eq!(
15927 server_restarts.load(atomic::Ordering::Acquire),
15928 2,
15929 "Should restart LSP server on another related LSP settings change"
15930 );
15931}
15932
15933#[gpui::test]
15934async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
15935 init_test(cx, |_| {});
15936
15937 let mut cx = EditorLspTestContext::new_rust(
15938 lsp::ServerCapabilities {
15939 completion_provider: Some(lsp::CompletionOptions {
15940 trigger_characters: Some(vec![".".to_string()]),
15941 resolve_provider: Some(true),
15942 ..Default::default()
15943 }),
15944 ..Default::default()
15945 },
15946 cx,
15947 )
15948 .await;
15949
15950 cx.set_state("fn main() { let a = 2ˇ; }");
15951 cx.simulate_keystroke(".");
15952 let completion_item = lsp::CompletionItem {
15953 label: "some".into(),
15954 kind: Some(lsp::CompletionItemKind::SNIPPET),
15955 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15956 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15957 kind: lsp::MarkupKind::Markdown,
15958 value: "```rust\nSome(2)\n```".to_string(),
15959 })),
15960 deprecated: Some(false),
15961 sort_text: Some("fffffff2".to_string()),
15962 filter_text: Some("some".to_string()),
15963 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15964 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15965 range: lsp::Range {
15966 start: lsp::Position {
15967 line: 0,
15968 character: 22,
15969 },
15970 end: lsp::Position {
15971 line: 0,
15972 character: 22,
15973 },
15974 },
15975 new_text: "Some(2)".to_string(),
15976 })),
15977 additional_text_edits: Some(vec![lsp::TextEdit {
15978 range: lsp::Range {
15979 start: lsp::Position {
15980 line: 0,
15981 character: 20,
15982 },
15983 end: lsp::Position {
15984 line: 0,
15985 character: 22,
15986 },
15987 },
15988 new_text: "".to_string(),
15989 }]),
15990 ..Default::default()
15991 };
15992
15993 let closure_completion_item = completion_item.clone();
15994 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15995 let task_completion_item = closure_completion_item.clone();
15996 async move {
15997 Ok(Some(lsp::CompletionResponse::Array(vec![
15998 task_completion_item,
15999 ])))
16000 }
16001 });
16002
16003 request.next().await;
16004
16005 cx.condition(|editor, _| editor.context_menu_visible())
16006 .await;
16007 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
16008 editor
16009 .confirm_completion(&ConfirmCompletion::default(), window, cx)
16010 .unwrap()
16011 });
16012 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
16013
16014 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
16015 let task_completion_item = completion_item.clone();
16016 async move { Ok(task_completion_item) }
16017 })
16018 .next()
16019 .await
16020 .unwrap();
16021 apply_additional_edits.await.unwrap();
16022 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
16023}
16024
16025#[gpui::test]
16026async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
16027 init_test(cx, |_| {});
16028
16029 let mut cx = EditorLspTestContext::new_rust(
16030 lsp::ServerCapabilities {
16031 completion_provider: Some(lsp::CompletionOptions {
16032 trigger_characters: Some(vec![".".to_string()]),
16033 resolve_provider: Some(true),
16034 ..Default::default()
16035 }),
16036 ..Default::default()
16037 },
16038 cx,
16039 )
16040 .await;
16041
16042 cx.set_state("fn main() { let a = 2ˇ; }");
16043 cx.simulate_keystroke(".");
16044
16045 let item1 = lsp::CompletionItem {
16046 label: "method id()".to_string(),
16047 filter_text: Some("id".to_string()),
16048 detail: None,
16049 documentation: None,
16050 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16051 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16052 new_text: ".id".to_string(),
16053 })),
16054 ..lsp::CompletionItem::default()
16055 };
16056
16057 let item2 = lsp::CompletionItem {
16058 label: "other".to_string(),
16059 filter_text: Some("other".to_string()),
16060 detail: None,
16061 documentation: None,
16062 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16063 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16064 new_text: ".other".to_string(),
16065 })),
16066 ..lsp::CompletionItem::default()
16067 };
16068
16069 let item1 = item1.clone();
16070 cx.set_request_handler::<lsp::request::Completion, _, _>({
16071 let item1 = item1.clone();
16072 move |_, _, _| {
16073 let item1 = item1.clone();
16074 let item2 = item2.clone();
16075 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
16076 }
16077 })
16078 .next()
16079 .await;
16080
16081 cx.condition(|editor, _| editor.context_menu_visible())
16082 .await;
16083 cx.update_editor(|editor, _, _| {
16084 let context_menu = editor.context_menu.borrow_mut();
16085 let context_menu = context_menu
16086 .as_ref()
16087 .expect("Should have the context menu deployed");
16088 match context_menu {
16089 CodeContextMenu::Completions(completions_menu) => {
16090 let completions = completions_menu.completions.borrow_mut();
16091 assert_eq!(
16092 completions
16093 .iter()
16094 .map(|completion| &completion.label.text)
16095 .collect::<Vec<_>>(),
16096 vec!["method id()", "other"]
16097 )
16098 }
16099 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
16100 }
16101 });
16102
16103 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
16104 let item1 = item1.clone();
16105 move |_, item_to_resolve, _| {
16106 let item1 = item1.clone();
16107 async move {
16108 if item1 == item_to_resolve {
16109 Ok(lsp::CompletionItem {
16110 label: "method id()".to_string(),
16111 filter_text: Some("id".to_string()),
16112 detail: Some("Now resolved!".to_string()),
16113 documentation: Some(lsp::Documentation::String("Docs".to_string())),
16114 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16115 range: lsp::Range::new(
16116 lsp::Position::new(0, 22),
16117 lsp::Position::new(0, 22),
16118 ),
16119 new_text: ".id".to_string(),
16120 })),
16121 ..lsp::CompletionItem::default()
16122 })
16123 } else {
16124 Ok(item_to_resolve)
16125 }
16126 }
16127 }
16128 })
16129 .next()
16130 .await
16131 .unwrap();
16132 cx.run_until_parked();
16133
16134 cx.update_editor(|editor, window, cx| {
16135 editor.context_menu_next(&Default::default(), window, cx);
16136 });
16137
16138 cx.update_editor(|editor, _, _| {
16139 let context_menu = editor.context_menu.borrow_mut();
16140 let context_menu = context_menu
16141 .as_ref()
16142 .expect("Should have the context menu deployed");
16143 match context_menu {
16144 CodeContextMenu::Completions(completions_menu) => {
16145 let completions = completions_menu.completions.borrow_mut();
16146 assert_eq!(
16147 completions
16148 .iter()
16149 .map(|completion| &completion.label.text)
16150 .collect::<Vec<_>>(),
16151 vec!["method id() Now resolved!", "other"],
16152 "Should update first completion label, but not second as the filter text did not match."
16153 );
16154 }
16155 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
16156 }
16157 });
16158}
16159
16160#[gpui::test]
16161async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
16162 init_test(cx, |_| {});
16163 let mut cx = EditorLspTestContext::new_rust(
16164 lsp::ServerCapabilities {
16165 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
16166 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
16167 completion_provider: Some(lsp::CompletionOptions {
16168 resolve_provider: Some(true),
16169 ..Default::default()
16170 }),
16171 ..Default::default()
16172 },
16173 cx,
16174 )
16175 .await;
16176 cx.set_state(indoc! {"
16177 struct TestStruct {
16178 field: i32
16179 }
16180
16181 fn mainˇ() {
16182 let unused_var = 42;
16183 let test_struct = TestStruct { field: 42 };
16184 }
16185 "});
16186 let symbol_range = cx.lsp_range(indoc! {"
16187 struct TestStruct {
16188 field: i32
16189 }
16190
16191 «fn main»() {
16192 let unused_var = 42;
16193 let test_struct = TestStruct { field: 42 };
16194 }
16195 "});
16196 let mut hover_requests =
16197 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
16198 Ok(Some(lsp::Hover {
16199 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
16200 kind: lsp::MarkupKind::Markdown,
16201 value: "Function documentation".to_string(),
16202 }),
16203 range: Some(symbol_range),
16204 }))
16205 });
16206
16207 // Case 1: Test that code action menu hide hover popover
16208 cx.dispatch_action(Hover);
16209 hover_requests.next().await;
16210 cx.condition(|editor, _| editor.hover_state.visible()).await;
16211 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
16212 move |_, _, _| async move {
16213 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
16214 lsp::CodeAction {
16215 title: "Remove unused variable".to_string(),
16216 kind: Some(CodeActionKind::QUICKFIX),
16217 edit: Some(lsp::WorkspaceEdit {
16218 changes: Some(
16219 [(
16220 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
16221 vec![lsp::TextEdit {
16222 range: lsp::Range::new(
16223 lsp::Position::new(5, 4),
16224 lsp::Position::new(5, 27),
16225 ),
16226 new_text: "".to_string(),
16227 }],
16228 )]
16229 .into_iter()
16230 .collect(),
16231 ),
16232 ..Default::default()
16233 }),
16234 ..Default::default()
16235 },
16236 )]))
16237 },
16238 );
16239 cx.update_editor(|editor, window, cx| {
16240 editor.toggle_code_actions(
16241 &ToggleCodeActions {
16242 deployed_from: None,
16243 quick_launch: false,
16244 },
16245 window,
16246 cx,
16247 );
16248 });
16249 code_action_requests.next().await;
16250 cx.run_until_parked();
16251 cx.condition(|editor, _| editor.context_menu_visible())
16252 .await;
16253 cx.update_editor(|editor, _, _| {
16254 assert!(
16255 !editor.hover_state.visible(),
16256 "Hover popover should be hidden when code action menu is shown"
16257 );
16258 // Hide code actions
16259 editor.context_menu.take();
16260 });
16261
16262 // Case 2: Test that code completions hide hover popover
16263 cx.dispatch_action(Hover);
16264 hover_requests.next().await;
16265 cx.condition(|editor, _| editor.hover_state.visible()).await;
16266 let counter = Arc::new(AtomicUsize::new(0));
16267 let mut completion_requests =
16268 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16269 let counter = counter.clone();
16270 async move {
16271 counter.fetch_add(1, atomic::Ordering::Release);
16272 Ok(Some(lsp::CompletionResponse::Array(vec![
16273 lsp::CompletionItem {
16274 label: "main".into(),
16275 kind: Some(lsp::CompletionItemKind::FUNCTION),
16276 detail: Some("() -> ()".to_string()),
16277 ..Default::default()
16278 },
16279 lsp::CompletionItem {
16280 label: "TestStruct".into(),
16281 kind: Some(lsp::CompletionItemKind::STRUCT),
16282 detail: Some("struct TestStruct".to_string()),
16283 ..Default::default()
16284 },
16285 ])))
16286 }
16287 });
16288 cx.update_editor(|editor, window, cx| {
16289 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
16290 });
16291 completion_requests.next().await;
16292 cx.condition(|editor, _| editor.context_menu_visible())
16293 .await;
16294 cx.update_editor(|editor, _, _| {
16295 assert!(
16296 !editor.hover_state.visible(),
16297 "Hover popover should be hidden when completion menu is shown"
16298 );
16299 });
16300}
16301
16302#[gpui::test]
16303async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
16304 init_test(cx, |_| {});
16305
16306 let mut cx = EditorLspTestContext::new_rust(
16307 lsp::ServerCapabilities {
16308 completion_provider: Some(lsp::CompletionOptions {
16309 trigger_characters: Some(vec![".".to_string()]),
16310 resolve_provider: Some(true),
16311 ..Default::default()
16312 }),
16313 ..Default::default()
16314 },
16315 cx,
16316 )
16317 .await;
16318
16319 cx.set_state("fn main() { let a = 2ˇ; }");
16320 cx.simulate_keystroke(".");
16321
16322 let unresolved_item_1 = lsp::CompletionItem {
16323 label: "id".to_string(),
16324 filter_text: Some("id".to_string()),
16325 detail: None,
16326 documentation: None,
16327 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16328 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16329 new_text: ".id".to_string(),
16330 })),
16331 ..lsp::CompletionItem::default()
16332 };
16333 let resolved_item_1 = lsp::CompletionItem {
16334 additional_text_edits: Some(vec![lsp::TextEdit {
16335 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
16336 new_text: "!!".to_string(),
16337 }]),
16338 ..unresolved_item_1.clone()
16339 };
16340 let unresolved_item_2 = lsp::CompletionItem {
16341 label: "other".to_string(),
16342 filter_text: Some("other".to_string()),
16343 detail: None,
16344 documentation: None,
16345 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16346 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16347 new_text: ".other".to_string(),
16348 })),
16349 ..lsp::CompletionItem::default()
16350 };
16351 let resolved_item_2 = lsp::CompletionItem {
16352 additional_text_edits: Some(vec![lsp::TextEdit {
16353 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
16354 new_text: "??".to_string(),
16355 }]),
16356 ..unresolved_item_2.clone()
16357 };
16358
16359 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
16360 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
16361 cx.lsp
16362 .server
16363 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
16364 let unresolved_item_1 = unresolved_item_1.clone();
16365 let resolved_item_1 = resolved_item_1.clone();
16366 let unresolved_item_2 = unresolved_item_2.clone();
16367 let resolved_item_2 = resolved_item_2.clone();
16368 let resolve_requests_1 = resolve_requests_1.clone();
16369 let resolve_requests_2 = resolve_requests_2.clone();
16370 move |unresolved_request, _| {
16371 let unresolved_item_1 = unresolved_item_1.clone();
16372 let resolved_item_1 = resolved_item_1.clone();
16373 let unresolved_item_2 = unresolved_item_2.clone();
16374 let resolved_item_2 = resolved_item_2.clone();
16375 let resolve_requests_1 = resolve_requests_1.clone();
16376 let resolve_requests_2 = resolve_requests_2.clone();
16377 async move {
16378 if unresolved_request == unresolved_item_1 {
16379 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
16380 Ok(resolved_item_1.clone())
16381 } else if unresolved_request == unresolved_item_2 {
16382 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
16383 Ok(resolved_item_2.clone())
16384 } else {
16385 panic!("Unexpected completion item {unresolved_request:?}")
16386 }
16387 }
16388 }
16389 })
16390 .detach();
16391
16392 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16393 let unresolved_item_1 = unresolved_item_1.clone();
16394 let unresolved_item_2 = unresolved_item_2.clone();
16395 async move {
16396 Ok(Some(lsp::CompletionResponse::Array(vec![
16397 unresolved_item_1,
16398 unresolved_item_2,
16399 ])))
16400 }
16401 })
16402 .next()
16403 .await;
16404
16405 cx.condition(|editor, _| editor.context_menu_visible())
16406 .await;
16407 cx.update_editor(|editor, _, _| {
16408 let context_menu = editor.context_menu.borrow_mut();
16409 let context_menu = context_menu
16410 .as_ref()
16411 .expect("Should have the context menu deployed");
16412 match context_menu {
16413 CodeContextMenu::Completions(completions_menu) => {
16414 let completions = completions_menu.completions.borrow_mut();
16415 assert_eq!(
16416 completions
16417 .iter()
16418 .map(|completion| &completion.label.text)
16419 .collect::<Vec<_>>(),
16420 vec!["id", "other"]
16421 )
16422 }
16423 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
16424 }
16425 });
16426 cx.run_until_parked();
16427
16428 cx.update_editor(|editor, window, cx| {
16429 editor.context_menu_next(&ContextMenuNext, window, cx);
16430 });
16431 cx.run_until_parked();
16432 cx.update_editor(|editor, window, cx| {
16433 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
16434 });
16435 cx.run_until_parked();
16436 cx.update_editor(|editor, window, cx| {
16437 editor.context_menu_next(&ContextMenuNext, window, cx);
16438 });
16439 cx.run_until_parked();
16440 cx.update_editor(|editor, window, cx| {
16441 editor
16442 .compose_completion(&ComposeCompletion::default(), window, cx)
16443 .expect("No task returned")
16444 })
16445 .await
16446 .expect("Completion failed");
16447 cx.run_until_parked();
16448
16449 cx.update_editor(|editor, _, cx| {
16450 assert_eq!(
16451 resolve_requests_1.load(atomic::Ordering::Acquire),
16452 1,
16453 "Should always resolve once despite multiple selections"
16454 );
16455 assert_eq!(
16456 resolve_requests_2.load(atomic::Ordering::Acquire),
16457 1,
16458 "Should always resolve once after multiple selections and applying the completion"
16459 );
16460 assert_eq!(
16461 editor.text(cx),
16462 "fn main() { let a = ??.other; }",
16463 "Should use resolved data when applying the completion"
16464 );
16465 });
16466}
16467
16468#[gpui::test]
16469async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
16470 init_test(cx, |_| {});
16471
16472 let item_0 = lsp::CompletionItem {
16473 label: "abs".into(),
16474 insert_text: Some("abs".into()),
16475 data: Some(json!({ "very": "special"})),
16476 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
16477 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
16478 lsp::InsertReplaceEdit {
16479 new_text: "abs".to_string(),
16480 insert: lsp::Range::default(),
16481 replace: lsp::Range::default(),
16482 },
16483 )),
16484 ..lsp::CompletionItem::default()
16485 };
16486 let items = iter::once(item_0.clone())
16487 .chain((11..51).map(|i| lsp::CompletionItem {
16488 label: format!("item_{}", i),
16489 insert_text: Some(format!("item_{}", i)),
16490 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
16491 ..lsp::CompletionItem::default()
16492 }))
16493 .collect::<Vec<_>>();
16494
16495 let default_commit_characters = vec!["?".to_string()];
16496 let default_data = json!({ "default": "data"});
16497 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
16498 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
16499 let default_edit_range = lsp::Range {
16500 start: lsp::Position {
16501 line: 0,
16502 character: 5,
16503 },
16504 end: lsp::Position {
16505 line: 0,
16506 character: 5,
16507 },
16508 };
16509
16510 let mut cx = EditorLspTestContext::new_rust(
16511 lsp::ServerCapabilities {
16512 completion_provider: Some(lsp::CompletionOptions {
16513 trigger_characters: Some(vec![".".to_string()]),
16514 resolve_provider: Some(true),
16515 ..Default::default()
16516 }),
16517 ..Default::default()
16518 },
16519 cx,
16520 )
16521 .await;
16522
16523 cx.set_state("fn main() { let a = 2ˇ; }");
16524 cx.simulate_keystroke(".");
16525
16526 let completion_data = default_data.clone();
16527 let completion_characters = default_commit_characters.clone();
16528 let completion_items = items.clone();
16529 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16530 let default_data = completion_data.clone();
16531 let default_commit_characters = completion_characters.clone();
16532 let items = completion_items.clone();
16533 async move {
16534 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16535 items,
16536 item_defaults: Some(lsp::CompletionListItemDefaults {
16537 data: Some(default_data.clone()),
16538 commit_characters: Some(default_commit_characters.clone()),
16539 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
16540 default_edit_range,
16541 )),
16542 insert_text_format: Some(default_insert_text_format),
16543 insert_text_mode: Some(default_insert_text_mode),
16544 }),
16545 ..lsp::CompletionList::default()
16546 })))
16547 }
16548 })
16549 .next()
16550 .await;
16551
16552 let resolved_items = Arc::new(Mutex::new(Vec::new()));
16553 cx.lsp
16554 .server
16555 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
16556 let closure_resolved_items = resolved_items.clone();
16557 move |item_to_resolve, _| {
16558 let closure_resolved_items = closure_resolved_items.clone();
16559 async move {
16560 closure_resolved_items.lock().push(item_to_resolve.clone());
16561 Ok(item_to_resolve)
16562 }
16563 }
16564 })
16565 .detach();
16566
16567 cx.condition(|editor, _| editor.context_menu_visible())
16568 .await;
16569 cx.run_until_parked();
16570 cx.update_editor(|editor, _, _| {
16571 let menu = editor.context_menu.borrow_mut();
16572 match menu.as_ref().expect("should have the completions menu") {
16573 CodeContextMenu::Completions(completions_menu) => {
16574 assert_eq!(
16575 completions_menu
16576 .entries
16577 .borrow()
16578 .iter()
16579 .map(|mat| mat.string.clone())
16580 .collect::<Vec<String>>(),
16581 items
16582 .iter()
16583 .map(|completion| completion.label.clone())
16584 .collect::<Vec<String>>()
16585 );
16586 }
16587 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
16588 }
16589 });
16590 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
16591 // with 4 from the end.
16592 assert_eq!(
16593 *resolved_items.lock(),
16594 [&items[0..16], &items[items.len() - 4..items.len()]]
16595 .concat()
16596 .iter()
16597 .cloned()
16598 .map(|mut item| {
16599 if item.data.is_none() {
16600 item.data = Some(default_data.clone());
16601 }
16602 item
16603 })
16604 .collect::<Vec<lsp::CompletionItem>>(),
16605 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
16606 );
16607 resolved_items.lock().clear();
16608
16609 cx.update_editor(|editor, window, cx| {
16610 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
16611 });
16612 cx.run_until_parked();
16613 // Completions that have already been resolved are skipped.
16614 assert_eq!(
16615 *resolved_items.lock(),
16616 items[items.len() - 17..items.len() - 4]
16617 .iter()
16618 .cloned()
16619 .map(|mut item| {
16620 if item.data.is_none() {
16621 item.data = Some(default_data.clone());
16622 }
16623 item
16624 })
16625 .collect::<Vec<lsp::CompletionItem>>()
16626 );
16627 resolved_items.lock().clear();
16628}
16629
16630#[gpui::test]
16631async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
16632 init_test(cx, |_| {});
16633
16634 let mut cx = EditorLspTestContext::new(
16635 Language::new(
16636 LanguageConfig {
16637 matcher: LanguageMatcher {
16638 path_suffixes: vec!["jsx".into()],
16639 ..Default::default()
16640 },
16641 overrides: [(
16642 "element".into(),
16643 LanguageConfigOverride {
16644 completion_query_characters: Override::Set(['-'].into_iter().collect()),
16645 ..Default::default()
16646 },
16647 )]
16648 .into_iter()
16649 .collect(),
16650 ..Default::default()
16651 },
16652 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16653 )
16654 .with_override_query("(jsx_self_closing_element) @element")
16655 .unwrap(),
16656 lsp::ServerCapabilities {
16657 completion_provider: Some(lsp::CompletionOptions {
16658 trigger_characters: Some(vec![":".to_string()]),
16659 ..Default::default()
16660 }),
16661 ..Default::default()
16662 },
16663 cx,
16664 )
16665 .await;
16666
16667 cx.lsp
16668 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16669 Ok(Some(lsp::CompletionResponse::Array(vec![
16670 lsp::CompletionItem {
16671 label: "bg-blue".into(),
16672 ..Default::default()
16673 },
16674 lsp::CompletionItem {
16675 label: "bg-red".into(),
16676 ..Default::default()
16677 },
16678 lsp::CompletionItem {
16679 label: "bg-yellow".into(),
16680 ..Default::default()
16681 },
16682 ])))
16683 });
16684
16685 cx.set_state(r#"<p class="bgˇ" />"#);
16686
16687 // Trigger completion when typing a dash, because the dash is an extra
16688 // word character in the 'element' scope, which contains the cursor.
16689 cx.simulate_keystroke("-");
16690 cx.executor().run_until_parked();
16691 cx.update_editor(|editor, _, _| {
16692 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16693 {
16694 assert_eq!(
16695 completion_menu_entries(menu),
16696 &["bg-blue", "bg-red", "bg-yellow"]
16697 );
16698 } else {
16699 panic!("expected completion menu to be open");
16700 }
16701 });
16702
16703 cx.simulate_keystroke("l");
16704 cx.executor().run_until_parked();
16705 cx.update_editor(|editor, _, _| {
16706 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16707 {
16708 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
16709 } else {
16710 panic!("expected completion menu to be open");
16711 }
16712 });
16713
16714 // When filtering completions, consider the character after the '-' to
16715 // be the start of a subword.
16716 cx.set_state(r#"<p class="yelˇ" />"#);
16717 cx.simulate_keystroke("l");
16718 cx.executor().run_until_parked();
16719 cx.update_editor(|editor, _, _| {
16720 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16721 {
16722 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
16723 } else {
16724 panic!("expected completion menu to be open");
16725 }
16726 });
16727}
16728
16729fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
16730 let entries = menu.entries.borrow();
16731 entries.iter().map(|mat| mat.string.clone()).collect()
16732}
16733
16734#[gpui::test]
16735async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
16736 init_test(cx, |settings| {
16737 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
16738 Formatter::Prettier,
16739 )))
16740 });
16741
16742 let fs = FakeFs::new(cx.executor());
16743 fs.insert_file(path!("/file.ts"), Default::default()).await;
16744
16745 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
16746 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16747
16748 language_registry.add(Arc::new(Language::new(
16749 LanguageConfig {
16750 name: "TypeScript".into(),
16751 matcher: LanguageMatcher {
16752 path_suffixes: vec!["ts".to_string()],
16753 ..Default::default()
16754 },
16755 ..Default::default()
16756 },
16757 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
16758 )));
16759 update_test_language_settings(cx, |settings| {
16760 settings.defaults.prettier = Some(PrettierSettings {
16761 allowed: true,
16762 ..PrettierSettings::default()
16763 });
16764 });
16765
16766 let test_plugin = "test_plugin";
16767 let _ = language_registry.register_fake_lsp(
16768 "TypeScript",
16769 FakeLspAdapter {
16770 prettier_plugins: vec![test_plugin],
16771 ..Default::default()
16772 },
16773 );
16774
16775 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
16776 let buffer = project
16777 .update(cx, |project, cx| {
16778 project.open_local_buffer(path!("/file.ts"), cx)
16779 })
16780 .await
16781 .unwrap();
16782
16783 let buffer_text = "one\ntwo\nthree\n";
16784 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16785 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16786 editor.update_in(cx, |editor, window, cx| {
16787 editor.set_text(buffer_text, window, cx)
16788 });
16789
16790 editor
16791 .update_in(cx, |editor, window, cx| {
16792 editor.perform_format(
16793 project.clone(),
16794 FormatTrigger::Manual,
16795 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16796 window,
16797 cx,
16798 )
16799 })
16800 .unwrap()
16801 .await;
16802 assert_eq!(
16803 editor.update(cx, |editor, cx| editor.text(cx)),
16804 buffer_text.to_string() + prettier_format_suffix,
16805 "Test prettier formatting was not applied to the original buffer text",
16806 );
16807
16808 update_test_language_settings(cx, |settings| {
16809 settings.defaults.formatter = Some(SelectedFormatter::Auto)
16810 });
16811 let format = editor.update_in(cx, |editor, window, cx| {
16812 editor.perform_format(
16813 project.clone(),
16814 FormatTrigger::Manual,
16815 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16816 window,
16817 cx,
16818 )
16819 });
16820 format.await.unwrap();
16821 assert_eq!(
16822 editor.update(cx, |editor, cx| editor.text(cx)),
16823 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
16824 "Autoformatting (via test prettier) was not applied to the original buffer text",
16825 );
16826}
16827
16828#[gpui::test]
16829async fn test_addition_reverts(cx: &mut TestAppContext) {
16830 init_test(cx, |_| {});
16831 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16832 let base_text = indoc! {r#"
16833 struct Row;
16834 struct Row1;
16835 struct Row2;
16836
16837 struct Row4;
16838 struct Row5;
16839 struct Row6;
16840
16841 struct Row8;
16842 struct Row9;
16843 struct Row10;"#};
16844
16845 // When addition hunks are not adjacent to carets, no hunk revert is performed
16846 assert_hunk_revert(
16847 indoc! {r#"struct Row;
16848 struct Row1;
16849 struct Row1.1;
16850 struct Row1.2;
16851 struct Row2;ˇ
16852
16853 struct Row4;
16854 struct Row5;
16855 struct Row6;
16856
16857 struct Row8;
16858 ˇstruct Row9;
16859 struct Row9.1;
16860 struct Row9.2;
16861 struct Row9.3;
16862 struct Row10;"#},
16863 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16864 indoc! {r#"struct Row;
16865 struct Row1;
16866 struct Row1.1;
16867 struct Row1.2;
16868 struct Row2;ˇ
16869
16870 struct Row4;
16871 struct Row5;
16872 struct Row6;
16873
16874 struct Row8;
16875 ˇstruct Row9;
16876 struct Row9.1;
16877 struct Row9.2;
16878 struct Row9.3;
16879 struct Row10;"#},
16880 base_text,
16881 &mut cx,
16882 );
16883 // Same for selections
16884 assert_hunk_revert(
16885 indoc! {r#"struct Row;
16886 struct Row1;
16887 struct Row2;
16888 struct Row2.1;
16889 struct Row2.2;
16890 «ˇ
16891 struct Row4;
16892 struct» Row5;
16893 «struct Row6;
16894 ˇ»
16895 struct Row9.1;
16896 struct Row9.2;
16897 struct Row9.3;
16898 struct Row8;
16899 struct Row9;
16900 struct Row10;"#},
16901 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16902 indoc! {r#"struct Row;
16903 struct Row1;
16904 struct Row2;
16905 struct Row2.1;
16906 struct Row2.2;
16907 «ˇ
16908 struct Row4;
16909 struct» Row5;
16910 «struct Row6;
16911 ˇ»
16912 struct Row9.1;
16913 struct Row9.2;
16914 struct Row9.3;
16915 struct Row8;
16916 struct Row9;
16917 struct Row10;"#},
16918 base_text,
16919 &mut cx,
16920 );
16921
16922 // When carets and selections intersect the addition hunks, those are reverted.
16923 // Adjacent carets got merged.
16924 assert_hunk_revert(
16925 indoc! {r#"struct Row;
16926 ˇ// something on the top
16927 struct Row1;
16928 struct Row2;
16929 struct Roˇw3.1;
16930 struct Row2.2;
16931 struct Row2.3;ˇ
16932
16933 struct Row4;
16934 struct ˇRow5.1;
16935 struct Row5.2;
16936 struct «Rowˇ»5.3;
16937 struct Row5;
16938 struct Row6;
16939 ˇ
16940 struct Row9.1;
16941 struct «Rowˇ»9.2;
16942 struct «ˇRow»9.3;
16943 struct Row8;
16944 struct Row9;
16945 «ˇ// something on bottom»
16946 struct Row10;"#},
16947 vec![
16948 DiffHunkStatusKind::Added,
16949 DiffHunkStatusKind::Added,
16950 DiffHunkStatusKind::Added,
16951 DiffHunkStatusKind::Added,
16952 DiffHunkStatusKind::Added,
16953 ],
16954 indoc! {r#"struct Row;
16955 ˇstruct Row1;
16956 struct Row2;
16957 ˇ
16958 struct Row4;
16959 ˇstruct Row5;
16960 struct Row6;
16961 ˇ
16962 ˇstruct Row8;
16963 struct Row9;
16964 ˇstruct Row10;"#},
16965 base_text,
16966 &mut cx,
16967 );
16968}
16969
16970#[gpui::test]
16971async fn test_modification_reverts(cx: &mut TestAppContext) {
16972 init_test(cx, |_| {});
16973 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16974 let base_text = indoc! {r#"
16975 struct Row;
16976 struct Row1;
16977 struct Row2;
16978
16979 struct Row4;
16980 struct Row5;
16981 struct Row6;
16982
16983 struct Row8;
16984 struct Row9;
16985 struct Row10;"#};
16986
16987 // Modification hunks behave the same as the addition ones.
16988 assert_hunk_revert(
16989 indoc! {r#"struct Row;
16990 struct Row1;
16991 struct Row33;
16992 ˇ
16993 struct Row4;
16994 struct Row5;
16995 struct Row6;
16996 ˇ
16997 struct Row99;
16998 struct Row9;
16999 struct Row10;"#},
17000 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
17001 indoc! {r#"struct Row;
17002 struct Row1;
17003 struct Row33;
17004 ˇ
17005 struct Row4;
17006 struct Row5;
17007 struct Row6;
17008 ˇ
17009 struct Row99;
17010 struct Row9;
17011 struct Row10;"#},
17012 base_text,
17013 &mut cx,
17014 );
17015 assert_hunk_revert(
17016 indoc! {r#"struct Row;
17017 struct Row1;
17018 struct Row33;
17019 «ˇ
17020 struct Row4;
17021 struct» Row5;
17022 «struct Row6;
17023 ˇ»
17024 struct Row99;
17025 struct Row9;
17026 struct Row10;"#},
17027 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
17028 indoc! {r#"struct Row;
17029 struct Row1;
17030 struct Row33;
17031 «ˇ
17032 struct Row4;
17033 struct» Row5;
17034 «struct Row6;
17035 ˇ»
17036 struct Row99;
17037 struct Row9;
17038 struct Row10;"#},
17039 base_text,
17040 &mut cx,
17041 );
17042
17043 assert_hunk_revert(
17044 indoc! {r#"ˇstruct Row1.1;
17045 struct Row1;
17046 «ˇstr»uct Row22;
17047
17048 struct ˇRow44;
17049 struct Row5;
17050 struct «Rˇ»ow66;ˇ
17051
17052 «struˇ»ct Row88;
17053 struct Row9;
17054 struct Row1011;ˇ"#},
17055 vec![
17056 DiffHunkStatusKind::Modified,
17057 DiffHunkStatusKind::Modified,
17058 DiffHunkStatusKind::Modified,
17059 DiffHunkStatusKind::Modified,
17060 DiffHunkStatusKind::Modified,
17061 DiffHunkStatusKind::Modified,
17062 ],
17063 indoc! {r#"struct Row;
17064 ˇstruct Row1;
17065 struct Row2;
17066 ˇ
17067 struct Row4;
17068 ˇstruct Row5;
17069 struct Row6;
17070 ˇ
17071 struct Row8;
17072 ˇstruct Row9;
17073 struct Row10;ˇ"#},
17074 base_text,
17075 &mut cx,
17076 );
17077}
17078
17079#[gpui::test]
17080async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
17081 init_test(cx, |_| {});
17082 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17083 let base_text = indoc! {r#"
17084 one
17085
17086 two
17087 three
17088 "#};
17089
17090 cx.set_head_text(base_text);
17091 cx.set_state("\nˇ\n");
17092 cx.executor().run_until_parked();
17093 cx.update_editor(|editor, _window, cx| {
17094 editor.expand_selected_diff_hunks(cx);
17095 });
17096 cx.executor().run_until_parked();
17097 cx.update_editor(|editor, window, cx| {
17098 editor.backspace(&Default::default(), window, cx);
17099 });
17100 cx.run_until_parked();
17101 cx.assert_state_with_diff(
17102 indoc! {r#"
17103
17104 - two
17105 - threeˇ
17106 +
17107 "#}
17108 .to_string(),
17109 );
17110}
17111
17112#[gpui::test]
17113async fn test_deletion_reverts(cx: &mut TestAppContext) {
17114 init_test(cx, |_| {});
17115 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
17116 let base_text = indoc! {r#"struct Row;
17117struct Row1;
17118struct Row2;
17119
17120struct Row4;
17121struct Row5;
17122struct Row6;
17123
17124struct Row8;
17125struct Row9;
17126struct Row10;"#};
17127
17128 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
17129 assert_hunk_revert(
17130 indoc! {r#"struct Row;
17131 struct Row2;
17132
17133 ˇstruct Row4;
17134 struct Row5;
17135 struct Row6;
17136 ˇ
17137 struct Row8;
17138 struct Row10;"#},
17139 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
17140 indoc! {r#"struct Row;
17141 struct Row2;
17142
17143 ˇstruct Row4;
17144 struct Row5;
17145 struct Row6;
17146 ˇ
17147 struct Row8;
17148 struct Row10;"#},
17149 base_text,
17150 &mut cx,
17151 );
17152 assert_hunk_revert(
17153 indoc! {r#"struct Row;
17154 struct Row2;
17155
17156 «ˇstruct Row4;
17157 struct» Row5;
17158 «struct Row6;
17159 ˇ»
17160 struct Row8;
17161 struct Row10;"#},
17162 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
17163 indoc! {r#"struct Row;
17164 struct Row2;
17165
17166 «ˇstruct Row4;
17167 struct» Row5;
17168 «struct Row6;
17169 ˇ»
17170 struct Row8;
17171 struct Row10;"#},
17172 base_text,
17173 &mut cx,
17174 );
17175
17176 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
17177 assert_hunk_revert(
17178 indoc! {r#"struct Row;
17179 ˇstruct Row2;
17180
17181 struct Row4;
17182 struct Row5;
17183 struct Row6;
17184
17185 struct Row8;ˇ
17186 struct Row10;"#},
17187 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
17188 indoc! {r#"struct Row;
17189 struct Row1;
17190 ˇstruct Row2;
17191
17192 struct Row4;
17193 struct Row5;
17194 struct Row6;
17195
17196 struct Row8;ˇ
17197 struct Row9;
17198 struct Row10;"#},
17199 base_text,
17200 &mut cx,
17201 );
17202 assert_hunk_revert(
17203 indoc! {r#"struct Row;
17204 struct Row2«ˇ;
17205 struct Row4;
17206 struct» Row5;
17207 «struct Row6;
17208
17209 struct Row8;ˇ»
17210 struct Row10;"#},
17211 vec![
17212 DiffHunkStatusKind::Deleted,
17213 DiffHunkStatusKind::Deleted,
17214 DiffHunkStatusKind::Deleted,
17215 ],
17216 indoc! {r#"struct Row;
17217 struct Row1;
17218 struct Row2«ˇ;
17219
17220 struct Row4;
17221 struct» Row5;
17222 «struct Row6;
17223
17224 struct Row8;ˇ»
17225 struct Row9;
17226 struct Row10;"#},
17227 base_text,
17228 &mut cx,
17229 );
17230}
17231
17232#[gpui::test]
17233async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
17234 init_test(cx, |_| {});
17235
17236 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
17237 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
17238 let base_text_3 =
17239 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
17240
17241 let text_1 = edit_first_char_of_every_line(base_text_1);
17242 let text_2 = edit_first_char_of_every_line(base_text_2);
17243 let text_3 = edit_first_char_of_every_line(base_text_3);
17244
17245 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
17246 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
17247 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
17248
17249 let multibuffer = cx.new(|cx| {
17250 let mut multibuffer = MultiBuffer::new(ReadWrite);
17251 multibuffer.push_excerpts(
17252 buffer_1.clone(),
17253 [
17254 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17255 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17256 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17257 ],
17258 cx,
17259 );
17260 multibuffer.push_excerpts(
17261 buffer_2.clone(),
17262 [
17263 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17264 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17265 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17266 ],
17267 cx,
17268 );
17269 multibuffer.push_excerpts(
17270 buffer_3.clone(),
17271 [
17272 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17273 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17274 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17275 ],
17276 cx,
17277 );
17278 multibuffer
17279 });
17280
17281 let fs = FakeFs::new(cx.executor());
17282 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
17283 let (editor, cx) = cx
17284 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
17285 editor.update_in(cx, |editor, _window, cx| {
17286 for (buffer, diff_base) in [
17287 (buffer_1.clone(), base_text_1),
17288 (buffer_2.clone(), base_text_2),
17289 (buffer_3.clone(), base_text_3),
17290 ] {
17291 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
17292 editor
17293 .buffer
17294 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
17295 }
17296 });
17297 cx.executor().run_until_parked();
17298
17299 editor.update_in(cx, |editor, window, cx| {
17300 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}");
17301 editor.select_all(&SelectAll, window, cx);
17302 editor.git_restore(&Default::default(), window, cx);
17303 });
17304 cx.executor().run_until_parked();
17305
17306 // When all ranges are selected, all buffer hunks are reverted.
17307 editor.update(cx, |editor, cx| {
17308 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");
17309 });
17310 buffer_1.update(cx, |buffer, _| {
17311 assert_eq!(buffer.text(), base_text_1);
17312 });
17313 buffer_2.update(cx, |buffer, _| {
17314 assert_eq!(buffer.text(), base_text_2);
17315 });
17316 buffer_3.update(cx, |buffer, _| {
17317 assert_eq!(buffer.text(), base_text_3);
17318 });
17319
17320 editor.update_in(cx, |editor, window, cx| {
17321 editor.undo(&Default::default(), window, cx);
17322 });
17323
17324 editor.update_in(cx, |editor, window, cx| {
17325 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17326 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
17327 });
17328 editor.git_restore(&Default::default(), window, cx);
17329 });
17330
17331 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
17332 // but not affect buffer_2 and its related excerpts.
17333 editor.update(cx, |editor, cx| {
17334 assert_eq!(
17335 editor.text(cx),
17336 "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}"
17337 );
17338 });
17339 buffer_1.update(cx, |buffer, _| {
17340 assert_eq!(buffer.text(), base_text_1);
17341 });
17342 buffer_2.update(cx, |buffer, _| {
17343 assert_eq!(
17344 buffer.text(),
17345 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
17346 );
17347 });
17348 buffer_3.update(cx, |buffer, _| {
17349 assert_eq!(
17350 buffer.text(),
17351 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
17352 );
17353 });
17354
17355 fn edit_first_char_of_every_line(text: &str) -> String {
17356 text.split('\n')
17357 .map(|line| format!("X{}", &line[1..]))
17358 .collect::<Vec<_>>()
17359 .join("\n")
17360 }
17361}
17362
17363#[gpui::test]
17364async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
17365 init_test(cx, |_| {});
17366
17367 let cols = 4;
17368 let rows = 10;
17369 let sample_text_1 = sample_text(rows, cols, 'a');
17370 assert_eq!(
17371 sample_text_1,
17372 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
17373 );
17374 let sample_text_2 = sample_text(rows, cols, 'l');
17375 assert_eq!(
17376 sample_text_2,
17377 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
17378 );
17379 let sample_text_3 = sample_text(rows, cols, 'v');
17380 assert_eq!(
17381 sample_text_3,
17382 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
17383 );
17384
17385 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
17386 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
17387 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
17388
17389 let multi_buffer = cx.new(|cx| {
17390 let mut multibuffer = MultiBuffer::new(ReadWrite);
17391 multibuffer.push_excerpts(
17392 buffer_1.clone(),
17393 [
17394 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17395 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17396 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17397 ],
17398 cx,
17399 );
17400 multibuffer.push_excerpts(
17401 buffer_2.clone(),
17402 [
17403 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17404 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17405 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17406 ],
17407 cx,
17408 );
17409 multibuffer.push_excerpts(
17410 buffer_3.clone(),
17411 [
17412 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17413 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17414 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17415 ],
17416 cx,
17417 );
17418 multibuffer
17419 });
17420
17421 let fs = FakeFs::new(cx.executor());
17422 fs.insert_tree(
17423 "/a",
17424 json!({
17425 "main.rs": sample_text_1,
17426 "other.rs": sample_text_2,
17427 "lib.rs": sample_text_3,
17428 }),
17429 )
17430 .await;
17431 let project = Project::test(fs, ["/a".as_ref()], cx).await;
17432 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17433 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17434 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17435 Editor::new(
17436 EditorMode::full(),
17437 multi_buffer,
17438 Some(project.clone()),
17439 window,
17440 cx,
17441 )
17442 });
17443 let multibuffer_item_id = workspace
17444 .update(cx, |workspace, window, cx| {
17445 assert!(
17446 workspace.active_item(cx).is_none(),
17447 "active item should be None before the first item is added"
17448 );
17449 workspace.add_item_to_active_pane(
17450 Box::new(multi_buffer_editor.clone()),
17451 None,
17452 true,
17453 window,
17454 cx,
17455 );
17456 let active_item = workspace
17457 .active_item(cx)
17458 .expect("should have an active item after adding the multi buffer");
17459 assert!(
17460 !active_item.is_singleton(cx),
17461 "A multi buffer was expected to active after adding"
17462 );
17463 active_item.item_id()
17464 })
17465 .unwrap();
17466 cx.executor().run_until_parked();
17467
17468 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17469 editor.change_selections(
17470 SelectionEffects::scroll(Autoscroll::Next),
17471 window,
17472 cx,
17473 |s| s.select_ranges(Some(1..2)),
17474 );
17475 editor.open_excerpts(&OpenExcerpts, window, cx);
17476 });
17477 cx.executor().run_until_parked();
17478 let first_item_id = workspace
17479 .update(cx, |workspace, window, cx| {
17480 let active_item = workspace
17481 .active_item(cx)
17482 .expect("should have an active item after navigating into the 1st buffer");
17483 let first_item_id = active_item.item_id();
17484 assert_ne!(
17485 first_item_id, multibuffer_item_id,
17486 "Should navigate into the 1st buffer and activate it"
17487 );
17488 assert!(
17489 active_item.is_singleton(cx),
17490 "New active item should be a singleton buffer"
17491 );
17492 assert_eq!(
17493 active_item
17494 .act_as::<Editor>(cx)
17495 .expect("should have navigated into an editor for the 1st buffer")
17496 .read(cx)
17497 .text(cx),
17498 sample_text_1
17499 );
17500
17501 workspace
17502 .go_back(workspace.active_pane().downgrade(), window, cx)
17503 .detach_and_log_err(cx);
17504
17505 first_item_id
17506 })
17507 .unwrap();
17508 cx.executor().run_until_parked();
17509 workspace
17510 .update(cx, |workspace, _, cx| {
17511 let active_item = workspace
17512 .active_item(cx)
17513 .expect("should have an active item after navigating back");
17514 assert_eq!(
17515 active_item.item_id(),
17516 multibuffer_item_id,
17517 "Should navigate back to the multi buffer"
17518 );
17519 assert!(!active_item.is_singleton(cx));
17520 })
17521 .unwrap();
17522
17523 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17524 editor.change_selections(
17525 SelectionEffects::scroll(Autoscroll::Next),
17526 window,
17527 cx,
17528 |s| s.select_ranges(Some(39..40)),
17529 );
17530 editor.open_excerpts(&OpenExcerpts, window, cx);
17531 });
17532 cx.executor().run_until_parked();
17533 let second_item_id = workspace
17534 .update(cx, |workspace, window, cx| {
17535 let active_item = workspace
17536 .active_item(cx)
17537 .expect("should have an active item after navigating into the 2nd buffer");
17538 let second_item_id = active_item.item_id();
17539 assert_ne!(
17540 second_item_id, multibuffer_item_id,
17541 "Should navigate away from the multibuffer"
17542 );
17543 assert_ne!(
17544 second_item_id, first_item_id,
17545 "Should navigate into the 2nd buffer and activate it"
17546 );
17547 assert!(
17548 active_item.is_singleton(cx),
17549 "New active item should be a singleton buffer"
17550 );
17551 assert_eq!(
17552 active_item
17553 .act_as::<Editor>(cx)
17554 .expect("should have navigated into an editor")
17555 .read(cx)
17556 .text(cx),
17557 sample_text_2
17558 );
17559
17560 workspace
17561 .go_back(workspace.active_pane().downgrade(), window, cx)
17562 .detach_and_log_err(cx);
17563
17564 second_item_id
17565 })
17566 .unwrap();
17567 cx.executor().run_until_parked();
17568 workspace
17569 .update(cx, |workspace, _, cx| {
17570 let active_item = workspace
17571 .active_item(cx)
17572 .expect("should have an active item after navigating back from the 2nd buffer");
17573 assert_eq!(
17574 active_item.item_id(),
17575 multibuffer_item_id,
17576 "Should navigate back from the 2nd buffer to the multi buffer"
17577 );
17578 assert!(!active_item.is_singleton(cx));
17579 })
17580 .unwrap();
17581
17582 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17583 editor.change_selections(
17584 SelectionEffects::scroll(Autoscroll::Next),
17585 window,
17586 cx,
17587 |s| s.select_ranges(Some(70..70)),
17588 );
17589 editor.open_excerpts(&OpenExcerpts, window, cx);
17590 });
17591 cx.executor().run_until_parked();
17592 workspace
17593 .update(cx, |workspace, window, cx| {
17594 let active_item = workspace
17595 .active_item(cx)
17596 .expect("should have an active item after navigating into the 3rd buffer");
17597 let third_item_id = active_item.item_id();
17598 assert_ne!(
17599 third_item_id, multibuffer_item_id,
17600 "Should navigate into the 3rd buffer and activate it"
17601 );
17602 assert_ne!(third_item_id, first_item_id);
17603 assert_ne!(third_item_id, second_item_id);
17604 assert!(
17605 active_item.is_singleton(cx),
17606 "New active item should be a singleton buffer"
17607 );
17608 assert_eq!(
17609 active_item
17610 .act_as::<Editor>(cx)
17611 .expect("should have navigated into an editor")
17612 .read(cx)
17613 .text(cx),
17614 sample_text_3
17615 );
17616
17617 workspace
17618 .go_back(workspace.active_pane().downgrade(), window, cx)
17619 .detach_and_log_err(cx);
17620 })
17621 .unwrap();
17622 cx.executor().run_until_parked();
17623 workspace
17624 .update(cx, |workspace, _, cx| {
17625 let active_item = workspace
17626 .active_item(cx)
17627 .expect("should have an active item after navigating back from the 3rd buffer");
17628 assert_eq!(
17629 active_item.item_id(),
17630 multibuffer_item_id,
17631 "Should navigate back from the 3rd buffer to the multi buffer"
17632 );
17633 assert!(!active_item.is_singleton(cx));
17634 })
17635 .unwrap();
17636}
17637
17638#[gpui::test]
17639async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17640 init_test(cx, |_| {});
17641
17642 let mut cx = EditorTestContext::new(cx).await;
17643
17644 let diff_base = r#"
17645 use some::mod;
17646
17647 const A: u32 = 42;
17648
17649 fn main() {
17650 println!("hello");
17651
17652 println!("world");
17653 }
17654 "#
17655 .unindent();
17656
17657 cx.set_state(
17658 &r#"
17659 use some::modified;
17660
17661 ˇ
17662 fn main() {
17663 println!("hello there");
17664
17665 println!("around the");
17666 println!("world");
17667 }
17668 "#
17669 .unindent(),
17670 );
17671
17672 cx.set_head_text(&diff_base);
17673 executor.run_until_parked();
17674
17675 cx.update_editor(|editor, window, cx| {
17676 editor.go_to_next_hunk(&GoToHunk, window, cx);
17677 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17678 });
17679 executor.run_until_parked();
17680 cx.assert_state_with_diff(
17681 r#"
17682 use some::modified;
17683
17684
17685 fn main() {
17686 - println!("hello");
17687 + ˇ println!("hello there");
17688
17689 println!("around the");
17690 println!("world");
17691 }
17692 "#
17693 .unindent(),
17694 );
17695
17696 cx.update_editor(|editor, window, cx| {
17697 for _ in 0..2 {
17698 editor.go_to_next_hunk(&GoToHunk, window, cx);
17699 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17700 }
17701 });
17702 executor.run_until_parked();
17703 cx.assert_state_with_diff(
17704 r#"
17705 - use some::mod;
17706 + ˇuse some::modified;
17707
17708
17709 fn main() {
17710 - println!("hello");
17711 + println!("hello there");
17712
17713 + println!("around the");
17714 println!("world");
17715 }
17716 "#
17717 .unindent(),
17718 );
17719
17720 cx.update_editor(|editor, window, cx| {
17721 editor.go_to_next_hunk(&GoToHunk, window, cx);
17722 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17723 });
17724 executor.run_until_parked();
17725 cx.assert_state_with_diff(
17726 r#"
17727 - use some::mod;
17728 + use some::modified;
17729
17730 - const A: u32 = 42;
17731 ˇ
17732 fn main() {
17733 - println!("hello");
17734 + println!("hello there");
17735
17736 + println!("around the");
17737 println!("world");
17738 }
17739 "#
17740 .unindent(),
17741 );
17742
17743 cx.update_editor(|editor, window, cx| {
17744 editor.cancel(&Cancel, window, cx);
17745 });
17746
17747 cx.assert_state_with_diff(
17748 r#"
17749 use some::modified;
17750
17751 ˇ
17752 fn main() {
17753 println!("hello there");
17754
17755 println!("around the");
17756 println!("world");
17757 }
17758 "#
17759 .unindent(),
17760 );
17761}
17762
17763#[gpui::test]
17764async fn test_diff_base_change_with_expanded_diff_hunks(
17765 executor: BackgroundExecutor,
17766 cx: &mut TestAppContext,
17767) {
17768 init_test(cx, |_| {});
17769
17770 let mut cx = EditorTestContext::new(cx).await;
17771
17772 let diff_base = r#"
17773 use some::mod1;
17774 use some::mod2;
17775
17776 const A: u32 = 42;
17777 const B: u32 = 42;
17778 const C: u32 = 42;
17779
17780 fn main() {
17781 println!("hello");
17782
17783 println!("world");
17784 }
17785 "#
17786 .unindent();
17787
17788 cx.set_state(
17789 &r#"
17790 use some::mod2;
17791
17792 const A: u32 = 42;
17793 const C: u32 = 42;
17794
17795 fn main(ˇ) {
17796 //println!("hello");
17797
17798 println!("world");
17799 //
17800 //
17801 }
17802 "#
17803 .unindent(),
17804 );
17805
17806 cx.set_head_text(&diff_base);
17807 executor.run_until_parked();
17808
17809 cx.update_editor(|editor, window, cx| {
17810 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17811 });
17812 executor.run_until_parked();
17813 cx.assert_state_with_diff(
17814 r#"
17815 - use some::mod1;
17816 use some::mod2;
17817
17818 const A: u32 = 42;
17819 - const B: u32 = 42;
17820 const C: u32 = 42;
17821
17822 fn main(ˇ) {
17823 - println!("hello");
17824 + //println!("hello");
17825
17826 println!("world");
17827 + //
17828 + //
17829 }
17830 "#
17831 .unindent(),
17832 );
17833
17834 cx.set_head_text("new diff base!");
17835 executor.run_until_parked();
17836 cx.assert_state_with_diff(
17837 r#"
17838 - new diff base!
17839 + use some::mod2;
17840 +
17841 + const A: u32 = 42;
17842 + const C: u32 = 42;
17843 +
17844 + fn main(ˇ) {
17845 + //println!("hello");
17846 +
17847 + println!("world");
17848 + //
17849 + //
17850 + }
17851 "#
17852 .unindent(),
17853 );
17854}
17855
17856#[gpui::test]
17857async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
17858 init_test(cx, |_| {});
17859
17860 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17861 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17862 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17863 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17864 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
17865 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
17866
17867 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
17868 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
17869 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
17870
17871 let multi_buffer = cx.new(|cx| {
17872 let mut multibuffer = MultiBuffer::new(ReadWrite);
17873 multibuffer.push_excerpts(
17874 buffer_1.clone(),
17875 [
17876 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17877 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17878 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17879 ],
17880 cx,
17881 );
17882 multibuffer.push_excerpts(
17883 buffer_2.clone(),
17884 [
17885 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17886 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17887 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17888 ],
17889 cx,
17890 );
17891 multibuffer.push_excerpts(
17892 buffer_3.clone(),
17893 [
17894 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17895 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17896 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17897 ],
17898 cx,
17899 );
17900 multibuffer
17901 });
17902
17903 let editor =
17904 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17905 editor
17906 .update(cx, |editor, _window, cx| {
17907 for (buffer, diff_base) in [
17908 (buffer_1.clone(), file_1_old),
17909 (buffer_2.clone(), file_2_old),
17910 (buffer_3.clone(), file_3_old),
17911 ] {
17912 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
17913 editor
17914 .buffer
17915 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
17916 }
17917 })
17918 .unwrap();
17919
17920 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17921 cx.run_until_parked();
17922
17923 cx.assert_editor_state(
17924 &"
17925 ˇaaa
17926 ccc
17927 ddd
17928
17929 ggg
17930 hhh
17931
17932
17933 lll
17934 mmm
17935 NNN
17936
17937 qqq
17938 rrr
17939
17940 uuu
17941 111
17942 222
17943 333
17944
17945 666
17946 777
17947
17948 000
17949 !!!"
17950 .unindent(),
17951 );
17952
17953 cx.update_editor(|editor, window, cx| {
17954 editor.select_all(&SelectAll, window, cx);
17955 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17956 });
17957 cx.executor().run_until_parked();
17958
17959 cx.assert_state_with_diff(
17960 "
17961 «aaa
17962 - bbb
17963 ccc
17964 ddd
17965
17966 ggg
17967 hhh
17968
17969
17970 lll
17971 mmm
17972 - nnn
17973 + NNN
17974
17975 qqq
17976 rrr
17977
17978 uuu
17979 111
17980 222
17981 333
17982
17983 + 666
17984 777
17985
17986 000
17987 !!!ˇ»"
17988 .unindent(),
17989 );
17990}
17991
17992#[gpui::test]
17993async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
17994 init_test(cx, |_| {});
17995
17996 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
17997 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
17998
17999 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
18000 let multi_buffer = cx.new(|cx| {
18001 let mut multibuffer = MultiBuffer::new(ReadWrite);
18002 multibuffer.push_excerpts(
18003 buffer.clone(),
18004 [
18005 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
18006 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
18007 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
18008 ],
18009 cx,
18010 );
18011 multibuffer
18012 });
18013
18014 let editor =
18015 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
18016 editor
18017 .update(cx, |editor, _window, cx| {
18018 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
18019 editor
18020 .buffer
18021 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
18022 })
18023 .unwrap();
18024
18025 let mut cx = EditorTestContext::for_editor(editor, cx).await;
18026 cx.run_until_parked();
18027
18028 cx.update_editor(|editor, window, cx| {
18029 editor.expand_all_diff_hunks(&Default::default(), window, cx)
18030 });
18031 cx.executor().run_until_parked();
18032
18033 // When the start of a hunk coincides with the start of its excerpt,
18034 // the hunk is expanded. When the start of a a hunk is earlier than
18035 // the start of its excerpt, the hunk is not expanded.
18036 cx.assert_state_with_diff(
18037 "
18038 ˇaaa
18039 - bbb
18040 + BBB
18041
18042 - ddd
18043 - eee
18044 + DDD
18045 + EEE
18046 fff
18047
18048 iii
18049 "
18050 .unindent(),
18051 );
18052}
18053
18054#[gpui::test]
18055async fn test_edits_around_expanded_insertion_hunks(
18056 executor: BackgroundExecutor,
18057 cx: &mut TestAppContext,
18058) {
18059 init_test(cx, |_| {});
18060
18061 let mut cx = EditorTestContext::new(cx).await;
18062
18063 let diff_base = r#"
18064 use some::mod1;
18065 use some::mod2;
18066
18067 const A: u32 = 42;
18068
18069 fn main() {
18070 println!("hello");
18071
18072 println!("world");
18073 }
18074 "#
18075 .unindent();
18076 executor.run_until_parked();
18077 cx.set_state(
18078 &r#"
18079 use some::mod1;
18080 use some::mod2;
18081
18082 const A: u32 = 42;
18083 const B: u32 = 42;
18084 const C: u32 = 42;
18085 ˇ
18086
18087 fn main() {
18088 println!("hello");
18089
18090 println!("world");
18091 }
18092 "#
18093 .unindent(),
18094 );
18095
18096 cx.set_head_text(&diff_base);
18097 executor.run_until_parked();
18098
18099 cx.update_editor(|editor, window, cx| {
18100 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18101 });
18102 executor.run_until_parked();
18103
18104 cx.assert_state_with_diff(
18105 r#"
18106 use some::mod1;
18107 use some::mod2;
18108
18109 const A: u32 = 42;
18110 + const B: u32 = 42;
18111 + const C: u32 = 42;
18112 + ˇ
18113
18114 fn main() {
18115 println!("hello");
18116
18117 println!("world");
18118 }
18119 "#
18120 .unindent(),
18121 );
18122
18123 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
18124 executor.run_until_parked();
18125
18126 cx.assert_state_with_diff(
18127 r#"
18128 use some::mod1;
18129 use some::mod2;
18130
18131 const A: u32 = 42;
18132 + const B: u32 = 42;
18133 + const C: u32 = 42;
18134 + const D: u32 = 42;
18135 + ˇ
18136
18137 fn main() {
18138 println!("hello");
18139
18140 println!("world");
18141 }
18142 "#
18143 .unindent(),
18144 );
18145
18146 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
18147 executor.run_until_parked();
18148
18149 cx.assert_state_with_diff(
18150 r#"
18151 use some::mod1;
18152 use some::mod2;
18153
18154 const A: u32 = 42;
18155 + const B: u32 = 42;
18156 + const C: u32 = 42;
18157 + const D: u32 = 42;
18158 + const E: u32 = 42;
18159 + ˇ
18160
18161 fn main() {
18162 println!("hello");
18163
18164 println!("world");
18165 }
18166 "#
18167 .unindent(),
18168 );
18169
18170 cx.update_editor(|editor, window, cx| {
18171 editor.delete_line(&DeleteLine, window, cx);
18172 });
18173 executor.run_until_parked();
18174
18175 cx.assert_state_with_diff(
18176 r#"
18177 use some::mod1;
18178 use some::mod2;
18179
18180 const A: u32 = 42;
18181 + const B: u32 = 42;
18182 + const C: u32 = 42;
18183 + const D: u32 = 42;
18184 + const E: u32 = 42;
18185 ˇ
18186 fn main() {
18187 println!("hello");
18188
18189 println!("world");
18190 }
18191 "#
18192 .unindent(),
18193 );
18194
18195 cx.update_editor(|editor, window, cx| {
18196 editor.move_up(&MoveUp, window, cx);
18197 editor.delete_line(&DeleteLine, window, cx);
18198 editor.move_up(&MoveUp, window, cx);
18199 editor.delete_line(&DeleteLine, window, cx);
18200 editor.move_up(&MoveUp, window, cx);
18201 editor.delete_line(&DeleteLine, window, cx);
18202 });
18203 executor.run_until_parked();
18204 cx.assert_state_with_diff(
18205 r#"
18206 use some::mod1;
18207 use some::mod2;
18208
18209 const A: u32 = 42;
18210 + const B: u32 = 42;
18211 ˇ
18212 fn main() {
18213 println!("hello");
18214
18215 println!("world");
18216 }
18217 "#
18218 .unindent(),
18219 );
18220
18221 cx.update_editor(|editor, window, cx| {
18222 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
18223 editor.delete_line(&DeleteLine, window, cx);
18224 });
18225 executor.run_until_parked();
18226 cx.assert_state_with_diff(
18227 r#"
18228 ˇ
18229 fn main() {
18230 println!("hello");
18231
18232 println!("world");
18233 }
18234 "#
18235 .unindent(),
18236 );
18237}
18238
18239#[gpui::test]
18240async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
18241 init_test(cx, |_| {});
18242
18243 let mut cx = EditorTestContext::new(cx).await;
18244 cx.set_head_text(indoc! { "
18245 one
18246 two
18247 three
18248 four
18249 five
18250 "
18251 });
18252 cx.set_state(indoc! { "
18253 one
18254 ˇthree
18255 five
18256 "});
18257 cx.run_until_parked();
18258 cx.update_editor(|editor, window, cx| {
18259 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18260 });
18261 cx.assert_state_with_diff(
18262 indoc! { "
18263 one
18264 - two
18265 ˇthree
18266 - four
18267 five
18268 "}
18269 .to_string(),
18270 );
18271 cx.update_editor(|editor, window, cx| {
18272 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18273 });
18274
18275 cx.assert_state_with_diff(
18276 indoc! { "
18277 one
18278 ˇthree
18279 five
18280 "}
18281 .to_string(),
18282 );
18283
18284 cx.set_state(indoc! { "
18285 one
18286 ˇTWO
18287 three
18288 four
18289 five
18290 "});
18291 cx.run_until_parked();
18292 cx.update_editor(|editor, window, cx| {
18293 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18294 });
18295
18296 cx.assert_state_with_diff(
18297 indoc! { "
18298 one
18299 - two
18300 + ˇTWO
18301 three
18302 four
18303 five
18304 "}
18305 .to_string(),
18306 );
18307 cx.update_editor(|editor, window, cx| {
18308 editor.move_up(&Default::default(), window, cx);
18309 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18310 });
18311 cx.assert_state_with_diff(
18312 indoc! { "
18313 one
18314 ˇTWO
18315 three
18316 four
18317 five
18318 "}
18319 .to_string(),
18320 );
18321}
18322
18323#[gpui::test]
18324async fn test_edits_around_expanded_deletion_hunks(
18325 executor: BackgroundExecutor,
18326 cx: &mut TestAppContext,
18327) {
18328 init_test(cx, |_| {});
18329
18330 let mut cx = EditorTestContext::new(cx).await;
18331
18332 let diff_base = r#"
18333 use some::mod1;
18334 use some::mod2;
18335
18336 const A: u32 = 42;
18337 const B: u32 = 42;
18338 const C: u32 = 42;
18339
18340
18341 fn main() {
18342 println!("hello");
18343
18344 println!("world");
18345 }
18346 "#
18347 .unindent();
18348 executor.run_until_parked();
18349 cx.set_state(
18350 &r#"
18351 use some::mod1;
18352 use some::mod2;
18353
18354 ˇconst B: u32 = 42;
18355 const C: u32 = 42;
18356
18357
18358 fn main() {
18359 println!("hello");
18360
18361 println!("world");
18362 }
18363 "#
18364 .unindent(),
18365 );
18366
18367 cx.set_head_text(&diff_base);
18368 executor.run_until_parked();
18369
18370 cx.update_editor(|editor, window, cx| {
18371 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18372 });
18373 executor.run_until_parked();
18374
18375 cx.assert_state_with_diff(
18376 r#"
18377 use some::mod1;
18378 use some::mod2;
18379
18380 - const A: u32 = 42;
18381 ˇconst B: u32 = 42;
18382 const C: u32 = 42;
18383
18384
18385 fn main() {
18386 println!("hello");
18387
18388 println!("world");
18389 }
18390 "#
18391 .unindent(),
18392 );
18393
18394 cx.update_editor(|editor, window, cx| {
18395 editor.delete_line(&DeleteLine, window, cx);
18396 });
18397 executor.run_until_parked();
18398 cx.assert_state_with_diff(
18399 r#"
18400 use some::mod1;
18401 use some::mod2;
18402
18403 - const A: u32 = 42;
18404 - const B: u32 = 42;
18405 ˇconst C: u32 = 42;
18406
18407
18408 fn main() {
18409 println!("hello");
18410
18411 println!("world");
18412 }
18413 "#
18414 .unindent(),
18415 );
18416
18417 cx.update_editor(|editor, window, cx| {
18418 editor.delete_line(&DeleteLine, window, cx);
18419 });
18420 executor.run_until_parked();
18421 cx.assert_state_with_diff(
18422 r#"
18423 use some::mod1;
18424 use some::mod2;
18425
18426 - const A: u32 = 42;
18427 - const B: u32 = 42;
18428 - const C: u32 = 42;
18429 ˇ
18430
18431 fn main() {
18432 println!("hello");
18433
18434 println!("world");
18435 }
18436 "#
18437 .unindent(),
18438 );
18439
18440 cx.update_editor(|editor, window, cx| {
18441 editor.handle_input("replacement", window, cx);
18442 });
18443 executor.run_until_parked();
18444 cx.assert_state_with_diff(
18445 r#"
18446 use some::mod1;
18447 use some::mod2;
18448
18449 - const A: u32 = 42;
18450 - const B: u32 = 42;
18451 - const C: u32 = 42;
18452 -
18453 + replacementˇ
18454
18455 fn main() {
18456 println!("hello");
18457
18458 println!("world");
18459 }
18460 "#
18461 .unindent(),
18462 );
18463}
18464
18465#[gpui::test]
18466async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18467 init_test(cx, |_| {});
18468
18469 let mut cx = EditorTestContext::new(cx).await;
18470
18471 let base_text = r#"
18472 one
18473 two
18474 three
18475 four
18476 five
18477 "#
18478 .unindent();
18479 executor.run_until_parked();
18480 cx.set_state(
18481 &r#"
18482 one
18483 two
18484 fˇour
18485 five
18486 "#
18487 .unindent(),
18488 );
18489
18490 cx.set_head_text(&base_text);
18491 executor.run_until_parked();
18492
18493 cx.update_editor(|editor, window, cx| {
18494 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18495 });
18496 executor.run_until_parked();
18497
18498 cx.assert_state_with_diff(
18499 r#"
18500 one
18501 two
18502 - three
18503 fˇour
18504 five
18505 "#
18506 .unindent(),
18507 );
18508
18509 cx.update_editor(|editor, window, cx| {
18510 editor.backspace(&Backspace, window, cx);
18511 editor.backspace(&Backspace, window, cx);
18512 });
18513 executor.run_until_parked();
18514 cx.assert_state_with_diff(
18515 r#"
18516 one
18517 two
18518 - threeˇ
18519 - four
18520 + our
18521 five
18522 "#
18523 .unindent(),
18524 );
18525}
18526
18527#[gpui::test]
18528async fn test_edit_after_expanded_modification_hunk(
18529 executor: BackgroundExecutor,
18530 cx: &mut TestAppContext,
18531) {
18532 init_test(cx, |_| {});
18533
18534 let mut cx = EditorTestContext::new(cx).await;
18535
18536 let diff_base = r#"
18537 use some::mod1;
18538 use some::mod2;
18539
18540 const A: u32 = 42;
18541 const B: u32 = 42;
18542 const C: u32 = 42;
18543 const D: u32 = 42;
18544
18545
18546 fn main() {
18547 println!("hello");
18548
18549 println!("world");
18550 }"#
18551 .unindent();
18552
18553 cx.set_state(
18554 &r#"
18555 use some::mod1;
18556 use some::mod2;
18557
18558 const A: u32 = 42;
18559 const B: u32 = 42;
18560 const C: u32 = 43ˇ
18561 const D: u32 = 42;
18562
18563
18564 fn main() {
18565 println!("hello");
18566
18567 println!("world");
18568 }"#
18569 .unindent(),
18570 );
18571
18572 cx.set_head_text(&diff_base);
18573 executor.run_until_parked();
18574 cx.update_editor(|editor, window, cx| {
18575 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18576 });
18577 executor.run_until_parked();
18578
18579 cx.assert_state_with_diff(
18580 r#"
18581 use some::mod1;
18582 use some::mod2;
18583
18584 const A: u32 = 42;
18585 const B: u32 = 42;
18586 - const C: u32 = 42;
18587 + const C: u32 = 43ˇ
18588 const D: u32 = 42;
18589
18590
18591 fn main() {
18592 println!("hello");
18593
18594 println!("world");
18595 }"#
18596 .unindent(),
18597 );
18598
18599 cx.update_editor(|editor, window, cx| {
18600 editor.handle_input("\nnew_line\n", window, cx);
18601 });
18602 executor.run_until_parked();
18603
18604 cx.assert_state_with_diff(
18605 r#"
18606 use some::mod1;
18607 use some::mod2;
18608
18609 const A: u32 = 42;
18610 const B: u32 = 42;
18611 - const C: u32 = 42;
18612 + const C: u32 = 43
18613 + new_line
18614 + ˇ
18615 const D: u32 = 42;
18616
18617
18618 fn main() {
18619 println!("hello");
18620
18621 println!("world");
18622 }"#
18623 .unindent(),
18624 );
18625}
18626
18627#[gpui::test]
18628async fn test_stage_and_unstage_added_file_hunk(
18629 executor: BackgroundExecutor,
18630 cx: &mut TestAppContext,
18631) {
18632 init_test(cx, |_| {});
18633
18634 let mut cx = EditorTestContext::new(cx).await;
18635 cx.update_editor(|editor, _, cx| {
18636 editor.set_expand_all_diff_hunks(cx);
18637 });
18638
18639 let working_copy = r#"
18640 ˇfn main() {
18641 println!("hello, world!");
18642 }
18643 "#
18644 .unindent();
18645
18646 cx.set_state(&working_copy);
18647 executor.run_until_parked();
18648
18649 cx.assert_state_with_diff(
18650 r#"
18651 + ˇfn main() {
18652 + println!("hello, world!");
18653 + }
18654 "#
18655 .unindent(),
18656 );
18657 cx.assert_index_text(None);
18658
18659 cx.update_editor(|editor, window, cx| {
18660 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18661 });
18662 executor.run_until_parked();
18663 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
18664 cx.assert_state_with_diff(
18665 r#"
18666 + ˇfn main() {
18667 + println!("hello, world!");
18668 + }
18669 "#
18670 .unindent(),
18671 );
18672
18673 cx.update_editor(|editor, window, cx| {
18674 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18675 });
18676 executor.run_until_parked();
18677 cx.assert_index_text(None);
18678}
18679
18680async fn setup_indent_guides_editor(
18681 text: &str,
18682 cx: &mut TestAppContext,
18683) -> (BufferId, EditorTestContext) {
18684 init_test(cx, |_| {});
18685
18686 let mut cx = EditorTestContext::new(cx).await;
18687
18688 let buffer_id = cx.update_editor(|editor, window, cx| {
18689 editor.set_text(text, window, cx);
18690 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
18691
18692 buffer_ids[0]
18693 });
18694
18695 (buffer_id, cx)
18696}
18697
18698fn assert_indent_guides(
18699 range: Range<u32>,
18700 expected: Vec<IndentGuide>,
18701 active_indices: Option<Vec<usize>>,
18702 cx: &mut EditorTestContext,
18703) {
18704 let indent_guides = cx.update_editor(|editor, window, cx| {
18705 let snapshot = editor.snapshot(window, cx).display_snapshot;
18706 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
18707 editor,
18708 MultiBufferRow(range.start)..MultiBufferRow(range.end),
18709 true,
18710 &snapshot,
18711 cx,
18712 );
18713
18714 indent_guides.sort_by(|a, b| {
18715 a.depth.cmp(&b.depth).then(
18716 a.start_row
18717 .cmp(&b.start_row)
18718 .then(a.end_row.cmp(&b.end_row)),
18719 )
18720 });
18721 indent_guides
18722 });
18723
18724 if let Some(expected) = active_indices {
18725 let active_indices = cx.update_editor(|editor, window, cx| {
18726 let snapshot = editor.snapshot(window, cx).display_snapshot;
18727 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
18728 });
18729
18730 assert_eq!(
18731 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
18732 expected,
18733 "Active indent guide indices do not match"
18734 );
18735 }
18736
18737 assert_eq!(indent_guides, expected, "Indent guides do not match");
18738}
18739
18740fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
18741 IndentGuide {
18742 buffer_id,
18743 start_row: MultiBufferRow(start_row),
18744 end_row: MultiBufferRow(end_row),
18745 depth,
18746 tab_size: 4,
18747 settings: IndentGuideSettings {
18748 enabled: true,
18749 line_width: 1,
18750 active_line_width: 1,
18751 ..Default::default()
18752 },
18753 }
18754}
18755
18756#[gpui::test]
18757async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
18758 let (buffer_id, mut cx) = setup_indent_guides_editor(
18759 &"
18760 fn main() {
18761 let a = 1;
18762 }"
18763 .unindent(),
18764 cx,
18765 )
18766 .await;
18767
18768 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18769}
18770
18771#[gpui::test]
18772async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
18773 let (buffer_id, mut cx) = setup_indent_guides_editor(
18774 &"
18775 fn main() {
18776 let a = 1;
18777 let b = 2;
18778 }"
18779 .unindent(),
18780 cx,
18781 )
18782 .await;
18783
18784 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
18785}
18786
18787#[gpui::test]
18788async fn test_indent_guide_nested(cx: &mut TestAppContext) {
18789 let (buffer_id, mut cx) = setup_indent_guides_editor(
18790 &"
18791 fn main() {
18792 let a = 1;
18793 if a == 3 {
18794 let b = 2;
18795 } else {
18796 let c = 3;
18797 }
18798 }"
18799 .unindent(),
18800 cx,
18801 )
18802 .await;
18803
18804 assert_indent_guides(
18805 0..8,
18806 vec![
18807 indent_guide(buffer_id, 1, 6, 0),
18808 indent_guide(buffer_id, 3, 3, 1),
18809 indent_guide(buffer_id, 5, 5, 1),
18810 ],
18811 None,
18812 &mut cx,
18813 );
18814}
18815
18816#[gpui::test]
18817async fn test_indent_guide_tab(cx: &mut TestAppContext) {
18818 let (buffer_id, mut cx) = setup_indent_guides_editor(
18819 &"
18820 fn main() {
18821 let a = 1;
18822 let b = 2;
18823 let c = 3;
18824 }"
18825 .unindent(),
18826 cx,
18827 )
18828 .await;
18829
18830 assert_indent_guides(
18831 0..5,
18832 vec![
18833 indent_guide(buffer_id, 1, 3, 0),
18834 indent_guide(buffer_id, 2, 2, 1),
18835 ],
18836 None,
18837 &mut cx,
18838 );
18839}
18840
18841#[gpui::test]
18842async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
18843 let (buffer_id, mut cx) = setup_indent_guides_editor(
18844 &"
18845 fn main() {
18846 let a = 1;
18847
18848 let c = 3;
18849 }"
18850 .unindent(),
18851 cx,
18852 )
18853 .await;
18854
18855 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
18856}
18857
18858#[gpui::test]
18859async fn test_indent_guide_complex(cx: &mut TestAppContext) {
18860 let (buffer_id, mut cx) = setup_indent_guides_editor(
18861 &"
18862 fn main() {
18863 let a = 1;
18864
18865 let c = 3;
18866
18867 if a == 3 {
18868 let b = 2;
18869 } else {
18870 let c = 3;
18871 }
18872 }"
18873 .unindent(),
18874 cx,
18875 )
18876 .await;
18877
18878 assert_indent_guides(
18879 0..11,
18880 vec![
18881 indent_guide(buffer_id, 1, 9, 0),
18882 indent_guide(buffer_id, 6, 6, 1),
18883 indent_guide(buffer_id, 8, 8, 1),
18884 ],
18885 None,
18886 &mut cx,
18887 );
18888}
18889
18890#[gpui::test]
18891async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
18892 let (buffer_id, mut cx) = setup_indent_guides_editor(
18893 &"
18894 fn main() {
18895 let a = 1;
18896
18897 let c = 3;
18898
18899 if a == 3 {
18900 let b = 2;
18901 } else {
18902 let c = 3;
18903 }
18904 }"
18905 .unindent(),
18906 cx,
18907 )
18908 .await;
18909
18910 assert_indent_guides(
18911 1..11,
18912 vec![
18913 indent_guide(buffer_id, 1, 9, 0),
18914 indent_guide(buffer_id, 6, 6, 1),
18915 indent_guide(buffer_id, 8, 8, 1),
18916 ],
18917 None,
18918 &mut cx,
18919 );
18920}
18921
18922#[gpui::test]
18923async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
18924 let (buffer_id, mut cx) = setup_indent_guides_editor(
18925 &"
18926 fn main() {
18927 let a = 1;
18928
18929 let c = 3;
18930
18931 if a == 3 {
18932 let b = 2;
18933 } else {
18934 let c = 3;
18935 }
18936 }"
18937 .unindent(),
18938 cx,
18939 )
18940 .await;
18941
18942 assert_indent_guides(
18943 1..10,
18944 vec![
18945 indent_guide(buffer_id, 1, 9, 0),
18946 indent_guide(buffer_id, 6, 6, 1),
18947 indent_guide(buffer_id, 8, 8, 1),
18948 ],
18949 None,
18950 &mut cx,
18951 );
18952}
18953
18954#[gpui::test]
18955async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
18956 let (buffer_id, mut cx) = setup_indent_guides_editor(
18957 &"
18958 fn main() {
18959 if a {
18960 b(
18961 c,
18962 d,
18963 )
18964 } else {
18965 e(
18966 f
18967 )
18968 }
18969 }"
18970 .unindent(),
18971 cx,
18972 )
18973 .await;
18974
18975 assert_indent_guides(
18976 0..11,
18977 vec![
18978 indent_guide(buffer_id, 1, 10, 0),
18979 indent_guide(buffer_id, 2, 5, 1),
18980 indent_guide(buffer_id, 7, 9, 1),
18981 indent_guide(buffer_id, 3, 4, 2),
18982 indent_guide(buffer_id, 8, 8, 2),
18983 ],
18984 None,
18985 &mut cx,
18986 );
18987
18988 cx.update_editor(|editor, window, cx| {
18989 editor.fold_at(MultiBufferRow(2), window, cx);
18990 assert_eq!(
18991 editor.display_text(cx),
18992 "
18993 fn main() {
18994 if a {
18995 b(⋯
18996 )
18997 } else {
18998 e(
18999 f
19000 )
19001 }
19002 }"
19003 .unindent()
19004 );
19005 });
19006
19007 assert_indent_guides(
19008 0..11,
19009 vec![
19010 indent_guide(buffer_id, 1, 10, 0),
19011 indent_guide(buffer_id, 2, 5, 1),
19012 indent_guide(buffer_id, 7, 9, 1),
19013 indent_guide(buffer_id, 8, 8, 2),
19014 ],
19015 None,
19016 &mut cx,
19017 );
19018}
19019
19020#[gpui::test]
19021async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
19022 let (buffer_id, mut cx) = setup_indent_guides_editor(
19023 &"
19024 block1
19025 block2
19026 block3
19027 block4
19028 block2
19029 block1
19030 block1"
19031 .unindent(),
19032 cx,
19033 )
19034 .await;
19035
19036 assert_indent_guides(
19037 1..10,
19038 vec![
19039 indent_guide(buffer_id, 1, 4, 0),
19040 indent_guide(buffer_id, 2, 3, 1),
19041 indent_guide(buffer_id, 3, 3, 2),
19042 ],
19043 None,
19044 &mut cx,
19045 );
19046}
19047
19048#[gpui::test]
19049async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
19050 let (buffer_id, mut cx) = setup_indent_guides_editor(
19051 &"
19052 block1
19053 block2
19054 block3
19055
19056 block1
19057 block1"
19058 .unindent(),
19059 cx,
19060 )
19061 .await;
19062
19063 assert_indent_guides(
19064 0..6,
19065 vec![
19066 indent_guide(buffer_id, 1, 2, 0),
19067 indent_guide(buffer_id, 2, 2, 1),
19068 ],
19069 None,
19070 &mut cx,
19071 );
19072}
19073
19074#[gpui::test]
19075async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
19076 let (buffer_id, mut cx) = setup_indent_guides_editor(
19077 &"
19078 function component() {
19079 \treturn (
19080 \t\t\t
19081 \t\t<div>
19082 \t\t\t<abc></abc>
19083 \t\t</div>
19084 \t)
19085 }"
19086 .unindent(),
19087 cx,
19088 )
19089 .await;
19090
19091 assert_indent_guides(
19092 0..8,
19093 vec![
19094 indent_guide(buffer_id, 1, 6, 0),
19095 indent_guide(buffer_id, 2, 5, 1),
19096 indent_guide(buffer_id, 4, 4, 2),
19097 ],
19098 None,
19099 &mut cx,
19100 );
19101}
19102
19103#[gpui::test]
19104async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
19105 let (buffer_id, mut cx) = setup_indent_guides_editor(
19106 &"
19107 function component() {
19108 \treturn (
19109 \t
19110 \t\t<div>
19111 \t\t\t<abc></abc>
19112 \t\t</div>
19113 \t)
19114 }"
19115 .unindent(),
19116 cx,
19117 )
19118 .await;
19119
19120 assert_indent_guides(
19121 0..8,
19122 vec![
19123 indent_guide(buffer_id, 1, 6, 0),
19124 indent_guide(buffer_id, 2, 5, 1),
19125 indent_guide(buffer_id, 4, 4, 2),
19126 ],
19127 None,
19128 &mut cx,
19129 );
19130}
19131
19132#[gpui::test]
19133async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
19134 let (buffer_id, mut cx) = setup_indent_guides_editor(
19135 &"
19136 block1
19137
19138
19139
19140 block2
19141 "
19142 .unindent(),
19143 cx,
19144 )
19145 .await;
19146
19147 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
19148}
19149
19150#[gpui::test]
19151async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
19152 let (buffer_id, mut cx) = setup_indent_guides_editor(
19153 &"
19154 def a:
19155 \tb = 3
19156 \tif True:
19157 \t\tc = 4
19158 \t\td = 5
19159 \tprint(b)
19160 "
19161 .unindent(),
19162 cx,
19163 )
19164 .await;
19165
19166 assert_indent_guides(
19167 0..6,
19168 vec![
19169 indent_guide(buffer_id, 1, 5, 0),
19170 indent_guide(buffer_id, 3, 4, 1),
19171 ],
19172 None,
19173 &mut cx,
19174 );
19175}
19176
19177#[gpui::test]
19178async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
19179 let (buffer_id, mut cx) = setup_indent_guides_editor(
19180 &"
19181 fn main() {
19182 let a = 1;
19183 }"
19184 .unindent(),
19185 cx,
19186 )
19187 .await;
19188
19189 cx.update_editor(|editor, window, cx| {
19190 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19191 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
19192 });
19193 });
19194
19195 assert_indent_guides(
19196 0..3,
19197 vec![indent_guide(buffer_id, 1, 1, 0)],
19198 Some(vec![0]),
19199 &mut cx,
19200 );
19201}
19202
19203#[gpui::test]
19204async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
19205 let (buffer_id, mut cx) = setup_indent_guides_editor(
19206 &"
19207 fn main() {
19208 if 1 == 2 {
19209 let a = 1;
19210 }
19211 }"
19212 .unindent(),
19213 cx,
19214 )
19215 .await;
19216
19217 cx.update_editor(|editor, window, cx| {
19218 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19219 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
19220 });
19221 });
19222
19223 assert_indent_guides(
19224 0..4,
19225 vec![
19226 indent_guide(buffer_id, 1, 3, 0),
19227 indent_guide(buffer_id, 2, 2, 1),
19228 ],
19229 Some(vec![1]),
19230 &mut cx,
19231 );
19232
19233 cx.update_editor(|editor, window, cx| {
19234 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19235 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
19236 });
19237 });
19238
19239 assert_indent_guides(
19240 0..4,
19241 vec![
19242 indent_guide(buffer_id, 1, 3, 0),
19243 indent_guide(buffer_id, 2, 2, 1),
19244 ],
19245 Some(vec![1]),
19246 &mut cx,
19247 );
19248
19249 cx.update_editor(|editor, window, cx| {
19250 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19251 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
19252 });
19253 });
19254
19255 assert_indent_guides(
19256 0..4,
19257 vec![
19258 indent_guide(buffer_id, 1, 3, 0),
19259 indent_guide(buffer_id, 2, 2, 1),
19260 ],
19261 Some(vec![0]),
19262 &mut cx,
19263 );
19264}
19265
19266#[gpui::test]
19267async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
19268 let (buffer_id, mut cx) = setup_indent_guides_editor(
19269 &"
19270 fn main() {
19271 let a = 1;
19272
19273 let b = 2;
19274 }"
19275 .unindent(),
19276 cx,
19277 )
19278 .await;
19279
19280 cx.update_editor(|editor, window, cx| {
19281 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19282 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
19283 });
19284 });
19285
19286 assert_indent_guides(
19287 0..5,
19288 vec![indent_guide(buffer_id, 1, 3, 0)],
19289 Some(vec![0]),
19290 &mut cx,
19291 );
19292}
19293
19294#[gpui::test]
19295async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
19296 let (buffer_id, mut cx) = setup_indent_guides_editor(
19297 &"
19298 def m:
19299 a = 1
19300 pass"
19301 .unindent(),
19302 cx,
19303 )
19304 .await;
19305
19306 cx.update_editor(|editor, window, cx| {
19307 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19308 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
19309 });
19310 });
19311
19312 assert_indent_guides(
19313 0..3,
19314 vec![indent_guide(buffer_id, 1, 2, 0)],
19315 Some(vec![0]),
19316 &mut cx,
19317 );
19318}
19319
19320#[gpui::test]
19321async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
19322 init_test(cx, |_| {});
19323 let mut cx = EditorTestContext::new(cx).await;
19324 let text = indoc! {
19325 "
19326 impl A {
19327 fn b() {
19328 0;
19329 3;
19330 5;
19331 6;
19332 7;
19333 }
19334 }
19335 "
19336 };
19337 let base_text = indoc! {
19338 "
19339 impl A {
19340 fn b() {
19341 0;
19342 1;
19343 2;
19344 3;
19345 4;
19346 }
19347 fn c() {
19348 5;
19349 6;
19350 7;
19351 }
19352 }
19353 "
19354 };
19355
19356 cx.update_editor(|editor, window, cx| {
19357 editor.set_text(text, window, cx);
19358
19359 editor.buffer().update(cx, |multibuffer, cx| {
19360 let buffer = multibuffer.as_singleton().unwrap();
19361 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
19362
19363 multibuffer.set_all_diff_hunks_expanded(cx);
19364 multibuffer.add_diff(diff, cx);
19365
19366 buffer.read(cx).remote_id()
19367 })
19368 });
19369 cx.run_until_parked();
19370
19371 cx.assert_state_with_diff(
19372 indoc! { "
19373 impl A {
19374 fn b() {
19375 0;
19376 - 1;
19377 - 2;
19378 3;
19379 - 4;
19380 - }
19381 - fn c() {
19382 5;
19383 6;
19384 7;
19385 }
19386 }
19387 ˇ"
19388 }
19389 .to_string(),
19390 );
19391
19392 let mut actual_guides = cx.update_editor(|editor, window, cx| {
19393 editor
19394 .snapshot(window, cx)
19395 .buffer_snapshot
19396 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
19397 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
19398 .collect::<Vec<_>>()
19399 });
19400 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
19401 assert_eq!(
19402 actual_guides,
19403 vec![
19404 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
19405 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
19406 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
19407 ]
19408 );
19409}
19410
19411#[gpui::test]
19412async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19413 init_test(cx, |_| {});
19414 let mut cx = EditorTestContext::new(cx).await;
19415
19416 let diff_base = r#"
19417 a
19418 b
19419 c
19420 "#
19421 .unindent();
19422
19423 cx.set_state(
19424 &r#"
19425 ˇA
19426 b
19427 C
19428 "#
19429 .unindent(),
19430 );
19431 cx.set_head_text(&diff_base);
19432 cx.update_editor(|editor, window, cx| {
19433 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19434 });
19435 executor.run_until_parked();
19436
19437 let both_hunks_expanded = r#"
19438 - a
19439 + ˇA
19440 b
19441 - c
19442 + C
19443 "#
19444 .unindent();
19445
19446 cx.assert_state_with_diff(both_hunks_expanded.clone());
19447
19448 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19449 let snapshot = editor.snapshot(window, cx);
19450 let hunks = editor
19451 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19452 .collect::<Vec<_>>();
19453 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19454 let buffer_id = hunks[0].buffer_id;
19455 hunks
19456 .into_iter()
19457 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
19458 .collect::<Vec<_>>()
19459 });
19460 assert_eq!(hunk_ranges.len(), 2);
19461
19462 cx.update_editor(|editor, _, cx| {
19463 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19464 });
19465 executor.run_until_parked();
19466
19467 let second_hunk_expanded = r#"
19468 ˇA
19469 b
19470 - c
19471 + C
19472 "#
19473 .unindent();
19474
19475 cx.assert_state_with_diff(second_hunk_expanded);
19476
19477 cx.update_editor(|editor, _, cx| {
19478 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19479 });
19480 executor.run_until_parked();
19481
19482 cx.assert_state_with_diff(both_hunks_expanded.clone());
19483
19484 cx.update_editor(|editor, _, cx| {
19485 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19486 });
19487 executor.run_until_parked();
19488
19489 let first_hunk_expanded = r#"
19490 - a
19491 + ˇA
19492 b
19493 C
19494 "#
19495 .unindent();
19496
19497 cx.assert_state_with_diff(first_hunk_expanded);
19498
19499 cx.update_editor(|editor, _, cx| {
19500 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19501 });
19502 executor.run_until_parked();
19503
19504 cx.assert_state_with_diff(both_hunks_expanded);
19505
19506 cx.set_state(
19507 &r#"
19508 ˇA
19509 b
19510 "#
19511 .unindent(),
19512 );
19513 cx.run_until_parked();
19514
19515 // TODO this cursor position seems bad
19516 cx.assert_state_with_diff(
19517 r#"
19518 - ˇa
19519 + A
19520 b
19521 "#
19522 .unindent(),
19523 );
19524
19525 cx.update_editor(|editor, window, cx| {
19526 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19527 });
19528
19529 cx.assert_state_with_diff(
19530 r#"
19531 - ˇa
19532 + A
19533 b
19534 - c
19535 "#
19536 .unindent(),
19537 );
19538
19539 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19540 let snapshot = editor.snapshot(window, cx);
19541 let hunks = editor
19542 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19543 .collect::<Vec<_>>();
19544 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19545 let buffer_id = hunks[0].buffer_id;
19546 hunks
19547 .into_iter()
19548 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
19549 .collect::<Vec<_>>()
19550 });
19551 assert_eq!(hunk_ranges.len(), 2);
19552
19553 cx.update_editor(|editor, _, cx| {
19554 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19555 });
19556 executor.run_until_parked();
19557
19558 cx.assert_state_with_diff(
19559 r#"
19560 - ˇa
19561 + A
19562 b
19563 "#
19564 .unindent(),
19565 );
19566}
19567
19568#[gpui::test]
19569async fn test_toggle_deletion_hunk_at_start_of_file(
19570 executor: BackgroundExecutor,
19571 cx: &mut TestAppContext,
19572) {
19573 init_test(cx, |_| {});
19574 let mut cx = EditorTestContext::new(cx).await;
19575
19576 let diff_base = r#"
19577 a
19578 b
19579 c
19580 "#
19581 .unindent();
19582
19583 cx.set_state(
19584 &r#"
19585 ˇb
19586 c
19587 "#
19588 .unindent(),
19589 );
19590 cx.set_head_text(&diff_base);
19591 cx.update_editor(|editor, window, cx| {
19592 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19593 });
19594 executor.run_until_parked();
19595
19596 let hunk_expanded = r#"
19597 - a
19598 ˇb
19599 c
19600 "#
19601 .unindent();
19602
19603 cx.assert_state_with_diff(hunk_expanded.clone());
19604
19605 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19606 let snapshot = editor.snapshot(window, cx);
19607 let hunks = editor
19608 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19609 .collect::<Vec<_>>();
19610 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19611 let buffer_id = hunks[0].buffer_id;
19612 hunks
19613 .into_iter()
19614 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
19615 .collect::<Vec<_>>()
19616 });
19617 assert_eq!(hunk_ranges.len(), 1);
19618
19619 cx.update_editor(|editor, _, cx| {
19620 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19621 });
19622 executor.run_until_parked();
19623
19624 let hunk_collapsed = r#"
19625 ˇb
19626 c
19627 "#
19628 .unindent();
19629
19630 cx.assert_state_with_diff(hunk_collapsed);
19631
19632 cx.update_editor(|editor, _, cx| {
19633 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19634 });
19635 executor.run_until_parked();
19636
19637 cx.assert_state_with_diff(hunk_expanded);
19638}
19639
19640#[gpui::test]
19641async fn test_display_diff_hunks(cx: &mut TestAppContext) {
19642 init_test(cx, |_| {});
19643
19644 let fs = FakeFs::new(cx.executor());
19645 fs.insert_tree(
19646 path!("/test"),
19647 json!({
19648 ".git": {},
19649 "file-1": "ONE\n",
19650 "file-2": "TWO\n",
19651 "file-3": "THREE\n",
19652 }),
19653 )
19654 .await;
19655
19656 fs.set_head_for_repo(
19657 path!("/test/.git").as_ref(),
19658 &[
19659 ("file-1".into(), "one\n".into()),
19660 ("file-2".into(), "two\n".into()),
19661 ("file-3".into(), "three\n".into()),
19662 ],
19663 "deadbeef",
19664 );
19665
19666 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
19667 let mut buffers = vec![];
19668 for i in 1..=3 {
19669 let buffer = project
19670 .update(cx, |project, cx| {
19671 let path = format!(path!("/test/file-{}"), i);
19672 project.open_local_buffer(path, cx)
19673 })
19674 .await
19675 .unwrap();
19676 buffers.push(buffer);
19677 }
19678
19679 let multibuffer = cx.new(|cx| {
19680 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
19681 multibuffer.set_all_diff_hunks_expanded(cx);
19682 for buffer in &buffers {
19683 let snapshot = buffer.read(cx).snapshot();
19684 multibuffer.set_excerpts_for_path(
19685 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
19686 buffer.clone(),
19687 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
19688 DEFAULT_MULTIBUFFER_CONTEXT,
19689 cx,
19690 );
19691 }
19692 multibuffer
19693 });
19694
19695 let editor = cx.add_window(|window, cx| {
19696 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
19697 });
19698 cx.run_until_parked();
19699
19700 let snapshot = editor
19701 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19702 .unwrap();
19703 let hunks = snapshot
19704 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
19705 .map(|hunk| match hunk {
19706 DisplayDiffHunk::Unfolded {
19707 display_row_range, ..
19708 } => display_row_range,
19709 DisplayDiffHunk::Folded { .. } => unreachable!(),
19710 })
19711 .collect::<Vec<_>>();
19712 assert_eq!(
19713 hunks,
19714 [
19715 DisplayRow(2)..DisplayRow(4),
19716 DisplayRow(7)..DisplayRow(9),
19717 DisplayRow(12)..DisplayRow(14),
19718 ]
19719 );
19720}
19721
19722#[gpui::test]
19723async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
19724 init_test(cx, |_| {});
19725
19726 let mut cx = EditorTestContext::new(cx).await;
19727 cx.set_head_text(indoc! { "
19728 one
19729 two
19730 three
19731 four
19732 five
19733 "
19734 });
19735 cx.set_index_text(indoc! { "
19736 one
19737 two
19738 three
19739 four
19740 five
19741 "
19742 });
19743 cx.set_state(indoc! {"
19744 one
19745 TWO
19746 ˇTHREE
19747 FOUR
19748 five
19749 "});
19750 cx.run_until_parked();
19751 cx.update_editor(|editor, window, cx| {
19752 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19753 });
19754 cx.run_until_parked();
19755 cx.assert_index_text(Some(indoc! {"
19756 one
19757 TWO
19758 THREE
19759 FOUR
19760 five
19761 "}));
19762 cx.set_state(indoc! { "
19763 one
19764 TWO
19765 ˇTHREE-HUNDRED
19766 FOUR
19767 five
19768 "});
19769 cx.run_until_parked();
19770 cx.update_editor(|editor, window, cx| {
19771 let snapshot = editor.snapshot(window, cx);
19772 let hunks = editor
19773 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19774 .collect::<Vec<_>>();
19775 assert_eq!(hunks.len(), 1);
19776 assert_eq!(
19777 hunks[0].status(),
19778 DiffHunkStatus {
19779 kind: DiffHunkStatusKind::Modified,
19780 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
19781 }
19782 );
19783
19784 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19785 });
19786 cx.run_until_parked();
19787 cx.assert_index_text(Some(indoc! {"
19788 one
19789 TWO
19790 THREE-HUNDRED
19791 FOUR
19792 five
19793 "}));
19794}
19795
19796#[gpui::test]
19797fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
19798 init_test(cx, |_| {});
19799
19800 let editor = cx.add_window(|window, cx| {
19801 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
19802 build_editor(buffer, window, cx)
19803 });
19804
19805 let render_args = Arc::new(Mutex::new(None));
19806 let snapshot = editor
19807 .update(cx, |editor, window, cx| {
19808 let snapshot = editor.buffer().read(cx).snapshot(cx);
19809 let range =
19810 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
19811
19812 struct RenderArgs {
19813 row: MultiBufferRow,
19814 folded: bool,
19815 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
19816 }
19817
19818 let crease = Crease::inline(
19819 range,
19820 FoldPlaceholder::test(),
19821 {
19822 let toggle_callback = render_args.clone();
19823 move |row, folded, callback, _window, _cx| {
19824 *toggle_callback.lock() = Some(RenderArgs {
19825 row,
19826 folded,
19827 callback,
19828 });
19829 div()
19830 }
19831 },
19832 |_row, _folded, _window, _cx| div(),
19833 );
19834
19835 editor.insert_creases(Some(crease), cx);
19836 let snapshot = editor.snapshot(window, cx);
19837 let _div =
19838 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
19839 snapshot
19840 })
19841 .unwrap();
19842
19843 let render_args = render_args.lock().take().unwrap();
19844 assert_eq!(render_args.row, MultiBufferRow(1));
19845 assert!(!render_args.folded);
19846 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19847
19848 cx.update_window(*editor, |_, window, cx| {
19849 (render_args.callback)(true, window, cx)
19850 })
19851 .unwrap();
19852 let snapshot = editor
19853 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19854 .unwrap();
19855 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
19856
19857 cx.update_window(*editor, |_, window, cx| {
19858 (render_args.callback)(false, window, cx)
19859 })
19860 .unwrap();
19861 let snapshot = editor
19862 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19863 .unwrap();
19864 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19865}
19866
19867#[gpui::test]
19868async fn test_input_text(cx: &mut TestAppContext) {
19869 init_test(cx, |_| {});
19870 let mut cx = EditorTestContext::new(cx).await;
19871
19872 cx.set_state(
19873 &r#"ˇone
19874 two
19875
19876 three
19877 fourˇ
19878 five
19879
19880 siˇx"#
19881 .unindent(),
19882 );
19883
19884 cx.dispatch_action(HandleInput(String::new()));
19885 cx.assert_editor_state(
19886 &r#"ˇone
19887 two
19888
19889 three
19890 fourˇ
19891 five
19892
19893 siˇx"#
19894 .unindent(),
19895 );
19896
19897 cx.dispatch_action(HandleInput("AAAA".to_string()));
19898 cx.assert_editor_state(
19899 &r#"AAAAˇone
19900 two
19901
19902 three
19903 fourAAAAˇ
19904 five
19905
19906 siAAAAˇx"#
19907 .unindent(),
19908 );
19909}
19910
19911#[gpui::test]
19912async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
19913 init_test(cx, |_| {});
19914
19915 let mut cx = EditorTestContext::new(cx).await;
19916 cx.set_state(
19917 r#"let foo = 1;
19918let foo = 2;
19919let foo = 3;
19920let fooˇ = 4;
19921let foo = 5;
19922let foo = 6;
19923let foo = 7;
19924let foo = 8;
19925let foo = 9;
19926let foo = 10;
19927let foo = 11;
19928let foo = 12;
19929let foo = 13;
19930let foo = 14;
19931let foo = 15;"#,
19932 );
19933
19934 cx.update_editor(|e, window, cx| {
19935 assert_eq!(
19936 e.next_scroll_position,
19937 NextScrollCursorCenterTopBottom::Center,
19938 "Default next scroll direction is center",
19939 );
19940
19941 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19942 assert_eq!(
19943 e.next_scroll_position,
19944 NextScrollCursorCenterTopBottom::Top,
19945 "After center, next scroll direction should be top",
19946 );
19947
19948 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19949 assert_eq!(
19950 e.next_scroll_position,
19951 NextScrollCursorCenterTopBottom::Bottom,
19952 "After top, next scroll direction should be bottom",
19953 );
19954
19955 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19956 assert_eq!(
19957 e.next_scroll_position,
19958 NextScrollCursorCenterTopBottom::Center,
19959 "After bottom, scrolling should start over",
19960 );
19961
19962 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19963 assert_eq!(
19964 e.next_scroll_position,
19965 NextScrollCursorCenterTopBottom::Top,
19966 "Scrolling continues if retriggered fast enough"
19967 );
19968 });
19969
19970 cx.executor()
19971 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
19972 cx.executor().run_until_parked();
19973 cx.update_editor(|e, _, _| {
19974 assert_eq!(
19975 e.next_scroll_position,
19976 NextScrollCursorCenterTopBottom::Center,
19977 "If scrolling is not triggered fast enough, it should reset"
19978 );
19979 });
19980}
19981
19982#[gpui::test]
19983async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
19984 init_test(cx, |_| {});
19985 let mut cx = EditorLspTestContext::new_rust(
19986 lsp::ServerCapabilities {
19987 definition_provider: Some(lsp::OneOf::Left(true)),
19988 references_provider: Some(lsp::OneOf::Left(true)),
19989 ..lsp::ServerCapabilities::default()
19990 },
19991 cx,
19992 )
19993 .await;
19994
19995 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
19996 let go_to_definition = cx
19997 .lsp
19998 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19999 move |params, _| async move {
20000 if empty_go_to_definition {
20001 Ok(None)
20002 } else {
20003 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
20004 uri: params.text_document_position_params.text_document.uri,
20005 range: lsp::Range::new(
20006 lsp::Position::new(4, 3),
20007 lsp::Position::new(4, 6),
20008 ),
20009 })))
20010 }
20011 },
20012 );
20013 let references = cx
20014 .lsp
20015 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
20016 Ok(Some(vec![lsp::Location {
20017 uri: params.text_document_position.text_document.uri,
20018 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
20019 }]))
20020 });
20021 (go_to_definition, references)
20022 };
20023
20024 cx.set_state(
20025 &r#"fn one() {
20026 let mut a = ˇtwo();
20027 }
20028
20029 fn two() {}"#
20030 .unindent(),
20031 );
20032 set_up_lsp_handlers(false, &mut cx);
20033 let navigated = cx
20034 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
20035 .await
20036 .expect("Failed to navigate to definition");
20037 assert_eq!(
20038 navigated,
20039 Navigated::Yes,
20040 "Should have navigated to definition from the GetDefinition response"
20041 );
20042 cx.assert_editor_state(
20043 &r#"fn one() {
20044 let mut a = two();
20045 }
20046
20047 fn «twoˇ»() {}"#
20048 .unindent(),
20049 );
20050
20051 let editors = cx.update_workspace(|workspace, _, cx| {
20052 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
20053 });
20054 cx.update_editor(|_, _, test_editor_cx| {
20055 assert_eq!(
20056 editors.len(),
20057 1,
20058 "Initially, only one, test, editor should be open in the workspace"
20059 );
20060 assert_eq!(
20061 test_editor_cx.entity(),
20062 editors.last().expect("Asserted len is 1").clone()
20063 );
20064 });
20065
20066 set_up_lsp_handlers(true, &mut cx);
20067 let navigated = cx
20068 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
20069 .await
20070 .expect("Failed to navigate to lookup references");
20071 assert_eq!(
20072 navigated,
20073 Navigated::Yes,
20074 "Should have navigated to references as a fallback after empty GoToDefinition response"
20075 );
20076 // We should not change the selections in the existing file,
20077 // if opening another milti buffer with the references
20078 cx.assert_editor_state(
20079 &r#"fn one() {
20080 let mut a = two();
20081 }
20082
20083 fn «twoˇ»() {}"#
20084 .unindent(),
20085 );
20086 let editors = cx.update_workspace(|workspace, _, cx| {
20087 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
20088 });
20089 cx.update_editor(|_, _, test_editor_cx| {
20090 assert_eq!(
20091 editors.len(),
20092 2,
20093 "After falling back to references search, we open a new editor with the results"
20094 );
20095 let references_fallback_text = editors
20096 .into_iter()
20097 .find(|new_editor| *new_editor != test_editor_cx.entity())
20098 .expect("Should have one non-test editor now")
20099 .read(test_editor_cx)
20100 .text(test_editor_cx);
20101 assert_eq!(
20102 references_fallback_text, "fn one() {\n let mut a = two();\n}",
20103 "Should use the range from the references response and not the GoToDefinition one"
20104 );
20105 });
20106}
20107
20108#[gpui::test]
20109async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
20110 init_test(cx, |_| {});
20111 cx.update(|cx| {
20112 let mut editor_settings = EditorSettings::get_global(cx).clone();
20113 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
20114 EditorSettings::override_global(editor_settings, cx);
20115 });
20116 let mut cx = EditorLspTestContext::new_rust(
20117 lsp::ServerCapabilities {
20118 definition_provider: Some(lsp::OneOf::Left(true)),
20119 references_provider: Some(lsp::OneOf::Left(true)),
20120 ..lsp::ServerCapabilities::default()
20121 },
20122 cx,
20123 )
20124 .await;
20125 let original_state = r#"fn one() {
20126 let mut a = ˇtwo();
20127 }
20128
20129 fn two() {}"#
20130 .unindent();
20131 cx.set_state(&original_state);
20132
20133 let mut go_to_definition = cx
20134 .lsp
20135 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
20136 move |_, _| async move { Ok(None) },
20137 );
20138 let _references = cx
20139 .lsp
20140 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
20141 panic!("Should not call for references with no go to definition fallback")
20142 });
20143
20144 let navigated = cx
20145 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
20146 .await
20147 .expect("Failed to navigate to lookup references");
20148 go_to_definition
20149 .next()
20150 .await
20151 .expect("Should have called the go_to_definition handler");
20152
20153 assert_eq!(
20154 navigated,
20155 Navigated::No,
20156 "Should have navigated to references as a fallback after empty GoToDefinition response"
20157 );
20158 cx.assert_editor_state(&original_state);
20159 let editors = cx.update_workspace(|workspace, _, cx| {
20160 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
20161 });
20162 cx.update_editor(|_, _, _| {
20163 assert_eq!(
20164 editors.len(),
20165 1,
20166 "After unsuccessful fallback, no other editor should have been opened"
20167 );
20168 });
20169}
20170
20171#[gpui::test]
20172async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
20173 init_test(cx, |_| {});
20174
20175 let language = Arc::new(Language::new(
20176 LanguageConfig::default(),
20177 Some(tree_sitter_rust::LANGUAGE.into()),
20178 ));
20179
20180 let text = r#"
20181 #[cfg(test)]
20182 mod tests() {
20183 #[test]
20184 fn runnable_1() {
20185 let a = 1;
20186 }
20187
20188 #[test]
20189 fn runnable_2() {
20190 let a = 1;
20191 let b = 2;
20192 }
20193 }
20194 "#
20195 .unindent();
20196
20197 let fs = FakeFs::new(cx.executor());
20198 fs.insert_file("/file.rs", Default::default()).await;
20199
20200 let project = Project::test(fs, ["/a".as_ref()], cx).await;
20201 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20202 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20203 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
20204 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
20205
20206 let editor = cx.new_window_entity(|window, cx| {
20207 Editor::new(
20208 EditorMode::full(),
20209 multi_buffer,
20210 Some(project.clone()),
20211 window,
20212 cx,
20213 )
20214 });
20215
20216 editor.update_in(cx, |editor, window, cx| {
20217 let snapshot = editor.buffer().read(cx).snapshot(cx);
20218 editor.tasks.insert(
20219 (buffer.read(cx).remote_id(), 3),
20220 RunnableTasks {
20221 templates: vec![],
20222 offset: snapshot.anchor_before(43),
20223 column: 0,
20224 extra_variables: HashMap::default(),
20225 context_range: BufferOffset(43)..BufferOffset(85),
20226 },
20227 );
20228 editor.tasks.insert(
20229 (buffer.read(cx).remote_id(), 8),
20230 RunnableTasks {
20231 templates: vec![],
20232 offset: snapshot.anchor_before(86),
20233 column: 0,
20234 extra_variables: HashMap::default(),
20235 context_range: BufferOffset(86)..BufferOffset(191),
20236 },
20237 );
20238
20239 // Test finding task when cursor is inside function body
20240 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20241 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
20242 });
20243 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
20244 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
20245
20246 // Test finding task when cursor is on function name
20247 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20248 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
20249 });
20250 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
20251 assert_eq!(row, 8, "Should find task when cursor is on function name");
20252 });
20253}
20254
20255#[gpui::test]
20256async fn test_folding_buffers(cx: &mut TestAppContext) {
20257 init_test(cx, |_| {});
20258
20259 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
20260 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
20261 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
20262
20263 let fs = FakeFs::new(cx.executor());
20264 fs.insert_tree(
20265 path!("/a"),
20266 json!({
20267 "first.rs": sample_text_1,
20268 "second.rs": sample_text_2,
20269 "third.rs": sample_text_3,
20270 }),
20271 )
20272 .await;
20273 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20274 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20275 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20276 let worktree = project.update(cx, |project, cx| {
20277 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20278 assert_eq!(worktrees.len(), 1);
20279 worktrees.pop().unwrap()
20280 });
20281 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20282
20283 let buffer_1 = project
20284 .update(cx, |project, cx| {
20285 project.open_buffer((worktree_id, "first.rs"), cx)
20286 })
20287 .await
20288 .unwrap();
20289 let buffer_2 = project
20290 .update(cx, |project, cx| {
20291 project.open_buffer((worktree_id, "second.rs"), cx)
20292 })
20293 .await
20294 .unwrap();
20295 let buffer_3 = project
20296 .update(cx, |project, cx| {
20297 project.open_buffer((worktree_id, "third.rs"), cx)
20298 })
20299 .await
20300 .unwrap();
20301
20302 let multi_buffer = cx.new(|cx| {
20303 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20304 multi_buffer.push_excerpts(
20305 buffer_1.clone(),
20306 [
20307 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20308 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20309 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20310 ],
20311 cx,
20312 );
20313 multi_buffer.push_excerpts(
20314 buffer_2.clone(),
20315 [
20316 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20317 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20318 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20319 ],
20320 cx,
20321 );
20322 multi_buffer.push_excerpts(
20323 buffer_3.clone(),
20324 [
20325 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20326 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20327 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20328 ],
20329 cx,
20330 );
20331 multi_buffer
20332 });
20333 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20334 Editor::new(
20335 EditorMode::full(),
20336 multi_buffer.clone(),
20337 Some(project.clone()),
20338 window,
20339 cx,
20340 )
20341 });
20342
20343 assert_eq!(
20344 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20345 "\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",
20346 );
20347
20348 multi_buffer_editor.update(cx, |editor, cx| {
20349 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
20350 });
20351 assert_eq!(
20352 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20353 "\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",
20354 "After folding the first buffer, its text should not be displayed"
20355 );
20356
20357 multi_buffer_editor.update(cx, |editor, cx| {
20358 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
20359 });
20360 assert_eq!(
20361 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20362 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
20363 "After folding the second buffer, its text should not be displayed"
20364 );
20365
20366 multi_buffer_editor.update(cx, |editor, cx| {
20367 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
20368 });
20369 assert_eq!(
20370 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20371 "\n\n\n\n\n",
20372 "After folding the third buffer, its text should not be displayed"
20373 );
20374
20375 // Emulate selection inside the fold logic, that should work
20376 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20377 editor
20378 .snapshot(window, cx)
20379 .next_line_boundary(Point::new(0, 4));
20380 });
20381
20382 multi_buffer_editor.update(cx, |editor, cx| {
20383 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
20384 });
20385 assert_eq!(
20386 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20387 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
20388 "After unfolding the second buffer, its text should be displayed"
20389 );
20390
20391 // Typing inside of buffer 1 causes that buffer to be unfolded.
20392 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20393 assert_eq!(
20394 multi_buffer
20395 .read(cx)
20396 .snapshot(cx)
20397 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
20398 .collect::<String>(),
20399 "bbbb"
20400 );
20401 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
20402 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
20403 });
20404 editor.handle_input("B", window, cx);
20405 });
20406
20407 assert_eq!(
20408 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20409 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
20410 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
20411 );
20412
20413 multi_buffer_editor.update(cx, |editor, cx| {
20414 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
20415 });
20416 assert_eq!(
20417 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20418 "\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",
20419 "After unfolding the all buffers, all original text should be displayed"
20420 );
20421}
20422
20423#[gpui::test]
20424async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
20425 init_test(cx, |_| {});
20426
20427 let sample_text_1 = "1111\n2222\n3333".to_string();
20428 let sample_text_2 = "4444\n5555\n6666".to_string();
20429 let sample_text_3 = "7777\n8888\n9999".to_string();
20430
20431 let fs = FakeFs::new(cx.executor());
20432 fs.insert_tree(
20433 path!("/a"),
20434 json!({
20435 "first.rs": sample_text_1,
20436 "second.rs": sample_text_2,
20437 "third.rs": sample_text_3,
20438 }),
20439 )
20440 .await;
20441 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20442 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20443 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20444 let worktree = project.update(cx, |project, cx| {
20445 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20446 assert_eq!(worktrees.len(), 1);
20447 worktrees.pop().unwrap()
20448 });
20449 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20450
20451 let buffer_1 = project
20452 .update(cx, |project, cx| {
20453 project.open_buffer((worktree_id, "first.rs"), cx)
20454 })
20455 .await
20456 .unwrap();
20457 let buffer_2 = project
20458 .update(cx, |project, cx| {
20459 project.open_buffer((worktree_id, "second.rs"), cx)
20460 })
20461 .await
20462 .unwrap();
20463 let buffer_3 = project
20464 .update(cx, |project, cx| {
20465 project.open_buffer((worktree_id, "third.rs"), cx)
20466 })
20467 .await
20468 .unwrap();
20469
20470 let multi_buffer = cx.new(|cx| {
20471 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20472 multi_buffer.push_excerpts(
20473 buffer_1.clone(),
20474 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20475 cx,
20476 );
20477 multi_buffer.push_excerpts(
20478 buffer_2.clone(),
20479 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20480 cx,
20481 );
20482 multi_buffer.push_excerpts(
20483 buffer_3.clone(),
20484 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20485 cx,
20486 );
20487 multi_buffer
20488 });
20489
20490 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20491 Editor::new(
20492 EditorMode::full(),
20493 multi_buffer,
20494 Some(project.clone()),
20495 window,
20496 cx,
20497 )
20498 });
20499
20500 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
20501 assert_eq!(
20502 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20503 full_text,
20504 );
20505
20506 multi_buffer_editor.update(cx, |editor, cx| {
20507 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
20508 });
20509 assert_eq!(
20510 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20511 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
20512 "After folding the first buffer, its text should not be displayed"
20513 );
20514
20515 multi_buffer_editor.update(cx, |editor, cx| {
20516 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
20517 });
20518
20519 assert_eq!(
20520 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20521 "\n\n\n\n\n\n7777\n8888\n9999",
20522 "After folding the second buffer, its text should not be displayed"
20523 );
20524
20525 multi_buffer_editor.update(cx, |editor, cx| {
20526 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
20527 });
20528 assert_eq!(
20529 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20530 "\n\n\n\n\n",
20531 "After folding the third buffer, its text should not be displayed"
20532 );
20533
20534 multi_buffer_editor.update(cx, |editor, cx| {
20535 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
20536 });
20537 assert_eq!(
20538 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20539 "\n\n\n\n4444\n5555\n6666\n\n",
20540 "After unfolding the second buffer, its text should be displayed"
20541 );
20542
20543 multi_buffer_editor.update(cx, |editor, cx| {
20544 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
20545 });
20546 assert_eq!(
20547 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20548 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
20549 "After unfolding the first buffer, its text should be displayed"
20550 );
20551
20552 multi_buffer_editor.update(cx, |editor, cx| {
20553 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
20554 });
20555 assert_eq!(
20556 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20557 full_text,
20558 "After unfolding all buffers, all original text should be displayed"
20559 );
20560}
20561
20562#[gpui::test]
20563async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
20564 init_test(cx, |_| {});
20565
20566 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
20567
20568 let fs = FakeFs::new(cx.executor());
20569 fs.insert_tree(
20570 path!("/a"),
20571 json!({
20572 "main.rs": sample_text,
20573 }),
20574 )
20575 .await;
20576 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20577 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20578 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20579 let worktree = project.update(cx, |project, cx| {
20580 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20581 assert_eq!(worktrees.len(), 1);
20582 worktrees.pop().unwrap()
20583 });
20584 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20585
20586 let buffer_1 = project
20587 .update(cx, |project, cx| {
20588 project.open_buffer((worktree_id, "main.rs"), cx)
20589 })
20590 .await
20591 .unwrap();
20592
20593 let multi_buffer = cx.new(|cx| {
20594 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20595 multi_buffer.push_excerpts(
20596 buffer_1.clone(),
20597 [ExcerptRange::new(
20598 Point::new(0, 0)
20599 ..Point::new(
20600 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
20601 0,
20602 ),
20603 )],
20604 cx,
20605 );
20606 multi_buffer
20607 });
20608 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20609 Editor::new(
20610 EditorMode::full(),
20611 multi_buffer,
20612 Some(project.clone()),
20613 window,
20614 cx,
20615 )
20616 });
20617
20618 let selection_range = Point::new(1, 0)..Point::new(2, 0);
20619 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20620 enum TestHighlight {}
20621 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
20622 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
20623 editor.highlight_text::<TestHighlight>(
20624 vec![highlight_range.clone()],
20625 HighlightStyle::color(Hsla::green()),
20626 cx,
20627 );
20628 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20629 s.select_ranges(Some(highlight_range))
20630 });
20631 });
20632
20633 let full_text = format!("\n\n{sample_text}");
20634 assert_eq!(
20635 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20636 full_text,
20637 );
20638}
20639
20640#[gpui::test]
20641async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
20642 init_test(cx, |_| {});
20643 cx.update(|cx| {
20644 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
20645 "keymaps/default-linux.json",
20646 cx,
20647 )
20648 .unwrap();
20649 cx.bind_keys(default_key_bindings);
20650 });
20651
20652 let (editor, cx) = cx.add_window_view(|window, cx| {
20653 let multi_buffer = MultiBuffer::build_multi(
20654 [
20655 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
20656 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
20657 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
20658 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
20659 ],
20660 cx,
20661 );
20662 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
20663
20664 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
20665 // fold all but the second buffer, so that we test navigating between two
20666 // adjacent folded buffers, as well as folded buffers at the start and
20667 // end the multibuffer
20668 editor.fold_buffer(buffer_ids[0], cx);
20669 editor.fold_buffer(buffer_ids[2], cx);
20670 editor.fold_buffer(buffer_ids[3], cx);
20671
20672 editor
20673 });
20674 cx.simulate_resize(size(px(1000.), px(1000.)));
20675
20676 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
20677 cx.assert_excerpts_with_selections(indoc! {"
20678 [EXCERPT]
20679 ˇ[FOLDED]
20680 [EXCERPT]
20681 a1
20682 b1
20683 [EXCERPT]
20684 [FOLDED]
20685 [EXCERPT]
20686 [FOLDED]
20687 "
20688 });
20689 cx.simulate_keystroke("down");
20690 cx.assert_excerpts_with_selections(indoc! {"
20691 [EXCERPT]
20692 [FOLDED]
20693 [EXCERPT]
20694 ˇa1
20695 b1
20696 [EXCERPT]
20697 [FOLDED]
20698 [EXCERPT]
20699 [FOLDED]
20700 "
20701 });
20702 cx.simulate_keystroke("down");
20703 cx.assert_excerpts_with_selections(indoc! {"
20704 [EXCERPT]
20705 [FOLDED]
20706 [EXCERPT]
20707 a1
20708 ˇb1
20709 [EXCERPT]
20710 [FOLDED]
20711 [EXCERPT]
20712 [FOLDED]
20713 "
20714 });
20715 cx.simulate_keystroke("down");
20716 cx.assert_excerpts_with_selections(indoc! {"
20717 [EXCERPT]
20718 [FOLDED]
20719 [EXCERPT]
20720 a1
20721 b1
20722 ˇ[EXCERPT]
20723 [FOLDED]
20724 [EXCERPT]
20725 [FOLDED]
20726 "
20727 });
20728 cx.simulate_keystroke("down");
20729 cx.assert_excerpts_with_selections(indoc! {"
20730 [EXCERPT]
20731 [FOLDED]
20732 [EXCERPT]
20733 a1
20734 b1
20735 [EXCERPT]
20736 ˇ[FOLDED]
20737 [EXCERPT]
20738 [FOLDED]
20739 "
20740 });
20741 for _ in 0..5 {
20742 cx.simulate_keystroke("down");
20743 cx.assert_excerpts_with_selections(indoc! {"
20744 [EXCERPT]
20745 [FOLDED]
20746 [EXCERPT]
20747 a1
20748 b1
20749 [EXCERPT]
20750 [FOLDED]
20751 [EXCERPT]
20752 ˇ[FOLDED]
20753 "
20754 });
20755 }
20756
20757 cx.simulate_keystroke("up");
20758 cx.assert_excerpts_with_selections(indoc! {"
20759 [EXCERPT]
20760 [FOLDED]
20761 [EXCERPT]
20762 a1
20763 b1
20764 [EXCERPT]
20765 ˇ[FOLDED]
20766 [EXCERPT]
20767 [FOLDED]
20768 "
20769 });
20770 cx.simulate_keystroke("up");
20771 cx.assert_excerpts_with_selections(indoc! {"
20772 [EXCERPT]
20773 [FOLDED]
20774 [EXCERPT]
20775 a1
20776 b1
20777 ˇ[EXCERPT]
20778 [FOLDED]
20779 [EXCERPT]
20780 [FOLDED]
20781 "
20782 });
20783 cx.simulate_keystroke("up");
20784 cx.assert_excerpts_with_selections(indoc! {"
20785 [EXCERPT]
20786 [FOLDED]
20787 [EXCERPT]
20788 a1
20789 ˇb1
20790 [EXCERPT]
20791 [FOLDED]
20792 [EXCERPT]
20793 [FOLDED]
20794 "
20795 });
20796 cx.simulate_keystroke("up");
20797 cx.assert_excerpts_with_selections(indoc! {"
20798 [EXCERPT]
20799 [FOLDED]
20800 [EXCERPT]
20801 ˇa1
20802 b1
20803 [EXCERPT]
20804 [FOLDED]
20805 [EXCERPT]
20806 [FOLDED]
20807 "
20808 });
20809 for _ in 0..5 {
20810 cx.simulate_keystroke("up");
20811 cx.assert_excerpts_with_selections(indoc! {"
20812 [EXCERPT]
20813 ˇ[FOLDED]
20814 [EXCERPT]
20815 a1
20816 b1
20817 [EXCERPT]
20818 [FOLDED]
20819 [EXCERPT]
20820 [FOLDED]
20821 "
20822 });
20823 }
20824}
20825
20826#[gpui::test]
20827async fn test_edit_prediction_text(cx: &mut TestAppContext) {
20828 init_test(cx, |_| {});
20829
20830 // Simple insertion
20831 assert_highlighted_edits(
20832 "Hello, world!",
20833 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
20834 true,
20835 cx,
20836 |highlighted_edits, cx| {
20837 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
20838 assert_eq!(highlighted_edits.highlights.len(), 1);
20839 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
20840 assert_eq!(
20841 highlighted_edits.highlights[0].1.background_color,
20842 Some(cx.theme().status().created_background)
20843 );
20844 },
20845 )
20846 .await;
20847
20848 // Replacement
20849 assert_highlighted_edits(
20850 "This is a test.",
20851 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
20852 false,
20853 cx,
20854 |highlighted_edits, cx| {
20855 assert_eq!(highlighted_edits.text, "That is a test.");
20856 assert_eq!(highlighted_edits.highlights.len(), 1);
20857 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
20858 assert_eq!(
20859 highlighted_edits.highlights[0].1.background_color,
20860 Some(cx.theme().status().created_background)
20861 );
20862 },
20863 )
20864 .await;
20865
20866 // Multiple edits
20867 assert_highlighted_edits(
20868 "Hello, world!",
20869 vec![
20870 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
20871 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
20872 ],
20873 false,
20874 cx,
20875 |highlighted_edits, cx| {
20876 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
20877 assert_eq!(highlighted_edits.highlights.len(), 2);
20878 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
20879 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
20880 assert_eq!(
20881 highlighted_edits.highlights[0].1.background_color,
20882 Some(cx.theme().status().created_background)
20883 );
20884 assert_eq!(
20885 highlighted_edits.highlights[1].1.background_color,
20886 Some(cx.theme().status().created_background)
20887 );
20888 },
20889 )
20890 .await;
20891
20892 // Multiple lines with edits
20893 assert_highlighted_edits(
20894 "First line\nSecond line\nThird line\nFourth line",
20895 vec![
20896 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
20897 (
20898 Point::new(2, 0)..Point::new(2, 10),
20899 "New third line".to_string(),
20900 ),
20901 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
20902 ],
20903 false,
20904 cx,
20905 |highlighted_edits, cx| {
20906 assert_eq!(
20907 highlighted_edits.text,
20908 "Second modified\nNew third line\nFourth updated line"
20909 );
20910 assert_eq!(highlighted_edits.highlights.len(), 3);
20911 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
20912 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
20913 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
20914 for highlight in &highlighted_edits.highlights {
20915 assert_eq!(
20916 highlight.1.background_color,
20917 Some(cx.theme().status().created_background)
20918 );
20919 }
20920 },
20921 )
20922 .await;
20923}
20924
20925#[gpui::test]
20926async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
20927 init_test(cx, |_| {});
20928
20929 // Deletion
20930 assert_highlighted_edits(
20931 "Hello, world!",
20932 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
20933 true,
20934 cx,
20935 |highlighted_edits, cx| {
20936 assert_eq!(highlighted_edits.text, "Hello, world!");
20937 assert_eq!(highlighted_edits.highlights.len(), 1);
20938 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
20939 assert_eq!(
20940 highlighted_edits.highlights[0].1.background_color,
20941 Some(cx.theme().status().deleted_background)
20942 );
20943 },
20944 )
20945 .await;
20946
20947 // Insertion
20948 assert_highlighted_edits(
20949 "Hello, world!",
20950 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
20951 true,
20952 cx,
20953 |highlighted_edits, cx| {
20954 assert_eq!(highlighted_edits.highlights.len(), 1);
20955 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
20956 assert_eq!(
20957 highlighted_edits.highlights[0].1.background_color,
20958 Some(cx.theme().status().created_background)
20959 );
20960 },
20961 )
20962 .await;
20963}
20964
20965async fn assert_highlighted_edits(
20966 text: &str,
20967 edits: Vec<(Range<Point>, String)>,
20968 include_deletions: bool,
20969 cx: &mut TestAppContext,
20970 assertion_fn: impl Fn(HighlightedText, &App),
20971) {
20972 let window = cx.add_window(|window, cx| {
20973 let buffer = MultiBuffer::build_simple(text, cx);
20974 Editor::new(EditorMode::full(), buffer, None, window, cx)
20975 });
20976 let cx = &mut VisualTestContext::from_window(*window, cx);
20977
20978 let (buffer, snapshot) = window
20979 .update(cx, |editor, _window, cx| {
20980 (
20981 editor.buffer().clone(),
20982 editor.buffer().read(cx).snapshot(cx),
20983 )
20984 })
20985 .unwrap();
20986
20987 let edits = edits
20988 .into_iter()
20989 .map(|(range, edit)| {
20990 (
20991 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
20992 edit,
20993 )
20994 })
20995 .collect::<Vec<_>>();
20996
20997 let text_anchor_edits = edits
20998 .clone()
20999 .into_iter()
21000 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
21001 .collect::<Vec<_>>();
21002
21003 let edit_preview = window
21004 .update(cx, |_, _window, cx| {
21005 buffer
21006 .read(cx)
21007 .as_singleton()
21008 .unwrap()
21009 .read(cx)
21010 .preview_edits(text_anchor_edits.into(), cx)
21011 })
21012 .unwrap()
21013 .await;
21014
21015 cx.update(|_window, cx| {
21016 let highlighted_edits = edit_prediction_edit_text(
21017 snapshot.as_singleton().unwrap().2,
21018 &edits,
21019 &edit_preview,
21020 include_deletions,
21021 cx,
21022 );
21023 assertion_fn(highlighted_edits, cx)
21024 });
21025}
21026
21027#[track_caller]
21028fn assert_breakpoint(
21029 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
21030 path: &Arc<Path>,
21031 expected: Vec<(u32, Breakpoint)>,
21032) {
21033 if expected.is_empty() {
21034 assert!(!breakpoints.contains_key(path), "{}", path.display());
21035 } else {
21036 let mut breakpoint = breakpoints
21037 .get(path)
21038 .unwrap()
21039 .iter()
21040 .map(|breakpoint| {
21041 (
21042 breakpoint.row,
21043 Breakpoint {
21044 message: breakpoint.message.clone(),
21045 state: breakpoint.state,
21046 condition: breakpoint.condition.clone(),
21047 hit_condition: breakpoint.hit_condition.clone(),
21048 },
21049 )
21050 })
21051 .collect::<Vec<_>>();
21052
21053 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
21054
21055 assert_eq!(expected, breakpoint);
21056 }
21057}
21058
21059fn add_log_breakpoint_at_cursor(
21060 editor: &mut Editor,
21061 log_message: &str,
21062 window: &mut Window,
21063 cx: &mut Context<Editor>,
21064) {
21065 let (anchor, bp) = editor
21066 .breakpoints_at_cursors(window, cx)
21067 .first()
21068 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
21069 .unwrap_or_else(|| {
21070 let cursor_position: Point = editor.selections.newest(cx).head();
21071
21072 let breakpoint_position = editor
21073 .snapshot(window, cx)
21074 .display_snapshot
21075 .buffer_snapshot
21076 .anchor_before(Point::new(cursor_position.row, 0));
21077
21078 (breakpoint_position, Breakpoint::new_log(log_message))
21079 });
21080
21081 editor.edit_breakpoint_at_anchor(
21082 anchor,
21083 bp,
21084 BreakpointEditAction::EditLogMessage(log_message.into()),
21085 cx,
21086 );
21087}
21088
21089#[gpui::test]
21090async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
21091 init_test(cx, |_| {});
21092
21093 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
21094 let fs = FakeFs::new(cx.executor());
21095 fs.insert_tree(
21096 path!("/a"),
21097 json!({
21098 "main.rs": sample_text,
21099 }),
21100 )
21101 .await;
21102 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21103 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21104 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21105
21106 let fs = FakeFs::new(cx.executor());
21107 fs.insert_tree(
21108 path!("/a"),
21109 json!({
21110 "main.rs": sample_text,
21111 }),
21112 )
21113 .await;
21114 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21115 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21116 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21117 let worktree_id = workspace
21118 .update(cx, |workspace, _window, cx| {
21119 workspace.project().update(cx, |project, cx| {
21120 project.worktrees(cx).next().unwrap().read(cx).id()
21121 })
21122 })
21123 .unwrap();
21124
21125 let buffer = project
21126 .update(cx, |project, cx| {
21127 project.open_buffer((worktree_id, "main.rs"), cx)
21128 })
21129 .await
21130 .unwrap();
21131
21132 let (editor, cx) = cx.add_window_view(|window, cx| {
21133 Editor::new(
21134 EditorMode::full(),
21135 MultiBuffer::build_from_buffer(buffer, cx),
21136 Some(project.clone()),
21137 window,
21138 cx,
21139 )
21140 });
21141
21142 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
21143 let abs_path = project.read_with(cx, |project, cx| {
21144 project
21145 .absolute_path(&project_path, cx)
21146 .map(Arc::from)
21147 .unwrap()
21148 });
21149
21150 // assert we can add breakpoint on the first line
21151 editor.update_in(cx, |editor, window, cx| {
21152 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21153 editor.move_to_end(&MoveToEnd, window, cx);
21154 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21155 });
21156
21157 let breakpoints = editor.update(cx, |editor, cx| {
21158 editor
21159 .breakpoint_store()
21160 .as_ref()
21161 .unwrap()
21162 .read(cx)
21163 .all_source_breakpoints(cx)
21164 });
21165
21166 assert_eq!(1, breakpoints.len());
21167 assert_breakpoint(
21168 &breakpoints,
21169 &abs_path,
21170 vec![
21171 (0, Breakpoint::new_standard()),
21172 (3, Breakpoint::new_standard()),
21173 ],
21174 );
21175
21176 editor.update_in(cx, |editor, window, cx| {
21177 editor.move_to_beginning(&MoveToBeginning, window, cx);
21178 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21179 });
21180
21181 let breakpoints = editor.update(cx, |editor, cx| {
21182 editor
21183 .breakpoint_store()
21184 .as_ref()
21185 .unwrap()
21186 .read(cx)
21187 .all_source_breakpoints(cx)
21188 });
21189
21190 assert_eq!(1, breakpoints.len());
21191 assert_breakpoint(
21192 &breakpoints,
21193 &abs_path,
21194 vec![(3, Breakpoint::new_standard())],
21195 );
21196
21197 editor.update_in(cx, |editor, window, cx| {
21198 editor.move_to_end(&MoveToEnd, window, cx);
21199 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21200 });
21201
21202 let breakpoints = editor.update(cx, |editor, cx| {
21203 editor
21204 .breakpoint_store()
21205 .as_ref()
21206 .unwrap()
21207 .read(cx)
21208 .all_source_breakpoints(cx)
21209 });
21210
21211 assert_eq!(0, breakpoints.len());
21212 assert_breakpoint(&breakpoints, &abs_path, vec![]);
21213}
21214
21215#[gpui::test]
21216async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
21217 init_test(cx, |_| {});
21218
21219 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
21220
21221 let fs = FakeFs::new(cx.executor());
21222 fs.insert_tree(
21223 path!("/a"),
21224 json!({
21225 "main.rs": sample_text,
21226 }),
21227 )
21228 .await;
21229 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21230 let (workspace, cx) =
21231 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21232
21233 let worktree_id = workspace.update(cx, |workspace, cx| {
21234 workspace.project().update(cx, |project, cx| {
21235 project.worktrees(cx).next().unwrap().read(cx).id()
21236 })
21237 });
21238
21239 let buffer = project
21240 .update(cx, |project, cx| {
21241 project.open_buffer((worktree_id, "main.rs"), cx)
21242 })
21243 .await
21244 .unwrap();
21245
21246 let (editor, cx) = cx.add_window_view(|window, cx| {
21247 Editor::new(
21248 EditorMode::full(),
21249 MultiBuffer::build_from_buffer(buffer, cx),
21250 Some(project.clone()),
21251 window,
21252 cx,
21253 )
21254 });
21255
21256 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
21257 let abs_path = project.read_with(cx, |project, cx| {
21258 project
21259 .absolute_path(&project_path, cx)
21260 .map(Arc::from)
21261 .unwrap()
21262 });
21263
21264 editor.update_in(cx, |editor, window, cx| {
21265 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
21266 });
21267
21268 let breakpoints = editor.update(cx, |editor, cx| {
21269 editor
21270 .breakpoint_store()
21271 .as_ref()
21272 .unwrap()
21273 .read(cx)
21274 .all_source_breakpoints(cx)
21275 });
21276
21277 assert_breakpoint(
21278 &breakpoints,
21279 &abs_path,
21280 vec![(0, Breakpoint::new_log("hello world"))],
21281 );
21282
21283 // Removing a log message from a log breakpoint should remove it
21284 editor.update_in(cx, |editor, window, cx| {
21285 add_log_breakpoint_at_cursor(editor, "", window, cx);
21286 });
21287
21288 let breakpoints = editor.update(cx, |editor, cx| {
21289 editor
21290 .breakpoint_store()
21291 .as_ref()
21292 .unwrap()
21293 .read(cx)
21294 .all_source_breakpoints(cx)
21295 });
21296
21297 assert_breakpoint(&breakpoints, &abs_path, vec![]);
21298
21299 editor.update_in(cx, |editor, window, cx| {
21300 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21301 editor.move_to_end(&MoveToEnd, window, cx);
21302 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21303 // Not adding a log message to a standard breakpoint shouldn't remove it
21304 add_log_breakpoint_at_cursor(editor, "", window, cx);
21305 });
21306
21307 let breakpoints = editor.update(cx, |editor, cx| {
21308 editor
21309 .breakpoint_store()
21310 .as_ref()
21311 .unwrap()
21312 .read(cx)
21313 .all_source_breakpoints(cx)
21314 });
21315
21316 assert_breakpoint(
21317 &breakpoints,
21318 &abs_path,
21319 vec![
21320 (0, Breakpoint::new_standard()),
21321 (3, Breakpoint::new_standard()),
21322 ],
21323 );
21324
21325 editor.update_in(cx, |editor, window, cx| {
21326 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
21327 });
21328
21329 let breakpoints = editor.update(cx, |editor, cx| {
21330 editor
21331 .breakpoint_store()
21332 .as_ref()
21333 .unwrap()
21334 .read(cx)
21335 .all_source_breakpoints(cx)
21336 });
21337
21338 assert_breakpoint(
21339 &breakpoints,
21340 &abs_path,
21341 vec![
21342 (0, Breakpoint::new_standard()),
21343 (3, Breakpoint::new_log("hello world")),
21344 ],
21345 );
21346
21347 editor.update_in(cx, |editor, window, cx| {
21348 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
21349 });
21350
21351 let breakpoints = editor.update(cx, |editor, cx| {
21352 editor
21353 .breakpoint_store()
21354 .as_ref()
21355 .unwrap()
21356 .read(cx)
21357 .all_source_breakpoints(cx)
21358 });
21359
21360 assert_breakpoint(
21361 &breakpoints,
21362 &abs_path,
21363 vec![
21364 (0, Breakpoint::new_standard()),
21365 (3, Breakpoint::new_log("hello Earth!!")),
21366 ],
21367 );
21368}
21369
21370/// This also tests that Editor::breakpoint_at_cursor_head is working properly
21371/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
21372/// or when breakpoints were placed out of order. This tests for a regression too
21373#[gpui::test]
21374async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
21375 init_test(cx, |_| {});
21376
21377 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
21378 let fs = FakeFs::new(cx.executor());
21379 fs.insert_tree(
21380 path!("/a"),
21381 json!({
21382 "main.rs": sample_text,
21383 }),
21384 )
21385 .await;
21386 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21387 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21388 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21389
21390 let fs = FakeFs::new(cx.executor());
21391 fs.insert_tree(
21392 path!("/a"),
21393 json!({
21394 "main.rs": sample_text,
21395 }),
21396 )
21397 .await;
21398 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21399 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21400 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21401 let worktree_id = workspace
21402 .update(cx, |workspace, _window, cx| {
21403 workspace.project().update(cx, |project, cx| {
21404 project.worktrees(cx).next().unwrap().read(cx).id()
21405 })
21406 })
21407 .unwrap();
21408
21409 let buffer = project
21410 .update(cx, |project, cx| {
21411 project.open_buffer((worktree_id, "main.rs"), cx)
21412 })
21413 .await
21414 .unwrap();
21415
21416 let (editor, cx) = cx.add_window_view(|window, cx| {
21417 Editor::new(
21418 EditorMode::full(),
21419 MultiBuffer::build_from_buffer(buffer, cx),
21420 Some(project.clone()),
21421 window,
21422 cx,
21423 )
21424 });
21425
21426 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
21427 let abs_path = project.read_with(cx, |project, cx| {
21428 project
21429 .absolute_path(&project_path, cx)
21430 .map(Arc::from)
21431 .unwrap()
21432 });
21433
21434 // assert we can add breakpoint on the first line
21435 editor.update_in(cx, |editor, window, cx| {
21436 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21437 editor.move_to_end(&MoveToEnd, window, cx);
21438 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21439 editor.move_up(&MoveUp, window, cx);
21440 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21441 });
21442
21443 let breakpoints = editor.update(cx, |editor, cx| {
21444 editor
21445 .breakpoint_store()
21446 .as_ref()
21447 .unwrap()
21448 .read(cx)
21449 .all_source_breakpoints(cx)
21450 });
21451
21452 assert_eq!(1, breakpoints.len());
21453 assert_breakpoint(
21454 &breakpoints,
21455 &abs_path,
21456 vec![
21457 (0, Breakpoint::new_standard()),
21458 (2, Breakpoint::new_standard()),
21459 (3, Breakpoint::new_standard()),
21460 ],
21461 );
21462
21463 editor.update_in(cx, |editor, window, cx| {
21464 editor.move_to_beginning(&MoveToBeginning, window, cx);
21465 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21466 editor.move_to_end(&MoveToEnd, window, cx);
21467 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21468 // Disabling a breakpoint that doesn't exist should do nothing
21469 editor.move_up(&MoveUp, window, cx);
21470 editor.move_up(&MoveUp, window, cx);
21471 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21472 });
21473
21474 let breakpoints = editor.update(cx, |editor, cx| {
21475 editor
21476 .breakpoint_store()
21477 .as_ref()
21478 .unwrap()
21479 .read(cx)
21480 .all_source_breakpoints(cx)
21481 });
21482
21483 let disable_breakpoint = {
21484 let mut bp = Breakpoint::new_standard();
21485 bp.state = BreakpointState::Disabled;
21486 bp
21487 };
21488
21489 assert_eq!(1, breakpoints.len());
21490 assert_breakpoint(
21491 &breakpoints,
21492 &abs_path,
21493 vec![
21494 (0, disable_breakpoint.clone()),
21495 (2, Breakpoint::new_standard()),
21496 (3, disable_breakpoint.clone()),
21497 ],
21498 );
21499
21500 editor.update_in(cx, |editor, window, cx| {
21501 editor.move_to_beginning(&MoveToBeginning, window, cx);
21502 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
21503 editor.move_to_end(&MoveToEnd, window, cx);
21504 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
21505 editor.move_up(&MoveUp, window, cx);
21506 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21507 });
21508
21509 let breakpoints = editor.update(cx, |editor, cx| {
21510 editor
21511 .breakpoint_store()
21512 .as_ref()
21513 .unwrap()
21514 .read(cx)
21515 .all_source_breakpoints(cx)
21516 });
21517
21518 assert_eq!(1, breakpoints.len());
21519 assert_breakpoint(
21520 &breakpoints,
21521 &abs_path,
21522 vec![
21523 (0, Breakpoint::new_standard()),
21524 (2, disable_breakpoint),
21525 (3, Breakpoint::new_standard()),
21526 ],
21527 );
21528}
21529
21530#[gpui::test]
21531async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
21532 init_test(cx, |_| {});
21533 let capabilities = lsp::ServerCapabilities {
21534 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
21535 prepare_provider: Some(true),
21536 work_done_progress_options: Default::default(),
21537 })),
21538 ..Default::default()
21539 };
21540 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21541
21542 cx.set_state(indoc! {"
21543 struct Fˇoo {}
21544 "});
21545
21546 cx.update_editor(|editor, _, cx| {
21547 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21548 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21549 editor.highlight_background::<DocumentHighlightRead>(
21550 &[highlight_range],
21551 |theme| theme.colors().editor_document_highlight_read_background,
21552 cx,
21553 );
21554 });
21555
21556 let mut prepare_rename_handler = cx
21557 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
21558 move |_, _, _| async move {
21559 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
21560 start: lsp::Position {
21561 line: 0,
21562 character: 7,
21563 },
21564 end: lsp::Position {
21565 line: 0,
21566 character: 10,
21567 },
21568 })))
21569 },
21570 );
21571 let prepare_rename_task = cx
21572 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21573 .expect("Prepare rename was not started");
21574 prepare_rename_handler.next().await.unwrap();
21575 prepare_rename_task.await.expect("Prepare rename failed");
21576
21577 let mut rename_handler =
21578 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21579 let edit = lsp::TextEdit {
21580 range: lsp::Range {
21581 start: lsp::Position {
21582 line: 0,
21583 character: 7,
21584 },
21585 end: lsp::Position {
21586 line: 0,
21587 character: 10,
21588 },
21589 },
21590 new_text: "FooRenamed".to_string(),
21591 };
21592 Ok(Some(lsp::WorkspaceEdit::new(
21593 // Specify the same edit twice
21594 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
21595 )))
21596 });
21597 let rename_task = cx
21598 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21599 .expect("Confirm rename was not started");
21600 rename_handler.next().await.unwrap();
21601 rename_task.await.expect("Confirm rename failed");
21602 cx.run_until_parked();
21603
21604 // Despite two edits, only one is actually applied as those are identical
21605 cx.assert_editor_state(indoc! {"
21606 struct FooRenamedˇ {}
21607 "});
21608}
21609
21610#[gpui::test]
21611async fn test_rename_without_prepare(cx: &mut TestAppContext) {
21612 init_test(cx, |_| {});
21613 // These capabilities indicate that the server does not support prepare rename.
21614 let capabilities = lsp::ServerCapabilities {
21615 rename_provider: Some(lsp::OneOf::Left(true)),
21616 ..Default::default()
21617 };
21618 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21619
21620 cx.set_state(indoc! {"
21621 struct Fˇoo {}
21622 "});
21623
21624 cx.update_editor(|editor, _window, cx| {
21625 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21626 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21627 editor.highlight_background::<DocumentHighlightRead>(
21628 &[highlight_range],
21629 |theme| theme.colors().editor_document_highlight_read_background,
21630 cx,
21631 );
21632 });
21633
21634 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21635 .expect("Prepare rename was not started")
21636 .await
21637 .expect("Prepare rename failed");
21638
21639 let mut rename_handler =
21640 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21641 let edit = lsp::TextEdit {
21642 range: lsp::Range {
21643 start: lsp::Position {
21644 line: 0,
21645 character: 7,
21646 },
21647 end: lsp::Position {
21648 line: 0,
21649 character: 10,
21650 },
21651 },
21652 new_text: "FooRenamed".to_string(),
21653 };
21654 Ok(Some(lsp::WorkspaceEdit::new(
21655 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
21656 )))
21657 });
21658 let rename_task = cx
21659 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21660 .expect("Confirm rename was not started");
21661 rename_handler.next().await.unwrap();
21662 rename_task.await.expect("Confirm rename failed");
21663 cx.run_until_parked();
21664
21665 // Correct range is renamed, as `surrounding_word` is used to find it.
21666 cx.assert_editor_state(indoc! {"
21667 struct FooRenamedˇ {}
21668 "});
21669}
21670
21671#[gpui::test]
21672async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
21673 init_test(cx, |_| {});
21674 let mut cx = EditorTestContext::new(cx).await;
21675
21676 let language = Arc::new(
21677 Language::new(
21678 LanguageConfig::default(),
21679 Some(tree_sitter_html::LANGUAGE.into()),
21680 )
21681 .with_brackets_query(
21682 r#"
21683 ("<" @open "/>" @close)
21684 ("</" @open ">" @close)
21685 ("<" @open ">" @close)
21686 ("\"" @open "\"" @close)
21687 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
21688 "#,
21689 )
21690 .unwrap(),
21691 );
21692 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21693
21694 cx.set_state(indoc! {"
21695 <span>ˇ</span>
21696 "});
21697 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21698 cx.assert_editor_state(indoc! {"
21699 <span>
21700 ˇ
21701 </span>
21702 "});
21703
21704 cx.set_state(indoc! {"
21705 <span><span></span>ˇ</span>
21706 "});
21707 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21708 cx.assert_editor_state(indoc! {"
21709 <span><span></span>
21710 ˇ</span>
21711 "});
21712
21713 cx.set_state(indoc! {"
21714 <span>ˇ
21715 </span>
21716 "});
21717 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21718 cx.assert_editor_state(indoc! {"
21719 <span>
21720 ˇ
21721 </span>
21722 "});
21723}
21724
21725#[gpui::test(iterations = 10)]
21726async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
21727 init_test(cx, |_| {});
21728
21729 let fs = FakeFs::new(cx.executor());
21730 fs.insert_tree(
21731 path!("/dir"),
21732 json!({
21733 "a.ts": "a",
21734 }),
21735 )
21736 .await;
21737
21738 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
21739 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21740 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21741
21742 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21743 language_registry.add(Arc::new(Language::new(
21744 LanguageConfig {
21745 name: "TypeScript".into(),
21746 matcher: LanguageMatcher {
21747 path_suffixes: vec!["ts".to_string()],
21748 ..Default::default()
21749 },
21750 ..Default::default()
21751 },
21752 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
21753 )));
21754 let mut fake_language_servers = language_registry.register_fake_lsp(
21755 "TypeScript",
21756 FakeLspAdapter {
21757 capabilities: lsp::ServerCapabilities {
21758 code_lens_provider: Some(lsp::CodeLensOptions {
21759 resolve_provider: Some(true),
21760 }),
21761 execute_command_provider: Some(lsp::ExecuteCommandOptions {
21762 commands: vec!["_the/command".to_string()],
21763 ..lsp::ExecuteCommandOptions::default()
21764 }),
21765 ..lsp::ServerCapabilities::default()
21766 },
21767 ..FakeLspAdapter::default()
21768 },
21769 );
21770
21771 let editor = workspace
21772 .update(cx, |workspace, window, cx| {
21773 workspace.open_abs_path(
21774 PathBuf::from(path!("/dir/a.ts")),
21775 OpenOptions::default(),
21776 window,
21777 cx,
21778 )
21779 })
21780 .unwrap()
21781 .await
21782 .unwrap()
21783 .downcast::<Editor>()
21784 .unwrap();
21785 cx.executor().run_until_parked();
21786
21787 let fake_server = fake_language_servers.next().await.unwrap();
21788
21789 let buffer = editor.update(cx, |editor, cx| {
21790 editor
21791 .buffer()
21792 .read(cx)
21793 .as_singleton()
21794 .expect("have opened a single file by path")
21795 });
21796
21797 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
21798 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
21799 drop(buffer_snapshot);
21800 let actions = cx
21801 .update_window(*workspace, |_, window, cx| {
21802 project.code_actions(&buffer, anchor..anchor, window, cx)
21803 })
21804 .unwrap();
21805
21806 fake_server
21807 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21808 Ok(Some(vec![
21809 lsp::CodeLens {
21810 range: lsp::Range::default(),
21811 command: Some(lsp::Command {
21812 title: "Code lens command".to_owned(),
21813 command: "_the/command".to_owned(),
21814 arguments: None,
21815 }),
21816 data: None,
21817 },
21818 lsp::CodeLens {
21819 range: lsp::Range::default(),
21820 command: Some(lsp::Command {
21821 title: "Command not in capabilities".to_owned(),
21822 command: "not in capabilities".to_owned(),
21823 arguments: None,
21824 }),
21825 data: None,
21826 },
21827 lsp::CodeLens {
21828 range: lsp::Range {
21829 start: lsp::Position {
21830 line: 1,
21831 character: 1,
21832 },
21833 end: lsp::Position {
21834 line: 1,
21835 character: 1,
21836 },
21837 },
21838 command: Some(lsp::Command {
21839 title: "Command not in range".to_owned(),
21840 command: "_the/command".to_owned(),
21841 arguments: None,
21842 }),
21843 data: None,
21844 },
21845 ]))
21846 })
21847 .next()
21848 .await;
21849
21850 let actions = actions.await.unwrap();
21851 assert_eq!(
21852 actions.len(),
21853 1,
21854 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
21855 );
21856 let action = actions[0].clone();
21857 let apply = project.update(cx, |project, cx| {
21858 project.apply_code_action(buffer.clone(), action, true, cx)
21859 });
21860
21861 // Resolving the code action does not populate its edits. In absence of
21862 // edits, we must execute the given command.
21863 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
21864 |mut lens, _| async move {
21865 let lens_command = lens.command.as_mut().expect("should have a command");
21866 assert_eq!(lens_command.title, "Code lens command");
21867 lens_command.arguments = Some(vec![json!("the-argument")]);
21868 Ok(lens)
21869 },
21870 );
21871
21872 // While executing the command, the language server sends the editor
21873 // a `workspaceEdit` request.
21874 fake_server
21875 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
21876 let fake = fake_server.clone();
21877 move |params, _| {
21878 assert_eq!(params.command, "_the/command");
21879 let fake = fake.clone();
21880 async move {
21881 fake.server
21882 .request::<lsp::request::ApplyWorkspaceEdit>(
21883 lsp::ApplyWorkspaceEditParams {
21884 label: None,
21885 edit: lsp::WorkspaceEdit {
21886 changes: Some(
21887 [(
21888 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
21889 vec![lsp::TextEdit {
21890 range: lsp::Range::new(
21891 lsp::Position::new(0, 0),
21892 lsp::Position::new(0, 0),
21893 ),
21894 new_text: "X".into(),
21895 }],
21896 )]
21897 .into_iter()
21898 .collect(),
21899 ),
21900 ..lsp::WorkspaceEdit::default()
21901 },
21902 },
21903 )
21904 .await
21905 .into_response()
21906 .unwrap();
21907 Ok(Some(json!(null)))
21908 }
21909 }
21910 })
21911 .next()
21912 .await;
21913
21914 // Applying the code lens command returns a project transaction containing the edits
21915 // sent by the language server in its `workspaceEdit` request.
21916 let transaction = apply.await.unwrap();
21917 assert!(transaction.0.contains_key(&buffer));
21918 buffer.update(cx, |buffer, cx| {
21919 assert_eq!(buffer.text(), "Xa");
21920 buffer.undo(cx);
21921 assert_eq!(buffer.text(), "a");
21922 });
21923
21924 let actions_after_edits = cx
21925 .update_window(*workspace, |_, window, cx| {
21926 project.code_actions(&buffer, anchor..anchor, window, cx)
21927 })
21928 .unwrap()
21929 .await
21930 .unwrap();
21931 assert_eq!(
21932 actions, actions_after_edits,
21933 "For the same selection, same code lens actions should be returned"
21934 );
21935
21936 let _responses =
21937 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21938 panic!("No more code lens requests are expected");
21939 });
21940 editor.update_in(cx, |editor, window, cx| {
21941 editor.select_all(&SelectAll, window, cx);
21942 });
21943 cx.executor().run_until_parked();
21944 let new_actions = cx
21945 .update_window(*workspace, |_, window, cx| {
21946 project.code_actions(&buffer, anchor..anchor, window, cx)
21947 })
21948 .unwrap()
21949 .await
21950 .unwrap();
21951 assert_eq!(
21952 actions, new_actions,
21953 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
21954 );
21955}
21956
21957#[gpui::test]
21958async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
21959 init_test(cx, |_| {});
21960
21961 let fs = FakeFs::new(cx.executor());
21962 let main_text = r#"fn main() {
21963println!("1");
21964println!("2");
21965println!("3");
21966println!("4");
21967println!("5");
21968}"#;
21969 let lib_text = "mod foo {}";
21970 fs.insert_tree(
21971 path!("/a"),
21972 json!({
21973 "lib.rs": lib_text,
21974 "main.rs": main_text,
21975 }),
21976 )
21977 .await;
21978
21979 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21980 let (workspace, cx) =
21981 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21982 let worktree_id = workspace.update(cx, |workspace, cx| {
21983 workspace.project().update(cx, |project, cx| {
21984 project.worktrees(cx).next().unwrap().read(cx).id()
21985 })
21986 });
21987
21988 let expected_ranges = vec![
21989 Point::new(0, 0)..Point::new(0, 0),
21990 Point::new(1, 0)..Point::new(1, 1),
21991 Point::new(2, 0)..Point::new(2, 2),
21992 Point::new(3, 0)..Point::new(3, 3),
21993 ];
21994
21995 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21996 let editor_1 = workspace
21997 .update_in(cx, |workspace, window, cx| {
21998 workspace.open_path(
21999 (worktree_id, "main.rs"),
22000 Some(pane_1.downgrade()),
22001 true,
22002 window,
22003 cx,
22004 )
22005 })
22006 .unwrap()
22007 .await
22008 .downcast::<Editor>()
22009 .unwrap();
22010 pane_1.update(cx, |pane, cx| {
22011 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22012 open_editor.update(cx, |editor, cx| {
22013 assert_eq!(
22014 editor.display_text(cx),
22015 main_text,
22016 "Original main.rs text on initial open",
22017 );
22018 assert_eq!(
22019 editor
22020 .selections
22021 .all::<Point>(cx)
22022 .into_iter()
22023 .map(|s| s.range())
22024 .collect::<Vec<_>>(),
22025 vec![Point::zero()..Point::zero()],
22026 "Default selections on initial open",
22027 );
22028 })
22029 });
22030 editor_1.update_in(cx, |editor, window, cx| {
22031 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22032 s.select_ranges(expected_ranges.clone());
22033 });
22034 });
22035
22036 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
22037 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
22038 });
22039 let editor_2 = workspace
22040 .update_in(cx, |workspace, window, cx| {
22041 workspace.open_path(
22042 (worktree_id, "main.rs"),
22043 Some(pane_2.downgrade()),
22044 true,
22045 window,
22046 cx,
22047 )
22048 })
22049 .unwrap()
22050 .await
22051 .downcast::<Editor>()
22052 .unwrap();
22053 pane_2.update(cx, |pane, cx| {
22054 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22055 open_editor.update(cx, |editor, cx| {
22056 assert_eq!(
22057 editor.display_text(cx),
22058 main_text,
22059 "Original main.rs text on initial open in another panel",
22060 );
22061 assert_eq!(
22062 editor
22063 .selections
22064 .all::<Point>(cx)
22065 .into_iter()
22066 .map(|s| s.range())
22067 .collect::<Vec<_>>(),
22068 vec![Point::zero()..Point::zero()],
22069 "Default selections on initial open in another panel",
22070 );
22071 })
22072 });
22073
22074 editor_2.update_in(cx, |editor, window, cx| {
22075 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
22076 });
22077
22078 let _other_editor_1 = workspace
22079 .update_in(cx, |workspace, window, cx| {
22080 workspace.open_path(
22081 (worktree_id, "lib.rs"),
22082 Some(pane_1.downgrade()),
22083 true,
22084 window,
22085 cx,
22086 )
22087 })
22088 .unwrap()
22089 .await
22090 .downcast::<Editor>()
22091 .unwrap();
22092 pane_1
22093 .update_in(cx, |pane, window, cx| {
22094 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
22095 })
22096 .await
22097 .unwrap();
22098 drop(editor_1);
22099 pane_1.update(cx, |pane, cx| {
22100 pane.active_item()
22101 .unwrap()
22102 .downcast::<Editor>()
22103 .unwrap()
22104 .update(cx, |editor, cx| {
22105 assert_eq!(
22106 editor.display_text(cx),
22107 lib_text,
22108 "Other file should be open and active",
22109 );
22110 });
22111 assert_eq!(pane.items().count(), 1, "No other editors should be open");
22112 });
22113
22114 let _other_editor_2 = workspace
22115 .update_in(cx, |workspace, window, cx| {
22116 workspace.open_path(
22117 (worktree_id, "lib.rs"),
22118 Some(pane_2.downgrade()),
22119 true,
22120 window,
22121 cx,
22122 )
22123 })
22124 .unwrap()
22125 .await
22126 .downcast::<Editor>()
22127 .unwrap();
22128 pane_2
22129 .update_in(cx, |pane, window, cx| {
22130 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
22131 })
22132 .await
22133 .unwrap();
22134 drop(editor_2);
22135 pane_2.update(cx, |pane, cx| {
22136 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22137 open_editor.update(cx, |editor, cx| {
22138 assert_eq!(
22139 editor.display_text(cx),
22140 lib_text,
22141 "Other file should be open and active in another panel too",
22142 );
22143 });
22144 assert_eq!(
22145 pane.items().count(),
22146 1,
22147 "No other editors should be open in another pane",
22148 );
22149 });
22150
22151 let _editor_1_reopened = workspace
22152 .update_in(cx, |workspace, window, cx| {
22153 workspace.open_path(
22154 (worktree_id, "main.rs"),
22155 Some(pane_1.downgrade()),
22156 true,
22157 window,
22158 cx,
22159 )
22160 })
22161 .unwrap()
22162 .await
22163 .downcast::<Editor>()
22164 .unwrap();
22165 let _editor_2_reopened = workspace
22166 .update_in(cx, |workspace, window, cx| {
22167 workspace.open_path(
22168 (worktree_id, "main.rs"),
22169 Some(pane_2.downgrade()),
22170 true,
22171 window,
22172 cx,
22173 )
22174 })
22175 .unwrap()
22176 .await
22177 .downcast::<Editor>()
22178 .unwrap();
22179 pane_1.update(cx, |pane, cx| {
22180 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22181 open_editor.update(cx, |editor, cx| {
22182 assert_eq!(
22183 editor.display_text(cx),
22184 main_text,
22185 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
22186 );
22187 assert_eq!(
22188 editor
22189 .selections
22190 .all::<Point>(cx)
22191 .into_iter()
22192 .map(|s| s.range())
22193 .collect::<Vec<_>>(),
22194 expected_ranges,
22195 "Previous editor in the 1st panel had selections and should get them restored on reopen",
22196 );
22197 })
22198 });
22199 pane_2.update(cx, |pane, cx| {
22200 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22201 open_editor.update(cx, |editor, cx| {
22202 assert_eq!(
22203 editor.display_text(cx),
22204 r#"fn main() {
22205⋯rintln!("1");
22206⋯intln!("2");
22207⋯ntln!("3");
22208println!("4");
22209println!("5");
22210}"#,
22211 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
22212 );
22213 assert_eq!(
22214 editor
22215 .selections
22216 .all::<Point>(cx)
22217 .into_iter()
22218 .map(|s| s.range())
22219 .collect::<Vec<_>>(),
22220 vec![Point::zero()..Point::zero()],
22221 "Previous editor in the 2nd pane had no selections changed hence should restore none",
22222 );
22223 })
22224 });
22225}
22226
22227#[gpui::test]
22228async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
22229 init_test(cx, |_| {});
22230
22231 let fs = FakeFs::new(cx.executor());
22232 let main_text = r#"fn main() {
22233println!("1");
22234println!("2");
22235println!("3");
22236println!("4");
22237println!("5");
22238}"#;
22239 let lib_text = "mod foo {}";
22240 fs.insert_tree(
22241 path!("/a"),
22242 json!({
22243 "lib.rs": lib_text,
22244 "main.rs": main_text,
22245 }),
22246 )
22247 .await;
22248
22249 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22250 let (workspace, cx) =
22251 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22252 let worktree_id = workspace.update(cx, |workspace, cx| {
22253 workspace.project().update(cx, |project, cx| {
22254 project.worktrees(cx).next().unwrap().read(cx).id()
22255 })
22256 });
22257
22258 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22259 let editor = workspace
22260 .update_in(cx, |workspace, window, cx| {
22261 workspace.open_path(
22262 (worktree_id, "main.rs"),
22263 Some(pane.downgrade()),
22264 true,
22265 window,
22266 cx,
22267 )
22268 })
22269 .unwrap()
22270 .await
22271 .downcast::<Editor>()
22272 .unwrap();
22273 pane.update(cx, |pane, cx| {
22274 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22275 open_editor.update(cx, |editor, cx| {
22276 assert_eq!(
22277 editor.display_text(cx),
22278 main_text,
22279 "Original main.rs text on initial open",
22280 );
22281 })
22282 });
22283 editor.update_in(cx, |editor, window, cx| {
22284 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
22285 });
22286
22287 cx.update_global(|store: &mut SettingsStore, cx| {
22288 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
22289 s.restore_on_file_reopen = Some(false);
22290 });
22291 });
22292 editor.update_in(cx, |editor, window, cx| {
22293 editor.fold_ranges(
22294 vec![
22295 Point::new(1, 0)..Point::new(1, 1),
22296 Point::new(2, 0)..Point::new(2, 2),
22297 Point::new(3, 0)..Point::new(3, 3),
22298 ],
22299 false,
22300 window,
22301 cx,
22302 );
22303 });
22304 pane.update_in(cx, |pane, window, cx| {
22305 pane.close_all_items(&CloseAllItems::default(), window, cx)
22306 })
22307 .await
22308 .unwrap();
22309 pane.update(cx, |pane, _| {
22310 assert!(pane.active_item().is_none());
22311 });
22312 cx.update_global(|store: &mut SettingsStore, cx| {
22313 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
22314 s.restore_on_file_reopen = Some(true);
22315 });
22316 });
22317
22318 let _editor_reopened = workspace
22319 .update_in(cx, |workspace, window, cx| {
22320 workspace.open_path(
22321 (worktree_id, "main.rs"),
22322 Some(pane.downgrade()),
22323 true,
22324 window,
22325 cx,
22326 )
22327 })
22328 .unwrap()
22329 .await
22330 .downcast::<Editor>()
22331 .unwrap();
22332 pane.update(cx, |pane, cx| {
22333 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22334 open_editor.update(cx, |editor, cx| {
22335 assert_eq!(
22336 editor.display_text(cx),
22337 main_text,
22338 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
22339 );
22340 })
22341 });
22342}
22343
22344#[gpui::test]
22345async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
22346 struct EmptyModalView {
22347 focus_handle: gpui::FocusHandle,
22348 }
22349 impl EventEmitter<DismissEvent> for EmptyModalView {}
22350 impl Render for EmptyModalView {
22351 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
22352 div()
22353 }
22354 }
22355 impl Focusable for EmptyModalView {
22356 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
22357 self.focus_handle.clone()
22358 }
22359 }
22360 impl workspace::ModalView for EmptyModalView {}
22361 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
22362 EmptyModalView {
22363 focus_handle: cx.focus_handle(),
22364 }
22365 }
22366
22367 init_test(cx, |_| {});
22368
22369 let fs = FakeFs::new(cx.executor());
22370 let project = Project::test(fs, [], cx).await;
22371 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22372 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
22373 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22374 let editor = cx.new_window_entity(|window, cx| {
22375 Editor::new(
22376 EditorMode::full(),
22377 buffer,
22378 Some(project.clone()),
22379 window,
22380 cx,
22381 )
22382 });
22383 workspace
22384 .update(cx, |workspace, window, cx| {
22385 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
22386 })
22387 .unwrap();
22388 editor.update_in(cx, |editor, window, cx| {
22389 editor.open_context_menu(&OpenContextMenu, window, cx);
22390 assert!(editor.mouse_context_menu.is_some());
22391 });
22392 workspace
22393 .update(cx, |workspace, window, cx| {
22394 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
22395 })
22396 .unwrap();
22397 cx.read(|cx| {
22398 assert!(editor.read(cx).mouse_context_menu.is_none());
22399 });
22400}
22401
22402#[gpui::test]
22403async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
22404 init_test(cx, |_| {});
22405
22406 let fs = FakeFs::new(cx.executor());
22407 fs.insert_file(path!("/file.html"), Default::default())
22408 .await;
22409
22410 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
22411
22412 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22413 let html_language = Arc::new(Language::new(
22414 LanguageConfig {
22415 name: "HTML".into(),
22416 matcher: LanguageMatcher {
22417 path_suffixes: vec!["html".to_string()],
22418 ..LanguageMatcher::default()
22419 },
22420 brackets: BracketPairConfig {
22421 pairs: vec![BracketPair {
22422 start: "<".into(),
22423 end: ">".into(),
22424 close: true,
22425 ..Default::default()
22426 }],
22427 ..Default::default()
22428 },
22429 ..Default::default()
22430 },
22431 Some(tree_sitter_html::LANGUAGE.into()),
22432 ));
22433 language_registry.add(html_language);
22434 let mut fake_servers = language_registry.register_fake_lsp(
22435 "HTML",
22436 FakeLspAdapter {
22437 capabilities: lsp::ServerCapabilities {
22438 completion_provider: Some(lsp::CompletionOptions {
22439 resolve_provider: Some(true),
22440 ..Default::default()
22441 }),
22442 ..Default::default()
22443 },
22444 ..Default::default()
22445 },
22446 );
22447
22448 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22449 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22450
22451 let worktree_id = workspace
22452 .update(cx, |workspace, _window, cx| {
22453 workspace.project().update(cx, |project, cx| {
22454 project.worktrees(cx).next().unwrap().read(cx).id()
22455 })
22456 })
22457 .unwrap();
22458 project
22459 .update(cx, |project, cx| {
22460 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
22461 })
22462 .await
22463 .unwrap();
22464 let editor = workspace
22465 .update(cx, |workspace, window, cx| {
22466 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
22467 })
22468 .unwrap()
22469 .await
22470 .unwrap()
22471 .downcast::<Editor>()
22472 .unwrap();
22473
22474 let fake_server = fake_servers.next().await.unwrap();
22475 editor.update_in(cx, |editor, window, cx| {
22476 editor.set_text("<ad></ad>", window, cx);
22477 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22478 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
22479 });
22480 let Some((buffer, _)) = editor
22481 .buffer
22482 .read(cx)
22483 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
22484 else {
22485 panic!("Failed to get buffer for selection position");
22486 };
22487 let buffer = buffer.read(cx);
22488 let buffer_id = buffer.remote_id();
22489 let opening_range =
22490 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
22491 let closing_range =
22492 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
22493 let mut linked_ranges = HashMap::default();
22494 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
22495 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
22496 });
22497 let mut completion_handle =
22498 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
22499 Ok(Some(lsp::CompletionResponse::Array(vec![
22500 lsp::CompletionItem {
22501 label: "head".to_string(),
22502 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22503 lsp::InsertReplaceEdit {
22504 new_text: "head".to_string(),
22505 insert: lsp::Range::new(
22506 lsp::Position::new(0, 1),
22507 lsp::Position::new(0, 3),
22508 ),
22509 replace: lsp::Range::new(
22510 lsp::Position::new(0, 1),
22511 lsp::Position::new(0, 3),
22512 ),
22513 },
22514 )),
22515 ..Default::default()
22516 },
22517 ])))
22518 });
22519 editor.update_in(cx, |editor, window, cx| {
22520 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
22521 });
22522 cx.run_until_parked();
22523 completion_handle.next().await.unwrap();
22524 editor.update(cx, |editor, _| {
22525 assert!(
22526 editor.context_menu_visible(),
22527 "Completion menu should be visible"
22528 );
22529 });
22530 editor.update_in(cx, |editor, window, cx| {
22531 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
22532 });
22533 cx.executor().run_until_parked();
22534 editor.update(cx, |editor, cx| {
22535 assert_eq!(editor.text(cx), "<head></head>");
22536 });
22537}
22538
22539#[gpui::test]
22540async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
22541 init_test(cx, |_| {});
22542
22543 let fs = FakeFs::new(cx.executor());
22544 fs.insert_tree(
22545 path!("/root"),
22546 json!({
22547 "a": {
22548 "main.rs": "fn main() {}",
22549 },
22550 "foo": {
22551 "bar": {
22552 "external_file.rs": "pub mod external {}",
22553 }
22554 }
22555 }),
22556 )
22557 .await;
22558
22559 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
22560 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22561 language_registry.add(rust_lang());
22562 let _fake_servers = language_registry.register_fake_lsp(
22563 "Rust",
22564 FakeLspAdapter {
22565 ..FakeLspAdapter::default()
22566 },
22567 );
22568 let (workspace, cx) =
22569 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22570 let worktree_id = workspace.update(cx, |workspace, cx| {
22571 workspace.project().update(cx, |project, cx| {
22572 project.worktrees(cx).next().unwrap().read(cx).id()
22573 })
22574 });
22575
22576 let assert_language_servers_count =
22577 |expected: usize, context: &str, cx: &mut VisualTestContext| {
22578 project.update(cx, |project, cx| {
22579 let current = project
22580 .lsp_store()
22581 .read(cx)
22582 .as_local()
22583 .unwrap()
22584 .language_servers
22585 .len();
22586 assert_eq!(expected, current, "{context}");
22587 });
22588 };
22589
22590 assert_language_servers_count(
22591 0,
22592 "No servers should be running before any file is open",
22593 cx,
22594 );
22595 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22596 let main_editor = workspace
22597 .update_in(cx, |workspace, window, cx| {
22598 workspace.open_path(
22599 (worktree_id, "main.rs"),
22600 Some(pane.downgrade()),
22601 true,
22602 window,
22603 cx,
22604 )
22605 })
22606 .unwrap()
22607 .await
22608 .downcast::<Editor>()
22609 .unwrap();
22610 pane.update(cx, |pane, cx| {
22611 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22612 open_editor.update(cx, |editor, cx| {
22613 assert_eq!(
22614 editor.display_text(cx),
22615 "fn main() {}",
22616 "Original main.rs text on initial open",
22617 );
22618 });
22619 assert_eq!(open_editor, main_editor);
22620 });
22621 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
22622
22623 let external_editor = workspace
22624 .update_in(cx, |workspace, window, cx| {
22625 workspace.open_abs_path(
22626 PathBuf::from("/root/foo/bar/external_file.rs"),
22627 OpenOptions::default(),
22628 window,
22629 cx,
22630 )
22631 })
22632 .await
22633 .expect("opening external file")
22634 .downcast::<Editor>()
22635 .expect("downcasted external file's open element to editor");
22636 pane.update(cx, |pane, cx| {
22637 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22638 open_editor.update(cx, |editor, cx| {
22639 assert_eq!(
22640 editor.display_text(cx),
22641 "pub mod external {}",
22642 "External file is open now",
22643 );
22644 });
22645 assert_eq!(open_editor, external_editor);
22646 });
22647 assert_language_servers_count(
22648 1,
22649 "Second, external, *.rs file should join the existing server",
22650 cx,
22651 );
22652
22653 pane.update_in(cx, |pane, window, cx| {
22654 pane.close_active_item(&CloseActiveItem::default(), window, cx)
22655 })
22656 .await
22657 .unwrap();
22658 pane.update_in(cx, |pane, window, cx| {
22659 pane.navigate_backward(window, cx);
22660 });
22661 cx.run_until_parked();
22662 pane.update(cx, |pane, cx| {
22663 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22664 open_editor.update(cx, |editor, cx| {
22665 assert_eq!(
22666 editor.display_text(cx),
22667 "pub mod external {}",
22668 "External file is open now",
22669 );
22670 });
22671 });
22672 assert_language_servers_count(
22673 1,
22674 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
22675 cx,
22676 );
22677
22678 cx.update(|_, cx| {
22679 workspace::reload(cx);
22680 });
22681 assert_language_servers_count(
22682 1,
22683 "After reloading the worktree with local and external files opened, only one project should be started",
22684 cx,
22685 );
22686}
22687
22688#[gpui::test]
22689async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
22690 init_test(cx, |_| {});
22691
22692 let mut cx = EditorTestContext::new(cx).await;
22693 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22694 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22695
22696 // test cursor move to start of each line on tab
22697 // for `if`, `elif`, `else`, `while`, `with` and `for`
22698 cx.set_state(indoc! {"
22699 def main():
22700 ˇ for item in items:
22701 ˇ while item.active:
22702 ˇ if item.value > 10:
22703 ˇ continue
22704 ˇ elif item.value < 0:
22705 ˇ break
22706 ˇ else:
22707 ˇ with item.context() as ctx:
22708 ˇ yield count
22709 ˇ else:
22710 ˇ log('while else')
22711 ˇ else:
22712 ˇ log('for else')
22713 "});
22714 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22715 cx.assert_editor_state(indoc! {"
22716 def main():
22717 ˇfor item in items:
22718 ˇwhile item.active:
22719 ˇif item.value > 10:
22720 ˇcontinue
22721 ˇelif item.value < 0:
22722 ˇbreak
22723 ˇelse:
22724 ˇwith item.context() as ctx:
22725 ˇyield count
22726 ˇelse:
22727 ˇlog('while else')
22728 ˇelse:
22729 ˇlog('for else')
22730 "});
22731 // test relative indent is preserved when tab
22732 // for `if`, `elif`, `else`, `while`, `with` and `for`
22733 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22734 cx.assert_editor_state(indoc! {"
22735 def main():
22736 ˇfor item in items:
22737 ˇwhile item.active:
22738 ˇif item.value > 10:
22739 ˇcontinue
22740 ˇelif item.value < 0:
22741 ˇbreak
22742 ˇelse:
22743 ˇwith item.context() as ctx:
22744 ˇyield count
22745 ˇelse:
22746 ˇlog('while else')
22747 ˇelse:
22748 ˇlog('for else')
22749 "});
22750
22751 // test cursor move to start of each line on tab
22752 // for `try`, `except`, `else`, `finally`, `match` and `def`
22753 cx.set_state(indoc! {"
22754 def main():
22755 ˇ try:
22756 ˇ fetch()
22757 ˇ except ValueError:
22758 ˇ handle_error()
22759 ˇ else:
22760 ˇ match value:
22761 ˇ case _:
22762 ˇ finally:
22763 ˇ def status():
22764 ˇ return 0
22765 "});
22766 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22767 cx.assert_editor_state(indoc! {"
22768 def main():
22769 ˇtry:
22770 ˇfetch()
22771 ˇexcept ValueError:
22772 ˇhandle_error()
22773 ˇelse:
22774 ˇmatch value:
22775 ˇcase _:
22776 ˇfinally:
22777 ˇdef status():
22778 ˇreturn 0
22779 "});
22780 // test relative indent is preserved when tab
22781 // for `try`, `except`, `else`, `finally`, `match` and `def`
22782 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22783 cx.assert_editor_state(indoc! {"
22784 def main():
22785 ˇtry:
22786 ˇfetch()
22787 ˇexcept ValueError:
22788 ˇhandle_error()
22789 ˇelse:
22790 ˇmatch value:
22791 ˇcase _:
22792 ˇfinally:
22793 ˇdef status():
22794 ˇreturn 0
22795 "});
22796}
22797
22798#[gpui::test]
22799async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
22800 init_test(cx, |_| {});
22801
22802 let mut cx = EditorTestContext::new(cx).await;
22803 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22804 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22805
22806 // test `else` auto outdents when typed inside `if` block
22807 cx.set_state(indoc! {"
22808 def main():
22809 if i == 2:
22810 return
22811 ˇ
22812 "});
22813 cx.update_editor(|editor, window, cx| {
22814 editor.handle_input("else:", window, cx);
22815 });
22816 cx.assert_editor_state(indoc! {"
22817 def main():
22818 if i == 2:
22819 return
22820 else:ˇ
22821 "});
22822
22823 // test `except` auto outdents when typed inside `try` block
22824 cx.set_state(indoc! {"
22825 def main():
22826 try:
22827 i = 2
22828 ˇ
22829 "});
22830 cx.update_editor(|editor, window, cx| {
22831 editor.handle_input("except:", window, cx);
22832 });
22833 cx.assert_editor_state(indoc! {"
22834 def main():
22835 try:
22836 i = 2
22837 except:ˇ
22838 "});
22839
22840 // test `else` auto outdents when typed inside `except` block
22841 cx.set_state(indoc! {"
22842 def main():
22843 try:
22844 i = 2
22845 except:
22846 j = 2
22847 ˇ
22848 "});
22849 cx.update_editor(|editor, window, cx| {
22850 editor.handle_input("else:", window, cx);
22851 });
22852 cx.assert_editor_state(indoc! {"
22853 def main():
22854 try:
22855 i = 2
22856 except:
22857 j = 2
22858 else:ˇ
22859 "});
22860
22861 // test `finally` auto outdents when typed inside `else` block
22862 cx.set_state(indoc! {"
22863 def main():
22864 try:
22865 i = 2
22866 except:
22867 j = 2
22868 else:
22869 k = 2
22870 ˇ
22871 "});
22872 cx.update_editor(|editor, window, cx| {
22873 editor.handle_input("finally:", window, cx);
22874 });
22875 cx.assert_editor_state(indoc! {"
22876 def main():
22877 try:
22878 i = 2
22879 except:
22880 j = 2
22881 else:
22882 k = 2
22883 finally:ˇ
22884 "});
22885
22886 // test `else` does not outdents when typed inside `except` block right after for block
22887 cx.set_state(indoc! {"
22888 def main():
22889 try:
22890 i = 2
22891 except:
22892 for i in range(n):
22893 pass
22894 ˇ
22895 "});
22896 cx.update_editor(|editor, window, cx| {
22897 editor.handle_input("else:", window, cx);
22898 });
22899 cx.assert_editor_state(indoc! {"
22900 def main():
22901 try:
22902 i = 2
22903 except:
22904 for i in range(n):
22905 pass
22906 else:ˇ
22907 "});
22908
22909 // test `finally` auto outdents when typed inside `else` block right after for block
22910 cx.set_state(indoc! {"
22911 def main():
22912 try:
22913 i = 2
22914 except:
22915 j = 2
22916 else:
22917 for i in range(n):
22918 pass
22919 ˇ
22920 "});
22921 cx.update_editor(|editor, window, cx| {
22922 editor.handle_input("finally:", window, cx);
22923 });
22924 cx.assert_editor_state(indoc! {"
22925 def main():
22926 try:
22927 i = 2
22928 except:
22929 j = 2
22930 else:
22931 for i in range(n):
22932 pass
22933 finally:ˇ
22934 "});
22935
22936 // test `except` outdents to inner "try" block
22937 cx.set_state(indoc! {"
22938 def main():
22939 try:
22940 i = 2
22941 if i == 2:
22942 try:
22943 i = 3
22944 ˇ
22945 "});
22946 cx.update_editor(|editor, window, cx| {
22947 editor.handle_input("except:", window, cx);
22948 });
22949 cx.assert_editor_state(indoc! {"
22950 def main():
22951 try:
22952 i = 2
22953 if i == 2:
22954 try:
22955 i = 3
22956 except:ˇ
22957 "});
22958
22959 // test `except` outdents to outer "try" block
22960 cx.set_state(indoc! {"
22961 def main():
22962 try:
22963 i = 2
22964 if i == 2:
22965 try:
22966 i = 3
22967 ˇ
22968 "});
22969 cx.update_editor(|editor, window, cx| {
22970 editor.handle_input("except:", window, cx);
22971 });
22972 cx.assert_editor_state(indoc! {"
22973 def main():
22974 try:
22975 i = 2
22976 if i == 2:
22977 try:
22978 i = 3
22979 except:ˇ
22980 "});
22981
22982 // test `else` stays at correct indent when typed after `for` block
22983 cx.set_state(indoc! {"
22984 def main():
22985 for i in range(10):
22986 if i == 3:
22987 break
22988 ˇ
22989 "});
22990 cx.update_editor(|editor, window, cx| {
22991 editor.handle_input("else:", window, cx);
22992 });
22993 cx.assert_editor_state(indoc! {"
22994 def main():
22995 for i in range(10):
22996 if i == 3:
22997 break
22998 else:ˇ
22999 "});
23000
23001 // test does not outdent on typing after line with square brackets
23002 cx.set_state(indoc! {"
23003 def f() -> list[str]:
23004 ˇ
23005 "});
23006 cx.update_editor(|editor, window, cx| {
23007 editor.handle_input("a", window, cx);
23008 });
23009 cx.assert_editor_state(indoc! {"
23010 def f() -> list[str]:
23011 aˇ
23012 "});
23013
23014 // test does not outdent on typing : after case keyword
23015 cx.set_state(indoc! {"
23016 match 1:
23017 caseˇ
23018 "});
23019 cx.update_editor(|editor, window, cx| {
23020 editor.handle_input(":", window, cx);
23021 });
23022 cx.assert_editor_state(indoc! {"
23023 match 1:
23024 case:ˇ
23025 "});
23026}
23027
23028#[gpui::test]
23029async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
23030 init_test(cx, |_| {});
23031 update_test_language_settings(cx, |settings| {
23032 settings.defaults.extend_comment_on_newline = Some(false);
23033 });
23034 let mut cx = EditorTestContext::new(cx).await;
23035 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
23036 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23037
23038 // test correct indent after newline on comment
23039 cx.set_state(indoc! {"
23040 # COMMENT:ˇ
23041 "});
23042 cx.update_editor(|editor, window, cx| {
23043 editor.newline(&Newline, window, cx);
23044 });
23045 cx.assert_editor_state(indoc! {"
23046 # COMMENT:
23047 ˇ
23048 "});
23049
23050 // test correct indent after newline in brackets
23051 cx.set_state(indoc! {"
23052 {ˇ}
23053 "});
23054 cx.update_editor(|editor, window, cx| {
23055 editor.newline(&Newline, window, cx);
23056 });
23057 cx.run_until_parked();
23058 cx.assert_editor_state(indoc! {"
23059 {
23060 ˇ
23061 }
23062 "});
23063
23064 cx.set_state(indoc! {"
23065 (ˇ)
23066 "});
23067 cx.update_editor(|editor, window, cx| {
23068 editor.newline(&Newline, window, cx);
23069 });
23070 cx.run_until_parked();
23071 cx.assert_editor_state(indoc! {"
23072 (
23073 ˇ
23074 )
23075 "});
23076
23077 // do not indent after empty lists or dictionaries
23078 cx.set_state(indoc! {"
23079 a = []ˇ
23080 "});
23081 cx.update_editor(|editor, window, cx| {
23082 editor.newline(&Newline, window, cx);
23083 });
23084 cx.run_until_parked();
23085 cx.assert_editor_state(indoc! {"
23086 a = []
23087 ˇ
23088 "});
23089}
23090
23091#[gpui::test]
23092async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
23093 init_test(cx, |_| {});
23094
23095 let mut cx = EditorTestContext::new(cx).await;
23096 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
23097 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23098
23099 // test cursor move to start of each line on tab
23100 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
23101 cx.set_state(indoc! {"
23102 function main() {
23103 ˇ for item in $items; do
23104 ˇ while [ -n \"$item\" ]; do
23105 ˇ if [ \"$value\" -gt 10 ]; then
23106 ˇ continue
23107 ˇ elif [ \"$value\" -lt 0 ]; then
23108 ˇ break
23109 ˇ else
23110 ˇ echo \"$item\"
23111 ˇ fi
23112 ˇ done
23113 ˇ done
23114 ˇ}
23115 "});
23116 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23117 cx.assert_editor_state(indoc! {"
23118 function main() {
23119 ˇfor item in $items; do
23120 ˇwhile [ -n \"$item\" ]; do
23121 ˇif [ \"$value\" -gt 10 ]; then
23122 ˇcontinue
23123 ˇelif [ \"$value\" -lt 0 ]; then
23124 ˇbreak
23125 ˇelse
23126 ˇecho \"$item\"
23127 ˇfi
23128 ˇdone
23129 ˇdone
23130 ˇ}
23131 "});
23132 // test relative indent is preserved when tab
23133 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23134 cx.assert_editor_state(indoc! {"
23135 function main() {
23136 ˇfor item in $items; do
23137 ˇwhile [ -n \"$item\" ]; do
23138 ˇif [ \"$value\" -gt 10 ]; then
23139 ˇcontinue
23140 ˇelif [ \"$value\" -lt 0 ]; then
23141 ˇbreak
23142 ˇelse
23143 ˇecho \"$item\"
23144 ˇfi
23145 ˇdone
23146 ˇdone
23147 ˇ}
23148 "});
23149
23150 // test cursor move to start of each line on tab
23151 // for `case` statement with patterns
23152 cx.set_state(indoc! {"
23153 function handle() {
23154 ˇ case \"$1\" in
23155 ˇ start)
23156 ˇ echo \"a\"
23157 ˇ ;;
23158 ˇ stop)
23159 ˇ echo \"b\"
23160 ˇ ;;
23161 ˇ *)
23162 ˇ echo \"c\"
23163 ˇ ;;
23164 ˇ esac
23165 ˇ}
23166 "});
23167 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
23168 cx.assert_editor_state(indoc! {"
23169 function handle() {
23170 ˇcase \"$1\" in
23171 ˇstart)
23172 ˇecho \"a\"
23173 ˇ;;
23174 ˇstop)
23175 ˇecho \"b\"
23176 ˇ;;
23177 ˇ*)
23178 ˇecho \"c\"
23179 ˇ;;
23180 ˇesac
23181 ˇ}
23182 "});
23183}
23184
23185#[gpui::test]
23186async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
23187 init_test(cx, |_| {});
23188
23189 let mut cx = EditorTestContext::new(cx).await;
23190 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
23191 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23192
23193 // test indents on comment insert
23194 cx.set_state(indoc! {"
23195 function main() {
23196 ˇ for item in $items; do
23197 ˇ while [ -n \"$item\" ]; do
23198 ˇ if [ \"$value\" -gt 10 ]; then
23199 ˇ continue
23200 ˇ elif [ \"$value\" -lt 0 ]; then
23201 ˇ break
23202 ˇ else
23203 ˇ echo \"$item\"
23204 ˇ fi
23205 ˇ done
23206 ˇ done
23207 ˇ}
23208 "});
23209 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
23210 cx.assert_editor_state(indoc! {"
23211 function main() {
23212 #ˇ for item in $items; do
23213 #ˇ while [ -n \"$item\" ]; do
23214 #ˇ if [ \"$value\" -gt 10 ]; then
23215 #ˇ continue
23216 #ˇ elif [ \"$value\" -lt 0 ]; then
23217 #ˇ break
23218 #ˇ else
23219 #ˇ echo \"$item\"
23220 #ˇ fi
23221 #ˇ done
23222 #ˇ done
23223 #ˇ}
23224 "});
23225}
23226
23227#[gpui::test]
23228async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
23229 init_test(cx, |_| {});
23230
23231 let mut cx = EditorTestContext::new(cx).await;
23232 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
23233 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23234
23235 // test `else` auto outdents when typed inside `if` block
23236 cx.set_state(indoc! {"
23237 if [ \"$1\" = \"test\" ]; then
23238 echo \"foo bar\"
23239 ˇ
23240 "});
23241 cx.update_editor(|editor, window, cx| {
23242 editor.handle_input("else", window, cx);
23243 });
23244 cx.assert_editor_state(indoc! {"
23245 if [ \"$1\" = \"test\" ]; then
23246 echo \"foo bar\"
23247 elseˇ
23248 "});
23249
23250 // test `elif` auto outdents when typed inside `if` block
23251 cx.set_state(indoc! {"
23252 if [ \"$1\" = \"test\" ]; then
23253 echo \"foo bar\"
23254 ˇ
23255 "});
23256 cx.update_editor(|editor, window, cx| {
23257 editor.handle_input("elif", window, cx);
23258 });
23259 cx.assert_editor_state(indoc! {"
23260 if [ \"$1\" = \"test\" ]; then
23261 echo \"foo bar\"
23262 elifˇ
23263 "});
23264
23265 // test `fi` auto outdents when typed inside `else` block
23266 cx.set_state(indoc! {"
23267 if [ \"$1\" = \"test\" ]; then
23268 echo \"foo bar\"
23269 else
23270 echo \"bar baz\"
23271 ˇ
23272 "});
23273 cx.update_editor(|editor, window, cx| {
23274 editor.handle_input("fi", window, cx);
23275 });
23276 cx.assert_editor_state(indoc! {"
23277 if [ \"$1\" = \"test\" ]; then
23278 echo \"foo bar\"
23279 else
23280 echo \"bar baz\"
23281 fiˇ
23282 "});
23283
23284 // test `done` auto outdents when typed inside `while` block
23285 cx.set_state(indoc! {"
23286 while read line; do
23287 echo \"$line\"
23288 ˇ
23289 "});
23290 cx.update_editor(|editor, window, cx| {
23291 editor.handle_input("done", window, cx);
23292 });
23293 cx.assert_editor_state(indoc! {"
23294 while read line; do
23295 echo \"$line\"
23296 doneˇ
23297 "});
23298
23299 // test `done` auto outdents when typed inside `for` block
23300 cx.set_state(indoc! {"
23301 for file in *.txt; do
23302 cat \"$file\"
23303 ˇ
23304 "});
23305 cx.update_editor(|editor, window, cx| {
23306 editor.handle_input("done", window, cx);
23307 });
23308 cx.assert_editor_state(indoc! {"
23309 for file in *.txt; do
23310 cat \"$file\"
23311 doneˇ
23312 "});
23313
23314 // test `esac` auto outdents when typed inside `case` block
23315 cx.set_state(indoc! {"
23316 case \"$1\" in
23317 start)
23318 echo \"foo bar\"
23319 ;;
23320 stop)
23321 echo \"bar baz\"
23322 ;;
23323 ˇ
23324 "});
23325 cx.update_editor(|editor, window, cx| {
23326 editor.handle_input("esac", window, cx);
23327 });
23328 cx.assert_editor_state(indoc! {"
23329 case \"$1\" in
23330 start)
23331 echo \"foo bar\"
23332 ;;
23333 stop)
23334 echo \"bar baz\"
23335 ;;
23336 esacˇ
23337 "});
23338
23339 // test `*)` auto outdents when typed inside `case` block
23340 cx.set_state(indoc! {"
23341 case \"$1\" in
23342 start)
23343 echo \"foo bar\"
23344 ;;
23345 ˇ
23346 "});
23347 cx.update_editor(|editor, window, cx| {
23348 editor.handle_input("*)", window, cx);
23349 });
23350 cx.assert_editor_state(indoc! {"
23351 case \"$1\" in
23352 start)
23353 echo \"foo bar\"
23354 ;;
23355 *)ˇ
23356 "});
23357
23358 // test `fi` outdents to correct level with nested if blocks
23359 cx.set_state(indoc! {"
23360 if [ \"$1\" = \"test\" ]; then
23361 echo \"outer if\"
23362 if [ \"$2\" = \"debug\" ]; then
23363 echo \"inner if\"
23364 ˇ
23365 "});
23366 cx.update_editor(|editor, window, cx| {
23367 editor.handle_input("fi", window, cx);
23368 });
23369 cx.assert_editor_state(indoc! {"
23370 if [ \"$1\" = \"test\" ]; then
23371 echo \"outer if\"
23372 if [ \"$2\" = \"debug\" ]; then
23373 echo \"inner if\"
23374 fiˇ
23375 "});
23376}
23377
23378#[gpui::test]
23379async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
23380 init_test(cx, |_| {});
23381 update_test_language_settings(cx, |settings| {
23382 settings.defaults.extend_comment_on_newline = Some(false);
23383 });
23384 let mut cx = EditorTestContext::new(cx).await;
23385 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
23386 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23387
23388 // test correct indent after newline on comment
23389 cx.set_state(indoc! {"
23390 # COMMENT:ˇ
23391 "});
23392 cx.update_editor(|editor, window, cx| {
23393 editor.newline(&Newline, window, cx);
23394 });
23395 cx.assert_editor_state(indoc! {"
23396 # COMMENT:
23397 ˇ
23398 "});
23399
23400 // test correct indent after newline after `then`
23401 cx.set_state(indoc! {"
23402
23403 if [ \"$1\" = \"test\" ]; thenˇ
23404 "});
23405 cx.update_editor(|editor, window, cx| {
23406 editor.newline(&Newline, window, cx);
23407 });
23408 cx.run_until_parked();
23409 cx.assert_editor_state(indoc! {"
23410
23411 if [ \"$1\" = \"test\" ]; then
23412 ˇ
23413 "});
23414
23415 // test correct indent after newline after `else`
23416 cx.set_state(indoc! {"
23417 if [ \"$1\" = \"test\" ]; then
23418 elseˇ
23419 "});
23420 cx.update_editor(|editor, window, cx| {
23421 editor.newline(&Newline, window, cx);
23422 });
23423 cx.run_until_parked();
23424 cx.assert_editor_state(indoc! {"
23425 if [ \"$1\" = \"test\" ]; then
23426 else
23427 ˇ
23428 "});
23429
23430 // test correct indent after newline after `elif`
23431 cx.set_state(indoc! {"
23432 if [ \"$1\" = \"test\" ]; then
23433 elifˇ
23434 "});
23435 cx.update_editor(|editor, window, cx| {
23436 editor.newline(&Newline, window, cx);
23437 });
23438 cx.run_until_parked();
23439 cx.assert_editor_state(indoc! {"
23440 if [ \"$1\" = \"test\" ]; then
23441 elif
23442 ˇ
23443 "});
23444
23445 // test correct indent after newline after `do`
23446 cx.set_state(indoc! {"
23447 for file in *.txt; doˇ
23448 "});
23449 cx.update_editor(|editor, window, cx| {
23450 editor.newline(&Newline, window, cx);
23451 });
23452 cx.run_until_parked();
23453 cx.assert_editor_state(indoc! {"
23454 for file in *.txt; do
23455 ˇ
23456 "});
23457
23458 // test correct indent after newline after case pattern
23459 cx.set_state(indoc! {"
23460 case \"$1\" in
23461 start)ˇ
23462 "});
23463 cx.update_editor(|editor, window, cx| {
23464 editor.newline(&Newline, window, cx);
23465 });
23466 cx.run_until_parked();
23467 cx.assert_editor_state(indoc! {"
23468 case \"$1\" in
23469 start)
23470 ˇ
23471 "});
23472
23473 // test correct indent after newline after case pattern
23474 cx.set_state(indoc! {"
23475 case \"$1\" in
23476 start)
23477 ;;
23478 *)ˇ
23479 "});
23480 cx.update_editor(|editor, window, cx| {
23481 editor.newline(&Newline, window, cx);
23482 });
23483 cx.run_until_parked();
23484 cx.assert_editor_state(indoc! {"
23485 case \"$1\" in
23486 start)
23487 ;;
23488 *)
23489 ˇ
23490 "});
23491
23492 // test correct indent after newline after function opening brace
23493 cx.set_state(indoc! {"
23494 function test() {ˇ}
23495 "});
23496 cx.update_editor(|editor, window, cx| {
23497 editor.newline(&Newline, window, cx);
23498 });
23499 cx.run_until_parked();
23500 cx.assert_editor_state(indoc! {"
23501 function test() {
23502 ˇ
23503 }
23504 "});
23505
23506 // test no extra indent after semicolon on same line
23507 cx.set_state(indoc! {"
23508 echo \"test\";ˇ
23509 "});
23510 cx.update_editor(|editor, window, cx| {
23511 editor.newline(&Newline, window, cx);
23512 });
23513 cx.run_until_parked();
23514 cx.assert_editor_state(indoc! {"
23515 echo \"test\";
23516 ˇ
23517 "});
23518}
23519
23520fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
23521 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
23522 point..point
23523}
23524
23525#[track_caller]
23526fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
23527 let (text, ranges) = marked_text_ranges(marked_text, true);
23528 assert_eq!(editor.text(cx), text);
23529 assert_eq!(
23530 editor.selections.ranges(cx),
23531 ranges,
23532 "Assert selections are {}",
23533 marked_text
23534 );
23535}
23536
23537pub fn handle_signature_help_request(
23538 cx: &mut EditorLspTestContext,
23539 mocked_response: lsp::SignatureHelp,
23540) -> impl Future<Output = ()> + use<> {
23541 let mut request =
23542 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
23543 let mocked_response = mocked_response.clone();
23544 async move { Ok(Some(mocked_response)) }
23545 });
23546
23547 async move {
23548 request.next().await;
23549 }
23550}
23551
23552#[track_caller]
23553pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
23554 cx.update_editor(|editor, _, _| {
23555 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
23556 let entries = menu.entries.borrow();
23557 let entries = entries
23558 .iter()
23559 .map(|entry| entry.string.as_str())
23560 .collect::<Vec<_>>();
23561 assert_eq!(entries, expected);
23562 } else {
23563 panic!("Expected completions menu");
23564 }
23565 });
23566}
23567
23568/// Handle completion request passing a marked string specifying where the completion
23569/// should be triggered from using '|' character, what range should be replaced, and what completions
23570/// should be returned using '<' and '>' to delimit the range.
23571///
23572/// Also see `handle_completion_request_with_insert_and_replace`.
23573#[track_caller]
23574pub fn handle_completion_request(
23575 marked_string: &str,
23576 completions: Vec<&'static str>,
23577 is_incomplete: bool,
23578 counter: Arc<AtomicUsize>,
23579 cx: &mut EditorLspTestContext,
23580) -> impl Future<Output = ()> {
23581 let complete_from_marker: TextRangeMarker = '|'.into();
23582 let replace_range_marker: TextRangeMarker = ('<', '>').into();
23583 let (_, mut marked_ranges) = marked_text_ranges_by(
23584 marked_string,
23585 vec![complete_from_marker.clone(), replace_range_marker.clone()],
23586 );
23587
23588 let complete_from_position =
23589 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
23590 let replace_range =
23591 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
23592
23593 let mut request =
23594 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
23595 let completions = completions.clone();
23596 counter.fetch_add(1, atomic::Ordering::Release);
23597 async move {
23598 assert_eq!(params.text_document_position.text_document.uri, url.clone());
23599 assert_eq!(
23600 params.text_document_position.position,
23601 complete_from_position
23602 );
23603 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
23604 is_incomplete,
23605 item_defaults: None,
23606 items: completions
23607 .iter()
23608 .map(|completion_text| lsp::CompletionItem {
23609 label: completion_text.to_string(),
23610 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
23611 range: replace_range,
23612 new_text: completion_text.to_string(),
23613 })),
23614 ..Default::default()
23615 })
23616 .collect(),
23617 })))
23618 }
23619 });
23620
23621 async move {
23622 request.next().await;
23623 }
23624}
23625
23626/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
23627/// given instead, which also contains an `insert` range.
23628///
23629/// This function uses markers to define ranges:
23630/// - `|` marks the cursor position
23631/// - `<>` marks the replace range
23632/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
23633pub fn handle_completion_request_with_insert_and_replace(
23634 cx: &mut EditorLspTestContext,
23635 marked_string: &str,
23636 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
23637 counter: Arc<AtomicUsize>,
23638) -> impl Future<Output = ()> {
23639 let complete_from_marker: TextRangeMarker = '|'.into();
23640 let replace_range_marker: TextRangeMarker = ('<', '>').into();
23641 let insert_range_marker: TextRangeMarker = ('{', '}').into();
23642
23643 let (_, mut marked_ranges) = marked_text_ranges_by(
23644 marked_string,
23645 vec![
23646 complete_from_marker.clone(),
23647 replace_range_marker.clone(),
23648 insert_range_marker.clone(),
23649 ],
23650 );
23651
23652 let complete_from_position =
23653 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
23654 let replace_range =
23655 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
23656
23657 let insert_range = match marked_ranges.remove(&insert_range_marker) {
23658 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
23659 _ => lsp::Range {
23660 start: replace_range.start,
23661 end: complete_from_position,
23662 },
23663 };
23664
23665 let mut request =
23666 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
23667 let completions = completions.clone();
23668 counter.fetch_add(1, atomic::Ordering::Release);
23669 async move {
23670 assert_eq!(params.text_document_position.text_document.uri, url.clone());
23671 assert_eq!(
23672 params.text_document_position.position, complete_from_position,
23673 "marker `|` position doesn't match",
23674 );
23675 Ok(Some(lsp::CompletionResponse::Array(
23676 completions
23677 .iter()
23678 .map(|(label, new_text)| lsp::CompletionItem {
23679 label: label.to_string(),
23680 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
23681 lsp::InsertReplaceEdit {
23682 insert: insert_range,
23683 replace: replace_range,
23684 new_text: new_text.to_string(),
23685 },
23686 )),
23687 ..Default::default()
23688 })
23689 .collect(),
23690 )))
23691 }
23692 });
23693
23694 async move {
23695 request.next().await;
23696 }
23697}
23698
23699fn handle_resolve_completion_request(
23700 cx: &mut EditorLspTestContext,
23701 edits: Option<Vec<(&'static str, &'static str)>>,
23702) -> impl Future<Output = ()> {
23703 let edits = edits.map(|edits| {
23704 edits
23705 .iter()
23706 .map(|(marked_string, new_text)| {
23707 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
23708 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
23709 lsp::TextEdit::new(replace_range, new_text.to_string())
23710 })
23711 .collect::<Vec<_>>()
23712 });
23713
23714 let mut request =
23715 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
23716 let edits = edits.clone();
23717 async move {
23718 Ok(lsp::CompletionItem {
23719 additional_text_edits: edits,
23720 ..Default::default()
23721 })
23722 }
23723 });
23724
23725 async move {
23726 request.next().await;
23727 }
23728}
23729
23730pub(crate) fn update_test_language_settings(
23731 cx: &mut TestAppContext,
23732 f: impl Fn(&mut AllLanguageSettingsContent),
23733) {
23734 cx.update(|cx| {
23735 SettingsStore::update_global(cx, |store, cx| {
23736 store.update_user_settings::<AllLanguageSettings>(cx, f);
23737 });
23738 });
23739}
23740
23741pub(crate) fn update_test_project_settings(
23742 cx: &mut TestAppContext,
23743 f: impl Fn(&mut ProjectSettings),
23744) {
23745 cx.update(|cx| {
23746 SettingsStore::update_global(cx, |store, cx| {
23747 store.update_user_settings::<ProjectSettings>(cx, f);
23748 });
23749 });
23750}
23751
23752pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
23753 cx.update(|cx| {
23754 assets::Assets.load_test_fonts(cx);
23755 let store = SettingsStore::test(cx);
23756 cx.set_global(store);
23757 theme::init(theme::LoadThemes::JustBase, cx);
23758 release_channel::init(SemanticVersion::default(), cx);
23759 client::init_settings(cx);
23760 language::init(cx);
23761 Project::init_settings(cx);
23762 workspace::init_settings(cx);
23763 crate::init(cx);
23764 });
23765 zlog::init_test();
23766 update_test_language_settings(cx, f);
23767}
23768
23769#[track_caller]
23770fn assert_hunk_revert(
23771 not_reverted_text_with_selections: &str,
23772 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
23773 expected_reverted_text_with_selections: &str,
23774 base_text: &str,
23775 cx: &mut EditorLspTestContext,
23776) {
23777 cx.set_state(not_reverted_text_with_selections);
23778 cx.set_head_text(base_text);
23779 cx.executor().run_until_parked();
23780
23781 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
23782 let snapshot = editor.snapshot(window, cx);
23783 let reverted_hunk_statuses = snapshot
23784 .buffer_snapshot
23785 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
23786 .map(|hunk| hunk.status().kind)
23787 .collect::<Vec<_>>();
23788
23789 editor.git_restore(&Default::default(), window, cx);
23790 reverted_hunk_statuses
23791 });
23792 cx.executor().run_until_parked();
23793 cx.assert_editor_state(expected_reverted_text_with_selections);
23794 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
23795}
23796
23797#[gpui::test(iterations = 10)]
23798async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
23799 init_test(cx, |_| {});
23800
23801 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
23802 let counter = diagnostic_requests.clone();
23803
23804 let fs = FakeFs::new(cx.executor());
23805 fs.insert_tree(
23806 path!("/a"),
23807 json!({
23808 "first.rs": "fn main() { let a = 5; }",
23809 "second.rs": "// Test file",
23810 }),
23811 )
23812 .await;
23813
23814 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23815 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23816 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23817
23818 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23819 language_registry.add(rust_lang());
23820 let mut fake_servers = language_registry.register_fake_lsp(
23821 "Rust",
23822 FakeLspAdapter {
23823 capabilities: lsp::ServerCapabilities {
23824 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
23825 lsp::DiagnosticOptions {
23826 identifier: None,
23827 inter_file_dependencies: true,
23828 workspace_diagnostics: true,
23829 work_done_progress_options: Default::default(),
23830 },
23831 )),
23832 ..Default::default()
23833 },
23834 ..Default::default()
23835 },
23836 );
23837
23838 let editor = workspace
23839 .update(cx, |workspace, window, cx| {
23840 workspace.open_abs_path(
23841 PathBuf::from(path!("/a/first.rs")),
23842 OpenOptions::default(),
23843 window,
23844 cx,
23845 )
23846 })
23847 .unwrap()
23848 .await
23849 .unwrap()
23850 .downcast::<Editor>()
23851 .unwrap();
23852 let fake_server = fake_servers.next().await.unwrap();
23853 let server_id = fake_server.server.server_id();
23854 let mut first_request = fake_server
23855 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
23856 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
23857 let result_id = Some(new_result_id.to_string());
23858 assert_eq!(
23859 params.text_document.uri,
23860 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
23861 );
23862 async move {
23863 Ok(lsp::DocumentDiagnosticReportResult::Report(
23864 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
23865 related_documents: None,
23866 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
23867 items: Vec::new(),
23868 result_id,
23869 },
23870 }),
23871 ))
23872 }
23873 });
23874
23875 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
23876 project.update(cx, |project, cx| {
23877 let buffer_id = editor
23878 .read(cx)
23879 .buffer()
23880 .read(cx)
23881 .as_singleton()
23882 .expect("created a singleton buffer")
23883 .read(cx)
23884 .remote_id();
23885 let buffer_result_id = project
23886 .lsp_store()
23887 .read(cx)
23888 .result_id(server_id, buffer_id, cx);
23889 assert_eq!(expected, buffer_result_id);
23890 });
23891 };
23892
23893 ensure_result_id(None, cx);
23894 cx.executor().advance_clock(Duration::from_millis(60));
23895 cx.executor().run_until_parked();
23896 assert_eq!(
23897 diagnostic_requests.load(atomic::Ordering::Acquire),
23898 1,
23899 "Opening file should trigger diagnostic request"
23900 );
23901 first_request
23902 .next()
23903 .await
23904 .expect("should have sent the first diagnostics pull request");
23905 ensure_result_id(Some("1".to_string()), cx);
23906
23907 // Editing should trigger diagnostics
23908 editor.update_in(cx, |editor, window, cx| {
23909 editor.handle_input("2", window, cx)
23910 });
23911 cx.executor().advance_clock(Duration::from_millis(60));
23912 cx.executor().run_until_parked();
23913 assert_eq!(
23914 diagnostic_requests.load(atomic::Ordering::Acquire),
23915 2,
23916 "Editing should trigger diagnostic request"
23917 );
23918 ensure_result_id(Some("2".to_string()), cx);
23919
23920 // Moving cursor should not trigger diagnostic request
23921 editor.update_in(cx, |editor, window, cx| {
23922 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23923 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
23924 });
23925 });
23926 cx.executor().advance_clock(Duration::from_millis(60));
23927 cx.executor().run_until_parked();
23928 assert_eq!(
23929 diagnostic_requests.load(atomic::Ordering::Acquire),
23930 2,
23931 "Cursor movement should not trigger diagnostic request"
23932 );
23933 ensure_result_id(Some("2".to_string()), cx);
23934 // Multiple rapid edits should be debounced
23935 for _ in 0..5 {
23936 editor.update_in(cx, |editor, window, cx| {
23937 editor.handle_input("x", window, cx)
23938 });
23939 }
23940 cx.executor().advance_clock(Duration::from_millis(60));
23941 cx.executor().run_until_parked();
23942
23943 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
23944 assert!(
23945 final_requests <= 4,
23946 "Multiple rapid edits should be debounced (got {final_requests} requests)",
23947 );
23948 ensure_result_id(Some(final_requests.to_string()), cx);
23949}
23950
23951#[gpui::test]
23952async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
23953 // Regression test for issue #11671
23954 // Previously, adding a cursor after moving multiple cursors would reset
23955 // the cursor count instead of adding to the existing cursors.
23956 init_test(cx, |_| {});
23957 let mut cx = EditorTestContext::new(cx).await;
23958
23959 // Create a simple buffer with cursor at start
23960 cx.set_state(indoc! {"
23961 ˇaaaa
23962 bbbb
23963 cccc
23964 dddd
23965 eeee
23966 ffff
23967 gggg
23968 hhhh"});
23969
23970 // Add 2 cursors below (so we have 3 total)
23971 cx.update_editor(|editor, window, cx| {
23972 editor.add_selection_below(&Default::default(), window, cx);
23973 editor.add_selection_below(&Default::default(), window, cx);
23974 });
23975
23976 // Verify we have 3 cursors
23977 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
23978 assert_eq!(
23979 initial_count, 3,
23980 "Should have 3 cursors after adding 2 below"
23981 );
23982
23983 // Move down one line
23984 cx.update_editor(|editor, window, cx| {
23985 editor.move_down(&MoveDown, window, cx);
23986 });
23987
23988 // Add another cursor below
23989 cx.update_editor(|editor, window, cx| {
23990 editor.add_selection_below(&Default::default(), window, cx);
23991 });
23992
23993 // Should now have 4 cursors (3 original + 1 new)
23994 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
23995 assert_eq!(
23996 final_count, 4,
23997 "Should have 4 cursors after moving and adding another"
23998 );
23999}
24000
24001#[gpui::test(iterations = 10)]
24002async fn test_document_colors(cx: &mut TestAppContext) {
24003 let expected_color = Rgba {
24004 r: 0.33,
24005 g: 0.33,
24006 b: 0.33,
24007 a: 0.33,
24008 };
24009
24010 init_test(cx, |_| {});
24011
24012 let fs = FakeFs::new(cx.executor());
24013 fs.insert_tree(
24014 path!("/a"),
24015 json!({
24016 "first.rs": "fn main() { let a = 5; }",
24017 }),
24018 )
24019 .await;
24020
24021 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24022 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24023 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24024
24025 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24026 language_registry.add(rust_lang());
24027 let mut fake_servers = language_registry.register_fake_lsp(
24028 "Rust",
24029 FakeLspAdapter {
24030 capabilities: lsp::ServerCapabilities {
24031 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
24032 ..lsp::ServerCapabilities::default()
24033 },
24034 name: "rust-analyzer",
24035 ..FakeLspAdapter::default()
24036 },
24037 );
24038 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
24039 "Rust",
24040 FakeLspAdapter {
24041 capabilities: lsp::ServerCapabilities {
24042 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
24043 ..lsp::ServerCapabilities::default()
24044 },
24045 name: "not-rust-analyzer",
24046 ..FakeLspAdapter::default()
24047 },
24048 );
24049
24050 let editor = workspace
24051 .update(cx, |workspace, window, cx| {
24052 workspace.open_abs_path(
24053 PathBuf::from(path!("/a/first.rs")),
24054 OpenOptions::default(),
24055 window,
24056 cx,
24057 )
24058 })
24059 .unwrap()
24060 .await
24061 .unwrap()
24062 .downcast::<Editor>()
24063 .unwrap();
24064 let fake_language_server = fake_servers.next().await.unwrap();
24065 let fake_language_server_without_capabilities =
24066 fake_servers_without_capabilities.next().await.unwrap();
24067 let requests_made = Arc::new(AtomicUsize::new(0));
24068 let closure_requests_made = Arc::clone(&requests_made);
24069 let mut color_request_handle = fake_language_server
24070 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
24071 let requests_made = Arc::clone(&closure_requests_made);
24072 async move {
24073 assert_eq!(
24074 params.text_document.uri,
24075 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
24076 );
24077 requests_made.fetch_add(1, atomic::Ordering::Release);
24078 Ok(vec![
24079 lsp::ColorInformation {
24080 range: lsp::Range {
24081 start: lsp::Position {
24082 line: 0,
24083 character: 0,
24084 },
24085 end: lsp::Position {
24086 line: 0,
24087 character: 1,
24088 },
24089 },
24090 color: lsp::Color {
24091 red: 0.33,
24092 green: 0.33,
24093 blue: 0.33,
24094 alpha: 0.33,
24095 },
24096 },
24097 lsp::ColorInformation {
24098 range: lsp::Range {
24099 start: lsp::Position {
24100 line: 0,
24101 character: 0,
24102 },
24103 end: lsp::Position {
24104 line: 0,
24105 character: 1,
24106 },
24107 },
24108 color: lsp::Color {
24109 red: 0.33,
24110 green: 0.33,
24111 blue: 0.33,
24112 alpha: 0.33,
24113 },
24114 },
24115 ])
24116 }
24117 });
24118
24119 let _handle = fake_language_server_without_capabilities
24120 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
24121 panic!("Should not be called");
24122 });
24123 cx.executor().advance_clock(Duration::from_millis(100));
24124 color_request_handle.next().await.unwrap();
24125 cx.run_until_parked();
24126 assert_eq!(
24127 1,
24128 requests_made.load(atomic::Ordering::Acquire),
24129 "Should query for colors once per editor open"
24130 );
24131 editor.update_in(cx, |editor, _, cx| {
24132 assert_eq!(
24133 vec![expected_color],
24134 extract_color_inlays(editor, cx),
24135 "Should have an initial inlay"
24136 );
24137 });
24138
24139 // opening another file in a split should not influence the LSP query counter
24140 workspace
24141 .update(cx, |workspace, window, cx| {
24142 assert_eq!(
24143 workspace.panes().len(),
24144 1,
24145 "Should have one pane with one editor"
24146 );
24147 workspace.move_item_to_pane_in_direction(
24148 &MoveItemToPaneInDirection {
24149 direction: SplitDirection::Right,
24150 focus: false,
24151 clone: true,
24152 },
24153 window,
24154 cx,
24155 );
24156 })
24157 .unwrap();
24158 cx.run_until_parked();
24159 workspace
24160 .update(cx, |workspace, _, cx| {
24161 let panes = workspace.panes();
24162 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
24163 for pane in panes {
24164 let editor = pane
24165 .read(cx)
24166 .active_item()
24167 .and_then(|item| item.downcast::<Editor>())
24168 .expect("Should have opened an editor in each split");
24169 let editor_file = editor
24170 .read(cx)
24171 .buffer()
24172 .read(cx)
24173 .as_singleton()
24174 .expect("test deals with singleton buffers")
24175 .read(cx)
24176 .file()
24177 .expect("test buffese should have a file")
24178 .path();
24179 assert_eq!(
24180 editor_file.as_ref(),
24181 Path::new("first.rs"),
24182 "Both editors should be opened for the same file"
24183 )
24184 }
24185 })
24186 .unwrap();
24187
24188 cx.executor().advance_clock(Duration::from_millis(500));
24189 let save = editor.update_in(cx, |editor, window, cx| {
24190 editor.move_to_end(&MoveToEnd, window, cx);
24191 editor.handle_input("dirty", window, cx);
24192 editor.save(
24193 SaveOptions {
24194 format: true,
24195 autosave: true,
24196 },
24197 project.clone(),
24198 window,
24199 cx,
24200 )
24201 });
24202 save.await.unwrap();
24203
24204 color_request_handle.next().await.unwrap();
24205 cx.run_until_parked();
24206 assert_eq!(
24207 3,
24208 requests_made.load(atomic::Ordering::Acquire),
24209 "Should query for colors once per save and once per formatting after save"
24210 );
24211
24212 drop(editor);
24213 let close = workspace
24214 .update(cx, |workspace, window, cx| {
24215 workspace.active_pane().update(cx, |pane, cx| {
24216 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24217 })
24218 })
24219 .unwrap();
24220 close.await.unwrap();
24221 let close = workspace
24222 .update(cx, |workspace, window, cx| {
24223 workspace.active_pane().update(cx, |pane, cx| {
24224 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24225 })
24226 })
24227 .unwrap();
24228 close.await.unwrap();
24229 assert_eq!(
24230 3,
24231 requests_made.load(atomic::Ordering::Acquire),
24232 "After saving and closing all editors, no extra requests should be made"
24233 );
24234 workspace
24235 .update(cx, |workspace, _, cx| {
24236 assert!(
24237 workspace.active_item(cx).is_none(),
24238 "Should close all editors"
24239 )
24240 })
24241 .unwrap();
24242
24243 workspace
24244 .update(cx, |workspace, window, cx| {
24245 workspace.active_pane().update(cx, |pane, cx| {
24246 pane.navigate_backward(window, cx);
24247 })
24248 })
24249 .unwrap();
24250 cx.executor().advance_clock(Duration::from_millis(100));
24251 cx.run_until_parked();
24252 let editor = workspace
24253 .update(cx, |workspace, _, cx| {
24254 workspace
24255 .active_item(cx)
24256 .expect("Should have reopened the editor again after navigating back")
24257 .downcast::<Editor>()
24258 .expect("Should be an editor")
24259 })
24260 .unwrap();
24261 color_request_handle.next().await.unwrap();
24262 assert_eq!(
24263 3,
24264 requests_made.load(atomic::Ordering::Acquire),
24265 "Cache should be reused on buffer close and reopen"
24266 );
24267 editor.update(cx, |editor, cx| {
24268 assert_eq!(
24269 vec![expected_color],
24270 extract_color_inlays(editor, cx),
24271 "Should have an initial inlay"
24272 );
24273 });
24274}
24275
24276#[gpui::test]
24277async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
24278 init_test(cx, |_| {});
24279 let (editor, cx) = cx.add_window_view(Editor::single_line);
24280 editor.update_in(cx, |editor, window, cx| {
24281 editor.set_text("oops\n\nwow\n", window, cx)
24282 });
24283 cx.run_until_parked();
24284 editor.update(cx, |editor, cx| {
24285 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
24286 });
24287 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
24288 cx.run_until_parked();
24289 editor.update(cx, |editor, cx| {
24290 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
24291 });
24292}
24293
24294#[track_caller]
24295fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
24296 editor
24297 .all_inlays(cx)
24298 .into_iter()
24299 .filter_map(|inlay| inlay.get_color())
24300 .map(Rgba::from)
24301 .collect()
24302}