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().clone();
78 cx.subscribe_in(
79 &entity,
80 window,
81 move |_, _, event: &EditorEvent, _, _| match event {
82 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
83 EditorEvent::BufferEdited => {
84 events.borrow_mut().push(("editor1", "buffer edited"))
85 }
86 _ => {}
87 },
88 )
89 .detach();
90 Editor::for_buffer(buffer.clone(), None, window, cx)
91 }
92 });
93
94 let editor2 = cx.add_window({
95 let events = events.clone();
96 |window, cx| {
97 cx.subscribe_in(
98 &cx.entity().clone(),
99 window,
100 move |_, _, event: &EditorEvent, _, _| match event {
101 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
102 EditorEvent::BufferEdited => {
103 events.borrow_mut().push(("editor2", "buffer edited"))
104 }
105 _ => {}
106 },
107 )
108 .detach();
109 Editor::for_buffer(buffer.clone(), None, window, cx)
110 }
111 });
112
113 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
114
115 // Mutating editor 1 will emit an `Edited` event only for that editor.
116 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
117 assert_eq!(
118 mem::take(&mut *events.borrow_mut()),
119 [
120 ("editor1", "edited"),
121 ("editor1", "buffer edited"),
122 ("editor2", "buffer edited"),
123 ]
124 );
125
126 // Mutating editor 2 will emit an `Edited` event only for that editor.
127 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
128 assert_eq!(
129 mem::take(&mut *events.borrow_mut()),
130 [
131 ("editor2", "edited"),
132 ("editor1", "buffer edited"),
133 ("editor2", "buffer edited"),
134 ]
135 );
136
137 // Undoing on editor 1 will emit an `Edited` event only for that editor.
138 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
139 assert_eq!(
140 mem::take(&mut *events.borrow_mut()),
141 [
142 ("editor1", "edited"),
143 ("editor1", "buffer edited"),
144 ("editor2", "buffer edited"),
145 ]
146 );
147
148 // Redoing on editor 1 will emit an `Edited` event only for that editor.
149 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
150 assert_eq!(
151 mem::take(&mut *events.borrow_mut()),
152 [
153 ("editor1", "edited"),
154 ("editor1", "buffer edited"),
155 ("editor2", "buffer edited"),
156 ]
157 );
158
159 // Undoing on editor 2 will emit an `Edited` event only for that editor.
160 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
161 assert_eq!(
162 mem::take(&mut *events.borrow_mut()),
163 [
164 ("editor2", "edited"),
165 ("editor1", "buffer edited"),
166 ("editor2", "buffer edited"),
167 ]
168 );
169
170 // Redoing on editor 2 will emit an `Edited` event only for that editor.
171 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
172 assert_eq!(
173 mem::take(&mut *events.borrow_mut()),
174 [
175 ("editor2", "edited"),
176 ("editor1", "buffer edited"),
177 ("editor2", "buffer edited"),
178 ]
179 );
180
181 // No event is emitted when the mutation is a no-op.
182 _ = editor2.update(cx, |editor, window, cx| {
183 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
184 s.select_ranges([0..0])
185 });
186
187 editor.backspace(&Backspace, window, cx);
188 });
189 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
190}
191
192#[gpui::test]
193fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
194 init_test(cx, |_| {});
195
196 let mut now = Instant::now();
197 let group_interval = Duration::from_millis(1);
198 let buffer = cx.new(|cx| {
199 let mut buf = language::Buffer::local("123456", cx);
200 buf.set_group_interval(group_interval);
201 buf
202 });
203 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
204 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
205
206 _ = editor.update(cx, |editor, window, cx| {
207 editor.start_transaction_at(now, window, cx);
208 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
209 s.select_ranges([2..4])
210 });
211
212 editor.insert("cd", window, cx);
213 editor.end_transaction_at(now, cx);
214 assert_eq!(editor.text(cx), "12cd56");
215 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
216
217 editor.start_transaction_at(now, window, cx);
218 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
219 s.select_ranges([4..5])
220 });
221 editor.insert("e", window, cx);
222 editor.end_transaction_at(now, cx);
223 assert_eq!(editor.text(cx), "12cde6");
224 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
225
226 now += group_interval + Duration::from_millis(1);
227 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
228 s.select_ranges([2..2])
229 });
230
231 // Simulate an edit in another editor
232 buffer.update(cx, |buffer, cx| {
233 buffer.start_transaction_at(now, cx);
234 buffer.edit([(0..1, "a")], None, cx);
235 buffer.edit([(1..1, "b")], None, cx);
236 buffer.end_transaction_at(now, cx);
237 });
238
239 assert_eq!(editor.text(cx), "ab2cde6");
240 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
241
242 // Last transaction happened past the group interval in a different editor.
243 // Undo it individually and don't restore selections.
244 editor.undo(&Undo, window, cx);
245 assert_eq!(editor.text(cx), "12cde6");
246 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
247
248 // First two transactions happened within the group interval in this editor.
249 // Undo them together and restore selections.
250 editor.undo(&Undo, window, cx);
251 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
252 assert_eq!(editor.text(cx), "123456");
253 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
254
255 // Redo the first two transactions together.
256 editor.redo(&Redo, window, cx);
257 assert_eq!(editor.text(cx), "12cde6");
258 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
259
260 // Redo the last transaction on its own.
261 editor.redo(&Redo, window, cx);
262 assert_eq!(editor.text(cx), "ab2cde6");
263 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
264
265 // Test empty transactions.
266 editor.start_transaction_at(now, window, cx);
267 editor.end_transaction_at(now, cx);
268 editor.undo(&Undo, window, cx);
269 assert_eq!(editor.text(cx), "12cde6");
270 });
271}
272
273#[gpui::test]
274fn test_ime_composition(cx: &mut TestAppContext) {
275 init_test(cx, |_| {});
276
277 let buffer = cx.new(|cx| {
278 let mut buffer = language::Buffer::local("abcde", cx);
279 // Ensure automatic grouping doesn't occur.
280 buffer.set_group_interval(Duration::ZERO);
281 buffer
282 });
283
284 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
285 cx.add_window(|window, cx| {
286 let mut editor = build_editor(buffer.clone(), window, cx);
287
288 // Start a new IME composition.
289 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
290 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
291 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
292 assert_eq!(editor.text(cx), "äbcde");
293 assert_eq!(
294 editor.marked_text_ranges(cx),
295 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
296 );
297
298 // Finalize IME composition.
299 editor.replace_text_in_range(None, "ā", window, cx);
300 assert_eq!(editor.text(cx), "ābcde");
301 assert_eq!(editor.marked_text_ranges(cx), None);
302
303 // IME composition edits are grouped and are undone/redone at once.
304 editor.undo(&Default::default(), window, cx);
305 assert_eq!(editor.text(cx), "abcde");
306 assert_eq!(editor.marked_text_ranges(cx), None);
307 editor.redo(&Default::default(), window, cx);
308 assert_eq!(editor.text(cx), "ābcde");
309 assert_eq!(editor.marked_text_ranges(cx), None);
310
311 // Start a new IME composition.
312 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
313 assert_eq!(
314 editor.marked_text_ranges(cx),
315 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
316 );
317
318 // Undoing during an IME composition cancels it.
319 editor.undo(&Default::default(), window, cx);
320 assert_eq!(editor.text(cx), "ābcde");
321 assert_eq!(editor.marked_text_ranges(cx), None);
322
323 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
324 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
325 assert_eq!(editor.text(cx), "ābcdè");
326 assert_eq!(
327 editor.marked_text_ranges(cx),
328 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
329 );
330
331 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
332 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
333 assert_eq!(editor.text(cx), "ābcdę");
334 assert_eq!(editor.marked_text_ranges(cx), None);
335
336 // Start a new IME composition with multiple cursors.
337 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
338 s.select_ranges([
339 OffsetUtf16(1)..OffsetUtf16(1),
340 OffsetUtf16(3)..OffsetUtf16(3),
341 OffsetUtf16(5)..OffsetUtf16(5),
342 ])
343 });
344 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
345 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
346 assert_eq!(
347 editor.marked_text_ranges(cx),
348 Some(vec![
349 OffsetUtf16(0)..OffsetUtf16(3),
350 OffsetUtf16(4)..OffsetUtf16(7),
351 OffsetUtf16(8)..OffsetUtf16(11)
352 ])
353 );
354
355 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
356 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
357 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
358 assert_eq!(
359 editor.marked_text_ranges(cx),
360 Some(vec![
361 OffsetUtf16(1)..OffsetUtf16(2),
362 OffsetUtf16(5)..OffsetUtf16(6),
363 OffsetUtf16(9)..OffsetUtf16(10)
364 ])
365 );
366
367 // Finalize IME composition with multiple cursors.
368 editor.replace_text_in_range(Some(9..10), "2", window, cx);
369 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
370 assert_eq!(editor.marked_text_ranges(cx), None);
371
372 editor
373 });
374}
375
376#[gpui::test]
377fn test_selection_with_mouse(cx: &mut TestAppContext) {
378 init_test(cx, |_| {});
379
380 let editor = cx.add_window(|window, cx| {
381 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
382 build_editor(buffer, window, cx)
383 });
384
385 _ = editor.update(cx, |editor, window, cx| {
386 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
387 });
388 assert_eq!(
389 editor
390 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
391 .unwrap(),
392 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
393 );
394
395 _ = editor.update(cx, |editor, window, cx| {
396 editor.update_selection(
397 DisplayPoint::new(DisplayRow(3), 3),
398 0,
399 gpui::Point::<f32>::default(),
400 window,
401 cx,
402 );
403 });
404
405 assert_eq!(
406 editor
407 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
408 .unwrap(),
409 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
410 );
411
412 _ = editor.update(cx, |editor, window, cx| {
413 editor.update_selection(
414 DisplayPoint::new(DisplayRow(1), 1),
415 0,
416 gpui::Point::<f32>::default(),
417 window,
418 cx,
419 );
420 });
421
422 assert_eq!(
423 editor
424 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
425 .unwrap(),
426 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
427 );
428
429 _ = editor.update(cx, |editor, window, cx| {
430 editor.end_selection(window, cx);
431 editor.update_selection(
432 DisplayPoint::new(DisplayRow(3), 3),
433 0,
434 gpui::Point::<f32>::default(),
435 window,
436 cx,
437 );
438 });
439
440 assert_eq!(
441 editor
442 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
443 .unwrap(),
444 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
445 );
446
447 _ = editor.update(cx, |editor, window, cx| {
448 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
449 editor.update_selection(
450 DisplayPoint::new(DisplayRow(0), 0),
451 0,
452 gpui::Point::<f32>::default(),
453 window,
454 cx,
455 );
456 });
457
458 assert_eq!(
459 editor
460 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
461 .unwrap(),
462 [
463 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
464 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
465 ]
466 );
467
468 _ = editor.update(cx, |editor, window, cx| {
469 editor.end_selection(window, cx);
470 });
471
472 assert_eq!(
473 editor
474 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
475 .unwrap(),
476 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
477 );
478}
479
480#[gpui::test]
481fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
482 init_test(cx, |_| {});
483
484 let editor = cx.add_window(|window, cx| {
485 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
486 build_editor(buffer, window, cx)
487 });
488
489 _ = editor.update(cx, |editor, window, cx| {
490 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
491 });
492
493 _ = editor.update(cx, |editor, window, cx| {
494 editor.end_selection(window, cx);
495 });
496
497 _ = editor.update(cx, |editor, window, cx| {
498 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
499 });
500
501 _ = editor.update(cx, |editor, window, cx| {
502 editor.end_selection(window, cx);
503 });
504
505 assert_eq!(
506 editor
507 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
508 .unwrap(),
509 [
510 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
511 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
512 ]
513 );
514
515 _ = editor.update(cx, |editor, window, cx| {
516 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
517 });
518
519 _ = editor.update(cx, |editor, window, cx| {
520 editor.end_selection(window, cx);
521 });
522
523 assert_eq!(
524 editor
525 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
526 .unwrap(),
527 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
528 );
529}
530
531#[gpui::test]
532fn test_canceling_pending_selection(cx: &mut TestAppContext) {
533 init_test(cx, |_| {});
534
535 let editor = cx.add_window(|window, cx| {
536 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
537 build_editor(buffer, window, cx)
538 });
539
540 _ = editor.update(cx, |editor, window, cx| {
541 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
542 assert_eq!(
543 editor.selections.display_ranges(cx),
544 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
545 );
546 });
547
548 _ = editor.update(cx, |editor, window, cx| {
549 editor.update_selection(
550 DisplayPoint::new(DisplayRow(3), 3),
551 0,
552 gpui::Point::<f32>::default(),
553 window,
554 cx,
555 );
556 assert_eq!(
557 editor.selections.display_ranges(cx),
558 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
559 );
560 });
561
562 _ = editor.update(cx, |editor, window, cx| {
563 editor.cancel(&Cancel, window, cx);
564 editor.update_selection(
565 DisplayPoint::new(DisplayRow(1), 1),
566 0,
567 gpui::Point::<f32>::default(),
568 window,
569 cx,
570 );
571 assert_eq!(
572 editor.selections.display_ranges(cx),
573 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
574 );
575 });
576}
577
578#[gpui::test]
579fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
580 init_test(cx, |_| {});
581
582 let editor = cx.add_window(|window, cx| {
583 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
584 build_editor(buffer, window, cx)
585 });
586
587 _ = editor.update(cx, |editor, window, cx| {
588 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
589 assert_eq!(
590 editor.selections.display_ranges(cx),
591 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
592 );
593
594 editor.move_down(&Default::default(), window, cx);
595 assert_eq!(
596 editor.selections.display_ranges(cx),
597 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
598 );
599
600 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
601 assert_eq!(
602 editor.selections.display_ranges(cx),
603 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
604 );
605
606 editor.move_up(&Default::default(), window, cx);
607 assert_eq!(
608 editor.selections.display_ranges(cx),
609 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
610 );
611 });
612}
613
614#[gpui::test]
615fn test_clone(cx: &mut TestAppContext) {
616 init_test(cx, |_| {});
617
618 let (text, selection_ranges) = marked_text_ranges(
619 indoc! {"
620 one
621 two
622 threeˇ
623 four
624 fiveˇ
625 "},
626 true,
627 );
628
629 let editor = cx.add_window(|window, cx| {
630 let buffer = MultiBuffer::build_simple(&text, cx);
631 build_editor(buffer, window, cx)
632 });
633
634 _ = editor.update(cx, |editor, window, cx| {
635 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
636 s.select_ranges(selection_ranges.clone())
637 });
638 editor.fold_creases(
639 vec![
640 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
641 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
642 ],
643 true,
644 window,
645 cx,
646 );
647 });
648
649 let cloned_editor = editor
650 .update(cx, |editor, _, cx| {
651 cx.open_window(Default::default(), |window, cx| {
652 cx.new(|cx| editor.clone(window, cx))
653 })
654 })
655 .unwrap()
656 .unwrap();
657
658 let snapshot = editor
659 .update(cx, |e, window, cx| e.snapshot(window, cx))
660 .unwrap();
661 let cloned_snapshot = cloned_editor
662 .update(cx, |e, window, cx| e.snapshot(window, cx))
663 .unwrap();
664
665 assert_eq!(
666 cloned_editor
667 .update(cx, |e, _, cx| e.display_text(cx))
668 .unwrap(),
669 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
670 );
671 assert_eq!(
672 cloned_snapshot
673 .folds_in_range(0..text.len())
674 .collect::<Vec<_>>(),
675 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
676 );
677 assert_set_eq!(
678 cloned_editor
679 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
680 .unwrap(),
681 editor
682 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
683 .unwrap()
684 );
685 assert_set_eq!(
686 cloned_editor
687 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
688 .unwrap(),
689 editor
690 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
691 .unwrap()
692 );
693}
694
695#[gpui::test]
696async fn test_navigation_history(cx: &mut TestAppContext) {
697 init_test(cx, |_| {});
698
699 use workspace::item::Item;
700
701 let fs = FakeFs::new(cx.executor());
702 let project = Project::test(fs, [], cx).await;
703 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
704 let pane = workspace
705 .update(cx, |workspace, _, _| workspace.active_pane().clone())
706 .unwrap();
707
708 _ = workspace.update(cx, |_v, window, cx| {
709 cx.new(|cx| {
710 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
711 let mut editor = build_editor(buffer.clone(), window, cx);
712 let handle = cx.entity();
713 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
714
715 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
716 editor.nav_history.as_mut().unwrap().pop_backward(cx)
717 }
718
719 // Move the cursor a small distance.
720 // Nothing is added to the navigation history.
721 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
722 s.select_display_ranges([
723 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
724 ])
725 });
726 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
727 s.select_display_ranges([
728 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
729 ])
730 });
731 assert!(pop_history(&mut editor, cx).is_none());
732
733 // Move the cursor a large distance.
734 // The history can jump back to the previous position.
735 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
736 s.select_display_ranges([
737 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
738 ])
739 });
740 let nav_entry = pop_history(&mut editor, cx).unwrap();
741 editor.navigate(nav_entry.data.unwrap(), window, cx);
742 assert_eq!(nav_entry.item.id(), cx.entity_id());
743 assert_eq!(
744 editor.selections.display_ranges(cx),
745 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
746 );
747 assert!(pop_history(&mut editor, cx).is_none());
748
749 // Move the cursor a small distance via the mouse.
750 // Nothing is added to the navigation history.
751 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
752 editor.end_selection(window, cx);
753 assert_eq!(
754 editor.selections.display_ranges(cx),
755 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
756 );
757 assert!(pop_history(&mut editor, cx).is_none());
758
759 // Move the cursor a large distance via the mouse.
760 // The history can jump back to the previous position.
761 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
762 editor.end_selection(window, cx);
763 assert_eq!(
764 editor.selections.display_ranges(cx),
765 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
766 );
767 let nav_entry = pop_history(&mut editor, cx).unwrap();
768 editor.navigate(nav_entry.data.unwrap(), window, cx);
769 assert_eq!(nav_entry.item.id(), cx.entity_id());
770 assert_eq!(
771 editor.selections.display_ranges(cx),
772 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
773 );
774 assert!(pop_history(&mut editor, cx).is_none());
775
776 // Set scroll position to check later
777 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
778 let original_scroll_position = editor.scroll_manager.anchor();
779
780 // Jump to the end of the document and adjust scroll
781 editor.move_to_end(&MoveToEnd, window, cx);
782 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
783 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
784
785 let nav_entry = pop_history(&mut editor, cx).unwrap();
786 editor.navigate(nav_entry.data.unwrap(), window, cx);
787 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
788
789 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
790 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
791 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
792 let invalid_point = Point::new(9999, 0);
793 editor.navigate(
794 Box::new(NavigationData {
795 cursor_anchor: invalid_anchor,
796 cursor_position: invalid_point,
797 scroll_anchor: ScrollAnchor {
798 anchor: invalid_anchor,
799 offset: Default::default(),
800 },
801 scroll_top_row: invalid_point.row,
802 }),
803 window,
804 cx,
805 );
806 assert_eq!(
807 editor.selections.display_ranges(cx),
808 &[editor.max_point(cx)..editor.max_point(cx)]
809 );
810 assert_eq!(
811 editor.scroll_position(cx),
812 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
813 );
814
815 editor
816 })
817 });
818}
819
820#[gpui::test]
821fn test_cancel(cx: &mut TestAppContext) {
822 init_test(cx, |_| {});
823
824 let editor = cx.add_window(|window, cx| {
825 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
826 build_editor(buffer, window, cx)
827 });
828
829 _ = editor.update(cx, |editor, window, cx| {
830 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
831 editor.update_selection(
832 DisplayPoint::new(DisplayRow(1), 1),
833 0,
834 gpui::Point::<f32>::default(),
835 window,
836 cx,
837 );
838 editor.end_selection(window, cx);
839
840 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
841 editor.update_selection(
842 DisplayPoint::new(DisplayRow(0), 3),
843 0,
844 gpui::Point::<f32>::default(),
845 window,
846 cx,
847 );
848 editor.end_selection(window, cx);
849 assert_eq!(
850 editor.selections.display_ranges(cx),
851 [
852 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
853 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
854 ]
855 );
856 });
857
858 _ = editor.update(cx, |editor, window, cx| {
859 editor.cancel(&Cancel, window, cx);
860 assert_eq!(
861 editor.selections.display_ranges(cx),
862 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
863 );
864 });
865
866 _ = editor.update(cx, |editor, window, cx| {
867 editor.cancel(&Cancel, window, cx);
868 assert_eq!(
869 editor.selections.display_ranges(cx),
870 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
871 );
872 });
873}
874
875#[gpui::test]
876fn test_fold_action(cx: &mut TestAppContext) {
877 init_test(cx, |_| {});
878
879 let editor = cx.add_window(|window, cx| {
880 let buffer = MultiBuffer::build_simple(
881 &"
882 impl Foo {
883 // Hello!
884
885 fn a() {
886 1
887 }
888
889 fn b() {
890 2
891 }
892
893 fn c() {
894 3
895 }
896 }
897 "
898 .unindent(),
899 cx,
900 );
901 build_editor(buffer.clone(), window, cx)
902 });
903
904 _ = editor.update(cx, |editor, window, cx| {
905 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
906 s.select_display_ranges([
907 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
908 ]);
909 });
910 editor.fold(&Fold, window, cx);
911 assert_eq!(
912 editor.display_text(cx),
913 "
914 impl Foo {
915 // Hello!
916
917 fn a() {
918 1
919 }
920
921 fn b() {⋯
922 }
923
924 fn c() {⋯
925 }
926 }
927 "
928 .unindent(),
929 );
930
931 editor.fold(&Fold, window, cx);
932 assert_eq!(
933 editor.display_text(cx),
934 "
935 impl Foo {⋯
936 }
937 "
938 .unindent(),
939 );
940
941 editor.unfold_lines(&UnfoldLines, window, cx);
942 assert_eq!(
943 editor.display_text(cx),
944 "
945 impl Foo {
946 // Hello!
947
948 fn a() {
949 1
950 }
951
952 fn b() {⋯
953 }
954
955 fn c() {⋯
956 }
957 }
958 "
959 .unindent(),
960 );
961
962 editor.unfold_lines(&UnfoldLines, window, cx);
963 assert_eq!(
964 editor.display_text(cx),
965 editor.buffer.read(cx).read(cx).text()
966 );
967 });
968}
969
970#[gpui::test]
971fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
972 init_test(cx, |_| {});
973
974 let editor = cx.add_window(|window, cx| {
975 let buffer = MultiBuffer::build_simple(
976 &"
977 class Foo:
978 # Hello!
979
980 def a():
981 print(1)
982
983 def b():
984 print(2)
985
986 def c():
987 print(3)
988 "
989 .unindent(),
990 cx,
991 );
992 build_editor(buffer.clone(), window, cx)
993 });
994
995 _ = editor.update(cx, |editor, window, cx| {
996 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
997 s.select_display_ranges([
998 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
999 ]);
1000 });
1001 editor.fold(&Fold, window, cx);
1002 assert_eq!(
1003 editor.display_text(cx),
1004 "
1005 class Foo:
1006 # Hello!
1007
1008 def a():
1009 print(1)
1010
1011 def b():⋯
1012
1013 def c():⋯
1014 "
1015 .unindent(),
1016 );
1017
1018 editor.fold(&Fold, window, cx);
1019 assert_eq!(
1020 editor.display_text(cx),
1021 "
1022 class Foo:⋯
1023 "
1024 .unindent(),
1025 );
1026
1027 editor.unfold_lines(&UnfoldLines, window, cx);
1028 assert_eq!(
1029 editor.display_text(cx),
1030 "
1031 class Foo:
1032 # Hello!
1033
1034 def a():
1035 print(1)
1036
1037 def b():⋯
1038
1039 def c():⋯
1040 "
1041 .unindent(),
1042 );
1043
1044 editor.unfold_lines(&UnfoldLines, window, cx);
1045 assert_eq!(
1046 editor.display_text(cx),
1047 editor.buffer.read(cx).read(cx).text()
1048 );
1049 });
1050}
1051
1052#[gpui::test]
1053fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1054 init_test(cx, |_| {});
1055
1056 let editor = cx.add_window(|window, cx| {
1057 let buffer = MultiBuffer::build_simple(
1058 &"
1059 class Foo:
1060 # Hello!
1061
1062 def a():
1063 print(1)
1064
1065 def b():
1066 print(2)
1067
1068
1069 def c():
1070 print(3)
1071
1072
1073 "
1074 .unindent(),
1075 cx,
1076 );
1077 build_editor(buffer.clone(), window, cx)
1078 });
1079
1080 _ = editor.update(cx, |editor, window, cx| {
1081 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1082 s.select_display_ranges([
1083 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1084 ]);
1085 });
1086 editor.fold(&Fold, window, cx);
1087 assert_eq!(
1088 editor.display_text(cx),
1089 "
1090 class Foo:
1091 # Hello!
1092
1093 def a():
1094 print(1)
1095
1096 def b():⋯
1097
1098
1099 def c():⋯
1100
1101
1102 "
1103 .unindent(),
1104 );
1105
1106 editor.fold(&Fold, window, cx);
1107 assert_eq!(
1108 editor.display_text(cx),
1109 "
1110 class Foo:⋯
1111
1112
1113 "
1114 .unindent(),
1115 );
1116
1117 editor.unfold_lines(&UnfoldLines, window, cx);
1118 assert_eq!(
1119 editor.display_text(cx),
1120 "
1121 class Foo:
1122 # Hello!
1123
1124 def a():
1125 print(1)
1126
1127 def b():⋯
1128
1129
1130 def c():⋯
1131
1132
1133 "
1134 .unindent(),
1135 );
1136
1137 editor.unfold_lines(&UnfoldLines, window, cx);
1138 assert_eq!(
1139 editor.display_text(cx),
1140 editor.buffer.read(cx).read(cx).text()
1141 );
1142 });
1143}
1144
1145#[gpui::test]
1146fn test_fold_at_level(cx: &mut TestAppContext) {
1147 init_test(cx, |_| {});
1148
1149 let editor = cx.add_window(|window, cx| {
1150 let buffer = MultiBuffer::build_simple(
1151 &"
1152 class Foo:
1153 # Hello!
1154
1155 def a():
1156 print(1)
1157
1158 def b():
1159 print(2)
1160
1161
1162 class Bar:
1163 # World!
1164
1165 def a():
1166 print(1)
1167
1168 def b():
1169 print(2)
1170
1171
1172 "
1173 .unindent(),
1174 cx,
1175 );
1176 build_editor(buffer.clone(), window, cx)
1177 });
1178
1179 _ = editor.update(cx, |editor, window, cx| {
1180 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1181 assert_eq!(
1182 editor.display_text(cx),
1183 "
1184 class Foo:
1185 # Hello!
1186
1187 def a():⋯
1188
1189 def b():⋯
1190
1191
1192 class Bar:
1193 # World!
1194
1195 def a():⋯
1196
1197 def b():⋯
1198
1199
1200 "
1201 .unindent(),
1202 );
1203
1204 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1205 assert_eq!(
1206 editor.display_text(cx),
1207 "
1208 class Foo:⋯
1209
1210
1211 class Bar:⋯
1212
1213
1214 "
1215 .unindent(),
1216 );
1217
1218 editor.unfold_all(&UnfoldAll, window, cx);
1219 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1220 assert_eq!(
1221 editor.display_text(cx),
1222 "
1223 class Foo:
1224 # Hello!
1225
1226 def a():
1227 print(1)
1228
1229 def b():
1230 print(2)
1231
1232
1233 class Bar:
1234 # World!
1235
1236 def a():
1237 print(1)
1238
1239 def b():
1240 print(2)
1241
1242
1243 "
1244 .unindent(),
1245 );
1246
1247 assert_eq!(
1248 editor.display_text(cx),
1249 editor.buffer.read(cx).read(cx).text()
1250 );
1251 });
1252}
1253
1254#[gpui::test]
1255fn test_move_cursor(cx: &mut TestAppContext) {
1256 init_test(cx, |_| {});
1257
1258 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1259 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1260
1261 buffer.update(cx, |buffer, cx| {
1262 buffer.edit(
1263 vec![
1264 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1265 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1266 ],
1267 None,
1268 cx,
1269 );
1270 });
1271 _ = editor.update(cx, |editor, window, cx| {
1272 assert_eq!(
1273 editor.selections.display_ranges(cx),
1274 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1275 );
1276
1277 editor.move_down(&MoveDown, window, cx);
1278 assert_eq!(
1279 editor.selections.display_ranges(cx),
1280 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1281 );
1282
1283 editor.move_right(&MoveRight, window, cx);
1284 assert_eq!(
1285 editor.selections.display_ranges(cx),
1286 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1287 );
1288
1289 editor.move_left(&MoveLeft, window, cx);
1290 assert_eq!(
1291 editor.selections.display_ranges(cx),
1292 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1293 );
1294
1295 editor.move_up(&MoveUp, window, cx);
1296 assert_eq!(
1297 editor.selections.display_ranges(cx),
1298 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1299 );
1300
1301 editor.move_to_end(&MoveToEnd, window, cx);
1302 assert_eq!(
1303 editor.selections.display_ranges(cx),
1304 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1305 );
1306
1307 editor.move_to_beginning(&MoveToBeginning, window, cx);
1308 assert_eq!(
1309 editor.selections.display_ranges(cx),
1310 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1311 );
1312
1313 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1314 s.select_display_ranges([
1315 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1316 ]);
1317 });
1318 editor.select_to_beginning(&SelectToBeginning, window, cx);
1319 assert_eq!(
1320 editor.selections.display_ranges(cx),
1321 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1322 );
1323
1324 editor.select_to_end(&SelectToEnd, window, cx);
1325 assert_eq!(
1326 editor.selections.display_ranges(cx),
1327 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1328 );
1329 });
1330}
1331
1332#[gpui::test]
1333fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1334 init_test(cx, |_| {});
1335
1336 let editor = cx.add_window(|window, cx| {
1337 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1338 build_editor(buffer.clone(), window, cx)
1339 });
1340
1341 assert_eq!('🟥'.len_utf8(), 4);
1342 assert_eq!('α'.len_utf8(), 2);
1343
1344 _ = editor.update(cx, |editor, window, cx| {
1345 editor.fold_creases(
1346 vec![
1347 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1348 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1349 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1350 ],
1351 true,
1352 window,
1353 cx,
1354 );
1355 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1356
1357 editor.move_right(&MoveRight, window, cx);
1358 assert_eq!(
1359 editor.selections.display_ranges(cx),
1360 &[empty_range(0, "🟥".len())]
1361 );
1362 editor.move_right(&MoveRight, window, cx);
1363 assert_eq!(
1364 editor.selections.display_ranges(cx),
1365 &[empty_range(0, "🟥🟧".len())]
1366 );
1367 editor.move_right(&MoveRight, window, cx);
1368 assert_eq!(
1369 editor.selections.display_ranges(cx),
1370 &[empty_range(0, "🟥🟧⋯".len())]
1371 );
1372
1373 editor.move_down(&MoveDown, window, cx);
1374 assert_eq!(
1375 editor.selections.display_ranges(cx),
1376 &[empty_range(1, "ab⋯e".len())]
1377 );
1378 editor.move_left(&MoveLeft, window, cx);
1379 assert_eq!(
1380 editor.selections.display_ranges(cx),
1381 &[empty_range(1, "ab⋯".len())]
1382 );
1383 editor.move_left(&MoveLeft, window, cx);
1384 assert_eq!(
1385 editor.selections.display_ranges(cx),
1386 &[empty_range(1, "ab".len())]
1387 );
1388 editor.move_left(&MoveLeft, window, cx);
1389 assert_eq!(
1390 editor.selections.display_ranges(cx),
1391 &[empty_range(1, "a".len())]
1392 );
1393
1394 editor.move_down(&MoveDown, window, cx);
1395 assert_eq!(
1396 editor.selections.display_ranges(cx),
1397 &[empty_range(2, "α".len())]
1398 );
1399 editor.move_right(&MoveRight, window, cx);
1400 assert_eq!(
1401 editor.selections.display_ranges(cx),
1402 &[empty_range(2, "αβ".len())]
1403 );
1404 editor.move_right(&MoveRight, window, cx);
1405 assert_eq!(
1406 editor.selections.display_ranges(cx),
1407 &[empty_range(2, "αβ⋯".len())]
1408 );
1409 editor.move_right(&MoveRight, window, cx);
1410 assert_eq!(
1411 editor.selections.display_ranges(cx),
1412 &[empty_range(2, "αβ⋯ε".len())]
1413 );
1414
1415 editor.move_up(&MoveUp, window, cx);
1416 assert_eq!(
1417 editor.selections.display_ranges(cx),
1418 &[empty_range(1, "ab⋯e".len())]
1419 );
1420 editor.move_down(&MoveDown, window, cx);
1421 assert_eq!(
1422 editor.selections.display_ranges(cx),
1423 &[empty_range(2, "αβ⋯ε".len())]
1424 );
1425 editor.move_up(&MoveUp, window, cx);
1426 assert_eq!(
1427 editor.selections.display_ranges(cx),
1428 &[empty_range(1, "ab⋯e".len())]
1429 );
1430
1431 editor.move_up(&MoveUp, window, cx);
1432 assert_eq!(
1433 editor.selections.display_ranges(cx),
1434 &[empty_range(0, "🟥🟧".len())]
1435 );
1436 editor.move_left(&MoveLeft, window, cx);
1437 assert_eq!(
1438 editor.selections.display_ranges(cx),
1439 &[empty_range(0, "🟥".len())]
1440 );
1441 editor.move_left(&MoveLeft, window, cx);
1442 assert_eq!(
1443 editor.selections.display_ranges(cx),
1444 &[empty_range(0, "".len())]
1445 );
1446 });
1447}
1448
1449#[gpui::test]
1450fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1451 init_test(cx, |_| {});
1452
1453 let editor = cx.add_window(|window, cx| {
1454 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1455 build_editor(buffer.clone(), window, cx)
1456 });
1457 _ = editor.update(cx, |editor, window, cx| {
1458 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1459 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1460 });
1461
1462 // moving above start of document should move selection to start of document,
1463 // but the next move down should still be at the original goal_x
1464 editor.move_up(&MoveUp, window, cx);
1465 assert_eq!(
1466 editor.selections.display_ranges(cx),
1467 &[empty_range(0, "".len())]
1468 );
1469
1470 editor.move_down(&MoveDown, window, cx);
1471 assert_eq!(
1472 editor.selections.display_ranges(cx),
1473 &[empty_range(1, "abcd".len())]
1474 );
1475
1476 editor.move_down(&MoveDown, window, cx);
1477 assert_eq!(
1478 editor.selections.display_ranges(cx),
1479 &[empty_range(2, "αβγ".len())]
1480 );
1481
1482 editor.move_down(&MoveDown, window, cx);
1483 assert_eq!(
1484 editor.selections.display_ranges(cx),
1485 &[empty_range(3, "abcd".len())]
1486 );
1487
1488 editor.move_down(&MoveDown, window, cx);
1489 assert_eq!(
1490 editor.selections.display_ranges(cx),
1491 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1492 );
1493
1494 // moving past end of document should not change goal_x
1495 editor.move_down(&MoveDown, window, cx);
1496 assert_eq!(
1497 editor.selections.display_ranges(cx),
1498 &[empty_range(5, "".len())]
1499 );
1500
1501 editor.move_down(&MoveDown, window, cx);
1502 assert_eq!(
1503 editor.selections.display_ranges(cx),
1504 &[empty_range(5, "".len())]
1505 );
1506
1507 editor.move_up(&MoveUp, window, cx);
1508 assert_eq!(
1509 editor.selections.display_ranges(cx),
1510 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1511 );
1512
1513 editor.move_up(&MoveUp, window, cx);
1514 assert_eq!(
1515 editor.selections.display_ranges(cx),
1516 &[empty_range(3, "abcd".len())]
1517 );
1518
1519 editor.move_up(&MoveUp, window, cx);
1520 assert_eq!(
1521 editor.selections.display_ranges(cx),
1522 &[empty_range(2, "αβγ".len())]
1523 );
1524 });
1525}
1526
1527#[gpui::test]
1528fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1529 init_test(cx, |_| {});
1530 let move_to_beg = MoveToBeginningOfLine {
1531 stop_at_soft_wraps: true,
1532 stop_at_indent: true,
1533 };
1534
1535 let delete_to_beg = DeleteToBeginningOfLine {
1536 stop_at_indent: false,
1537 };
1538
1539 let move_to_end = MoveToEndOfLine {
1540 stop_at_soft_wraps: true,
1541 };
1542
1543 let editor = cx.add_window(|window, cx| {
1544 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1545 build_editor(buffer, window, cx)
1546 });
1547 _ = editor.update(cx, |editor, window, cx| {
1548 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1549 s.select_display_ranges([
1550 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1551 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1552 ]);
1553 });
1554 });
1555
1556 _ = editor.update(cx, |editor, window, cx| {
1557 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1558 assert_eq!(
1559 editor.selections.display_ranges(cx),
1560 &[
1561 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1562 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1563 ]
1564 );
1565 });
1566
1567 _ = editor.update(cx, |editor, window, cx| {
1568 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1569 assert_eq!(
1570 editor.selections.display_ranges(cx),
1571 &[
1572 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1573 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1574 ]
1575 );
1576 });
1577
1578 _ = editor.update(cx, |editor, window, cx| {
1579 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1580 assert_eq!(
1581 editor.selections.display_ranges(cx),
1582 &[
1583 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1584 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1585 ]
1586 );
1587 });
1588
1589 _ = editor.update(cx, |editor, window, cx| {
1590 editor.move_to_end_of_line(&move_to_end, window, cx);
1591 assert_eq!(
1592 editor.selections.display_ranges(cx),
1593 &[
1594 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1595 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1596 ]
1597 );
1598 });
1599
1600 // Moving to the end of line again is a no-op.
1601 _ = editor.update(cx, |editor, window, cx| {
1602 editor.move_to_end_of_line(&move_to_end, window, cx);
1603 assert_eq!(
1604 editor.selections.display_ranges(cx),
1605 &[
1606 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1607 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1608 ]
1609 );
1610 });
1611
1612 _ = editor.update(cx, |editor, window, cx| {
1613 editor.move_left(&MoveLeft, window, cx);
1614 editor.select_to_beginning_of_line(
1615 &SelectToBeginningOfLine {
1616 stop_at_soft_wraps: true,
1617 stop_at_indent: true,
1618 },
1619 window,
1620 cx,
1621 );
1622 assert_eq!(
1623 editor.selections.display_ranges(cx),
1624 &[
1625 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1626 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1627 ]
1628 );
1629 });
1630
1631 _ = editor.update(cx, |editor, window, cx| {
1632 editor.select_to_beginning_of_line(
1633 &SelectToBeginningOfLine {
1634 stop_at_soft_wraps: true,
1635 stop_at_indent: true,
1636 },
1637 window,
1638 cx,
1639 );
1640 assert_eq!(
1641 editor.selections.display_ranges(cx),
1642 &[
1643 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1644 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1645 ]
1646 );
1647 });
1648
1649 _ = editor.update(cx, |editor, window, cx| {
1650 editor.select_to_beginning_of_line(
1651 &SelectToBeginningOfLine {
1652 stop_at_soft_wraps: true,
1653 stop_at_indent: true,
1654 },
1655 window,
1656 cx,
1657 );
1658 assert_eq!(
1659 editor.selections.display_ranges(cx),
1660 &[
1661 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1662 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1663 ]
1664 );
1665 });
1666
1667 _ = editor.update(cx, |editor, window, cx| {
1668 editor.select_to_end_of_line(
1669 &SelectToEndOfLine {
1670 stop_at_soft_wraps: true,
1671 },
1672 window,
1673 cx,
1674 );
1675 assert_eq!(
1676 editor.selections.display_ranges(cx),
1677 &[
1678 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1679 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1680 ]
1681 );
1682 });
1683
1684 _ = editor.update(cx, |editor, window, cx| {
1685 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1686 assert_eq!(editor.display_text(cx), "ab\n de");
1687 assert_eq!(
1688 editor.selections.display_ranges(cx),
1689 &[
1690 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1691 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1692 ]
1693 );
1694 });
1695
1696 _ = editor.update(cx, |editor, window, cx| {
1697 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1698 assert_eq!(editor.display_text(cx), "\n");
1699 assert_eq!(
1700 editor.selections.display_ranges(cx),
1701 &[
1702 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1703 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1704 ]
1705 );
1706 });
1707}
1708
1709#[gpui::test]
1710fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1711 init_test(cx, |_| {});
1712 let move_to_beg = MoveToBeginningOfLine {
1713 stop_at_soft_wraps: false,
1714 stop_at_indent: false,
1715 };
1716
1717 let move_to_end = MoveToEndOfLine {
1718 stop_at_soft_wraps: false,
1719 };
1720
1721 let editor = cx.add_window(|window, cx| {
1722 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1723 build_editor(buffer, window, cx)
1724 });
1725
1726 _ = editor.update(cx, |editor, window, cx| {
1727 editor.set_wrap_width(Some(140.0.into()), cx);
1728
1729 // We expect the following lines after wrapping
1730 // ```
1731 // thequickbrownfox
1732 // jumpedoverthelazydo
1733 // gs
1734 // ```
1735 // The final `gs` was soft-wrapped onto a new line.
1736 assert_eq!(
1737 "thequickbrownfox\njumpedoverthelaz\nydogs",
1738 editor.display_text(cx),
1739 );
1740
1741 // First, let's assert behavior on the first line, that was not soft-wrapped.
1742 // Start the cursor at the `k` on the first line
1743 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1744 s.select_display_ranges([
1745 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1746 ]);
1747 });
1748
1749 // Moving to the beginning of the line should put us at the beginning of the line.
1750 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1751 assert_eq!(
1752 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1753 editor.selections.display_ranges(cx)
1754 );
1755
1756 // Moving to the end of the line should put us at the end of the line.
1757 editor.move_to_end_of_line(&move_to_end, window, cx);
1758 assert_eq!(
1759 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1760 editor.selections.display_ranges(cx)
1761 );
1762
1763 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1764 // Start the cursor at the last line (`y` that was wrapped to a new line)
1765 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1766 s.select_display_ranges([
1767 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1768 ]);
1769 });
1770
1771 // Moving to the beginning of the line should put us at the start of the second line of
1772 // display text, i.e., the `j`.
1773 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1774 assert_eq!(
1775 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1776 editor.selections.display_ranges(cx)
1777 );
1778
1779 // Moving to the beginning of the line again should be a no-op.
1780 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1781 assert_eq!(
1782 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1783 editor.selections.display_ranges(cx)
1784 );
1785
1786 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1787 // next display line.
1788 editor.move_to_end_of_line(&move_to_end, window, cx);
1789 assert_eq!(
1790 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1791 editor.selections.display_ranges(cx)
1792 );
1793
1794 // Moving to the end of the line again should be a no-op.
1795 editor.move_to_end_of_line(&move_to_end, window, cx);
1796 assert_eq!(
1797 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1798 editor.selections.display_ranges(cx)
1799 );
1800 });
1801}
1802
1803#[gpui::test]
1804fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1805 init_test(cx, |_| {});
1806
1807 let move_to_beg = MoveToBeginningOfLine {
1808 stop_at_soft_wraps: true,
1809 stop_at_indent: true,
1810 };
1811
1812 let select_to_beg = SelectToBeginningOfLine {
1813 stop_at_soft_wraps: true,
1814 stop_at_indent: true,
1815 };
1816
1817 let delete_to_beg = DeleteToBeginningOfLine {
1818 stop_at_indent: true,
1819 };
1820
1821 let move_to_end = MoveToEndOfLine {
1822 stop_at_soft_wraps: false,
1823 };
1824
1825 let editor = cx.add_window(|window, cx| {
1826 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1827 build_editor(buffer, window, cx)
1828 });
1829
1830 _ = editor.update(cx, |editor, window, cx| {
1831 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1832 s.select_display_ranges([
1833 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1834 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1835 ]);
1836 });
1837
1838 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1839 // and the second cursor at the first non-whitespace character in the line.
1840 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1841 assert_eq!(
1842 editor.selections.display_ranges(cx),
1843 &[
1844 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1845 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1846 ]
1847 );
1848
1849 // Moving to the beginning of the line again should be a no-op for the first cursor,
1850 // and should move the second cursor to the beginning of the line.
1851 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1852 assert_eq!(
1853 editor.selections.display_ranges(cx),
1854 &[
1855 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1856 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1857 ]
1858 );
1859
1860 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1861 // and should move the second cursor back to the first non-whitespace character in the line.
1862 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1863 assert_eq!(
1864 editor.selections.display_ranges(cx),
1865 &[
1866 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1867 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1868 ]
1869 );
1870
1871 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1872 // and to the first non-whitespace character in the line for the second cursor.
1873 editor.move_to_end_of_line(&move_to_end, window, cx);
1874 editor.move_left(&MoveLeft, window, cx);
1875 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1876 assert_eq!(
1877 editor.selections.display_ranges(cx),
1878 &[
1879 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1880 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1881 ]
1882 );
1883
1884 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1885 // and should select to the beginning of the line for the second cursor.
1886 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1887 assert_eq!(
1888 editor.selections.display_ranges(cx),
1889 &[
1890 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1891 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1892 ]
1893 );
1894
1895 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1896 // and should delete to the first non-whitespace character in the line for the second cursor.
1897 editor.move_to_end_of_line(&move_to_end, window, cx);
1898 editor.move_left(&MoveLeft, window, cx);
1899 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1900 assert_eq!(editor.text(cx), "c\n f");
1901 });
1902}
1903
1904#[gpui::test]
1905fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1906 init_test(cx, |_| {});
1907
1908 let editor = cx.add_window(|window, cx| {
1909 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1910 build_editor(buffer, window, cx)
1911 });
1912 _ = editor.update(cx, |editor, window, cx| {
1913 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1914 s.select_display_ranges([
1915 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1916 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1917 ])
1918 });
1919 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1920 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1921
1922 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1923 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1924
1925 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1926 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1927
1928 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1929 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1930
1931 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1932 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
1933
1934 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1935 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1936
1937 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1938 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1939
1940 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1941 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1942
1943 editor.move_right(&MoveRight, window, cx);
1944 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1945 assert_selection_ranges(
1946 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
1947 editor,
1948 cx,
1949 );
1950
1951 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1952 assert_selection_ranges(
1953 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
1954 editor,
1955 cx,
1956 );
1957
1958 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1959 assert_selection_ranges(
1960 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
1961 editor,
1962 cx,
1963 );
1964 });
1965}
1966
1967#[gpui::test]
1968fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1969 init_test(cx, |_| {});
1970
1971 let editor = cx.add_window(|window, cx| {
1972 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1973 build_editor(buffer, window, cx)
1974 });
1975
1976 _ = editor.update(cx, |editor, window, cx| {
1977 editor.set_wrap_width(Some(140.0.into()), cx);
1978 assert_eq!(
1979 editor.display_text(cx),
1980 "use one::{\n two::three::\n four::five\n};"
1981 );
1982
1983 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1984 s.select_display_ranges([
1985 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1986 ]);
1987 });
1988
1989 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1990 assert_eq!(
1991 editor.selections.display_ranges(cx),
1992 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1993 );
1994
1995 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1996 assert_eq!(
1997 editor.selections.display_ranges(cx),
1998 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1999 );
2000
2001 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2002 assert_eq!(
2003 editor.selections.display_ranges(cx),
2004 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2005 );
2006
2007 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2008 assert_eq!(
2009 editor.selections.display_ranges(cx),
2010 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2011 );
2012
2013 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2014 assert_eq!(
2015 editor.selections.display_ranges(cx),
2016 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2017 );
2018
2019 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2020 assert_eq!(
2021 editor.selections.display_ranges(cx),
2022 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2023 );
2024 });
2025}
2026
2027#[gpui::test]
2028async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2029 init_test(cx, |_| {});
2030 let mut cx = EditorTestContext::new(cx).await;
2031
2032 let line_height = cx.editor(|editor, window, _| {
2033 editor
2034 .style()
2035 .unwrap()
2036 .text
2037 .line_height_in_pixels(window.rem_size())
2038 });
2039 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2040
2041 cx.set_state(
2042 &r#"ˇone
2043 two
2044
2045 three
2046 fourˇ
2047 five
2048
2049 six"#
2050 .unindent(),
2051 );
2052
2053 cx.update_editor(|editor, window, cx| {
2054 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2055 });
2056 cx.assert_editor_state(
2057 &r#"one
2058 two
2059 ˇ
2060 three
2061 four
2062 five
2063 ˇ
2064 six"#
2065 .unindent(),
2066 );
2067
2068 cx.update_editor(|editor, window, cx| {
2069 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2070 });
2071 cx.assert_editor_state(
2072 &r#"one
2073 two
2074
2075 three
2076 four
2077 five
2078 ˇ
2079 sixˇ"#
2080 .unindent(),
2081 );
2082
2083 cx.update_editor(|editor, window, cx| {
2084 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2085 });
2086 cx.assert_editor_state(
2087 &r#"one
2088 two
2089
2090 three
2091 four
2092 five
2093
2094 sixˇ"#
2095 .unindent(),
2096 );
2097
2098 cx.update_editor(|editor, window, cx| {
2099 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2100 });
2101 cx.assert_editor_state(
2102 &r#"one
2103 two
2104
2105 three
2106 four
2107 five
2108 ˇ
2109 six"#
2110 .unindent(),
2111 );
2112
2113 cx.update_editor(|editor, window, cx| {
2114 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2115 });
2116 cx.assert_editor_state(
2117 &r#"one
2118 two
2119 ˇ
2120 three
2121 four
2122 five
2123
2124 six"#
2125 .unindent(),
2126 );
2127
2128 cx.update_editor(|editor, window, cx| {
2129 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2130 });
2131 cx.assert_editor_state(
2132 &r#"ˇone
2133 two
2134
2135 three
2136 four
2137 five
2138
2139 six"#
2140 .unindent(),
2141 );
2142}
2143
2144#[gpui::test]
2145async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2146 init_test(cx, |_| {});
2147 let mut cx = EditorTestContext::new(cx).await;
2148 let line_height = cx.editor(|editor, window, _| {
2149 editor
2150 .style()
2151 .unwrap()
2152 .text
2153 .line_height_in_pixels(window.rem_size())
2154 });
2155 let window = cx.window;
2156 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2157
2158 cx.set_state(
2159 r#"ˇone
2160 two
2161 three
2162 four
2163 five
2164 six
2165 seven
2166 eight
2167 nine
2168 ten
2169 "#,
2170 );
2171
2172 cx.update_editor(|editor, window, cx| {
2173 assert_eq!(
2174 editor.snapshot(window, cx).scroll_position(),
2175 gpui::Point::new(0., 0.)
2176 );
2177 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2178 assert_eq!(
2179 editor.snapshot(window, cx).scroll_position(),
2180 gpui::Point::new(0., 3.)
2181 );
2182 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2183 assert_eq!(
2184 editor.snapshot(window, cx).scroll_position(),
2185 gpui::Point::new(0., 6.)
2186 );
2187 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2188 assert_eq!(
2189 editor.snapshot(window, cx).scroll_position(),
2190 gpui::Point::new(0., 3.)
2191 );
2192
2193 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2194 assert_eq!(
2195 editor.snapshot(window, cx).scroll_position(),
2196 gpui::Point::new(0., 1.)
2197 );
2198 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2199 assert_eq!(
2200 editor.snapshot(window, cx).scroll_position(),
2201 gpui::Point::new(0., 3.)
2202 );
2203 });
2204}
2205
2206#[gpui::test]
2207async fn test_autoscroll(cx: &mut TestAppContext) {
2208 init_test(cx, |_| {});
2209 let mut cx = EditorTestContext::new(cx).await;
2210
2211 let line_height = cx.update_editor(|editor, window, cx| {
2212 editor.set_vertical_scroll_margin(2, cx);
2213 editor
2214 .style()
2215 .unwrap()
2216 .text
2217 .line_height_in_pixels(window.rem_size())
2218 });
2219 let window = cx.window;
2220 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2221
2222 cx.set_state(
2223 r#"ˇone
2224 two
2225 three
2226 four
2227 five
2228 six
2229 seven
2230 eight
2231 nine
2232 ten
2233 "#,
2234 );
2235 cx.update_editor(|editor, window, cx| {
2236 assert_eq!(
2237 editor.snapshot(window, cx).scroll_position(),
2238 gpui::Point::new(0., 0.0)
2239 );
2240 });
2241
2242 // Add a cursor below the visible area. Since both cursors cannot fit
2243 // on screen, the editor autoscrolls to reveal the newest cursor, and
2244 // allows the vertical scroll margin below that cursor.
2245 cx.update_editor(|editor, window, cx| {
2246 editor.change_selections(Default::default(), window, cx, |selections| {
2247 selections.select_ranges([
2248 Point::new(0, 0)..Point::new(0, 0),
2249 Point::new(6, 0)..Point::new(6, 0),
2250 ]);
2251 })
2252 });
2253 cx.update_editor(|editor, window, cx| {
2254 assert_eq!(
2255 editor.snapshot(window, cx).scroll_position(),
2256 gpui::Point::new(0., 3.0)
2257 );
2258 });
2259
2260 // Move down. The editor cursor scrolls down to track the newest cursor.
2261 cx.update_editor(|editor, window, cx| {
2262 editor.move_down(&Default::default(), window, cx);
2263 });
2264 cx.update_editor(|editor, window, cx| {
2265 assert_eq!(
2266 editor.snapshot(window, cx).scroll_position(),
2267 gpui::Point::new(0., 4.0)
2268 );
2269 });
2270
2271 // Add a cursor above the visible area. Since both cursors fit on screen,
2272 // the editor scrolls to show both.
2273 cx.update_editor(|editor, window, cx| {
2274 editor.change_selections(Default::default(), window, cx, |selections| {
2275 selections.select_ranges([
2276 Point::new(1, 0)..Point::new(1, 0),
2277 Point::new(6, 0)..Point::new(6, 0),
2278 ]);
2279 })
2280 });
2281 cx.update_editor(|editor, window, cx| {
2282 assert_eq!(
2283 editor.snapshot(window, cx).scroll_position(),
2284 gpui::Point::new(0., 1.0)
2285 );
2286 });
2287}
2288
2289#[gpui::test]
2290async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2291 init_test(cx, |_| {});
2292 let mut cx = EditorTestContext::new(cx).await;
2293
2294 let line_height = cx.editor(|editor, window, _cx| {
2295 editor
2296 .style()
2297 .unwrap()
2298 .text
2299 .line_height_in_pixels(window.rem_size())
2300 });
2301 let window = cx.window;
2302 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2303 cx.set_state(
2304 &r#"
2305 ˇone
2306 two
2307 threeˇ
2308 four
2309 five
2310 six
2311 seven
2312 eight
2313 nine
2314 ten
2315 "#
2316 .unindent(),
2317 );
2318
2319 cx.update_editor(|editor, window, cx| {
2320 editor.move_page_down(&MovePageDown::default(), window, cx)
2321 });
2322 cx.assert_editor_state(
2323 &r#"
2324 one
2325 two
2326 three
2327 ˇfour
2328 five
2329 sixˇ
2330 seven
2331 eight
2332 nine
2333 ten
2334 "#
2335 .unindent(),
2336 );
2337
2338 cx.update_editor(|editor, window, cx| {
2339 editor.move_page_down(&MovePageDown::default(), window, cx)
2340 });
2341 cx.assert_editor_state(
2342 &r#"
2343 one
2344 two
2345 three
2346 four
2347 five
2348 six
2349 ˇseven
2350 eight
2351 nineˇ
2352 ten
2353 "#
2354 .unindent(),
2355 );
2356
2357 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2358 cx.assert_editor_state(
2359 &r#"
2360 one
2361 two
2362 three
2363 ˇfour
2364 five
2365 sixˇ
2366 seven
2367 eight
2368 nine
2369 ten
2370 "#
2371 .unindent(),
2372 );
2373
2374 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2375 cx.assert_editor_state(
2376 &r#"
2377 ˇone
2378 two
2379 threeˇ
2380 four
2381 five
2382 six
2383 seven
2384 eight
2385 nine
2386 ten
2387 "#
2388 .unindent(),
2389 );
2390
2391 // Test select collapsing
2392 cx.update_editor(|editor, window, cx| {
2393 editor.move_page_down(&MovePageDown::default(), window, cx);
2394 editor.move_page_down(&MovePageDown::default(), window, cx);
2395 editor.move_page_down(&MovePageDown::default(), window, cx);
2396 });
2397 cx.assert_editor_state(
2398 &r#"
2399 one
2400 two
2401 three
2402 four
2403 five
2404 six
2405 seven
2406 eight
2407 nine
2408 ˇten
2409 ˇ"#
2410 .unindent(),
2411 );
2412}
2413
2414#[gpui::test]
2415async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2416 init_test(cx, |_| {});
2417 let mut cx = EditorTestContext::new(cx).await;
2418 cx.set_state("one «two threeˇ» four");
2419 cx.update_editor(|editor, window, cx| {
2420 editor.delete_to_beginning_of_line(
2421 &DeleteToBeginningOfLine {
2422 stop_at_indent: false,
2423 },
2424 window,
2425 cx,
2426 );
2427 assert_eq!(editor.text(cx), " four");
2428 });
2429}
2430
2431#[gpui::test]
2432fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2433 init_test(cx, |_| {});
2434
2435 let editor = cx.add_window(|window, cx| {
2436 let buffer = MultiBuffer::build_simple("one two three four", cx);
2437 build_editor(buffer.clone(), window, cx)
2438 });
2439
2440 _ = editor.update(cx, |editor, window, cx| {
2441 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2442 s.select_display_ranges([
2443 // an empty selection - the preceding word fragment is deleted
2444 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2445 // characters selected - they are deleted
2446 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2447 ])
2448 });
2449 editor.delete_to_previous_word_start(
2450 &DeleteToPreviousWordStart {
2451 ignore_newlines: false,
2452 },
2453 window,
2454 cx,
2455 );
2456 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2457 });
2458
2459 _ = editor.update(cx, |editor, window, cx| {
2460 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2461 s.select_display_ranges([
2462 // an empty selection - the following word fragment is deleted
2463 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2464 // characters selected - they are deleted
2465 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2466 ])
2467 });
2468 editor.delete_to_next_word_end(
2469 &DeleteToNextWordEnd {
2470 ignore_newlines: false,
2471 },
2472 window,
2473 cx,
2474 );
2475 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2476 });
2477}
2478
2479#[gpui::test]
2480fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2481 init_test(cx, |_| {});
2482
2483 let editor = cx.add_window(|window, cx| {
2484 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2485 build_editor(buffer.clone(), window, cx)
2486 });
2487 let del_to_prev_word_start = DeleteToPreviousWordStart {
2488 ignore_newlines: false,
2489 };
2490 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2491 ignore_newlines: true,
2492 };
2493
2494 _ = editor.update(cx, |editor, window, cx| {
2495 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2496 s.select_display_ranges([
2497 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2498 ])
2499 });
2500 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2501 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2502 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2503 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2504 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2505 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2506 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2507 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2508 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2509 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2510 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2511 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2512 });
2513}
2514
2515#[gpui::test]
2516fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2517 init_test(cx, |_| {});
2518
2519 let editor = cx.add_window(|window, cx| {
2520 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2521 build_editor(buffer.clone(), window, cx)
2522 });
2523 let del_to_next_word_end = DeleteToNextWordEnd {
2524 ignore_newlines: false,
2525 };
2526 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2527 ignore_newlines: true,
2528 };
2529
2530 _ = editor.update(cx, |editor, window, cx| {
2531 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2532 s.select_display_ranges([
2533 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2534 ])
2535 });
2536 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2537 assert_eq!(
2538 editor.buffer.read(cx).read(cx).text(),
2539 "one\n two\nthree\n four"
2540 );
2541 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2542 assert_eq!(
2543 editor.buffer.read(cx).read(cx).text(),
2544 "\n two\nthree\n four"
2545 );
2546 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2547 assert_eq!(
2548 editor.buffer.read(cx).read(cx).text(),
2549 "two\nthree\n four"
2550 );
2551 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2552 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2553 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2554 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2555 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2556 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2557 });
2558}
2559
2560#[gpui::test]
2561fn test_newline(cx: &mut TestAppContext) {
2562 init_test(cx, |_| {});
2563
2564 let editor = cx.add_window(|window, cx| {
2565 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2566 build_editor(buffer.clone(), window, cx)
2567 });
2568
2569 _ = editor.update(cx, |editor, window, cx| {
2570 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2571 s.select_display_ranges([
2572 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2573 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2574 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2575 ])
2576 });
2577
2578 editor.newline(&Newline, window, cx);
2579 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2580 });
2581}
2582
2583#[gpui::test]
2584fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2585 init_test(cx, |_| {});
2586
2587 let editor = cx.add_window(|window, cx| {
2588 let buffer = MultiBuffer::build_simple(
2589 "
2590 a
2591 b(
2592 X
2593 )
2594 c(
2595 X
2596 )
2597 "
2598 .unindent()
2599 .as_str(),
2600 cx,
2601 );
2602 let mut editor = build_editor(buffer.clone(), window, cx);
2603 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2604 s.select_ranges([
2605 Point::new(2, 4)..Point::new(2, 5),
2606 Point::new(5, 4)..Point::new(5, 5),
2607 ])
2608 });
2609 editor
2610 });
2611
2612 _ = editor.update(cx, |editor, window, cx| {
2613 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2614 editor.buffer.update(cx, |buffer, cx| {
2615 buffer.edit(
2616 [
2617 (Point::new(1, 2)..Point::new(3, 0), ""),
2618 (Point::new(4, 2)..Point::new(6, 0), ""),
2619 ],
2620 None,
2621 cx,
2622 );
2623 assert_eq!(
2624 buffer.read(cx).text(),
2625 "
2626 a
2627 b()
2628 c()
2629 "
2630 .unindent()
2631 );
2632 });
2633 assert_eq!(
2634 editor.selections.ranges(cx),
2635 &[
2636 Point::new(1, 2)..Point::new(1, 2),
2637 Point::new(2, 2)..Point::new(2, 2),
2638 ],
2639 );
2640
2641 editor.newline(&Newline, window, cx);
2642 assert_eq!(
2643 editor.text(cx),
2644 "
2645 a
2646 b(
2647 )
2648 c(
2649 )
2650 "
2651 .unindent()
2652 );
2653
2654 // The selections are moved after the inserted newlines
2655 assert_eq!(
2656 editor.selections.ranges(cx),
2657 &[
2658 Point::new(2, 0)..Point::new(2, 0),
2659 Point::new(4, 0)..Point::new(4, 0),
2660 ],
2661 );
2662 });
2663}
2664
2665#[gpui::test]
2666async fn test_newline_above(cx: &mut TestAppContext) {
2667 init_test(cx, |settings| {
2668 settings.defaults.tab_size = NonZeroU32::new(4)
2669 });
2670
2671 let language = Arc::new(
2672 Language::new(
2673 LanguageConfig::default(),
2674 Some(tree_sitter_rust::LANGUAGE.into()),
2675 )
2676 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2677 .unwrap(),
2678 );
2679
2680 let mut cx = EditorTestContext::new(cx).await;
2681 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2682 cx.set_state(indoc! {"
2683 const a: ˇA = (
2684 (ˇ
2685 «const_functionˇ»(ˇ),
2686 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2687 )ˇ
2688 ˇ);ˇ
2689 "});
2690
2691 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2692 cx.assert_editor_state(indoc! {"
2693 ˇ
2694 const a: A = (
2695 ˇ
2696 (
2697 ˇ
2698 ˇ
2699 const_function(),
2700 ˇ
2701 ˇ
2702 ˇ
2703 ˇ
2704 something_else,
2705 ˇ
2706 )
2707 ˇ
2708 ˇ
2709 );
2710 "});
2711}
2712
2713#[gpui::test]
2714async fn test_newline_below(cx: &mut TestAppContext) {
2715 init_test(cx, |settings| {
2716 settings.defaults.tab_size = NonZeroU32::new(4)
2717 });
2718
2719 let language = Arc::new(
2720 Language::new(
2721 LanguageConfig::default(),
2722 Some(tree_sitter_rust::LANGUAGE.into()),
2723 )
2724 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2725 .unwrap(),
2726 );
2727
2728 let mut cx = EditorTestContext::new(cx).await;
2729 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2730 cx.set_state(indoc! {"
2731 const a: ˇA = (
2732 (ˇ
2733 «const_functionˇ»(ˇ),
2734 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2735 )ˇ
2736 ˇ);ˇ
2737 "});
2738
2739 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2740 cx.assert_editor_state(indoc! {"
2741 const a: A = (
2742 ˇ
2743 (
2744 ˇ
2745 const_function(),
2746 ˇ
2747 ˇ
2748 something_else,
2749 ˇ
2750 ˇ
2751 ˇ
2752 ˇ
2753 )
2754 ˇ
2755 );
2756 ˇ
2757 ˇ
2758 "});
2759}
2760
2761#[gpui::test]
2762async fn test_newline_comments(cx: &mut TestAppContext) {
2763 init_test(cx, |settings| {
2764 settings.defaults.tab_size = NonZeroU32::new(4)
2765 });
2766
2767 let language = Arc::new(Language::new(
2768 LanguageConfig {
2769 line_comments: vec!["// ".into()],
2770 ..LanguageConfig::default()
2771 },
2772 None,
2773 ));
2774 {
2775 let mut cx = EditorTestContext::new(cx).await;
2776 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2777 cx.set_state(indoc! {"
2778 // Fooˇ
2779 "});
2780
2781 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2782 cx.assert_editor_state(indoc! {"
2783 // Foo
2784 // ˇ
2785 "});
2786 // Ensure that we add comment prefix when existing line contains space
2787 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2788 cx.assert_editor_state(
2789 indoc! {"
2790 // Foo
2791 //s
2792 // ˇ
2793 "}
2794 .replace("s", " ") // s is used as space placeholder to prevent format on save
2795 .as_str(),
2796 );
2797 // Ensure that we add comment prefix when existing line does not contain space
2798 cx.set_state(indoc! {"
2799 // Foo
2800 //ˇ
2801 "});
2802 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2803 cx.assert_editor_state(indoc! {"
2804 // Foo
2805 //
2806 // ˇ
2807 "});
2808 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2809 cx.set_state(indoc! {"
2810 ˇ// Foo
2811 "});
2812 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2813 cx.assert_editor_state(indoc! {"
2814
2815 ˇ// Foo
2816 "});
2817 }
2818 // Ensure that comment continuations can be disabled.
2819 update_test_language_settings(cx, |settings| {
2820 settings.defaults.extend_comment_on_newline = Some(false);
2821 });
2822 let mut cx = EditorTestContext::new(cx).await;
2823 cx.set_state(indoc! {"
2824 // Fooˇ
2825 "});
2826 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2827 cx.assert_editor_state(indoc! {"
2828 // Foo
2829 ˇ
2830 "});
2831}
2832
2833#[gpui::test]
2834async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
2835 init_test(cx, |settings| {
2836 settings.defaults.tab_size = NonZeroU32::new(4)
2837 });
2838
2839 let language = Arc::new(Language::new(
2840 LanguageConfig {
2841 line_comments: vec!["// ".into(), "/// ".into()],
2842 ..LanguageConfig::default()
2843 },
2844 None,
2845 ));
2846 {
2847 let mut cx = EditorTestContext::new(cx).await;
2848 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2849 cx.set_state(indoc! {"
2850 //ˇ
2851 "});
2852 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2853 cx.assert_editor_state(indoc! {"
2854 //
2855 // ˇ
2856 "});
2857
2858 cx.set_state(indoc! {"
2859 ///ˇ
2860 "});
2861 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2862 cx.assert_editor_state(indoc! {"
2863 ///
2864 /// ˇ
2865 "});
2866 }
2867}
2868
2869#[gpui::test]
2870async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
2871 init_test(cx, |settings| {
2872 settings.defaults.tab_size = NonZeroU32::new(4)
2873 });
2874
2875 let language = Arc::new(
2876 Language::new(
2877 LanguageConfig {
2878 documentation_comment: Some(language::BlockCommentConfig {
2879 start: "/**".into(),
2880 end: "*/".into(),
2881 prefix: "* ".into(),
2882 tab_size: 1,
2883 }),
2884
2885 ..LanguageConfig::default()
2886 },
2887 Some(tree_sitter_rust::LANGUAGE.into()),
2888 )
2889 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
2890 .unwrap(),
2891 );
2892
2893 {
2894 let mut cx = EditorTestContext::new(cx).await;
2895 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2896 cx.set_state(indoc! {"
2897 /**ˇ
2898 "});
2899
2900 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2901 cx.assert_editor_state(indoc! {"
2902 /**
2903 * ˇ
2904 "});
2905 // Ensure that if cursor is before the comment start,
2906 // we do not actually insert a comment prefix.
2907 cx.set_state(indoc! {"
2908 ˇ/**
2909 "});
2910 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2911 cx.assert_editor_state(indoc! {"
2912
2913 ˇ/**
2914 "});
2915 // Ensure that if cursor is between it doesn't add comment prefix.
2916 cx.set_state(indoc! {"
2917 /*ˇ*
2918 "});
2919 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2920 cx.assert_editor_state(indoc! {"
2921 /*
2922 ˇ*
2923 "});
2924 // Ensure that if suffix exists on same line after cursor it adds new line.
2925 cx.set_state(indoc! {"
2926 /**ˇ*/
2927 "});
2928 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2929 cx.assert_editor_state(indoc! {"
2930 /**
2931 * ˇ
2932 */
2933 "});
2934 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2935 cx.set_state(indoc! {"
2936 /**ˇ */
2937 "});
2938 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2939 cx.assert_editor_state(indoc! {"
2940 /**
2941 * ˇ
2942 */
2943 "});
2944 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2945 cx.set_state(indoc! {"
2946 /** ˇ*/
2947 "});
2948 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2949 cx.assert_editor_state(
2950 indoc! {"
2951 /**s
2952 * ˇ
2953 */
2954 "}
2955 .replace("s", " ") // s is used as space placeholder to prevent format on save
2956 .as_str(),
2957 );
2958 // Ensure that delimiter space is preserved when newline on already
2959 // spaced delimiter.
2960 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2961 cx.assert_editor_state(
2962 indoc! {"
2963 /**s
2964 *s
2965 * ˇ
2966 */
2967 "}
2968 .replace("s", " ") // s is used as space placeholder to prevent format on save
2969 .as_str(),
2970 );
2971 // Ensure that delimiter space is preserved when space is not
2972 // on existing delimiter.
2973 cx.set_state(indoc! {"
2974 /**
2975 *ˇ
2976 */
2977 "});
2978 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2979 cx.assert_editor_state(indoc! {"
2980 /**
2981 *
2982 * ˇ
2983 */
2984 "});
2985 // Ensure that if suffix exists on same line after cursor it
2986 // doesn't add extra new line if prefix is not on same line.
2987 cx.set_state(indoc! {"
2988 /**
2989 ˇ*/
2990 "});
2991 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2992 cx.assert_editor_state(indoc! {"
2993 /**
2994
2995 ˇ*/
2996 "});
2997 // Ensure that it detects suffix after existing prefix.
2998 cx.set_state(indoc! {"
2999 /**ˇ/
3000 "});
3001 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3002 cx.assert_editor_state(indoc! {"
3003 /**
3004 ˇ/
3005 "});
3006 // Ensure that if suffix exists on same line before
3007 // cursor it does not add comment prefix.
3008 cx.set_state(indoc! {"
3009 /** */ˇ
3010 "});
3011 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3012 cx.assert_editor_state(indoc! {"
3013 /** */
3014 ˇ
3015 "});
3016 // Ensure that if suffix exists on same line before
3017 // cursor it does not add comment prefix.
3018 cx.set_state(indoc! {"
3019 /**
3020 *
3021 */ˇ
3022 "});
3023 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3024 cx.assert_editor_state(indoc! {"
3025 /**
3026 *
3027 */
3028 ˇ
3029 "});
3030
3031 // Ensure that inline comment followed by code
3032 // doesn't add comment prefix on newline
3033 cx.set_state(indoc! {"
3034 /** */ textˇ
3035 "});
3036 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3037 cx.assert_editor_state(indoc! {"
3038 /** */ text
3039 ˇ
3040 "});
3041
3042 // Ensure that text after comment end tag
3043 // doesn't add comment prefix on newline
3044 cx.set_state(indoc! {"
3045 /**
3046 *
3047 */ˇtext
3048 "});
3049 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3050 cx.assert_editor_state(indoc! {"
3051 /**
3052 *
3053 */
3054 ˇtext
3055 "});
3056
3057 // Ensure if not comment block it doesn't
3058 // add comment prefix on newline
3059 cx.set_state(indoc! {"
3060 * textˇ
3061 "});
3062 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3063 cx.assert_editor_state(indoc! {"
3064 * text
3065 ˇ
3066 "});
3067 }
3068 // Ensure that comment continuations can be disabled.
3069 update_test_language_settings(cx, |settings| {
3070 settings.defaults.extend_comment_on_newline = Some(false);
3071 });
3072 let mut cx = EditorTestContext::new(cx).await;
3073 cx.set_state(indoc! {"
3074 /**ˇ
3075 "});
3076 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3077 cx.assert_editor_state(indoc! {"
3078 /**
3079 ˇ
3080 "});
3081}
3082
3083#[gpui::test]
3084async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3085 init_test(cx, |settings| {
3086 settings.defaults.tab_size = NonZeroU32::new(4)
3087 });
3088
3089 let lua_language = Arc::new(Language::new(
3090 LanguageConfig {
3091 line_comments: vec!["--".into()],
3092 block_comment: Some(language::BlockCommentConfig {
3093 start: "--[[".into(),
3094 prefix: "".into(),
3095 end: "]]".into(),
3096 tab_size: 0,
3097 }),
3098 ..LanguageConfig::default()
3099 },
3100 None,
3101 ));
3102
3103 let mut cx = EditorTestContext::new(cx).await;
3104 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3105
3106 // Line with line comment should extend
3107 cx.set_state(indoc! {"
3108 --ˇ
3109 "});
3110 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3111 cx.assert_editor_state(indoc! {"
3112 --
3113 --ˇ
3114 "});
3115
3116 // Line with block comment that matches line comment should not extend
3117 cx.set_state(indoc! {"
3118 --[[ˇ
3119 "});
3120 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3121 cx.assert_editor_state(indoc! {"
3122 --[[
3123 ˇ
3124 "});
3125}
3126
3127#[gpui::test]
3128fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3129 init_test(cx, |_| {});
3130
3131 let editor = cx.add_window(|window, cx| {
3132 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3133 let mut editor = build_editor(buffer.clone(), window, cx);
3134 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3135 s.select_ranges([3..4, 11..12, 19..20])
3136 });
3137 editor
3138 });
3139
3140 _ = editor.update(cx, |editor, window, cx| {
3141 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3142 editor.buffer.update(cx, |buffer, cx| {
3143 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3144 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3145 });
3146 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3147
3148 editor.insert("Z", window, cx);
3149 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3150
3151 // The selections are moved after the inserted characters
3152 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3153 });
3154}
3155
3156#[gpui::test]
3157async fn test_tab(cx: &mut TestAppContext) {
3158 init_test(cx, |settings| {
3159 settings.defaults.tab_size = NonZeroU32::new(3)
3160 });
3161
3162 let mut cx = EditorTestContext::new(cx).await;
3163 cx.set_state(indoc! {"
3164 ˇabˇc
3165 ˇ🏀ˇ🏀ˇefg
3166 dˇ
3167 "});
3168 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3169 cx.assert_editor_state(indoc! {"
3170 ˇab ˇc
3171 ˇ🏀 ˇ🏀 ˇefg
3172 d ˇ
3173 "});
3174
3175 cx.set_state(indoc! {"
3176 a
3177 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3178 "});
3179 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3180 cx.assert_editor_state(indoc! {"
3181 a
3182 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3183 "});
3184}
3185
3186#[gpui::test]
3187async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3188 init_test(cx, |_| {});
3189
3190 let mut cx = EditorTestContext::new(cx).await;
3191 let language = Arc::new(
3192 Language::new(
3193 LanguageConfig::default(),
3194 Some(tree_sitter_rust::LANGUAGE.into()),
3195 )
3196 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3197 .unwrap(),
3198 );
3199 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3200
3201 // test when all cursors are not at suggested indent
3202 // then simply move to their suggested indent location
3203 cx.set_state(indoc! {"
3204 const a: B = (
3205 c(
3206 ˇ
3207 ˇ )
3208 );
3209 "});
3210 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3211 cx.assert_editor_state(indoc! {"
3212 const a: B = (
3213 c(
3214 ˇ
3215 ˇ)
3216 );
3217 "});
3218
3219 // test cursor already at suggested indent not moving when
3220 // other cursors are yet to reach their suggested indents
3221 cx.set_state(indoc! {"
3222 ˇ
3223 const a: B = (
3224 c(
3225 d(
3226 ˇ
3227 )
3228 ˇ
3229 ˇ )
3230 );
3231 "});
3232 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3233 cx.assert_editor_state(indoc! {"
3234 ˇ
3235 const a: B = (
3236 c(
3237 d(
3238 ˇ
3239 )
3240 ˇ
3241 ˇ)
3242 );
3243 "});
3244 // test when all cursors are at suggested indent then tab is inserted
3245 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3246 cx.assert_editor_state(indoc! {"
3247 ˇ
3248 const a: B = (
3249 c(
3250 d(
3251 ˇ
3252 )
3253 ˇ
3254 ˇ)
3255 );
3256 "});
3257
3258 // test when current indent is less than suggested indent,
3259 // we adjust line to match suggested indent and move cursor to it
3260 //
3261 // when no other cursor is at word boundary, all of them should move
3262 cx.set_state(indoc! {"
3263 const a: B = (
3264 c(
3265 d(
3266 ˇ
3267 ˇ )
3268 ˇ )
3269 );
3270 "});
3271 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3272 cx.assert_editor_state(indoc! {"
3273 const a: B = (
3274 c(
3275 d(
3276 ˇ
3277 ˇ)
3278 ˇ)
3279 );
3280 "});
3281
3282 // test when current indent is less than suggested indent,
3283 // we adjust line to match suggested indent and move cursor to it
3284 //
3285 // when some other cursor is at word boundary, it should not move
3286 cx.set_state(indoc! {"
3287 const a: B = (
3288 c(
3289 d(
3290 ˇ
3291 ˇ )
3292 ˇ)
3293 );
3294 "});
3295 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3296 cx.assert_editor_state(indoc! {"
3297 const a: B = (
3298 c(
3299 d(
3300 ˇ
3301 ˇ)
3302 ˇ)
3303 );
3304 "});
3305
3306 // test when current indent is more than suggested indent,
3307 // we just move cursor to current indent instead of suggested indent
3308 //
3309 // when no other cursor is at word boundary, all of them should move
3310 cx.set_state(indoc! {"
3311 const a: B = (
3312 c(
3313 d(
3314 ˇ
3315 ˇ )
3316 ˇ )
3317 );
3318 "});
3319 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3320 cx.assert_editor_state(indoc! {"
3321 const a: B = (
3322 c(
3323 d(
3324 ˇ
3325 ˇ)
3326 ˇ)
3327 );
3328 "});
3329 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3330 cx.assert_editor_state(indoc! {"
3331 const a: B = (
3332 c(
3333 d(
3334 ˇ
3335 ˇ)
3336 ˇ)
3337 );
3338 "});
3339
3340 // test when current indent is more than suggested indent,
3341 // we just move cursor to current indent instead of suggested indent
3342 //
3343 // when some other cursor is at word boundary, it doesn't move
3344 cx.set_state(indoc! {"
3345 const a: B = (
3346 c(
3347 d(
3348 ˇ
3349 ˇ )
3350 ˇ)
3351 );
3352 "});
3353 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3354 cx.assert_editor_state(indoc! {"
3355 const a: B = (
3356 c(
3357 d(
3358 ˇ
3359 ˇ)
3360 ˇ)
3361 );
3362 "});
3363
3364 // handle auto-indent when there are multiple cursors on the same line
3365 cx.set_state(indoc! {"
3366 const a: B = (
3367 c(
3368 ˇ ˇ
3369 ˇ )
3370 );
3371 "});
3372 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3373 cx.assert_editor_state(indoc! {"
3374 const a: B = (
3375 c(
3376 ˇ
3377 ˇ)
3378 );
3379 "});
3380}
3381
3382#[gpui::test]
3383async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3384 init_test(cx, |settings| {
3385 settings.defaults.tab_size = NonZeroU32::new(3)
3386 });
3387
3388 let mut cx = EditorTestContext::new(cx).await;
3389 cx.set_state(indoc! {"
3390 ˇ
3391 \t ˇ
3392 \t ˇ
3393 \t ˇ
3394 \t \t\t \t \t\t \t\t \t \t ˇ
3395 "});
3396
3397 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3398 cx.assert_editor_state(indoc! {"
3399 ˇ
3400 \t ˇ
3401 \t ˇ
3402 \t ˇ
3403 \t \t\t \t \t\t \t\t \t \t ˇ
3404 "});
3405}
3406
3407#[gpui::test]
3408async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3409 init_test(cx, |settings| {
3410 settings.defaults.tab_size = NonZeroU32::new(4)
3411 });
3412
3413 let language = Arc::new(
3414 Language::new(
3415 LanguageConfig::default(),
3416 Some(tree_sitter_rust::LANGUAGE.into()),
3417 )
3418 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3419 .unwrap(),
3420 );
3421
3422 let mut cx = EditorTestContext::new(cx).await;
3423 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3424 cx.set_state(indoc! {"
3425 fn a() {
3426 if b {
3427 \t ˇc
3428 }
3429 }
3430 "});
3431
3432 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3433 cx.assert_editor_state(indoc! {"
3434 fn a() {
3435 if b {
3436 ˇc
3437 }
3438 }
3439 "});
3440}
3441
3442#[gpui::test]
3443async fn test_indent_outdent(cx: &mut TestAppContext) {
3444 init_test(cx, |settings| {
3445 settings.defaults.tab_size = NonZeroU32::new(4);
3446 });
3447
3448 let mut cx = EditorTestContext::new(cx).await;
3449
3450 cx.set_state(indoc! {"
3451 «oneˇ» «twoˇ»
3452 three
3453 four
3454 "});
3455 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3456 cx.assert_editor_state(indoc! {"
3457 «oneˇ» «twoˇ»
3458 three
3459 four
3460 "});
3461
3462 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3463 cx.assert_editor_state(indoc! {"
3464 «oneˇ» «twoˇ»
3465 three
3466 four
3467 "});
3468
3469 // select across line ending
3470 cx.set_state(indoc! {"
3471 one two
3472 t«hree
3473 ˇ» four
3474 "});
3475 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3476 cx.assert_editor_state(indoc! {"
3477 one two
3478 t«hree
3479 ˇ» four
3480 "});
3481
3482 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3483 cx.assert_editor_state(indoc! {"
3484 one two
3485 t«hree
3486 ˇ» four
3487 "});
3488
3489 // Ensure that indenting/outdenting works when the cursor is at column 0.
3490 cx.set_state(indoc! {"
3491 one two
3492 ˇthree
3493 four
3494 "});
3495 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3496 cx.assert_editor_state(indoc! {"
3497 one two
3498 ˇthree
3499 four
3500 "});
3501
3502 cx.set_state(indoc! {"
3503 one two
3504 ˇ three
3505 four
3506 "});
3507 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3508 cx.assert_editor_state(indoc! {"
3509 one two
3510 ˇthree
3511 four
3512 "});
3513}
3514
3515#[gpui::test]
3516async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3517 // This is a regression test for issue #33761
3518 init_test(cx, |_| {});
3519
3520 let mut cx = EditorTestContext::new(cx).await;
3521 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3522 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3523
3524 cx.set_state(
3525 r#"ˇ# ingress:
3526ˇ# api:
3527ˇ# enabled: false
3528ˇ# pathType: Prefix
3529ˇ# console:
3530ˇ# enabled: false
3531ˇ# pathType: Prefix
3532"#,
3533 );
3534
3535 // Press tab to indent all lines
3536 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3537
3538 cx.assert_editor_state(
3539 r#" ˇ# ingress:
3540 ˇ# api:
3541 ˇ# enabled: false
3542 ˇ# pathType: Prefix
3543 ˇ# console:
3544 ˇ# enabled: false
3545 ˇ# pathType: Prefix
3546"#,
3547 );
3548}
3549
3550#[gpui::test]
3551async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3552 // This is a test to make sure our fix for issue #33761 didn't break anything
3553 init_test(cx, |_| {});
3554
3555 let mut cx = EditorTestContext::new(cx).await;
3556 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3557 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3558
3559 cx.set_state(
3560 r#"ˇingress:
3561ˇ api:
3562ˇ enabled: false
3563ˇ pathType: Prefix
3564"#,
3565 );
3566
3567 // Press tab to indent all lines
3568 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3569
3570 cx.assert_editor_state(
3571 r#"ˇingress:
3572 ˇapi:
3573 ˇenabled: false
3574 ˇpathType: Prefix
3575"#,
3576 );
3577}
3578
3579#[gpui::test]
3580async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3581 init_test(cx, |settings| {
3582 settings.defaults.hard_tabs = Some(true);
3583 });
3584
3585 let mut cx = EditorTestContext::new(cx).await;
3586
3587 // select two ranges on one line
3588 cx.set_state(indoc! {"
3589 «oneˇ» «twoˇ»
3590 three
3591 four
3592 "});
3593 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3594 cx.assert_editor_state(indoc! {"
3595 \t«oneˇ» «twoˇ»
3596 three
3597 four
3598 "});
3599 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3600 cx.assert_editor_state(indoc! {"
3601 \t\t«oneˇ» «twoˇ»
3602 three
3603 four
3604 "});
3605 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3606 cx.assert_editor_state(indoc! {"
3607 \t«oneˇ» «twoˇ»
3608 three
3609 four
3610 "});
3611 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3612 cx.assert_editor_state(indoc! {"
3613 «oneˇ» «twoˇ»
3614 three
3615 four
3616 "});
3617
3618 // select across a line ending
3619 cx.set_state(indoc! {"
3620 one two
3621 t«hree
3622 ˇ»four
3623 "});
3624 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3625 cx.assert_editor_state(indoc! {"
3626 one two
3627 \tt«hree
3628 ˇ»four
3629 "});
3630 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3631 cx.assert_editor_state(indoc! {"
3632 one two
3633 \t\tt«hree
3634 ˇ»four
3635 "});
3636 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3637 cx.assert_editor_state(indoc! {"
3638 one two
3639 \tt«hree
3640 ˇ»four
3641 "});
3642 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3643 cx.assert_editor_state(indoc! {"
3644 one two
3645 t«hree
3646 ˇ»four
3647 "});
3648
3649 // Ensure that indenting/outdenting works when the cursor is at column 0.
3650 cx.set_state(indoc! {"
3651 one two
3652 ˇthree
3653 four
3654 "});
3655 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3656 cx.assert_editor_state(indoc! {"
3657 one two
3658 ˇthree
3659 four
3660 "});
3661 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3662 cx.assert_editor_state(indoc! {"
3663 one two
3664 \tˇthree
3665 four
3666 "});
3667 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3668 cx.assert_editor_state(indoc! {"
3669 one two
3670 ˇthree
3671 four
3672 "});
3673}
3674
3675#[gpui::test]
3676fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3677 init_test(cx, |settings| {
3678 settings.languages.0.extend([
3679 (
3680 "TOML".into(),
3681 LanguageSettingsContent {
3682 tab_size: NonZeroU32::new(2),
3683 ..Default::default()
3684 },
3685 ),
3686 (
3687 "Rust".into(),
3688 LanguageSettingsContent {
3689 tab_size: NonZeroU32::new(4),
3690 ..Default::default()
3691 },
3692 ),
3693 ]);
3694 });
3695
3696 let toml_language = Arc::new(Language::new(
3697 LanguageConfig {
3698 name: "TOML".into(),
3699 ..Default::default()
3700 },
3701 None,
3702 ));
3703 let rust_language = Arc::new(Language::new(
3704 LanguageConfig {
3705 name: "Rust".into(),
3706 ..Default::default()
3707 },
3708 None,
3709 ));
3710
3711 let toml_buffer =
3712 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3713 let rust_buffer =
3714 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3715 let multibuffer = cx.new(|cx| {
3716 let mut multibuffer = MultiBuffer::new(ReadWrite);
3717 multibuffer.push_excerpts(
3718 toml_buffer.clone(),
3719 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3720 cx,
3721 );
3722 multibuffer.push_excerpts(
3723 rust_buffer.clone(),
3724 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3725 cx,
3726 );
3727 multibuffer
3728 });
3729
3730 cx.add_window(|window, cx| {
3731 let mut editor = build_editor(multibuffer, window, cx);
3732
3733 assert_eq!(
3734 editor.text(cx),
3735 indoc! {"
3736 a = 1
3737 b = 2
3738
3739 const c: usize = 3;
3740 "}
3741 );
3742
3743 select_ranges(
3744 &mut editor,
3745 indoc! {"
3746 «aˇ» = 1
3747 b = 2
3748
3749 «const c:ˇ» usize = 3;
3750 "},
3751 window,
3752 cx,
3753 );
3754
3755 editor.tab(&Tab, window, cx);
3756 assert_text_with_selections(
3757 &mut editor,
3758 indoc! {"
3759 «aˇ» = 1
3760 b = 2
3761
3762 «const c:ˇ» usize = 3;
3763 "},
3764 cx,
3765 );
3766 editor.backtab(&Backtab, window, cx);
3767 assert_text_with_selections(
3768 &mut editor,
3769 indoc! {"
3770 «aˇ» = 1
3771 b = 2
3772
3773 «const c:ˇ» usize = 3;
3774 "},
3775 cx,
3776 );
3777
3778 editor
3779 });
3780}
3781
3782#[gpui::test]
3783async fn test_backspace(cx: &mut TestAppContext) {
3784 init_test(cx, |_| {});
3785
3786 let mut cx = EditorTestContext::new(cx).await;
3787
3788 // Basic backspace
3789 cx.set_state(indoc! {"
3790 onˇe two three
3791 fou«rˇ» five six
3792 seven «ˇeight nine
3793 »ten
3794 "});
3795 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3796 cx.assert_editor_state(indoc! {"
3797 oˇe two three
3798 fouˇ five six
3799 seven ˇten
3800 "});
3801
3802 // Test backspace inside and around indents
3803 cx.set_state(indoc! {"
3804 zero
3805 ˇone
3806 ˇtwo
3807 ˇ ˇ ˇ three
3808 ˇ ˇ four
3809 "});
3810 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3811 cx.assert_editor_state(indoc! {"
3812 zero
3813 ˇone
3814 ˇtwo
3815 ˇ threeˇ four
3816 "});
3817}
3818
3819#[gpui::test]
3820async fn test_delete(cx: &mut TestAppContext) {
3821 init_test(cx, |_| {});
3822
3823 let mut cx = EditorTestContext::new(cx).await;
3824 cx.set_state(indoc! {"
3825 onˇe two three
3826 fou«rˇ» five six
3827 seven «ˇeight nine
3828 »ten
3829 "});
3830 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3831 cx.assert_editor_state(indoc! {"
3832 onˇ two three
3833 fouˇ five six
3834 seven ˇten
3835 "});
3836}
3837
3838#[gpui::test]
3839fn test_delete_line(cx: &mut TestAppContext) {
3840 init_test(cx, |_| {});
3841
3842 let editor = cx.add_window(|window, cx| {
3843 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3844 build_editor(buffer, window, cx)
3845 });
3846 _ = editor.update(cx, |editor, window, cx| {
3847 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3848 s.select_display_ranges([
3849 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3850 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3851 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3852 ])
3853 });
3854 editor.delete_line(&DeleteLine, window, cx);
3855 assert_eq!(editor.display_text(cx), "ghi");
3856 assert_eq!(
3857 editor.selections.display_ranges(cx),
3858 vec![
3859 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3860 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3861 ]
3862 );
3863 });
3864
3865 let editor = cx.add_window(|window, cx| {
3866 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3867 build_editor(buffer, window, cx)
3868 });
3869 _ = editor.update(cx, |editor, window, cx| {
3870 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3871 s.select_display_ranges([
3872 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3873 ])
3874 });
3875 editor.delete_line(&DeleteLine, window, cx);
3876 assert_eq!(editor.display_text(cx), "ghi\n");
3877 assert_eq!(
3878 editor.selections.display_ranges(cx),
3879 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3880 );
3881 });
3882}
3883
3884#[gpui::test]
3885fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3886 init_test(cx, |_| {});
3887
3888 cx.add_window(|window, cx| {
3889 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3890 let mut editor = build_editor(buffer.clone(), window, cx);
3891 let buffer = buffer.read(cx).as_singleton().unwrap();
3892
3893 assert_eq!(
3894 editor.selections.ranges::<Point>(cx),
3895 &[Point::new(0, 0)..Point::new(0, 0)]
3896 );
3897
3898 // When on single line, replace newline at end by space
3899 editor.join_lines(&JoinLines, window, cx);
3900 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3901 assert_eq!(
3902 editor.selections.ranges::<Point>(cx),
3903 &[Point::new(0, 3)..Point::new(0, 3)]
3904 );
3905
3906 // When multiple lines are selected, remove newlines that are spanned by the selection
3907 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3908 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3909 });
3910 editor.join_lines(&JoinLines, window, cx);
3911 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3912 assert_eq!(
3913 editor.selections.ranges::<Point>(cx),
3914 &[Point::new(0, 11)..Point::new(0, 11)]
3915 );
3916
3917 // Undo should be transactional
3918 editor.undo(&Undo, window, cx);
3919 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3920 assert_eq!(
3921 editor.selections.ranges::<Point>(cx),
3922 &[Point::new(0, 5)..Point::new(2, 2)]
3923 );
3924
3925 // When joining an empty line don't insert a space
3926 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3927 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3928 });
3929 editor.join_lines(&JoinLines, window, cx);
3930 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3931 assert_eq!(
3932 editor.selections.ranges::<Point>(cx),
3933 [Point::new(2, 3)..Point::new(2, 3)]
3934 );
3935
3936 // We can remove trailing newlines
3937 editor.join_lines(&JoinLines, window, cx);
3938 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3939 assert_eq!(
3940 editor.selections.ranges::<Point>(cx),
3941 [Point::new(2, 3)..Point::new(2, 3)]
3942 );
3943
3944 // We don't blow up on the last line
3945 editor.join_lines(&JoinLines, window, cx);
3946 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3947 assert_eq!(
3948 editor.selections.ranges::<Point>(cx),
3949 [Point::new(2, 3)..Point::new(2, 3)]
3950 );
3951
3952 // reset to test indentation
3953 editor.buffer.update(cx, |buffer, cx| {
3954 buffer.edit(
3955 [
3956 (Point::new(1, 0)..Point::new(1, 2), " "),
3957 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3958 ],
3959 None,
3960 cx,
3961 )
3962 });
3963
3964 // We remove any leading spaces
3965 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3966 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3967 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3968 });
3969 editor.join_lines(&JoinLines, window, cx);
3970 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3971
3972 // We don't insert a space for a line containing only spaces
3973 editor.join_lines(&JoinLines, window, cx);
3974 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3975
3976 // We ignore any leading tabs
3977 editor.join_lines(&JoinLines, window, cx);
3978 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3979
3980 editor
3981 });
3982}
3983
3984#[gpui::test]
3985fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3986 init_test(cx, |_| {});
3987
3988 cx.add_window(|window, cx| {
3989 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3990 let mut editor = build_editor(buffer.clone(), window, cx);
3991 let buffer = buffer.read(cx).as_singleton().unwrap();
3992
3993 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3994 s.select_ranges([
3995 Point::new(0, 2)..Point::new(1, 1),
3996 Point::new(1, 2)..Point::new(1, 2),
3997 Point::new(3, 1)..Point::new(3, 2),
3998 ])
3999 });
4000
4001 editor.join_lines(&JoinLines, window, cx);
4002 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4003
4004 assert_eq!(
4005 editor.selections.ranges::<Point>(cx),
4006 [
4007 Point::new(0, 7)..Point::new(0, 7),
4008 Point::new(1, 3)..Point::new(1, 3)
4009 ]
4010 );
4011 editor
4012 });
4013}
4014
4015#[gpui::test]
4016async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4017 init_test(cx, |_| {});
4018
4019 let mut cx = EditorTestContext::new(cx).await;
4020
4021 let diff_base = r#"
4022 Line 0
4023 Line 1
4024 Line 2
4025 Line 3
4026 "#
4027 .unindent();
4028
4029 cx.set_state(
4030 &r#"
4031 ˇLine 0
4032 Line 1
4033 Line 2
4034 Line 3
4035 "#
4036 .unindent(),
4037 );
4038
4039 cx.set_head_text(&diff_base);
4040 executor.run_until_parked();
4041
4042 // Join lines
4043 cx.update_editor(|editor, window, cx| {
4044 editor.join_lines(&JoinLines, window, cx);
4045 });
4046 executor.run_until_parked();
4047
4048 cx.assert_editor_state(
4049 &r#"
4050 Line 0ˇ Line 1
4051 Line 2
4052 Line 3
4053 "#
4054 .unindent(),
4055 );
4056 // Join again
4057 cx.update_editor(|editor, window, cx| {
4058 editor.join_lines(&JoinLines, window, cx);
4059 });
4060 executor.run_until_parked();
4061
4062 cx.assert_editor_state(
4063 &r#"
4064 Line 0 Line 1ˇ Line 2
4065 Line 3
4066 "#
4067 .unindent(),
4068 );
4069}
4070
4071#[gpui::test]
4072async fn test_custom_newlines_cause_no_false_positive_diffs(
4073 executor: BackgroundExecutor,
4074 cx: &mut TestAppContext,
4075) {
4076 init_test(cx, |_| {});
4077 let mut cx = EditorTestContext::new(cx).await;
4078 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4079 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4080 executor.run_until_parked();
4081
4082 cx.update_editor(|editor, window, cx| {
4083 let snapshot = editor.snapshot(window, cx);
4084 assert_eq!(
4085 snapshot
4086 .buffer_snapshot
4087 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
4088 .collect::<Vec<_>>(),
4089 Vec::new(),
4090 "Should not have any diffs for files with custom newlines"
4091 );
4092 });
4093}
4094
4095#[gpui::test]
4096async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4097 init_test(cx, |_| {});
4098
4099 let mut cx = EditorTestContext::new(cx).await;
4100
4101 // Test sort_lines_case_insensitive()
4102 cx.set_state(indoc! {"
4103 «z
4104 y
4105 x
4106 Z
4107 Y
4108 Xˇ»
4109 "});
4110 cx.update_editor(|e, window, cx| {
4111 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4112 });
4113 cx.assert_editor_state(indoc! {"
4114 «x
4115 X
4116 y
4117 Y
4118 z
4119 Zˇ»
4120 "});
4121
4122 // Test sort_lines_by_length()
4123 //
4124 // Demonstrates:
4125 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4126 // - sort is stable
4127 cx.set_state(indoc! {"
4128 «123
4129 æ
4130 12
4131 ∞
4132 1
4133 æˇ»
4134 "});
4135 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4136 cx.assert_editor_state(indoc! {"
4137 «æ
4138 ∞
4139 1
4140 æ
4141 12
4142 123ˇ»
4143 "});
4144
4145 // Test reverse_lines()
4146 cx.set_state(indoc! {"
4147 «5
4148 4
4149 3
4150 2
4151 1ˇ»
4152 "});
4153 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4154 cx.assert_editor_state(indoc! {"
4155 «1
4156 2
4157 3
4158 4
4159 5ˇ»
4160 "});
4161
4162 // Skip testing shuffle_line()
4163
4164 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4165 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4166
4167 // Don't manipulate when cursor is on single line, but expand the selection
4168 cx.set_state(indoc! {"
4169 ddˇdd
4170 ccc
4171 bb
4172 a
4173 "});
4174 cx.update_editor(|e, window, cx| {
4175 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4176 });
4177 cx.assert_editor_state(indoc! {"
4178 «ddddˇ»
4179 ccc
4180 bb
4181 a
4182 "});
4183
4184 // Basic manipulate case
4185 // Start selection moves to column 0
4186 // End of selection shrinks to fit shorter line
4187 cx.set_state(indoc! {"
4188 dd«d
4189 ccc
4190 bb
4191 aaaaaˇ»
4192 "});
4193 cx.update_editor(|e, window, cx| {
4194 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4195 });
4196 cx.assert_editor_state(indoc! {"
4197 «aaaaa
4198 bb
4199 ccc
4200 dddˇ»
4201 "});
4202
4203 // Manipulate case with newlines
4204 cx.set_state(indoc! {"
4205 dd«d
4206 ccc
4207
4208 bb
4209 aaaaa
4210
4211 ˇ»
4212 "});
4213 cx.update_editor(|e, window, cx| {
4214 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4215 });
4216 cx.assert_editor_state(indoc! {"
4217 «
4218
4219 aaaaa
4220 bb
4221 ccc
4222 dddˇ»
4223
4224 "});
4225
4226 // Adding new line
4227 cx.set_state(indoc! {"
4228 aa«a
4229 bbˇ»b
4230 "});
4231 cx.update_editor(|e, window, cx| {
4232 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4233 });
4234 cx.assert_editor_state(indoc! {"
4235 «aaa
4236 bbb
4237 added_lineˇ»
4238 "});
4239
4240 // Removing line
4241 cx.set_state(indoc! {"
4242 aa«a
4243 bbbˇ»
4244 "});
4245 cx.update_editor(|e, window, cx| {
4246 e.manipulate_immutable_lines(window, cx, |lines| {
4247 lines.pop();
4248 })
4249 });
4250 cx.assert_editor_state(indoc! {"
4251 «aaaˇ»
4252 "});
4253
4254 // Removing all lines
4255 cx.set_state(indoc! {"
4256 aa«a
4257 bbbˇ»
4258 "});
4259 cx.update_editor(|e, window, cx| {
4260 e.manipulate_immutable_lines(window, cx, |lines| {
4261 lines.drain(..);
4262 })
4263 });
4264 cx.assert_editor_state(indoc! {"
4265 ˇ
4266 "});
4267}
4268
4269#[gpui::test]
4270async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4271 init_test(cx, |_| {});
4272
4273 let mut cx = EditorTestContext::new(cx).await;
4274
4275 // Consider continuous selection as single selection
4276 cx.set_state(indoc! {"
4277 Aaa«aa
4278 cˇ»c«c
4279 bb
4280 aaaˇ»aa
4281 "});
4282 cx.update_editor(|e, window, cx| {
4283 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4284 });
4285 cx.assert_editor_state(indoc! {"
4286 «Aaaaa
4287 ccc
4288 bb
4289 aaaaaˇ»
4290 "});
4291
4292 cx.set_state(indoc! {"
4293 Aaa«aa
4294 cˇ»c«c
4295 bb
4296 aaaˇ»aa
4297 "});
4298 cx.update_editor(|e, window, cx| {
4299 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4300 });
4301 cx.assert_editor_state(indoc! {"
4302 «Aaaaa
4303 ccc
4304 bbˇ»
4305 "});
4306
4307 // Consider non continuous selection as distinct dedup operations
4308 cx.set_state(indoc! {"
4309 «aaaaa
4310 bb
4311 aaaaa
4312 aaaaaˇ»
4313
4314 aaa«aaˇ»
4315 "});
4316 cx.update_editor(|e, window, cx| {
4317 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4318 });
4319 cx.assert_editor_state(indoc! {"
4320 «aaaaa
4321 bbˇ»
4322
4323 «aaaaaˇ»
4324 "});
4325}
4326
4327#[gpui::test]
4328async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4329 init_test(cx, |_| {});
4330
4331 let mut cx = EditorTestContext::new(cx).await;
4332
4333 cx.set_state(indoc! {"
4334 «Aaa
4335 aAa
4336 Aaaˇ»
4337 "});
4338 cx.update_editor(|e, window, cx| {
4339 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4340 });
4341 cx.assert_editor_state(indoc! {"
4342 «Aaa
4343 aAaˇ»
4344 "});
4345
4346 cx.set_state(indoc! {"
4347 «Aaa
4348 aAa
4349 aaAˇ»
4350 "});
4351 cx.update_editor(|e, window, cx| {
4352 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4353 });
4354 cx.assert_editor_state(indoc! {"
4355 «Aaaˇ»
4356 "});
4357}
4358
4359#[gpui::test]
4360async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4361 init_test(cx, |_| {});
4362
4363 let mut cx = EditorTestContext::new(cx).await;
4364
4365 // Manipulate with multiple selections on a single line
4366 cx.set_state(indoc! {"
4367 dd«dd
4368 cˇ»c«c
4369 bb
4370 aaaˇ»aa
4371 "});
4372 cx.update_editor(|e, window, cx| {
4373 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4374 });
4375 cx.assert_editor_state(indoc! {"
4376 «aaaaa
4377 bb
4378 ccc
4379 ddddˇ»
4380 "});
4381
4382 // Manipulate with multiple disjoin selections
4383 cx.set_state(indoc! {"
4384 5«
4385 4
4386 3
4387 2
4388 1ˇ»
4389
4390 dd«dd
4391 ccc
4392 bb
4393 aaaˇ»aa
4394 "});
4395 cx.update_editor(|e, window, cx| {
4396 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4397 });
4398 cx.assert_editor_state(indoc! {"
4399 «1
4400 2
4401 3
4402 4
4403 5ˇ»
4404
4405 «aaaaa
4406 bb
4407 ccc
4408 ddddˇ»
4409 "});
4410
4411 // Adding lines on each selection
4412 cx.set_state(indoc! {"
4413 2«
4414 1ˇ»
4415
4416 bb«bb
4417 aaaˇ»aa
4418 "});
4419 cx.update_editor(|e, window, cx| {
4420 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4421 });
4422 cx.assert_editor_state(indoc! {"
4423 «2
4424 1
4425 added lineˇ»
4426
4427 «bbbb
4428 aaaaa
4429 added lineˇ»
4430 "});
4431
4432 // Removing lines on each selection
4433 cx.set_state(indoc! {"
4434 2«
4435 1ˇ»
4436
4437 bb«bb
4438 aaaˇ»aa
4439 "});
4440 cx.update_editor(|e, window, cx| {
4441 e.manipulate_immutable_lines(window, cx, |lines| {
4442 lines.pop();
4443 })
4444 });
4445 cx.assert_editor_state(indoc! {"
4446 «2ˇ»
4447
4448 «bbbbˇ»
4449 "});
4450}
4451
4452#[gpui::test]
4453async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4454 init_test(cx, |settings| {
4455 settings.defaults.tab_size = NonZeroU32::new(3)
4456 });
4457
4458 let mut cx = EditorTestContext::new(cx).await;
4459
4460 // MULTI SELECTION
4461 // Ln.1 "«" tests empty lines
4462 // Ln.9 tests just leading whitespace
4463 cx.set_state(indoc! {"
4464 «
4465 abc // No indentationˇ»
4466 «\tabc // 1 tabˇ»
4467 \t\tabc « ˇ» // 2 tabs
4468 \t ab«c // Tab followed by space
4469 \tabc // Space followed by tab (3 spaces should be the result)
4470 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4471 abˇ»ˇc ˇ ˇ // Already space indented«
4472 \t
4473 \tabc\tdef // Only the leading tab is manipulatedˇ»
4474 "});
4475 cx.update_editor(|e, window, cx| {
4476 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4477 });
4478 cx.assert_editor_state(
4479 indoc! {"
4480 «
4481 abc // No indentation
4482 abc // 1 tab
4483 abc // 2 tabs
4484 abc // Tab followed by space
4485 abc // Space followed by tab (3 spaces should be the result)
4486 abc // Mixed indentation (tab conversion depends on the column)
4487 abc // Already space indented
4488 ·
4489 abc\tdef // Only the leading tab is manipulatedˇ»
4490 "}
4491 .replace("·", "")
4492 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4493 );
4494
4495 // Test on just a few lines, the others should remain unchanged
4496 // Only lines (3, 5, 10, 11) should change
4497 cx.set_state(
4498 indoc! {"
4499 ·
4500 abc // No indentation
4501 \tabcˇ // 1 tab
4502 \t\tabc // 2 tabs
4503 \t abcˇ // Tab followed by space
4504 \tabc // Space followed by tab (3 spaces should be the result)
4505 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4506 abc // Already space indented
4507 «\t
4508 \tabc\tdef // Only the leading tab is manipulatedˇ»
4509 "}
4510 .replace("·", "")
4511 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4512 );
4513 cx.update_editor(|e, window, cx| {
4514 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4515 });
4516 cx.assert_editor_state(
4517 indoc! {"
4518 ·
4519 abc // No indentation
4520 « abc // 1 tabˇ»
4521 \t\tabc // 2 tabs
4522 « abc // Tab followed by spaceˇ»
4523 \tabc // Space followed by tab (3 spaces should be the result)
4524 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4525 abc // Already space indented
4526 « ·
4527 abc\tdef // Only the leading tab is manipulatedˇ»
4528 "}
4529 .replace("·", "")
4530 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4531 );
4532
4533 // SINGLE SELECTION
4534 // Ln.1 "«" tests empty lines
4535 // Ln.9 tests just leading whitespace
4536 cx.set_state(indoc! {"
4537 «
4538 abc // No indentation
4539 \tabc // 1 tab
4540 \t\tabc // 2 tabs
4541 \t abc // Tab followed by space
4542 \tabc // Space followed by tab (3 spaces should be the result)
4543 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4544 abc // Already space indented
4545 \t
4546 \tabc\tdef // Only the leading tab is manipulatedˇ»
4547 "});
4548 cx.update_editor(|e, window, cx| {
4549 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4550 });
4551 cx.assert_editor_state(
4552 indoc! {"
4553 «
4554 abc // No indentation
4555 abc // 1 tab
4556 abc // 2 tabs
4557 abc // Tab followed by space
4558 abc // Space followed by tab (3 spaces should be the result)
4559 abc // Mixed indentation (tab conversion depends on the column)
4560 abc // Already space indented
4561 ·
4562 abc\tdef // Only the leading tab is manipulatedˇ»
4563 "}
4564 .replace("·", "")
4565 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4566 );
4567}
4568
4569#[gpui::test]
4570async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
4571 init_test(cx, |settings| {
4572 settings.defaults.tab_size = NonZeroU32::new(3)
4573 });
4574
4575 let mut cx = EditorTestContext::new(cx).await;
4576
4577 // MULTI SELECTION
4578 // Ln.1 "«" tests empty lines
4579 // Ln.11 tests just leading whitespace
4580 cx.set_state(indoc! {"
4581 «
4582 abˇ»ˇc // No indentation
4583 abc ˇ ˇ // 1 space (< 3 so dont convert)
4584 abc « // 2 spaces (< 3 so dont convert)
4585 abc // 3 spaces (convert)
4586 abc ˇ» // 5 spaces (1 tab + 2 spaces)
4587 «\tˇ»\t«\tˇ»abc // Already tab indented
4588 «\t abc // Tab followed by space
4589 \tabc // Space followed by tab (should be consumed due to tab)
4590 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4591 \tˇ» «\t
4592 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
4593 "});
4594 cx.update_editor(|e, window, cx| {
4595 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4596 });
4597 cx.assert_editor_state(indoc! {"
4598 «
4599 abc // No indentation
4600 abc // 1 space (< 3 so dont convert)
4601 abc // 2 spaces (< 3 so dont convert)
4602 \tabc // 3 spaces (convert)
4603 \t abc // 5 spaces (1 tab + 2 spaces)
4604 \t\t\tabc // Already tab indented
4605 \t abc // Tab followed by space
4606 \tabc // Space followed by tab (should be consumed due to tab)
4607 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4608 \t\t\t
4609 \tabc \t // Only the leading spaces should be convertedˇ»
4610 "});
4611
4612 // Test on just a few lines, the other should remain unchanged
4613 // Only lines (4, 8, 11, 12) should change
4614 cx.set_state(
4615 indoc! {"
4616 ·
4617 abc // No indentation
4618 abc // 1 space (< 3 so dont convert)
4619 abc // 2 spaces (< 3 so dont convert)
4620 « abc // 3 spaces (convert)ˇ»
4621 abc // 5 spaces (1 tab + 2 spaces)
4622 \t\t\tabc // Already tab indented
4623 \t abc // Tab followed by space
4624 \tabc ˇ // Space followed by tab (should be consumed due to tab)
4625 \t\t \tabc // Mixed indentation
4626 \t \t \t \tabc // Mixed indentation
4627 \t \tˇ
4628 « abc \t // Only the leading spaces should be convertedˇ»
4629 "}
4630 .replace("·", "")
4631 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4632 );
4633 cx.update_editor(|e, window, cx| {
4634 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4635 });
4636 cx.assert_editor_state(
4637 indoc! {"
4638 ·
4639 abc // No indentation
4640 abc // 1 space (< 3 so dont convert)
4641 abc // 2 spaces (< 3 so dont convert)
4642 «\tabc // 3 spaces (convert)ˇ»
4643 abc // 5 spaces (1 tab + 2 spaces)
4644 \t\t\tabc // Already tab indented
4645 \t abc // Tab followed by space
4646 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
4647 \t\t \tabc // Mixed indentation
4648 \t \t \t \tabc // Mixed indentation
4649 «\t\t\t
4650 \tabc \t // Only the leading spaces should be convertedˇ»
4651 "}
4652 .replace("·", "")
4653 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4654 );
4655
4656 // SINGLE SELECTION
4657 // Ln.1 "«" tests empty lines
4658 // Ln.11 tests just leading whitespace
4659 cx.set_state(indoc! {"
4660 «
4661 abc // No indentation
4662 abc // 1 space (< 3 so dont convert)
4663 abc // 2 spaces (< 3 so dont convert)
4664 abc // 3 spaces (convert)
4665 abc // 5 spaces (1 tab + 2 spaces)
4666 \t\t\tabc // Already tab indented
4667 \t abc // Tab followed by space
4668 \tabc // Space followed by tab (should be consumed due to tab)
4669 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4670 \t \t
4671 abc \t // Only the leading spaces should be convertedˇ»
4672 "});
4673 cx.update_editor(|e, window, cx| {
4674 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4675 });
4676 cx.assert_editor_state(indoc! {"
4677 «
4678 abc // No indentation
4679 abc // 1 space (< 3 so dont convert)
4680 abc // 2 spaces (< 3 so dont convert)
4681 \tabc // 3 spaces (convert)
4682 \t abc // 5 spaces (1 tab + 2 spaces)
4683 \t\t\tabc // Already tab indented
4684 \t abc // Tab followed by space
4685 \tabc // Space followed by tab (should be consumed due to tab)
4686 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4687 \t\t\t
4688 \tabc \t // Only the leading spaces should be convertedˇ»
4689 "});
4690}
4691
4692#[gpui::test]
4693async fn test_toggle_case(cx: &mut TestAppContext) {
4694 init_test(cx, |_| {});
4695
4696 let mut cx = EditorTestContext::new(cx).await;
4697
4698 // If all lower case -> upper case
4699 cx.set_state(indoc! {"
4700 «hello worldˇ»
4701 "});
4702 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4703 cx.assert_editor_state(indoc! {"
4704 «HELLO WORLDˇ»
4705 "});
4706
4707 // If all upper case -> lower case
4708 cx.set_state(indoc! {"
4709 «HELLO WORLDˇ»
4710 "});
4711 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4712 cx.assert_editor_state(indoc! {"
4713 «hello worldˇ»
4714 "});
4715
4716 // If any upper case characters are identified -> lower case
4717 // This matches JetBrains IDEs
4718 cx.set_state(indoc! {"
4719 «hEllo worldˇ»
4720 "});
4721 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4722 cx.assert_editor_state(indoc! {"
4723 «hello worldˇ»
4724 "});
4725}
4726
4727#[gpui::test]
4728async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
4729 init_test(cx, |_| {});
4730
4731 let mut cx = EditorTestContext::new(cx).await;
4732
4733 cx.set_state(indoc! {"
4734 «implement-windows-supportˇ»
4735 "});
4736 cx.update_editor(|e, window, cx| {
4737 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
4738 });
4739 cx.assert_editor_state(indoc! {"
4740 «Implement windows supportˇ»
4741 "});
4742}
4743
4744#[gpui::test]
4745async fn test_manipulate_text(cx: &mut TestAppContext) {
4746 init_test(cx, |_| {});
4747
4748 let mut cx = EditorTestContext::new(cx).await;
4749
4750 // Test convert_to_upper_case()
4751 cx.set_state(indoc! {"
4752 «hello worldˇ»
4753 "});
4754 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4755 cx.assert_editor_state(indoc! {"
4756 «HELLO WORLDˇ»
4757 "});
4758
4759 // Test convert_to_lower_case()
4760 cx.set_state(indoc! {"
4761 «HELLO WORLDˇ»
4762 "});
4763 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4764 cx.assert_editor_state(indoc! {"
4765 «hello worldˇ»
4766 "});
4767
4768 // Test multiple line, single selection case
4769 cx.set_state(indoc! {"
4770 «The quick brown
4771 fox jumps over
4772 the lazy dogˇ»
4773 "});
4774 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4775 cx.assert_editor_state(indoc! {"
4776 «The Quick Brown
4777 Fox Jumps Over
4778 The Lazy Dogˇ»
4779 "});
4780
4781 // Test multiple line, single selection case
4782 cx.set_state(indoc! {"
4783 «The quick brown
4784 fox jumps over
4785 the lazy dogˇ»
4786 "});
4787 cx.update_editor(|e, window, cx| {
4788 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4789 });
4790 cx.assert_editor_state(indoc! {"
4791 «TheQuickBrown
4792 FoxJumpsOver
4793 TheLazyDogˇ»
4794 "});
4795
4796 // From here on out, test more complex cases of manipulate_text()
4797
4798 // Test no selection case - should affect words cursors are in
4799 // Cursor at beginning, middle, and end of word
4800 cx.set_state(indoc! {"
4801 ˇhello big beauˇtiful worldˇ
4802 "});
4803 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4804 cx.assert_editor_state(indoc! {"
4805 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4806 "});
4807
4808 // Test multiple selections on a single line and across multiple lines
4809 cx.set_state(indoc! {"
4810 «Theˇ» quick «brown
4811 foxˇ» jumps «overˇ»
4812 the «lazyˇ» dog
4813 "});
4814 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4815 cx.assert_editor_state(indoc! {"
4816 «THEˇ» quick «BROWN
4817 FOXˇ» jumps «OVERˇ»
4818 the «LAZYˇ» dog
4819 "});
4820
4821 // Test case where text length grows
4822 cx.set_state(indoc! {"
4823 «tschüߡ»
4824 "});
4825 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4826 cx.assert_editor_state(indoc! {"
4827 «TSCHÜSSˇ»
4828 "});
4829
4830 // Test to make sure we don't crash when text shrinks
4831 cx.set_state(indoc! {"
4832 aaa_bbbˇ
4833 "});
4834 cx.update_editor(|e, window, cx| {
4835 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4836 });
4837 cx.assert_editor_state(indoc! {"
4838 «aaaBbbˇ»
4839 "});
4840
4841 // Test to make sure we all aware of the fact that each word can grow and shrink
4842 // Final selections should be aware of this fact
4843 cx.set_state(indoc! {"
4844 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4845 "});
4846 cx.update_editor(|e, window, cx| {
4847 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4848 });
4849 cx.assert_editor_state(indoc! {"
4850 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4851 "});
4852
4853 cx.set_state(indoc! {"
4854 «hElLo, WoRld!ˇ»
4855 "});
4856 cx.update_editor(|e, window, cx| {
4857 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4858 });
4859 cx.assert_editor_state(indoc! {"
4860 «HeLlO, wOrLD!ˇ»
4861 "});
4862}
4863
4864#[gpui::test]
4865fn test_duplicate_line(cx: &mut TestAppContext) {
4866 init_test(cx, |_| {});
4867
4868 let editor = cx.add_window(|window, cx| {
4869 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4870 build_editor(buffer, window, cx)
4871 });
4872 _ = editor.update(cx, |editor, window, cx| {
4873 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4874 s.select_display_ranges([
4875 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4876 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4877 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4878 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4879 ])
4880 });
4881 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4882 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4883 assert_eq!(
4884 editor.selections.display_ranges(cx),
4885 vec![
4886 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4887 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4888 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4889 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4890 ]
4891 );
4892 });
4893
4894 let editor = cx.add_window(|window, cx| {
4895 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4896 build_editor(buffer, window, cx)
4897 });
4898 _ = editor.update(cx, |editor, window, cx| {
4899 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4900 s.select_display_ranges([
4901 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4902 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4903 ])
4904 });
4905 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4906 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4907 assert_eq!(
4908 editor.selections.display_ranges(cx),
4909 vec![
4910 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4911 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4912 ]
4913 );
4914 });
4915
4916 // With `move_upwards` the selections stay in place, except for
4917 // the lines inserted above them
4918 let editor = cx.add_window(|window, cx| {
4919 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4920 build_editor(buffer, window, cx)
4921 });
4922 _ = editor.update(cx, |editor, window, cx| {
4923 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4924 s.select_display_ranges([
4925 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4926 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4927 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4928 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4929 ])
4930 });
4931 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4932 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4933 assert_eq!(
4934 editor.selections.display_ranges(cx),
4935 vec![
4936 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4937 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4938 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4939 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4940 ]
4941 );
4942 });
4943
4944 let editor = cx.add_window(|window, cx| {
4945 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4946 build_editor(buffer, window, cx)
4947 });
4948 _ = editor.update(cx, |editor, window, cx| {
4949 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4950 s.select_display_ranges([
4951 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4952 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4953 ])
4954 });
4955 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4956 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4957 assert_eq!(
4958 editor.selections.display_ranges(cx),
4959 vec![
4960 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4961 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4962 ]
4963 );
4964 });
4965
4966 let editor = cx.add_window(|window, cx| {
4967 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4968 build_editor(buffer, window, cx)
4969 });
4970 _ = editor.update(cx, |editor, window, cx| {
4971 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4972 s.select_display_ranges([
4973 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4974 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4975 ])
4976 });
4977 editor.duplicate_selection(&DuplicateSelection, window, cx);
4978 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4979 assert_eq!(
4980 editor.selections.display_ranges(cx),
4981 vec![
4982 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4983 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4984 ]
4985 );
4986 });
4987}
4988
4989#[gpui::test]
4990fn test_move_line_up_down(cx: &mut TestAppContext) {
4991 init_test(cx, |_| {});
4992
4993 let editor = cx.add_window(|window, cx| {
4994 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4995 build_editor(buffer, window, cx)
4996 });
4997 _ = editor.update(cx, |editor, window, cx| {
4998 editor.fold_creases(
4999 vec![
5000 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5001 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5002 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5003 ],
5004 true,
5005 window,
5006 cx,
5007 );
5008 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5009 s.select_display_ranges([
5010 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5011 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5012 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5013 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5014 ])
5015 });
5016 assert_eq!(
5017 editor.display_text(cx),
5018 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5019 );
5020
5021 editor.move_line_up(&MoveLineUp, window, cx);
5022 assert_eq!(
5023 editor.display_text(cx),
5024 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5025 );
5026 assert_eq!(
5027 editor.selections.display_ranges(cx),
5028 vec![
5029 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5030 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5031 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5032 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5033 ]
5034 );
5035 });
5036
5037 _ = editor.update(cx, |editor, window, cx| {
5038 editor.move_line_down(&MoveLineDown, window, cx);
5039 assert_eq!(
5040 editor.display_text(cx),
5041 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5042 );
5043 assert_eq!(
5044 editor.selections.display_ranges(cx),
5045 vec![
5046 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5047 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5048 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5049 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5050 ]
5051 );
5052 });
5053
5054 _ = editor.update(cx, |editor, window, cx| {
5055 editor.move_line_down(&MoveLineDown, window, cx);
5056 assert_eq!(
5057 editor.display_text(cx),
5058 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5059 );
5060 assert_eq!(
5061 editor.selections.display_ranges(cx),
5062 vec![
5063 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5064 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5065 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5066 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5067 ]
5068 );
5069 });
5070
5071 _ = editor.update(cx, |editor, window, cx| {
5072 editor.move_line_up(&MoveLineUp, window, cx);
5073 assert_eq!(
5074 editor.display_text(cx),
5075 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5076 );
5077 assert_eq!(
5078 editor.selections.display_ranges(cx),
5079 vec![
5080 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5081 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5082 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5083 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5084 ]
5085 );
5086 });
5087}
5088
5089#[gpui::test]
5090fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5091 init_test(cx, |_| {});
5092 let editor = cx.add_window(|window, cx| {
5093 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5094 build_editor(buffer, window, cx)
5095 });
5096 _ = editor.update(cx, |editor, window, cx| {
5097 editor.fold_creases(
5098 vec![Crease::simple(
5099 Point::new(6, 4)..Point::new(7, 4),
5100 FoldPlaceholder::test(),
5101 )],
5102 true,
5103 window,
5104 cx,
5105 );
5106 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5107 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5108 });
5109 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5110 editor.move_line_up(&MoveLineUp, window, cx);
5111 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5112 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5113 });
5114}
5115
5116#[gpui::test]
5117fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5118 init_test(cx, |_| {});
5119
5120 let editor = cx.add_window(|window, cx| {
5121 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5122 build_editor(buffer, window, cx)
5123 });
5124 _ = editor.update(cx, |editor, window, cx| {
5125 let snapshot = editor.buffer.read(cx).snapshot(cx);
5126 editor.insert_blocks(
5127 [BlockProperties {
5128 style: BlockStyle::Fixed,
5129 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5130 height: Some(1),
5131 render: Arc::new(|_| div().into_any()),
5132 priority: 0,
5133 }],
5134 Some(Autoscroll::fit()),
5135 cx,
5136 );
5137 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5138 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5139 });
5140 editor.move_line_down(&MoveLineDown, window, cx);
5141 });
5142}
5143
5144#[gpui::test]
5145async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5146 init_test(cx, |_| {});
5147
5148 let mut cx = EditorTestContext::new(cx).await;
5149 cx.set_state(
5150 &"
5151 ˇzero
5152 one
5153 two
5154 three
5155 four
5156 five
5157 "
5158 .unindent(),
5159 );
5160
5161 // Create a four-line block that replaces three lines of text.
5162 cx.update_editor(|editor, window, cx| {
5163 let snapshot = editor.snapshot(window, cx);
5164 let snapshot = &snapshot.buffer_snapshot;
5165 let placement = BlockPlacement::Replace(
5166 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5167 );
5168 editor.insert_blocks(
5169 [BlockProperties {
5170 placement,
5171 height: Some(4),
5172 style: BlockStyle::Sticky,
5173 render: Arc::new(|_| gpui::div().into_any_element()),
5174 priority: 0,
5175 }],
5176 None,
5177 cx,
5178 );
5179 });
5180
5181 // Move down so that the cursor touches the block.
5182 cx.update_editor(|editor, window, cx| {
5183 editor.move_down(&Default::default(), window, cx);
5184 });
5185 cx.assert_editor_state(
5186 &"
5187 zero
5188 «one
5189 two
5190 threeˇ»
5191 four
5192 five
5193 "
5194 .unindent(),
5195 );
5196
5197 // Move down past the block.
5198 cx.update_editor(|editor, window, cx| {
5199 editor.move_down(&Default::default(), window, cx);
5200 });
5201 cx.assert_editor_state(
5202 &"
5203 zero
5204 one
5205 two
5206 three
5207 ˇfour
5208 five
5209 "
5210 .unindent(),
5211 );
5212}
5213
5214#[gpui::test]
5215fn test_transpose(cx: &mut TestAppContext) {
5216 init_test(cx, |_| {});
5217
5218 _ = cx.add_window(|window, cx| {
5219 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5220 editor.set_style(EditorStyle::default(), window, cx);
5221 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5222 s.select_ranges([1..1])
5223 });
5224 editor.transpose(&Default::default(), window, cx);
5225 assert_eq!(editor.text(cx), "bac");
5226 assert_eq!(editor.selections.ranges(cx), [2..2]);
5227
5228 editor.transpose(&Default::default(), window, cx);
5229 assert_eq!(editor.text(cx), "bca");
5230 assert_eq!(editor.selections.ranges(cx), [3..3]);
5231
5232 editor.transpose(&Default::default(), window, cx);
5233 assert_eq!(editor.text(cx), "bac");
5234 assert_eq!(editor.selections.ranges(cx), [3..3]);
5235
5236 editor
5237 });
5238
5239 _ = cx.add_window(|window, cx| {
5240 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5241 editor.set_style(EditorStyle::default(), window, cx);
5242 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5243 s.select_ranges([3..3])
5244 });
5245 editor.transpose(&Default::default(), window, cx);
5246 assert_eq!(editor.text(cx), "acb\nde");
5247 assert_eq!(editor.selections.ranges(cx), [3..3]);
5248
5249 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5250 s.select_ranges([4..4])
5251 });
5252 editor.transpose(&Default::default(), window, cx);
5253 assert_eq!(editor.text(cx), "acbd\ne");
5254 assert_eq!(editor.selections.ranges(cx), [5..5]);
5255
5256 editor.transpose(&Default::default(), window, cx);
5257 assert_eq!(editor.text(cx), "acbde\n");
5258 assert_eq!(editor.selections.ranges(cx), [6..6]);
5259
5260 editor.transpose(&Default::default(), window, cx);
5261 assert_eq!(editor.text(cx), "acbd\ne");
5262 assert_eq!(editor.selections.ranges(cx), [6..6]);
5263
5264 editor
5265 });
5266
5267 _ = cx.add_window(|window, cx| {
5268 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5269 editor.set_style(EditorStyle::default(), window, cx);
5270 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5271 s.select_ranges([1..1, 2..2, 4..4])
5272 });
5273 editor.transpose(&Default::default(), window, cx);
5274 assert_eq!(editor.text(cx), "bacd\ne");
5275 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5276
5277 editor.transpose(&Default::default(), window, cx);
5278 assert_eq!(editor.text(cx), "bcade\n");
5279 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5280
5281 editor.transpose(&Default::default(), window, cx);
5282 assert_eq!(editor.text(cx), "bcda\ne");
5283 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5284
5285 editor.transpose(&Default::default(), window, cx);
5286 assert_eq!(editor.text(cx), "bcade\n");
5287 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5288
5289 editor.transpose(&Default::default(), window, cx);
5290 assert_eq!(editor.text(cx), "bcaed\n");
5291 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5292
5293 editor
5294 });
5295
5296 _ = cx.add_window(|window, cx| {
5297 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5298 editor.set_style(EditorStyle::default(), window, cx);
5299 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5300 s.select_ranges([4..4])
5301 });
5302 editor.transpose(&Default::default(), window, cx);
5303 assert_eq!(editor.text(cx), "🏀🍐✋");
5304 assert_eq!(editor.selections.ranges(cx), [8..8]);
5305
5306 editor.transpose(&Default::default(), window, cx);
5307 assert_eq!(editor.text(cx), "🏀✋🍐");
5308 assert_eq!(editor.selections.ranges(cx), [11..11]);
5309
5310 editor.transpose(&Default::default(), window, cx);
5311 assert_eq!(editor.text(cx), "🏀🍐✋");
5312 assert_eq!(editor.selections.ranges(cx), [11..11]);
5313
5314 editor
5315 });
5316}
5317
5318#[gpui::test]
5319async fn test_rewrap(cx: &mut TestAppContext) {
5320 init_test(cx, |settings| {
5321 settings.languages.0.extend([
5322 (
5323 "Markdown".into(),
5324 LanguageSettingsContent {
5325 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5326 preferred_line_length: Some(40),
5327 ..Default::default()
5328 },
5329 ),
5330 (
5331 "Plain Text".into(),
5332 LanguageSettingsContent {
5333 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5334 preferred_line_length: Some(40),
5335 ..Default::default()
5336 },
5337 ),
5338 (
5339 "C++".into(),
5340 LanguageSettingsContent {
5341 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5342 preferred_line_length: Some(40),
5343 ..Default::default()
5344 },
5345 ),
5346 (
5347 "Python".into(),
5348 LanguageSettingsContent {
5349 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5350 preferred_line_length: Some(40),
5351 ..Default::default()
5352 },
5353 ),
5354 (
5355 "Rust".into(),
5356 LanguageSettingsContent {
5357 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5358 preferred_line_length: Some(40),
5359 ..Default::default()
5360 },
5361 ),
5362 ])
5363 });
5364
5365 let mut cx = EditorTestContext::new(cx).await;
5366
5367 let cpp_language = Arc::new(Language::new(
5368 LanguageConfig {
5369 name: "C++".into(),
5370 line_comments: vec!["// ".into()],
5371 ..LanguageConfig::default()
5372 },
5373 None,
5374 ));
5375 let python_language = Arc::new(Language::new(
5376 LanguageConfig {
5377 name: "Python".into(),
5378 line_comments: vec!["# ".into()],
5379 ..LanguageConfig::default()
5380 },
5381 None,
5382 ));
5383 let markdown_language = Arc::new(Language::new(
5384 LanguageConfig {
5385 name: "Markdown".into(),
5386 rewrap_prefixes: vec![
5387 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5388 regex::Regex::new("[-*+]\\s+").unwrap(),
5389 ],
5390 ..LanguageConfig::default()
5391 },
5392 None,
5393 ));
5394 let rust_language = Arc::new(Language::new(
5395 LanguageConfig {
5396 name: "Rust".into(),
5397 line_comments: vec!["// ".into(), "/// ".into()],
5398 ..LanguageConfig::default()
5399 },
5400 Some(tree_sitter_rust::LANGUAGE.into()),
5401 ));
5402
5403 let plaintext_language = Arc::new(Language::new(
5404 LanguageConfig {
5405 name: "Plain Text".into(),
5406 ..LanguageConfig::default()
5407 },
5408 None,
5409 ));
5410
5411 // Test basic rewrapping of a long line with a cursor
5412 assert_rewrap(
5413 indoc! {"
5414 // ˇThis is a long comment that needs to be wrapped.
5415 "},
5416 indoc! {"
5417 // ˇThis is a long comment that needs to
5418 // be wrapped.
5419 "},
5420 cpp_language.clone(),
5421 &mut cx,
5422 );
5423
5424 // Test rewrapping a full selection
5425 assert_rewrap(
5426 indoc! {"
5427 «// This selected long comment needs to be wrapped.ˇ»"
5428 },
5429 indoc! {"
5430 «// This selected long comment needs to
5431 // be wrapped.ˇ»"
5432 },
5433 cpp_language.clone(),
5434 &mut cx,
5435 );
5436
5437 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5438 assert_rewrap(
5439 indoc! {"
5440 // ˇThis is the first line.
5441 // Thisˇ is the second line.
5442 // This is the thirdˇ line, all part of one paragraph.
5443 "},
5444 indoc! {"
5445 // ˇThis is the first line. Thisˇ is the
5446 // second line. This is the thirdˇ line,
5447 // all part of one paragraph.
5448 "},
5449 cpp_language.clone(),
5450 &mut cx,
5451 );
5452
5453 // Test multiple cursors in different paragraphs trigger separate rewraps
5454 assert_rewrap(
5455 indoc! {"
5456 // ˇThis is the first paragraph, first line.
5457 // ˇThis is the first paragraph, second line.
5458
5459 // ˇThis is the second paragraph, first line.
5460 // ˇThis is the second paragraph, second line.
5461 "},
5462 indoc! {"
5463 // ˇThis is the first paragraph, first
5464 // line. ˇThis is the first paragraph,
5465 // second line.
5466
5467 // ˇThis is the second paragraph, first
5468 // line. ˇThis is the second paragraph,
5469 // second line.
5470 "},
5471 cpp_language.clone(),
5472 &mut cx,
5473 );
5474
5475 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
5476 assert_rewrap(
5477 indoc! {"
5478 «// A regular long long comment to be wrapped.
5479 /// A documentation long comment to be wrapped.ˇ»
5480 "},
5481 indoc! {"
5482 «// A regular long long comment to be
5483 // wrapped.
5484 /// A documentation long comment to be
5485 /// wrapped.ˇ»
5486 "},
5487 rust_language.clone(),
5488 &mut cx,
5489 );
5490
5491 // Test that change in indentation level trigger seperate rewraps
5492 assert_rewrap(
5493 indoc! {"
5494 fn foo() {
5495 «// This is a long comment at the base indent.
5496 // This is a long comment at the next indent.ˇ»
5497 }
5498 "},
5499 indoc! {"
5500 fn foo() {
5501 «// This is a long comment at the
5502 // base indent.
5503 // This is a long comment at the
5504 // next indent.ˇ»
5505 }
5506 "},
5507 rust_language.clone(),
5508 &mut cx,
5509 );
5510
5511 // Test that different comment prefix characters (e.g., '#') are handled correctly
5512 assert_rewrap(
5513 indoc! {"
5514 # ˇThis is a long comment using a pound sign.
5515 "},
5516 indoc! {"
5517 # ˇThis is a long comment using a pound
5518 # sign.
5519 "},
5520 python_language.clone(),
5521 &mut cx,
5522 );
5523
5524 // Test rewrapping only affects comments, not code even when selected
5525 assert_rewrap(
5526 indoc! {"
5527 «/// This doc comment is long and should be wrapped.
5528 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5529 "},
5530 indoc! {"
5531 «/// This doc comment is long and should
5532 /// be wrapped.
5533 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5534 "},
5535 rust_language.clone(),
5536 &mut cx,
5537 );
5538
5539 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
5540 assert_rewrap(
5541 indoc! {"
5542 # Header
5543
5544 A long long long line of markdown text to wrap.ˇ
5545 "},
5546 indoc! {"
5547 # Header
5548
5549 A long long long line of markdown text
5550 to wrap.ˇ
5551 "},
5552 markdown_language.clone(),
5553 &mut cx,
5554 );
5555
5556 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
5557 assert_rewrap(
5558 indoc! {"
5559 «1. This is a numbered list item that is very long and needs to be wrapped properly.
5560 2. This is a numbered list item that is very long and needs to be wrapped properly.
5561 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
5562 "},
5563 indoc! {"
5564 «1. This is a numbered list item that is
5565 very long and needs to be wrapped
5566 properly.
5567 2. This is a numbered list item that is
5568 very long and needs to be wrapped
5569 properly.
5570 - This is an unordered list item that is
5571 also very long and should not merge
5572 with the numbered item.ˇ»
5573 "},
5574 markdown_language.clone(),
5575 &mut cx,
5576 );
5577
5578 // Test that rewrapping add indents for rewrapping boundary if not exists already.
5579 assert_rewrap(
5580 indoc! {"
5581 «1. This is a numbered list item that is
5582 very long and needs to be wrapped
5583 properly.
5584 2. This is a numbered list item that is
5585 very long and needs to be wrapped
5586 properly.
5587 - This is an unordered list item that is
5588 also very long and should not merge with
5589 the numbered item.ˇ»
5590 "},
5591 indoc! {"
5592 «1. This is a numbered list item that is
5593 very long and needs to be wrapped
5594 properly.
5595 2. This is a numbered list item that is
5596 very long and needs to be wrapped
5597 properly.
5598 - This is an unordered list item that is
5599 also very long and should not merge
5600 with the numbered item.ˇ»
5601 "},
5602 markdown_language.clone(),
5603 &mut cx,
5604 );
5605
5606 // Test that rewrapping maintain indents even when they already exists.
5607 assert_rewrap(
5608 indoc! {"
5609 «1. This is a numbered list
5610 item that is very long and needs to be wrapped properly.
5611 2. This is a numbered list
5612 item that is very long and needs to be wrapped properly.
5613 - This is an unordered list item that is also very long and
5614 should not merge with the numbered item.ˇ»
5615 "},
5616 indoc! {"
5617 «1. This is a numbered list item that is
5618 very long and needs to be wrapped
5619 properly.
5620 2. This is a numbered list item that is
5621 very long and needs to be wrapped
5622 properly.
5623 - This is an unordered list item that is
5624 also very long and should not merge
5625 with the numbered item.ˇ»
5626 "},
5627 markdown_language.clone(),
5628 &mut cx,
5629 );
5630
5631 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
5632 assert_rewrap(
5633 indoc! {"
5634 ˇThis is a very long line of plain text that will be wrapped.
5635 "},
5636 indoc! {"
5637 ˇThis is a very long line of plain text
5638 that will be wrapped.
5639 "},
5640 plaintext_language.clone(),
5641 &mut cx,
5642 );
5643
5644 // Test that non-commented code acts as a paragraph boundary within a selection
5645 assert_rewrap(
5646 indoc! {"
5647 «// This is the first long comment block to be wrapped.
5648 fn my_func(a: u32);
5649 // This is the second long comment block to be wrapped.ˇ»
5650 "},
5651 indoc! {"
5652 «// This is the first long comment block
5653 // to be wrapped.
5654 fn my_func(a: u32);
5655 // This is the second long comment block
5656 // to be wrapped.ˇ»
5657 "},
5658 rust_language.clone(),
5659 &mut cx,
5660 );
5661
5662 // Test rewrapping multiple selections, including ones with blank lines or tabs
5663 assert_rewrap(
5664 indoc! {"
5665 «ˇThis is a very long line that will be wrapped.
5666
5667 This is another paragraph in the same selection.»
5668
5669 «\tThis is a very long indented line that will be wrapped.ˇ»
5670 "},
5671 indoc! {"
5672 «ˇThis is a very long line that will be
5673 wrapped.
5674
5675 This is another paragraph in the same
5676 selection.»
5677
5678 «\tThis is a very long indented line
5679 \tthat will be wrapped.ˇ»
5680 "},
5681 plaintext_language.clone(),
5682 &mut cx,
5683 );
5684
5685 // Test that an empty comment line acts as a paragraph boundary
5686 assert_rewrap(
5687 indoc! {"
5688 // ˇThis is a long comment that will be wrapped.
5689 //
5690 // And this is another long comment that will also be wrapped.ˇ
5691 "},
5692 indoc! {"
5693 // ˇThis is a long comment that will be
5694 // wrapped.
5695 //
5696 // And this is another long comment that
5697 // will also be wrapped.ˇ
5698 "},
5699 cpp_language,
5700 &mut cx,
5701 );
5702
5703 #[track_caller]
5704 fn assert_rewrap(
5705 unwrapped_text: &str,
5706 wrapped_text: &str,
5707 language: Arc<Language>,
5708 cx: &mut EditorTestContext,
5709 ) {
5710 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5711 cx.set_state(unwrapped_text);
5712 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5713 cx.assert_editor_state(wrapped_text);
5714 }
5715}
5716
5717#[gpui::test]
5718async fn test_hard_wrap(cx: &mut TestAppContext) {
5719 init_test(cx, |_| {});
5720 let mut cx = EditorTestContext::new(cx).await;
5721
5722 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5723 cx.update_editor(|editor, _, cx| {
5724 editor.set_hard_wrap(Some(14), cx);
5725 });
5726
5727 cx.set_state(indoc!(
5728 "
5729 one two three ˇ
5730 "
5731 ));
5732 cx.simulate_input("four");
5733 cx.run_until_parked();
5734
5735 cx.assert_editor_state(indoc!(
5736 "
5737 one two three
5738 fourˇ
5739 "
5740 ));
5741
5742 cx.update_editor(|editor, window, cx| {
5743 editor.newline(&Default::default(), window, cx);
5744 });
5745 cx.run_until_parked();
5746 cx.assert_editor_state(indoc!(
5747 "
5748 one two three
5749 four
5750 ˇ
5751 "
5752 ));
5753
5754 cx.simulate_input("five");
5755 cx.run_until_parked();
5756 cx.assert_editor_state(indoc!(
5757 "
5758 one two three
5759 four
5760 fiveˇ
5761 "
5762 ));
5763
5764 cx.update_editor(|editor, window, cx| {
5765 editor.newline(&Default::default(), window, cx);
5766 });
5767 cx.run_until_parked();
5768 cx.simulate_input("# ");
5769 cx.run_until_parked();
5770 cx.assert_editor_state(indoc!(
5771 "
5772 one two three
5773 four
5774 five
5775 # ˇ
5776 "
5777 ));
5778
5779 cx.update_editor(|editor, window, cx| {
5780 editor.newline(&Default::default(), window, cx);
5781 });
5782 cx.run_until_parked();
5783 cx.assert_editor_state(indoc!(
5784 "
5785 one two three
5786 four
5787 five
5788 #\x20
5789 #ˇ
5790 "
5791 ));
5792
5793 cx.simulate_input(" 6");
5794 cx.run_until_parked();
5795 cx.assert_editor_state(indoc!(
5796 "
5797 one two three
5798 four
5799 five
5800 #
5801 # 6ˇ
5802 "
5803 ));
5804}
5805
5806#[gpui::test]
5807async fn test_clipboard(cx: &mut TestAppContext) {
5808 init_test(cx, |_| {});
5809
5810 let mut cx = EditorTestContext::new(cx).await;
5811
5812 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5813 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5814 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5815
5816 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5817 cx.set_state("two ˇfour ˇsix ˇ");
5818 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5819 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5820
5821 // Paste again but with only two cursors. Since the number of cursors doesn't
5822 // match the number of slices in the clipboard, the entire clipboard text
5823 // is pasted at each cursor.
5824 cx.set_state("ˇtwo one✅ four three six five ˇ");
5825 cx.update_editor(|e, window, cx| {
5826 e.handle_input("( ", window, cx);
5827 e.paste(&Paste, window, cx);
5828 e.handle_input(") ", window, cx);
5829 });
5830 cx.assert_editor_state(
5831 &([
5832 "( one✅ ",
5833 "three ",
5834 "five ) ˇtwo one✅ four three six five ( one✅ ",
5835 "three ",
5836 "five ) ˇ",
5837 ]
5838 .join("\n")),
5839 );
5840
5841 // Cut with three selections, one of which is full-line.
5842 cx.set_state(indoc! {"
5843 1«2ˇ»3
5844 4ˇ567
5845 «8ˇ»9"});
5846 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5847 cx.assert_editor_state(indoc! {"
5848 1ˇ3
5849 ˇ9"});
5850
5851 // Paste with three selections, noticing how the copied selection that was full-line
5852 // gets inserted before the second cursor.
5853 cx.set_state(indoc! {"
5854 1ˇ3
5855 9ˇ
5856 «oˇ»ne"});
5857 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5858 cx.assert_editor_state(indoc! {"
5859 12ˇ3
5860 4567
5861 9ˇ
5862 8ˇne"});
5863
5864 // Copy with a single cursor only, which writes the whole line into the clipboard.
5865 cx.set_state(indoc! {"
5866 The quick brown
5867 fox juˇmps over
5868 the lazy dog"});
5869 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5870 assert_eq!(
5871 cx.read_from_clipboard()
5872 .and_then(|item| item.text().as_deref().map(str::to_string)),
5873 Some("fox jumps over\n".to_string())
5874 );
5875
5876 // Paste with three selections, noticing how the copied full-line selection is inserted
5877 // before the empty selections but replaces the selection that is non-empty.
5878 cx.set_state(indoc! {"
5879 Tˇhe quick brown
5880 «foˇ»x jumps over
5881 tˇhe lazy dog"});
5882 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5883 cx.assert_editor_state(indoc! {"
5884 fox jumps over
5885 Tˇhe quick brown
5886 fox jumps over
5887 ˇx jumps over
5888 fox jumps over
5889 tˇhe lazy dog"});
5890}
5891
5892#[gpui::test]
5893async fn test_copy_trim(cx: &mut TestAppContext) {
5894 init_test(cx, |_| {});
5895
5896 let mut cx = EditorTestContext::new(cx).await;
5897 cx.set_state(
5898 r#" «for selection in selections.iter() {
5899 let mut start = selection.start;
5900 let mut end = selection.end;
5901 let is_entire_line = selection.is_empty();
5902 if is_entire_line {
5903 start = Point::new(start.row, 0);ˇ»
5904 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5905 }
5906 "#,
5907 );
5908 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5909 assert_eq!(
5910 cx.read_from_clipboard()
5911 .and_then(|item| item.text().as_deref().map(str::to_string)),
5912 Some(
5913 "for selection in selections.iter() {
5914 let mut start = selection.start;
5915 let mut end = selection.end;
5916 let is_entire_line = selection.is_empty();
5917 if is_entire_line {
5918 start = Point::new(start.row, 0);"
5919 .to_string()
5920 ),
5921 "Regular copying preserves all indentation selected",
5922 );
5923 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5924 assert_eq!(
5925 cx.read_from_clipboard()
5926 .and_then(|item| item.text().as_deref().map(str::to_string)),
5927 Some(
5928 "for selection in selections.iter() {
5929let mut start = selection.start;
5930let mut end = selection.end;
5931let is_entire_line = selection.is_empty();
5932if is_entire_line {
5933 start = Point::new(start.row, 0);"
5934 .to_string()
5935 ),
5936 "Copying with stripping should strip all leading whitespaces"
5937 );
5938
5939 cx.set_state(
5940 r#" « for selection in selections.iter() {
5941 let mut start = selection.start;
5942 let mut end = selection.end;
5943 let is_entire_line = selection.is_empty();
5944 if is_entire_line {
5945 start = Point::new(start.row, 0);ˇ»
5946 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5947 }
5948 "#,
5949 );
5950 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5951 assert_eq!(
5952 cx.read_from_clipboard()
5953 .and_then(|item| item.text().as_deref().map(str::to_string)),
5954 Some(
5955 " for selection in selections.iter() {
5956 let mut start = selection.start;
5957 let mut end = selection.end;
5958 let is_entire_line = selection.is_empty();
5959 if is_entire_line {
5960 start = Point::new(start.row, 0);"
5961 .to_string()
5962 ),
5963 "Regular copying preserves all indentation selected",
5964 );
5965 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5966 assert_eq!(
5967 cx.read_from_clipboard()
5968 .and_then(|item| item.text().as_deref().map(str::to_string)),
5969 Some(
5970 "for selection in selections.iter() {
5971let mut start = selection.start;
5972let mut end = selection.end;
5973let is_entire_line = selection.is_empty();
5974if is_entire_line {
5975 start = Point::new(start.row, 0);"
5976 .to_string()
5977 ),
5978 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5979 );
5980
5981 cx.set_state(
5982 r#" «ˇ for selection in selections.iter() {
5983 let mut start = selection.start;
5984 let mut end = selection.end;
5985 let is_entire_line = selection.is_empty();
5986 if is_entire_line {
5987 start = Point::new(start.row, 0);»
5988 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5989 }
5990 "#,
5991 );
5992 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5993 assert_eq!(
5994 cx.read_from_clipboard()
5995 .and_then(|item| item.text().as_deref().map(str::to_string)),
5996 Some(
5997 " for selection in selections.iter() {
5998 let mut start = selection.start;
5999 let mut end = selection.end;
6000 let is_entire_line = selection.is_empty();
6001 if is_entire_line {
6002 start = Point::new(start.row, 0);"
6003 .to_string()
6004 ),
6005 "Regular copying for reverse selection works the same",
6006 );
6007 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6008 assert_eq!(
6009 cx.read_from_clipboard()
6010 .and_then(|item| item.text().as_deref().map(str::to_string)),
6011 Some(
6012 "for selection in selections.iter() {
6013let mut start = selection.start;
6014let mut end = selection.end;
6015let is_entire_line = selection.is_empty();
6016if is_entire_line {
6017 start = Point::new(start.row, 0);"
6018 .to_string()
6019 ),
6020 "Copying with stripping for reverse selection works the same"
6021 );
6022
6023 cx.set_state(
6024 r#" for selection «in selections.iter() {
6025 let mut start = selection.start;
6026 let mut end = selection.end;
6027 let is_entire_line = selection.is_empty();
6028 if is_entire_line {
6029 start = Point::new(start.row, 0);ˇ»
6030 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6031 }
6032 "#,
6033 );
6034 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6035 assert_eq!(
6036 cx.read_from_clipboard()
6037 .and_then(|item| item.text().as_deref().map(str::to_string)),
6038 Some(
6039 "in selections.iter() {
6040 let mut start = selection.start;
6041 let mut end = selection.end;
6042 let is_entire_line = selection.is_empty();
6043 if is_entire_line {
6044 start = Point::new(start.row, 0);"
6045 .to_string()
6046 ),
6047 "When selecting past the indent, the copying works as usual",
6048 );
6049 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6050 assert_eq!(
6051 cx.read_from_clipboard()
6052 .and_then(|item| item.text().as_deref().map(str::to_string)),
6053 Some(
6054 "in selections.iter() {
6055 let mut start = selection.start;
6056 let mut end = selection.end;
6057 let is_entire_line = selection.is_empty();
6058 if is_entire_line {
6059 start = Point::new(start.row, 0);"
6060 .to_string()
6061 ),
6062 "When selecting past the indent, nothing is trimmed"
6063 );
6064
6065 cx.set_state(
6066 r#" «for selection in selections.iter() {
6067 let mut start = selection.start;
6068
6069 let mut end = selection.end;
6070 let is_entire_line = selection.is_empty();
6071 if is_entire_line {
6072 start = Point::new(start.row, 0);
6073ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
6074 }
6075 "#,
6076 );
6077 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6078 assert_eq!(
6079 cx.read_from_clipboard()
6080 .and_then(|item| item.text().as_deref().map(str::to_string)),
6081 Some(
6082 "for selection in selections.iter() {
6083let mut start = selection.start;
6084
6085let mut end = selection.end;
6086let is_entire_line = selection.is_empty();
6087if is_entire_line {
6088 start = Point::new(start.row, 0);
6089"
6090 .to_string()
6091 ),
6092 "Copying with stripping should ignore empty lines"
6093 );
6094}
6095
6096#[gpui::test]
6097async fn test_paste_multiline(cx: &mut TestAppContext) {
6098 init_test(cx, |_| {});
6099
6100 let mut cx = EditorTestContext::new(cx).await;
6101 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6102
6103 // Cut an indented block, without the leading whitespace.
6104 cx.set_state(indoc! {"
6105 const a: B = (
6106 c(),
6107 «d(
6108 e,
6109 f
6110 )ˇ»
6111 );
6112 "});
6113 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6114 cx.assert_editor_state(indoc! {"
6115 const a: B = (
6116 c(),
6117 ˇ
6118 );
6119 "});
6120
6121 // Paste it at the same position.
6122 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6123 cx.assert_editor_state(indoc! {"
6124 const a: B = (
6125 c(),
6126 d(
6127 e,
6128 f
6129 )ˇ
6130 );
6131 "});
6132
6133 // Paste it at a line with a lower indent level.
6134 cx.set_state(indoc! {"
6135 ˇ
6136 const a: B = (
6137 c(),
6138 );
6139 "});
6140 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6141 cx.assert_editor_state(indoc! {"
6142 d(
6143 e,
6144 f
6145 )ˇ
6146 const a: B = (
6147 c(),
6148 );
6149 "});
6150
6151 // Cut an indented block, with the leading whitespace.
6152 cx.set_state(indoc! {"
6153 const a: B = (
6154 c(),
6155 « d(
6156 e,
6157 f
6158 )
6159 ˇ»);
6160 "});
6161 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6162 cx.assert_editor_state(indoc! {"
6163 const a: B = (
6164 c(),
6165 ˇ);
6166 "});
6167
6168 // Paste it at the same position.
6169 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6170 cx.assert_editor_state(indoc! {"
6171 const a: B = (
6172 c(),
6173 d(
6174 e,
6175 f
6176 )
6177 ˇ);
6178 "});
6179
6180 // Paste it at a line with a higher indent level.
6181 cx.set_state(indoc! {"
6182 const a: B = (
6183 c(),
6184 d(
6185 e,
6186 fˇ
6187 )
6188 );
6189 "});
6190 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6191 cx.assert_editor_state(indoc! {"
6192 const a: B = (
6193 c(),
6194 d(
6195 e,
6196 f d(
6197 e,
6198 f
6199 )
6200 ˇ
6201 )
6202 );
6203 "});
6204
6205 // Copy an indented block, starting mid-line
6206 cx.set_state(indoc! {"
6207 const a: B = (
6208 c(),
6209 somethin«g(
6210 e,
6211 f
6212 )ˇ»
6213 );
6214 "});
6215 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6216
6217 // Paste it on a line with a lower indent level
6218 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
6219 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6220 cx.assert_editor_state(indoc! {"
6221 const a: B = (
6222 c(),
6223 something(
6224 e,
6225 f
6226 )
6227 );
6228 g(
6229 e,
6230 f
6231 )ˇ"});
6232}
6233
6234#[gpui::test]
6235async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
6236 init_test(cx, |_| {});
6237
6238 cx.write_to_clipboard(ClipboardItem::new_string(
6239 " d(\n e\n );\n".into(),
6240 ));
6241
6242 let mut cx = EditorTestContext::new(cx).await;
6243 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6244
6245 cx.set_state(indoc! {"
6246 fn a() {
6247 b();
6248 if c() {
6249 ˇ
6250 }
6251 }
6252 "});
6253
6254 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6255 cx.assert_editor_state(indoc! {"
6256 fn a() {
6257 b();
6258 if c() {
6259 d(
6260 e
6261 );
6262 ˇ
6263 }
6264 }
6265 "});
6266
6267 cx.set_state(indoc! {"
6268 fn a() {
6269 b();
6270 ˇ
6271 }
6272 "});
6273
6274 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6275 cx.assert_editor_state(indoc! {"
6276 fn a() {
6277 b();
6278 d(
6279 e
6280 );
6281 ˇ
6282 }
6283 "});
6284}
6285
6286#[gpui::test]
6287fn test_select_all(cx: &mut TestAppContext) {
6288 init_test(cx, |_| {});
6289
6290 let editor = cx.add_window(|window, cx| {
6291 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
6292 build_editor(buffer, window, cx)
6293 });
6294 _ = editor.update(cx, |editor, window, cx| {
6295 editor.select_all(&SelectAll, window, cx);
6296 assert_eq!(
6297 editor.selections.display_ranges(cx),
6298 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
6299 );
6300 });
6301}
6302
6303#[gpui::test]
6304fn test_select_line(cx: &mut TestAppContext) {
6305 init_test(cx, |_| {});
6306
6307 let editor = cx.add_window(|window, cx| {
6308 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
6309 build_editor(buffer, window, cx)
6310 });
6311 _ = editor.update(cx, |editor, window, cx| {
6312 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6313 s.select_display_ranges([
6314 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6315 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6316 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6317 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
6318 ])
6319 });
6320 editor.select_line(&SelectLine, window, cx);
6321 assert_eq!(
6322 editor.selections.display_ranges(cx),
6323 vec![
6324 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
6325 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
6326 ]
6327 );
6328 });
6329
6330 _ = editor.update(cx, |editor, window, cx| {
6331 editor.select_line(&SelectLine, window, cx);
6332 assert_eq!(
6333 editor.selections.display_ranges(cx),
6334 vec![
6335 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
6336 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
6337 ]
6338 );
6339 });
6340
6341 _ = editor.update(cx, |editor, window, cx| {
6342 editor.select_line(&SelectLine, window, cx);
6343 assert_eq!(
6344 editor.selections.display_ranges(cx),
6345 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
6346 );
6347 });
6348}
6349
6350#[gpui::test]
6351async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
6352 init_test(cx, |_| {});
6353 let mut cx = EditorTestContext::new(cx).await;
6354
6355 #[track_caller]
6356 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
6357 cx.set_state(initial_state);
6358 cx.update_editor(|e, window, cx| {
6359 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
6360 });
6361 cx.assert_editor_state(expected_state);
6362 }
6363
6364 // Selection starts and ends at the middle of lines, left-to-right
6365 test(
6366 &mut cx,
6367 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
6368 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6369 );
6370 // Same thing, right-to-left
6371 test(
6372 &mut cx,
6373 "aa\nb«b\ncc\ndd\neˇ»e\nff",
6374 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6375 );
6376
6377 // Whole buffer, left-to-right, last line *doesn't* end with newline
6378 test(
6379 &mut cx,
6380 "«ˇaa\nbb\ncc\ndd\nee\nff»",
6381 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6382 );
6383 // Same thing, right-to-left
6384 test(
6385 &mut cx,
6386 "«aa\nbb\ncc\ndd\nee\nffˇ»",
6387 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6388 );
6389
6390 // Whole buffer, left-to-right, last line ends with newline
6391 test(
6392 &mut cx,
6393 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
6394 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6395 );
6396 // Same thing, right-to-left
6397 test(
6398 &mut cx,
6399 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
6400 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6401 );
6402
6403 // Starts at the end of a line, ends at the start of another
6404 test(
6405 &mut cx,
6406 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
6407 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
6408 );
6409}
6410
6411#[gpui::test]
6412async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
6413 init_test(cx, |_| {});
6414
6415 let editor = cx.add_window(|window, cx| {
6416 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
6417 build_editor(buffer, window, cx)
6418 });
6419
6420 // setup
6421 _ = editor.update(cx, |editor, window, cx| {
6422 editor.fold_creases(
6423 vec![
6424 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
6425 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
6426 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
6427 ],
6428 true,
6429 window,
6430 cx,
6431 );
6432 assert_eq!(
6433 editor.display_text(cx),
6434 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6435 );
6436 });
6437
6438 _ = editor.update(cx, |editor, window, cx| {
6439 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6440 s.select_display_ranges([
6441 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6442 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6443 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6444 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
6445 ])
6446 });
6447 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6448 assert_eq!(
6449 editor.display_text(cx),
6450 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6451 );
6452 });
6453 EditorTestContext::for_editor(editor, cx)
6454 .await
6455 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
6456
6457 _ = editor.update(cx, |editor, window, cx| {
6458 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6459 s.select_display_ranges([
6460 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
6461 ])
6462 });
6463 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6464 assert_eq!(
6465 editor.display_text(cx),
6466 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
6467 );
6468 assert_eq!(
6469 editor.selections.display_ranges(cx),
6470 [
6471 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
6472 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
6473 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
6474 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
6475 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
6476 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
6477 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
6478 ]
6479 );
6480 });
6481 EditorTestContext::for_editor(editor, cx)
6482 .await
6483 .assert_editor_state(
6484 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
6485 );
6486}
6487
6488#[gpui::test]
6489async fn test_add_selection_above_below(cx: &mut TestAppContext) {
6490 init_test(cx, |_| {});
6491
6492 let mut cx = EditorTestContext::new(cx).await;
6493
6494 cx.set_state(indoc!(
6495 r#"abc
6496 defˇghi
6497
6498 jk
6499 nlmo
6500 "#
6501 ));
6502
6503 cx.update_editor(|editor, window, cx| {
6504 editor.add_selection_above(&Default::default(), window, cx);
6505 });
6506
6507 cx.assert_editor_state(indoc!(
6508 r#"abcˇ
6509 defˇghi
6510
6511 jk
6512 nlmo
6513 "#
6514 ));
6515
6516 cx.update_editor(|editor, window, cx| {
6517 editor.add_selection_above(&Default::default(), window, cx);
6518 });
6519
6520 cx.assert_editor_state(indoc!(
6521 r#"abcˇ
6522 defˇghi
6523
6524 jk
6525 nlmo
6526 "#
6527 ));
6528
6529 cx.update_editor(|editor, window, cx| {
6530 editor.add_selection_below(&Default::default(), window, cx);
6531 });
6532
6533 cx.assert_editor_state(indoc!(
6534 r#"abc
6535 defˇghi
6536
6537 jk
6538 nlmo
6539 "#
6540 ));
6541
6542 cx.update_editor(|editor, window, cx| {
6543 editor.undo_selection(&Default::default(), window, cx);
6544 });
6545
6546 cx.assert_editor_state(indoc!(
6547 r#"abcˇ
6548 defˇghi
6549
6550 jk
6551 nlmo
6552 "#
6553 ));
6554
6555 cx.update_editor(|editor, window, cx| {
6556 editor.redo_selection(&Default::default(), window, cx);
6557 });
6558
6559 cx.assert_editor_state(indoc!(
6560 r#"abc
6561 defˇghi
6562
6563 jk
6564 nlmo
6565 "#
6566 ));
6567
6568 cx.update_editor(|editor, window, cx| {
6569 editor.add_selection_below(&Default::default(), window, cx);
6570 });
6571
6572 cx.assert_editor_state(indoc!(
6573 r#"abc
6574 defˇghi
6575 ˇ
6576 jk
6577 nlmo
6578 "#
6579 ));
6580
6581 cx.update_editor(|editor, window, cx| {
6582 editor.add_selection_below(&Default::default(), window, cx);
6583 });
6584
6585 cx.assert_editor_state(indoc!(
6586 r#"abc
6587 defˇghi
6588 ˇ
6589 jkˇ
6590 nlmo
6591 "#
6592 ));
6593
6594 cx.update_editor(|editor, window, cx| {
6595 editor.add_selection_below(&Default::default(), window, cx);
6596 });
6597
6598 cx.assert_editor_state(indoc!(
6599 r#"abc
6600 defˇghi
6601 ˇ
6602 jkˇ
6603 nlmˇo
6604 "#
6605 ));
6606
6607 cx.update_editor(|editor, window, cx| {
6608 editor.add_selection_below(&Default::default(), window, cx);
6609 });
6610
6611 cx.assert_editor_state(indoc!(
6612 r#"abc
6613 defˇghi
6614 ˇ
6615 jkˇ
6616 nlmˇo
6617 ˇ"#
6618 ));
6619
6620 // change selections
6621 cx.set_state(indoc!(
6622 r#"abc
6623 def«ˇg»hi
6624
6625 jk
6626 nlmo
6627 "#
6628 ));
6629
6630 cx.update_editor(|editor, window, cx| {
6631 editor.add_selection_below(&Default::default(), window, cx);
6632 });
6633
6634 cx.assert_editor_state(indoc!(
6635 r#"abc
6636 def«ˇg»hi
6637
6638 jk
6639 nlm«ˇo»
6640 "#
6641 ));
6642
6643 cx.update_editor(|editor, window, cx| {
6644 editor.add_selection_below(&Default::default(), window, cx);
6645 });
6646
6647 cx.assert_editor_state(indoc!(
6648 r#"abc
6649 def«ˇg»hi
6650
6651 jk
6652 nlm«ˇo»
6653 "#
6654 ));
6655
6656 cx.update_editor(|editor, window, cx| {
6657 editor.add_selection_above(&Default::default(), window, cx);
6658 });
6659
6660 cx.assert_editor_state(indoc!(
6661 r#"abc
6662 def«ˇg»hi
6663
6664 jk
6665 nlmo
6666 "#
6667 ));
6668
6669 cx.update_editor(|editor, window, cx| {
6670 editor.add_selection_above(&Default::default(), window, cx);
6671 });
6672
6673 cx.assert_editor_state(indoc!(
6674 r#"abc
6675 def«ˇg»hi
6676
6677 jk
6678 nlmo
6679 "#
6680 ));
6681
6682 // Change selections again
6683 cx.set_state(indoc!(
6684 r#"a«bc
6685 defgˇ»hi
6686
6687 jk
6688 nlmo
6689 "#
6690 ));
6691
6692 cx.update_editor(|editor, window, cx| {
6693 editor.add_selection_below(&Default::default(), window, cx);
6694 });
6695
6696 cx.assert_editor_state(indoc!(
6697 r#"a«bcˇ»
6698 d«efgˇ»hi
6699
6700 j«kˇ»
6701 nlmo
6702 "#
6703 ));
6704
6705 cx.update_editor(|editor, window, cx| {
6706 editor.add_selection_below(&Default::default(), window, cx);
6707 });
6708 cx.assert_editor_state(indoc!(
6709 r#"a«bcˇ»
6710 d«efgˇ»hi
6711
6712 j«kˇ»
6713 n«lmoˇ»
6714 "#
6715 ));
6716 cx.update_editor(|editor, window, cx| {
6717 editor.add_selection_above(&Default::default(), window, cx);
6718 });
6719
6720 cx.assert_editor_state(indoc!(
6721 r#"a«bcˇ»
6722 d«efgˇ»hi
6723
6724 j«kˇ»
6725 nlmo
6726 "#
6727 ));
6728
6729 // Change selections again
6730 cx.set_state(indoc!(
6731 r#"abc
6732 d«ˇefghi
6733
6734 jk
6735 nlm»o
6736 "#
6737 ));
6738
6739 cx.update_editor(|editor, window, cx| {
6740 editor.add_selection_above(&Default::default(), window, cx);
6741 });
6742
6743 cx.assert_editor_state(indoc!(
6744 r#"a«ˇbc»
6745 d«ˇef»ghi
6746
6747 j«ˇk»
6748 n«ˇlm»o
6749 "#
6750 ));
6751
6752 cx.update_editor(|editor, window, cx| {
6753 editor.add_selection_below(&Default::default(), window, cx);
6754 });
6755
6756 cx.assert_editor_state(indoc!(
6757 r#"abc
6758 d«ˇef»ghi
6759
6760 j«ˇk»
6761 n«ˇlm»o
6762 "#
6763 ));
6764}
6765
6766#[gpui::test]
6767async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
6768 init_test(cx, |_| {});
6769 let mut cx = EditorTestContext::new(cx).await;
6770
6771 cx.set_state(indoc!(
6772 r#"line onˇe
6773 liˇne two
6774 line three
6775 line four"#
6776 ));
6777
6778 cx.update_editor(|editor, window, cx| {
6779 editor.add_selection_below(&Default::default(), window, cx);
6780 });
6781
6782 // test multiple cursors expand in the same direction
6783 cx.assert_editor_state(indoc!(
6784 r#"line onˇe
6785 liˇne twˇo
6786 liˇne three
6787 line four"#
6788 ));
6789
6790 cx.update_editor(|editor, window, cx| {
6791 editor.add_selection_below(&Default::default(), window, cx);
6792 });
6793
6794 cx.update_editor(|editor, window, cx| {
6795 editor.add_selection_below(&Default::default(), window, cx);
6796 });
6797
6798 // test multiple cursors expand below overflow
6799 cx.assert_editor_state(indoc!(
6800 r#"line onˇe
6801 liˇne twˇo
6802 liˇne thˇree
6803 liˇne foˇur"#
6804 ));
6805
6806 cx.update_editor(|editor, window, cx| {
6807 editor.add_selection_above(&Default::default(), window, cx);
6808 });
6809
6810 // test multiple cursors retrieves back correctly
6811 cx.assert_editor_state(indoc!(
6812 r#"line onˇe
6813 liˇne twˇo
6814 liˇne thˇree
6815 line four"#
6816 ));
6817
6818 cx.update_editor(|editor, window, cx| {
6819 editor.add_selection_above(&Default::default(), window, cx);
6820 });
6821
6822 cx.update_editor(|editor, window, cx| {
6823 editor.add_selection_above(&Default::default(), window, cx);
6824 });
6825
6826 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
6827 cx.assert_editor_state(indoc!(
6828 r#"liˇne onˇe
6829 liˇne two
6830 line three
6831 line four"#
6832 ));
6833
6834 cx.update_editor(|editor, window, cx| {
6835 editor.undo_selection(&Default::default(), window, cx);
6836 });
6837
6838 // test undo
6839 cx.assert_editor_state(indoc!(
6840 r#"line onˇe
6841 liˇne twˇo
6842 line three
6843 line four"#
6844 ));
6845
6846 cx.update_editor(|editor, window, cx| {
6847 editor.redo_selection(&Default::default(), window, cx);
6848 });
6849
6850 // test redo
6851 cx.assert_editor_state(indoc!(
6852 r#"liˇne onˇe
6853 liˇne two
6854 line three
6855 line four"#
6856 ));
6857
6858 cx.set_state(indoc!(
6859 r#"abcd
6860 ef«ghˇ»
6861 ijkl
6862 «mˇ»nop"#
6863 ));
6864
6865 cx.update_editor(|editor, window, cx| {
6866 editor.add_selection_above(&Default::default(), window, cx);
6867 });
6868
6869 // test multiple selections expand in the same direction
6870 cx.assert_editor_state(indoc!(
6871 r#"ab«cdˇ»
6872 ef«ghˇ»
6873 «iˇ»jkl
6874 «mˇ»nop"#
6875 ));
6876
6877 cx.update_editor(|editor, window, cx| {
6878 editor.add_selection_above(&Default::default(), window, cx);
6879 });
6880
6881 // test multiple selection upward overflow
6882 cx.assert_editor_state(indoc!(
6883 r#"ab«cdˇ»
6884 «eˇ»f«ghˇ»
6885 «iˇ»jkl
6886 «mˇ»nop"#
6887 ));
6888
6889 cx.update_editor(|editor, window, cx| {
6890 editor.add_selection_below(&Default::default(), window, cx);
6891 });
6892
6893 // test multiple selection retrieves back correctly
6894 cx.assert_editor_state(indoc!(
6895 r#"abcd
6896 ef«ghˇ»
6897 «iˇ»jkl
6898 «mˇ»nop"#
6899 ));
6900
6901 cx.update_editor(|editor, window, cx| {
6902 editor.add_selection_below(&Default::default(), window, cx);
6903 });
6904
6905 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
6906 cx.assert_editor_state(indoc!(
6907 r#"abcd
6908 ef«ghˇ»
6909 ij«klˇ»
6910 «mˇ»nop"#
6911 ));
6912
6913 cx.update_editor(|editor, window, cx| {
6914 editor.undo_selection(&Default::default(), window, cx);
6915 });
6916
6917 // test undo
6918 cx.assert_editor_state(indoc!(
6919 r#"abcd
6920 ef«ghˇ»
6921 «iˇ»jkl
6922 «mˇ»nop"#
6923 ));
6924
6925 cx.update_editor(|editor, window, cx| {
6926 editor.redo_selection(&Default::default(), window, cx);
6927 });
6928
6929 // test redo
6930 cx.assert_editor_state(indoc!(
6931 r#"abcd
6932 ef«ghˇ»
6933 ij«klˇ»
6934 «mˇ»nop"#
6935 ));
6936}
6937
6938#[gpui::test]
6939async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
6940 init_test(cx, |_| {});
6941 let mut cx = EditorTestContext::new(cx).await;
6942
6943 cx.set_state(indoc!(
6944 r#"line onˇe
6945 liˇne two
6946 line three
6947 line four"#
6948 ));
6949
6950 cx.update_editor(|editor, window, cx| {
6951 editor.add_selection_below(&Default::default(), window, cx);
6952 editor.add_selection_below(&Default::default(), window, cx);
6953 editor.add_selection_below(&Default::default(), window, cx);
6954 });
6955
6956 // initial state with two multi cursor groups
6957 cx.assert_editor_state(indoc!(
6958 r#"line onˇe
6959 liˇne twˇo
6960 liˇne thˇree
6961 liˇne foˇur"#
6962 ));
6963
6964 // add single cursor in middle - simulate opt click
6965 cx.update_editor(|editor, window, cx| {
6966 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
6967 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6968 editor.end_selection(window, cx);
6969 });
6970
6971 cx.assert_editor_state(indoc!(
6972 r#"line onˇe
6973 liˇne twˇo
6974 liˇneˇ thˇree
6975 liˇne foˇur"#
6976 ));
6977
6978 cx.update_editor(|editor, window, cx| {
6979 editor.add_selection_above(&Default::default(), window, cx);
6980 });
6981
6982 // test new added selection expands above and existing selection shrinks
6983 cx.assert_editor_state(indoc!(
6984 r#"line onˇe
6985 liˇneˇ twˇo
6986 liˇneˇ thˇree
6987 line four"#
6988 ));
6989
6990 cx.update_editor(|editor, window, cx| {
6991 editor.add_selection_above(&Default::default(), window, cx);
6992 });
6993
6994 // test new added selection expands above and existing selection shrinks
6995 cx.assert_editor_state(indoc!(
6996 r#"lineˇ onˇe
6997 liˇneˇ twˇo
6998 lineˇ three
6999 line four"#
7000 ));
7001
7002 // intial state with two selection groups
7003 cx.set_state(indoc!(
7004 r#"abcd
7005 ef«ghˇ»
7006 ijkl
7007 «mˇ»nop"#
7008 ));
7009
7010 cx.update_editor(|editor, window, cx| {
7011 editor.add_selection_above(&Default::default(), window, cx);
7012 editor.add_selection_above(&Default::default(), window, cx);
7013 });
7014
7015 cx.assert_editor_state(indoc!(
7016 r#"ab«cdˇ»
7017 «eˇ»f«ghˇ»
7018 «iˇ»jkl
7019 «mˇ»nop"#
7020 ));
7021
7022 // add single selection in middle - simulate opt drag
7023 cx.update_editor(|editor, window, cx| {
7024 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
7025 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7026 editor.update_selection(
7027 DisplayPoint::new(DisplayRow(2), 4),
7028 0,
7029 gpui::Point::<f32>::default(),
7030 window,
7031 cx,
7032 );
7033 editor.end_selection(window, cx);
7034 });
7035
7036 cx.assert_editor_state(indoc!(
7037 r#"ab«cdˇ»
7038 «eˇ»f«ghˇ»
7039 «iˇ»jk«lˇ»
7040 «mˇ»nop"#
7041 ));
7042
7043 cx.update_editor(|editor, window, cx| {
7044 editor.add_selection_below(&Default::default(), window, cx);
7045 });
7046
7047 // test new added selection expands below, others shrinks from above
7048 cx.assert_editor_state(indoc!(
7049 r#"abcd
7050 ef«ghˇ»
7051 «iˇ»jk«lˇ»
7052 «mˇ»no«pˇ»"#
7053 ));
7054}
7055
7056#[gpui::test]
7057async fn test_select_next(cx: &mut TestAppContext) {
7058 init_test(cx, |_| {});
7059
7060 let mut cx = EditorTestContext::new(cx).await;
7061 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7062
7063 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7064 .unwrap();
7065 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7066
7067 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7068 .unwrap();
7069 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7070
7071 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7072 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7073
7074 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7075 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7076
7077 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7078 .unwrap();
7079 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7080
7081 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7082 .unwrap();
7083 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7084
7085 // Test selection direction should be preserved
7086 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7087
7088 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7089 .unwrap();
7090 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
7091}
7092
7093#[gpui::test]
7094async fn test_select_all_matches(cx: &mut TestAppContext) {
7095 init_test(cx, |_| {});
7096
7097 let mut cx = EditorTestContext::new(cx).await;
7098
7099 // Test caret-only selections
7100 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7101 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7102 .unwrap();
7103 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7104
7105 // Test left-to-right selections
7106 cx.set_state("abc\n«abcˇ»\nabc");
7107 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7108 .unwrap();
7109 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
7110
7111 // Test right-to-left selections
7112 cx.set_state("abc\n«ˇabc»\nabc");
7113 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7114 .unwrap();
7115 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
7116
7117 // Test selecting whitespace with caret selection
7118 cx.set_state("abc\nˇ abc\nabc");
7119 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7120 .unwrap();
7121 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7122
7123 // Test selecting whitespace with left-to-right selection
7124 cx.set_state("abc\n«ˇ »abc\nabc");
7125 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7126 .unwrap();
7127 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
7128
7129 // Test no matches with right-to-left selection
7130 cx.set_state("abc\n« ˇ»abc\nabc");
7131 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7132 .unwrap();
7133 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7134
7135 // Test with a single word and clip_at_line_ends=true (#29823)
7136 cx.set_state("aˇbc");
7137 cx.update_editor(|e, window, cx| {
7138 e.set_clip_at_line_ends(true, cx);
7139 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
7140 e.set_clip_at_line_ends(false, cx);
7141 });
7142 cx.assert_editor_state("«abcˇ»");
7143}
7144
7145#[gpui::test]
7146async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
7147 init_test(cx, |_| {});
7148
7149 let mut cx = EditorTestContext::new(cx).await;
7150
7151 let large_body_1 = "\nd".repeat(200);
7152 let large_body_2 = "\ne".repeat(200);
7153
7154 cx.set_state(&format!(
7155 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
7156 ));
7157 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
7158 let scroll_position = editor.scroll_position(cx);
7159 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
7160 scroll_position
7161 });
7162
7163 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7164 .unwrap();
7165 cx.assert_editor_state(&format!(
7166 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
7167 ));
7168 let scroll_position_after_selection =
7169 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
7170 assert_eq!(
7171 initial_scroll_position, scroll_position_after_selection,
7172 "Scroll position should not change after selecting all matches"
7173 );
7174}
7175
7176#[gpui::test]
7177async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
7178 init_test(cx, |_| {});
7179
7180 let mut cx = EditorLspTestContext::new_rust(
7181 lsp::ServerCapabilities {
7182 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7183 ..Default::default()
7184 },
7185 cx,
7186 )
7187 .await;
7188
7189 cx.set_state(indoc! {"
7190 line 1
7191 line 2
7192 linˇe 3
7193 line 4
7194 line 5
7195 "});
7196
7197 // Make an edit
7198 cx.update_editor(|editor, window, cx| {
7199 editor.handle_input("X", window, cx);
7200 });
7201
7202 // Move cursor to a different position
7203 cx.update_editor(|editor, window, cx| {
7204 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7205 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
7206 });
7207 });
7208
7209 cx.assert_editor_state(indoc! {"
7210 line 1
7211 line 2
7212 linXe 3
7213 line 4
7214 liˇne 5
7215 "});
7216
7217 cx.lsp
7218 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
7219 Ok(Some(vec![lsp::TextEdit::new(
7220 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
7221 "PREFIX ".to_string(),
7222 )]))
7223 });
7224
7225 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
7226 .unwrap()
7227 .await
7228 .unwrap();
7229
7230 cx.assert_editor_state(indoc! {"
7231 PREFIX line 1
7232 line 2
7233 linXe 3
7234 line 4
7235 liˇne 5
7236 "});
7237
7238 // Undo formatting
7239 cx.update_editor(|editor, window, cx| {
7240 editor.undo(&Default::default(), window, cx);
7241 });
7242
7243 // Verify cursor moved back to position after edit
7244 cx.assert_editor_state(indoc! {"
7245 line 1
7246 line 2
7247 linXˇe 3
7248 line 4
7249 line 5
7250 "});
7251}
7252
7253#[gpui::test]
7254async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
7255 init_test(cx, |_| {});
7256
7257 let mut cx = EditorTestContext::new(cx).await;
7258
7259 let provider = cx.new(|_| FakeEditPredictionProvider::default());
7260 cx.update_editor(|editor, window, cx| {
7261 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
7262 });
7263
7264 cx.set_state(indoc! {"
7265 line 1
7266 line 2
7267 linˇe 3
7268 line 4
7269 line 5
7270 line 6
7271 line 7
7272 line 8
7273 line 9
7274 line 10
7275 "});
7276
7277 let snapshot = cx.buffer_snapshot();
7278 let edit_position = snapshot.anchor_after(Point::new(2, 4));
7279
7280 cx.update(|_, cx| {
7281 provider.update(cx, |provider, _| {
7282 provider.set_edit_prediction(Some(edit_prediction::EditPrediction {
7283 id: None,
7284 edits: vec![(edit_position..edit_position, "X".into())],
7285 edit_preview: None,
7286 }))
7287 })
7288 });
7289
7290 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
7291 cx.update_editor(|editor, window, cx| {
7292 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
7293 });
7294
7295 cx.assert_editor_state(indoc! {"
7296 line 1
7297 line 2
7298 lineXˇ 3
7299 line 4
7300 line 5
7301 line 6
7302 line 7
7303 line 8
7304 line 9
7305 line 10
7306 "});
7307
7308 cx.update_editor(|editor, window, cx| {
7309 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7310 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
7311 });
7312 });
7313
7314 cx.assert_editor_state(indoc! {"
7315 line 1
7316 line 2
7317 lineX 3
7318 line 4
7319 line 5
7320 line 6
7321 line 7
7322 line 8
7323 line 9
7324 liˇne 10
7325 "});
7326
7327 cx.update_editor(|editor, window, cx| {
7328 editor.undo(&Default::default(), window, cx);
7329 });
7330
7331 cx.assert_editor_state(indoc! {"
7332 line 1
7333 line 2
7334 lineˇ 3
7335 line 4
7336 line 5
7337 line 6
7338 line 7
7339 line 8
7340 line 9
7341 line 10
7342 "});
7343}
7344
7345#[gpui::test]
7346async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
7347 init_test(cx, |_| {});
7348
7349 let mut cx = EditorTestContext::new(cx).await;
7350 cx.set_state(
7351 r#"let foo = 2;
7352lˇet foo = 2;
7353let fooˇ = 2;
7354let foo = 2;
7355let foo = ˇ2;"#,
7356 );
7357
7358 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7359 .unwrap();
7360 cx.assert_editor_state(
7361 r#"let foo = 2;
7362«letˇ» foo = 2;
7363let «fooˇ» = 2;
7364let foo = 2;
7365let foo = «2ˇ»;"#,
7366 );
7367
7368 // noop for multiple selections with different contents
7369 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7370 .unwrap();
7371 cx.assert_editor_state(
7372 r#"let foo = 2;
7373«letˇ» foo = 2;
7374let «fooˇ» = 2;
7375let foo = 2;
7376let foo = «2ˇ»;"#,
7377 );
7378
7379 // Test last selection direction should be preserved
7380 cx.set_state(
7381 r#"let foo = 2;
7382let foo = 2;
7383let «fooˇ» = 2;
7384let «ˇfoo» = 2;
7385let foo = 2;"#,
7386 );
7387
7388 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7389 .unwrap();
7390 cx.assert_editor_state(
7391 r#"let foo = 2;
7392let foo = 2;
7393let «fooˇ» = 2;
7394let «ˇfoo» = 2;
7395let «ˇfoo» = 2;"#,
7396 );
7397}
7398
7399#[gpui::test]
7400async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
7401 init_test(cx, |_| {});
7402
7403 let mut cx =
7404 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
7405
7406 cx.assert_editor_state(indoc! {"
7407 ˇbbb
7408 ccc
7409
7410 bbb
7411 ccc
7412 "});
7413 cx.dispatch_action(SelectPrevious::default());
7414 cx.assert_editor_state(indoc! {"
7415 «bbbˇ»
7416 ccc
7417
7418 bbb
7419 ccc
7420 "});
7421 cx.dispatch_action(SelectPrevious::default());
7422 cx.assert_editor_state(indoc! {"
7423 «bbbˇ»
7424 ccc
7425
7426 «bbbˇ»
7427 ccc
7428 "});
7429}
7430
7431#[gpui::test]
7432async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
7433 init_test(cx, |_| {});
7434
7435 let mut cx = EditorTestContext::new(cx).await;
7436 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7437
7438 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7439 .unwrap();
7440 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7441
7442 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7443 .unwrap();
7444 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7445
7446 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7447 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7448
7449 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7450 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7451
7452 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7453 .unwrap();
7454 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
7455
7456 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7457 .unwrap();
7458 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7459}
7460
7461#[gpui::test]
7462async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
7463 init_test(cx, |_| {});
7464
7465 let mut cx = EditorTestContext::new(cx).await;
7466 cx.set_state("aˇ");
7467
7468 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7469 .unwrap();
7470 cx.assert_editor_state("«aˇ»");
7471 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7472 .unwrap();
7473 cx.assert_editor_state("«aˇ»");
7474}
7475
7476#[gpui::test]
7477async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
7478 init_test(cx, |_| {});
7479
7480 let mut cx = EditorTestContext::new(cx).await;
7481 cx.set_state(
7482 r#"let foo = 2;
7483lˇet foo = 2;
7484let fooˇ = 2;
7485let foo = 2;
7486let foo = ˇ2;"#,
7487 );
7488
7489 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7490 .unwrap();
7491 cx.assert_editor_state(
7492 r#"let foo = 2;
7493«letˇ» foo = 2;
7494let «fooˇ» = 2;
7495let foo = 2;
7496let foo = «2ˇ»;"#,
7497 );
7498
7499 // noop for multiple selections with different contents
7500 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7501 .unwrap();
7502 cx.assert_editor_state(
7503 r#"let foo = 2;
7504«letˇ» foo = 2;
7505let «fooˇ» = 2;
7506let foo = 2;
7507let foo = «2ˇ»;"#,
7508 );
7509}
7510
7511#[gpui::test]
7512async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
7513 init_test(cx, |_| {});
7514
7515 let mut cx = EditorTestContext::new(cx).await;
7516 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7517
7518 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7519 .unwrap();
7520 // selection direction is preserved
7521 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7522
7523 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7524 .unwrap();
7525 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7526
7527 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7528 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7529
7530 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7531 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7532
7533 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7534 .unwrap();
7535 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
7536
7537 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7538 .unwrap();
7539 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
7540}
7541
7542#[gpui::test]
7543async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
7544 init_test(cx, |_| {});
7545
7546 let language = Arc::new(Language::new(
7547 LanguageConfig::default(),
7548 Some(tree_sitter_rust::LANGUAGE.into()),
7549 ));
7550
7551 let text = r#"
7552 use mod1::mod2::{mod3, mod4};
7553
7554 fn fn_1(param1: bool, param2: &str) {
7555 let var1 = "text";
7556 }
7557 "#
7558 .unindent();
7559
7560 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7561 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7562 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7563
7564 editor
7565 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7566 .await;
7567
7568 editor.update_in(cx, |editor, window, cx| {
7569 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7570 s.select_display_ranges([
7571 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
7572 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
7573 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
7574 ]);
7575 });
7576 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7577 });
7578 editor.update(cx, |editor, cx| {
7579 assert_text_with_selections(
7580 editor,
7581 indoc! {r#"
7582 use mod1::mod2::{mod3, «mod4ˇ»};
7583
7584 fn fn_1«ˇ(param1: bool, param2: &str)» {
7585 let var1 = "«ˇtext»";
7586 }
7587 "#},
7588 cx,
7589 );
7590 });
7591
7592 editor.update_in(cx, |editor, window, cx| {
7593 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7594 });
7595 editor.update(cx, |editor, cx| {
7596 assert_text_with_selections(
7597 editor,
7598 indoc! {r#"
7599 use mod1::mod2::«{mod3, mod4}ˇ»;
7600
7601 «ˇfn fn_1(param1: bool, param2: &str) {
7602 let var1 = "text";
7603 }»
7604 "#},
7605 cx,
7606 );
7607 });
7608
7609 editor.update_in(cx, |editor, window, cx| {
7610 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7611 });
7612 assert_eq!(
7613 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7614 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7615 );
7616
7617 // Trying to expand the selected syntax node one more time has no effect.
7618 editor.update_in(cx, |editor, window, cx| {
7619 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7620 });
7621 assert_eq!(
7622 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7623 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7624 );
7625
7626 editor.update_in(cx, |editor, window, cx| {
7627 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7628 });
7629 editor.update(cx, |editor, cx| {
7630 assert_text_with_selections(
7631 editor,
7632 indoc! {r#"
7633 use mod1::mod2::«{mod3, mod4}ˇ»;
7634
7635 «ˇfn fn_1(param1: bool, param2: &str) {
7636 let var1 = "text";
7637 }»
7638 "#},
7639 cx,
7640 );
7641 });
7642
7643 editor.update_in(cx, |editor, window, cx| {
7644 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7645 });
7646 editor.update(cx, |editor, cx| {
7647 assert_text_with_selections(
7648 editor,
7649 indoc! {r#"
7650 use mod1::mod2::{mod3, «mod4ˇ»};
7651
7652 fn fn_1«ˇ(param1: bool, param2: &str)» {
7653 let var1 = "«ˇtext»";
7654 }
7655 "#},
7656 cx,
7657 );
7658 });
7659
7660 editor.update_in(cx, |editor, window, cx| {
7661 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7662 });
7663 editor.update(cx, |editor, cx| {
7664 assert_text_with_selections(
7665 editor,
7666 indoc! {r#"
7667 use mod1::mod2::{mod3, mo«ˇ»d4};
7668
7669 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7670 let var1 = "te«ˇ»xt";
7671 }
7672 "#},
7673 cx,
7674 );
7675 });
7676
7677 // Trying to shrink the selected syntax node one more time has no effect.
7678 editor.update_in(cx, |editor, window, cx| {
7679 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7680 });
7681 editor.update_in(cx, |editor, _, cx| {
7682 assert_text_with_selections(
7683 editor,
7684 indoc! {r#"
7685 use mod1::mod2::{mod3, mo«ˇ»d4};
7686
7687 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7688 let var1 = "te«ˇ»xt";
7689 }
7690 "#},
7691 cx,
7692 );
7693 });
7694
7695 // Ensure that we keep expanding the selection if the larger selection starts or ends within
7696 // a fold.
7697 editor.update_in(cx, |editor, window, cx| {
7698 editor.fold_creases(
7699 vec![
7700 Crease::simple(
7701 Point::new(0, 21)..Point::new(0, 24),
7702 FoldPlaceholder::test(),
7703 ),
7704 Crease::simple(
7705 Point::new(3, 20)..Point::new(3, 22),
7706 FoldPlaceholder::test(),
7707 ),
7708 ],
7709 true,
7710 window,
7711 cx,
7712 );
7713 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7714 });
7715 editor.update(cx, |editor, cx| {
7716 assert_text_with_selections(
7717 editor,
7718 indoc! {r#"
7719 use mod1::mod2::«{mod3, mod4}ˇ»;
7720
7721 fn fn_1«ˇ(param1: bool, param2: &str)» {
7722 let var1 = "«ˇtext»";
7723 }
7724 "#},
7725 cx,
7726 );
7727 });
7728}
7729
7730#[gpui::test]
7731async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
7732 init_test(cx, |_| {});
7733
7734 let language = Arc::new(Language::new(
7735 LanguageConfig::default(),
7736 Some(tree_sitter_rust::LANGUAGE.into()),
7737 ));
7738
7739 let text = "let a = 2;";
7740
7741 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7742 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7743 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7744
7745 editor
7746 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7747 .await;
7748
7749 // Test case 1: Cursor at end of word
7750 editor.update_in(cx, |editor, window, cx| {
7751 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7752 s.select_display_ranges([
7753 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
7754 ]);
7755 });
7756 });
7757 editor.update(cx, |editor, cx| {
7758 assert_text_with_selections(editor, "let aˇ = 2;", cx);
7759 });
7760 editor.update_in(cx, |editor, window, cx| {
7761 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7762 });
7763 editor.update(cx, |editor, cx| {
7764 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
7765 });
7766 editor.update_in(cx, |editor, window, cx| {
7767 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7768 });
7769 editor.update(cx, |editor, cx| {
7770 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7771 });
7772
7773 // Test case 2: Cursor at end of statement
7774 editor.update_in(cx, |editor, window, cx| {
7775 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7776 s.select_display_ranges([
7777 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
7778 ]);
7779 });
7780 });
7781 editor.update(cx, |editor, cx| {
7782 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
7783 });
7784 editor.update_in(cx, |editor, window, cx| {
7785 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7786 });
7787 editor.update(cx, |editor, cx| {
7788 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7789 });
7790}
7791
7792#[gpui::test]
7793async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
7794 init_test(cx, |_| {});
7795
7796 let language = Arc::new(Language::new(
7797 LanguageConfig::default(),
7798 Some(tree_sitter_rust::LANGUAGE.into()),
7799 ));
7800
7801 let text = r#"
7802 use mod1::mod2::{mod3, mod4};
7803
7804 fn fn_1(param1: bool, param2: &str) {
7805 let var1 = "hello world";
7806 }
7807 "#
7808 .unindent();
7809
7810 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7811 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7812 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7813
7814 editor
7815 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7816 .await;
7817
7818 // Test 1: Cursor on a letter of a string word
7819 editor.update_in(cx, |editor, window, cx| {
7820 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7821 s.select_display_ranges([
7822 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
7823 ]);
7824 });
7825 });
7826 editor.update_in(cx, |editor, window, cx| {
7827 assert_text_with_selections(
7828 editor,
7829 indoc! {r#"
7830 use mod1::mod2::{mod3, mod4};
7831
7832 fn fn_1(param1: bool, param2: &str) {
7833 let var1 = "hˇello world";
7834 }
7835 "#},
7836 cx,
7837 );
7838 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7839 assert_text_with_selections(
7840 editor,
7841 indoc! {r#"
7842 use mod1::mod2::{mod3, mod4};
7843
7844 fn fn_1(param1: bool, param2: &str) {
7845 let var1 = "«ˇhello» world";
7846 }
7847 "#},
7848 cx,
7849 );
7850 });
7851
7852 // Test 2: Partial selection within a word
7853 editor.update_in(cx, |editor, window, cx| {
7854 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7855 s.select_display_ranges([
7856 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7857 ]);
7858 });
7859 });
7860 editor.update_in(cx, |editor, window, cx| {
7861 assert_text_with_selections(
7862 editor,
7863 indoc! {r#"
7864 use mod1::mod2::{mod3, mod4};
7865
7866 fn fn_1(param1: bool, param2: &str) {
7867 let var1 = "h«elˇ»lo world";
7868 }
7869 "#},
7870 cx,
7871 );
7872 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7873 assert_text_with_selections(
7874 editor,
7875 indoc! {r#"
7876 use mod1::mod2::{mod3, mod4};
7877
7878 fn fn_1(param1: bool, param2: &str) {
7879 let var1 = "«ˇhello» world";
7880 }
7881 "#},
7882 cx,
7883 );
7884 });
7885
7886 // Test 3: Complete word already selected
7887 editor.update_in(cx, |editor, window, cx| {
7888 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7889 s.select_display_ranges([
7890 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7891 ]);
7892 });
7893 });
7894 editor.update_in(cx, |editor, window, cx| {
7895 assert_text_with_selections(
7896 editor,
7897 indoc! {r#"
7898 use mod1::mod2::{mod3, mod4};
7899
7900 fn fn_1(param1: bool, param2: &str) {
7901 let var1 = "«helloˇ» world";
7902 }
7903 "#},
7904 cx,
7905 );
7906 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7907 assert_text_with_selections(
7908 editor,
7909 indoc! {r#"
7910 use mod1::mod2::{mod3, mod4};
7911
7912 fn fn_1(param1: bool, param2: &str) {
7913 let var1 = "«hello worldˇ»";
7914 }
7915 "#},
7916 cx,
7917 );
7918 });
7919
7920 // Test 4: Selection spanning across words
7921 editor.update_in(cx, |editor, window, cx| {
7922 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7923 s.select_display_ranges([
7924 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7925 ]);
7926 });
7927 });
7928 editor.update_in(cx, |editor, window, cx| {
7929 assert_text_with_selections(
7930 editor,
7931 indoc! {r#"
7932 use mod1::mod2::{mod3, mod4};
7933
7934 fn fn_1(param1: bool, param2: &str) {
7935 let var1 = "hel«lo woˇ»rld";
7936 }
7937 "#},
7938 cx,
7939 );
7940 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7941 assert_text_with_selections(
7942 editor,
7943 indoc! {r#"
7944 use mod1::mod2::{mod3, mod4};
7945
7946 fn fn_1(param1: bool, param2: &str) {
7947 let var1 = "«ˇhello world»";
7948 }
7949 "#},
7950 cx,
7951 );
7952 });
7953
7954 // Test 5: Expansion beyond string
7955 editor.update_in(cx, |editor, window, cx| {
7956 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7957 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7958 assert_text_with_selections(
7959 editor,
7960 indoc! {r#"
7961 use mod1::mod2::{mod3, mod4};
7962
7963 fn fn_1(param1: bool, param2: &str) {
7964 «ˇlet var1 = "hello world";»
7965 }
7966 "#},
7967 cx,
7968 );
7969 });
7970}
7971
7972#[gpui::test]
7973async fn test_unwrap_syntax_node(cx: &mut gpui::TestAppContext) {
7974 init_test(cx, |_| {});
7975
7976 let mut cx = EditorTestContext::new(cx).await;
7977
7978 let language = Arc::new(Language::new(
7979 LanguageConfig::default(),
7980 Some(tree_sitter_rust::LANGUAGE.into()),
7981 ));
7982
7983 cx.update_buffer(|buffer, cx| {
7984 buffer.set_language(Some(language), cx);
7985 });
7986
7987 cx.set_state(
7988 &r#"
7989 use mod1::mod2::{«mod3ˇ», mod4};
7990 "#
7991 .unindent(),
7992 );
7993 cx.update_editor(|editor, window, cx| {
7994 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
7995 });
7996 cx.assert_editor_state(
7997 &r#"
7998 use mod1::mod2::«mod3ˇ»;
7999 "#
8000 .unindent(),
8001 );
8002}
8003
8004#[gpui::test]
8005async fn test_fold_function_bodies(cx: &mut TestAppContext) {
8006 init_test(cx, |_| {});
8007
8008 let base_text = r#"
8009 impl A {
8010 // this is an uncommitted comment
8011
8012 fn b() {
8013 c();
8014 }
8015
8016 // this is another uncommitted comment
8017
8018 fn d() {
8019 // e
8020 // f
8021 }
8022 }
8023
8024 fn g() {
8025 // h
8026 }
8027 "#
8028 .unindent();
8029
8030 let text = r#"
8031 ˇimpl A {
8032
8033 fn b() {
8034 c();
8035 }
8036
8037 fn d() {
8038 // e
8039 // f
8040 }
8041 }
8042
8043 fn g() {
8044 // h
8045 }
8046 "#
8047 .unindent();
8048
8049 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8050 cx.set_state(&text);
8051 cx.set_head_text(&base_text);
8052 cx.update_editor(|editor, window, cx| {
8053 editor.expand_all_diff_hunks(&Default::default(), window, cx);
8054 });
8055
8056 cx.assert_state_with_diff(
8057 "
8058 ˇimpl A {
8059 - // this is an uncommitted comment
8060
8061 fn b() {
8062 c();
8063 }
8064
8065 - // this is another uncommitted comment
8066 -
8067 fn d() {
8068 // e
8069 // f
8070 }
8071 }
8072
8073 fn g() {
8074 // h
8075 }
8076 "
8077 .unindent(),
8078 );
8079
8080 let expected_display_text = "
8081 impl A {
8082 // this is an uncommitted comment
8083
8084 fn b() {
8085 ⋯
8086 }
8087
8088 // this is another uncommitted comment
8089
8090 fn d() {
8091 ⋯
8092 }
8093 }
8094
8095 fn g() {
8096 ⋯
8097 }
8098 "
8099 .unindent();
8100
8101 cx.update_editor(|editor, window, cx| {
8102 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
8103 assert_eq!(editor.display_text(cx), expected_display_text);
8104 });
8105}
8106
8107#[gpui::test]
8108async fn test_autoindent(cx: &mut TestAppContext) {
8109 init_test(cx, |_| {});
8110
8111 let language = Arc::new(
8112 Language::new(
8113 LanguageConfig {
8114 brackets: BracketPairConfig {
8115 pairs: vec![
8116 BracketPair {
8117 start: "{".to_string(),
8118 end: "}".to_string(),
8119 close: false,
8120 surround: false,
8121 newline: true,
8122 },
8123 BracketPair {
8124 start: "(".to_string(),
8125 end: ")".to_string(),
8126 close: false,
8127 surround: false,
8128 newline: true,
8129 },
8130 ],
8131 ..Default::default()
8132 },
8133 ..Default::default()
8134 },
8135 Some(tree_sitter_rust::LANGUAGE.into()),
8136 )
8137 .with_indents_query(
8138 r#"
8139 (_ "(" ")" @end) @indent
8140 (_ "{" "}" @end) @indent
8141 "#,
8142 )
8143 .unwrap(),
8144 );
8145
8146 let text = "fn a() {}";
8147
8148 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8149 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8150 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8151 editor
8152 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8153 .await;
8154
8155 editor.update_in(cx, |editor, window, cx| {
8156 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8157 s.select_ranges([5..5, 8..8, 9..9])
8158 });
8159 editor.newline(&Newline, window, cx);
8160 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
8161 assert_eq!(
8162 editor.selections.ranges(cx),
8163 &[
8164 Point::new(1, 4)..Point::new(1, 4),
8165 Point::new(3, 4)..Point::new(3, 4),
8166 Point::new(5, 0)..Point::new(5, 0)
8167 ]
8168 );
8169 });
8170}
8171
8172#[gpui::test]
8173async fn test_autoindent_selections(cx: &mut TestAppContext) {
8174 init_test(cx, |_| {});
8175
8176 {
8177 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8178 cx.set_state(indoc! {"
8179 impl A {
8180
8181 fn b() {}
8182
8183 «fn c() {
8184
8185 }ˇ»
8186 }
8187 "});
8188
8189 cx.update_editor(|editor, window, cx| {
8190 editor.autoindent(&Default::default(), window, cx);
8191 });
8192
8193 cx.assert_editor_state(indoc! {"
8194 impl A {
8195
8196 fn b() {}
8197
8198 «fn c() {
8199
8200 }ˇ»
8201 }
8202 "});
8203 }
8204
8205 {
8206 let mut cx = EditorTestContext::new_multibuffer(
8207 cx,
8208 [indoc! { "
8209 impl A {
8210 «
8211 // a
8212 fn b(){}
8213 »
8214 «
8215 }
8216 fn c(){}
8217 »
8218 "}],
8219 );
8220
8221 let buffer = cx.update_editor(|editor, _, cx| {
8222 let buffer = editor.buffer().update(cx, |buffer, _| {
8223 buffer.all_buffers().iter().next().unwrap().clone()
8224 });
8225 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
8226 buffer
8227 });
8228
8229 cx.run_until_parked();
8230 cx.update_editor(|editor, window, cx| {
8231 editor.select_all(&Default::default(), window, cx);
8232 editor.autoindent(&Default::default(), window, cx)
8233 });
8234 cx.run_until_parked();
8235
8236 cx.update(|_, cx| {
8237 assert_eq!(
8238 buffer.read(cx).text(),
8239 indoc! { "
8240 impl A {
8241
8242 // a
8243 fn b(){}
8244
8245
8246 }
8247 fn c(){}
8248
8249 " }
8250 )
8251 });
8252 }
8253}
8254
8255#[gpui::test]
8256async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
8257 init_test(cx, |_| {});
8258
8259 let mut cx = EditorTestContext::new(cx).await;
8260
8261 let language = Arc::new(Language::new(
8262 LanguageConfig {
8263 brackets: BracketPairConfig {
8264 pairs: vec![
8265 BracketPair {
8266 start: "{".to_string(),
8267 end: "}".to_string(),
8268 close: true,
8269 surround: true,
8270 newline: true,
8271 },
8272 BracketPair {
8273 start: "(".to_string(),
8274 end: ")".to_string(),
8275 close: true,
8276 surround: true,
8277 newline: true,
8278 },
8279 BracketPair {
8280 start: "/*".to_string(),
8281 end: " */".to_string(),
8282 close: true,
8283 surround: true,
8284 newline: true,
8285 },
8286 BracketPair {
8287 start: "[".to_string(),
8288 end: "]".to_string(),
8289 close: false,
8290 surround: false,
8291 newline: true,
8292 },
8293 BracketPair {
8294 start: "\"".to_string(),
8295 end: "\"".to_string(),
8296 close: true,
8297 surround: true,
8298 newline: false,
8299 },
8300 BracketPair {
8301 start: "<".to_string(),
8302 end: ">".to_string(),
8303 close: false,
8304 surround: true,
8305 newline: true,
8306 },
8307 ],
8308 ..Default::default()
8309 },
8310 autoclose_before: "})]".to_string(),
8311 ..Default::default()
8312 },
8313 Some(tree_sitter_rust::LANGUAGE.into()),
8314 ));
8315
8316 cx.language_registry().add(language.clone());
8317 cx.update_buffer(|buffer, cx| {
8318 buffer.set_language(Some(language), cx);
8319 });
8320
8321 cx.set_state(
8322 &r#"
8323 🏀ˇ
8324 εˇ
8325 ❤️ˇ
8326 "#
8327 .unindent(),
8328 );
8329
8330 // autoclose multiple nested brackets at multiple cursors
8331 cx.update_editor(|editor, window, cx| {
8332 editor.handle_input("{", window, cx);
8333 editor.handle_input("{", window, cx);
8334 editor.handle_input("{", window, cx);
8335 });
8336 cx.assert_editor_state(
8337 &"
8338 🏀{{{ˇ}}}
8339 ε{{{ˇ}}}
8340 ❤️{{{ˇ}}}
8341 "
8342 .unindent(),
8343 );
8344
8345 // insert a different closing bracket
8346 cx.update_editor(|editor, window, cx| {
8347 editor.handle_input(")", window, cx);
8348 });
8349 cx.assert_editor_state(
8350 &"
8351 🏀{{{)ˇ}}}
8352 ε{{{)ˇ}}}
8353 ❤️{{{)ˇ}}}
8354 "
8355 .unindent(),
8356 );
8357
8358 // skip over the auto-closed brackets when typing a closing bracket
8359 cx.update_editor(|editor, window, cx| {
8360 editor.move_right(&MoveRight, window, cx);
8361 editor.handle_input("}", window, cx);
8362 editor.handle_input("}", window, cx);
8363 editor.handle_input("}", window, cx);
8364 });
8365 cx.assert_editor_state(
8366 &"
8367 🏀{{{)}}}}ˇ
8368 ε{{{)}}}}ˇ
8369 ❤️{{{)}}}}ˇ
8370 "
8371 .unindent(),
8372 );
8373
8374 // autoclose multi-character pairs
8375 cx.set_state(
8376 &"
8377 ˇ
8378 ˇ
8379 "
8380 .unindent(),
8381 );
8382 cx.update_editor(|editor, window, cx| {
8383 editor.handle_input("/", window, cx);
8384 editor.handle_input("*", window, cx);
8385 });
8386 cx.assert_editor_state(
8387 &"
8388 /*ˇ */
8389 /*ˇ */
8390 "
8391 .unindent(),
8392 );
8393
8394 // one cursor autocloses a multi-character pair, one cursor
8395 // does not autoclose.
8396 cx.set_state(
8397 &"
8398 /ˇ
8399 ˇ
8400 "
8401 .unindent(),
8402 );
8403 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
8404 cx.assert_editor_state(
8405 &"
8406 /*ˇ */
8407 *ˇ
8408 "
8409 .unindent(),
8410 );
8411
8412 // Don't autoclose if the next character isn't whitespace and isn't
8413 // listed in the language's "autoclose_before" section.
8414 cx.set_state("ˇa b");
8415 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8416 cx.assert_editor_state("{ˇa b");
8417
8418 // Don't autoclose if `close` is false for the bracket pair
8419 cx.set_state("ˇ");
8420 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
8421 cx.assert_editor_state("[ˇ");
8422
8423 // Surround with brackets if text is selected
8424 cx.set_state("«aˇ» b");
8425 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8426 cx.assert_editor_state("{«aˇ»} b");
8427
8428 // Autoclose when not immediately after a word character
8429 cx.set_state("a ˇ");
8430 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8431 cx.assert_editor_state("a \"ˇ\"");
8432
8433 // Autoclose pair where the start and end characters are the same
8434 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8435 cx.assert_editor_state("a \"\"ˇ");
8436
8437 // Don't autoclose when immediately after a word character
8438 cx.set_state("aˇ");
8439 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8440 cx.assert_editor_state("a\"ˇ");
8441
8442 // Do autoclose when after a non-word character
8443 cx.set_state("{ˇ");
8444 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8445 cx.assert_editor_state("{\"ˇ\"");
8446
8447 // Non identical pairs autoclose regardless of preceding character
8448 cx.set_state("aˇ");
8449 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8450 cx.assert_editor_state("a{ˇ}");
8451
8452 // Don't autoclose pair if autoclose is disabled
8453 cx.set_state("ˇ");
8454 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8455 cx.assert_editor_state("<ˇ");
8456
8457 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
8458 cx.set_state("«aˇ» b");
8459 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8460 cx.assert_editor_state("<«aˇ»> b");
8461}
8462
8463#[gpui::test]
8464async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
8465 init_test(cx, |settings| {
8466 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8467 });
8468
8469 let mut cx = EditorTestContext::new(cx).await;
8470
8471 let language = Arc::new(Language::new(
8472 LanguageConfig {
8473 brackets: BracketPairConfig {
8474 pairs: vec![
8475 BracketPair {
8476 start: "{".to_string(),
8477 end: "}".to_string(),
8478 close: true,
8479 surround: true,
8480 newline: true,
8481 },
8482 BracketPair {
8483 start: "(".to_string(),
8484 end: ")".to_string(),
8485 close: true,
8486 surround: true,
8487 newline: true,
8488 },
8489 BracketPair {
8490 start: "[".to_string(),
8491 end: "]".to_string(),
8492 close: false,
8493 surround: false,
8494 newline: true,
8495 },
8496 ],
8497 ..Default::default()
8498 },
8499 autoclose_before: "})]".to_string(),
8500 ..Default::default()
8501 },
8502 Some(tree_sitter_rust::LANGUAGE.into()),
8503 ));
8504
8505 cx.language_registry().add(language.clone());
8506 cx.update_buffer(|buffer, cx| {
8507 buffer.set_language(Some(language), cx);
8508 });
8509
8510 cx.set_state(
8511 &"
8512 ˇ
8513 ˇ
8514 ˇ
8515 "
8516 .unindent(),
8517 );
8518
8519 // ensure only matching closing brackets are skipped over
8520 cx.update_editor(|editor, window, cx| {
8521 editor.handle_input("}", window, cx);
8522 editor.move_left(&MoveLeft, window, cx);
8523 editor.handle_input(")", window, cx);
8524 editor.move_left(&MoveLeft, window, cx);
8525 });
8526 cx.assert_editor_state(
8527 &"
8528 ˇ)}
8529 ˇ)}
8530 ˇ)}
8531 "
8532 .unindent(),
8533 );
8534
8535 // skip-over closing brackets at multiple cursors
8536 cx.update_editor(|editor, window, cx| {
8537 editor.handle_input(")", window, cx);
8538 editor.handle_input("}", window, cx);
8539 });
8540 cx.assert_editor_state(
8541 &"
8542 )}ˇ
8543 )}ˇ
8544 )}ˇ
8545 "
8546 .unindent(),
8547 );
8548
8549 // ignore non-close brackets
8550 cx.update_editor(|editor, window, cx| {
8551 editor.handle_input("]", window, cx);
8552 editor.move_left(&MoveLeft, window, cx);
8553 editor.handle_input("]", window, cx);
8554 });
8555 cx.assert_editor_state(
8556 &"
8557 )}]ˇ]
8558 )}]ˇ]
8559 )}]ˇ]
8560 "
8561 .unindent(),
8562 );
8563}
8564
8565#[gpui::test]
8566async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8567 init_test(cx, |_| {});
8568
8569 let mut cx = EditorTestContext::new(cx).await;
8570
8571 let html_language = Arc::new(
8572 Language::new(
8573 LanguageConfig {
8574 name: "HTML".into(),
8575 brackets: BracketPairConfig {
8576 pairs: vec![
8577 BracketPair {
8578 start: "<".into(),
8579 end: ">".into(),
8580 close: true,
8581 ..Default::default()
8582 },
8583 BracketPair {
8584 start: "{".into(),
8585 end: "}".into(),
8586 close: true,
8587 ..Default::default()
8588 },
8589 BracketPair {
8590 start: "(".into(),
8591 end: ")".into(),
8592 close: true,
8593 ..Default::default()
8594 },
8595 ],
8596 ..Default::default()
8597 },
8598 autoclose_before: "})]>".into(),
8599 ..Default::default()
8600 },
8601 Some(tree_sitter_html::LANGUAGE.into()),
8602 )
8603 .with_injection_query(
8604 r#"
8605 (script_element
8606 (raw_text) @injection.content
8607 (#set! injection.language "javascript"))
8608 "#,
8609 )
8610 .unwrap(),
8611 );
8612
8613 let javascript_language = Arc::new(Language::new(
8614 LanguageConfig {
8615 name: "JavaScript".into(),
8616 brackets: BracketPairConfig {
8617 pairs: vec![
8618 BracketPair {
8619 start: "/*".into(),
8620 end: " */".into(),
8621 close: true,
8622 ..Default::default()
8623 },
8624 BracketPair {
8625 start: "{".into(),
8626 end: "}".into(),
8627 close: true,
8628 ..Default::default()
8629 },
8630 BracketPair {
8631 start: "(".into(),
8632 end: ")".into(),
8633 close: true,
8634 ..Default::default()
8635 },
8636 ],
8637 ..Default::default()
8638 },
8639 autoclose_before: "})]>".into(),
8640 ..Default::default()
8641 },
8642 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8643 ));
8644
8645 cx.language_registry().add(html_language.clone());
8646 cx.language_registry().add(javascript_language.clone());
8647 cx.executor().run_until_parked();
8648
8649 cx.update_buffer(|buffer, cx| {
8650 buffer.set_language(Some(html_language), cx);
8651 });
8652
8653 cx.set_state(
8654 &r#"
8655 <body>ˇ
8656 <script>
8657 var x = 1;ˇ
8658 </script>
8659 </body>ˇ
8660 "#
8661 .unindent(),
8662 );
8663
8664 // Precondition: different languages are active at different locations.
8665 cx.update_editor(|editor, window, cx| {
8666 let snapshot = editor.snapshot(window, cx);
8667 let cursors = editor.selections.ranges::<usize>(cx);
8668 let languages = cursors
8669 .iter()
8670 .map(|c| snapshot.language_at(c.start).unwrap().name())
8671 .collect::<Vec<_>>();
8672 assert_eq!(
8673 languages,
8674 &["HTML".into(), "JavaScript".into(), "HTML".into()]
8675 );
8676 });
8677
8678 // Angle brackets autoclose in HTML, but not JavaScript.
8679 cx.update_editor(|editor, window, cx| {
8680 editor.handle_input("<", window, cx);
8681 editor.handle_input("a", window, cx);
8682 });
8683 cx.assert_editor_state(
8684 &r#"
8685 <body><aˇ>
8686 <script>
8687 var x = 1;<aˇ
8688 </script>
8689 </body><aˇ>
8690 "#
8691 .unindent(),
8692 );
8693
8694 // Curly braces and parens autoclose in both HTML and JavaScript.
8695 cx.update_editor(|editor, window, cx| {
8696 editor.handle_input(" b=", window, cx);
8697 editor.handle_input("{", window, cx);
8698 editor.handle_input("c", window, cx);
8699 editor.handle_input("(", window, cx);
8700 });
8701 cx.assert_editor_state(
8702 &r#"
8703 <body><a b={c(ˇ)}>
8704 <script>
8705 var x = 1;<a b={c(ˇ)}
8706 </script>
8707 </body><a b={c(ˇ)}>
8708 "#
8709 .unindent(),
8710 );
8711
8712 // Brackets that were already autoclosed are skipped.
8713 cx.update_editor(|editor, window, cx| {
8714 editor.handle_input(")", window, cx);
8715 editor.handle_input("d", window, cx);
8716 editor.handle_input("}", window, cx);
8717 });
8718 cx.assert_editor_state(
8719 &r#"
8720 <body><a b={c()d}ˇ>
8721 <script>
8722 var x = 1;<a b={c()d}ˇ
8723 </script>
8724 </body><a b={c()d}ˇ>
8725 "#
8726 .unindent(),
8727 );
8728 cx.update_editor(|editor, window, cx| {
8729 editor.handle_input(">", window, cx);
8730 });
8731 cx.assert_editor_state(
8732 &r#"
8733 <body><a b={c()d}>ˇ
8734 <script>
8735 var x = 1;<a b={c()d}>ˇ
8736 </script>
8737 </body><a b={c()d}>ˇ
8738 "#
8739 .unindent(),
8740 );
8741
8742 // Reset
8743 cx.set_state(
8744 &r#"
8745 <body>ˇ
8746 <script>
8747 var x = 1;ˇ
8748 </script>
8749 </body>ˇ
8750 "#
8751 .unindent(),
8752 );
8753
8754 cx.update_editor(|editor, window, cx| {
8755 editor.handle_input("<", window, cx);
8756 });
8757 cx.assert_editor_state(
8758 &r#"
8759 <body><ˇ>
8760 <script>
8761 var x = 1;<ˇ
8762 </script>
8763 </body><ˇ>
8764 "#
8765 .unindent(),
8766 );
8767
8768 // When backspacing, the closing angle brackets are removed.
8769 cx.update_editor(|editor, window, cx| {
8770 editor.backspace(&Backspace, window, cx);
8771 });
8772 cx.assert_editor_state(
8773 &r#"
8774 <body>ˇ
8775 <script>
8776 var x = 1;ˇ
8777 </script>
8778 </body>ˇ
8779 "#
8780 .unindent(),
8781 );
8782
8783 // Block comments autoclose in JavaScript, but not HTML.
8784 cx.update_editor(|editor, window, cx| {
8785 editor.handle_input("/", window, cx);
8786 editor.handle_input("*", window, cx);
8787 });
8788 cx.assert_editor_state(
8789 &r#"
8790 <body>/*ˇ
8791 <script>
8792 var x = 1;/*ˇ */
8793 </script>
8794 </body>/*ˇ
8795 "#
8796 .unindent(),
8797 );
8798}
8799
8800#[gpui::test]
8801async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
8802 init_test(cx, |_| {});
8803
8804 let mut cx = EditorTestContext::new(cx).await;
8805
8806 let rust_language = Arc::new(
8807 Language::new(
8808 LanguageConfig {
8809 name: "Rust".into(),
8810 brackets: serde_json::from_value(json!([
8811 { "start": "{", "end": "}", "close": true, "newline": true },
8812 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
8813 ]))
8814 .unwrap(),
8815 autoclose_before: "})]>".into(),
8816 ..Default::default()
8817 },
8818 Some(tree_sitter_rust::LANGUAGE.into()),
8819 )
8820 .with_override_query("(string_literal) @string")
8821 .unwrap(),
8822 );
8823
8824 cx.language_registry().add(rust_language.clone());
8825 cx.update_buffer(|buffer, cx| {
8826 buffer.set_language(Some(rust_language), cx);
8827 });
8828
8829 cx.set_state(
8830 &r#"
8831 let x = ˇ
8832 "#
8833 .unindent(),
8834 );
8835
8836 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
8837 cx.update_editor(|editor, window, cx| {
8838 editor.handle_input("\"", window, cx);
8839 });
8840 cx.assert_editor_state(
8841 &r#"
8842 let x = "ˇ"
8843 "#
8844 .unindent(),
8845 );
8846
8847 // Inserting another quotation mark. The cursor moves across the existing
8848 // automatically-inserted quotation mark.
8849 cx.update_editor(|editor, window, cx| {
8850 editor.handle_input("\"", window, cx);
8851 });
8852 cx.assert_editor_state(
8853 &r#"
8854 let x = ""ˇ
8855 "#
8856 .unindent(),
8857 );
8858
8859 // Reset
8860 cx.set_state(
8861 &r#"
8862 let x = ˇ
8863 "#
8864 .unindent(),
8865 );
8866
8867 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
8868 cx.update_editor(|editor, window, cx| {
8869 editor.handle_input("\"", window, cx);
8870 editor.handle_input(" ", window, cx);
8871 editor.move_left(&Default::default(), window, cx);
8872 editor.handle_input("\\", window, cx);
8873 editor.handle_input("\"", window, cx);
8874 });
8875 cx.assert_editor_state(
8876 &r#"
8877 let x = "\"ˇ "
8878 "#
8879 .unindent(),
8880 );
8881
8882 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8883 // mark. Nothing is inserted.
8884 cx.update_editor(|editor, window, cx| {
8885 editor.move_right(&Default::default(), window, cx);
8886 editor.handle_input("\"", window, cx);
8887 });
8888 cx.assert_editor_state(
8889 &r#"
8890 let x = "\" "ˇ
8891 "#
8892 .unindent(),
8893 );
8894}
8895
8896#[gpui::test]
8897async fn test_surround_with_pair(cx: &mut TestAppContext) {
8898 init_test(cx, |_| {});
8899
8900 let language = Arc::new(Language::new(
8901 LanguageConfig {
8902 brackets: BracketPairConfig {
8903 pairs: vec![
8904 BracketPair {
8905 start: "{".to_string(),
8906 end: "}".to_string(),
8907 close: true,
8908 surround: true,
8909 newline: true,
8910 },
8911 BracketPair {
8912 start: "/* ".to_string(),
8913 end: "*/".to_string(),
8914 close: true,
8915 surround: true,
8916 ..Default::default()
8917 },
8918 ],
8919 ..Default::default()
8920 },
8921 ..Default::default()
8922 },
8923 Some(tree_sitter_rust::LANGUAGE.into()),
8924 ));
8925
8926 let text = r#"
8927 a
8928 b
8929 c
8930 "#
8931 .unindent();
8932
8933 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8934 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8935 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8936 editor
8937 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8938 .await;
8939
8940 editor.update_in(cx, |editor, window, cx| {
8941 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8942 s.select_display_ranges([
8943 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8944 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8945 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8946 ])
8947 });
8948
8949 editor.handle_input("{", window, cx);
8950 editor.handle_input("{", window, cx);
8951 editor.handle_input("{", window, cx);
8952 assert_eq!(
8953 editor.text(cx),
8954 "
8955 {{{a}}}
8956 {{{b}}}
8957 {{{c}}}
8958 "
8959 .unindent()
8960 );
8961 assert_eq!(
8962 editor.selections.display_ranges(cx),
8963 [
8964 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
8965 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
8966 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
8967 ]
8968 );
8969
8970 editor.undo(&Undo, window, cx);
8971 editor.undo(&Undo, window, cx);
8972 editor.undo(&Undo, window, cx);
8973 assert_eq!(
8974 editor.text(cx),
8975 "
8976 a
8977 b
8978 c
8979 "
8980 .unindent()
8981 );
8982 assert_eq!(
8983 editor.selections.display_ranges(cx),
8984 [
8985 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8986 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8987 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8988 ]
8989 );
8990
8991 // Ensure inserting the first character of a multi-byte bracket pair
8992 // doesn't surround the selections with the bracket.
8993 editor.handle_input("/", window, cx);
8994 assert_eq!(
8995 editor.text(cx),
8996 "
8997 /
8998 /
8999 /
9000 "
9001 .unindent()
9002 );
9003 assert_eq!(
9004 editor.selections.display_ranges(cx),
9005 [
9006 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
9007 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
9008 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
9009 ]
9010 );
9011
9012 editor.undo(&Undo, window, cx);
9013 assert_eq!(
9014 editor.text(cx),
9015 "
9016 a
9017 b
9018 c
9019 "
9020 .unindent()
9021 );
9022 assert_eq!(
9023 editor.selections.display_ranges(cx),
9024 [
9025 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
9026 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
9027 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
9028 ]
9029 );
9030
9031 // Ensure inserting the last character of a multi-byte bracket pair
9032 // doesn't surround the selections with the bracket.
9033 editor.handle_input("*", window, cx);
9034 assert_eq!(
9035 editor.text(cx),
9036 "
9037 *
9038 *
9039 *
9040 "
9041 .unindent()
9042 );
9043 assert_eq!(
9044 editor.selections.display_ranges(cx),
9045 [
9046 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
9047 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
9048 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
9049 ]
9050 );
9051 });
9052}
9053
9054#[gpui::test]
9055async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
9056 init_test(cx, |_| {});
9057
9058 let language = Arc::new(Language::new(
9059 LanguageConfig {
9060 brackets: BracketPairConfig {
9061 pairs: vec![BracketPair {
9062 start: "{".to_string(),
9063 end: "}".to_string(),
9064 close: true,
9065 surround: true,
9066 newline: true,
9067 }],
9068 ..Default::default()
9069 },
9070 autoclose_before: "}".to_string(),
9071 ..Default::default()
9072 },
9073 Some(tree_sitter_rust::LANGUAGE.into()),
9074 ));
9075
9076 let text = r#"
9077 a
9078 b
9079 c
9080 "#
9081 .unindent();
9082
9083 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9084 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9085 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9086 editor
9087 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9088 .await;
9089
9090 editor.update_in(cx, |editor, window, cx| {
9091 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9092 s.select_ranges([
9093 Point::new(0, 1)..Point::new(0, 1),
9094 Point::new(1, 1)..Point::new(1, 1),
9095 Point::new(2, 1)..Point::new(2, 1),
9096 ])
9097 });
9098
9099 editor.handle_input("{", window, cx);
9100 editor.handle_input("{", window, cx);
9101 editor.handle_input("_", window, cx);
9102 assert_eq!(
9103 editor.text(cx),
9104 "
9105 a{{_}}
9106 b{{_}}
9107 c{{_}}
9108 "
9109 .unindent()
9110 );
9111 assert_eq!(
9112 editor.selections.ranges::<Point>(cx),
9113 [
9114 Point::new(0, 4)..Point::new(0, 4),
9115 Point::new(1, 4)..Point::new(1, 4),
9116 Point::new(2, 4)..Point::new(2, 4)
9117 ]
9118 );
9119
9120 editor.backspace(&Default::default(), window, cx);
9121 editor.backspace(&Default::default(), window, cx);
9122 assert_eq!(
9123 editor.text(cx),
9124 "
9125 a{}
9126 b{}
9127 c{}
9128 "
9129 .unindent()
9130 );
9131 assert_eq!(
9132 editor.selections.ranges::<Point>(cx),
9133 [
9134 Point::new(0, 2)..Point::new(0, 2),
9135 Point::new(1, 2)..Point::new(1, 2),
9136 Point::new(2, 2)..Point::new(2, 2)
9137 ]
9138 );
9139
9140 editor.delete_to_previous_word_start(&Default::default(), window, cx);
9141 assert_eq!(
9142 editor.text(cx),
9143 "
9144 a
9145 b
9146 c
9147 "
9148 .unindent()
9149 );
9150 assert_eq!(
9151 editor.selections.ranges::<Point>(cx),
9152 [
9153 Point::new(0, 1)..Point::new(0, 1),
9154 Point::new(1, 1)..Point::new(1, 1),
9155 Point::new(2, 1)..Point::new(2, 1)
9156 ]
9157 );
9158 });
9159}
9160
9161#[gpui::test]
9162async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
9163 init_test(cx, |settings| {
9164 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9165 });
9166
9167 let mut cx = EditorTestContext::new(cx).await;
9168
9169 let language = Arc::new(Language::new(
9170 LanguageConfig {
9171 brackets: BracketPairConfig {
9172 pairs: vec![
9173 BracketPair {
9174 start: "{".to_string(),
9175 end: "}".to_string(),
9176 close: true,
9177 surround: true,
9178 newline: true,
9179 },
9180 BracketPair {
9181 start: "(".to_string(),
9182 end: ")".to_string(),
9183 close: true,
9184 surround: true,
9185 newline: true,
9186 },
9187 BracketPair {
9188 start: "[".to_string(),
9189 end: "]".to_string(),
9190 close: false,
9191 surround: true,
9192 newline: true,
9193 },
9194 ],
9195 ..Default::default()
9196 },
9197 autoclose_before: "})]".to_string(),
9198 ..Default::default()
9199 },
9200 Some(tree_sitter_rust::LANGUAGE.into()),
9201 ));
9202
9203 cx.language_registry().add(language.clone());
9204 cx.update_buffer(|buffer, cx| {
9205 buffer.set_language(Some(language), cx);
9206 });
9207
9208 cx.set_state(
9209 &"
9210 {(ˇ)}
9211 [[ˇ]]
9212 {(ˇ)}
9213 "
9214 .unindent(),
9215 );
9216
9217 cx.update_editor(|editor, window, cx| {
9218 editor.backspace(&Default::default(), window, cx);
9219 editor.backspace(&Default::default(), window, cx);
9220 });
9221
9222 cx.assert_editor_state(
9223 &"
9224 ˇ
9225 ˇ]]
9226 ˇ
9227 "
9228 .unindent(),
9229 );
9230
9231 cx.update_editor(|editor, window, cx| {
9232 editor.handle_input("{", window, cx);
9233 editor.handle_input("{", window, cx);
9234 editor.move_right(&MoveRight, window, cx);
9235 editor.move_right(&MoveRight, window, cx);
9236 editor.move_left(&MoveLeft, window, cx);
9237 editor.move_left(&MoveLeft, window, cx);
9238 editor.backspace(&Default::default(), window, cx);
9239 });
9240
9241 cx.assert_editor_state(
9242 &"
9243 {ˇ}
9244 {ˇ}]]
9245 {ˇ}
9246 "
9247 .unindent(),
9248 );
9249
9250 cx.update_editor(|editor, window, cx| {
9251 editor.backspace(&Default::default(), window, cx);
9252 });
9253
9254 cx.assert_editor_state(
9255 &"
9256 ˇ
9257 ˇ]]
9258 ˇ
9259 "
9260 .unindent(),
9261 );
9262}
9263
9264#[gpui::test]
9265async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
9266 init_test(cx, |_| {});
9267
9268 let language = Arc::new(Language::new(
9269 LanguageConfig::default(),
9270 Some(tree_sitter_rust::LANGUAGE.into()),
9271 ));
9272
9273 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
9274 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9275 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9276 editor
9277 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9278 .await;
9279
9280 editor.update_in(cx, |editor, window, cx| {
9281 editor.set_auto_replace_emoji_shortcode(true);
9282
9283 editor.handle_input("Hello ", window, cx);
9284 editor.handle_input(":wave", window, cx);
9285 assert_eq!(editor.text(cx), "Hello :wave".unindent());
9286
9287 editor.handle_input(":", window, cx);
9288 assert_eq!(editor.text(cx), "Hello 👋".unindent());
9289
9290 editor.handle_input(" :smile", window, cx);
9291 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
9292
9293 editor.handle_input(":", window, cx);
9294 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
9295
9296 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
9297 editor.handle_input(":wave", window, cx);
9298 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
9299
9300 editor.handle_input(":", window, cx);
9301 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
9302
9303 editor.handle_input(":1", window, cx);
9304 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
9305
9306 editor.handle_input(":", window, cx);
9307 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
9308
9309 // Ensure shortcode does not get replaced when it is part of a word
9310 editor.handle_input(" Test:wave", window, cx);
9311 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
9312
9313 editor.handle_input(":", window, cx);
9314 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
9315
9316 editor.set_auto_replace_emoji_shortcode(false);
9317
9318 // Ensure shortcode does not get replaced when auto replace is off
9319 editor.handle_input(" :wave", window, cx);
9320 assert_eq!(
9321 editor.text(cx),
9322 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
9323 );
9324
9325 editor.handle_input(":", window, cx);
9326 assert_eq!(
9327 editor.text(cx),
9328 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
9329 );
9330 });
9331}
9332
9333#[gpui::test]
9334async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
9335 init_test(cx, |_| {});
9336
9337 let (text, insertion_ranges) = marked_text_ranges(
9338 indoc! {"
9339 ˇ
9340 "},
9341 false,
9342 );
9343
9344 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
9345 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9346
9347 _ = editor.update_in(cx, |editor, window, cx| {
9348 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
9349
9350 editor
9351 .insert_snippet(&insertion_ranges, snippet, window, cx)
9352 .unwrap();
9353
9354 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
9355 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
9356 assert_eq!(editor.text(cx), expected_text);
9357 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
9358 }
9359
9360 assert(
9361 editor,
9362 cx,
9363 indoc! {"
9364 type «» =•
9365 "},
9366 );
9367
9368 assert!(editor.context_menu_visible(), "There should be a matches");
9369 });
9370}
9371
9372#[gpui::test]
9373async fn test_snippets(cx: &mut TestAppContext) {
9374 init_test(cx, |_| {});
9375
9376 let mut cx = EditorTestContext::new(cx).await;
9377
9378 cx.set_state(indoc! {"
9379 a.ˇ b
9380 a.ˇ b
9381 a.ˇ b
9382 "});
9383
9384 cx.update_editor(|editor, window, cx| {
9385 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
9386 let insertion_ranges = editor
9387 .selections
9388 .all(cx)
9389 .iter()
9390 .map(|s| s.range().clone())
9391 .collect::<Vec<_>>();
9392 editor
9393 .insert_snippet(&insertion_ranges, snippet, window, cx)
9394 .unwrap();
9395 });
9396
9397 cx.assert_editor_state(indoc! {"
9398 a.f(«oneˇ», two, «threeˇ») b
9399 a.f(«oneˇ», two, «threeˇ») b
9400 a.f(«oneˇ», two, «threeˇ») b
9401 "});
9402
9403 // Can't move earlier than the first tab stop
9404 cx.update_editor(|editor, window, cx| {
9405 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9406 });
9407 cx.assert_editor_state(indoc! {"
9408 a.f(«oneˇ», two, «threeˇ») b
9409 a.f(«oneˇ», two, «threeˇ») b
9410 a.f(«oneˇ», two, «threeˇ») b
9411 "});
9412
9413 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9414 cx.assert_editor_state(indoc! {"
9415 a.f(one, «twoˇ», three) b
9416 a.f(one, «twoˇ», three) b
9417 a.f(one, «twoˇ», three) b
9418 "});
9419
9420 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
9421 cx.assert_editor_state(indoc! {"
9422 a.f(«oneˇ», two, «threeˇ») b
9423 a.f(«oneˇ», two, «threeˇ») b
9424 a.f(«oneˇ», two, «threeˇ») b
9425 "});
9426
9427 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9428 cx.assert_editor_state(indoc! {"
9429 a.f(one, «twoˇ», three) b
9430 a.f(one, «twoˇ», three) b
9431 a.f(one, «twoˇ», three) b
9432 "});
9433 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9434 cx.assert_editor_state(indoc! {"
9435 a.f(one, two, three)ˇ b
9436 a.f(one, two, three)ˇ b
9437 a.f(one, two, three)ˇ b
9438 "});
9439
9440 // As soon as the last tab stop is reached, snippet state is gone
9441 cx.update_editor(|editor, window, cx| {
9442 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9443 });
9444 cx.assert_editor_state(indoc! {"
9445 a.f(one, two, three)ˇ b
9446 a.f(one, two, three)ˇ b
9447 a.f(one, two, three)ˇ b
9448 "});
9449}
9450
9451#[gpui::test]
9452async fn test_snippet_indentation(cx: &mut TestAppContext) {
9453 init_test(cx, |_| {});
9454
9455 let mut cx = EditorTestContext::new(cx).await;
9456
9457 cx.update_editor(|editor, window, cx| {
9458 let snippet = Snippet::parse(indoc! {"
9459 /*
9460 * Multiline comment with leading indentation
9461 *
9462 * $1
9463 */
9464 $0"})
9465 .unwrap();
9466 let insertion_ranges = editor
9467 .selections
9468 .all(cx)
9469 .iter()
9470 .map(|s| s.range().clone())
9471 .collect::<Vec<_>>();
9472 editor
9473 .insert_snippet(&insertion_ranges, snippet, window, cx)
9474 .unwrap();
9475 });
9476
9477 cx.assert_editor_state(indoc! {"
9478 /*
9479 * Multiline comment with leading indentation
9480 *
9481 * ˇ
9482 */
9483 "});
9484
9485 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9486 cx.assert_editor_state(indoc! {"
9487 /*
9488 * Multiline comment with leading indentation
9489 *
9490 *•
9491 */
9492 ˇ"});
9493}
9494
9495#[gpui::test]
9496async fn test_document_format_during_save(cx: &mut TestAppContext) {
9497 init_test(cx, |_| {});
9498
9499 let fs = FakeFs::new(cx.executor());
9500 fs.insert_file(path!("/file.rs"), Default::default()).await;
9501
9502 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
9503
9504 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9505 language_registry.add(rust_lang());
9506 let mut fake_servers = language_registry.register_fake_lsp(
9507 "Rust",
9508 FakeLspAdapter {
9509 capabilities: lsp::ServerCapabilities {
9510 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9511 ..Default::default()
9512 },
9513 ..Default::default()
9514 },
9515 );
9516
9517 let buffer = project
9518 .update(cx, |project, cx| {
9519 project.open_local_buffer(path!("/file.rs"), cx)
9520 })
9521 .await
9522 .unwrap();
9523
9524 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9525 let (editor, cx) = cx.add_window_view(|window, cx| {
9526 build_editor_with_project(project.clone(), buffer, window, cx)
9527 });
9528 editor.update_in(cx, |editor, window, cx| {
9529 editor.set_text("one\ntwo\nthree\n", window, cx)
9530 });
9531 assert!(cx.read(|cx| editor.is_dirty(cx)));
9532
9533 cx.executor().start_waiting();
9534 let fake_server = fake_servers.next().await.unwrap();
9535
9536 {
9537 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9538 move |params, _| async move {
9539 assert_eq!(
9540 params.text_document.uri,
9541 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9542 );
9543 assert_eq!(params.options.tab_size, 4);
9544 Ok(Some(vec![lsp::TextEdit::new(
9545 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9546 ", ".to_string(),
9547 )]))
9548 },
9549 );
9550 let save = editor
9551 .update_in(cx, |editor, window, cx| {
9552 editor.save(
9553 SaveOptions {
9554 format: true,
9555 autosave: false,
9556 },
9557 project.clone(),
9558 window,
9559 cx,
9560 )
9561 })
9562 .unwrap();
9563 cx.executor().start_waiting();
9564 save.await;
9565
9566 assert_eq!(
9567 editor.update(cx, |editor, cx| editor.text(cx)),
9568 "one, two\nthree\n"
9569 );
9570 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9571 }
9572
9573 {
9574 editor.update_in(cx, |editor, window, cx| {
9575 editor.set_text("one\ntwo\nthree\n", window, cx)
9576 });
9577 assert!(cx.read(|cx| editor.is_dirty(cx)));
9578
9579 // Ensure we can still save even if formatting hangs.
9580 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9581 move |params, _| async move {
9582 assert_eq!(
9583 params.text_document.uri,
9584 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9585 );
9586 futures::future::pending::<()>().await;
9587 unreachable!()
9588 },
9589 );
9590 let save = editor
9591 .update_in(cx, |editor, window, cx| {
9592 editor.save(
9593 SaveOptions {
9594 format: true,
9595 autosave: false,
9596 },
9597 project.clone(),
9598 window,
9599 cx,
9600 )
9601 })
9602 .unwrap();
9603 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9604 cx.executor().start_waiting();
9605 save.await;
9606 assert_eq!(
9607 editor.update(cx, |editor, cx| editor.text(cx)),
9608 "one\ntwo\nthree\n"
9609 );
9610 }
9611
9612 // Set rust language override and assert overridden tabsize is sent to language server
9613 update_test_language_settings(cx, |settings| {
9614 settings.languages.0.insert(
9615 "Rust".into(),
9616 LanguageSettingsContent {
9617 tab_size: NonZeroU32::new(8),
9618 ..Default::default()
9619 },
9620 );
9621 });
9622
9623 {
9624 editor.update_in(cx, |editor, window, cx| {
9625 editor.set_text("somehting_new\n", window, cx)
9626 });
9627 assert!(cx.read(|cx| editor.is_dirty(cx)));
9628 let _formatting_request_signal = fake_server
9629 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9630 assert_eq!(
9631 params.text_document.uri,
9632 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9633 );
9634 assert_eq!(params.options.tab_size, 8);
9635 Ok(Some(vec![]))
9636 });
9637 let save = editor
9638 .update_in(cx, |editor, window, cx| {
9639 editor.save(
9640 SaveOptions {
9641 format: true,
9642 autosave: false,
9643 },
9644 project.clone(),
9645 window,
9646 cx,
9647 )
9648 })
9649 .unwrap();
9650 cx.executor().start_waiting();
9651 save.await;
9652 }
9653}
9654
9655#[gpui::test]
9656async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
9657 init_test(cx, |settings| {
9658 settings.defaults.ensure_final_newline_on_save = Some(false);
9659 });
9660
9661 let fs = FakeFs::new(cx.executor());
9662 fs.insert_file(path!("/file.txt"), "foo".into()).await;
9663
9664 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
9665
9666 let buffer = project
9667 .update(cx, |project, cx| {
9668 project.open_local_buffer(path!("/file.txt"), cx)
9669 })
9670 .await
9671 .unwrap();
9672
9673 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9674 let (editor, cx) = cx.add_window_view(|window, cx| {
9675 build_editor_with_project(project.clone(), buffer, window, cx)
9676 });
9677 editor.update_in(cx, |editor, window, cx| {
9678 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9679 s.select_ranges([0..0])
9680 });
9681 });
9682 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9683
9684 editor.update_in(cx, |editor, window, cx| {
9685 editor.handle_input("\n", window, cx)
9686 });
9687 cx.run_until_parked();
9688 save(&editor, &project, cx).await;
9689 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9690
9691 editor.update_in(cx, |editor, window, cx| {
9692 editor.undo(&Default::default(), window, cx);
9693 });
9694 save(&editor, &project, cx).await;
9695 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9696
9697 editor.update_in(cx, |editor, window, cx| {
9698 editor.redo(&Default::default(), window, cx);
9699 });
9700 cx.run_until_parked();
9701 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9702
9703 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
9704 let save = editor
9705 .update_in(cx, |editor, window, cx| {
9706 editor.save(
9707 SaveOptions {
9708 format: true,
9709 autosave: false,
9710 },
9711 project.clone(),
9712 window,
9713 cx,
9714 )
9715 })
9716 .unwrap();
9717 cx.executor().start_waiting();
9718 save.await;
9719 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9720 }
9721}
9722
9723#[gpui::test]
9724async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9725 init_test(cx, |_| {});
9726
9727 let cols = 4;
9728 let rows = 10;
9729 let sample_text_1 = sample_text(rows, cols, 'a');
9730 assert_eq!(
9731 sample_text_1,
9732 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9733 );
9734 let sample_text_2 = sample_text(rows, cols, 'l');
9735 assert_eq!(
9736 sample_text_2,
9737 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9738 );
9739 let sample_text_3 = sample_text(rows, cols, 'v');
9740 assert_eq!(
9741 sample_text_3,
9742 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9743 );
9744
9745 let fs = FakeFs::new(cx.executor());
9746 fs.insert_tree(
9747 path!("/a"),
9748 json!({
9749 "main.rs": sample_text_1,
9750 "other.rs": sample_text_2,
9751 "lib.rs": sample_text_3,
9752 }),
9753 )
9754 .await;
9755
9756 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9757 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9758 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9759
9760 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9761 language_registry.add(rust_lang());
9762 let mut fake_servers = language_registry.register_fake_lsp(
9763 "Rust",
9764 FakeLspAdapter {
9765 capabilities: lsp::ServerCapabilities {
9766 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9767 ..Default::default()
9768 },
9769 ..Default::default()
9770 },
9771 );
9772
9773 let worktree = project.update(cx, |project, cx| {
9774 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
9775 assert_eq!(worktrees.len(), 1);
9776 worktrees.pop().unwrap()
9777 });
9778 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9779
9780 let buffer_1 = project
9781 .update(cx, |project, cx| {
9782 project.open_buffer((worktree_id, "main.rs"), cx)
9783 })
9784 .await
9785 .unwrap();
9786 let buffer_2 = project
9787 .update(cx, |project, cx| {
9788 project.open_buffer((worktree_id, "other.rs"), cx)
9789 })
9790 .await
9791 .unwrap();
9792 let buffer_3 = project
9793 .update(cx, |project, cx| {
9794 project.open_buffer((worktree_id, "lib.rs"), cx)
9795 })
9796 .await
9797 .unwrap();
9798
9799 let multi_buffer = cx.new(|cx| {
9800 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9801 multi_buffer.push_excerpts(
9802 buffer_1.clone(),
9803 [
9804 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9805 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9806 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9807 ],
9808 cx,
9809 );
9810 multi_buffer.push_excerpts(
9811 buffer_2.clone(),
9812 [
9813 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9814 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9815 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9816 ],
9817 cx,
9818 );
9819 multi_buffer.push_excerpts(
9820 buffer_3.clone(),
9821 [
9822 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9823 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9824 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9825 ],
9826 cx,
9827 );
9828 multi_buffer
9829 });
9830 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
9831 Editor::new(
9832 EditorMode::full(),
9833 multi_buffer,
9834 Some(project.clone()),
9835 window,
9836 cx,
9837 )
9838 });
9839
9840 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9841 editor.change_selections(
9842 SelectionEffects::scroll(Autoscroll::Next),
9843 window,
9844 cx,
9845 |s| s.select_ranges(Some(1..2)),
9846 );
9847 editor.insert("|one|two|three|", window, cx);
9848 });
9849 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9850 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9851 editor.change_selections(
9852 SelectionEffects::scroll(Autoscroll::Next),
9853 window,
9854 cx,
9855 |s| s.select_ranges(Some(60..70)),
9856 );
9857 editor.insert("|four|five|six|", window, cx);
9858 });
9859 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9860
9861 // First two buffers should be edited, but not the third one.
9862 assert_eq!(
9863 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9864 "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}",
9865 );
9866 buffer_1.update(cx, |buffer, _| {
9867 assert!(buffer.is_dirty());
9868 assert_eq!(
9869 buffer.text(),
9870 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
9871 )
9872 });
9873 buffer_2.update(cx, |buffer, _| {
9874 assert!(buffer.is_dirty());
9875 assert_eq!(
9876 buffer.text(),
9877 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
9878 )
9879 });
9880 buffer_3.update(cx, |buffer, _| {
9881 assert!(!buffer.is_dirty());
9882 assert_eq!(buffer.text(), sample_text_3,)
9883 });
9884 cx.executor().run_until_parked();
9885
9886 cx.executor().start_waiting();
9887 let save = multi_buffer_editor
9888 .update_in(cx, |editor, window, cx| {
9889 editor.save(
9890 SaveOptions {
9891 format: true,
9892 autosave: false,
9893 },
9894 project.clone(),
9895 window,
9896 cx,
9897 )
9898 })
9899 .unwrap();
9900
9901 let fake_server = fake_servers.next().await.unwrap();
9902 fake_server
9903 .server
9904 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
9905 Ok(Some(vec![lsp::TextEdit::new(
9906 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9907 format!("[{} formatted]", params.text_document.uri),
9908 )]))
9909 })
9910 .detach();
9911 save.await;
9912
9913 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
9914 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
9915 assert_eq!(
9916 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9917 uri!(
9918 "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}"
9919 ),
9920 );
9921 buffer_1.update(cx, |buffer, _| {
9922 assert!(!buffer.is_dirty());
9923 assert_eq!(
9924 buffer.text(),
9925 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
9926 )
9927 });
9928 buffer_2.update(cx, |buffer, _| {
9929 assert!(!buffer.is_dirty());
9930 assert_eq!(
9931 buffer.text(),
9932 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
9933 )
9934 });
9935 buffer_3.update(cx, |buffer, _| {
9936 assert!(!buffer.is_dirty());
9937 assert_eq!(buffer.text(), sample_text_3,)
9938 });
9939}
9940
9941#[gpui::test]
9942async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
9943 init_test(cx, |_| {});
9944
9945 let fs = FakeFs::new(cx.executor());
9946 fs.insert_tree(
9947 path!("/dir"),
9948 json!({
9949 "file1.rs": "fn main() { println!(\"hello\"); }",
9950 "file2.rs": "fn test() { println!(\"test\"); }",
9951 "file3.rs": "fn other() { println!(\"other\"); }\n",
9952 }),
9953 )
9954 .await;
9955
9956 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
9957 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9958 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9959
9960 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9961 language_registry.add(rust_lang());
9962
9963 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
9964 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9965
9966 // Open three buffers
9967 let buffer_1 = project
9968 .update(cx, |project, cx| {
9969 project.open_buffer((worktree_id, "file1.rs"), cx)
9970 })
9971 .await
9972 .unwrap();
9973 let buffer_2 = project
9974 .update(cx, |project, cx| {
9975 project.open_buffer((worktree_id, "file2.rs"), cx)
9976 })
9977 .await
9978 .unwrap();
9979 let buffer_3 = project
9980 .update(cx, |project, cx| {
9981 project.open_buffer((worktree_id, "file3.rs"), cx)
9982 })
9983 .await
9984 .unwrap();
9985
9986 // Create a multi-buffer with all three buffers
9987 let multi_buffer = cx.new(|cx| {
9988 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9989 multi_buffer.push_excerpts(
9990 buffer_1.clone(),
9991 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9992 cx,
9993 );
9994 multi_buffer.push_excerpts(
9995 buffer_2.clone(),
9996 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9997 cx,
9998 );
9999 multi_buffer.push_excerpts(
10000 buffer_3.clone(),
10001 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
10002 cx,
10003 );
10004 multi_buffer
10005 });
10006
10007 let editor = cx.new_window_entity(|window, cx| {
10008 Editor::new(
10009 EditorMode::full(),
10010 multi_buffer,
10011 Some(project.clone()),
10012 window,
10013 cx,
10014 )
10015 });
10016
10017 // Edit only the first buffer
10018 editor.update_in(cx, |editor, window, cx| {
10019 editor.change_selections(
10020 SelectionEffects::scroll(Autoscroll::Next),
10021 window,
10022 cx,
10023 |s| s.select_ranges(Some(10..10)),
10024 );
10025 editor.insert("// edited", window, cx);
10026 });
10027
10028 // Verify that only buffer 1 is dirty
10029 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
10030 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10031 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10032
10033 // Get write counts after file creation (files were created with initial content)
10034 // We expect each file to have been written once during creation
10035 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
10036 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
10037 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
10038
10039 // Perform autosave
10040 let save_task = editor.update_in(cx, |editor, window, cx| {
10041 editor.save(
10042 SaveOptions {
10043 format: true,
10044 autosave: true,
10045 },
10046 project.clone(),
10047 window,
10048 cx,
10049 )
10050 });
10051 save_task.await.unwrap();
10052
10053 // Only the dirty buffer should have been saved
10054 assert_eq!(
10055 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
10056 1,
10057 "Buffer 1 was dirty, so it should have been written once during autosave"
10058 );
10059 assert_eq!(
10060 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
10061 0,
10062 "Buffer 2 was clean, so it should not have been written during autosave"
10063 );
10064 assert_eq!(
10065 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
10066 0,
10067 "Buffer 3 was clean, so it should not have been written during autosave"
10068 );
10069
10070 // Verify buffer states after autosave
10071 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10072 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10073 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10074
10075 // Now perform a manual save (format = true)
10076 let save_task = editor.update_in(cx, |editor, window, cx| {
10077 editor.save(
10078 SaveOptions {
10079 format: true,
10080 autosave: false,
10081 },
10082 project.clone(),
10083 window,
10084 cx,
10085 )
10086 });
10087 save_task.await.unwrap();
10088
10089 // During manual save, clean buffers don't get written to disk
10090 // They just get did_save called for language server notifications
10091 assert_eq!(
10092 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
10093 1,
10094 "Buffer 1 should only have been written once total (during autosave, not manual save)"
10095 );
10096 assert_eq!(
10097 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
10098 0,
10099 "Buffer 2 should not have been written at all"
10100 );
10101 assert_eq!(
10102 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
10103 0,
10104 "Buffer 3 should not have been written at all"
10105 );
10106}
10107
10108async fn setup_range_format_test(
10109 cx: &mut TestAppContext,
10110) -> (
10111 Entity<Project>,
10112 Entity<Editor>,
10113 &mut gpui::VisualTestContext,
10114 lsp::FakeLanguageServer,
10115) {
10116 init_test(cx, |_| {});
10117
10118 let fs = FakeFs::new(cx.executor());
10119 fs.insert_file(path!("/file.rs"), Default::default()).await;
10120
10121 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10122
10123 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10124 language_registry.add(rust_lang());
10125 let mut fake_servers = language_registry.register_fake_lsp(
10126 "Rust",
10127 FakeLspAdapter {
10128 capabilities: lsp::ServerCapabilities {
10129 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
10130 ..lsp::ServerCapabilities::default()
10131 },
10132 ..FakeLspAdapter::default()
10133 },
10134 );
10135
10136 let buffer = project
10137 .update(cx, |project, cx| {
10138 project.open_local_buffer(path!("/file.rs"), cx)
10139 })
10140 .await
10141 .unwrap();
10142
10143 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10144 let (editor, cx) = cx.add_window_view(|window, cx| {
10145 build_editor_with_project(project.clone(), buffer, window, cx)
10146 });
10147
10148 cx.executor().start_waiting();
10149 let fake_server = fake_servers.next().await.unwrap();
10150
10151 (project, editor, cx, fake_server)
10152}
10153
10154#[gpui::test]
10155async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
10156 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10157
10158 editor.update_in(cx, |editor, window, cx| {
10159 editor.set_text("one\ntwo\nthree\n", window, cx)
10160 });
10161 assert!(cx.read(|cx| editor.is_dirty(cx)));
10162
10163 let save = editor
10164 .update_in(cx, |editor, window, cx| {
10165 editor.save(
10166 SaveOptions {
10167 format: true,
10168 autosave: false,
10169 },
10170 project.clone(),
10171 window,
10172 cx,
10173 )
10174 })
10175 .unwrap();
10176 fake_server
10177 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10178 assert_eq!(
10179 params.text_document.uri,
10180 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10181 );
10182 assert_eq!(params.options.tab_size, 4);
10183 Ok(Some(vec![lsp::TextEdit::new(
10184 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10185 ", ".to_string(),
10186 )]))
10187 })
10188 .next()
10189 .await;
10190 cx.executor().start_waiting();
10191 save.await;
10192 assert_eq!(
10193 editor.update(cx, |editor, cx| editor.text(cx)),
10194 "one, two\nthree\n"
10195 );
10196 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10197}
10198
10199#[gpui::test]
10200async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
10201 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10202
10203 editor.update_in(cx, |editor, window, cx| {
10204 editor.set_text("one\ntwo\nthree\n", window, cx)
10205 });
10206 assert!(cx.read(|cx| editor.is_dirty(cx)));
10207
10208 // Test that save still works when formatting hangs
10209 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
10210 move |params, _| async move {
10211 assert_eq!(
10212 params.text_document.uri,
10213 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10214 );
10215 futures::future::pending::<()>().await;
10216 unreachable!()
10217 },
10218 );
10219 let save = editor
10220 .update_in(cx, |editor, window, cx| {
10221 editor.save(
10222 SaveOptions {
10223 format: true,
10224 autosave: false,
10225 },
10226 project.clone(),
10227 window,
10228 cx,
10229 )
10230 })
10231 .unwrap();
10232 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10233 cx.executor().start_waiting();
10234 save.await;
10235 assert_eq!(
10236 editor.update(cx, |editor, cx| editor.text(cx)),
10237 "one\ntwo\nthree\n"
10238 );
10239 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10240}
10241
10242#[gpui::test]
10243async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
10244 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10245
10246 // Buffer starts clean, no formatting should be requested
10247 let save = editor
10248 .update_in(cx, |editor, window, cx| {
10249 editor.save(
10250 SaveOptions {
10251 format: false,
10252 autosave: false,
10253 },
10254 project.clone(),
10255 window,
10256 cx,
10257 )
10258 })
10259 .unwrap();
10260 let _pending_format_request = fake_server
10261 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
10262 panic!("Should not be invoked");
10263 })
10264 .next();
10265 cx.executor().start_waiting();
10266 save.await;
10267 cx.run_until_parked();
10268}
10269
10270#[gpui::test]
10271async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
10272 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10273
10274 // Set Rust language override and assert overridden tabsize is sent to language server
10275 update_test_language_settings(cx, |settings| {
10276 settings.languages.0.insert(
10277 "Rust".into(),
10278 LanguageSettingsContent {
10279 tab_size: NonZeroU32::new(8),
10280 ..Default::default()
10281 },
10282 );
10283 });
10284
10285 editor.update_in(cx, |editor, window, cx| {
10286 editor.set_text("something_new\n", window, cx)
10287 });
10288 assert!(cx.read(|cx| editor.is_dirty(cx)));
10289 let save = editor
10290 .update_in(cx, |editor, window, cx| {
10291 editor.save(
10292 SaveOptions {
10293 format: true,
10294 autosave: false,
10295 },
10296 project.clone(),
10297 window,
10298 cx,
10299 )
10300 })
10301 .unwrap();
10302 fake_server
10303 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10304 assert_eq!(
10305 params.text_document.uri,
10306 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10307 );
10308 assert_eq!(params.options.tab_size, 8);
10309 Ok(Some(Vec::new()))
10310 })
10311 .next()
10312 .await;
10313 save.await;
10314}
10315
10316#[gpui::test]
10317async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
10318 init_test(cx, |settings| {
10319 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
10320 Formatter::LanguageServer { name: None },
10321 )))
10322 });
10323
10324 let fs = FakeFs::new(cx.executor());
10325 fs.insert_file(path!("/file.rs"), Default::default()).await;
10326
10327 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10328
10329 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10330 language_registry.add(Arc::new(Language::new(
10331 LanguageConfig {
10332 name: "Rust".into(),
10333 matcher: LanguageMatcher {
10334 path_suffixes: vec!["rs".to_string()],
10335 ..Default::default()
10336 },
10337 ..LanguageConfig::default()
10338 },
10339 Some(tree_sitter_rust::LANGUAGE.into()),
10340 )));
10341 update_test_language_settings(cx, |settings| {
10342 // Enable Prettier formatting for the same buffer, and ensure
10343 // LSP is called instead of Prettier.
10344 settings.defaults.prettier = Some(PrettierSettings {
10345 allowed: true,
10346 ..PrettierSettings::default()
10347 });
10348 });
10349 let mut fake_servers = language_registry.register_fake_lsp(
10350 "Rust",
10351 FakeLspAdapter {
10352 capabilities: lsp::ServerCapabilities {
10353 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10354 ..Default::default()
10355 },
10356 ..Default::default()
10357 },
10358 );
10359
10360 let buffer = project
10361 .update(cx, |project, cx| {
10362 project.open_local_buffer(path!("/file.rs"), cx)
10363 })
10364 .await
10365 .unwrap();
10366
10367 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10368 let (editor, cx) = cx.add_window_view(|window, cx| {
10369 build_editor_with_project(project.clone(), buffer, window, cx)
10370 });
10371 editor.update_in(cx, |editor, window, cx| {
10372 editor.set_text("one\ntwo\nthree\n", window, cx)
10373 });
10374
10375 cx.executor().start_waiting();
10376 let fake_server = fake_servers.next().await.unwrap();
10377
10378 let format = editor
10379 .update_in(cx, |editor, window, cx| {
10380 editor.perform_format(
10381 project.clone(),
10382 FormatTrigger::Manual,
10383 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10384 window,
10385 cx,
10386 )
10387 })
10388 .unwrap();
10389 fake_server
10390 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10391 assert_eq!(
10392 params.text_document.uri,
10393 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10394 );
10395 assert_eq!(params.options.tab_size, 4);
10396 Ok(Some(vec![lsp::TextEdit::new(
10397 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10398 ", ".to_string(),
10399 )]))
10400 })
10401 .next()
10402 .await;
10403 cx.executor().start_waiting();
10404 format.await;
10405 assert_eq!(
10406 editor.update(cx, |editor, cx| editor.text(cx)),
10407 "one, two\nthree\n"
10408 );
10409
10410 editor.update_in(cx, |editor, window, cx| {
10411 editor.set_text("one\ntwo\nthree\n", window, cx)
10412 });
10413 // Ensure we don't lock if formatting hangs.
10414 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10415 move |params, _| async move {
10416 assert_eq!(
10417 params.text_document.uri,
10418 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10419 );
10420 futures::future::pending::<()>().await;
10421 unreachable!()
10422 },
10423 );
10424 let format = editor
10425 .update_in(cx, |editor, window, cx| {
10426 editor.perform_format(
10427 project,
10428 FormatTrigger::Manual,
10429 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10430 window,
10431 cx,
10432 )
10433 })
10434 .unwrap();
10435 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10436 cx.executor().start_waiting();
10437 format.await;
10438 assert_eq!(
10439 editor.update(cx, |editor, cx| editor.text(cx)),
10440 "one\ntwo\nthree\n"
10441 );
10442}
10443
10444#[gpui::test]
10445async fn test_multiple_formatters(cx: &mut TestAppContext) {
10446 init_test(cx, |settings| {
10447 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
10448 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10449 Formatter::LanguageServer { name: None },
10450 Formatter::CodeActions(
10451 [
10452 ("code-action-1".into(), true),
10453 ("code-action-2".into(), true),
10454 ]
10455 .into_iter()
10456 .collect(),
10457 ),
10458 ])))
10459 });
10460
10461 let fs = FakeFs::new(cx.executor());
10462 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
10463 .await;
10464
10465 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10466 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10467 language_registry.add(rust_lang());
10468
10469 let mut fake_servers = language_registry.register_fake_lsp(
10470 "Rust",
10471 FakeLspAdapter {
10472 capabilities: lsp::ServerCapabilities {
10473 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10474 execute_command_provider: Some(lsp::ExecuteCommandOptions {
10475 commands: vec!["the-command-for-code-action-1".into()],
10476 ..Default::default()
10477 }),
10478 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10479 ..Default::default()
10480 },
10481 ..Default::default()
10482 },
10483 );
10484
10485 let buffer = project
10486 .update(cx, |project, cx| {
10487 project.open_local_buffer(path!("/file.rs"), cx)
10488 })
10489 .await
10490 .unwrap();
10491
10492 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10493 let (editor, cx) = cx.add_window_view(|window, cx| {
10494 build_editor_with_project(project.clone(), buffer, window, cx)
10495 });
10496
10497 cx.executor().start_waiting();
10498
10499 let fake_server = fake_servers.next().await.unwrap();
10500 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10501 move |_params, _| async move {
10502 Ok(Some(vec![lsp::TextEdit::new(
10503 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10504 "applied-formatting\n".to_string(),
10505 )]))
10506 },
10507 );
10508 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10509 move |params, _| async move {
10510 assert_eq!(
10511 params.context.only,
10512 Some(vec!["code-action-1".into(), "code-action-2".into()])
10513 );
10514 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
10515 Ok(Some(vec![
10516 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10517 kind: Some("code-action-1".into()),
10518 edit: Some(lsp::WorkspaceEdit::new(
10519 [(
10520 uri.clone(),
10521 vec![lsp::TextEdit::new(
10522 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10523 "applied-code-action-1-edit\n".to_string(),
10524 )],
10525 )]
10526 .into_iter()
10527 .collect(),
10528 )),
10529 command: Some(lsp::Command {
10530 command: "the-command-for-code-action-1".into(),
10531 ..Default::default()
10532 }),
10533 ..Default::default()
10534 }),
10535 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10536 kind: Some("code-action-2".into()),
10537 edit: Some(lsp::WorkspaceEdit::new(
10538 [(
10539 uri.clone(),
10540 vec![lsp::TextEdit::new(
10541 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10542 "applied-code-action-2-edit\n".to_string(),
10543 )],
10544 )]
10545 .into_iter()
10546 .collect(),
10547 )),
10548 ..Default::default()
10549 }),
10550 ]))
10551 },
10552 );
10553
10554 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
10555 move |params, _| async move { Ok(params) }
10556 });
10557
10558 let command_lock = Arc::new(futures::lock::Mutex::new(()));
10559 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
10560 let fake = fake_server.clone();
10561 let lock = command_lock.clone();
10562 move |params, _| {
10563 assert_eq!(params.command, "the-command-for-code-action-1");
10564 let fake = fake.clone();
10565 let lock = lock.clone();
10566 async move {
10567 lock.lock().await;
10568 fake.server
10569 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
10570 label: None,
10571 edit: lsp::WorkspaceEdit {
10572 changes: Some(
10573 [(
10574 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
10575 vec![lsp::TextEdit {
10576 range: lsp::Range::new(
10577 lsp::Position::new(0, 0),
10578 lsp::Position::new(0, 0),
10579 ),
10580 new_text: "applied-code-action-1-command\n".into(),
10581 }],
10582 )]
10583 .into_iter()
10584 .collect(),
10585 ),
10586 ..Default::default()
10587 },
10588 })
10589 .await
10590 .into_response()
10591 .unwrap();
10592 Ok(Some(json!(null)))
10593 }
10594 }
10595 });
10596
10597 cx.executor().start_waiting();
10598 editor
10599 .update_in(cx, |editor, window, cx| {
10600 editor.perform_format(
10601 project.clone(),
10602 FormatTrigger::Manual,
10603 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10604 window,
10605 cx,
10606 )
10607 })
10608 .unwrap()
10609 .await;
10610 editor.update(cx, |editor, cx| {
10611 assert_eq!(
10612 editor.text(cx),
10613 r#"
10614 applied-code-action-2-edit
10615 applied-code-action-1-command
10616 applied-code-action-1-edit
10617 applied-formatting
10618 one
10619 two
10620 three
10621 "#
10622 .unindent()
10623 );
10624 });
10625
10626 editor.update_in(cx, |editor, window, cx| {
10627 editor.undo(&Default::default(), window, cx);
10628 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10629 });
10630
10631 // Perform a manual edit while waiting for an LSP command
10632 // that's being run as part of a formatting code action.
10633 let lock_guard = command_lock.lock().await;
10634 let format = editor
10635 .update_in(cx, |editor, window, cx| {
10636 editor.perform_format(
10637 project.clone(),
10638 FormatTrigger::Manual,
10639 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10640 window,
10641 cx,
10642 )
10643 })
10644 .unwrap();
10645 cx.run_until_parked();
10646 editor.update(cx, |editor, cx| {
10647 assert_eq!(
10648 editor.text(cx),
10649 r#"
10650 applied-code-action-1-edit
10651 applied-formatting
10652 one
10653 two
10654 three
10655 "#
10656 .unindent()
10657 );
10658
10659 editor.buffer.update(cx, |buffer, cx| {
10660 let ix = buffer.len(cx);
10661 buffer.edit([(ix..ix, "edited\n")], None, cx);
10662 });
10663 });
10664
10665 // Allow the LSP command to proceed. Because the buffer was edited,
10666 // the second code action will not be run.
10667 drop(lock_guard);
10668 format.await;
10669 editor.update_in(cx, |editor, window, cx| {
10670 assert_eq!(
10671 editor.text(cx),
10672 r#"
10673 applied-code-action-1-command
10674 applied-code-action-1-edit
10675 applied-formatting
10676 one
10677 two
10678 three
10679 edited
10680 "#
10681 .unindent()
10682 );
10683
10684 // The manual edit is undone first, because it is the last thing the user did
10685 // (even though the command completed afterwards).
10686 editor.undo(&Default::default(), window, cx);
10687 assert_eq!(
10688 editor.text(cx),
10689 r#"
10690 applied-code-action-1-command
10691 applied-code-action-1-edit
10692 applied-formatting
10693 one
10694 two
10695 three
10696 "#
10697 .unindent()
10698 );
10699
10700 // All the formatting (including the command, which completed after the manual edit)
10701 // is undone together.
10702 editor.undo(&Default::default(), window, cx);
10703 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10704 });
10705}
10706
10707#[gpui::test]
10708async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
10709 init_test(cx, |settings| {
10710 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10711 Formatter::LanguageServer { name: None },
10712 ])))
10713 });
10714
10715 let fs = FakeFs::new(cx.executor());
10716 fs.insert_file(path!("/file.ts"), Default::default()).await;
10717
10718 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10719
10720 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10721 language_registry.add(Arc::new(Language::new(
10722 LanguageConfig {
10723 name: "TypeScript".into(),
10724 matcher: LanguageMatcher {
10725 path_suffixes: vec!["ts".to_string()],
10726 ..Default::default()
10727 },
10728 ..LanguageConfig::default()
10729 },
10730 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10731 )));
10732 update_test_language_settings(cx, |settings| {
10733 settings.defaults.prettier = Some(PrettierSettings {
10734 allowed: true,
10735 ..PrettierSettings::default()
10736 });
10737 });
10738 let mut fake_servers = language_registry.register_fake_lsp(
10739 "TypeScript",
10740 FakeLspAdapter {
10741 capabilities: lsp::ServerCapabilities {
10742 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10743 ..Default::default()
10744 },
10745 ..Default::default()
10746 },
10747 );
10748
10749 let buffer = project
10750 .update(cx, |project, cx| {
10751 project.open_local_buffer(path!("/file.ts"), cx)
10752 })
10753 .await
10754 .unwrap();
10755
10756 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10757 let (editor, cx) = cx.add_window_view(|window, cx| {
10758 build_editor_with_project(project.clone(), buffer, window, cx)
10759 });
10760 editor.update_in(cx, |editor, window, cx| {
10761 editor.set_text(
10762 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10763 window,
10764 cx,
10765 )
10766 });
10767
10768 cx.executor().start_waiting();
10769 let fake_server = fake_servers.next().await.unwrap();
10770
10771 let format = editor
10772 .update_in(cx, |editor, window, cx| {
10773 editor.perform_code_action_kind(
10774 project.clone(),
10775 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10776 window,
10777 cx,
10778 )
10779 })
10780 .unwrap();
10781 fake_server
10782 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
10783 assert_eq!(
10784 params.text_document.uri,
10785 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10786 );
10787 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
10788 lsp::CodeAction {
10789 title: "Organize Imports".to_string(),
10790 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
10791 edit: Some(lsp::WorkspaceEdit {
10792 changes: Some(
10793 [(
10794 params.text_document.uri.clone(),
10795 vec![lsp::TextEdit::new(
10796 lsp::Range::new(
10797 lsp::Position::new(1, 0),
10798 lsp::Position::new(2, 0),
10799 ),
10800 "".to_string(),
10801 )],
10802 )]
10803 .into_iter()
10804 .collect(),
10805 ),
10806 ..Default::default()
10807 }),
10808 ..Default::default()
10809 },
10810 )]))
10811 })
10812 .next()
10813 .await;
10814 cx.executor().start_waiting();
10815 format.await;
10816 assert_eq!(
10817 editor.update(cx, |editor, cx| editor.text(cx)),
10818 "import { a } from 'module';\n\nconst x = a;\n"
10819 );
10820
10821 editor.update_in(cx, |editor, window, cx| {
10822 editor.set_text(
10823 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10824 window,
10825 cx,
10826 )
10827 });
10828 // Ensure we don't lock if code action hangs.
10829 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10830 move |params, _| async move {
10831 assert_eq!(
10832 params.text_document.uri,
10833 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10834 );
10835 futures::future::pending::<()>().await;
10836 unreachable!()
10837 },
10838 );
10839 let format = editor
10840 .update_in(cx, |editor, window, cx| {
10841 editor.perform_code_action_kind(
10842 project,
10843 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10844 window,
10845 cx,
10846 )
10847 })
10848 .unwrap();
10849 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
10850 cx.executor().start_waiting();
10851 format.await;
10852 assert_eq!(
10853 editor.update(cx, |editor, cx| editor.text(cx)),
10854 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
10855 );
10856}
10857
10858#[gpui::test]
10859async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
10860 init_test(cx, |_| {});
10861
10862 let mut cx = EditorLspTestContext::new_rust(
10863 lsp::ServerCapabilities {
10864 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10865 ..Default::default()
10866 },
10867 cx,
10868 )
10869 .await;
10870
10871 cx.set_state(indoc! {"
10872 one.twoˇ
10873 "});
10874
10875 // The format request takes a long time. When it completes, it inserts
10876 // a newline and an indent before the `.`
10877 cx.lsp
10878 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
10879 let executor = cx.background_executor().clone();
10880 async move {
10881 executor.timer(Duration::from_millis(100)).await;
10882 Ok(Some(vec![lsp::TextEdit {
10883 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
10884 new_text: "\n ".into(),
10885 }]))
10886 }
10887 });
10888
10889 // Submit a format request.
10890 let format_1 = cx
10891 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10892 .unwrap();
10893 cx.executor().run_until_parked();
10894
10895 // Submit a second format request.
10896 let format_2 = cx
10897 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10898 .unwrap();
10899 cx.executor().run_until_parked();
10900
10901 // Wait for both format requests to complete
10902 cx.executor().advance_clock(Duration::from_millis(200));
10903 cx.executor().start_waiting();
10904 format_1.await.unwrap();
10905 cx.executor().start_waiting();
10906 format_2.await.unwrap();
10907
10908 // The formatting edits only happens once.
10909 cx.assert_editor_state(indoc! {"
10910 one
10911 .twoˇ
10912 "});
10913}
10914
10915#[gpui::test]
10916async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
10917 init_test(cx, |settings| {
10918 settings.defaults.formatter = Some(SelectedFormatter::Auto)
10919 });
10920
10921 let mut cx = EditorLspTestContext::new_rust(
10922 lsp::ServerCapabilities {
10923 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10924 ..Default::default()
10925 },
10926 cx,
10927 )
10928 .await;
10929
10930 // Set up a buffer white some trailing whitespace and no trailing newline.
10931 cx.set_state(
10932 &[
10933 "one ", //
10934 "twoˇ", //
10935 "three ", //
10936 "four", //
10937 ]
10938 .join("\n"),
10939 );
10940
10941 // Submit a format request.
10942 let format = cx
10943 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10944 .unwrap();
10945
10946 // Record which buffer changes have been sent to the language server
10947 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
10948 cx.lsp
10949 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
10950 let buffer_changes = buffer_changes.clone();
10951 move |params, _| {
10952 buffer_changes.lock().extend(
10953 params
10954 .content_changes
10955 .into_iter()
10956 .map(|e| (e.range.unwrap(), e.text)),
10957 );
10958 }
10959 });
10960
10961 // Handle formatting requests to the language server.
10962 cx.lsp
10963 .set_request_handler::<lsp::request::Formatting, _, _>({
10964 let buffer_changes = buffer_changes.clone();
10965 move |_, _| {
10966 // When formatting is requested, trailing whitespace has already been stripped,
10967 // and the trailing newline has already been added.
10968 assert_eq!(
10969 &buffer_changes.lock()[1..],
10970 &[
10971 (
10972 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
10973 "".into()
10974 ),
10975 (
10976 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
10977 "".into()
10978 ),
10979 (
10980 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
10981 "\n".into()
10982 ),
10983 ]
10984 );
10985
10986 // Insert blank lines between each line of the buffer.
10987 async move {
10988 Ok(Some(vec![
10989 lsp::TextEdit {
10990 range: lsp::Range::new(
10991 lsp::Position::new(1, 0),
10992 lsp::Position::new(1, 0),
10993 ),
10994 new_text: "\n".into(),
10995 },
10996 lsp::TextEdit {
10997 range: lsp::Range::new(
10998 lsp::Position::new(2, 0),
10999 lsp::Position::new(2, 0),
11000 ),
11001 new_text: "\n".into(),
11002 },
11003 ]))
11004 }
11005 }
11006 });
11007
11008 // After formatting the buffer, the trailing whitespace is stripped,
11009 // a newline is appended, and the edits provided by the language server
11010 // have been applied.
11011 format.await.unwrap();
11012 cx.assert_editor_state(
11013 &[
11014 "one", //
11015 "", //
11016 "twoˇ", //
11017 "", //
11018 "three", //
11019 "four", //
11020 "", //
11021 ]
11022 .join("\n"),
11023 );
11024
11025 // Undoing the formatting undoes the trailing whitespace removal, the
11026 // trailing newline, and the LSP edits.
11027 cx.update_buffer(|buffer, cx| buffer.undo(cx));
11028 cx.assert_editor_state(
11029 &[
11030 "one ", //
11031 "twoˇ", //
11032 "three ", //
11033 "four", //
11034 ]
11035 .join("\n"),
11036 );
11037}
11038
11039#[gpui::test]
11040async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
11041 cx: &mut TestAppContext,
11042) {
11043 init_test(cx, |_| {});
11044
11045 cx.update(|cx| {
11046 cx.update_global::<SettingsStore, _>(|settings, cx| {
11047 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11048 settings.auto_signature_help = Some(true);
11049 });
11050 });
11051 });
11052
11053 let mut cx = EditorLspTestContext::new_rust(
11054 lsp::ServerCapabilities {
11055 signature_help_provider: Some(lsp::SignatureHelpOptions {
11056 ..Default::default()
11057 }),
11058 ..Default::default()
11059 },
11060 cx,
11061 )
11062 .await;
11063
11064 let language = Language::new(
11065 LanguageConfig {
11066 name: "Rust".into(),
11067 brackets: BracketPairConfig {
11068 pairs: vec![
11069 BracketPair {
11070 start: "{".to_string(),
11071 end: "}".to_string(),
11072 close: true,
11073 surround: true,
11074 newline: true,
11075 },
11076 BracketPair {
11077 start: "(".to_string(),
11078 end: ")".to_string(),
11079 close: true,
11080 surround: true,
11081 newline: true,
11082 },
11083 BracketPair {
11084 start: "/*".to_string(),
11085 end: " */".to_string(),
11086 close: true,
11087 surround: true,
11088 newline: true,
11089 },
11090 BracketPair {
11091 start: "[".to_string(),
11092 end: "]".to_string(),
11093 close: false,
11094 surround: false,
11095 newline: true,
11096 },
11097 BracketPair {
11098 start: "\"".to_string(),
11099 end: "\"".to_string(),
11100 close: true,
11101 surround: true,
11102 newline: false,
11103 },
11104 BracketPair {
11105 start: "<".to_string(),
11106 end: ">".to_string(),
11107 close: false,
11108 surround: true,
11109 newline: true,
11110 },
11111 ],
11112 ..Default::default()
11113 },
11114 autoclose_before: "})]".to_string(),
11115 ..Default::default()
11116 },
11117 Some(tree_sitter_rust::LANGUAGE.into()),
11118 );
11119 let language = Arc::new(language);
11120
11121 cx.language_registry().add(language.clone());
11122 cx.update_buffer(|buffer, cx| {
11123 buffer.set_language(Some(language), cx);
11124 });
11125
11126 cx.set_state(
11127 &r#"
11128 fn main() {
11129 sampleˇ
11130 }
11131 "#
11132 .unindent(),
11133 );
11134
11135 cx.update_editor(|editor, window, cx| {
11136 editor.handle_input("(", window, cx);
11137 });
11138 cx.assert_editor_state(
11139 &"
11140 fn main() {
11141 sample(ˇ)
11142 }
11143 "
11144 .unindent(),
11145 );
11146
11147 let mocked_response = lsp::SignatureHelp {
11148 signatures: vec![lsp::SignatureInformation {
11149 label: "fn sample(param1: u8, param2: u8)".to_string(),
11150 documentation: None,
11151 parameters: Some(vec![
11152 lsp::ParameterInformation {
11153 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11154 documentation: None,
11155 },
11156 lsp::ParameterInformation {
11157 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11158 documentation: None,
11159 },
11160 ]),
11161 active_parameter: None,
11162 }],
11163 active_signature: Some(0),
11164 active_parameter: Some(0),
11165 };
11166 handle_signature_help_request(&mut cx, mocked_response).await;
11167
11168 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11169 .await;
11170
11171 cx.editor(|editor, _, _| {
11172 let signature_help_state = editor.signature_help_state.popover().cloned();
11173 let signature = signature_help_state.unwrap();
11174 assert_eq!(
11175 signature.signatures[signature.current_signature].label,
11176 "fn sample(param1: u8, param2: u8)"
11177 );
11178 });
11179}
11180
11181#[gpui::test]
11182async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
11183 init_test(cx, |_| {});
11184
11185 cx.update(|cx| {
11186 cx.update_global::<SettingsStore, _>(|settings, cx| {
11187 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11188 settings.auto_signature_help = Some(false);
11189 settings.show_signature_help_after_edits = Some(false);
11190 });
11191 });
11192 });
11193
11194 let mut cx = EditorLspTestContext::new_rust(
11195 lsp::ServerCapabilities {
11196 signature_help_provider: Some(lsp::SignatureHelpOptions {
11197 ..Default::default()
11198 }),
11199 ..Default::default()
11200 },
11201 cx,
11202 )
11203 .await;
11204
11205 let language = Language::new(
11206 LanguageConfig {
11207 name: "Rust".into(),
11208 brackets: BracketPairConfig {
11209 pairs: vec![
11210 BracketPair {
11211 start: "{".to_string(),
11212 end: "}".to_string(),
11213 close: true,
11214 surround: true,
11215 newline: true,
11216 },
11217 BracketPair {
11218 start: "(".to_string(),
11219 end: ")".to_string(),
11220 close: true,
11221 surround: true,
11222 newline: true,
11223 },
11224 BracketPair {
11225 start: "/*".to_string(),
11226 end: " */".to_string(),
11227 close: true,
11228 surround: true,
11229 newline: true,
11230 },
11231 BracketPair {
11232 start: "[".to_string(),
11233 end: "]".to_string(),
11234 close: false,
11235 surround: false,
11236 newline: true,
11237 },
11238 BracketPair {
11239 start: "\"".to_string(),
11240 end: "\"".to_string(),
11241 close: true,
11242 surround: true,
11243 newline: false,
11244 },
11245 BracketPair {
11246 start: "<".to_string(),
11247 end: ">".to_string(),
11248 close: false,
11249 surround: true,
11250 newline: true,
11251 },
11252 ],
11253 ..Default::default()
11254 },
11255 autoclose_before: "})]".to_string(),
11256 ..Default::default()
11257 },
11258 Some(tree_sitter_rust::LANGUAGE.into()),
11259 );
11260 let language = Arc::new(language);
11261
11262 cx.language_registry().add(language.clone());
11263 cx.update_buffer(|buffer, cx| {
11264 buffer.set_language(Some(language), cx);
11265 });
11266
11267 // Ensure that signature_help is not called when no signature help is enabled.
11268 cx.set_state(
11269 &r#"
11270 fn main() {
11271 sampleˇ
11272 }
11273 "#
11274 .unindent(),
11275 );
11276 cx.update_editor(|editor, window, cx| {
11277 editor.handle_input("(", window, cx);
11278 });
11279 cx.assert_editor_state(
11280 &"
11281 fn main() {
11282 sample(ˇ)
11283 }
11284 "
11285 .unindent(),
11286 );
11287 cx.editor(|editor, _, _| {
11288 assert!(editor.signature_help_state.task().is_none());
11289 });
11290
11291 let mocked_response = lsp::SignatureHelp {
11292 signatures: vec![lsp::SignatureInformation {
11293 label: "fn sample(param1: u8, param2: u8)".to_string(),
11294 documentation: None,
11295 parameters: Some(vec![
11296 lsp::ParameterInformation {
11297 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11298 documentation: None,
11299 },
11300 lsp::ParameterInformation {
11301 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11302 documentation: None,
11303 },
11304 ]),
11305 active_parameter: None,
11306 }],
11307 active_signature: Some(0),
11308 active_parameter: Some(0),
11309 };
11310
11311 // Ensure that signature_help is called when enabled afte edits
11312 cx.update(|_, cx| {
11313 cx.update_global::<SettingsStore, _>(|settings, cx| {
11314 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11315 settings.auto_signature_help = Some(false);
11316 settings.show_signature_help_after_edits = Some(true);
11317 });
11318 });
11319 });
11320 cx.set_state(
11321 &r#"
11322 fn main() {
11323 sampleˇ
11324 }
11325 "#
11326 .unindent(),
11327 );
11328 cx.update_editor(|editor, window, cx| {
11329 editor.handle_input("(", window, cx);
11330 });
11331 cx.assert_editor_state(
11332 &"
11333 fn main() {
11334 sample(ˇ)
11335 }
11336 "
11337 .unindent(),
11338 );
11339 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11340 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11341 .await;
11342 cx.update_editor(|editor, _, _| {
11343 let signature_help_state = editor.signature_help_state.popover().cloned();
11344 assert!(signature_help_state.is_some());
11345 let signature = signature_help_state.unwrap();
11346 assert_eq!(
11347 signature.signatures[signature.current_signature].label,
11348 "fn sample(param1: u8, param2: u8)"
11349 );
11350 editor.signature_help_state = SignatureHelpState::default();
11351 });
11352
11353 // Ensure that signature_help is called when auto signature help override is enabled
11354 cx.update(|_, cx| {
11355 cx.update_global::<SettingsStore, _>(|settings, cx| {
11356 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11357 settings.auto_signature_help = Some(true);
11358 settings.show_signature_help_after_edits = Some(false);
11359 });
11360 });
11361 });
11362 cx.set_state(
11363 &r#"
11364 fn main() {
11365 sampleˇ
11366 }
11367 "#
11368 .unindent(),
11369 );
11370 cx.update_editor(|editor, window, cx| {
11371 editor.handle_input("(", window, cx);
11372 });
11373 cx.assert_editor_state(
11374 &"
11375 fn main() {
11376 sample(ˇ)
11377 }
11378 "
11379 .unindent(),
11380 );
11381 handle_signature_help_request(&mut cx, mocked_response).await;
11382 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11383 .await;
11384 cx.editor(|editor, _, _| {
11385 let signature_help_state = editor.signature_help_state.popover().cloned();
11386 assert!(signature_help_state.is_some());
11387 let signature = signature_help_state.unwrap();
11388 assert_eq!(
11389 signature.signatures[signature.current_signature].label,
11390 "fn sample(param1: u8, param2: u8)"
11391 );
11392 });
11393}
11394
11395#[gpui::test]
11396async fn test_signature_help(cx: &mut TestAppContext) {
11397 init_test(cx, |_| {});
11398 cx.update(|cx| {
11399 cx.update_global::<SettingsStore, _>(|settings, cx| {
11400 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11401 settings.auto_signature_help = Some(true);
11402 });
11403 });
11404 });
11405
11406 let mut cx = EditorLspTestContext::new_rust(
11407 lsp::ServerCapabilities {
11408 signature_help_provider: Some(lsp::SignatureHelpOptions {
11409 ..Default::default()
11410 }),
11411 ..Default::default()
11412 },
11413 cx,
11414 )
11415 .await;
11416
11417 // A test that directly calls `show_signature_help`
11418 cx.update_editor(|editor, window, cx| {
11419 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11420 });
11421
11422 let mocked_response = lsp::SignatureHelp {
11423 signatures: vec![lsp::SignatureInformation {
11424 label: "fn sample(param1: u8, param2: u8)".to_string(),
11425 documentation: None,
11426 parameters: Some(vec![
11427 lsp::ParameterInformation {
11428 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11429 documentation: None,
11430 },
11431 lsp::ParameterInformation {
11432 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11433 documentation: None,
11434 },
11435 ]),
11436 active_parameter: None,
11437 }],
11438 active_signature: Some(0),
11439 active_parameter: Some(0),
11440 };
11441 handle_signature_help_request(&mut cx, mocked_response).await;
11442
11443 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11444 .await;
11445
11446 cx.editor(|editor, _, _| {
11447 let signature_help_state = editor.signature_help_state.popover().cloned();
11448 assert!(signature_help_state.is_some());
11449 let signature = signature_help_state.unwrap();
11450 assert_eq!(
11451 signature.signatures[signature.current_signature].label,
11452 "fn sample(param1: u8, param2: u8)"
11453 );
11454 });
11455
11456 // When exiting outside from inside the brackets, `signature_help` is closed.
11457 cx.set_state(indoc! {"
11458 fn main() {
11459 sample(ˇ);
11460 }
11461
11462 fn sample(param1: u8, param2: u8) {}
11463 "});
11464
11465 cx.update_editor(|editor, window, cx| {
11466 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11467 s.select_ranges([0..0])
11468 });
11469 });
11470
11471 let mocked_response = lsp::SignatureHelp {
11472 signatures: Vec::new(),
11473 active_signature: None,
11474 active_parameter: None,
11475 };
11476 handle_signature_help_request(&mut cx, mocked_response).await;
11477
11478 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11479 .await;
11480
11481 cx.editor(|editor, _, _| {
11482 assert!(!editor.signature_help_state.is_shown());
11483 });
11484
11485 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
11486 cx.set_state(indoc! {"
11487 fn main() {
11488 sample(ˇ);
11489 }
11490
11491 fn sample(param1: u8, param2: u8) {}
11492 "});
11493
11494 let mocked_response = lsp::SignatureHelp {
11495 signatures: vec![lsp::SignatureInformation {
11496 label: "fn sample(param1: u8, param2: u8)".to_string(),
11497 documentation: None,
11498 parameters: Some(vec![
11499 lsp::ParameterInformation {
11500 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11501 documentation: None,
11502 },
11503 lsp::ParameterInformation {
11504 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11505 documentation: None,
11506 },
11507 ]),
11508 active_parameter: None,
11509 }],
11510 active_signature: Some(0),
11511 active_parameter: Some(0),
11512 };
11513 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11514 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11515 .await;
11516 cx.editor(|editor, _, _| {
11517 assert!(editor.signature_help_state.is_shown());
11518 });
11519
11520 // Restore the popover with more parameter input
11521 cx.set_state(indoc! {"
11522 fn main() {
11523 sample(param1, param2ˇ);
11524 }
11525
11526 fn sample(param1: u8, param2: u8) {}
11527 "});
11528
11529 let mocked_response = lsp::SignatureHelp {
11530 signatures: vec![lsp::SignatureInformation {
11531 label: "fn sample(param1: u8, param2: u8)".to_string(),
11532 documentation: None,
11533 parameters: Some(vec![
11534 lsp::ParameterInformation {
11535 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11536 documentation: None,
11537 },
11538 lsp::ParameterInformation {
11539 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11540 documentation: None,
11541 },
11542 ]),
11543 active_parameter: None,
11544 }],
11545 active_signature: Some(0),
11546 active_parameter: Some(1),
11547 };
11548 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11549 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11550 .await;
11551
11552 // When selecting a range, the popover is gone.
11553 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
11554 cx.update_editor(|editor, window, cx| {
11555 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11556 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11557 })
11558 });
11559 cx.assert_editor_state(indoc! {"
11560 fn main() {
11561 sample(param1, «ˇparam2»);
11562 }
11563
11564 fn sample(param1: u8, param2: u8) {}
11565 "});
11566 cx.editor(|editor, _, _| {
11567 assert!(!editor.signature_help_state.is_shown());
11568 });
11569
11570 // When unselecting again, the popover is back if within the brackets.
11571 cx.update_editor(|editor, window, cx| {
11572 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11573 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11574 })
11575 });
11576 cx.assert_editor_state(indoc! {"
11577 fn main() {
11578 sample(param1, ˇparam2);
11579 }
11580
11581 fn sample(param1: u8, param2: u8) {}
11582 "});
11583 handle_signature_help_request(&mut cx, mocked_response).await;
11584 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11585 .await;
11586 cx.editor(|editor, _, _| {
11587 assert!(editor.signature_help_state.is_shown());
11588 });
11589
11590 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
11591 cx.update_editor(|editor, window, cx| {
11592 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11593 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
11594 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11595 })
11596 });
11597 cx.assert_editor_state(indoc! {"
11598 fn main() {
11599 sample(param1, ˇparam2);
11600 }
11601
11602 fn sample(param1: u8, param2: u8) {}
11603 "});
11604
11605 let mocked_response = lsp::SignatureHelp {
11606 signatures: vec![lsp::SignatureInformation {
11607 label: "fn sample(param1: u8, param2: u8)".to_string(),
11608 documentation: None,
11609 parameters: Some(vec![
11610 lsp::ParameterInformation {
11611 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11612 documentation: None,
11613 },
11614 lsp::ParameterInformation {
11615 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11616 documentation: None,
11617 },
11618 ]),
11619 active_parameter: None,
11620 }],
11621 active_signature: Some(0),
11622 active_parameter: Some(1),
11623 };
11624 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11625 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11626 .await;
11627 cx.update_editor(|editor, _, cx| {
11628 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
11629 });
11630 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11631 .await;
11632 cx.update_editor(|editor, window, cx| {
11633 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11634 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11635 })
11636 });
11637 cx.assert_editor_state(indoc! {"
11638 fn main() {
11639 sample(param1, «ˇparam2»);
11640 }
11641
11642 fn sample(param1: u8, param2: u8) {}
11643 "});
11644 cx.update_editor(|editor, window, cx| {
11645 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11646 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11647 })
11648 });
11649 cx.assert_editor_state(indoc! {"
11650 fn main() {
11651 sample(param1, ˇparam2);
11652 }
11653
11654 fn sample(param1: u8, param2: u8) {}
11655 "});
11656 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
11657 .await;
11658}
11659
11660#[gpui::test]
11661async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
11662 init_test(cx, |_| {});
11663
11664 let mut cx = EditorLspTestContext::new_rust(
11665 lsp::ServerCapabilities {
11666 signature_help_provider: Some(lsp::SignatureHelpOptions {
11667 ..Default::default()
11668 }),
11669 ..Default::default()
11670 },
11671 cx,
11672 )
11673 .await;
11674
11675 cx.set_state(indoc! {"
11676 fn main() {
11677 overloadedˇ
11678 }
11679 "});
11680
11681 cx.update_editor(|editor, window, cx| {
11682 editor.handle_input("(", window, cx);
11683 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11684 });
11685
11686 // Mock response with 3 signatures
11687 let mocked_response = lsp::SignatureHelp {
11688 signatures: vec![
11689 lsp::SignatureInformation {
11690 label: "fn overloaded(x: i32)".to_string(),
11691 documentation: None,
11692 parameters: Some(vec![lsp::ParameterInformation {
11693 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11694 documentation: None,
11695 }]),
11696 active_parameter: None,
11697 },
11698 lsp::SignatureInformation {
11699 label: "fn overloaded(x: i32, y: i32)".to_string(),
11700 documentation: None,
11701 parameters: Some(vec![
11702 lsp::ParameterInformation {
11703 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11704 documentation: None,
11705 },
11706 lsp::ParameterInformation {
11707 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11708 documentation: None,
11709 },
11710 ]),
11711 active_parameter: None,
11712 },
11713 lsp::SignatureInformation {
11714 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
11715 documentation: None,
11716 parameters: Some(vec![
11717 lsp::ParameterInformation {
11718 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11719 documentation: None,
11720 },
11721 lsp::ParameterInformation {
11722 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11723 documentation: None,
11724 },
11725 lsp::ParameterInformation {
11726 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
11727 documentation: None,
11728 },
11729 ]),
11730 active_parameter: None,
11731 },
11732 ],
11733 active_signature: Some(1),
11734 active_parameter: Some(0),
11735 };
11736 handle_signature_help_request(&mut cx, mocked_response).await;
11737
11738 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11739 .await;
11740
11741 // Verify we have multiple signatures and the right one is selected
11742 cx.editor(|editor, _, _| {
11743 let popover = editor.signature_help_state.popover().cloned().unwrap();
11744 assert_eq!(popover.signatures.len(), 3);
11745 // active_signature was 1, so that should be the current
11746 assert_eq!(popover.current_signature, 1);
11747 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
11748 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
11749 assert_eq!(
11750 popover.signatures[2].label,
11751 "fn overloaded(x: i32, y: i32, z: i32)"
11752 );
11753 });
11754
11755 // Test navigation functionality
11756 cx.update_editor(|editor, window, cx| {
11757 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11758 });
11759
11760 cx.editor(|editor, _, _| {
11761 let popover = editor.signature_help_state.popover().cloned().unwrap();
11762 assert_eq!(popover.current_signature, 2);
11763 });
11764
11765 // Test wrap around
11766 cx.update_editor(|editor, window, cx| {
11767 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11768 });
11769
11770 cx.editor(|editor, _, _| {
11771 let popover = editor.signature_help_state.popover().cloned().unwrap();
11772 assert_eq!(popover.current_signature, 0);
11773 });
11774
11775 // Test previous navigation
11776 cx.update_editor(|editor, window, cx| {
11777 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
11778 });
11779
11780 cx.editor(|editor, _, _| {
11781 let popover = editor.signature_help_state.popover().cloned().unwrap();
11782 assert_eq!(popover.current_signature, 2);
11783 });
11784}
11785
11786#[gpui::test]
11787async fn test_completion_mode(cx: &mut TestAppContext) {
11788 init_test(cx, |_| {});
11789 let mut cx = EditorLspTestContext::new_rust(
11790 lsp::ServerCapabilities {
11791 completion_provider: Some(lsp::CompletionOptions {
11792 resolve_provider: Some(true),
11793 ..Default::default()
11794 }),
11795 ..Default::default()
11796 },
11797 cx,
11798 )
11799 .await;
11800
11801 struct Run {
11802 run_description: &'static str,
11803 initial_state: String,
11804 buffer_marked_text: String,
11805 completion_label: &'static str,
11806 completion_text: &'static str,
11807 expected_with_insert_mode: String,
11808 expected_with_replace_mode: String,
11809 expected_with_replace_subsequence_mode: String,
11810 expected_with_replace_suffix_mode: String,
11811 }
11812
11813 let runs = [
11814 Run {
11815 run_description: "Start of word matches completion text",
11816 initial_state: "before ediˇ after".into(),
11817 buffer_marked_text: "before <edi|> after".into(),
11818 completion_label: "editor",
11819 completion_text: "editor",
11820 expected_with_insert_mode: "before editorˇ after".into(),
11821 expected_with_replace_mode: "before editorˇ after".into(),
11822 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11823 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11824 },
11825 Run {
11826 run_description: "Accept same text at the middle of the word",
11827 initial_state: "before ediˇtor after".into(),
11828 buffer_marked_text: "before <edi|tor> after".into(),
11829 completion_label: "editor",
11830 completion_text: "editor",
11831 expected_with_insert_mode: "before editorˇtor after".into(),
11832 expected_with_replace_mode: "before editorˇ after".into(),
11833 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11834 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11835 },
11836 Run {
11837 run_description: "End of word matches completion text -- cursor at end",
11838 initial_state: "before torˇ after".into(),
11839 buffer_marked_text: "before <tor|> after".into(),
11840 completion_label: "editor",
11841 completion_text: "editor",
11842 expected_with_insert_mode: "before editorˇ after".into(),
11843 expected_with_replace_mode: "before editorˇ after".into(),
11844 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11845 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11846 },
11847 Run {
11848 run_description: "End of word matches completion text -- cursor at start",
11849 initial_state: "before ˇtor after".into(),
11850 buffer_marked_text: "before <|tor> after".into(),
11851 completion_label: "editor",
11852 completion_text: "editor",
11853 expected_with_insert_mode: "before editorˇtor after".into(),
11854 expected_with_replace_mode: "before editorˇ after".into(),
11855 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11856 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11857 },
11858 Run {
11859 run_description: "Prepend text containing whitespace",
11860 initial_state: "pˇfield: bool".into(),
11861 buffer_marked_text: "<p|field>: bool".into(),
11862 completion_label: "pub ",
11863 completion_text: "pub ",
11864 expected_with_insert_mode: "pub ˇfield: bool".into(),
11865 expected_with_replace_mode: "pub ˇ: bool".into(),
11866 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
11867 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
11868 },
11869 Run {
11870 run_description: "Add element to start of list",
11871 initial_state: "[element_ˇelement_2]".into(),
11872 buffer_marked_text: "[<element_|element_2>]".into(),
11873 completion_label: "element_1",
11874 completion_text: "element_1",
11875 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
11876 expected_with_replace_mode: "[element_1ˇ]".into(),
11877 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
11878 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
11879 },
11880 Run {
11881 run_description: "Add element to start of list -- first and second elements are equal",
11882 initial_state: "[elˇelement]".into(),
11883 buffer_marked_text: "[<el|element>]".into(),
11884 completion_label: "element",
11885 completion_text: "element",
11886 expected_with_insert_mode: "[elementˇelement]".into(),
11887 expected_with_replace_mode: "[elementˇ]".into(),
11888 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
11889 expected_with_replace_suffix_mode: "[elementˇ]".into(),
11890 },
11891 Run {
11892 run_description: "Ends with matching suffix",
11893 initial_state: "SubˇError".into(),
11894 buffer_marked_text: "<Sub|Error>".into(),
11895 completion_label: "SubscriptionError",
11896 completion_text: "SubscriptionError",
11897 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
11898 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11899 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11900 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
11901 },
11902 Run {
11903 run_description: "Suffix is a subsequence -- contiguous",
11904 initial_state: "SubˇErr".into(),
11905 buffer_marked_text: "<Sub|Err>".into(),
11906 completion_label: "SubscriptionError",
11907 completion_text: "SubscriptionError",
11908 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
11909 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11910 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11911 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
11912 },
11913 Run {
11914 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
11915 initial_state: "Suˇscrirr".into(),
11916 buffer_marked_text: "<Su|scrirr>".into(),
11917 completion_label: "SubscriptionError",
11918 completion_text: "SubscriptionError",
11919 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
11920 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11921 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11922 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
11923 },
11924 Run {
11925 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
11926 initial_state: "foo(indˇix)".into(),
11927 buffer_marked_text: "foo(<ind|ix>)".into(),
11928 completion_label: "node_index",
11929 completion_text: "node_index",
11930 expected_with_insert_mode: "foo(node_indexˇix)".into(),
11931 expected_with_replace_mode: "foo(node_indexˇ)".into(),
11932 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
11933 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
11934 },
11935 Run {
11936 run_description: "Replace range ends before cursor - should extend to cursor",
11937 initial_state: "before editˇo after".into(),
11938 buffer_marked_text: "before <{ed}>it|o after".into(),
11939 completion_label: "editor",
11940 completion_text: "editor",
11941 expected_with_insert_mode: "before editorˇo after".into(),
11942 expected_with_replace_mode: "before editorˇo after".into(),
11943 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
11944 expected_with_replace_suffix_mode: "before editorˇo after".into(),
11945 },
11946 Run {
11947 run_description: "Uses label for suffix matching",
11948 initial_state: "before ediˇtor after".into(),
11949 buffer_marked_text: "before <edi|tor> after".into(),
11950 completion_label: "editor",
11951 completion_text: "editor()",
11952 expected_with_insert_mode: "before editor()ˇtor after".into(),
11953 expected_with_replace_mode: "before editor()ˇ after".into(),
11954 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
11955 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
11956 },
11957 Run {
11958 run_description: "Case insensitive subsequence and suffix matching",
11959 initial_state: "before EDiˇtoR after".into(),
11960 buffer_marked_text: "before <EDi|toR> after".into(),
11961 completion_label: "editor",
11962 completion_text: "editor",
11963 expected_with_insert_mode: "before editorˇtoR after".into(),
11964 expected_with_replace_mode: "before editorˇ after".into(),
11965 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11966 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11967 },
11968 ];
11969
11970 for run in runs {
11971 let run_variations = [
11972 (LspInsertMode::Insert, run.expected_with_insert_mode),
11973 (LspInsertMode::Replace, run.expected_with_replace_mode),
11974 (
11975 LspInsertMode::ReplaceSubsequence,
11976 run.expected_with_replace_subsequence_mode,
11977 ),
11978 (
11979 LspInsertMode::ReplaceSuffix,
11980 run.expected_with_replace_suffix_mode,
11981 ),
11982 ];
11983
11984 for (lsp_insert_mode, expected_text) in run_variations {
11985 eprintln!(
11986 "run = {:?}, mode = {lsp_insert_mode:.?}",
11987 run.run_description,
11988 );
11989
11990 update_test_language_settings(&mut cx, |settings| {
11991 settings.defaults.completions = Some(CompletionSettings {
11992 lsp_insert_mode,
11993 words: WordsCompletionMode::Disabled,
11994 lsp: true,
11995 lsp_fetch_timeout_ms: 0,
11996 });
11997 });
11998
11999 cx.set_state(&run.initial_state);
12000 cx.update_editor(|editor, window, cx| {
12001 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12002 });
12003
12004 let counter = Arc::new(AtomicUsize::new(0));
12005 handle_completion_request_with_insert_and_replace(
12006 &mut cx,
12007 &run.buffer_marked_text,
12008 vec![(run.completion_label, run.completion_text)],
12009 counter.clone(),
12010 )
12011 .await;
12012 cx.condition(|editor, _| editor.context_menu_visible())
12013 .await;
12014 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12015
12016 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12017 editor
12018 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12019 .unwrap()
12020 });
12021 cx.assert_editor_state(&expected_text);
12022 handle_resolve_completion_request(&mut cx, None).await;
12023 apply_additional_edits.await.unwrap();
12024 }
12025 }
12026}
12027
12028#[gpui::test]
12029async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
12030 init_test(cx, |_| {});
12031 let mut cx = EditorLspTestContext::new_rust(
12032 lsp::ServerCapabilities {
12033 completion_provider: Some(lsp::CompletionOptions {
12034 resolve_provider: Some(true),
12035 ..Default::default()
12036 }),
12037 ..Default::default()
12038 },
12039 cx,
12040 )
12041 .await;
12042
12043 let initial_state = "SubˇError";
12044 let buffer_marked_text = "<Sub|Error>";
12045 let completion_text = "SubscriptionError";
12046 let expected_with_insert_mode = "SubscriptionErrorˇError";
12047 let expected_with_replace_mode = "SubscriptionErrorˇ";
12048
12049 update_test_language_settings(&mut cx, |settings| {
12050 settings.defaults.completions = Some(CompletionSettings {
12051 words: WordsCompletionMode::Disabled,
12052 // set the opposite here to ensure that the action is overriding the default behavior
12053 lsp_insert_mode: LspInsertMode::Insert,
12054 lsp: true,
12055 lsp_fetch_timeout_ms: 0,
12056 });
12057 });
12058
12059 cx.set_state(initial_state);
12060 cx.update_editor(|editor, window, cx| {
12061 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12062 });
12063
12064 let counter = Arc::new(AtomicUsize::new(0));
12065 handle_completion_request_with_insert_and_replace(
12066 &mut cx,
12067 &buffer_marked_text,
12068 vec![(completion_text, completion_text)],
12069 counter.clone(),
12070 )
12071 .await;
12072 cx.condition(|editor, _| editor.context_menu_visible())
12073 .await;
12074 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12075
12076 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12077 editor
12078 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12079 .unwrap()
12080 });
12081 cx.assert_editor_state(&expected_with_replace_mode);
12082 handle_resolve_completion_request(&mut cx, None).await;
12083 apply_additional_edits.await.unwrap();
12084
12085 update_test_language_settings(&mut cx, |settings| {
12086 settings.defaults.completions = Some(CompletionSettings {
12087 words: WordsCompletionMode::Disabled,
12088 // set the opposite here to ensure that the action is overriding the default behavior
12089 lsp_insert_mode: LspInsertMode::Replace,
12090 lsp: true,
12091 lsp_fetch_timeout_ms: 0,
12092 });
12093 });
12094
12095 cx.set_state(initial_state);
12096 cx.update_editor(|editor, window, cx| {
12097 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12098 });
12099 handle_completion_request_with_insert_and_replace(
12100 &mut cx,
12101 &buffer_marked_text,
12102 vec![(completion_text, completion_text)],
12103 counter.clone(),
12104 )
12105 .await;
12106 cx.condition(|editor, _| editor.context_menu_visible())
12107 .await;
12108 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12109
12110 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12111 editor
12112 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
12113 .unwrap()
12114 });
12115 cx.assert_editor_state(&expected_with_insert_mode);
12116 handle_resolve_completion_request(&mut cx, None).await;
12117 apply_additional_edits.await.unwrap();
12118}
12119
12120#[gpui::test]
12121async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
12122 init_test(cx, |_| {});
12123 let mut cx = EditorLspTestContext::new_rust(
12124 lsp::ServerCapabilities {
12125 completion_provider: Some(lsp::CompletionOptions {
12126 resolve_provider: Some(true),
12127 ..Default::default()
12128 }),
12129 ..Default::default()
12130 },
12131 cx,
12132 )
12133 .await;
12134
12135 // scenario: surrounding text matches completion text
12136 let completion_text = "to_offset";
12137 let initial_state = indoc! {"
12138 1. buf.to_offˇsuffix
12139 2. buf.to_offˇsuf
12140 3. buf.to_offˇfix
12141 4. buf.to_offˇ
12142 5. into_offˇensive
12143 6. ˇsuffix
12144 7. let ˇ //
12145 8. aaˇzz
12146 9. buf.to_off«zzzzzˇ»suffix
12147 10. buf.«ˇzzzzz»suffix
12148 11. to_off«ˇzzzzz»
12149
12150 buf.to_offˇsuffix // newest cursor
12151 "};
12152 let completion_marked_buffer = indoc! {"
12153 1. buf.to_offsuffix
12154 2. buf.to_offsuf
12155 3. buf.to_offfix
12156 4. buf.to_off
12157 5. into_offensive
12158 6. suffix
12159 7. let //
12160 8. aazz
12161 9. buf.to_offzzzzzsuffix
12162 10. buf.zzzzzsuffix
12163 11. to_offzzzzz
12164
12165 buf.<to_off|suffix> // newest cursor
12166 "};
12167 let expected = indoc! {"
12168 1. buf.to_offsetˇ
12169 2. buf.to_offsetˇsuf
12170 3. buf.to_offsetˇfix
12171 4. buf.to_offsetˇ
12172 5. into_offsetˇensive
12173 6. to_offsetˇsuffix
12174 7. let to_offsetˇ //
12175 8. aato_offsetˇzz
12176 9. buf.to_offsetˇ
12177 10. buf.to_offsetˇsuffix
12178 11. to_offsetˇ
12179
12180 buf.to_offsetˇ // newest cursor
12181 "};
12182 cx.set_state(initial_state);
12183 cx.update_editor(|editor, window, cx| {
12184 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12185 });
12186 handle_completion_request_with_insert_and_replace(
12187 &mut cx,
12188 completion_marked_buffer,
12189 vec![(completion_text, completion_text)],
12190 Arc::new(AtomicUsize::new(0)),
12191 )
12192 .await;
12193 cx.condition(|editor, _| editor.context_menu_visible())
12194 .await;
12195 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12196 editor
12197 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12198 .unwrap()
12199 });
12200 cx.assert_editor_state(expected);
12201 handle_resolve_completion_request(&mut cx, None).await;
12202 apply_additional_edits.await.unwrap();
12203
12204 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
12205 let completion_text = "foo_and_bar";
12206 let initial_state = indoc! {"
12207 1. ooanbˇ
12208 2. zooanbˇ
12209 3. ooanbˇz
12210 4. zooanbˇz
12211 5. ooanˇ
12212 6. oanbˇ
12213
12214 ooanbˇ
12215 "};
12216 let completion_marked_buffer = indoc! {"
12217 1. ooanb
12218 2. zooanb
12219 3. ooanbz
12220 4. zooanbz
12221 5. ooan
12222 6. oanb
12223
12224 <ooanb|>
12225 "};
12226 let expected = indoc! {"
12227 1. foo_and_barˇ
12228 2. zfoo_and_barˇ
12229 3. foo_and_barˇz
12230 4. zfoo_and_barˇz
12231 5. ooanfoo_and_barˇ
12232 6. oanbfoo_and_barˇ
12233
12234 foo_and_barˇ
12235 "};
12236 cx.set_state(initial_state);
12237 cx.update_editor(|editor, window, cx| {
12238 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12239 });
12240 handle_completion_request_with_insert_and_replace(
12241 &mut cx,
12242 completion_marked_buffer,
12243 vec![(completion_text, completion_text)],
12244 Arc::new(AtomicUsize::new(0)),
12245 )
12246 .await;
12247 cx.condition(|editor, _| editor.context_menu_visible())
12248 .await;
12249 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12250 editor
12251 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12252 .unwrap()
12253 });
12254 cx.assert_editor_state(expected);
12255 handle_resolve_completion_request(&mut cx, None).await;
12256 apply_additional_edits.await.unwrap();
12257
12258 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
12259 // (expects the same as if it was inserted at the end)
12260 let completion_text = "foo_and_bar";
12261 let initial_state = indoc! {"
12262 1. ooˇanb
12263 2. zooˇanb
12264 3. ooˇanbz
12265 4. zooˇanbz
12266
12267 ooˇanb
12268 "};
12269 let completion_marked_buffer = indoc! {"
12270 1. ooanb
12271 2. zooanb
12272 3. ooanbz
12273 4. zooanbz
12274
12275 <oo|anb>
12276 "};
12277 let expected = indoc! {"
12278 1. foo_and_barˇ
12279 2. zfoo_and_barˇ
12280 3. foo_and_barˇz
12281 4. zfoo_and_barˇz
12282
12283 foo_and_barˇ
12284 "};
12285 cx.set_state(initial_state);
12286 cx.update_editor(|editor, window, cx| {
12287 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12288 });
12289 handle_completion_request_with_insert_and_replace(
12290 &mut cx,
12291 completion_marked_buffer,
12292 vec![(completion_text, completion_text)],
12293 Arc::new(AtomicUsize::new(0)),
12294 )
12295 .await;
12296 cx.condition(|editor, _| editor.context_menu_visible())
12297 .await;
12298 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12299 editor
12300 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12301 .unwrap()
12302 });
12303 cx.assert_editor_state(expected);
12304 handle_resolve_completion_request(&mut cx, None).await;
12305 apply_additional_edits.await.unwrap();
12306}
12307
12308// This used to crash
12309#[gpui::test]
12310async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
12311 init_test(cx, |_| {});
12312
12313 let buffer_text = indoc! {"
12314 fn main() {
12315 10.satu;
12316
12317 //
12318 // separate cursors so they open in different excerpts (manually reproducible)
12319 //
12320
12321 10.satu20;
12322 }
12323 "};
12324 let multibuffer_text_with_selections = indoc! {"
12325 fn main() {
12326 10.satuˇ;
12327
12328 //
12329
12330 //
12331
12332 10.satuˇ20;
12333 }
12334 "};
12335 let expected_multibuffer = indoc! {"
12336 fn main() {
12337 10.saturating_sub()ˇ;
12338
12339 //
12340
12341 //
12342
12343 10.saturating_sub()ˇ;
12344 }
12345 "};
12346
12347 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
12348 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
12349
12350 let fs = FakeFs::new(cx.executor());
12351 fs.insert_tree(
12352 path!("/a"),
12353 json!({
12354 "main.rs": buffer_text,
12355 }),
12356 )
12357 .await;
12358
12359 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12360 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12361 language_registry.add(rust_lang());
12362 let mut fake_servers = language_registry.register_fake_lsp(
12363 "Rust",
12364 FakeLspAdapter {
12365 capabilities: lsp::ServerCapabilities {
12366 completion_provider: Some(lsp::CompletionOptions {
12367 resolve_provider: None,
12368 ..lsp::CompletionOptions::default()
12369 }),
12370 ..lsp::ServerCapabilities::default()
12371 },
12372 ..FakeLspAdapter::default()
12373 },
12374 );
12375 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12376 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12377 let buffer = project
12378 .update(cx, |project, cx| {
12379 project.open_local_buffer(path!("/a/main.rs"), cx)
12380 })
12381 .await
12382 .unwrap();
12383
12384 let multi_buffer = cx.new(|cx| {
12385 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
12386 multi_buffer.push_excerpts(
12387 buffer.clone(),
12388 [ExcerptRange::new(0..first_excerpt_end)],
12389 cx,
12390 );
12391 multi_buffer.push_excerpts(
12392 buffer.clone(),
12393 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
12394 cx,
12395 );
12396 multi_buffer
12397 });
12398
12399 let editor = workspace
12400 .update(cx, |_, window, cx| {
12401 cx.new(|cx| {
12402 Editor::new(
12403 EditorMode::Full {
12404 scale_ui_elements_with_buffer_font_size: false,
12405 show_active_line_background: false,
12406 sized_by_content: false,
12407 },
12408 multi_buffer.clone(),
12409 Some(project.clone()),
12410 window,
12411 cx,
12412 )
12413 })
12414 })
12415 .unwrap();
12416
12417 let pane = workspace
12418 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12419 .unwrap();
12420 pane.update_in(cx, |pane, window, cx| {
12421 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
12422 });
12423
12424 let fake_server = fake_servers.next().await.unwrap();
12425
12426 editor.update_in(cx, |editor, window, cx| {
12427 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12428 s.select_ranges([
12429 Point::new(1, 11)..Point::new(1, 11),
12430 Point::new(7, 11)..Point::new(7, 11),
12431 ])
12432 });
12433
12434 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
12435 });
12436
12437 editor.update_in(cx, |editor, window, cx| {
12438 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12439 });
12440
12441 fake_server
12442 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12443 let completion_item = lsp::CompletionItem {
12444 label: "saturating_sub()".into(),
12445 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
12446 lsp::InsertReplaceEdit {
12447 new_text: "saturating_sub()".to_owned(),
12448 insert: lsp::Range::new(
12449 lsp::Position::new(7, 7),
12450 lsp::Position::new(7, 11),
12451 ),
12452 replace: lsp::Range::new(
12453 lsp::Position::new(7, 7),
12454 lsp::Position::new(7, 13),
12455 ),
12456 },
12457 )),
12458 ..lsp::CompletionItem::default()
12459 };
12460
12461 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
12462 })
12463 .next()
12464 .await
12465 .unwrap();
12466
12467 cx.condition(&editor, |editor, _| editor.context_menu_visible())
12468 .await;
12469
12470 editor
12471 .update_in(cx, |editor, window, cx| {
12472 editor
12473 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12474 .unwrap()
12475 })
12476 .await
12477 .unwrap();
12478
12479 editor.update(cx, |editor, cx| {
12480 assert_text_with_selections(editor, expected_multibuffer, cx);
12481 })
12482}
12483
12484#[gpui::test]
12485async fn test_completion(cx: &mut TestAppContext) {
12486 init_test(cx, |_| {});
12487
12488 let mut cx = EditorLspTestContext::new_rust(
12489 lsp::ServerCapabilities {
12490 completion_provider: Some(lsp::CompletionOptions {
12491 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12492 resolve_provider: Some(true),
12493 ..Default::default()
12494 }),
12495 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12496 ..Default::default()
12497 },
12498 cx,
12499 )
12500 .await;
12501 let counter = Arc::new(AtomicUsize::new(0));
12502
12503 cx.set_state(indoc! {"
12504 oneˇ
12505 two
12506 three
12507 "});
12508 cx.simulate_keystroke(".");
12509 handle_completion_request(
12510 indoc! {"
12511 one.|<>
12512 two
12513 three
12514 "},
12515 vec!["first_completion", "second_completion"],
12516 true,
12517 counter.clone(),
12518 &mut cx,
12519 )
12520 .await;
12521 cx.condition(|editor, _| editor.context_menu_visible())
12522 .await;
12523 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12524
12525 let _handler = handle_signature_help_request(
12526 &mut cx,
12527 lsp::SignatureHelp {
12528 signatures: vec![lsp::SignatureInformation {
12529 label: "test signature".to_string(),
12530 documentation: None,
12531 parameters: Some(vec![lsp::ParameterInformation {
12532 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
12533 documentation: None,
12534 }]),
12535 active_parameter: None,
12536 }],
12537 active_signature: None,
12538 active_parameter: None,
12539 },
12540 );
12541 cx.update_editor(|editor, window, cx| {
12542 assert!(
12543 !editor.signature_help_state.is_shown(),
12544 "No signature help was called for"
12545 );
12546 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12547 });
12548 cx.run_until_parked();
12549 cx.update_editor(|editor, _, _| {
12550 assert!(
12551 !editor.signature_help_state.is_shown(),
12552 "No signature help should be shown when completions menu is open"
12553 );
12554 });
12555
12556 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12557 editor.context_menu_next(&Default::default(), window, cx);
12558 editor
12559 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12560 .unwrap()
12561 });
12562 cx.assert_editor_state(indoc! {"
12563 one.second_completionˇ
12564 two
12565 three
12566 "});
12567
12568 handle_resolve_completion_request(
12569 &mut cx,
12570 Some(vec![
12571 (
12572 //This overlaps with the primary completion edit which is
12573 //misbehavior from the LSP spec, test that we filter it out
12574 indoc! {"
12575 one.second_ˇcompletion
12576 two
12577 threeˇ
12578 "},
12579 "overlapping additional edit",
12580 ),
12581 (
12582 indoc! {"
12583 one.second_completion
12584 two
12585 threeˇ
12586 "},
12587 "\nadditional edit",
12588 ),
12589 ]),
12590 )
12591 .await;
12592 apply_additional_edits.await.unwrap();
12593 cx.assert_editor_state(indoc! {"
12594 one.second_completionˇ
12595 two
12596 three
12597 additional edit
12598 "});
12599
12600 cx.set_state(indoc! {"
12601 one.second_completion
12602 twoˇ
12603 threeˇ
12604 additional edit
12605 "});
12606 cx.simulate_keystroke(" ");
12607 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12608 cx.simulate_keystroke("s");
12609 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12610
12611 cx.assert_editor_state(indoc! {"
12612 one.second_completion
12613 two sˇ
12614 three sˇ
12615 additional edit
12616 "});
12617 handle_completion_request(
12618 indoc! {"
12619 one.second_completion
12620 two s
12621 three <s|>
12622 additional edit
12623 "},
12624 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12625 true,
12626 counter.clone(),
12627 &mut cx,
12628 )
12629 .await;
12630 cx.condition(|editor, _| editor.context_menu_visible())
12631 .await;
12632 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12633
12634 cx.simulate_keystroke("i");
12635
12636 handle_completion_request(
12637 indoc! {"
12638 one.second_completion
12639 two si
12640 three <si|>
12641 additional edit
12642 "},
12643 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12644 true,
12645 counter.clone(),
12646 &mut cx,
12647 )
12648 .await;
12649 cx.condition(|editor, _| editor.context_menu_visible())
12650 .await;
12651 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12652
12653 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12654 editor
12655 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12656 .unwrap()
12657 });
12658 cx.assert_editor_state(indoc! {"
12659 one.second_completion
12660 two sixth_completionˇ
12661 three sixth_completionˇ
12662 additional edit
12663 "});
12664
12665 apply_additional_edits.await.unwrap();
12666
12667 update_test_language_settings(&mut cx, |settings| {
12668 settings.defaults.show_completions_on_input = Some(false);
12669 });
12670 cx.set_state("editorˇ");
12671 cx.simulate_keystroke(".");
12672 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12673 cx.simulate_keystrokes("c l o");
12674 cx.assert_editor_state("editor.cloˇ");
12675 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12676 cx.update_editor(|editor, window, cx| {
12677 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12678 });
12679 handle_completion_request(
12680 "editor.<clo|>",
12681 vec!["close", "clobber"],
12682 true,
12683 counter.clone(),
12684 &mut cx,
12685 )
12686 .await;
12687 cx.condition(|editor, _| editor.context_menu_visible())
12688 .await;
12689 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
12690
12691 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12692 editor
12693 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12694 .unwrap()
12695 });
12696 cx.assert_editor_state("editor.clobberˇ");
12697 handle_resolve_completion_request(&mut cx, None).await;
12698 apply_additional_edits.await.unwrap();
12699}
12700
12701#[gpui::test]
12702async fn test_completion_reuse(cx: &mut TestAppContext) {
12703 init_test(cx, |_| {});
12704
12705 let mut cx = EditorLspTestContext::new_rust(
12706 lsp::ServerCapabilities {
12707 completion_provider: Some(lsp::CompletionOptions {
12708 trigger_characters: Some(vec![".".to_string()]),
12709 ..Default::default()
12710 }),
12711 ..Default::default()
12712 },
12713 cx,
12714 )
12715 .await;
12716
12717 let counter = Arc::new(AtomicUsize::new(0));
12718 cx.set_state("objˇ");
12719 cx.simulate_keystroke(".");
12720
12721 // Initial completion request returns complete results
12722 let is_incomplete = false;
12723 handle_completion_request(
12724 "obj.|<>",
12725 vec!["a", "ab", "abc"],
12726 is_incomplete,
12727 counter.clone(),
12728 &mut cx,
12729 )
12730 .await;
12731 cx.run_until_parked();
12732 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12733 cx.assert_editor_state("obj.ˇ");
12734 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12735
12736 // Type "a" - filters existing completions
12737 cx.simulate_keystroke("a");
12738 cx.run_until_parked();
12739 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12740 cx.assert_editor_state("obj.aˇ");
12741 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12742
12743 // Type "b" - filters existing completions
12744 cx.simulate_keystroke("b");
12745 cx.run_until_parked();
12746 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12747 cx.assert_editor_state("obj.abˇ");
12748 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12749
12750 // Type "c" - filters existing completions
12751 cx.simulate_keystroke("c");
12752 cx.run_until_parked();
12753 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12754 cx.assert_editor_state("obj.abcˇ");
12755 check_displayed_completions(vec!["abc"], &mut cx);
12756
12757 // Backspace to delete "c" - filters existing completions
12758 cx.update_editor(|editor, window, cx| {
12759 editor.backspace(&Backspace, window, cx);
12760 });
12761 cx.run_until_parked();
12762 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12763 cx.assert_editor_state("obj.abˇ");
12764 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12765
12766 // Moving cursor to the left dismisses menu.
12767 cx.update_editor(|editor, window, cx| {
12768 editor.move_left(&MoveLeft, window, cx);
12769 });
12770 cx.run_until_parked();
12771 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12772 cx.assert_editor_state("obj.aˇb");
12773 cx.update_editor(|editor, _, _| {
12774 assert_eq!(editor.context_menu_visible(), false);
12775 });
12776
12777 // Type "b" - new request
12778 cx.simulate_keystroke("b");
12779 let is_incomplete = false;
12780 handle_completion_request(
12781 "obj.<ab|>a",
12782 vec!["ab", "abc"],
12783 is_incomplete,
12784 counter.clone(),
12785 &mut cx,
12786 )
12787 .await;
12788 cx.run_until_parked();
12789 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12790 cx.assert_editor_state("obj.abˇb");
12791 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12792
12793 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
12794 cx.update_editor(|editor, window, cx| {
12795 editor.backspace(&Backspace, window, cx);
12796 });
12797 let is_incomplete = false;
12798 handle_completion_request(
12799 "obj.<a|>b",
12800 vec!["a", "ab", "abc"],
12801 is_incomplete,
12802 counter.clone(),
12803 &mut cx,
12804 )
12805 .await;
12806 cx.run_until_parked();
12807 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12808 cx.assert_editor_state("obj.aˇb");
12809 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12810
12811 // Backspace to delete "a" - dismisses menu.
12812 cx.update_editor(|editor, window, cx| {
12813 editor.backspace(&Backspace, window, cx);
12814 });
12815 cx.run_until_parked();
12816 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12817 cx.assert_editor_state("obj.ˇb");
12818 cx.update_editor(|editor, _, _| {
12819 assert_eq!(editor.context_menu_visible(), false);
12820 });
12821}
12822
12823#[gpui::test]
12824async fn test_word_completion(cx: &mut TestAppContext) {
12825 let lsp_fetch_timeout_ms = 10;
12826 init_test(cx, |language_settings| {
12827 language_settings.defaults.completions = Some(CompletionSettings {
12828 words: WordsCompletionMode::Fallback,
12829 lsp: true,
12830 lsp_fetch_timeout_ms: 10,
12831 lsp_insert_mode: LspInsertMode::Insert,
12832 });
12833 });
12834
12835 let mut cx = EditorLspTestContext::new_rust(
12836 lsp::ServerCapabilities {
12837 completion_provider: Some(lsp::CompletionOptions {
12838 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12839 ..lsp::CompletionOptions::default()
12840 }),
12841 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12842 ..lsp::ServerCapabilities::default()
12843 },
12844 cx,
12845 )
12846 .await;
12847
12848 let throttle_completions = Arc::new(AtomicBool::new(false));
12849
12850 let lsp_throttle_completions = throttle_completions.clone();
12851 let _completion_requests_handler =
12852 cx.lsp
12853 .server
12854 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
12855 let lsp_throttle_completions = lsp_throttle_completions.clone();
12856 let cx = cx.clone();
12857 async move {
12858 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
12859 cx.background_executor()
12860 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
12861 .await;
12862 }
12863 Ok(Some(lsp::CompletionResponse::Array(vec![
12864 lsp::CompletionItem {
12865 label: "first".into(),
12866 ..lsp::CompletionItem::default()
12867 },
12868 lsp::CompletionItem {
12869 label: "last".into(),
12870 ..lsp::CompletionItem::default()
12871 },
12872 ])))
12873 }
12874 });
12875
12876 cx.set_state(indoc! {"
12877 oneˇ
12878 two
12879 three
12880 "});
12881 cx.simulate_keystroke(".");
12882 cx.executor().run_until_parked();
12883 cx.condition(|editor, _| editor.context_menu_visible())
12884 .await;
12885 cx.update_editor(|editor, window, cx| {
12886 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12887 {
12888 assert_eq!(
12889 completion_menu_entries(&menu),
12890 &["first", "last"],
12891 "When LSP server is fast to reply, no fallback word completions are used"
12892 );
12893 } else {
12894 panic!("expected completion menu to be open");
12895 }
12896 editor.cancel(&Cancel, window, cx);
12897 });
12898 cx.executor().run_until_parked();
12899 cx.condition(|editor, _| !editor.context_menu_visible())
12900 .await;
12901
12902 throttle_completions.store(true, atomic::Ordering::Release);
12903 cx.simulate_keystroke(".");
12904 cx.executor()
12905 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
12906 cx.executor().run_until_parked();
12907 cx.condition(|editor, _| editor.context_menu_visible())
12908 .await;
12909 cx.update_editor(|editor, _, _| {
12910 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12911 {
12912 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
12913 "When LSP server is slow, document words can be shown instead, if configured accordingly");
12914 } else {
12915 panic!("expected completion menu to be open");
12916 }
12917 });
12918}
12919
12920#[gpui::test]
12921async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
12922 init_test(cx, |language_settings| {
12923 language_settings.defaults.completions = Some(CompletionSettings {
12924 words: WordsCompletionMode::Enabled,
12925 lsp: true,
12926 lsp_fetch_timeout_ms: 0,
12927 lsp_insert_mode: LspInsertMode::Insert,
12928 });
12929 });
12930
12931 let mut cx = EditorLspTestContext::new_rust(
12932 lsp::ServerCapabilities {
12933 completion_provider: Some(lsp::CompletionOptions {
12934 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12935 ..lsp::CompletionOptions::default()
12936 }),
12937 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12938 ..lsp::ServerCapabilities::default()
12939 },
12940 cx,
12941 )
12942 .await;
12943
12944 let _completion_requests_handler =
12945 cx.lsp
12946 .server
12947 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12948 Ok(Some(lsp::CompletionResponse::Array(vec![
12949 lsp::CompletionItem {
12950 label: "first".into(),
12951 ..lsp::CompletionItem::default()
12952 },
12953 lsp::CompletionItem {
12954 label: "last".into(),
12955 ..lsp::CompletionItem::default()
12956 },
12957 ])))
12958 });
12959
12960 cx.set_state(indoc! {"ˇ
12961 first
12962 last
12963 second
12964 "});
12965 cx.simulate_keystroke(".");
12966 cx.executor().run_until_parked();
12967 cx.condition(|editor, _| editor.context_menu_visible())
12968 .await;
12969 cx.update_editor(|editor, _, _| {
12970 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12971 {
12972 assert_eq!(
12973 completion_menu_entries(&menu),
12974 &["first", "last", "second"],
12975 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
12976 );
12977 } else {
12978 panic!("expected completion menu to be open");
12979 }
12980 });
12981}
12982
12983#[gpui::test]
12984async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
12985 init_test(cx, |language_settings| {
12986 language_settings.defaults.completions = Some(CompletionSettings {
12987 words: WordsCompletionMode::Disabled,
12988 lsp: true,
12989 lsp_fetch_timeout_ms: 0,
12990 lsp_insert_mode: LspInsertMode::Insert,
12991 });
12992 });
12993
12994 let mut cx = EditorLspTestContext::new_rust(
12995 lsp::ServerCapabilities {
12996 completion_provider: Some(lsp::CompletionOptions {
12997 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12998 ..lsp::CompletionOptions::default()
12999 }),
13000 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13001 ..lsp::ServerCapabilities::default()
13002 },
13003 cx,
13004 )
13005 .await;
13006
13007 let _completion_requests_handler =
13008 cx.lsp
13009 .server
13010 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
13011 panic!("LSP completions should not be queried when dealing with word completions")
13012 });
13013
13014 cx.set_state(indoc! {"ˇ
13015 first
13016 last
13017 second
13018 "});
13019 cx.update_editor(|editor, window, cx| {
13020 editor.show_word_completions(&ShowWordCompletions, window, cx);
13021 });
13022 cx.executor().run_until_parked();
13023 cx.condition(|editor, _| editor.context_menu_visible())
13024 .await;
13025 cx.update_editor(|editor, _, _| {
13026 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13027 {
13028 assert_eq!(
13029 completion_menu_entries(&menu),
13030 &["first", "last", "second"],
13031 "`ShowWordCompletions` action should show word completions"
13032 );
13033 } else {
13034 panic!("expected completion menu to be open");
13035 }
13036 });
13037
13038 cx.simulate_keystroke("l");
13039 cx.executor().run_until_parked();
13040 cx.condition(|editor, _| editor.context_menu_visible())
13041 .await;
13042 cx.update_editor(|editor, _, _| {
13043 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13044 {
13045 assert_eq!(
13046 completion_menu_entries(&menu),
13047 &["last"],
13048 "After showing word completions, further editing should filter them and not query the LSP"
13049 );
13050 } else {
13051 panic!("expected completion menu to be open");
13052 }
13053 });
13054}
13055
13056#[gpui::test]
13057async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
13058 init_test(cx, |language_settings| {
13059 language_settings.defaults.completions = Some(CompletionSettings {
13060 words: WordsCompletionMode::Fallback,
13061 lsp: false,
13062 lsp_fetch_timeout_ms: 0,
13063 lsp_insert_mode: LspInsertMode::Insert,
13064 });
13065 });
13066
13067 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13068
13069 cx.set_state(indoc! {"ˇ
13070 0_usize
13071 let
13072 33
13073 4.5f32
13074 "});
13075 cx.update_editor(|editor, window, cx| {
13076 editor.show_completions(&ShowCompletions::default(), window, cx);
13077 });
13078 cx.executor().run_until_parked();
13079 cx.condition(|editor, _| editor.context_menu_visible())
13080 .await;
13081 cx.update_editor(|editor, window, cx| {
13082 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13083 {
13084 assert_eq!(
13085 completion_menu_entries(&menu),
13086 &["let"],
13087 "With no digits in the completion query, no digits should be in the word completions"
13088 );
13089 } else {
13090 panic!("expected completion menu to be open");
13091 }
13092 editor.cancel(&Cancel, window, cx);
13093 });
13094
13095 cx.set_state(indoc! {"3ˇ
13096 0_usize
13097 let
13098 3
13099 33.35f32
13100 "});
13101 cx.update_editor(|editor, window, cx| {
13102 editor.show_completions(&ShowCompletions::default(), window, cx);
13103 });
13104 cx.executor().run_until_parked();
13105 cx.condition(|editor, _| editor.context_menu_visible())
13106 .await;
13107 cx.update_editor(|editor, _, _| {
13108 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13109 {
13110 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
13111 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
13112 } else {
13113 panic!("expected completion menu to be open");
13114 }
13115 });
13116}
13117
13118fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
13119 let position = || lsp::Position {
13120 line: params.text_document_position.position.line,
13121 character: params.text_document_position.position.character,
13122 };
13123 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13124 range: lsp::Range {
13125 start: position(),
13126 end: position(),
13127 },
13128 new_text: text.to_string(),
13129 }))
13130}
13131
13132#[gpui::test]
13133async fn test_multiline_completion(cx: &mut TestAppContext) {
13134 init_test(cx, |_| {});
13135
13136 let fs = FakeFs::new(cx.executor());
13137 fs.insert_tree(
13138 path!("/a"),
13139 json!({
13140 "main.ts": "a",
13141 }),
13142 )
13143 .await;
13144
13145 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13146 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13147 let typescript_language = Arc::new(Language::new(
13148 LanguageConfig {
13149 name: "TypeScript".into(),
13150 matcher: LanguageMatcher {
13151 path_suffixes: vec!["ts".to_string()],
13152 ..LanguageMatcher::default()
13153 },
13154 line_comments: vec!["// ".into()],
13155 ..LanguageConfig::default()
13156 },
13157 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13158 ));
13159 language_registry.add(typescript_language.clone());
13160 let mut fake_servers = language_registry.register_fake_lsp(
13161 "TypeScript",
13162 FakeLspAdapter {
13163 capabilities: lsp::ServerCapabilities {
13164 completion_provider: Some(lsp::CompletionOptions {
13165 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13166 ..lsp::CompletionOptions::default()
13167 }),
13168 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13169 ..lsp::ServerCapabilities::default()
13170 },
13171 // Emulate vtsls label generation
13172 label_for_completion: Some(Box::new(|item, _| {
13173 let text = if let Some(description) = item
13174 .label_details
13175 .as_ref()
13176 .and_then(|label_details| label_details.description.as_ref())
13177 {
13178 format!("{} {}", item.label, description)
13179 } else if let Some(detail) = &item.detail {
13180 format!("{} {}", item.label, detail)
13181 } else {
13182 item.label.clone()
13183 };
13184 let len = text.len();
13185 Some(language::CodeLabel {
13186 text,
13187 runs: Vec::new(),
13188 filter_range: 0..len,
13189 })
13190 })),
13191 ..FakeLspAdapter::default()
13192 },
13193 );
13194 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13195 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13196 let worktree_id = workspace
13197 .update(cx, |workspace, _window, cx| {
13198 workspace.project().update(cx, |project, cx| {
13199 project.worktrees(cx).next().unwrap().read(cx).id()
13200 })
13201 })
13202 .unwrap();
13203 let _buffer = project
13204 .update(cx, |project, cx| {
13205 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
13206 })
13207 .await
13208 .unwrap();
13209 let editor = workspace
13210 .update(cx, |workspace, window, cx| {
13211 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
13212 })
13213 .unwrap()
13214 .await
13215 .unwrap()
13216 .downcast::<Editor>()
13217 .unwrap();
13218 let fake_server = fake_servers.next().await.unwrap();
13219
13220 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
13221 let multiline_label_2 = "a\nb\nc\n";
13222 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
13223 let multiline_description = "d\ne\nf\n";
13224 let multiline_detail_2 = "g\nh\ni\n";
13225
13226 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
13227 move |params, _| async move {
13228 Ok(Some(lsp::CompletionResponse::Array(vec![
13229 lsp::CompletionItem {
13230 label: multiline_label.to_string(),
13231 text_edit: gen_text_edit(¶ms, "new_text_1"),
13232 ..lsp::CompletionItem::default()
13233 },
13234 lsp::CompletionItem {
13235 label: "single line label 1".to_string(),
13236 detail: Some(multiline_detail.to_string()),
13237 text_edit: gen_text_edit(¶ms, "new_text_2"),
13238 ..lsp::CompletionItem::default()
13239 },
13240 lsp::CompletionItem {
13241 label: "single line label 2".to_string(),
13242 label_details: Some(lsp::CompletionItemLabelDetails {
13243 description: Some(multiline_description.to_string()),
13244 detail: None,
13245 }),
13246 text_edit: gen_text_edit(¶ms, "new_text_2"),
13247 ..lsp::CompletionItem::default()
13248 },
13249 lsp::CompletionItem {
13250 label: multiline_label_2.to_string(),
13251 detail: Some(multiline_detail_2.to_string()),
13252 text_edit: gen_text_edit(¶ms, "new_text_3"),
13253 ..lsp::CompletionItem::default()
13254 },
13255 lsp::CompletionItem {
13256 label: "Label with many spaces and \t but without newlines".to_string(),
13257 detail: Some(
13258 "Details with many spaces and \t but without newlines".to_string(),
13259 ),
13260 text_edit: gen_text_edit(¶ms, "new_text_4"),
13261 ..lsp::CompletionItem::default()
13262 },
13263 ])))
13264 },
13265 );
13266
13267 editor.update_in(cx, |editor, window, cx| {
13268 cx.focus_self(window);
13269 editor.move_to_end(&MoveToEnd, window, cx);
13270 editor.handle_input(".", window, cx);
13271 });
13272 cx.run_until_parked();
13273 completion_handle.next().await.unwrap();
13274
13275 editor.update(cx, |editor, _| {
13276 assert!(editor.context_menu_visible());
13277 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13278 {
13279 let completion_labels = menu
13280 .completions
13281 .borrow()
13282 .iter()
13283 .map(|c| c.label.text.clone())
13284 .collect::<Vec<_>>();
13285 assert_eq!(
13286 completion_labels,
13287 &[
13288 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
13289 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
13290 "single line label 2 d e f ",
13291 "a b c g h i ",
13292 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
13293 ],
13294 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
13295 );
13296
13297 for completion in menu
13298 .completions
13299 .borrow()
13300 .iter() {
13301 assert_eq!(
13302 completion.label.filter_range,
13303 0..completion.label.text.len(),
13304 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
13305 );
13306 }
13307 } else {
13308 panic!("expected completion menu to be open");
13309 }
13310 });
13311}
13312
13313#[gpui::test]
13314async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
13315 init_test(cx, |_| {});
13316 let mut cx = EditorLspTestContext::new_rust(
13317 lsp::ServerCapabilities {
13318 completion_provider: Some(lsp::CompletionOptions {
13319 trigger_characters: Some(vec![".".to_string()]),
13320 ..Default::default()
13321 }),
13322 ..Default::default()
13323 },
13324 cx,
13325 )
13326 .await;
13327 cx.lsp
13328 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13329 Ok(Some(lsp::CompletionResponse::Array(vec![
13330 lsp::CompletionItem {
13331 label: "first".into(),
13332 ..Default::default()
13333 },
13334 lsp::CompletionItem {
13335 label: "last".into(),
13336 ..Default::default()
13337 },
13338 ])))
13339 });
13340 cx.set_state("variableˇ");
13341 cx.simulate_keystroke(".");
13342 cx.executor().run_until_parked();
13343
13344 cx.update_editor(|editor, _, _| {
13345 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13346 {
13347 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
13348 } else {
13349 panic!("expected completion menu to be open");
13350 }
13351 });
13352
13353 cx.update_editor(|editor, window, cx| {
13354 editor.move_page_down(&MovePageDown::default(), window, cx);
13355 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13356 {
13357 assert!(
13358 menu.selected_item == 1,
13359 "expected PageDown to select the last item from the context menu"
13360 );
13361 } else {
13362 panic!("expected completion menu to stay open after PageDown");
13363 }
13364 });
13365
13366 cx.update_editor(|editor, window, cx| {
13367 editor.move_page_up(&MovePageUp::default(), window, cx);
13368 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13369 {
13370 assert!(
13371 menu.selected_item == 0,
13372 "expected PageUp to select the first item from the context menu"
13373 );
13374 } else {
13375 panic!("expected completion menu to stay open after PageUp");
13376 }
13377 });
13378}
13379
13380#[gpui::test]
13381async fn test_as_is_completions(cx: &mut TestAppContext) {
13382 init_test(cx, |_| {});
13383 let mut cx = EditorLspTestContext::new_rust(
13384 lsp::ServerCapabilities {
13385 completion_provider: Some(lsp::CompletionOptions {
13386 ..Default::default()
13387 }),
13388 ..Default::default()
13389 },
13390 cx,
13391 )
13392 .await;
13393 cx.lsp
13394 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13395 Ok(Some(lsp::CompletionResponse::Array(vec![
13396 lsp::CompletionItem {
13397 label: "unsafe".into(),
13398 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13399 range: lsp::Range {
13400 start: lsp::Position {
13401 line: 1,
13402 character: 2,
13403 },
13404 end: lsp::Position {
13405 line: 1,
13406 character: 3,
13407 },
13408 },
13409 new_text: "unsafe".to_string(),
13410 })),
13411 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
13412 ..Default::default()
13413 },
13414 ])))
13415 });
13416 cx.set_state("fn a() {}\n nˇ");
13417 cx.executor().run_until_parked();
13418 cx.update_editor(|editor, window, cx| {
13419 editor.show_completions(
13420 &ShowCompletions {
13421 trigger: Some("\n".into()),
13422 },
13423 window,
13424 cx,
13425 );
13426 });
13427 cx.executor().run_until_parked();
13428
13429 cx.update_editor(|editor, window, cx| {
13430 editor.confirm_completion(&Default::default(), window, cx)
13431 });
13432 cx.executor().run_until_parked();
13433 cx.assert_editor_state("fn a() {}\n unsafeˇ");
13434}
13435
13436#[gpui::test]
13437async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
13438 init_test(cx, |_| {});
13439 let language =
13440 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
13441 let mut cx = EditorLspTestContext::new(
13442 language,
13443 lsp::ServerCapabilities {
13444 completion_provider: Some(lsp::CompletionOptions {
13445 ..lsp::CompletionOptions::default()
13446 }),
13447 ..lsp::ServerCapabilities::default()
13448 },
13449 cx,
13450 )
13451 .await;
13452
13453 cx.set_state(
13454 "#ifndef BAR_H
13455#define BAR_H
13456
13457#include <stdbool.h>
13458
13459int fn_branch(bool do_branch1, bool do_branch2);
13460
13461#endif // BAR_H
13462ˇ",
13463 );
13464 cx.executor().run_until_parked();
13465 cx.update_editor(|editor, window, cx| {
13466 editor.handle_input("#", window, cx);
13467 });
13468 cx.executor().run_until_parked();
13469 cx.update_editor(|editor, window, cx| {
13470 editor.handle_input("i", window, cx);
13471 });
13472 cx.executor().run_until_parked();
13473 cx.update_editor(|editor, window, cx| {
13474 editor.handle_input("n", window, cx);
13475 });
13476 cx.executor().run_until_parked();
13477 cx.assert_editor_state(
13478 "#ifndef BAR_H
13479#define BAR_H
13480
13481#include <stdbool.h>
13482
13483int fn_branch(bool do_branch1, bool do_branch2);
13484
13485#endif // BAR_H
13486#inˇ",
13487 );
13488
13489 cx.lsp
13490 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13491 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13492 is_incomplete: false,
13493 item_defaults: None,
13494 items: vec![lsp::CompletionItem {
13495 kind: Some(lsp::CompletionItemKind::SNIPPET),
13496 label_details: Some(lsp::CompletionItemLabelDetails {
13497 detail: Some("header".to_string()),
13498 description: None,
13499 }),
13500 label: " include".to_string(),
13501 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13502 range: lsp::Range {
13503 start: lsp::Position {
13504 line: 8,
13505 character: 1,
13506 },
13507 end: lsp::Position {
13508 line: 8,
13509 character: 1,
13510 },
13511 },
13512 new_text: "include \"$0\"".to_string(),
13513 })),
13514 sort_text: Some("40b67681include".to_string()),
13515 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13516 filter_text: Some("include".to_string()),
13517 insert_text: Some("include \"$0\"".to_string()),
13518 ..lsp::CompletionItem::default()
13519 }],
13520 })))
13521 });
13522 cx.update_editor(|editor, window, cx| {
13523 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13524 });
13525 cx.executor().run_until_parked();
13526 cx.update_editor(|editor, window, cx| {
13527 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
13528 });
13529 cx.executor().run_until_parked();
13530 cx.assert_editor_state(
13531 "#ifndef BAR_H
13532#define BAR_H
13533
13534#include <stdbool.h>
13535
13536int fn_branch(bool do_branch1, bool do_branch2);
13537
13538#endif // BAR_H
13539#include \"ˇ\"",
13540 );
13541
13542 cx.lsp
13543 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13544 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13545 is_incomplete: true,
13546 item_defaults: None,
13547 items: vec![lsp::CompletionItem {
13548 kind: Some(lsp::CompletionItemKind::FILE),
13549 label: "AGL/".to_string(),
13550 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13551 range: lsp::Range {
13552 start: lsp::Position {
13553 line: 8,
13554 character: 10,
13555 },
13556 end: lsp::Position {
13557 line: 8,
13558 character: 11,
13559 },
13560 },
13561 new_text: "AGL/".to_string(),
13562 })),
13563 sort_text: Some("40b67681AGL/".to_string()),
13564 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
13565 filter_text: Some("AGL/".to_string()),
13566 insert_text: Some("AGL/".to_string()),
13567 ..lsp::CompletionItem::default()
13568 }],
13569 })))
13570 });
13571 cx.update_editor(|editor, window, cx| {
13572 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13573 });
13574 cx.executor().run_until_parked();
13575 cx.update_editor(|editor, window, cx| {
13576 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
13577 });
13578 cx.executor().run_until_parked();
13579 cx.assert_editor_state(
13580 r##"#ifndef BAR_H
13581#define BAR_H
13582
13583#include <stdbool.h>
13584
13585int fn_branch(bool do_branch1, bool do_branch2);
13586
13587#endif // BAR_H
13588#include "AGL/ˇ"##,
13589 );
13590
13591 cx.update_editor(|editor, window, cx| {
13592 editor.handle_input("\"", window, cx);
13593 });
13594 cx.executor().run_until_parked();
13595 cx.assert_editor_state(
13596 r##"#ifndef BAR_H
13597#define BAR_H
13598
13599#include <stdbool.h>
13600
13601int fn_branch(bool do_branch1, bool do_branch2);
13602
13603#endif // BAR_H
13604#include "AGL/"ˇ"##,
13605 );
13606}
13607
13608#[gpui::test]
13609async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
13610 init_test(cx, |_| {});
13611
13612 let mut cx = EditorLspTestContext::new_rust(
13613 lsp::ServerCapabilities {
13614 completion_provider: Some(lsp::CompletionOptions {
13615 trigger_characters: Some(vec![".".to_string()]),
13616 resolve_provider: Some(true),
13617 ..Default::default()
13618 }),
13619 ..Default::default()
13620 },
13621 cx,
13622 )
13623 .await;
13624
13625 cx.set_state("fn main() { let a = 2ˇ; }");
13626 cx.simulate_keystroke(".");
13627 let completion_item = lsp::CompletionItem {
13628 label: "Some".into(),
13629 kind: Some(lsp::CompletionItemKind::SNIPPET),
13630 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13631 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13632 kind: lsp::MarkupKind::Markdown,
13633 value: "```rust\nSome(2)\n```".to_string(),
13634 })),
13635 deprecated: Some(false),
13636 sort_text: Some("Some".to_string()),
13637 filter_text: Some("Some".to_string()),
13638 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13639 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13640 range: lsp::Range {
13641 start: lsp::Position {
13642 line: 0,
13643 character: 22,
13644 },
13645 end: lsp::Position {
13646 line: 0,
13647 character: 22,
13648 },
13649 },
13650 new_text: "Some(2)".to_string(),
13651 })),
13652 additional_text_edits: Some(vec![lsp::TextEdit {
13653 range: lsp::Range {
13654 start: lsp::Position {
13655 line: 0,
13656 character: 20,
13657 },
13658 end: lsp::Position {
13659 line: 0,
13660 character: 22,
13661 },
13662 },
13663 new_text: "".to_string(),
13664 }]),
13665 ..Default::default()
13666 };
13667
13668 let closure_completion_item = completion_item.clone();
13669 let counter = Arc::new(AtomicUsize::new(0));
13670 let counter_clone = counter.clone();
13671 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13672 let task_completion_item = closure_completion_item.clone();
13673 counter_clone.fetch_add(1, atomic::Ordering::Release);
13674 async move {
13675 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13676 is_incomplete: true,
13677 item_defaults: None,
13678 items: vec![task_completion_item],
13679 })))
13680 }
13681 });
13682
13683 cx.condition(|editor, _| editor.context_menu_visible())
13684 .await;
13685 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
13686 assert!(request.next().await.is_some());
13687 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13688
13689 cx.simulate_keystrokes("S o m");
13690 cx.condition(|editor, _| editor.context_menu_visible())
13691 .await;
13692 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
13693 assert!(request.next().await.is_some());
13694 assert!(request.next().await.is_some());
13695 assert!(request.next().await.is_some());
13696 request.close();
13697 assert!(request.next().await.is_none());
13698 assert_eq!(
13699 counter.load(atomic::Ordering::Acquire),
13700 4,
13701 "With the completions menu open, only one LSP request should happen per input"
13702 );
13703}
13704
13705#[gpui::test]
13706async fn test_toggle_comment(cx: &mut TestAppContext) {
13707 init_test(cx, |_| {});
13708 let mut cx = EditorTestContext::new(cx).await;
13709 let language = Arc::new(Language::new(
13710 LanguageConfig {
13711 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13712 ..Default::default()
13713 },
13714 Some(tree_sitter_rust::LANGUAGE.into()),
13715 ));
13716 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13717
13718 // If multiple selections intersect a line, the line is only toggled once.
13719 cx.set_state(indoc! {"
13720 fn a() {
13721 «//b();
13722 ˇ»// «c();
13723 //ˇ» d();
13724 }
13725 "});
13726
13727 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13728
13729 cx.assert_editor_state(indoc! {"
13730 fn a() {
13731 «b();
13732 c();
13733 ˇ» d();
13734 }
13735 "});
13736
13737 // The comment prefix is inserted at the same column for every line in a
13738 // selection.
13739 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13740
13741 cx.assert_editor_state(indoc! {"
13742 fn a() {
13743 // «b();
13744 // c();
13745 ˇ»// d();
13746 }
13747 "});
13748
13749 // If a selection ends at the beginning of a line, that line is not toggled.
13750 cx.set_selections_state(indoc! {"
13751 fn a() {
13752 // b();
13753 «// c();
13754 ˇ» // d();
13755 }
13756 "});
13757
13758 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13759
13760 cx.assert_editor_state(indoc! {"
13761 fn a() {
13762 // b();
13763 «c();
13764 ˇ» // d();
13765 }
13766 "});
13767
13768 // If a selection span a single line and is empty, the line is toggled.
13769 cx.set_state(indoc! {"
13770 fn a() {
13771 a();
13772 b();
13773 ˇ
13774 }
13775 "});
13776
13777 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13778
13779 cx.assert_editor_state(indoc! {"
13780 fn a() {
13781 a();
13782 b();
13783 //•ˇ
13784 }
13785 "});
13786
13787 // If a selection span multiple lines, empty lines are not toggled.
13788 cx.set_state(indoc! {"
13789 fn a() {
13790 «a();
13791
13792 c();ˇ»
13793 }
13794 "});
13795
13796 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13797
13798 cx.assert_editor_state(indoc! {"
13799 fn a() {
13800 // «a();
13801
13802 // c();ˇ»
13803 }
13804 "});
13805
13806 // If a selection includes multiple comment prefixes, all lines are uncommented.
13807 cx.set_state(indoc! {"
13808 fn a() {
13809 «// a();
13810 /// b();
13811 //! c();ˇ»
13812 }
13813 "});
13814
13815 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13816
13817 cx.assert_editor_state(indoc! {"
13818 fn a() {
13819 «a();
13820 b();
13821 c();ˇ»
13822 }
13823 "});
13824}
13825
13826#[gpui::test]
13827async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
13828 init_test(cx, |_| {});
13829 let mut cx = EditorTestContext::new(cx).await;
13830 let language = Arc::new(Language::new(
13831 LanguageConfig {
13832 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13833 ..Default::default()
13834 },
13835 Some(tree_sitter_rust::LANGUAGE.into()),
13836 ));
13837 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13838
13839 let toggle_comments = &ToggleComments {
13840 advance_downwards: false,
13841 ignore_indent: true,
13842 };
13843
13844 // If multiple selections intersect a line, the line is only toggled once.
13845 cx.set_state(indoc! {"
13846 fn a() {
13847 // «b();
13848 // c();
13849 // ˇ» d();
13850 }
13851 "});
13852
13853 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13854
13855 cx.assert_editor_state(indoc! {"
13856 fn a() {
13857 «b();
13858 c();
13859 ˇ» d();
13860 }
13861 "});
13862
13863 // The comment prefix is inserted at the beginning of each line
13864 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13865
13866 cx.assert_editor_state(indoc! {"
13867 fn a() {
13868 // «b();
13869 // c();
13870 // ˇ» d();
13871 }
13872 "});
13873
13874 // If a selection ends at the beginning of a line, that line is not toggled.
13875 cx.set_selections_state(indoc! {"
13876 fn a() {
13877 // b();
13878 // «c();
13879 ˇ»// d();
13880 }
13881 "});
13882
13883 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13884
13885 cx.assert_editor_state(indoc! {"
13886 fn a() {
13887 // b();
13888 «c();
13889 ˇ»// d();
13890 }
13891 "});
13892
13893 // If a selection span a single line and is empty, the line is toggled.
13894 cx.set_state(indoc! {"
13895 fn a() {
13896 a();
13897 b();
13898 ˇ
13899 }
13900 "});
13901
13902 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13903
13904 cx.assert_editor_state(indoc! {"
13905 fn a() {
13906 a();
13907 b();
13908 //ˇ
13909 }
13910 "});
13911
13912 // If a selection span multiple lines, empty lines are not toggled.
13913 cx.set_state(indoc! {"
13914 fn a() {
13915 «a();
13916
13917 c();ˇ»
13918 }
13919 "});
13920
13921 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13922
13923 cx.assert_editor_state(indoc! {"
13924 fn a() {
13925 // «a();
13926
13927 // c();ˇ»
13928 }
13929 "});
13930
13931 // If a selection includes multiple comment prefixes, all lines are uncommented.
13932 cx.set_state(indoc! {"
13933 fn a() {
13934 // «a();
13935 /// b();
13936 //! c();ˇ»
13937 }
13938 "});
13939
13940 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13941
13942 cx.assert_editor_state(indoc! {"
13943 fn a() {
13944 «a();
13945 b();
13946 c();ˇ»
13947 }
13948 "});
13949}
13950
13951#[gpui::test]
13952async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
13953 init_test(cx, |_| {});
13954
13955 let language = Arc::new(Language::new(
13956 LanguageConfig {
13957 line_comments: vec!["// ".into()],
13958 ..Default::default()
13959 },
13960 Some(tree_sitter_rust::LANGUAGE.into()),
13961 ));
13962
13963 let mut cx = EditorTestContext::new(cx).await;
13964
13965 cx.language_registry().add(language.clone());
13966 cx.update_buffer(|buffer, cx| {
13967 buffer.set_language(Some(language), cx);
13968 });
13969
13970 let toggle_comments = &ToggleComments {
13971 advance_downwards: true,
13972 ignore_indent: false,
13973 };
13974
13975 // Single cursor on one line -> advance
13976 // Cursor moves horizontally 3 characters as well on non-blank line
13977 cx.set_state(indoc!(
13978 "fn a() {
13979 ˇdog();
13980 cat();
13981 }"
13982 ));
13983 cx.update_editor(|editor, window, cx| {
13984 editor.toggle_comments(toggle_comments, window, cx);
13985 });
13986 cx.assert_editor_state(indoc!(
13987 "fn a() {
13988 // dog();
13989 catˇ();
13990 }"
13991 ));
13992
13993 // Single selection on one line -> don't advance
13994 cx.set_state(indoc!(
13995 "fn a() {
13996 «dog()ˇ»;
13997 cat();
13998 }"
13999 ));
14000 cx.update_editor(|editor, window, cx| {
14001 editor.toggle_comments(toggle_comments, window, cx);
14002 });
14003 cx.assert_editor_state(indoc!(
14004 "fn a() {
14005 // «dog()ˇ»;
14006 cat();
14007 }"
14008 ));
14009
14010 // Multiple cursors on one line -> advance
14011 cx.set_state(indoc!(
14012 "fn a() {
14013 ˇdˇog();
14014 cat();
14015 }"
14016 ));
14017 cx.update_editor(|editor, window, cx| {
14018 editor.toggle_comments(toggle_comments, window, cx);
14019 });
14020 cx.assert_editor_state(indoc!(
14021 "fn a() {
14022 // dog();
14023 catˇ(ˇ);
14024 }"
14025 ));
14026
14027 // Multiple cursors on one line, with selection -> don't advance
14028 cx.set_state(indoc!(
14029 "fn a() {
14030 ˇdˇog«()ˇ»;
14031 cat();
14032 }"
14033 ));
14034 cx.update_editor(|editor, window, cx| {
14035 editor.toggle_comments(toggle_comments, window, cx);
14036 });
14037 cx.assert_editor_state(indoc!(
14038 "fn a() {
14039 // ˇdˇog«()ˇ»;
14040 cat();
14041 }"
14042 ));
14043
14044 // Single cursor on one line -> advance
14045 // Cursor moves to column 0 on blank line
14046 cx.set_state(indoc!(
14047 "fn a() {
14048 ˇdog();
14049
14050 cat();
14051 }"
14052 ));
14053 cx.update_editor(|editor, window, cx| {
14054 editor.toggle_comments(toggle_comments, window, cx);
14055 });
14056 cx.assert_editor_state(indoc!(
14057 "fn a() {
14058 // dog();
14059 ˇ
14060 cat();
14061 }"
14062 ));
14063
14064 // Single cursor on one line -> advance
14065 // Cursor starts and ends at column 0
14066 cx.set_state(indoc!(
14067 "fn a() {
14068 ˇ dog();
14069 cat();
14070 }"
14071 ));
14072 cx.update_editor(|editor, window, cx| {
14073 editor.toggle_comments(toggle_comments, window, cx);
14074 });
14075 cx.assert_editor_state(indoc!(
14076 "fn a() {
14077 // dog();
14078 ˇ cat();
14079 }"
14080 ));
14081}
14082
14083#[gpui::test]
14084async fn test_toggle_block_comment(cx: &mut TestAppContext) {
14085 init_test(cx, |_| {});
14086
14087 let mut cx = EditorTestContext::new(cx).await;
14088
14089 let html_language = Arc::new(
14090 Language::new(
14091 LanguageConfig {
14092 name: "HTML".into(),
14093 block_comment: Some(BlockCommentConfig {
14094 start: "<!-- ".into(),
14095 prefix: "".into(),
14096 end: " -->".into(),
14097 tab_size: 0,
14098 }),
14099 ..Default::default()
14100 },
14101 Some(tree_sitter_html::LANGUAGE.into()),
14102 )
14103 .with_injection_query(
14104 r#"
14105 (script_element
14106 (raw_text) @injection.content
14107 (#set! injection.language "javascript"))
14108 "#,
14109 )
14110 .unwrap(),
14111 );
14112
14113 let javascript_language = Arc::new(Language::new(
14114 LanguageConfig {
14115 name: "JavaScript".into(),
14116 line_comments: vec!["// ".into()],
14117 ..Default::default()
14118 },
14119 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
14120 ));
14121
14122 cx.language_registry().add(html_language.clone());
14123 cx.language_registry().add(javascript_language.clone());
14124 cx.update_buffer(|buffer, cx| {
14125 buffer.set_language(Some(html_language), cx);
14126 });
14127
14128 // Toggle comments for empty selections
14129 cx.set_state(
14130 &r#"
14131 <p>A</p>ˇ
14132 <p>B</p>ˇ
14133 <p>C</p>ˇ
14134 "#
14135 .unindent(),
14136 );
14137 cx.update_editor(|editor, window, cx| {
14138 editor.toggle_comments(&ToggleComments::default(), window, cx)
14139 });
14140 cx.assert_editor_state(
14141 &r#"
14142 <!-- <p>A</p>ˇ -->
14143 <!-- <p>B</p>ˇ -->
14144 <!-- <p>C</p>ˇ -->
14145 "#
14146 .unindent(),
14147 );
14148 cx.update_editor(|editor, window, cx| {
14149 editor.toggle_comments(&ToggleComments::default(), window, cx)
14150 });
14151 cx.assert_editor_state(
14152 &r#"
14153 <p>A</p>ˇ
14154 <p>B</p>ˇ
14155 <p>C</p>ˇ
14156 "#
14157 .unindent(),
14158 );
14159
14160 // Toggle comments for mixture of empty and non-empty selections, where
14161 // multiple selections occupy a given line.
14162 cx.set_state(
14163 &r#"
14164 <p>A«</p>
14165 <p>ˇ»B</p>ˇ
14166 <p>C«</p>
14167 <p>ˇ»D</p>ˇ
14168 "#
14169 .unindent(),
14170 );
14171
14172 cx.update_editor(|editor, window, cx| {
14173 editor.toggle_comments(&ToggleComments::default(), window, cx)
14174 });
14175 cx.assert_editor_state(
14176 &r#"
14177 <!-- <p>A«</p>
14178 <p>ˇ»B</p>ˇ -->
14179 <!-- <p>C«</p>
14180 <p>ˇ»D</p>ˇ -->
14181 "#
14182 .unindent(),
14183 );
14184 cx.update_editor(|editor, window, cx| {
14185 editor.toggle_comments(&ToggleComments::default(), window, cx)
14186 });
14187 cx.assert_editor_state(
14188 &r#"
14189 <p>A«</p>
14190 <p>ˇ»B</p>ˇ
14191 <p>C«</p>
14192 <p>ˇ»D</p>ˇ
14193 "#
14194 .unindent(),
14195 );
14196
14197 // Toggle comments when different languages are active for different
14198 // selections.
14199 cx.set_state(
14200 &r#"
14201 ˇ<script>
14202 ˇvar x = new Y();
14203 ˇ</script>
14204 "#
14205 .unindent(),
14206 );
14207 cx.executor().run_until_parked();
14208 cx.update_editor(|editor, window, cx| {
14209 editor.toggle_comments(&ToggleComments::default(), window, cx)
14210 });
14211 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
14212 // Uncommenting and commenting from this position brings in even more wrong artifacts.
14213 cx.assert_editor_state(
14214 &r#"
14215 <!-- ˇ<script> -->
14216 // ˇvar x = new Y();
14217 <!-- ˇ</script> -->
14218 "#
14219 .unindent(),
14220 );
14221}
14222
14223#[gpui::test]
14224fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
14225 init_test(cx, |_| {});
14226
14227 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14228 let multibuffer = cx.new(|cx| {
14229 let mut multibuffer = MultiBuffer::new(ReadWrite);
14230 multibuffer.push_excerpts(
14231 buffer.clone(),
14232 [
14233 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
14234 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
14235 ],
14236 cx,
14237 );
14238 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
14239 multibuffer
14240 });
14241
14242 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
14243 editor.update_in(cx, |editor, window, cx| {
14244 assert_eq!(editor.text(cx), "aaaa\nbbbb");
14245 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14246 s.select_ranges([
14247 Point::new(0, 0)..Point::new(0, 0),
14248 Point::new(1, 0)..Point::new(1, 0),
14249 ])
14250 });
14251
14252 editor.handle_input("X", window, cx);
14253 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
14254 assert_eq!(
14255 editor.selections.ranges(cx),
14256 [
14257 Point::new(0, 1)..Point::new(0, 1),
14258 Point::new(1, 1)..Point::new(1, 1),
14259 ]
14260 );
14261
14262 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
14263 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14264 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
14265 });
14266 editor.backspace(&Default::default(), window, cx);
14267 assert_eq!(editor.text(cx), "Xa\nbbb");
14268 assert_eq!(
14269 editor.selections.ranges(cx),
14270 [Point::new(1, 0)..Point::new(1, 0)]
14271 );
14272
14273 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14274 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
14275 });
14276 editor.backspace(&Default::default(), window, cx);
14277 assert_eq!(editor.text(cx), "X\nbb");
14278 assert_eq!(
14279 editor.selections.ranges(cx),
14280 [Point::new(0, 1)..Point::new(0, 1)]
14281 );
14282 });
14283}
14284
14285#[gpui::test]
14286fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
14287 init_test(cx, |_| {});
14288
14289 let markers = vec![('[', ']').into(), ('(', ')').into()];
14290 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
14291 indoc! {"
14292 [aaaa
14293 (bbbb]
14294 cccc)",
14295 },
14296 markers.clone(),
14297 );
14298 let excerpt_ranges = markers.into_iter().map(|marker| {
14299 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
14300 ExcerptRange::new(context.clone())
14301 });
14302 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
14303 let multibuffer = cx.new(|cx| {
14304 let mut multibuffer = MultiBuffer::new(ReadWrite);
14305 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
14306 multibuffer
14307 });
14308
14309 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
14310 editor.update_in(cx, |editor, window, cx| {
14311 let (expected_text, selection_ranges) = marked_text_ranges(
14312 indoc! {"
14313 aaaa
14314 bˇbbb
14315 bˇbbˇb
14316 cccc"
14317 },
14318 true,
14319 );
14320 assert_eq!(editor.text(cx), expected_text);
14321 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14322 s.select_ranges(selection_ranges)
14323 });
14324
14325 editor.handle_input("X", window, cx);
14326
14327 let (expected_text, expected_selections) = marked_text_ranges(
14328 indoc! {"
14329 aaaa
14330 bXˇbbXb
14331 bXˇbbXˇb
14332 cccc"
14333 },
14334 false,
14335 );
14336 assert_eq!(editor.text(cx), expected_text);
14337 assert_eq!(editor.selections.ranges(cx), expected_selections);
14338
14339 editor.newline(&Newline, window, cx);
14340 let (expected_text, expected_selections) = marked_text_ranges(
14341 indoc! {"
14342 aaaa
14343 bX
14344 ˇbbX
14345 b
14346 bX
14347 ˇbbX
14348 ˇb
14349 cccc"
14350 },
14351 false,
14352 );
14353 assert_eq!(editor.text(cx), expected_text);
14354 assert_eq!(editor.selections.ranges(cx), expected_selections);
14355 });
14356}
14357
14358#[gpui::test]
14359fn test_refresh_selections(cx: &mut TestAppContext) {
14360 init_test(cx, |_| {});
14361
14362 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14363 let mut excerpt1_id = None;
14364 let multibuffer = cx.new(|cx| {
14365 let mut multibuffer = MultiBuffer::new(ReadWrite);
14366 excerpt1_id = multibuffer
14367 .push_excerpts(
14368 buffer.clone(),
14369 [
14370 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14371 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14372 ],
14373 cx,
14374 )
14375 .into_iter()
14376 .next();
14377 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14378 multibuffer
14379 });
14380
14381 let editor = cx.add_window(|window, cx| {
14382 let mut editor = build_editor(multibuffer.clone(), window, cx);
14383 let snapshot = editor.snapshot(window, cx);
14384 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14385 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
14386 });
14387 editor.begin_selection(
14388 Point::new(2, 1).to_display_point(&snapshot),
14389 true,
14390 1,
14391 window,
14392 cx,
14393 );
14394 assert_eq!(
14395 editor.selections.ranges(cx),
14396 [
14397 Point::new(1, 3)..Point::new(1, 3),
14398 Point::new(2, 1)..Point::new(2, 1),
14399 ]
14400 );
14401 editor
14402 });
14403
14404 // Refreshing selections is a no-op when excerpts haven't changed.
14405 _ = editor.update(cx, |editor, window, cx| {
14406 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14407 assert_eq!(
14408 editor.selections.ranges(cx),
14409 [
14410 Point::new(1, 3)..Point::new(1, 3),
14411 Point::new(2, 1)..Point::new(2, 1),
14412 ]
14413 );
14414 });
14415
14416 multibuffer.update(cx, |multibuffer, cx| {
14417 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14418 });
14419 _ = editor.update(cx, |editor, window, cx| {
14420 // Removing an excerpt causes the first selection to become degenerate.
14421 assert_eq!(
14422 editor.selections.ranges(cx),
14423 [
14424 Point::new(0, 0)..Point::new(0, 0),
14425 Point::new(0, 1)..Point::new(0, 1)
14426 ]
14427 );
14428
14429 // Refreshing selections will relocate the first selection to the original buffer
14430 // location.
14431 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14432 assert_eq!(
14433 editor.selections.ranges(cx),
14434 [
14435 Point::new(0, 1)..Point::new(0, 1),
14436 Point::new(0, 3)..Point::new(0, 3)
14437 ]
14438 );
14439 assert!(editor.selections.pending_anchor().is_some());
14440 });
14441}
14442
14443#[gpui::test]
14444fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
14445 init_test(cx, |_| {});
14446
14447 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14448 let mut excerpt1_id = None;
14449 let multibuffer = cx.new(|cx| {
14450 let mut multibuffer = MultiBuffer::new(ReadWrite);
14451 excerpt1_id = multibuffer
14452 .push_excerpts(
14453 buffer.clone(),
14454 [
14455 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14456 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14457 ],
14458 cx,
14459 )
14460 .into_iter()
14461 .next();
14462 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14463 multibuffer
14464 });
14465
14466 let editor = cx.add_window(|window, cx| {
14467 let mut editor = build_editor(multibuffer.clone(), window, cx);
14468 let snapshot = editor.snapshot(window, cx);
14469 editor.begin_selection(
14470 Point::new(1, 3).to_display_point(&snapshot),
14471 false,
14472 1,
14473 window,
14474 cx,
14475 );
14476 assert_eq!(
14477 editor.selections.ranges(cx),
14478 [Point::new(1, 3)..Point::new(1, 3)]
14479 );
14480 editor
14481 });
14482
14483 multibuffer.update(cx, |multibuffer, cx| {
14484 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14485 });
14486 _ = editor.update(cx, |editor, window, cx| {
14487 assert_eq!(
14488 editor.selections.ranges(cx),
14489 [Point::new(0, 0)..Point::new(0, 0)]
14490 );
14491
14492 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
14493 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14494 assert_eq!(
14495 editor.selections.ranges(cx),
14496 [Point::new(0, 3)..Point::new(0, 3)]
14497 );
14498 assert!(editor.selections.pending_anchor().is_some());
14499 });
14500}
14501
14502#[gpui::test]
14503async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
14504 init_test(cx, |_| {});
14505
14506 let language = Arc::new(
14507 Language::new(
14508 LanguageConfig {
14509 brackets: BracketPairConfig {
14510 pairs: vec![
14511 BracketPair {
14512 start: "{".to_string(),
14513 end: "}".to_string(),
14514 close: true,
14515 surround: true,
14516 newline: true,
14517 },
14518 BracketPair {
14519 start: "/* ".to_string(),
14520 end: " */".to_string(),
14521 close: true,
14522 surround: true,
14523 newline: true,
14524 },
14525 ],
14526 ..Default::default()
14527 },
14528 ..Default::default()
14529 },
14530 Some(tree_sitter_rust::LANGUAGE.into()),
14531 )
14532 .with_indents_query("")
14533 .unwrap(),
14534 );
14535
14536 let text = concat!(
14537 "{ }\n", //
14538 " x\n", //
14539 " /* */\n", //
14540 "x\n", //
14541 "{{} }\n", //
14542 );
14543
14544 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
14545 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14546 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14547 editor
14548 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
14549 .await;
14550
14551 editor.update_in(cx, |editor, window, cx| {
14552 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14553 s.select_display_ranges([
14554 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
14555 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
14556 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
14557 ])
14558 });
14559 editor.newline(&Newline, window, cx);
14560
14561 assert_eq!(
14562 editor.buffer().read(cx).read(cx).text(),
14563 concat!(
14564 "{ \n", // Suppress rustfmt
14565 "\n", //
14566 "}\n", //
14567 " x\n", //
14568 " /* \n", //
14569 " \n", //
14570 " */\n", //
14571 "x\n", //
14572 "{{} \n", //
14573 "}\n", //
14574 )
14575 );
14576 });
14577}
14578
14579#[gpui::test]
14580fn test_highlighted_ranges(cx: &mut TestAppContext) {
14581 init_test(cx, |_| {});
14582
14583 let editor = cx.add_window(|window, cx| {
14584 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
14585 build_editor(buffer.clone(), window, cx)
14586 });
14587
14588 _ = editor.update(cx, |editor, window, cx| {
14589 struct Type1;
14590 struct Type2;
14591
14592 let buffer = editor.buffer.read(cx).snapshot(cx);
14593
14594 let anchor_range =
14595 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
14596
14597 editor.highlight_background::<Type1>(
14598 &[
14599 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
14600 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
14601 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
14602 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
14603 ],
14604 |_| Hsla::red(),
14605 cx,
14606 );
14607 editor.highlight_background::<Type2>(
14608 &[
14609 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
14610 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
14611 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
14612 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
14613 ],
14614 |_| Hsla::green(),
14615 cx,
14616 );
14617
14618 let snapshot = editor.snapshot(window, cx);
14619 let mut highlighted_ranges = editor.background_highlights_in_range(
14620 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
14621 &snapshot,
14622 cx.theme(),
14623 );
14624 // Enforce a consistent ordering based on color without relying on the ordering of the
14625 // highlight's `TypeId` which is non-executor.
14626 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
14627 assert_eq!(
14628 highlighted_ranges,
14629 &[
14630 (
14631 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
14632 Hsla::red(),
14633 ),
14634 (
14635 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14636 Hsla::red(),
14637 ),
14638 (
14639 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
14640 Hsla::green(),
14641 ),
14642 (
14643 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
14644 Hsla::green(),
14645 ),
14646 ]
14647 );
14648 assert_eq!(
14649 editor.background_highlights_in_range(
14650 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
14651 &snapshot,
14652 cx.theme(),
14653 ),
14654 &[(
14655 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14656 Hsla::red(),
14657 )]
14658 );
14659 });
14660}
14661
14662#[gpui::test]
14663async fn test_following(cx: &mut TestAppContext) {
14664 init_test(cx, |_| {});
14665
14666 let fs = FakeFs::new(cx.executor());
14667 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14668
14669 let buffer = project.update(cx, |project, cx| {
14670 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
14671 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
14672 });
14673 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
14674 let follower = cx.update(|cx| {
14675 cx.open_window(
14676 WindowOptions {
14677 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
14678 gpui::Point::new(px(0.), px(0.)),
14679 gpui::Point::new(px(10.), px(80.)),
14680 ))),
14681 ..Default::default()
14682 },
14683 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
14684 )
14685 .unwrap()
14686 });
14687
14688 let is_still_following = Rc::new(RefCell::new(true));
14689 let follower_edit_event_count = Rc::new(RefCell::new(0));
14690 let pending_update = Rc::new(RefCell::new(None));
14691 let leader_entity = leader.root(cx).unwrap();
14692 let follower_entity = follower.root(cx).unwrap();
14693 _ = follower.update(cx, {
14694 let update = pending_update.clone();
14695 let is_still_following = is_still_following.clone();
14696 let follower_edit_event_count = follower_edit_event_count.clone();
14697 |_, window, cx| {
14698 cx.subscribe_in(
14699 &leader_entity,
14700 window,
14701 move |_, leader, event, window, cx| {
14702 leader.read(cx).add_event_to_update_proto(
14703 event,
14704 &mut update.borrow_mut(),
14705 window,
14706 cx,
14707 );
14708 },
14709 )
14710 .detach();
14711
14712 cx.subscribe_in(
14713 &follower_entity,
14714 window,
14715 move |_, _, event: &EditorEvent, _window, _cx| {
14716 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
14717 *is_still_following.borrow_mut() = false;
14718 }
14719
14720 if let EditorEvent::BufferEdited = event {
14721 *follower_edit_event_count.borrow_mut() += 1;
14722 }
14723 },
14724 )
14725 .detach();
14726 }
14727 });
14728
14729 // Update the selections only
14730 _ = leader.update(cx, |leader, window, cx| {
14731 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14732 s.select_ranges([1..1])
14733 });
14734 });
14735 follower
14736 .update(cx, |follower, window, cx| {
14737 follower.apply_update_proto(
14738 &project,
14739 pending_update.borrow_mut().take().unwrap(),
14740 window,
14741 cx,
14742 )
14743 })
14744 .unwrap()
14745 .await
14746 .unwrap();
14747 _ = follower.update(cx, |follower, _, cx| {
14748 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
14749 });
14750 assert!(*is_still_following.borrow());
14751 assert_eq!(*follower_edit_event_count.borrow(), 0);
14752
14753 // Update the scroll position only
14754 _ = leader.update(cx, |leader, window, cx| {
14755 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14756 });
14757 follower
14758 .update(cx, |follower, window, cx| {
14759 follower.apply_update_proto(
14760 &project,
14761 pending_update.borrow_mut().take().unwrap(),
14762 window,
14763 cx,
14764 )
14765 })
14766 .unwrap()
14767 .await
14768 .unwrap();
14769 assert_eq!(
14770 follower
14771 .update(cx, |follower, _, cx| follower.scroll_position(cx))
14772 .unwrap(),
14773 gpui::Point::new(1.5, 3.5)
14774 );
14775 assert!(*is_still_following.borrow());
14776 assert_eq!(*follower_edit_event_count.borrow(), 0);
14777
14778 // Update the selections and scroll position. The follower's scroll position is updated
14779 // via autoscroll, not via the leader's exact scroll position.
14780 _ = leader.update(cx, |leader, window, cx| {
14781 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14782 s.select_ranges([0..0])
14783 });
14784 leader.request_autoscroll(Autoscroll::newest(), cx);
14785 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14786 });
14787 follower
14788 .update(cx, |follower, window, cx| {
14789 follower.apply_update_proto(
14790 &project,
14791 pending_update.borrow_mut().take().unwrap(),
14792 window,
14793 cx,
14794 )
14795 })
14796 .unwrap()
14797 .await
14798 .unwrap();
14799 _ = follower.update(cx, |follower, _, cx| {
14800 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
14801 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
14802 });
14803 assert!(*is_still_following.borrow());
14804
14805 // Creating a pending selection that precedes another selection
14806 _ = leader.update(cx, |leader, window, cx| {
14807 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14808 s.select_ranges([1..1])
14809 });
14810 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
14811 });
14812 follower
14813 .update(cx, |follower, window, cx| {
14814 follower.apply_update_proto(
14815 &project,
14816 pending_update.borrow_mut().take().unwrap(),
14817 window,
14818 cx,
14819 )
14820 })
14821 .unwrap()
14822 .await
14823 .unwrap();
14824 _ = follower.update(cx, |follower, _, cx| {
14825 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
14826 });
14827 assert!(*is_still_following.borrow());
14828
14829 // Extend the pending selection so that it surrounds another selection
14830 _ = leader.update(cx, |leader, window, cx| {
14831 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
14832 });
14833 follower
14834 .update(cx, |follower, window, cx| {
14835 follower.apply_update_proto(
14836 &project,
14837 pending_update.borrow_mut().take().unwrap(),
14838 window,
14839 cx,
14840 )
14841 })
14842 .unwrap()
14843 .await
14844 .unwrap();
14845 _ = follower.update(cx, |follower, _, cx| {
14846 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
14847 });
14848
14849 // Scrolling locally breaks the follow
14850 _ = follower.update(cx, |follower, window, cx| {
14851 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
14852 follower.set_scroll_anchor(
14853 ScrollAnchor {
14854 anchor: top_anchor,
14855 offset: gpui::Point::new(0.0, 0.5),
14856 },
14857 window,
14858 cx,
14859 );
14860 });
14861 assert!(!(*is_still_following.borrow()));
14862}
14863
14864#[gpui::test]
14865async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
14866 init_test(cx, |_| {});
14867
14868 let fs = FakeFs::new(cx.executor());
14869 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14870 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14871 let pane = workspace
14872 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14873 .unwrap();
14874
14875 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14876
14877 let leader = pane.update_in(cx, |_, window, cx| {
14878 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
14879 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
14880 });
14881
14882 // Start following the editor when it has no excerpts.
14883 let mut state_message =
14884 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14885 let workspace_entity = workspace.root(cx).unwrap();
14886 let follower_1 = cx
14887 .update_window(*workspace.deref(), |_, window, cx| {
14888 Editor::from_state_proto(
14889 workspace_entity,
14890 ViewId {
14891 creator: CollaboratorId::PeerId(PeerId::default()),
14892 id: 0,
14893 },
14894 &mut state_message,
14895 window,
14896 cx,
14897 )
14898 })
14899 .unwrap()
14900 .unwrap()
14901 .await
14902 .unwrap();
14903
14904 let update_message = Rc::new(RefCell::new(None));
14905 follower_1.update_in(cx, {
14906 let update = update_message.clone();
14907 |_, window, cx| {
14908 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
14909 leader.read(cx).add_event_to_update_proto(
14910 event,
14911 &mut update.borrow_mut(),
14912 window,
14913 cx,
14914 );
14915 })
14916 .detach();
14917 }
14918 });
14919
14920 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
14921 (
14922 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
14923 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
14924 )
14925 });
14926
14927 // Insert some excerpts.
14928 leader.update(cx, |leader, cx| {
14929 leader.buffer.update(cx, |multibuffer, cx| {
14930 multibuffer.set_excerpts_for_path(
14931 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
14932 buffer_1.clone(),
14933 vec![
14934 Point::row_range(0..3),
14935 Point::row_range(1..6),
14936 Point::row_range(12..15),
14937 ],
14938 0,
14939 cx,
14940 );
14941 multibuffer.set_excerpts_for_path(
14942 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
14943 buffer_2.clone(),
14944 vec![Point::row_range(0..6), Point::row_range(8..12)],
14945 0,
14946 cx,
14947 );
14948 });
14949 });
14950
14951 // Apply the update of adding the excerpts.
14952 follower_1
14953 .update_in(cx, |follower, window, cx| {
14954 follower.apply_update_proto(
14955 &project,
14956 update_message.borrow().clone().unwrap(),
14957 window,
14958 cx,
14959 )
14960 })
14961 .await
14962 .unwrap();
14963 assert_eq!(
14964 follower_1.update(cx, |editor, cx| editor.text(cx)),
14965 leader.update(cx, |editor, cx| editor.text(cx))
14966 );
14967 update_message.borrow_mut().take();
14968
14969 // Start following separately after it already has excerpts.
14970 let mut state_message =
14971 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14972 let workspace_entity = workspace.root(cx).unwrap();
14973 let follower_2 = cx
14974 .update_window(*workspace.deref(), |_, window, cx| {
14975 Editor::from_state_proto(
14976 workspace_entity,
14977 ViewId {
14978 creator: CollaboratorId::PeerId(PeerId::default()),
14979 id: 0,
14980 },
14981 &mut state_message,
14982 window,
14983 cx,
14984 )
14985 })
14986 .unwrap()
14987 .unwrap()
14988 .await
14989 .unwrap();
14990 assert_eq!(
14991 follower_2.update(cx, |editor, cx| editor.text(cx)),
14992 leader.update(cx, |editor, cx| editor.text(cx))
14993 );
14994
14995 // Remove some excerpts.
14996 leader.update(cx, |leader, cx| {
14997 leader.buffer.update(cx, |multibuffer, cx| {
14998 let excerpt_ids = multibuffer.excerpt_ids();
14999 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
15000 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
15001 });
15002 });
15003
15004 // Apply the update of removing the excerpts.
15005 follower_1
15006 .update_in(cx, |follower, window, cx| {
15007 follower.apply_update_proto(
15008 &project,
15009 update_message.borrow().clone().unwrap(),
15010 window,
15011 cx,
15012 )
15013 })
15014 .await
15015 .unwrap();
15016 follower_2
15017 .update_in(cx, |follower, window, cx| {
15018 follower.apply_update_proto(
15019 &project,
15020 update_message.borrow().clone().unwrap(),
15021 window,
15022 cx,
15023 )
15024 })
15025 .await
15026 .unwrap();
15027 update_message.borrow_mut().take();
15028 assert_eq!(
15029 follower_1.update(cx, |editor, cx| editor.text(cx)),
15030 leader.update(cx, |editor, cx| editor.text(cx))
15031 );
15032}
15033
15034#[gpui::test]
15035async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15036 init_test(cx, |_| {});
15037
15038 let mut cx = EditorTestContext::new(cx).await;
15039 let lsp_store =
15040 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
15041
15042 cx.set_state(indoc! {"
15043 ˇfn func(abc def: i32) -> u32 {
15044 }
15045 "});
15046
15047 cx.update(|_, cx| {
15048 lsp_store.update(cx, |lsp_store, cx| {
15049 lsp_store
15050 .update_diagnostics(
15051 LanguageServerId(0),
15052 lsp::PublishDiagnosticsParams {
15053 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
15054 version: None,
15055 diagnostics: vec![
15056 lsp::Diagnostic {
15057 range: lsp::Range::new(
15058 lsp::Position::new(0, 11),
15059 lsp::Position::new(0, 12),
15060 ),
15061 severity: Some(lsp::DiagnosticSeverity::ERROR),
15062 ..Default::default()
15063 },
15064 lsp::Diagnostic {
15065 range: lsp::Range::new(
15066 lsp::Position::new(0, 12),
15067 lsp::Position::new(0, 15),
15068 ),
15069 severity: Some(lsp::DiagnosticSeverity::ERROR),
15070 ..Default::default()
15071 },
15072 lsp::Diagnostic {
15073 range: lsp::Range::new(
15074 lsp::Position::new(0, 25),
15075 lsp::Position::new(0, 28),
15076 ),
15077 severity: Some(lsp::DiagnosticSeverity::ERROR),
15078 ..Default::default()
15079 },
15080 ],
15081 },
15082 None,
15083 DiagnosticSourceKind::Pushed,
15084 &[],
15085 cx,
15086 )
15087 .unwrap()
15088 });
15089 });
15090
15091 executor.run_until_parked();
15092
15093 cx.update_editor(|editor, window, cx| {
15094 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15095 });
15096
15097 cx.assert_editor_state(indoc! {"
15098 fn func(abc def: i32) -> ˇu32 {
15099 }
15100 "});
15101
15102 cx.update_editor(|editor, window, cx| {
15103 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15104 });
15105
15106 cx.assert_editor_state(indoc! {"
15107 fn func(abc ˇdef: i32) -> u32 {
15108 }
15109 "});
15110
15111 cx.update_editor(|editor, window, cx| {
15112 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15113 });
15114
15115 cx.assert_editor_state(indoc! {"
15116 fn func(abcˇ def: i32) -> u32 {
15117 }
15118 "});
15119
15120 cx.update_editor(|editor, window, cx| {
15121 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
15122 });
15123
15124 cx.assert_editor_state(indoc! {"
15125 fn func(abc def: i32) -> ˇu32 {
15126 }
15127 "});
15128}
15129
15130#[gpui::test]
15131async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15132 init_test(cx, |_| {});
15133
15134 let mut cx = EditorTestContext::new(cx).await;
15135
15136 let diff_base = r#"
15137 use some::mod;
15138
15139 const A: u32 = 42;
15140
15141 fn main() {
15142 println!("hello");
15143
15144 println!("world");
15145 }
15146 "#
15147 .unindent();
15148
15149 // Edits are modified, removed, modified, added
15150 cx.set_state(
15151 &r#"
15152 use some::modified;
15153
15154 ˇ
15155 fn main() {
15156 println!("hello there");
15157
15158 println!("around the");
15159 println!("world");
15160 }
15161 "#
15162 .unindent(),
15163 );
15164
15165 cx.set_head_text(&diff_base);
15166 executor.run_until_parked();
15167
15168 cx.update_editor(|editor, window, cx| {
15169 //Wrap around the bottom of the buffer
15170 for _ in 0..3 {
15171 editor.go_to_next_hunk(&GoToHunk, window, cx);
15172 }
15173 });
15174
15175 cx.assert_editor_state(
15176 &r#"
15177 ˇuse some::modified;
15178
15179
15180 fn main() {
15181 println!("hello there");
15182
15183 println!("around the");
15184 println!("world");
15185 }
15186 "#
15187 .unindent(),
15188 );
15189
15190 cx.update_editor(|editor, window, cx| {
15191 //Wrap around the top of the buffer
15192 for _ in 0..2 {
15193 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15194 }
15195 });
15196
15197 cx.assert_editor_state(
15198 &r#"
15199 use some::modified;
15200
15201
15202 fn main() {
15203 ˇ println!("hello there");
15204
15205 println!("around the");
15206 println!("world");
15207 }
15208 "#
15209 .unindent(),
15210 );
15211
15212 cx.update_editor(|editor, window, cx| {
15213 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15214 });
15215
15216 cx.assert_editor_state(
15217 &r#"
15218 use some::modified;
15219
15220 ˇ
15221 fn main() {
15222 println!("hello there");
15223
15224 println!("around the");
15225 println!("world");
15226 }
15227 "#
15228 .unindent(),
15229 );
15230
15231 cx.update_editor(|editor, window, cx| {
15232 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15233 });
15234
15235 cx.assert_editor_state(
15236 &r#"
15237 ˇuse some::modified;
15238
15239
15240 fn main() {
15241 println!("hello there");
15242
15243 println!("around the");
15244 println!("world");
15245 }
15246 "#
15247 .unindent(),
15248 );
15249
15250 cx.update_editor(|editor, window, cx| {
15251 for _ in 0..2 {
15252 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15253 }
15254 });
15255
15256 cx.assert_editor_state(
15257 &r#"
15258 use some::modified;
15259
15260
15261 fn main() {
15262 ˇ println!("hello there");
15263
15264 println!("around the");
15265 println!("world");
15266 }
15267 "#
15268 .unindent(),
15269 );
15270
15271 cx.update_editor(|editor, window, cx| {
15272 editor.fold(&Fold, window, cx);
15273 });
15274
15275 cx.update_editor(|editor, window, cx| {
15276 editor.go_to_next_hunk(&GoToHunk, window, cx);
15277 });
15278
15279 cx.assert_editor_state(
15280 &r#"
15281 ˇuse some::modified;
15282
15283
15284 fn main() {
15285 println!("hello there");
15286
15287 println!("around the");
15288 println!("world");
15289 }
15290 "#
15291 .unindent(),
15292 );
15293}
15294
15295#[test]
15296fn test_split_words() {
15297 fn split(text: &str) -> Vec<&str> {
15298 split_words(text).collect()
15299 }
15300
15301 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
15302 assert_eq!(split("hello_world"), &["hello_", "world"]);
15303 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
15304 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
15305 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
15306 assert_eq!(split("helloworld"), &["helloworld"]);
15307
15308 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
15309}
15310
15311#[gpui::test]
15312async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
15313 init_test(cx, |_| {});
15314
15315 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
15316 let mut assert = |before, after| {
15317 let _state_context = cx.set_state(before);
15318 cx.run_until_parked();
15319 cx.update_editor(|editor, window, cx| {
15320 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
15321 });
15322 cx.run_until_parked();
15323 cx.assert_editor_state(after);
15324 };
15325
15326 // Outside bracket jumps to outside of matching bracket
15327 assert("console.logˇ(var);", "console.log(var)ˇ;");
15328 assert("console.log(var)ˇ;", "console.logˇ(var);");
15329
15330 // Inside bracket jumps to inside of matching bracket
15331 assert("console.log(ˇvar);", "console.log(varˇ);");
15332 assert("console.log(varˇ);", "console.log(ˇvar);");
15333
15334 // When outside a bracket and inside, favor jumping to the inside bracket
15335 assert(
15336 "console.log('foo', [1, 2, 3]ˇ);",
15337 "console.log(ˇ'foo', [1, 2, 3]);",
15338 );
15339 assert(
15340 "console.log(ˇ'foo', [1, 2, 3]);",
15341 "console.log('foo', [1, 2, 3]ˇ);",
15342 );
15343
15344 // Bias forward if two options are equally likely
15345 assert(
15346 "let result = curried_fun()ˇ();",
15347 "let result = curried_fun()()ˇ;",
15348 );
15349
15350 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
15351 assert(
15352 indoc! {"
15353 function test() {
15354 console.log('test')ˇ
15355 }"},
15356 indoc! {"
15357 function test() {
15358 console.logˇ('test')
15359 }"},
15360 );
15361}
15362
15363#[gpui::test]
15364async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
15365 init_test(cx, |_| {});
15366
15367 let fs = FakeFs::new(cx.executor());
15368 fs.insert_tree(
15369 path!("/a"),
15370 json!({
15371 "main.rs": "fn main() { let a = 5; }",
15372 "other.rs": "// Test file",
15373 }),
15374 )
15375 .await;
15376 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15377
15378 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15379 language_registry.add(Arc::new(Language::new(
15380 LanguageConfig {
15381 name: "Rust".into(),
15382 matcher: LanguageMatcher {
15383 path_suffixes: vec!["rs".to_string()],
15384 ..Default::default()
15385 },
15386 brackets: BracketPairConfig {
15387 pairs: vec![BracketPair {
15388 start: "{".to_string(),
15389 end: "}".to_string(),
15390 close: true,
15391 surround: true,
15392 newline: true,
15393 }],
15394 disabled_scopes_by_bracket_ix: Vec::new(),
15395 },
15396 ..Default::default()
15397 },
15398 Some(tree_sitter_rust::LANGUAGE.into()),
15399 )));
15400 let mut fake_servers = language_registry.register_fake_lsp(
15401 "Rust",
15402 FakeLspAdapter {
15403 capabilities: lsp::ServerCapabilities {
15404 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15405 first_trigger_character: "{".to_string(),
15406 more_trigger_character: None,
15407 }),
15408 ..Default::default()
15409 },
15410 ..Default::default()
15411 },
15412 );
15413
15414 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15415
15416 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15417
15418 let worktree_id = workspace
15419 .update(cx, |workspace, _, cx| {
15420 workspace.project().update(cx, |project, cx| {
15421 project.worktrees(cx).next().unwrap().read(cx).id()
15422 })
15423 })
15424 .unwrap();
15425
15426 let buffer = project
15427 .update(cx, |project, cx| {
15428 project.open_local_buffer(path!("/a/main.rs"), cx)
15429 })
15430 .await
15431 .unwrap();
15432 let editor_handle = workspace
15433 .update(cx, |workspace, window, cx| {
15434 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
15435 })
15436 .unwrap()
15437 .await
15438 .unwrap()
15439 .downcast::<Editor>()
15440 .unwrap();
15441
15442 cx.executor().start_waiting();
15443 let fake_server = fake_servers.next().await.unwrap();
15444
15445 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
15446 |params, _| async move {
15447 assert_eq!(
15448 params.text_document_position.text_document.uri,
15449 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
15450 );
15451 assert_eq!(
15452 params.text_document_position.position,
15453 lsp::Position::new(0, 21),
15454 );
15455
15456 Ok(Some(vec![lsp::TextEdit {
15457 new_text: "]".to_string(),
15458 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15459 }]))
15460 },
15461 );
15462
15463 editor_handle.update_in(cx, |editor, window, cx| {
15464 window.focus(&editor.focus_handle(cx));
15465 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15466 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
15467 });
15468 editor.handle_input("{", window, cx);
15469 });
15470
15471 cx.executor().run_until_parked();
15472
15473 buffer.update(cx, |buffer, _| {
15474 assert_eq!(
15475 buffer.text(),
15476 "fn main() { let a = {5}; }",
15477 "No extra braces from on type formatting should appear in the buffer"
15478 )
15479 });
15480}
15481
15482#[gpui::test(iterations = 20, seeds(31))]
15483async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
15484 init_test(cx, |_| {});
15485
15486 let mut cx = EditorLspTestContext::new_rust(
15487 lsp::ServerCapabilities {
15488 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15489 first_trigger_character: ".".to_string(),
15490 more_trigger_character: None,
15491 }),
15492 ..Default::default()
15493 },
15494 cx,
15495 )
15496 .await;
15497
15498 cx.update_buffer(|buffer, _| {
15499 // This causes autoindent to be async.
15500 buffer.set_sync_parse_timeout(Duration::ZERO)
15501 });
15502
15503 cx.set_state("fn c() {\n d()ˇ\n}\n");
15504 cx.simulate_keystroke("\n");
15505 cx.run_until_parked();
15506
15507 let buffer_cloned =
15508 cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap().clone());
15509 let mut request =
15510 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
15511 let buffer_cloned = buffer_cloned.clone();
15512 async move {
15513 buffer_cloned.update(&mut cx, |buffer, _| {
15514 assert_eq!(
15515 buffer.text(),
15516 "fn c() {\n d()\n .\n}\n",
15517 "OnTypeFormatting should triggered after autoindent applied"
15518 )
15519 })?;
15520
15521 Ok(Some(vec![]))
15522 }
15523 });
15524
15525 cx.simulate_keystroke(".");
15526 cx.run_until_parked();
15527
15528 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
15529 assert!(request.next().await.is_some());
15530 request.close();
15531 assert!(request.next().await.is_none());
15532}
15533
15534#[gpui::test]
15535async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
15536 init_test(cx, |_| {});
15537
15538 let fs = FakeFs::new(cx.executor());
15539 fs.insert_tree(
15540 path!("/a"),
15541 json!({
15542 "main.rs": "fn main() { let a = 5; }",
15543 "other.rs": "// Test file",
15544 }),
15545 )
15546 .await;
15547
15548 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15549
15550 let server_restarts = Arc::new(AtomicUsize::new(0));
15551 let closure_restarts = Arc::clone(&server_restarts);
15552 let language_server_name = "test language server";
15553 let language_name: LanguageName = "Rust".into();
15554
15555 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15556 language_registry.add(Arc::new(Language::new(
15557 LanguageConfig {
15558 name: language_name.clone(),
15559 matcher: LanguageMatcher {
15560 path_suffixes: vec!["rs".to_string()],
15561 ..Default::default()
15562 },
15563 ..Default::default()
15564 },
15565 Some(tree_sitter_rust::LANGUAGE.into()),
15566 )));
15567 let mut fake_servers = language_registry.register_fake_lsp(
15568 "Rust",
15569 FakeLspAdapter {
15570 name: language_server_name,
15571 initialization_options: Some(json!({
15572 "testOptionValue": true
15573 })),
15574 initializer: Some(Box::new(move |fake_server| {
15575 let task_restarts = Arc::clone(&closure_restarts);
15576 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
15577 task_restarts.fetch_add(1, atomic::Ordering::Release);
15578 futures::future::ready(Ok(()))
15579 });
15580 })),
15581 ..Default::default()
15582 },
15583 );
15584
15585 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15586 let _buffer = project
15587 .update(cx, |project, cx| {
15588 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
15589 })
15590 .await
15591 .unwrap();
15592 let _fake_server = fake_servers.next().await.unwrap();
15593 update_test_language_settings(cx, |language_settings| {
15594 language_settings.languages.0.insert(
15595 language_name.clone(),
15596 LanguageSettingsContent {
15597 tab_size: NonZeroU32::new(8),
15598 ..Default::default()
15599 },
15600 );
15601 });
15602 cx.executor().run_until_parked();
15603 assert_eq!(
15604 server_restarts.load(atomic::Ordering::Acquire),
15605 0,
15606 "Should not restart LSP server on an unrelated change"
15607 );
15608
15609 update_test_project_settings(cx, |project_settings| {
15610 project_settings.lsp.insert(
15611 "Some other server name".into(),
15612 LspSettings {
15613 binary: None,
15614 settings: None,
15615 initialization_options: Some(json!({
15616 "some other init value": false
15617 })),
15618 enable_lsp_tasks: false,
15619 },
15620 );
15621 });
15622 cx.executor().run_until_parked();
15623 assert_eq!(
15624 server_restarts.load(atomic::Ordering::Acquire),
15625 0,
15626 "Should not restart LSP server on an unrelated LSP settings change"
15627 );
15628
15629 update_test_project_settings(cx, |project_settings| {
15630 project_settings.lsp.insert(
15631 language_server_name.into(),
15632 LspSettings {
15633 binary: None,
15634 settings: None,
15635 initialization_options: Some(json!({
15636 "anotherInitValue": false
15637 })),
15638 enable_lsp_tasks: false,
15639 },
15640 );
15641 });
15642 cx.executor().run_until_parked();
15643 assert_eq!(
15644 server_restarts.load(atomic::Ordering::Acquire),
15645 1,
15646 "Should restart LSP server on a related LSP settings change"
15647 );
15648
15649 update_test_project_settings(cx, |project_settings| {
15650 project_settings.lsp.insert(
15651 language_server_name.into(),
15652 LspSettings {
15653 binary: None,
15654 settings: None,
15655 initialization_options: Some(json!({
15656 "anotherInitValue": false
15657 })),
15658 enable_lsp_tasks: false,
15659 },
15660 );
15661 });
15662 cx.executor().run_until_parked();
15663 assert_eq!(
15664 server_restarts.load(atomic::Ordering::Acquire),
15665 1,
15666 "Should not restart LSP server on a related LSP settings change that is the same"
15667 );
15668
15669 update_test_project_settings(cx, |project_settings| {
15670 project_settings.lsp.insert(
15671 language_server_name.into(),
15672 LspSettings {
15673 binary: None,
15674 settings: None,
15675 initialization_options: None,
15676 enable_lsp_tasks: false,
15677 },
15678 );
15679 });
15680 cx.executor().run_until_parked();
15681 assert_eq!(
15682 server_restarts.load(atomic::Ordering::Acquire),
15683 2,
15684 "Should restart LSP server on another related LSP settings change"
15685 );
15686}
15687
15688#[gpui::test]
15689async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
15690 init_test(cx, |_| {});
15691
15692 let mut cx = EditorLspTestContext::new_rust(
15693 lsp::ServerCapabilities {
15694 completion_provider: Some(lsp::CompletionOptions {
15695 trigger_characters: Some(vec![".".to_string()]),
15696 resolve_provider: Some(true),
15697 ..Default::default()
15698 }),
15699 ..Default::default()
15700 },
15701 cx,
15702 )
15703 .await;
15704
15705 cx.set_state("fn main() { let a = 2ˇ; }");
15706 cx.simulate_keystroke(".");
15707 let completion_item = lsp::CompletionItem {
15708 label: "some".into(),
15709 kind: Some(lsp::CompletionItemKind::SNIPPET),
15710 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15711 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15712 kind: lsp::MarkupKind::Markdown,
15713 value: "```rust\nSome(2)\n```".to_string(),
15714 })),
15715 deprecated: Some(false),
15716 sort_text: Some("fffffff2".to_string()),
15717 filter_text: Some("some".to_string()),
15718 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15719 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15720 range: lsp::Range {
15721 start: lsp::Position {
15722 line: 0,
15723 character: 22,
15724 },
15725 end: lsp::Position {
15726 line: 0,
15727 character: 22,
15728 },
15729 },
15730 new_text: "Some(2)".to_string(),
15731 })),
15732 additional_text_edits: Some(vec![lsp::TextEdit {
15733 range: lsp::Range {
15734 start: lsp::Position {
15735 line: 0,
15736 character: 20,
15737 },
15738 end: lsp::Position {
15739 line: 0,
15740 character: 22,
15741 },
15742 },
15743 new_text: "".to_string(),
15744 }]),
15745 ..Default::default()
15746 };
15747
15748 let closure_completion_item = completion_item.clone();
15749 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15750 let task_completion_item = closure_completion_item.clone();
15751 async move {
15752 Ok(Some(lsp::CompletionResponse::Array(vec![
15753 task_completion_item,
15754 ])))
15755 }
15756 });
15757
15758 request.next().await;
15759
15760 cx.condition(|editor, _| editor.context_menu_visible())
15761 .await;
15762 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15763 editor
15764 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15765 .unwrap()
15766 });
15767 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
15768
15769 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
15770 let task_completion_item = completion_item.clone();
15771 async move { Ok(task_completion_item) }
15772 })
15773 .next()
15774 .await
15775 .unwrap();
15776 apply_additional_edits.await.unwrap();
15777 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
15778}
15779
15780#[gpui::test]
15781async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
15782 init_test(cx, |_| {});
15783
15784 let mut cx = EditorLspTestContext::new_rust(
15785 lsp::ServerCapabilities {
15786 completion_provider: Some(lsp::CompletionOptions {
15787 trigger_characters: Some(vec![".".to_string()]),
15788 resolve_provider: Some(true),
15789 ..Default::default()
15790 }),
15791 ..Default::default()
15792 },
15793 cx,
15794 )
15795 .await;
15796
15797 cx.set_state("fn main() { let a = 2ˇ; }");
15798 cx.simulate_keystroke(".");
15799
15800 let item1 = lsp::CompletionItem {
15801 label: "method id()".to_string(),
15802 filter_text: Some("id".to_string()),
15803 detail: None,
15804 documentation: None,
15805 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15806 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15807 new_text: ".id".to_string(),
15808 })),
15809 ..lsp::CompletionItem::default()
15810 };
15811
15812 let item2 = lsp::CompletionItem {
15813 label: "other".to_string(),
15814 filter_text: Some("other".to_string()),
15815 detail: None,
15816 documentation: None,
15817 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15818 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15819 new_text: ".other".to_string(),
15820 })),
15821 ..lsp::CompletionItem::default()
15822 };
15823
15824 let item1 = item1.clone();
15825 cx.set_request_handler::<lsp::request::Completion, _, _>({
15826 let item1 = item1.clone();
15827 move |_, _, _| {
15828 let item1 = item1.clone();
15829 let item2 = item2.clone();
15830 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
15831 }
15832 })
15833 .next()
15834 .await;
15835
15836 cx.condition(|editor, _| editor.context_menu_visible())
15837 .await;
15838 cx.update_editor(|editor, _, _| {
15839 let context_menu = editor.context_menu.borrow_mut();
15840 let context_menu = context_menu
15841 .as_ref()
15842 .expect("Should have the context menu deployed");
15843 match context_menu {
15844 CodeContextMenu::Completions(completions_menu) => {
15845 let completions = completions_menu.completions.borrow_mut();
15846 assert_eq!(
15847 completions
15848 .iter()
15849 .map(|completion| &completion.label.text)
15850 .collect::<Vec<_>>(),
15851 vec!["method id()", "other"]
15852 )
15853 }
15854 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15855 }
15856 });
15857
15858 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
15859 let item1 = item1.clone();
15860 move |_, item_to_resolve, _| {
15861 let item1 = item1.clone();
15862 async move {
15863 if item1 == item_to_resolve {
15864 Ok(lsp::CompletionItem {
15865 label: "method id()".to_string(),
15866 filter_text: Some("id".to_string()),
15867 detail: Some("Now resolved!".to_string()),
15868 documentation: Some(lsp::Documentation::String("Docs".to_string())),
15869 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15870 range: lsp::Range::new(
15871 lsp::Position::new(0, 22),
15872 lsp::Position::new(0, 22),
15873 ),
15874 new_text: ".id".to_string(),
15875 })),
15876 ..lsp::CompletionItem::default()
15877 })
15878 } else {
15879 Ok(item_to_resolve)
15880 }
15881 }
15882 }
15883 })
15884 .next()
15885 .await
15886 .unwrap();
15887 cx.run_until_parked();
15888
15889 cx.update_editor(|editor, window, cx| {
15890 editor.context_menu_next(&Default::default(), window, cx);
15891 });
15892
15893 cx.update_editor(|editor, _, _| {
15894 let context_menu = editor.context_menu.borrow_mut();
15895 let context_menu = context_menu
15896 .as_ref()
15897 .expect("Should have the context menu deployed");
15898 match context_menu {
15899 CodeContextMenu::Completions(completions_menu) => {
15900 let completions = completions_menu.completions.borrow_mut();
15901 assert_eq!(
15902 completions
15903 .iter()
15904 .map(|completion| &completion.label.text)
15905 .collect::<Vec<_>>(),
15906 vec!["method id() Now resolved!", "other"],
15907 "Should update first completion label, but not second as the filter text did not match."
15908 );
15909 }
15910 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15911 }
15912 });
15913}
15914
15915#[gpui::test]
15916async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
15917 init_test(cx, |_| {});
15918 let mut cx = EditorLspTestContext::new_rust(
15919 lsp::ServerCapabilities {
15920 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
15921 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
15922 completion_provider: Some(lsp::CompletionOptions {
15923 resolve_provider: Some(true),
15924 ..Default::default()
15925 }),
15926 ..Default::default()
15927 },
15928 cx,
15929 )
15930 .await;
15931 cx.set_state(indoc! {"
15932 struct TestStruct {
15933 field: i32
15934 }
15935
15936 fn mainˇ() {
15937 let unused_var = 42;
15938 let test_struct = TestStruct { field: 42 };
15939 }
15940 "});
15941 let symbol_range = cx.lsp_range(indoc! {"
15942 struct TestStruct {
15943 field: i32
15944 }
15945
15946 «fn main»() {
15947 let unused_var = 42;
15948 let test_struct = TestStruct { field: 42 };
15949 }
15950 "});
15951 let mut hover_requests =
15952 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
15953 Ok(Some(lsp::Hover {
15954 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
15955 kind: lsp::MarkupKind::Markdown,
15956 value: "Function documentation".to_string(),
15957 }),
15958 range: Some(symbol_range),
15959 }))
15960 });
15961
15962 // Case 1: Test that code action menu hide hover popover
15963 cx.dispatch_action(Hover);
15964 hover_requests.next().await;
15965 cx.condition(|editor, _| editor.hover_state.visible()).await;
15966 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
15967 move |_, _, _| async move {
15968 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
15969 lsp::CodeAction {
15970 title: "Remove unused variable".to_string(),
15971 kind: Some(CodeActionKind::QUICKFIX),
15972 edit: Some(lsp::WorkspaceEdit {
15973 changes: Some(
15974 [(
15975 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
15976 vec![lsp::TextEdit {
15977 range: lsp::Range::new(
15978 lsp::Position::new(5, 4),
15979 lsp::Position::new(5, 27),
15980 ),
15981 new_text: "".to_string(),
15982 }],
15983 )]
15984 .into_iter()
15985 .collect(),
15986 ),
15987 ..Default::default()
15988 }),
15989 ..Default::default()
15990 },
15991 )]))
15992 },
15993 );
15994 cx.update_editor(|editor, window, cx| {
15995 editor.toggle_code_actions(
15996 &ToggleCodeActions {
15997 deployed_from: None,
15998 quick_launch: false,
15999 },
16000 window,
16001 cx,
16002 );
16003 });
16004 code_action_requests.next().await;
16005 cx.run_until_parked();
16006 cx.condition(|editor, _| editor.context_menu_visible())
16007 .await;
16008 cx.update_editor(|editor, _, _| {
16009 assert!(
16010 !editor.hover_state.visible(),
16011 "Hover popover should be hidden when code action menu is shown"
16012 );
16013 // Hide code actions
16014 editor.context_menu.take();
16015 });
16016
16017 // Case 2: Test that code completions hide hover popover
16018 cx.dispatch_action(Hover);
16019 hover_requests.next().await;
16020 cx.condition(|editor, _| editor.hover_state.visible()).await;
16021 let counter = Arc::new(AtomicUsize::new(0));
16022 let mut completion_requests =
16023 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16024 let counter = counter.clone();
16025 async move {
16026 counter.fetch_add(1, atomic::Ordering::Release);
16027 Ok(Some(lsp::CompletionResponse::Array(vec![
16028 lsp::CompletionItem {
16029 label: "main".into(),
16030 kind: Some(lsp::CompletionItemKind::FUNCTION),
16031 detail: Some("() -> ()".to_string()),
16032 ..Default::default()
16033 },
16034 lsp::CompletionItem {
16035 label: "TestStruct".into(),
16036 kind: Some(lsp::CompletionItemKind::STRUCT),
16037 detail: Some("struct TestStruct".to_string()),
16038 ..Default::default()
16039 },
16040 ])))
16041 }
16042 });
16043 cx.update_editor(|editor, window, cx| {
16044 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
16045 });
16046 completion_requests.next().await;
16047 cx.condition(|editor, _| editor.context_menu_visible())
16048 .await;
16049 cx.update_editor(|editor, _, _| {
16050 assert!(
16051 !editor.hover_state.visible(),
16052 "Hover popover should be hidden when completion menu is shown"
16053 );
16054 });
16055}
16056
16057#[gpui::test]
16058async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
16059 init_test(cx, |_| {});
16060
16061 let mut cx = EditorLspTestContext::new_rust(
16062 lsp::ServerCapabilities {
16063 completion_provider: Some(lsp::CompletionOptions {
16064 trigger_characters: Some(vec![".".to_string()]),
16065 resolve_provider: Some(true),
16066 ..Default::default()
16067 }),
16068 ..Default::default()
16069 },
16070 cx,
16071 )
16072 .await;
16073
16074 cx.set_state("fn main() { let a = 2ˇ; }");
16075 cx.simulate_keystroke(".");
16076
16077 let unresolved_item_1 = lsp::CompletionItem {
16078 label: "id".to_string(),
16079 filter_text: Some("id".to_string()),
16080 detail: None,
16081 documentation: None,
16082 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16083 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16084 new_text: ".id".to_string(),
16085 })),
16086 ..lsp::CompletionItem::default()
16087 };
16088 let resolved_item_1 = lsp::CompletionItem {
16089 additional_text_edits: Some(vec![lsp::TextEdit {
16090 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
16091 new_text: "!!".to_string(),
16092 }]),
16093 ..unresolved_item_1.clone()
16094 };
16095 let unresolved_item_2 = lsp::CompletionItem {
16096 label: "other".to_string(),
16097 filter_text: Some("other".to_string()),
16098 detail: None,
16099 documentation: None,
16100 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16101 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16102 new_text: ".other".to_string(),
16103 })),
16104 ..lsp::CompletionItem::default()
16105 };
16106 let resolved_item_2 = lsp::CompletionItem {
16107 additional_text_edits: Some(vec![lsp::TextEdit {
16108 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
16109 new_text: "??".to_string(),
16110 }]),
16111 ..unresolved_item_2.clone()
16112 };
16113
16114 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
16115 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
16116 cx.lsp
16117 .server
16118 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
16119 let unresolved_item_1 = unresolved_item_1.clone();
16120 let resolved_item_1 = resolved_item_1.clone();
16121 let unresolved_item_2 = unresolved_item_2.clone();
16122 let resolved_item_2 = resolved_item_2.clone();
16123 let resolve_requests_1 = resolve_requests_1.clone();
16124 let resolve_requests_2 = resolve_requests_2.clone();
16125 move |unresolved_request, _| {
16126 let unresolved_item_1 = unresolved_item_1.clone();
16127 let resolved_item_1 = resolved_item_1.clone();
16128 let unresolved_item_2 = unresolved_item_2.clone();
16129 let resolved_item_2 = resolved_item_2.clone();
16130 let resolve_requests_1 = resolve_requests_1.clone();
16131 let resolve_requests_2 = resolve_requests_2.clone();
16132 async move {
16133 if unresolved_request == unresolved_item_1 {
16134 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
16135 Ok(resolved_item_1.clone())
16136 } else if unresolved_request == unresolved_item_2 {
16137 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
16138 Ok(resolved_item_2.clone())
16139 } else {
16140 panic!("Unexpected completion item {unresolved_request:?}")
16141 }
16142 }
16143 }
16144 })
16145 .detach();
16146
16147 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16148 let unresolved_item_1 = unresolved_item_1.clone();
16149 let unresolved_item_2 = unresolved_item_2.clone();
16150 async move {
16151 Ok(Some(lsp::CompletionResponse::Array(vec![
16152 unresolved_item_1,
16153 unresolved_item_2,
16154 ])))
16155 }
16156 })
16157 .next()
16158 .await;
16159
16160 cx.condition(|editor, _| editor.context_menu_visible())
16161 .await;
16162 cx.update_editor(|editor, _, _| {
16163 let context_menu = editor.context_menu.borrow_mut();
16164 let context_menu = context_menu
16165 .as_ref()
16166 .expect("Should have the context menu deployed");
16167 match context_menu {
16168 CodeContextMenu::Completions(completions_menu) => {
16169 let completions = completions_menu.completions.borrow_mut();
16170 assert_eq!(
16171 completions
16172 .iter()
16173 .map(|completion| &completion.label.text)
16174 .collect::<Vec<_>>(),
16175 vec!["id", "other"]
16176 )
16177 }
16178 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
16179 }
16180 });
16181 cx.run_until_parked();
16182
16183 cx.update_editor(|editor, window, cx| {
16184 editor.context_menu_next(&ContextMenuNext, window, cx);
16185 });
16186 cx.run_until_parked();
16187 cx.update_editor(|editor, window, cx| {
16188 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
16189 });
16190 cx.run_until_parked();
16191 cx.update_editor(|editor, window, cx| {
16192 editor.context_menu_next(&ContextMenuNext, window, cx);
16193 });
16194 cx.run_until_parked();
16195 cx.update_editor(|editor, window, cx| {
16196 editor
16197 .compose_completion(&ComposeCompletion::default(), window, cx)
16198 .expect("No task returned")
16199 })
16200 .await
16201 .expect("Completion failed");
16202 cx.run_until_parked();
16203
16204 cx.update_editor(|editor, _, cx| {
16205 assert_eq!(
16206 resolve_requests_1.load(atomic::Ordering::Acquire),
16207 1,
16208 "Should always resolve once despite multiple selections"
16209 );
16210 assert_eq!(
16211 resolve_requests_2.load(atomic::Ordering::Acquire),
16212 1,
16213 "Should always resolve once after multiple selections and applying the completion"
16214 );
16215 assert_eq!(
16216 editor.text(cx),
16217 "fn main() { let a = ??.other; }",
16218 "Should use resolved data when applying the completion"
16219 );
16220 });
16221}
16222
16223#[gpui::test]
16224async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
16225 init_test(cx, |_| {});
16226
16227 let item_0 = lsp::CompletionItem {
16228 label: "abs".into(),
16229 insert_text: Some("abs".into()),
16230 data: Some(json!({ "very": "special"})),
16231 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
16232 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
16233 lsp::InsertReplaceEdit {
16234 new_text: "abs".to_string(),
16235 insert: lsp::Range::default(),
16236 replace: lsp::Range::default(),
16237 },
16238 )),
16239 ..lsp::CompletionItem::default()
16240 };
16241 let items = iter::once(item_0.clone())
16242 .chain((11..51).map(|i| lsp::CompletionItem {
16243 label: format!("item_{}", i),
16244 insert_text: Some(format!("item_{}", i)),
16245 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
16246 ..lsp::CompletionItem::default()
16247 }))
16248 .collect::<Vec<_>>();
16249
16250 let default_commit_characters = vec!["?".to_string()];
16251 let default_data = json!({ "default": "data"});
16252 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
16253 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
16254 let default_edit_range = lsp::Range {
16255 start: lsp::Position {
16256 line: 0,
16257 character: 5,
16258 },
16259 end: lsp::Position {
16260 line: 0,
16261 character: 5,
16262 },
16263 };
16264
16265 let mut cx = EditorLspTestContext::new_rust(
16266 lsp::ServerCapabilities {
16267 completion_provider: Some(lsp::CompletionOptions {
16268 trigger_characters: Some(vec![".".to_string()]),
16269 resolve_provider: Some(true),
16270 ..Default::default()
16271 }),
16272 ..Default::default()
16273 },
16274 cx,
16275 )
16276 .await;
16277
16278 cx.set_state("fn main() { let a = 2ˇ; }");
16279 cx.simulate_keystroke(".");
16280
16281 let completion_data = default_data.clone();
16282 let completion_characters = default_commit_characters.clone();
16283 let completion_items = items.clone();
16284 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16285 let default_data = completion_data.clone();
16286 let default_commit_characters = completion_characters.clone();
16287 let items = completion_items.clone();
16288 async move {
16289 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16290 items,
16291 item_defaults: Some(lsp::CompletionListItemDefaults {
16292 data: Some(default_data.clone()),
16293 commit_characters: Some(default_commit_characters.clone()),
16294 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
16295 default_edit_range,
16296 )),
16297 insert_text_format: Some(default_insert_text_format),
16298 insert_text_mode: Some(default_insert_text_mode),
16299 }),
16300 ..lsp::CompletionList::default()
16301 })))
16302 }
16303 })
16304 .next()
16305 .await;
16306
16307 let resolved_items = Arc::new(Mutex::new(Vec::new()));
16308 cx.lsp
16309 .server
16310 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
16311 let closure_resolved_items = resolved_items.clone();
16312 move |item_to_resolve, _| {
16313 let closure_resolved_items = closure_resolved_items.clone();
16314 async move {
16315 closure_resolved_items.lock().push(item_to_resolve.clone());
16316 Ok(item_to_resolve)
16317 }
16318 }
16319 })
16320 .detach();
16321
16322 cx.condition(|editor, _| editor.context_menu_visible())
16323 .await;
16324 cx.run_until_parked();
16325 cx.update_editor(|editor, _, _| {
16326 let menu = editor.context_menu.borrow_mut();
16327 match menu.as_ref().expect("should have the completions menu") {
16328 CodeContextMenu::Completions(completions_menu) => {
16329 assert_eq!(
16330 completions_menu
16331 .entries
16332 .borrow()
16333 .iter()
16334 .map(|mat| mat.string.clone())
16335 .collect::<Vec<String>>(),
16336 items
16337 .iter()
16338 .map(|completion| completion.label.clone())
16339 .collect::<Vec<String>>()
16340 );
16341 }
16342 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
16343 }
16344 });
16345 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
16346 // with 4 from the end.
16347 assert_eq!(
16348 *resolved_items.lock(),
16349 [&items[0..16], &items[items.len() - 4..items.len()]]
16350 .concat()
16351 .iter()
16352 .cloned()
16353 .map(|mut item| {
16354 if item.data.is_none() {
16355 item.data = Some(default_data.clone());
16356 }
16357 item
16358 })
16359 .collect::<Vec<lsp::CompletionItem>>(),
16360 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
16361 );
16362 resolved_items.lock().clear();
16363
16364 cx.update_editor(|editor, window, cx| {
16365 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
16366 });
16367 cx.run_until_parked();
16368 // Completions that have already been resolved are skipped.
16369 assert_eq!(
16370 *resolved_items.lock(),
16371 items[items.len() - 17..items.len() - 4]
16372 .iter()
16373 .cloned()
16374 .map(|mut item| {
16375 if item.data.is_none() {
16376 item.data = Some(default_data.clone());
16377 }
16378 item
16379 })
16380 .collect::<Vec<lsp::CompletionItem>>()
16381 );
16382 resolved_items.lock().clear();
16383}
16384
16385#[gpui::test]
16386async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
16387 init_test(cx, |_| {});
16388
16389 let mut cx = EditorLspTestContext::new(
16390 Language::new(
16391 LanguageConfig {
16392 matcher: LanguageMatcher {
16393 path_suffixes: vec!["jsx".into()],
16394 ..Default::default()
16395 },
16396 overrides: [(
16397 "element".into(),
16398 LanguageConfigOverride {
16399 completion_query_characters: Override::Set(['-'].into_iter().collect()),
16400 ..Default::default()
16401 },
16402 )]
16403 .into_iter()
16404 .collect(),
16405 ..Default::default()
16406 },
16407 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16408 )
16409 .with_override_query("(jsx_self_closing_element) @element")
16410 .unwrap(),
16411 lsp::ServerCapabilities {
16412 completion_provider: Some(lsp::CompletionOptions {
16413 trigger_characters: Some(vec![":".to_string()]),
16414 ..Default::default()
16415 }),
16416 ..Default::default()
16417 },
16418 cx,
16419 )
16420 .await;
16421
16422 cx.lsp
16423 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16424 Ok(Some(lsp::CompletionResponse::Array(vec![
16425 lsp::CompletionItem {
16426 label: "bg-blue".into(),
16427 ..Default::default()
16428 },
16429 lsp::CompletionItem {
16430 label: "bg-red".into(),
16431 ..Default::default()
16432 },
16433 lsp::CompletionItem {
16434 label: "bg-yellow".into(),
16435 ..Default::default()
16436 },
16437 ])))
16438 });
16439
16440 cx.set_state(r#"<p class="bgˇ" />"#);
16441
16442 // Trigger completion when typing a dash, because the dash is an extra
16443 // word character in the 'element' scope, which contains the cursor.
16444 cx.simulate_keystroke("-");
16445 cx.executor().run_until_parked();
16446 cx.update_editor(|editor, _, _| {
16447 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16448 {
16449 assert_eq!(
16450 completion_menu_entries(&menu),
16451 &["bg-blue", "bg-red", "bg-yellow"]
16452 );
16453 } else {
16454 panic!("expected completion menu to be open");
16455 }
16456 });
16457
16458 cx.simulate_keystroke("l");
16459 cx.executor().run_until_parked();
16460 cx.update_editor(|editor, _, _| {
16461 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16462 {
16463 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
16464 } else {
16465 panic!("expected completion menu to be open");
16466 }
16467 });
16468
16469 // When filtering completions, consider the character after the '-' to
16470 // be the start of a subword.
16471 cx.set_state(r#"<p class="yelˇ" />"#);
16472 cx.simulate_keystroke("l");
16473 cx.executor().run_until_parked();
16474 cx.update_editor(|editor, _, _| {
16475 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16476 {
16477 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
16478 } else {
16479 panic!("expected completion menu to be open");
16480 }
16481 });
16482}
16483
16484fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
16485 let entries = menu.entries.borrow();
16486 entries.iter().map(|mat| mat.string.clone()).collect()
16487}
16488
16489#[gpui::test]
16490async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
16491 init_test(cx, |settings| {
16492 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
16493 Formatter::Prettier,
16494 )))
16495 });
16496
16497 let fs = FakeFs::new(cx.executor());
16498 fs.insert_file(path!("/file.ts"), Default::default()).await;
16499
16500 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
16501 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16502
16503 language_registry.add(Arc::new(Language::new(
16504 LanguageConfig {
16505 name: "TypeScript".into(),
16506 matcher: LanguageMatcher {
16507 path_suffixes: vec!["ts".to_string()],
16508 ..Default::default()
16509 },
16510 ..Default::default()
16511 },
16512 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
16513 )));
16514 update_test_language_settings(cx, |settings| {
16515 settings.defaults.prettier = Some(PrettierSettings {
16516 allowed: true,
16517 ..PrettierSettings::default()
16518 });
16519 });
16520
16521 let test_plugin = "test_plugin";
16522 let _ = language_registry.register_fake_lsp(
16523 "TypeScript",
16524 FakeLspAdapter {
16525 prettier_plugins: vec![test_plugin],
16526 ..Default::default()
16527 },
16528 );
16529
16530 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
16531 let buffer = project
16532 .update(cx, |project, cx| {
16533 project.open_local_buffer(path!("/file.ts"), cx)
16534 })
16535 .await
16536 .unwrap();
16537
16538 let buffer_text = "one\ntwo\nthree\n";
16539 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16540 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16541 editor.update_in(cx, |editor, window, cx| {
16542 editor.set_text(buffer_text, window, cx)
16543 });
16544
16545 editor
16546 .update_in(cx, |editor, window, cx| {
16547 editor.perform_format(
16548 project.clone(),
16549 FormatTrigger::Manual,
16550 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16551 window,
16552 cx,
16553 )
16554 })
16555 .unwrap()
16556 .await;
16557 assert_eq!(
16558 editor.update(cx, |editor, cx| editor.text(cx)),
16559 buffer_text.to_string() + prettier_format_suffix,
16560 "Test prettier formatting was not applied to the original buffer text",
16561 );
16562
16563 update_test_language_settings(cx, |settings| {
16564 settings.defaults.formatter = Some(SelectedFormatter::Auto)
16565 });
16566 let format = editor.update_in(cx, |editor, window, cx| {
16567 editor.perform_format(
16568 project.clone(),
16569 FormatTrigger::Manual,
16570 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16571 window,
16572 cx,
16573 )
16574 });
16575 format.await.unwrap();
16576 assert_eq!(
16577 editor.update(cx, |editor, cx| editor.text(cx)),
16578 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
16579 "Autoformatting (via test prettier) was not applied to the original buffer text",
16580 );
16581}
16582
16583#[gpui::test]
16584async fn test_addition_reverts(cx: &mut TestAppContext) {
16585 init_test(cx, |_| {});
16586 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16587 let base_text = indoc! {r#"
16588 struct Row;
16589 struct Row1;
16590 struct Row2;
16591
16592 struct Row4;
16593 struct Row5;
16594 struct Row6;
16595
16596 struct Row8;
16597 struct Row9;
16598 struct Row10;"#};
16599
16600 // When addition hunks are not adjacent to carets, no hunk revert is performed
16601 assert_hunk_revert(
16602 indoc! {r#"struct Row;
16603 struct Row1;
16604 struct Row1.1;
16605 struct Row1.2;
16606 struct Row2;ˇ
16607
16608 struct Row4;
16609 struct Row5;
16610 struct Row6;
16611
16612 struct Row8;
16613 ˇstruct Row9;
16614 struct Row9.1;
16615 struct Row9.2;
16616 struct Row9.3;
16617 struct Row10;"#},
16618 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16619 indoc! {r#"struct Row;
16620 struct Row1;
16621 struct Row1.1;
16622 struct Row1.2;
16623 struct Row2;ˇ
16624
16625 struct Row4;
16626 struct Row5;
16627 struct Row6;
16628
16629 struct Row8;
16630 ˇstruct Row9;
16631 struct Row9.1;
16632 struct Row9.2;
16633 struct Row9.3;
16634 struct Row10;"#},
16635 base_text,
16636 &mut cx,
16637 );
16638 // Same for selections
16639 assert_hunk_revert(
16640 indoc! {r#"struct Row;
16641 struct Row1;
16642 struct Row2;
16643 struct Row2.1;
16644 struct Row2.2;
16645 «ˇ
16646 struct Row4;
16647 struct» Row5;
16648 «struct Row6;
16649 ˇ»
16650 struct Row9.1;
16651 struct Row9.2;
16652 struct Row9.3;
16653 struct Row8;
16654 struct Row9;
16655 struct Row10;"#},
16656 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16657 indoc! {r#"struct Row;
16658 struct Row1;
16659 struct Row2;
16660 struct Row2.1;
16661 struct Row2.2;
16662 «ˇ
16663 struct Row4;
16664 struct» Row5;
16665 «struct Row6;
16666 ˇ»
16667 struct Row9.1;
16668 struct Row9.2;
16669 struct Row9.3;
16670 struct Row8;
16671 struct Row9;
16672 struct Row10;"#},
16673 base_text,
16674 &mut cx,
16675 );
16676
16677 // When carets and selections intersect the addition hunks, those are reverted.
16678 // Adjacent carets got merged.
16679 assert_hunk_revert(
16680 indoc! {r#"struct Row;
16681 ˇ// something on the top
16682 struct Row1;
16683 struct Row2;
16684 struct Roˇw3.1;
16685 struct Row2.2;
16686 struct Row2.3;ˇ
16687
16688 struct Row4;
16689 struct ˇRow5.1;
16690 struct Row5.2;
16691 struct «Rowˇ»5.3;
16692 struct Row5;
16693 struct Row6;
16694 ˇ
16695 struct Row9.1;
16696 struct «Rowˇ»9.2;
16697 struct «ˇRow»9.3;
16698 struct Row8;
16699 struct Row9;
16700 «ˇ// something on bottom»
16701 struct Row10;"#},
16702 vec![
16703 DiffHunkStatusKind::Added,
16704 DiffHunkStatusKind::Added,
16705 DiffHunkStatusKind::Added,
16706 DiffHunkStatusKind::Added,
16707 DiffHunkStatusKind::Added,
16708 ],
16709 indoc! {r#"struct Row;
16710 ˇstruct Row1;
16711 struct Row2;
16712 ˇ
16713 struct Row4;
16714 ˇstruct Row5;
16715 struct Row6;
16716 ˇ
16717 ˇstruct Row8;
16718 struct Row9;
16719 ˇstruct Row10;"#},
16720 base_text,
16721 &mut cx,
16722 );
16723}
16724
16725#[gpui::test]
16726async fn test_modification_reverts(cx: &mut TestAppContext) {
16727 init_test(cx, |_| {});
16728 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16729 let base_text = indoc! {r#"
16730 struct Row;
16731 struct Row1;
16732 struct Row2;
16733
16734 struct Row4;
16735 struct Row5;
16736 struct Row6;
16737
16738 struct Row8;
16739 struct Row9;
16740 struct Row10;"#};
16741
16742 // Modification hunks behave the same as the addition ones.
16743 assert_hunk_revert(
16744 indoc! {r#"struct Row;
16745 struct Row1;
16746 struct Row33;
16747 ˇ
16748 struct Row4;
16749 struct Row5;
16750 struct Row6;
16751 ˇ
16752 struct Row99;
16753 struct Row9;
16754 struct Row10;"#},
16755 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16756 indoc! {r#"struct Row;
16757 struct Row1;
16758 struct Row33;
16759 ˇ
16760 struct Row4;
16761 struct Row5;
16762 struct Row6;
16763 ˇ
16764 struct Row99;
16765 struct Row9;
16766 struct Row10;"#},
16767 base_text,
16768 &mut cx,
16769 );
16770 assert_hunk_revert(
16771 indoc! {r#"struct Row;
16772 struct Row1;
16773 struct Row33;
16774 «ˇ
16775 struct Row4;
16776 struct» Row5;
16777 «struct Row6;
16778 ˇ»
16779 struct Row99;
16780 struct Row9;
16781 struct Row10;"#},
16782 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16783 indoc! {r#"struct Row;
16784 struct Row1;
16785 struct Row33;
16786 «ˇ
16787 struct Row4;
16788 struct» Row5;
16789 «struct Row6;
16790 ˇ»
16791 struct Row99;
16792 struct Row9;
16793 struct Row10;"#},
16794 base_text,
16795 &mut cx,
16796 );
16797
16798 assert_hunk_revert(
16799 indoc! {r#"ˇstruct Row1.1;
16800 struct Row1;
16801 «ˇstr»uct Row22;
16802
16803 struct ˇRow44;
16804 struct Row5;
16805 struct «Rˇ»ow66;ˇ
16806
16807 «struˇ»ct Row88;
16808 struct Row9;
16809 struct Row1011;ˇ"#},
16810 vec![
16811 DiffHunkStatusKind::Modified,
16812 DiffHunkStatusKind::Modified,
16813 DiffHunkStatusKind::Modified,
16814 DiffHunkStatusKind::Modified,
16815 DiffHunkStatusKind::Modified,
16816 DiffHunkStatusKind::Modified,
16817 ],
16818 indoc! {r#"struct Row;
16819 ˇstruct Row1;
16820 struct Row2;
16821 ˇ
16822 struct Row4;
16823 ˇstruct Row5;
16824 struct Row6;
16825 ˇ
16826 struct Row8;
16827 ˇstruct Row9;
16828 struct Row10;ˇ"#},
16829 base_text,
16830 &mut cx,
16831 );
16832}
16833
16834#[gpui::test]
16835async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
16836 init_test(cx, |_| {});
16837 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16838 let base_text = indoc! {r#"
16839 one
16840
16841 two
16842 three
16843 "#};
16844
16845 cx.set_head_text(base_text);
16846 cx.set_state("\nˇ\n");
16847 cx.executor().run_until_parked();
16848 cx.update_editor(|editor, _window, cx| {
16849 editor.expand_selected_diff_hunks(cx);
16850 });
16851 cx.executor().run_until_parked();
16852 cx.update_editor(|editor, window, cx| {
16853 editor.backspace(&Default::default(), window, cx);
16854 });
16855 cx.run_until_parked();
16856 cx.assert_state_with_diff(
16857 indoc! {r#"
16858
16859 - two
16860 - threeˇ
16861 +
16862 "#}
16863 .to_string(),
16864 );
16865}
16866
16867#[gpui::test]
16868async fn test_deletion_reverts(cx: &mut TestAppContext) {
16869 init_test(cx, |_| {});
16870 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16871 let base_text = indoc! {r#"struct Row;
16872struct Row1;
16873struct Row2;
16874
16875struct Row4;
16876struct Row5;
16877struct Row6;
16878
16879struct Row8;
16880struct Row9;
16881struct Row10;"#};
16882
16883 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
16884 assert_hunk_revert(
16885 indoc! {r#"struct Row;
16886 struct Row2;
16887
16888 ˇstruct Row4;
16889 struct Row5;
16890 struct Row6;
16891 ˇ
16892 struct Row8;
16893 struct Row10;"#},
16894 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16895 indoc! {r#"struct Row;
16896 struct Row2;
16897
16898 ˇstruct Row4;
16899 struct Row5;
16900 struct Row6;
16901 ˇ
16902 struct Row8;
16903 struct Row10;"#},
16904 base_text,
16905 &mut cx,
16906 );
16907 assert_hunk_revert(
16908 indoc! {r#"struct Row;
16909 struct Row2;
16910
16911 «ˇstruct Row4;
16912 struct» Row5;
16913 «struct Row6;
16914 ˇ»
16915 struct Row8;
16916 struct Row10;"#},
16917 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16918 indoc! {r#"struct Row;
16919 struct Row2;
16920
16921 «ˇstruct Row4;
16922 struct» Row5;
16923 «struct Row6;
16924 ˇ»
16925 struct Row8;
16926 struct Row10;"#},
16927 base_text,
16928 &mut cx,
16929 );
16930
16931 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
16932 assert_hunk_revert(
16933 indoc! {r#"struct Row;
16934 ˇstruct Row2;
16935
16936 struct Row4;
16937 struct Row5;
16938 struct Row6;
16939
16940 struct Row8;ˇ
16941 struct Row10;"#},
16942 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16943 indoc! {r#"struct Row;
16944 struct Row1;
16945 ˇstruct Row2;
16946
16947 struct Row4;
16948 struct Row5;
16949 struct Row6;
16950
16951 struct Row8;ˇ
16952 struct Row9;
16953 struct Row10;"#},
16954 base_text,
16955 &mut cx,
16956 );
16957 assert_hunk_revert(
16958 indoc! {r#"struct Row;
16959 struct Row2«ˇ;
16960 struct Row4;
16961 struct» Row5;
16962 «struct Row6;
16963
16964 struct Row8;ˇ»
16965 struct Row10;"#},
16966 vec![
16967 DiffHunkStatusKind::Deleted,
16968 DiffHunkStatusKind::Deleted,
16969 DiffHunkStatusKind::Deleted,
16970 ],
16971 indoc! {r#"struct Row;
16972 struct Row1;
16973 struct Row2«ˇ;
16974
16975 struct Row4;
16976 struct» Row5;
16977 «struct Row6;
16978
16979 struct Row8;ˇ»
16980 struct Row9;
16981 struct Row10;"#},
16982 base_text,
16983 &mut cx,
16984 );
16985}
16986
16987#[gpui::test]
16988async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
16989 init_test(cx, |_| {});
16990
16991 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
16992 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
16993 let base_text_3 =
16994 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
16995
16996 let text_1 = edit_first_char_of_every_line(base_text_1);
16997 let text_2 = edit_first_char_of_every_line(base_text_2);
16998 let text_3 = edit_first_char_of_every_line(base_text_3);
16999
17000 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
17001 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
17002 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
17003
17004 let multibuffer = cx.new(|cx| {
17005 let mut multibuffer = MultiBuffer::new(ReadWrite);
17006 multibuffer.push_excerpts(
17007 buffer_1.clone(),
17008 [
17009 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17010 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17011 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17012 ],
17013 cx,
17014 );
17015 multibuffer.push_excerpts(
17016 buffer_2.clone(),
17017 [
17018 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17019 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17020 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17021 ],
17022 cx,
17023 );
17024 multibuffer.push_excerpts(
17025 buffer_3.clone(),
17026 [
17027 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17028 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17029 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17030 ],
17031 cx,
17032 );
17033 multibuffer
17034 });
17035
17036 let fs = FakeFs::new(cx.executor());
17037 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
17038 let (editor, cx) = cx
17039 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
17040 editor.update_in(cx, |editor, _window, cx| {
17041 for (buffer, diff_base) in [
17042 (buffer_1.clone(), base_text_1),
17043 (buffer_2.clone(), base_text_2),
17044 (buffer_3.clone(), base_text_3),
17045 ] {
17046 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
17047 editor
17048 .buffer
17049 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
17050 }
17051 });
17052 cx.executor().run_until_parked();
17053
17054 editor.update_in(cx, |editor, window, cx| {
17055 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}");
17056 editor.select_all(&SelectAll, window, cx);
17057 editor.git_restore(&Default::default(), window, cx);
17058 });
17059 cx.executor().run_until_parked();
17060
17061 // When all ranges are selected, all buffer hunks are reverted.
17062 editor.update(cx, |editor, cx| {
17063 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");
17064 });
17065 buffer_1.update(cx, |buffer, _| {
17066 assert_eq!(buffer.text(), base_text_1);
17067 });
17068 buffer_2.update(cx, |buffer, _| {
17069 assert_eq!(buffer.text(), base_text_2);
17070 });
17071 buffer_3.update(cx, |buffer, _| {
17072 assert_eq!(buffer.text(), base_text_3);
17073 });
17074
17075 editor.update_in(cx, |editor, window, cx| {
17076 editor.undo(&Default::default(), window, cx);
17077 });
17078
17079 editor.update_in(cx, |editor, window, cx| {
17080 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17081 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
17082 });
17083 editor.git_restore(&Default::default(), window, cx);
17084 });
17085
17086 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
17087 // but not affect buffer_2 and its related excerpts.
17088 editor.update(cx, |editor, cx| {
17089 assert_eq!(
17090 editor.text(cx),
17091 "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}"
17092 );
17093 });
17094 buffer_1.update(cx, |buffer, _| {
17095 assert_eq!(buffer.text(), base_text_1);
17096 });
17097 buffer_2.update(cx, |buffer, _| {
17098 assert_eq!(
17099 buffer.text(),
17100 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
17101 );
17102 });
17103 buffer_3.update(cx, |buffer, _| {
17104 assert_eq!(
17105 buffer.text(),
17106 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
17107 );
17108 });
17109
17110 fn edit_first_char_of_every_line(text: &str) -> String {
17111 text.split('\n')
17112 .map(|line| format!("X{}", &line[1..]))
17113 .collect::<Vec<_>>()
17114 .join("\n")
17115 }
17116}
17117
17118#[gpui::test]
17119async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
17120 init_test(cx, |_| {});
17121
17122 let cols = 4;
17123 let rows = 10;
17124 let sample_text_1 = sample_text(rows, cols, 'a');
17125 assert_eq!(
17126 sample_text_1,
17127 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
17128 );
17129 let sample_text_2 = sample_text(rows, cols, 'l');
17130 assert_eq!(
17131 sample_text_2,
17132 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
17133 );
17134 let sample_text_3 = sample_text(rows, cols, 'v');
17135 assert_eq!(
17136 sample_text_3,
17137 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
17138 );
17139
17140 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
17141 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
17142 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
17143
17144 let multi_buffer = cx.new(|cx| {
17145 let mut multibuffer = MultiBuffer::new(ReadWrite);
17146 multibuffer.push_excerpts(
17147 buffer_1.clone(),
17148 [
17149 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17150 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17151 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17152 ],
17153 cx,
17154 );
17155 multibuffer.push_excerpts(
17156 buffer_2.clone(),
17157 [
17158 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17159 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17160 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17161 ],
17162 cx,
17163 );
17164 multibuffer.push_excerpts(
17165 buffer_3.clone(),
17166 [
17167 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17168 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17169 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17170 ],
17171 cx,
17172 );
17173 multibuffer
17174 });
17175
17176 let fs = FakeFs::new(cx.executor());
17177 fs.insert_tree(
17178 "/a",
17179 json!({
17180 "main.rs": sample_text_1,
17181 "other.rs": sample_text_2,
17182 "lib.rs": sample_text_3,
17183 }),
17184 )
17185 .await;
17186 let project = Project::test(fs, ["/a".as_ref()], cx).await;
17187 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17188 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17189 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17190 Editor::new(
17191 EditorMode::full(),
17192 multi_buffer,
17193 Some(project.clone()),
17194 window,
17195 cx,
17196 )
17197 });
17198 let multibuffer_item_id = workspace
17199 .update(cx, |workspace, window, cx| {
17200 assert!(
17201 workspace.active_item(cx).is_none(),
17202 "active item should be None before the first item is added"
17203 );
17204 workspace.add_item_to_active_pane(
17205 Box::new(multi_buffer_editor.clone()),
17206 None,
17207 true,
17208 window,
17209 cx,
17210 );
17211 let active_item = workspace
17212 .active_item(cx)
17213 .expect("should have an active item after adding the multi buffer");
17214 assert!(
17215 !active_item.is_singleton(cx),
17216 "A multi buffer was expected to active after adding"
17217 );
17218 active_item.item_id()
17219 })
17220 .unwrap();
17221 cx.executor().run_until_parked();
17222
17223 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17224 editor.change_selections(
17225 SelectionEffects::scroll(Autoscroll::Next),
17226 window,
17227 cx,
17228 |s| s.select_ranges(Some(1..2)),
17229 );
17230 editor.open_excerpts(&OpenExcerpts, window, cx);
17231 });
17232 cx.executor().run_until_parked();
17233 let first_item_id = workspace
17234 .update(cx, |workspace, window, cx| {
17235 let active_item = workspace
17236 .active_item(cx)
17237 .expect("should have an active item after navigating into the 1st buffer");
17238 let first_item_id = active_item.item_id();
17239 assert_ne!(
17240 first_item_id, multibuffer_item_id,
17241 "Should navigate into the 1st buffer and activate it"
17242 );
17243 assert!(
17244 active_item.is_singleton(cx),
17245 "New active item should be a singleton buffer"
17246 );
17247 assert_eq!(
17248 active_item
17249 .act_as::<Editor>(cx)
17250 .expect("should have navigated into an editor for the 1st buffer")
17251 .read(cx)
17252 .text(cx),
17253 sample_text_1
17254 );
17255
17256 workspace
17257 .go_back(workspace.active_pane().downgrade(), window, cx)
17258 .detach_and_log_err(cx);
17259
17260 first_item_id
17261 })
17262 .unwrap();
17263 cx.executor().run_until_parked();
17264 workspace
17265 .update(cx, |workspace, _, cx| {
17266 let active_item = workspace
17267 .active_item(cx)
17268 .expect("should have an active item after navigating back");
17269 assert_eq!(
17270 active_item.item_id(),
17271 multibuffer_item_id,
17272 "Should navigate back to the multi buffer"
17273 );
17274 assert!(!active_item.is_singleton(cx));
17275 })
17276 .unwrap();
17277
17278 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17279 editor.change_selections(
17280 SelectionEffects::scroll(Autoscroll::Next),
17281 window,
17282 cx,
17283 |s| s.select_ranges(Some(39..40)),
17284 );
17285 editor.open_excerpts(&OpenExcerpts, window, cx);
17286 });
17287 cx.executor().run_until_parked();
17288 let second_item_id = workspace
17289 .update(cx, |workspace, window, cx| {
17290 let active_item = workspace
17291 .active_item(cx)
17292 .expect("should have an active item after navigating into the 2nd buffer");
17293 let second_item_id = active_item.item_id();
17294 assert_ne!(
17295 second_item_id, multibuffer_item_id,
17296 "Should navigate away from the multibuffer"
17297 );
17298 assert_ne!(
17299 second_item_id, first_item_id,
17300 "Should navigate into the 2nd buffer and activate it"
17301 );
17302 assert!(
17303 active_item.is_singleton(cx),
17304 "New active item should be a singleton buffer"
17305 );
17306 assert_eq!(
17307 active_item
17308 .act_as::<Editor>(cx)
17309 .expect("should have navigated into an editor")
17310 .read(cx)
17311 .text(cx),
17312 sample_text_2
17313 );
17314
17315 workspace
17316 .go_back(workspace.active_pane().downgrade(), window, cx)
17317 .detach_and_log_err(cx);
17318
17319 second_item_id
17320 })
17321 .unwrap();
17322 cx.executor().run_until_parked();
17323 workspace
17324 .update(cx, |workspace, _, cx| {
17325 let active_item = workspace
17326 .active_item(cx)
17327 .expect("should have an active item after navigating back from the 2nd buffer");
17328 assert_eq!(
17329 active_item.item_id(),
17330 multibuffer_item_id,
17331 "Should navigate back from the 2nd buffer to the multi buffer"
17332 );
17333 assert!(!active_item.is_singleton(cx));
17334 })
17335 .unwrap();
17336
17337 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17338 editor.change_selections(
17339 SelectionEffects::scroll(Autoscroll::Next),
17340 window,
17341 cx,
17342 |s| s.select_ranges(Some(70..70)),
17343 );
17344 editor.open_excerpts(&OpenExcerpts, window, cx);
17345 });
17346 cx.executor().run_until_parked();
17347 workspace
17348 .update(cx, |workspace, window, cx| {
17349 let active_item = workspace
17350 .active_item(cx)
17351 .expect("should have an active item after navigating into the 3rd buffer");
17352 let third_item_id = active_item.item_id();
17353 assert_ne!(
17354 third_item_id, multibuffer_item_id,
17355 "Should navigate into the 3rd buffer and activate it"
17356 );
17357 assert_ne!(third_item_id, first_item_id);
17358 assert_ne!(third_item_id, second_item_id);
17359 assert!(
17360 active_item.is_singleton(cx),
17361 "New active item should be a singleton buffer"
17362 );
17363 assert_eq!(
17364 active_item
17365 .act_as::<Editor>(cx)
17366 .expect("should have navigated into an editor")
17367 .read(cx)
17368 .text(cx),
17369 sample_text_3
17370 );
17371
17372 workspace
17373 .go_back(workspace.active_pane().downgrade(), window, cx)
17374 .detach_and_log_err(cx);
17375 })
17376 .unwrap();
17377 cx.executor().run_until_parked();
17378 workspace
17379 .update(cx, |workspace, _, cx| {
17380 let active_item = workspace
17381 .active_item(cx)
17382 .expect("should have an active item after navigating back from the 3rd buffer");
17383 assert_eq!(
17384 active_item.item_id(),
17385 multibuffer_item_id,
17386 "Should navigate back from the 3rd buffer to the multi buffer"
17387 );
17388 assert!(!active_item.is_singleton(cx));
17389 })
17390 .unwrap();
17391}
17392
17393#[gpui::test]
17394async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17395 init_test(cx, |_| {});
17396
17397 let mut cx = EditorTestContext::new(cx).await;
17398
17399 let diff_base = r#"
17400 use some::mod;
17401
17402 const A: u32 = 42;
17403
17404 fn main() {
17405 println!("hello");
17406
17407 println!("world");
17408 }
17409 "#
17410 .unindent();
17411
17412 cx.set_state(
17413 &r#"
17414 use some::modified;
17415
17416 ˇ
17417 fn main() {
17418 println!("hello there");
17419
17420 println!("around the");
17421 println!("world");
17422 }
17423 "#
17424 .unindent(),
17425 );
17426
17427 cx.set_head_text(&diff_base);
17428 executor.run_until_parked();
17429
17430 cx.update_editor(|editor, window, cx| {
17431 editor.go_to_next_hunk(&GoToHunk, window, cx);
17432 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17433 });
17434 executor.run_until_parked();
17435 cx.assert_state_with_diff(
17436 r#"
17437 use some::modified;
17438
17439
17440 fn main() {
17441 - println!("hello");
17442 + ˇ println!("hello there");
17443
17444 println!("around the");
17445 println!("world");
17446 }
17447 "#
17448 .unindent(),
17449 );
17450
17451 cx.update_editor(|editor, window, cx| {
17452 for _ in 0..2 {
17453 editor.go_to_next_hunk(&GoToHunk, window, cx);
17454 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17455 }
17456 });
17457 executor.run_until_parked();
17458 cx.assert_state_with_diff(
17459 r#"
17460 - use some::mod;
17461 + ˇuse some::modified;
17462
17463
17464 fn main() {
17465 - println!("hello");
17466 + println!("hello there");
17467
17468 + println!("around the");
17469 println!("world");
17470 }
17471 "#
17472 .unindent(),
17473 );
17474
17475 cx.update_editor(|editor, window, cx| {
17476 editor.go_to_next_hunk(&GoToHunk, window, cx);
17477 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17478 });
17479 executor.run_until_parked();
17480 cx.assert_state_with_diff(
17481 r#"
17482 - use some::mod;
17483 + use some::modified;
17484
17485 - const A: u32 = 42;
17486 ˇ
17487 fn main() {
17488 - println!("hello");
17489 + println!("hello there");
17490
17491 + println!("around the");
17492 println!("world");
17493 }
17494 "#
17495 .unindent(),
17496 );
17497
17498 cx.update_editor(|editor, window, cx| {
17499 editor.cancel(&Cancel, window, cx);
17500 });
17501
17502 cx.assert_state_with_diff(
17503 r#"
17504 use some::modified;
17505
17506 ˇ
17507 fn main() {
17508 println!("hello there");
17509
17510 println!("around the");
17511 println!("world");
17512 }
17513 "#
17514 .unindent(),
17515 );
17516}
17517
17518#[gpui::test]
17519async fn test_diff_base_change_with_expanded_diff_hunks(
17520 executor: BackgroundExecutor,
17521 cx: &mut TestAppContext,
17522) {
17523 init_test(cx, |_| {});
17524
17525 let mut cx = EditorTestContext::new(cx).await;
17526
17527 let diff_base = r#"
17528 use some::mod1;
17529 use some::mod2;
17530
17531 const A: u32 = 42;
17532 const B: u32 = 42;
17533 const C: u32 = 42;
17534
17535 fn main() {
17536 println!("hello");
17537
17538 println!("world");
17539 }
17540 "#
17541 .unindent();
17542
17543 cx.set_state(
17544 &r#"
17545 use some::mod2;
17546
17547 const A: u32 = 42;
17548 const C: u32 = 42;
17549
17550 fn main(ˇ) {
17551 //println!("hello");
17552
17553 println!("world");
17554 //
17555 //
17556 }
17557 "#
17558 .unindent(),
17559 );
17560
17561 cx.set_head_text(&diff_base);
17562 executor.run_until_parked();
17563
17564 cx.update_editor(|editor, window, cx| {
17565 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17566 });
17567 executor.run_until_parked();
17568 cx.assert_state_with_diff(
17569 r#"
17570 - use some::mod1;
17571 use some::mod2;
17572
17573 const A: u32 = 42;
17574 - const B: u32 = 42;
17575 const C: u32 = 42;
17576
17577 fn main(ˇ) {
17578 - println!("hello");
17579 + //println!("hello");
17580
17581 println!("world");
17582 + //
17583 + //
17584 }
17585 "#
17586 .unindent(),
17587 );
17588
17589 cx.set_head_text("new diff base!");
17590 executor.run_until_parked();
17591 cx.assert_state_with_diff(
17592 r#"
17593 - new diff base!
17594 + use some::mod2;
17595 +
17596 + const A: u32 = 42;
17597 + const C: u32 = 42;
17598 +
17599 + fn main(ˇ) {
17600 + //println!("hello");
17601 +
17602 + println!("world");
17603 + //
17604 + //
17605 + }
17606 "#
17607 .unindent(),
17608 );
17609}
17610
17611#[gpui::test]
17612async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
17613 init_test(cx, |_| {});
17614
17615 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17616 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17617 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17618 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17619 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
17620 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
17621
17622 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
17623 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
17624 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
17625
17626 let multi_buffer = cx.new(|cx| {
17627 let mut multibuffer = MultiBuffer::new(ReadWrite);
17628 multibuffer.push_excerpts(
17629 buffer_1.clone(),
17630 [
17631 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17632 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17633 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17634 ],
17635 cx,
17636 );
17637 multibuffer.push_excerpts(
17638 buffer_2.clone(),
17639 [
17640 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17641 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17642 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17643 ],
17644 cx,
17645 );
17646 multibuffer.push_excerpts(
17647 buffer_3.clone(),
17648 [
17649 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17650 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17651 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17652 ],
17653 cx,
17654 );
17655 multibuffer
17656 });
17657
17658 let editor =
17659 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17660 editor
17661 .update(cx, |editor, _window, cx| {
17662 for (buffer, diff_base) in [
17663 (buffer_1.clone(), file_1_old),
17664 (buffer_2.clone(), file_2_old),
17665 (buffer_3.clone(), file_3_old),
17666 ] {
17667 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
17668 editor
17669 .buffer
17670 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
17671 }
17672 })
17673 .unwrap();
17674
17675 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17676 cx.run_until_parked();
17677
17678 cx.assert_editor_state(
17679 &"
17680 ˇaaa
17681 ccc
17682 ddd
17683
17684 ggg
17685 hhh
17686
17687
17688 lll
17689 mmm
17690 NNN
17691
17692 qqq
17693 rrr
17694
17695 uuu
17696 111
17697 222
17698 333
17699
17700 666
17701 777
17702
17703 000
17704 !!!"
17705 .unindent(),
17706 );
17707
17708 cx.update_editor(|editor, window, cx| {
17709 editor.select_all(&SelectAll, window, cx);
17710 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17711 });
17712 cx.executor().run_until_parked();
17713
17714 cx.assert_state_with_diff(
17715 "
17716 «aaa
17717 - bbb
17718 ccc
17719 ddd
17720
17721 ggg
17722 hhh
17723
17724
17725 lll
17726 mmm
17727 - nnn
17728 + NNN
17729
17730 qqq
17731 rrr
17732
17733 uuu
17734 111
17735 222
17736 333
17737
17738 + 666
17739 777
17740
17741 000
17742 !!!ˇ»"
17743 .unindent(),
17744 );
17745}
17746
17747#[gpui::test]
17748async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
17749 init_test(cx, |_| {});
17750
17751 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
17752 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
17753
17754 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
17755 let multi_buffer = cx.new(|cx| {
17756 let mut multibuffer = MultiBuffer::new(ReadWrite);
17757 multibuffer.push_excerpts(
17758 buffer.clone(),
17759 [
17760 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
17761 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
17762 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
17763 ],
17764 cx,
17765 );
17766 multibuffer
17767 });
17768
17769 let editor =
17770 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17771 editor
17772 .update(cx, |editor, _window, cx| {
17773 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
17774 editor
17775 .buffer
17776 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
17777 })
17778 .unwrap();
17779
17780 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17781 cx.run_until_parked();
17782
17783 cx.update_editor(|editor, window, cx| {
17784 editor.expand_all_diff_hunks(&Default::default(), window, cx)
17785 });
17786 cx.executor().run_until_parked();
17787
17788 // When the start of a hunk coincides with the start of its excerpt,
17789 // the hunk is expanded. When the start of a a hunk is earlier than
17790 // the start of its excerpt, the hunk is not expanded.
17791 cx.assert_state_with_diff(
17792 "
17793 ˇaaa
17794 - bbb
17795 + BBB
17796
17797 - ddd
17798 - eee
17799 + DDD
17800 + EEE
17801 fff
17802
17803 iii
17804 "
17805 .unindent(),
17806 );
17807}
17808
17809#[gpui::test]
17810async fn test_edits_around_expanded_insertion_hunks(
17811 executor: BackgroundExecutor,
17812 cx: &mut TestAppContext,
17813) {
17814 init_test(cx, |_| {});
17815
17816 let mut cx = EditorTestContext::new(cx).await;
17817
17818 let diff_base = r#"
17819 use some::mod1;
17820 use some::mod2;
17821
17822 const A: u32 = 42;
17823
17824 fn main() {
17825 println!("hello");
17826
17827 println!("world");
17828 }
17829 "#
17830 .unindent();
17831 executor.run_until_parked();
17832 cx.set_state(
17833 &r#"
17834 use some::mod1;
17835 use some::mod2;
17836
17837 const A: u32 = 42;
17838 const B: u32 = 42;
17839 const C: u32 = 42;
17840 ˇ
17841
17842 fn main() {
17843 println!("hello");
17844
17845 println!("world");
17846 }
17847 "#
17848 .unindent(),
17849 );
17850
17851 cx.set_head_text(&diff_base);
17852 executor.run_until_parked();
17853
17854 cx.update_editor(|editor, window, cx| {
17855 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17856 });
17857 executor.run_until_parked();
17858
17859 cx.assert_state_with_diff(
17860 r#"
17861 use some::mod1;
17862 use some::mod2;
17863
17864 const A: u32 = 42;
17865 + const B: u32 = 42;
17866 + const C: u32 = 42;
17867 + ˇ
17868
17869 fn main() {
17870 println!("hello");
17871
17872 println!("world");
17873 }
17874 "#
17875 .unindent(),
17876 );
17877
17878 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
17879 executor.run_until_parked();
17880
17881 cx.assert_state_with_diff(
17882 r#"
17883 use some::mod1;
17884 use some::mod2;
17885
17886 const A: u32 = 42;
17887 + const B: u32 = 42;
17888 + const C: u32 = 42;
17889 + const D: u32 = 42;
17890 + ˇ
17891
17892 fn main() {
17893 println!("hello");
17894
17895 println!("world");
17896 }
17897 "#
17898 .unindent(),
17899 );
17900
17901 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
17902 executor.run_until_parked();
17903
17904 cx.assert_state_with_diff(
17905 r#"
17906 use some::mod1;
17907 use some::mod2;
17908
17909 const A: u32 = 42;
17910 + const B: u32 = 42;
17911 + const C: u32 = 42;
17912 + const D: u32 = 42;
17913 + const E: u32 = 42;
17914 + ˇ
17915
17916 fn main() {
17917 println!("hello");
17918
17919 println!("world");
17920 }
17921 "#
17922 .unindent(),
17923 );
17924
17925 cx.update_editor(|editor, window, cx| {
17926 editor.delete_line(&DeleteLine, window, cx);
17927 });
17928 executor.run_until_parked();
17929
17930 cx.assert_state_with_diff(
17931 r#"
17932 use some::mod1;
17933 use some::mod2;
17934
17935 const A: u32 = 42;
17936 + const B: u32 = 42;
17937 + const C: u32 = 42;
17938 + const D: u32 = 42;
17939 + const E: u32 = 42;
17940 ˇ
17941 fn main() {
17942 println!("hello");
17943
17944 println!("world");
17945 }
17946 "#
17947 .unindent(),
17948 );
17949
17950 cx.update_editor(|editor, window, cx| {
17951 editor.move_up(&MoveUp, window, cx);
17952 editor.delete_line(&DeleteLine, window, cx);
17953 editor.move_up(&MoveUp, window, cx);
17954 editor.delete_line(&DeleteLine, window, cx);
17955 editor.move_up(&MoveUp, window, cx);
17956 editor.delete_line(&DeleteLine, window, cx);
17957 });
17958 executor.run_until_parked();
17959 cx.assert_state_with_diff(
17960 r#"
17961 use some::mod1;
17962 use some::mod2;
17963
17964 const A: u32 = 42;
17965 + const B: u32 = 42;
17966 ˇ
17967 fn main() {
17968 println!("hello");
17969
17970 println!("world");
17971 }
17972 "#
17973 .unindent(),
17974 );
17975
17976 cx.update_editor(|editor, window, cx| {
17977 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
17978 editor.delete_line(&DeleteLine, window, cx);
17979 });
17980 executor.run_until_parked();
17981 cx.assert_state_with_diff(
17982 r#"
17983 ˇ
17984 fn main() {
17985 println!("hello");
17986
17987 println!("world");
17988 }
17989 "#
17990 .unindent(),
17991 );
17992}
17993
17994#[gpui::test]
17995async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
17996 init_test(cx, |_| {});
17997
17998 let mut cx = EditorTestContext::new(cx).await;
17999 cx.set_head_text(indoc! { "
18000 one
18001 two
18002 three
18003 four
18004 five
18005 "
18006 });
18007 cx.set_state(indoc! { "
18008 one
18009 ˇthree
18010 five
18011 "});
18012 cx.run_until_parked();
18013 cx.update_editor(|editor, window, cx| {
18014 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18015 });
18016 cx.assert_state_with_diff(
18017 indoc! { "
18018 one
18019 - two
18020 ˇthree
18021 - four
18022 five
18023 "}
18024 .to_string(),
18025 );
18026 cx.update_editor(|editor, window, cx| {
18027 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18028 });
18029
18030 cx.assert_state_with_diff(
18031 indoc! { "
18032 one
18033 ˇthree
18034 five
18035 "}
18036 .to_string(),
18037 );
18038
18039 cx.set_state(indoc! { "
18040 one
18041 ˇTWO
18042 three
18043 four
18044 five
18045 "});
18046 cx.run_until_parked();
18047 cx.update_editor(|editor, window, cx| {
18048 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18049 });
18050
18051 cx.assert_state_with_diff(
18052 indoc! { "
18053 one
18054 - two
18055 + ˇTWO
18056 three
18057 four
18058 five
18059 "}
18060 .to_string(),
18061 );
18062 cx.update_editor(|editor, window, cx| {
18063 editor.move_up(&Default::default(), window, cx);
18064 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
18065 });
18066 cx.assert_state_with_diff(
18067 indoc! { "
18068 one
18069 ˇTWO
18070 three
18071 four
18072 five
18073 "}
18074 .to_string(),
18075 );
18076}
18077
18078#[gpui::test]
18079async fn test_edits_around_expanded_deletion_hunks(
18080 executor: BackgroundExecutor,
18081 cx: &mut TestAppContext,
18082) {
18083 init_test(cx, |_| {});
18084
18085 let mut cx = EditorTestContext::new(cx).await;
18086
18087 let diff_base = r#"
18088 use some::mod1;
18089 use some::mod2;
18090
18091 const A: u32 = 42;
18092 const B: u32 = 42;
18093 const C: u32 = 42;
18094
18095
18096 fn main() {
18097 println!("hello");
18098
18099 println!("world");
18100 }
18101 "#
18102 .unindent();
18103 executor.run_until_parked();
18104 cx.set_state(
18105 &r#"
18106 use some::mod1;
18107 use some::mod2;
18108
18109 ˇconst B: u32 = 42;
18110 const C: u32 = 42;
18111
18112
18113 fn main() {
18114 println!("hello");
18115
18116 println!("world");
18117 }
18118 "#
18119 .unindent(),
18120 );
18121
18122 cx.set_head_text(&diff_base);
18123 executor.run_until_parked();
18124
18125 cx.update_editor(|editor, window, cx| {
18126 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18127 });
18128 executor.run_until_parked();
18129
18130 cx.assert_state_with_diff(
18131 r#"
18132 use some::mod1;
18133 use some::mod2;
18134
18135 - const A: u32 = 42;
18136 ˇconst B: u32 = 42;
18137 const C: u32 = 42;
18138
18139
18140 fn main() {
18141 println!("hello");
18142
18143 println!("world");
18144 }
18145 "#
18146 .unindent(),
18147 );
18148
18149 cx.update_editor(|editor, window, cx| {
18150 editor.delete_line(&DeleteLine, window, cx);
18151 });
18152 executor.run_until_parked();
18153 cx.assert_state_with_diff(
18154 r#"
18155 use some::mod1;
18156 use some::mod2;
18157
18158 - const A: u32 = 42;
18159 - const B: u32 = 42;
18160 ˇconst C: u32 = 42;
18161
18162
18163 fn main() {
18164 println!("hello");
18165
18166 println!("world");
18167 }
18168 "#
18169 .unindent(),
18170 );
18171
18172 cx.update_editor(|editor, window, cx| {
18173 editor.delete_line(&DeleteLine, window, cx);
18174 });
18175 executor.run_until_parked();
18176 cx.assert_state_with_diff(
18177 r#"
18178 use some::mod1;
18179 use some::mod2;
18180
18181 - const A: u32 = 42;
18182 - const B: u32 = 42;
18183 - const C: u32 = 42;
18184 ˇ
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.handle_input("replacement", window, cx);
18197 });
18198 executor.run_until_parked();
18199 cx.assert_state_with_diff(
18200 r#"
18201 use some::mod1;
18202 use some::mod2;
18203
18204 - const A: u32 = 42;
18205 - const B: u32 = 42;
18206 - const C: u32 = 42;
18207 -
18208 + replacementˇ
18209
18210 fn main() {
18211 println!("hello");
18212
18213 println!("world");
18214 }
18215 "#
18216 .unindent(),
18217 );
18218}
18219
18220#[gpui::test]
18221async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18222 init_test(cx, |_| {});
18223
18224 let mut cx = EditorTestContext::new(cx).await;
18225
18226 let base_text = r#"
18227 one
18228 two
18229 three
18230 four
18231 five
18232 "#
18233 .unindent();
18234 executor.run_until_parked();
18235 cx.set_state(
18236 &r#"
18237 one
18238 two
18239 fˇour
18240 five
18241 "#
18242 .unindent(),
18243 );
18244
18245 cx.set_head_text(&base_text);
18246 executor.run_until_parked();
18247
18248 cx.update_editor(|editor, window, cx| {
18249 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18250 });
18251 executor.run_until_parked();
18252
18253 cx.assert_state_with_diff(
18254 r#"
18255 one
18256 two
18257 - three
18258 fˇour
18259 five
18260 "#
18261 .unindent(),
18262 );
18263
18264 cx.update_editor(|editor, window, cx| {
18265 editor.backspace(&Backspace, window, cx);
18266 editor.backspace(&Backspace, window, cx);
18267 });
18268 executor.run_until_parked();
18269 cx.assert_state_with_diff(
18270 r#"
18271 one
18272 two
18273 - threeˇ
18274 - four
18275 + our
18276 five
18277 "#
18278 .unindent(),
18279 );
18280}
18281
18282#[gpui::test]
18283async fn test_edit_after_expanded_modification_hunk(
18284 executor: BackgroundExecutor,
18285 cx: &mut TestAppContext,
18286) {
18287 init_test(cx, |_| {});
18288
18289 let mut cx = EditorTestContext::new(cx).await;
18290
18291 let diff_base = r#"
18292 use some::mod1;
18293 use some::mod2;
18294
18295 const A: u32 = 42;
18296 const B: u32 = 42;
18297 const C: u32 = 42;
18298 const D: u32 = 42;
18299
18300
18301 fn main() {
18302 println!("hello");
18303
18304 println!("world");
18305 }"#
18306 .unindent();
18307
18308 cx.set_state(
18309 &r#"
18310 use some::mod1;
18311 use some::mod2;
18312
18313 const A: u32 = 42;
18314 const B: u32 = 42;
18315 const C: u32 = 43ˇ
18316 const D: u32 = 42;
18317
18318
18319 fn main() {
18320 println!("hello");
18321
18322 println!("world");
18323 }"#
18324 .unindent(),
18325 );
18326
18327 cx.set_head_text(&diff_base);
18328 executor.run_until_parked();
18329 cx.update_editor(|editor, window, cx| {
18330 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18331 });
18332 executor.run_until_parked();
18333
18334 cx.assert_state_with_diff(
18335 r#"
18336 use some::mod1;
18337 use some::mod2;
18338
18339 const A: u32 = 42;
18340 const B: u32 = 42;
18341 - const C: u32 = 42;
18342 + const C: u32 = 43ˇ
18343 const D: u32 = 42;
18344
18345
18346 fn main() {
18347 println!("hello");
18348
18349 println!("world");
18350 }"#
18351 .unindent(),
18352 );
18353
18354 cx.update_editor(|editor, window, cx| {
18355 editor.handle_input("\nnew_line\n", window, cx);
18356 });
18357 executor.run_until_parked();
18358
18359 cx.assert_state_with_diff(
18360 r#"
18361 use some::mod1;
18362 use some::mod2;
18363
18364 const A: u32 = 42;
18365 const B: u32 = 42;
18366 - const C: u32 = 42;
18367 + const C: u32 = 43
18368 + new_line
18369 + ˇ
18370 const D: u32 = 42;
18371
18372
18373 fn main() {
18374 println!("hello");
18375
18376 println!("world");
18377 }"#
18378 .unindent(),
18379 );
18380}
18381
18382#[gpui::test]
18383async fn test_stage_and_unstage_added_file_hunk(
18384 executor: BackgroundExecutor,
18385 cx: &mut TestAppContext,
18386) {
18387 init_test(cx, |_| {});
18388
18389 let mut cx = EditorTestContext::new(cx).await;
18390 cx.update_editor(|editor, _, cx| {
18391 editor.set_expand_all_diff_hunks(cx);
18392 });
18393
18394 let working_copy = r#"
18395 ˇfn main() {
18396 println!("hello, world!");
18397 }
18398 "#
18399 .unindent();
18400
18401 cx.set_state(&working_copy);
18402 executor.run_until_parked();
18403
18404 cx.assert_state_with_diff(
18405 r#"
18406 + ˇfn main() {
18407 + println!("hello, world!");
18408 + }
18409 "#
18410 .unindent(),
18411 );
18412 cx.assert_index_text(None);
18413
18414 cx.update_editor(|editor, window, cx| {
18415 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18416 });
18417 executor.run_until_parked();
18418 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
18419 cx.assert_state_with_diff(
18420 r#"
18421 + ˇfn main() {
18422 + println!("hello, world!");
18423 + }
18424 "#
18425 .unindent(),
18426 );
18427
18428 cx.update_editor(|editor, window, cx| {
18429 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18430 });
18431 executor.run_until_parked();
18432 cx.assert_index_text(None);
18433}
18434
18435async fn setup_indent_guides_editor(
18436 text: &str,
18437 cx: &mut TestAppContext,
18438) -> (BufferId, EditorTestContext) {
18439 init_test(cx, |_| {});
18440
18441 let mut cx = EditorTestContext::new(cx).await;
18442
18443 let buffer_id = cx.update_editor(|editor, window, cx| {
18444 editor.set_text(text, window, cx);
18445 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
18446
18447 buffer_ids[0]
18448 });
18449
18450 (buffer_id, cx)
18451}
18452
18453fn assert_indent_guides(
18454 range: Range<u32>,
18455 expected: Vec<IndentGuide>,
18456 active_indices: Option<Vec<usize>>,
18457 cx: &mut EditorTestContext,
18458) {
18459 let indent_guides = cx.update_editor(|editor, window, cx| {
18460 let snapshot = editor.snapshot(window, cx).display_snapshot;
18461 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
18462 editor,
18463 MultiBufferRow(range.start)..MultiBufferRow(range.end),
18464 true,
18465 &snapshot,
18466 cx,
18467 );
18468
18469 indent_guides.sort_by(|a, b| {
18470 a.depth.cmp(&b.depth).then(
18471 a.start_row
18472 .cmp(&b.start_row)
18473 .then(a.end_row.cmp(&b.end_row)),
18474 )
18475 });
18476 indent_guides
18477 });
18478
18479 if let Some(expected) = active_indices {
18480 let active_indices = cx.update_editor(|editor, window, cx| {
18481 let snapshot = editor.snapshot(window, cx).display_snapshot;
18482 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
18483 });
18484
18485 assert_eq!(
18486 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
18487 expected,
18488 "Active indent guide indices do not match"
18489 );
18490 }
18491
18492 assert_eq!(indent_guides, expected, "Indent guides do not match");
18493}
18494
18495fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
18496 IndentGuide {
18497 buffer_id,
18498 start_row: MultiBufferRow(start_row),
18499 end_row: MultiBufferRow(end_row),
18500 depth,
18501 tab_size: 4,
18502 settings: IndentGuideSettings {
18503 enabled: true,
18504 line_width: 1,
18505 active_line_width: 1,
18506 ..Default::default()
18507 },
18508 }
18509}
18510
18511#[gpui::test]
18512async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
18513 let (buffer_id, mut cx) = setup_indent_guides_editor(
18514 &"
18515 fn main() {
18516 let a = 1;
18517 }"
18518 .unindent(),
18519 cx,
18520 )
18521 .await;
18522
18523 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18524}
18525
18526#[gpui::test]
18527async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
18528 let (buffer_id, mut cx) = setup_indent_guides_editor(
18529 &"
18530 fn main() {
18531 let a = 1;
18532 let b = 2;
18533 }"
18534 .unindent(),
18535 cx,
18536 )
18537 .await;
18538
18539 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
18540}
18541
18542#[gpui::test]
18543async fn test_indent_guide_nested(cx: &mut TestAppContext) {
18544 let (buffer_id, mut cx) = setup_indent_guides_editor(
18545 &"
18546 fn main() {
18547 let a = 1;
18548 if a == 3 {
18549 let b = 2;
18550 } else {
18551 let c = 3;
18552 }
18553 }"
18554 .unindent(),
18555 cx,
18556 )
18557 .await;
18558
18559 assert_indent_guides(
18560 0..8,
18561 vec![
18562 indent_guide(buffer_id, 1, 6, 0),
18563 indent_guide(buffer_id, 3, 3, 1),
18564 indent_guide(buffer_id, 5, 5, 1),
18565 ],
18566 None,
18567 &mut cx,
18568 );
18569}
18570
18571#[gpui::test]
18572async fn test_indent_guide_tab(cx: &mut TestAppContext) {
18573 let (buffer_id, mut cx) = setup_indent_guides_editor(
18574 &"
18575 fn main() {
18576 let a = 1;
18577 let b = 2;
18578 let c = 3;
18579 }"
18580 .unindent(),
18581 cx,
18582 )
18583 .await;
18584
18585 assert_indent_guides(
18586 0..5,
18587 vec![
18588 indent_guide(buffer_id, 1, 3, 0),
18589 indent_guide(buffer_id, 2, 2, 1),
18590 ],
18591 None,
18592 &mut cx,
18593 );
18594}
18595
18596#[gpui::test]
18597async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
18598 let (buffer_id, mut cx) = setup_indent_guides_editor(
18599 &"
18600 fn main() {
18601 let a = 1;
18602
18603 let c = 3;
18604 }"
18605 .unindent(),
18606 cx,
18607 )
18608 .await;
18609
18610 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
18611}
18612
18613#[gpui::test]
18614async fn test_indent_guide_complex(cx: &mut TestAppContext) {
18615 let (buffer_id, mut cx) = setup_indent_guides_editor(
18616 &"
18617 fn main() {
18618 let a = 1;
18619
18620 let c = 3;
18621
18622 if a == 3 {
18623 let b = 2;
18624 } else {
18625 let c = 3;
18626 }
18627 }"
18628 .unindent(),
18629 cx,
18630 )
18631 .await;
18632
18633 assert_indent_guides(
18634 0..11,
18635 vec![
18636 indent_guide(buffer_id, 1, 9, 0),
18637 indent_guide(buffer_id, 6, 6, 1),
18638 indent_guide(buffer_id, 8, 8, 1),
18639 ],
18640 None,
18641 &mut cx,
18642 );
18643}
18644
18645#[gpui::test]
18646async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
18647 let (buffer_id, mut cx) = setup_indent_guides_editor(
18648 &"
18649 fn main() {
18650 let a = 1;
18651
18652 let c = 3;
18653
18654 if a == 3 {
18655 let b = 2;
18656 } else {
18657 let c = 3;
18658 }
18659 }"
18660 .unindent(),
18661 cx,
18662 )
18663 .await;
18664
18665 assert_indent_guides(
18666 1..11,
18667 vec![
18668 indent_guide(buffer_id, 1, 9, 0),
18669 indent_guide(buffer_id, 6, 6, 1),
18670 indent_guide(buffer_id, 8, 8, 1),
18671 ],
18672 None,
18673 &mut cx,
18674 );
18675}
18676
18677#[gpui::test]
18678async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
18679 let (buffer_id, mut cx) = setup_indent_guides_editor(
18680 &"
18681 fn main() {
18682 let a = 1;
18683
18684 let c = 3;
18685
18686 if a == 3 {
18687 let b = 2;
18688 } else {
18689 let c = 3;
18690 }
18691 }"
18692 .unindent(),
18693 cx,
18694 )
18695 .await;
18696
18697 assert_indent_guides(
18698 1..10,
18699 vec![
18700 indent_guide(buffer_id, 1, 9, 0),
18701 indent_guide(buffer_id, 6, 6, 1),
18702 indent_guide(buffer_id, 8, 8, 1),
18703 ],
18704 None,
18705 &mut cx,
18706 );
18707}
18708
18709#[gpui::test]
18710async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
18711 let (buffer_id, mut cx) = setup_indent_guides_editor(
18712 &"
18713 fn main() {
18714 if a {
18715 b(
18716 c,
18717 d,
18718 )
18719 } else {
18720 e(
18721 f
18722 )
18723 }
18724 }"
18725 .unindent(),
18726 cx,
18727 )
18728 .await;
18729
18730 assert_indent_guides(
18731 0..11,
18732 vec![
18733 indent_guide(buffer_id, 1, 10, 0),
18734 indent_guide(buffer_id, 2, 5, 1),
18735 indent_guide(buffer_id, 7, 9, 1),
18736 indent_guide(buffer_id, 3, 4, 2),
18737 indent_guide(buffer_id, 8, 8, 2),
18738 ],
18739 None,
18740 &mut cx,
18741 );
18742
18743 cx.update_editor(|editor, window, cx| {
18744 editor.fold_at(MultiBufferRow(2), window, cx);
18745 assert_eq!(
18746 editor.display_text(cx),
18747 "
18748 fn main() {
18749 if a {
18750 b(⋯
18751 )
18752 } else {
18753 e(
18754 f
18755 )
18756 }
18757 }"
18758 .unindent()
18759 );
18760 });
18761
18762 assert_indent_guides(
18763 0..11,
18764 vec![
18765 indent_guide(buffer_id, 1, 10, 0),
18766 indent_guide(buffer_id, 2, 5, 1),
18767 indent_guide(buffer_id, 7, 9, 1),
18768 indent_guide(buffer_id, 8, 8, 2),
18769 ],
18770 None,
18771 &mut cx,
18772 );
18773}
18774
18775#[gpui::test]
18776async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
18777 let (buffer_id, mut cx) = setup_indent_guides_editor(
18778 &"
18779 block1
18780 block2
18781 block3
18782 block4
18783 block2
18784 block1
18785 block1"
18786 .unindent(),
18787 cx,
18788 )
18789 .await;
18790
18791 assert_indent_guides(
18792 1..10,
18793 vec![
18794 indent_guide(buffer_id, 1, 4, 0),
18795 indent_guide(buffer_id, 2, 3, 1),
18796 indent_guide(buffer_id, 3, 3, 2),
18797 ],
18798 None,
18799 &mut cx,
18800 );
18801}
18802
18803#[gpui::test]
18804async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
18805 let (buffer_id, mut cx) = setup_indent_guides_editor(
18806 &"
18807 block1
18808 block2
18809 block3
18810
18811 block1
18812 block1"
18813 .unindent(),
18814 cx,
18815 )
18816 .await;
18817
18818 assert_indent_guides(
18819 0..6,
18820 vec![
18821 indent_guide(buffer_id, 1, 2, 0),
18822 indent_guide(buffer_id, 2, 2, 1),
18823 ],
18824 None,
18825 &mut cx,
18826 );
18827}
18828
18829#[gpui::test]
18830async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
18831 let (buffer_id, mut cx) = setup_indent_guides_editor(
18832 &"
18833 function component() {
18834 \treturn (
18835 \t\t\t
18836 \t\t<div>
18837 \t\t\t<abc></abc>
18838 \t\t</div>
18839 \t)
18840 }"
18841 .unindent(),
18842 cx,
18843 )
18844 .await;
18845
18846 assert_indent_guides(
18847 0..8,
18848 vec![
18849 indent_guide(buffer_id, 1, 6, 0),
18850 indent_guide(buffer_id, 2, 5, 1),
18851 indent_guide(buffer_id, 4, 4, 2),
18852 ],
18853 None,
18854 &mut cx,
18855 );
18856}
18857
18858#[gpui::test]
18859async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
18860 let (buffer_id, mut cx) = setup_indent_guides_editor(
18861 &"
18862 function component() {
18863 \treturn (
18864 \t
18865 \t\t<div>
18866 \t\t\t<abc></abc>
18867 \t\t</div>
18868 \t)
18869 }"
18870 .unindent(),
18871 cx,
18872 )
18873 .await;
18874
18875 assert_indent_guides(
18876 0..8,
18877 vec![
18878 indent_guide(buffer_id, 1, 6, 0),
18879 indent_guide(buffer_id, 2, 5, 1),
18880 indent_guide(buffer_id, 4, 4, 2),
18881 ],
18882 None,
18883 &mut cx,
18884 );
18885}
18886
18887#[gpui::test]
18888async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
18889 let (buffer_id, mut cx) = setup_indent_guides_editor(
18890 &"
18891 block1
18892
18893
18894
18895 block2
18896 "
18897 .unindent(),
18898 cx,
18899 )
18900 .await;
18901
18902 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18903}
18904
18905#[gpui::test]
18906async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
18907 let (buffer_id, mut cx) = setup_indent_guides_editor(
18908 &"
18909 def a:
18910 \tb = 3
18911 \tif True:
18912 \t\tc = 4
18913 \t\td = 5
18914 \tprint(b)
18915 "
18916 .unindent(),
18917 cx,
18918 )
18919 .await;
18920
18921 assert_indent_guides(
18922 0..6,
18923 vec![
18924 indent_guide(buffer_id, 1, 5, 0),
18925 indent_guide(buffer_id, 3, 4, 1),
18926 ],
18927 None,
18928 &mut cx,
18929 );
18930}
18931
18932#[gpui::test]
18933async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
18934 let (buffer_id, mut cx) = setup_indent_guides_editor(
18935 &"
18936 fn main() {
18937 let a = 1;
18938 }"
18939 .unindent(),
18940 cx,
18941 )
18942 .await;
18943
18944 cx.update_editor(|editor, window, cx| {
18945 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18946 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18947 });
18948 });
18949
18950 assert_indent_guides(
18951 0..3,
18952 vec![indent_guide(buffer_id, 1, 1, 0)],
18953 Some(vec![0]),
18954 &mut cx,
18955 );
18956}
18957
18958#[gpui::test]
18959async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
18960 let (buffer_id, mut cx) = setup_indent_guides_editor(
18961 &"
18962 fn main() {
18963 if 1 == 2 {
18964 let a = 1;
18965 }
18966 }"
18967 .unindent(),
18968 cx,
18969 )
18970 .await;
18971
18972 cx.update_editor(|editor, window, cx| {
18973 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18974 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18975 });
18976 });
18977
18978 assert_indent_guides(
18979 0..4,
18980 vec![
18981 indent_guide(buffer_id, 1, 3, 0),
18982 indent_guide(buffer_id, 2, 2, 1),
18983 ],
18984 Some(vec![1]),
18985 &mut cx,
18986 );
18987
18988 cx.update_editor(|editor, window, cx| {
18989 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18990 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18991 });
18992 });
18993
18994 assert_indent_guides(
18995 0..4,
18996 vec![
18997 indent_guide(buffer_id, 1, 3, 0),
18998 indent_guide(buffer_id, 2, 2, 1),
18999 ],
19000 Some(vec![1]),
19001 &mut cx,
19002 );
19003
19004 cx.update_editor(|editor, window, cx| {
19005 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19006 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
19007 });
19008 });
19009
19010 assert_indent_guides(
19011 0..4,
19012 vec![
19013 indent_guide(buffer_id, 1, 3, 0),
19014 indent_guide(buffer_id, 2, 2, 1),
19015 ],
19016 Some(vec![0]),
19017 &mut cx,
19018 );
19019}
19020
19021#[gpui::test]
19022async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
19023 let (buffer_id, mut cx) = setup_indent_guides_editor(
19024 &"
19025 fn main() {
19026 let a = 1;
19027
19028 let b = 2;
19029 }"
19030 .unindent(),
19031 cx,
19032 )
19033 .await;
19034
19035 cx.update_editor(|editor, window, cx| {
19036 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19037 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
19038 });
19039 });
19040
19041 assert_indent_guides(
19042 0..5,
19043 vec![indent_guide(buffer_id, 1, 3, 0)],
19044 Some(vec![0]),
19045 &mut cx,
19046 );
19047}
19048
19049#[gpui::test]
19050async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
19051 let (buffer_id, mut cx) = setup_indent_guides_editor(
19052 &"
19053 def m:
19054 a = 1
19055 pass"
19056 .unindent(),
19057 cx,
19058 )
19059 .await;
19060
19061 cx.update_editor(|editor, window, cx| {
19062 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19063 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
19064 });
19065 });
19066
19067 assert_indent_guides(
19068 0..3,
19069 vec![indent_guide(buffer_id, 1, 2, 0)],
19070 Some(vec![0]),
19071 &mut cx,
19072 );
19073}
19074
19075#[gpui::test]
19076async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
19077 init_test(cx, |_| {});
19078 let mut cx = EditorTestContext::new(cx).await;
19079 let text = indoc! {
19080 "
19081 impl A {
19082 fn b() {
19083 0;
19084 3;
19085 5;
19086 6;
19087 7;
19088 }
19089 }
19090 "
19091 };
19092 let base_text = indoc! {
19093 "
19094 impl A {
19095 fn b() {
19096 0;
19097 1;
19098 2;
19099 3;
19100 4;
19101 }
19102 fn c() {
19103 5;
19104 6;
19105 7;
19106 }
19107 }
19108 "
19109 };
19110
19111 cx.update_editor(|editor, window, cx| {
19112 editor.set_text(text, window, cx);
19113
19114 editor.buffer().update(cx, |multibuffer, cx| {
19115 let buffer = multibuffer.as_singleton().unwrap();
19116 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
19117
19118 multibuffer.set_all_diff_hunks_expanded(cx);
19119 multibuffer.add_diff(diff, cx);
19120
19121 buffer.read(cx).remote_id()
19122 })
19123 });
19124 cx.run_until_parked();
19125
19126 cx.assert_state_with_diff(
19127 indoc! { "
19128 impl A {
19129 fn b() {
19130 0;
19131 - 1;
19132 - 2;
19133 3;
19134 - 4;
19135 - }
19136 - fn c() {
19137 5;
19138 6;
19139 7;
19140 }
19141 }
19142 ˇ"
19143 }
19144 .to_string(),
19145 );
19146
19147 let mut actual_guides = cx.update_editor(|editor, window, cx| {
19148 editor
19149 .snapshot(window, cx)
19150 .buffer_snapshot
19151 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
19152 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
19153 .collect::<Vec<_>>()
19154 });
19155 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
19156 assert_eq!(
19157 actual_guides,
19158 vec![
19159 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
19160 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
19161 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
19162 ]
19163 );
19164}
19165
19166#[gpui::test]
19167async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19168 init_test(cx, |_| {});
19169 let mut cx = EditorTestContext::new(cx).await;
19170
19171 let diff_base = r#"
19172 a
19173 b
19174 c
19175 "#
19176 .unindent();
19177
19178 cx.set_state(
19179 &r#"
19180 ˇA
19181 b
19182 C
19183 "#
19184 .unindent(),
19185 );
19186 cx.set_head_text(&diff_base);
19187 cx.update_editor(|editor, window, cx| {
19188 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19189 });
19190 executor.run_until_parked();
19191
19192 let both_hunks_expanded = r#"
19193 - a
19194 + ˇA
19195 b
19196 - c
19197 + C
19198 "#
19199 .unindent();
19200
19201 cx.assert_state_with_diff(both_hunks_expanded.clone());
19202
19203 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19204 let snapshot = editor.snapshot(window, cx);
19205 let hunks = editor
19206 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19207 .collect::<Vec<_>>();
19208 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19209 let buffer_id = hunks[0].buffer_id;
19210 hunks
19211 .into_iter()
19212 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19213 .collect::<Vec<_>>()
19214 });
19215 assert_eq!(hunk_ranges.len(), 2);
19216
19217 cx.update_editor(|editor, _, cx| {
19218 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19219 });
19220 executor.run_until_parked();
19221
19222 let second_hunk_expanded = r#"
19223 ˇA
19224 b
19225 - c
19226 + C
19227 "#
19228 .unindent();
19229
19230 cx.assert_state_with_diff(second_hunk_expanded);
19231
19232 cx.update_editor(|editor, _, cx| {
19233 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19234 });
19235 executor.run_until_parked();
19236
19237 cx.assert_state_with_diff(both_hunks_expanded.clone());
19238
19239 cx.update_editor(|editor, _, cx| {
19240 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19241 });
19242 executor.run_until_parked();
19243
19244 let first_hunk_expanded = r#"
19245 - a
19246 + ˇA
19247 b
19248 C
19249 "#
19250 .unindent();
19251
19252 cx.assert_state_with_diff(first_hunk_expanded);
19253
19254 cx.update_editor(|editor, _, cx| {
19255 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19256 });
19257 executor.run_until_parked();
19258
19259 cx.assert_state_with_diff(both_hunks_expanded);
19260
19261 cx.set_state(
19262 &r#"
19263 ˇA
19264 b
19265 "#
19266 .unindent(),
19267 );
19268 cx.run_until_parked();
19269
19270 // TODO this cursor position seems bad
19271 cx.assert_state_with_diff(
19272 r#"
19273 - ˇa
19274 + A
19275 b
19276 "#
19277 .unindent(),
19278 );
19279
19280 cx.update_editor(|editor, window, cx| {
19281 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19282 });
19283
19284 cx.assert_state_with_diff(
19285 r#"
19286 - ˇa
19287 + A
19288 b
19289 - c
19290 "#
19291 .unindent(),
19292 );
19293
19294 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19295 let snapshot = editor.snapshot(window, cx);
19296 let hunks = editor
19297 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19298 .collect::<Vec<_>>();
19299 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19300 let buffer_id = hunks[0].buffer_id;
19301 hunks
19302 .into_iter()
19303 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19304 .collect::<Vec<_>>()
19305 });
19306 assert_eq!(hunk_ranges.len(), 2);
19307
19308 cx.update_editor(|editor, _, cx| {
19309 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19310 });
19311 executor.run_until_parked();
19312
19313 cx.assert_state_with_diff(
19314 r#"
19315 - ˇa
19316 + A
19317 b
19318 "#
19319 .unindent(),
19320 );
19321}
19322
19323#[gpui::test]
19324async fn test_toggle_deletion_hunk_at_start_of_file(
19325 executor: BackgroundExecutor,
19326 cx: &mut TestAppContext,
19327) {
19328 init_test(cx, |_| {});
19329 let mut cx = EditorTestContext::new(cx).await;
19330
19331 let diff_base = r#"
19332 a
19333 b
19334 c
19335 "#
19336 .unindent();
19337
19338 cx.set_state(
19339 &r#"
19340 ˇb
19341 c
19342 "#
19343 .unindent(),
19344 );
19345 cx.set_head_text(&diff_base);
19346 cx.update_editor(|editor, window, cx| {
19347 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19348 });
19349 executor.run_until_parked();
19350
19351 let hunk_expanded = r#"
19352 - a
19353 ˇb
19354 c
19355 "#
19356 .unindent();
19357
19358 cx.assert_state_with_diff(hunk_expanded.clone());
19359
19360 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19361 let snapshot = editor.snapshot(window, cx);
19362 let hunks = editor
19363 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19364 .collect::<Vec<_>>();
19365 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19366 let buffer_id = hunks[0].buffer_id;
19367 hunks
19368 .into_iter()
19369 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19370 .collect::<Vec<_>>()
19371 });
19372 assert_eq!(hunk_ranges.len(), 1);
19373
19374 cx.update_editor(|editor, _, cx| {
19375 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19376 });
19377 executor.run_until_parked();
19378
19379 let hunk_collapsed = r#"
19380 ˇb
19381 c
19382 "#
19383 .unindent();
19384
19385 cx.assert_state_with_diff(hunk_collapsed);
19386
19387 cx.update_editor(|editor, _, cx| {
19388 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19389 });
19390 executor.run_until_parked();
19391
19392 cx.assert_state_with_diff(hunk_expanded.clone());
19393}
19394
19395#[gpui::test]
19396async fn test_display_diff_hunks(cx: &mut TestAppContext) {
19397 init_test(cx, |_| {});
19398
19399 let fs = FakeFs::new(cx.executor());
19400 fs.insert_tree(
19401 path!("/test"),
19402 json!({
19403 ".git": {},
19404 "file-1": "ONE\n",
19405 "file-2": "TWO\n",
19406 "file-3": "THREE\n",
19407 }),
19408 )
19409 .await;
19410
19411 fs.set_head_for_repo(
19412 path!("/test/.git").as_ref(),
19413 &[
19414 ("file-1".into(), "one\n".into()),
19415 ("file-2".into(), "two\n".into()),
19416 ("file-3".into(), "three\n".into()),
19417 ],
19418 "deadbeef",
19419 );
19420
19421 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
19422 let mut buffers = vec![];
19423 for i in 1..=3 {
19424 let buffer = project
19425 .update(cx, |project, cx| {
19426 let path = format!(path!("/test/file-{}"), i);
19427 project.open_local_buffer(path, cx)
19428 })
19429 .await
19430 .unwrap();
19431 buffers.push(buffer);
19432 }
19433
19434 let multibuffer = cx.new(|cx| {
19435 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
19436 multibuffer.set_all_diff_hunks_expanded(cx);
19437 for buffer in &buffers {
19438 let snapshot = buffer.read(cx).snapshot();
19439 multibuffer.set_excerpts_for_path(
19440 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
19441 buffer.clone(),
19442 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
19443 DEFAULT_MULTIBUFFER_CONTEXT,
19444 cx,
19445 );
19446 }
19447 multibuffer
19448 });
19449
19450 let editor = cx.add_window(|window, cx| {
19451 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
19452 });
19453 cx.run_until_parked();
19454
19455 let snapshot = editor
19456 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19457 .unwrap();
19458 let hunks = snapshot
19459 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
19460 .map(|hunk| match hunk {
19461 DisplayDiffHunk::Unfolded {
19462 display_row_range, ..
19463 } => display_row_range,
19464 DisplayDiffHunk::Folded { .. } => unreachable!(),
19465 })
19466 .collect::<Vec<_>>();
19467 assert_eq!(
19468 hunks,
19469 [
19470 DisplayRow(2)..DisplayRow(4),
19471 DisplayRow(7)..DisplayRow(9),
19472 DisplayRow(12)..DisplayRow(14),
19473 ]
19474 );
19475}
19476
19477#[gpui::test]
19478async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
19479 init_test(cx, |_| {});
19480
19481 let mut cx = EditorTestContext::new(cx).await;
19482 cx.set_head_text(indoc! { "
19483 one
19484 two
19485 three
19486 four
19487 five
19488 "
19489 });
19490 cx.set_index_text(indoc! { "
19491 one
19492 two
19493 three
19494 four
19495 five
19496 "
19497 });
19498 cx.set_state(indoc! {"
19499 one
19500 TWO
19501 ˇTHREE
19502 FOUR
19503 five
19504 "});
19505 cx.run_until_parked();
19506 cx.update_editor(|editor, window, cx| {
19507 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19508 });
19509 cx.run_until_parked();
19510 cx.assert_index_text(Some(indoc! {"
19511 one
19512 TWO
19513 THREE
19514 FOUR
19515 five
19516 "}));
19517 cx.set_state(indoc! { "
19518 one
19519 TWO
19520 ˇTHREE-HUNDRED
19521 FOUR
19522 five
19523 "});
19524 cx.run_until_parked();
19525 cx.update_editor(|editor, window, cx| {
19526 let snapshot = editor.snapshot(window, cx);
19527 let hunks = editor
19528 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19529 .collect::<Vec<_>>();
19530 assert_eq!(hunks.len(), 1);
19531 assert_eq!(
19532 hunks[0].status(),
19533 DiffHunkStatus {
19534 kind: DiffHunkStatusKind::Modified,
19535 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
19536 }
19537 );
19538
19539 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19540 });
19541 cx.run_until_parked();
19542 cx.assert_index_text(Some(indoc! {"
19543 one
19544 TWO
19545 THREE-HUNDRED
19546 FOUR
19547 five
19548 "}));
19549}
19550
19551#[gpui::test]
19552fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
19553 init_test(cx, |_| {});
19554
19555 let editor = cx.add_window(|window, cx| {
19556 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
19557 build_editor(buffer, window, cx)
19558 });
19559
19560 let render_args = Arc::new(Mutex::new(None));
19561 let snapshot = editor
19562 .update(cx, |editor, window, cx| {
19563 let snapshot = editor.buffer().read(cx).snapshot(cx);
19564 let range =
19565 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
19566
19567 struct RenderArgs {
19568 row: MultiBufferRow,
19569 folded: bool,
19570 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
19571 }
19572
19573 let crease = Crease::inline(
19574 range,
19575 FoldPlaceholder::test(),
19576 {
19577 let toggle_callback = render_args.clone();
19578 move |row, folded, callback, _window, _cx| {
19579 *toggle_callback.lock() = Some(RenderArgs {
19580 row,
19581 folded,
19582 callback,
19583 });
19584 div()
19585 }
19586 },
19587 |_row, _folded, _window, _cx| div(),
19588 );
19589
19590 editor.insert_creases(Some(crease), cx);
19591 let snapshot = editor.snapshot(window, cx);
19592 let _div = snapshot.render_crease_toggle(
19593 MultiBufferRow(1),
19594 false,
19595 cx.entity().clone(),
19596 window,
19597 cx,
19598 );
19599 snapshot
19600 })
19601 .unwrap();
19602
19603 let render_args = render_args.lock().take().unwrap();
19604 assert_eq!(render_args.row, MultiBufferRow(1));
19605 assert!(!render_args.folded);
19606 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19607
19608 cx.update_window(*editor, |_, window, cx| {
19609 (render_args.callback)(true, window, cx)
19610 })
19611 .unwrap();
19612 let snapshot = editor
19613 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19614 .unwrap();
19615 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
19616
19617 cx.update_window(*editor, |_, window, cx| {
19618 (render_args.callback)(false, window, cx)
19619 })
19620 .unwrap();
19621 let snapshot = editor
19622 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19623 .unwrap();
19624 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19625}
19626
19627#[gpui::test]
19628async fn test_input_text(cx: &mut TestAppContext) {
19629 init_test(cx, |_| {});
19630 let mut cx = EditorTestContext::new(cx).await;
19631
19632 cx.set_state(
19633 &r#"ˇone
19634 two
19635
19636 three
19637 fourˇ
19638 five
19639
19640 siˇx"#
19641 .unindent(),
19642 );
19643
19644 cx.dispatch_action(HandleInput(String::new()));
19645 cx.assert_editor_state(
19646 &r#"ˇone
19647 two
19648
19649 three
19650 fourˇ
19651 five
19652
19653 siˇx"#
19654 .unindent(),
19655 );
19656
19657 cx.dispatch_action(HandleInput("AAAA".to_string()));
19658 cx.assert_editor_state(
19659 &r#"AAAAˇone
19660 two
19661
19662 three
19663 fourAAAAˇ
19664 five
19665
19666 siAAAAˇx"#
19667 .unindent(),
19668 );
19669}
19670
19671#[gpui::test]
19672async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
19673 init_test(cx, |_| {});
19674
19675 let mut cx = EditorTestContext::new(cx).await;
19676 cx.set_state(
19677 r#"let foo = 1;
19678let foo = 2;
19679let foo = 3;
19680let fooˇ = 4;
19681let foo = 5;
19682let foo = 6;
19683let foo = 7;
19684let foo = 8;
19685let foo = 9;
19686let foo = 10;
19687let foo = 11;
19688let foo = 12;
19689let foo = 13;
19690let foo = 14;
19691let foo = 15;"#,
19692 );
19693
19694 cx.update_editor(|e, window, cx| {
19695 assert_eq!(
19696 e.next_scroll_position,
19697 NextScrollCursorCenterTopBottom::Center,
19698 "Default next scroll direction is center",
19699 );
19700
19701 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19702 assert_eq!(
19703 e.next_scroll_position,
19704 NextScrollCursorCenterTopBottom::Top,
19705 "After center, next scroll direction should be top",
19706 );
19707
19708 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19709 assert_eq!(
19710 e.next_scroll_position,
19711 NextScrollCursorCenterTopBottom::Bottom,
19712 "After top, next scroll direction should be bottom",
19713 );
19714
19715 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19716 assert_eq!(
19717 e.next_scroll_position,
19718 NextScrollCursorCenterTopBottom::Center,
19719 "After bottom, scrolling should start over",
19720 );
19721
19722 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19723 assert_eq!(
19724 e.next_scroll_position,
19725 NextScrollCursorCenterTopBottom::Top,
19726 "Scrolling continues if retriggered fast enough"
19727 );
19728 });
19729
19730 cx.executor()
19731 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
19732 cx.executor().run_until_parked();
19733 cx.update_editor(|e, _, _| {
19734 assert_eq!(
19735 e.next_scroll_position,
19736 NextScrollCursorCenterTopBottom::Center,
19737 "If scrolling is not triggered fast enough, it should reset"
19738 );
19739 });
19740}
19741
19742#[gpui::test]
19743async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
19744 init_test(cx, |_| {});
19745 let mut cx = EditorLspTestContext::new_rust(
19746 lsp::ServerCapabilities {
19747 definition_provider: Some(lsp::OneOf::Left(true)),
19748 references_provider: Some(lsp::OneOf::Left(true)),
19749 ..lsp::ServerCapabilities::default()
19750 },
19751 cx,
19752 )
19753 .await;
19754
19755 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
19756 let go_to_definition = cx
19757 .lsp
19758 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19759 move |params, _| async move {
19760 if empty_go_to_definition {
19761 Ok(None)
19762 } else {
19763 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
19764 uri: params.text_document_position_params.text_document.uri,
19765 range: lsp::Range::new(
19766 lsp::Position::new(4, 3),
19767 lsp::Position::new(4, 6),
19768 ),
19769 })))
19770 }
19771 },
19772 );
19773 let references = cx
19774 .lsp
19775 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
19776 Ok(Some(vec![lsp::Location {
19777 uri: params.text_document_position.text_document.uri,
19778 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
19779 }]))
19780 });
19781 (go_to_definition, references)
19782 };
19783
19784 cx.set_state(
19785 &r#"fn one() {
19786 let mut a = ˇtwo();
19787 }
19788
19789 fn two() {}"#
19790 .unindent(),
19791 );
19792 set_up_lsp_handlers(false, &mut cx);
19793 let navigated = cx
19794 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19795 .await
19796 .expect("Failed to navigate to definition");
19797 assert_eq!(
19798 navigated,
19799 Navigated::Yes,
19800 "Should have navigated to definition from the GetDefinition response"
19801 );
19802 cx.assert_editor_state(
19803 &r#"fn one() {
19804 let mut a = two();
19805 }
19806
19807 fn «twoˇ»() {}"#
19808 .unindent(),
19809 );
19810
19811 let editors = cx.update_workspace(|workspace, _, cx| {
19812 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19813 });
19814 cx.update_editor(|_, _, test_editor_cx| {
19815 assert_eq!(
19816 editors.len(),
19817 1,
19818 "Initially, only one, test, editor should be open in the workspace"
19819 );
19820 assert_eq!(
19821 test_editor_cx.entity(),
19822 editors.last().expect("Asserted len is 1").clone()
19823 );
19824 });
19825
19826 set_up_lsp_handlers(true, &mut cx);
19827 let navigated = cx
19828 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19829 .await
19830 .expect("Failed to navigate to lookup references");
19831 assert_eq!(
19832 navigated,
19833 Navigated::Yes,
19834 "Should have navigated to references as a fallback after empty GoToDefinition response"
19835 );
19836 // We should not change the selections in the existing file,
19837 // if opening another milti buffer with the references
19838 cx.assert_editor_state(
19839 &r#"fn one() {
19840 let mut a = two();
19841 }
19842
19843 fn «twoˇ»() {}"#
19844 .unindent(),
19845 );
19846 let editors = cx.update_workspace(|workspace, _, cx| {
19847 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19848 });
19849 cx.update_editor(|_, _, test_editor_cx| {
19850 assert_eq!(
19851 editors.len(),
19852 2,
19853 "After falling back to references search, we open a new editor with the results"
19854 );
19855 let references_fallback_text = editors
19856 .into_iter()
19857 .find(|new_editor| *new_editor != test_editor_cx.entity())
19858 .expect("Should have one non-test editor now")
19859 .read(test_editor_cx)
19860 .text(test_editor_cx);
19861 assert_eq!(
19862 references_fallback_text, "fn one() {\n let mut a = two();\n}",
19863 "Should use the range from the references response and not the GoToDefinition one"
19864 );
19865 });
19866}
19867
19868#[gpui::test]
19869async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
19870 init_test(cx, |_| {});
19871 cx.update(|cx| {
19872 let mut editor_settings = EditorSettings::get_global(cx).clone();
19873 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
19874 EditorSettings::override_global(editor_settings, cx);
19875 });
19876 let mut cx = EditorLspTestContext::new_rust(
19877 lsp::ServerCapabilities {
19878 definition_provider: Some(lsp::OneOf::Left(true)),
19879 references_provider: Some(lsp::OneOf::Left(true)),
19880 ..lsp::ServerCapabilities::default()
19881 },
19882 cx,
19883 )
19884 .await;
19885 let original_state = r#"fn one() {
19886 let mut a = ˇtwo();
19887 }
19888
19889 fn two() {}"#
19890 .unindent();
19891 cx.set_state(&original_state);
19892
19893 let mut go_to_definition = cx
19894 .lsp
19895 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19896 move |_, _| async move { Ok(None) },
19897 );
19898 let _references = cx
19899 .lsp
19900 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
19901 panic!("Should not call for references with no go to definition fallback")
19902 });
19903
19904 let navigated = cx
19905 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19906 .await
19907 .expect("Failed to navigate to lookup references");
19908 go_to_definition
19909 .next()
19910 .await
19911 .expect("Should have called the go_to_definition handler");
19912
19913 assert_eq!(
19914 navigated,
19915 Navigated::No,
19916 "Should have navigated to references as a fallback after empty GoToDefinition response"
19917 );
19918 cx.assert_editor_state(&original_state);
19919 let editors = cx.update_workspace(|workspace, _, cx| {
19920 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19921 });
19922 cx.update_editor(|_, _, _| {
19923 assert_eq!(
19924 editors.len(),
19925 1,
19926 "After unsuccessful fallback, no other editor should have been opened"
19927 );
19928 });
19929}
19930
19931#[gpui::test]
19932async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
19933 init_test(cx, |_| {});
19934
19935 let language = Arc::new(Language::new(
19936 LanguageConfig::default(),
19937 Some(tree_sitter_rust::LANGUAGE.into()),
19938 ));
19939
19940 let text = r#"
19941 #[cfg(test)]
19942 mod tests() {
19943 #[test]
19944 fn runnable_1() {
19945 let a = 1;
19946 }
19947
19948 #[test]
19949 fn runnable_2() {
19950 let a = 1;
19951 let b = 2;
19952 }
19953 }
19954 "#
19955 .unindent();
19956
19957 let fs = FakeFs::new(cx.executor());
19958 fs.insert_file("/file.rs", Default::default()).await;
19959
19960 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19961 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19962 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19963 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
19964 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
19965
19966 let editor = cx.new_window_entity(|window, cx| {
19967 Editor::new(
19968 EditorMode::full(),
19969 multi_buffer,
19970 Some(project.clone()),
19971 window,
19972 cx,
19973 )
19974 });
19975
19976 editor.update_in(cx, |editor, window, cx| {
19977 let snapshot = editor.buffer().read(cx).snapshot(cx);
19978 editor.tasks.insert(
19979 (buffer.read(cx).remote_id(), 3),
19980 RunnableTasks {
19981 templates: vec![],
19982 offset: snapshot.anchor_before(43),
19983 column: 0,
19984 extra_variables: HashMap::default(),
19985 context_range: BufferOffset(43)..BufferOffset(85),
19986 },
19987 );
19988 editor.tasks.insert(
19989 (buffer.read(cx).remote_id(), 8),
19990 RunnableTasks {
19991 templates: vec![],
19992 offset: snapshot.anchor_before(86),
19993 column: 0,
19994 extra_variables: HashMap::default(),
19995 context_range: BufferOffset(86)..BufferOffset(191),
19996 },
19997 );
19998
19999 // Test finding task when cursor is inside function body
20000 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20001 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
20002 });
20003 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
20004 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
20005
20006 // Test finding task when cursor is on function name
20007 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20008 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
20009 });
20010 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
20011 assert_eq!(row, 8, "Should find task when cursor is on function name");
20012 });
20013}
20014
20015#[gpui::test]
20016async fn test_folding_buffers(cx: &mut TestAppContext) {
20017 init_test(cx, |_| {});
20018
20019 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
20020 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
20021 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
20022
20023 let fs = FakeFs::new(cx.executor());
20024 fs.insert_tree(
20025 path!("/a"),
20026 json!({
20027 "first.rs": sample_text_1,
20028 "second.rs": sample_text_2,
20029 "third.rs": sample_text_3,
20030 }),
20031 )
20032 .await;
20033 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20034 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20035 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20036 let worktree = project.update(cx, |project, cx| {
20037 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20038 assert_eq!(worktrees.len(), 1);
20039 worktrees.pop().unwrap()
20040 });
20041 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20042
20043 let buffer_1 = project
20044 .update(cx, |project, cx| {
20045 project.open_buffer((worktree_id, "first.rs"), cx)
20046 })
20047 .await
20048 .unwrap();
20049 let buffer_2 = project
20050 .update(cx, |project, cx| {
20051 project.open_buffer((worktree_id, "second.rs"), cx)
20052 })
20053 .await
20054 .unwrap();
20055 let buffer_3 = project
20056 .update(cx, |project, cx| {
20057 project.open_buffer((worktree_id, "third.rs"), cx)
20058 })
20059 .await
20060 .unwrap();
20061
20062 let multi_buffer = cx.new(|cx| {
20063 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20064 multi_buffer.push_excerpts(
20065 buffer_1.clone(),
20066 [
20067 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20068 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20069 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20070 ],
20071 cx,
20072 );
20073 multi_buffer.push_excerpts(
20074 buffer_2.clone(),
20075 [
20076 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20077 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20078 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20079 ],
20080 cx,
20081 );
20082 multi_buffer.push_excerpts(
20083 buffer_3.clone(),
20084 [
20085 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20086 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20087 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20088 ],
20089 cx,
20090 );
20091 multi_buffer
20092 });
20093 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20094 Editor::new(
20095 EditorMode::full(),
20096 multi_buffer.clone(),
20097 Some(project.clone()),
20098 window,
20099 cx,
20100 )
20101 });
20102
20103 assert_eq!(
20104 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20105 "\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",
20106 );
20107
20108 multi_buffer_editor.update(cx, |editor, cx| {
20109 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
20110 });
20111 assert_eq!(
20112 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20113 "\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",
20114 "After folding the first buffer, its text should not be displayed"
20115 );
20116
20117 multi_buffer_editor.update(cx, |editor, cx| {
20118 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
20119 });
20120 assert_eq!(
20121 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20122 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
20123 "After folding the second buffer, its text should not be displayed"
20124 );
20125
20126 multi_buffer_editor.update(cx, |editor, cx| {
20127 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
20128 });
20129 assert_eq!(
20130 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20131 "\n\n\n\n\n",
20132 "After folding the third buffer, its text should not be displayed"
20133 );
20134
20135 // Emulate selection inside the fold logic, that should work
20136 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20137 editor
20138 .snapshot(window, cx)
20139 .next_line_boundary(Point::new(0, 4));
20140 });
20141
20142 multi_buffer_editor.update(cx, |editor, cx| {
20143 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
20144 });
20145 assert_eq!(
20146 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20147 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
20148 "After unfolding the second buffer, its text should be displayed"
20149 );
20150
20151 // Typing inside of buffer 1 causes that buffer to be unfolded.
20152 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20153 assert_eq!(
20154 multi_buffer
20155 .read(cx)
20156 .snapshot(cx)
20157 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
20158 .collect::<String>(),
20159 "bbbb"
20160 );
20161 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
20162 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
20163 });
20164 editor.handle_input("B", window, cx);
20165 });
20166
20167 assert_eq!(
20168 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20169 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
20170 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
20171 );
20172
20173 multi_buffer_editor.update(cx, |editor, cx| {
20174 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
20175 });
20176 assert_eq!(
20177 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20178 "\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",
20179 "After unfolding the all buffers, all original text should be displayed"
20180 );
20181}
20182
20183#[gpui::test]
20184async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
20185 init_test(cx, |_| {});
20186
20187 let sample_text_1 = "1111\n2222\n3333".to_string();
20188 let sample_text_2 = "4444\n5555\n6666".to_string();
20189 let sample_text_3 = "7777\n8888\n9999".to_string();
20190
20191 let fs = FakeFs::new(cx.executor());
20192 fs.insert_tree(
20193 path!("/a"),
20194 json!({
20195 "first.rs": sample_text_1,
20196 "second.rs": sample_text_2,
20197 "third.rs": sample_text_3,
20198 }),
20199 )
20200 .await;
20201 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20202 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20203 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20204 let worktree = project.update(cx, |project, cx| {
20205 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20206 assert_eq!(worktrees.len(), 1);
20207 worktrees.pop().unwrap()
20208 });
20209 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20210
20211 let buffer_1 = project
20212 .update(cx, |project, cx| {
20213 project.open_buffer((worktree_id, "first.rs"), cx)
20214 })
20215 .await
20216 .unwrap();
20217 let buffer_2 = project
20218 .update(cx, |project, cx| {
20219 project.open_buffer((worktree_id, "second.rs"), cx)
20220 })
20221 .await
20222 .unwrap();
20223 let buffer_3 = project
20224 .update(cx, |project, cx| {
20225 project.open_buffer((worktree_id, "third.rs"), cx)
20226 })
20227 .await
20228 .unwrap();
20229
20230 let multi_buffer = cx.new(|cx| {
20231 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20232 multi_buffer.push_excerpts(
20233 buffer_1.clone(),
20234 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20235 cx,
20236 );
20237 multi_buffer.push_excerpts(
20238 buffer_2.clone(),
20239 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20240 cx,
20241 );
20242 multi_buffer.push_excerpts(
20243 buffer_3.clone(),
20244 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20245 cx,
20246 );
20247 multi_buffer
20248 });
20249
20250 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20251 Editor::new(
20252 EditorMode::full(),
20253 multi_buffer,
20254 Some(project.clone()),
20255 window,
20256 cx,
20257 )
20258 });
20259
20260 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
20261 assert_eq!(
20262 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20263 full_text,
20264 );
20265
20266 multi_buffer_editor.update(cx, |editor, cx| {
20267 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
20268 });
20269 assert_eq!(
20270 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20271 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
20272 "After folding the first buffer, its text should not be displayed"
20273 );
20274
20275 multi_buffer_editor.update(cx, |editor, cx| {
20276 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
20277 });
20278
20279 assert_eq!(
20280 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20281 "\n\n\n\n\n\n7777\n8888\n9999",
20282 "After folding the second buffer, its text should not be displayed"
20283 );
20284
20285 multi_buffer_editor.update(cx, |editor, cx| {
20286 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
20287 });
20288 assert_eq!(
20289 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20290 "\n\n\n\n\n",
20291 "After folding the third buffer, its text should not be displayed"
20292 );
20293
20294 multi_buffer_editor.update(cx, |editor, cx| {
20295 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
20296 });
20297 assert_eq!(
20298 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20299 "\n\n\n\n4444\n5555\n6666\n\n",
20300 "After unfolding the second buffer, its text should be displayed"
20301 );
20302
20303 multi_buffer_editor.update(cx, |editor, cx| {
20304 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
20305 });
20306 assert_eq!(
20307 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20308 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
20309 "After unfolding the first buffer, its text should be displayed"
20310 );
20311
20312 multi_buffer_editor.update(cx, |editor, cx| {
20313 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
20314 });
20315 assert_eq!(
20316 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20317 full_text,
20318 "After unfolding all buffers, all original text should be displayed"
20319 );
20320}
20321
20322#[gpui::test]
20323async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
20324 init_test(cx, |_| {});
20325
20326 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
20327
20328 let fs = FakeFs::new(cx.executor());
20329 fs.insert_tree(
20330 path!("/a"),
20331 json!({
20332 "main.rs": sample_text,
20333 }),
20334 )
20335 .await;
20336 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20337 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20338 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20339 let worktree = project.update(cx, |project, cx| {
20340 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20341 assert_eq!(worktrees.len(), 1);
20342 worktrees.pop().unwrap()
20343 });
20344 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20345
20346 let buffer_1 = project
20347 .update(cx, |project, cx| {
20348 project.open_buffer((worktree_id, "main.rs"), cx)
20349 })
20350 .await
20351 .unwrap();
20352
20353 let multi_buffer = cx.new(|cx| {
20354 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20355 multi_buffer.push_excerpts(
20356 buffer_1.clone(),
20357 [ExcerptRange::new(
20358 Point::new(0, 0)
20359 ..Point::new(
20360 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
20361 0,
20362 ),
20363 )],
20364 cx,
20365 );
20366 multi_buffer
20367 });
20368 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20369 Editor::new(
20370 EditorMode::full(),
20371 multi_buffer,
20372 Some(project.clone()),
20373 window,
20374 cx,
20375 )
20376 });
20377
20378 let selection_range = Point::new(1, 0)..Point::new(2, 0);
20379 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20380 enum TestHighlight {}
20381 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
20382 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
20383 editor.highlight_text::<TestHighlight>(
20384 vec![highlight_range.clone()],
20385 HighlightStyle::color(Hsla::green()),
20386 cx,
20387 );
20388 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20389 s.select_ranges(Some(highlight_range))
20390 });
20391 });
20392
20393 let full_text = format!("\n\n{sample_text}");
20394 assert_eq!(
20395 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20396 full_text,
20397 );
20398}
20399
20400#[gpui::test]
20401async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
20402 init_test(cx, |_| {});
20403 cx.update(|cx| {
20404 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
20405 "keymaps/default-linux.json",
20406 cx,
20407 )
20408 .unwrap();
20409 cx.bind_keys(default_key_bindings);
20410 });
20411
20412 let (editor, cx) = cx.add_window_view(|window, cx| {
20413 let multi_buffer = MultiBuffer::build_multi(
20414 [
20415 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
20416 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
20417 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
20418 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
20419 ],
20420 cx,
20421 );
20422 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
20423
20424 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
20425 // fold all but the second buffer, so that we test navigating between two
20426 // adjacent folded buffers, as well as folded buffers at the start and
20427 // end the multibuffer
20428 editor.fold_buffer(buffer_ids[0], cx);
20429 editor.fold_buffer(buffer_ids[2], cx);
20430 editor.fold_buffer(buffer_ids[3], cx);
20431
20432 editor
20433 });
20434 cx.simulate_resize(size(px(1000.), px(1000.)));
20435
20436 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
20437 cx.assert_excerpts_with_selections(indoc! {"
20438 [EXCERPT]
20439 ˇ[FOLDED]
20440 [EXCERPT]
20441 a1
20442 b1
20443 [EXCERPT]
20444 [FOLDED]
20445 [EXCERPT]
20446 [FOLDED]
20447 "
20448 });
20449 cx.simulate_keystroke("down");
20450 cx.assert_excerpts_with_selections(indoc! {"
20451 [EXCERPT]
20452 [FOLDED]
20453 [EXCERPT]
20454 ˇa1
20455 b1
20456 [EXCERPT]
20457 [FOLDED]
20458 [EXCERPT]
20459 [FOLDED]
20460 "
20461 });
20462 cx.simulate_keystroke("down");
20463 cx.assert_excerpts_with_selections(indoc! {"
20464 [EXCERPT]
20465 [FOLDED]
20466 [EXCERPT]
20467 a1
20468 ˇb1
20469 [EXCERPT]
20470 [FOLDED]
20471 [EXCERPT]
20472 [FOLDED]
20473 "
20474 });
20475 cx.simulate_keystroke("down");
20476 cx.assert_excerpts_with_selections(indoc! {"
20477 [EXCERPT]
20478 [FOLDED]
20479 [EXCERPT]
20480 a1
20481 b1
20482 ˇ[EXCERPT]
20483 [FOLDED]
20484 [EXCERPT]
20485 [FOLDED]
20486 "
20487 });
20488 cx.simulate_keystroke("down");
20489 cx.assert_excerpts_with_selections(indoc! {"
20490 [EXCERPT]
20491 [FOLDED]
20492 [EXCERPT]
20493 a1
20494 b1
20495 [EXCERPT]
20496 ˇ[FOLDED]
20497 [EXCERPT]
20498 [FOLDED]
20499 "
20500 });
20501 for _ in 0..5 {
20502 cx.simulate_keystroke("down");
20503 cx.assert_excerpts_with_selections(indoc! {"
20504 [EXCERPT]
20505 [FOLDED]
20506 [EXCERPT]
20507 a1
20508 b1
20509 [EXCERPT]
20510 [FOLDED]
20511 [EXCERPT]
20512 ˇ[FOLDED]
20513 "
20514 });
20515 }
20516
20517 cx.simulate_keystroke("up");
20518 cx.assert_excerpts_with_selections(indoc! {"
20519 [EXCERPT]
20520 [FOLDED]
20521 [EXCERPT]
20522 a1
20523 b1
20524 [EXCERPT]
20525 ˇ[FOLDED]
20526 [EXCERPT]
20527 [FOLDED]
20528 "
20529 });
20530 cx.simulate_keystroke("up");
20531 cx.assert_excerpts_with_selections(indoc! {"
20532 [EXCERPT]
20533 [FOLDED]
20534 [EXCERPT]
20535 a1
20536 b1
20537 ˇ[EXCERPT]
20538 [FOLDED]
20539 [EXCERPT]
20540 [FOLDED]
20541 "
20542 });
20543 cx.simulate_keystroke("up");
20544 cx.assert_excerpts_with_selections(indoc! {"
20545 [EXCERPT]
20546 [FOLDED]
20547 [EXCERPT]
20548 a1
20549 ˇb1
20550 [EXCERPT]
20551 [FOLDED]
20552 [EXCERPT]
20553 [FOLDED]
20554 "
20555 });
20556 cx.simulate_keystroke("up");
20557 cx.assert_excerpts_with_selections(indoc! {"
20558 [EXCERPT]
20559 [FOLDED]
20560 [EXCERPT]
20561 ˇa1
20562 b1
20563 [EXCERPT]
20564 [FOLDED]
20565 [EXCERPT]
20566 [FOLDED]
20567 "
20568 });
20569 for _ in 0..5 {
20570 cx.simulate_keystroke("up");
20571 cx.assert_excerpts_with_selections(indoc! {"
20572 [EXCERPT]
20573 ˇ[FOLDED]
20574 [EXCERPT]
20575 a1
20576 b1
20577 [EXCERPT]
20578 [FOLDED]
20579 [EXCERPT]
20580 [FOLDED]
20581 "
20582 });
20583 }
20584}
20585
20586#[gpui::test]
20587async fn test_edit_prediction_text(cx: &mut TestAppContext) {
20588 init_test(cx, |_| {});
20589
20590 // Simple insertion
20591 assert_highlighted_edits(
20592 "Hello, world!",
20593 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
20594 true,
20595 cx,
20596 |highlighted_edits, cx| {
20597 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
20598 assert_eq!(highlighted_edits.highlights.len(), 1);
20599 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
20600 assert_eq!(
20601 highlighted_edits.highlights[0].1.background_color,
20602 Some(cx.theme().status().created_background)
20603 );
20604 },
20605 )
20606 .await;
20607
20608 // Replacement
20609 assert_highlighted_edits(
20610 "This is a test.",
20611 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
20612 false,
20613 cx,
20614 |highlighted_edits, cx| {
20615 assert_eq!(highlighted_edits.text, "That is a test.");
20616 assert_eq!(highlighted_edits.highlights.len(), 1);
20617 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
20618 assert_eq!(
20619 highlighted_edits.highlights[0].1.background_color,
20620 Some(cx.theme().status().created_background)
20621 );
20622 },
20623 )
20624 .await;
20625
20626 // Multiple edits
20627 assert_highlighted_edits(
20628 "Hello, world!",
20629 vec![
20630 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
20631 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
20632 ],
20633 false,
20634 cx,
20635 |highlighted_edits, cx| {
20636 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
20637 assert_eq!(highlighted_edits.highlights.len(), 2);
20638 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
20639 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
20640 assert_eq!(
20641 highlighted_edits.highlights[0].1.background_color,
20642 Some(cx.theme().status().created_background)
20643 );
20644 assert_eq!(
20645 highlighted_edits.highlights[1].1.background_color,
20646 Some(cx.theme().status().created_background)
20647 );
20648 },
20649 )
20650 .await;
20651
20652 // Multiple lines with edits
20653 assert_highlighted_edits(
20654 "First line\nSecond line\nThird line\nFourth line",
20655 vec![
20656 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
20657 (
20658 Point::new(2, 0)..Point::new(2, 10),
20659 "New third line".to_string(),
20660 ),
20661 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
20662 ],
20663 false,
20664 cx,
20665 |highlighted_edits, cx| {
20666 assert_eq!(
20667 highlighted_edits.text,
20668 "Second modified\nNew third line\nFourth updated line"
20669 );
20670 assert_eq!(highlighted_edits.highlights.len(), 3);
20671 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
20672 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
20673 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
20674 for highlight in &highlighted_edits.highlights {
20675 assert_eq!(
20676 highlight.1.background_color,
20677 Some(cx.theme().status().created_background)
20678 );
20679 }
20680 },
20681 )
20682 .await;
20683}
20684
20685#[gpui::test]
20686async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
20687 init_test(cx, |_| {});
20688
20689 // Deletion
20690 assert_highlighted_edits(
20691 "Hello, world!",
20692 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
20693 true,
20694 cx,
20695 |highlighted_edits, cx| {
20696 assert_eq!(highlighted_edits.text, "Hello, world!");
20697 assert_eq!(highlighted_edits.highlights.len(), 1);
20698 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
20699 assert_eq!(
20700 highlighted_edits.highlights[0].1.background_color,
20701 Some(cx.theme().status().deleted_background)
20702 );
20703 },
20704 )
20705 .await;
20706
20707 // Insertion
20708 assert_highlighted_edits(
20709 "Hello, world!",
20710 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
20711 true,
20712 cx,
20713 |highlighted_edits, cx| {
20714 assert_eq!(highlighted_edits.highlights.len(), 1);
20715 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
20716 assert_eq!(
20717 highlighted_edits.highlights[0].1.background_color,
20718 Some(cx.theme().status().created_background)
20719 );
20720 },
20721 )
20722 .await;
20723}
20724
20725async fn assert_highlighted_edits(
20726 text: &str,
20727 edits: Vec<(Range<Point>, String)>,
20728 include_deletions: bool,
20729 cx: &mut TestAppContext,
20730 assertion_fn: impl Fn(HighlightedText, &App),
20731) {
20732 let window = cx.add_window(|window, cx| {
20733 let buffer = MultiBuffer::build_simple(text, cx);
20734 Editor::new(EditorMode::full(), buffer, None, window, cx)
20735 });
20736 let cx = &mut VisualTestContext::from_window(*window, cx);
20737
20738 let (buffer, snapshot) = window
20739 .update(cx, |editor, _window, cx| {
20740 (
20741 editor.buffer().clone(),
20742 editor.buffer().read(cx).snapshot(cx),
20743 )
20744 })
20745 .unwrap();
20746
20747 let edits = edits
20748 .into_iter()
20749 .map(|(range, edit)| {
20750 (
20751 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
20752 edit,
20753 )
20754 })
20755 .collect::<Vec<_>>();
20756
20757 let text_anchor_edits = edits
20758 .clone()
20759 .into_iter()
20760 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
20761 .collect::<Vec<_>>();
20762
20763 let edit_preview = window
20764 .update(cx, |_, _window, cx| {
20765 buffer
20766 .read(cx)
20767 .as_singleton()
20768 .unwrap()
20769 .read(cx)
20770 .preview_edits(text_anchor_edits.into(), cx)
20771 })
20772 .unwrap()
20773 .await;
20774
20775 cx.update(|_window, cx| {
20776 let highlighted_edits = edit_prediction_edit_text(
20777 &snapshot.as_singleton().unwrap().2,
20778 &edits,
20779 &edit_preview,
20780 include_deletions,
20781 cx,
20782 );
20783 assertion_fn(highlighted_edits, cx)
20784 });
20785}
20786
20787#[track_caller]
20788fn assert_breakpoint(
20789 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
20790 path: &Arc<Path>,
20791 expected: Vec<(u32, Breakpoint)>,
20792) {
20793 if expected.len() == 0usize {
20794 assert!(!breakpoints.contains_key(path), "{}", path.display());
20795 } else {
20796 let mut breakpoint = breakpoints
20797 .get(path)
20798 .unwrap()
20799 .into_iter()
20800 .map(|breakpoint| {
20801 (
20802 breakpoint.row,
20803 Breakpoint {
20804 message: breakpoint.message.clone(),
20805 state: breakpoint.state,
20806 condition: breakpoint.condition.clone(),
20807 hit_condition: breakpoint.hit_condition.clone(),
20808 },
20809 )
20810 })
20811 .collect::<Vec<_>>();
20812
20813 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
20814
20815 assert_eq!(expected, breakpoint);
20816 }
20817}
20818
20819fn add_log_breakpoint_at_cursor(
20820 editor: &mut Editor,
20821 log_message: &str,
20822 window: &mut Window,
20823 cx: &mut Context<Editor>,
20824) {
20825 let (anchor, bp) = editor
20826 .breakpoints_at_cursors(window, cx)
20827 .first()
20828 .and_then(|(anchor, bp)| {
20829 if let Some(bp) = bp {
20830 Some((*anchor, bp.clone()))
20831 } else {
20832 None
20833 }
20834 })
20835 .unwrap_or_else(|| {
20836 let cursor_position: Point = editor.selections.newest(cx).head();
20837
20838 let breakpoint_position = editor
20839 .snapshot(window, cx)
20840 .display_snapshot
20841 .buffer_snapshot
20842 .anchor_before(Point::new(cursor_position.row, 0));
20843
20844 (breakpoint_position, Breakpoint::new_log(&log_message))
20845 });
20846
20847 editor.edit_breakpoint_at_anchor(
20848 anchor,
20849 bp,
20850 BreakpointEditAction::EditLogMessage(log_message.into()),
20851 cx,
20852 );
20853}
20854
20855#[gpui::test]
20856async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
20857 init_test(cx, |_| {});
20858
20859 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20860 let fs = FakeFs::new(cx.executor());
20861 fs.insert_tree(
20862 path!("/a"),
20863 json!({
20864 "main.rs": sample_text,
20865 }),
20866 )
20867 .await;
20868 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20869 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20870 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20871
20872 let fs = FakeFs::new(cx.executor());
20873 fs.insert_tree(
20874 path!("/a"),
20875 json!({
20876 "main.rs": sample_text,
20877 }),
20878 )
20879 .await;
20880 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20881 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20882 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20883 let worktree_id = workspace
20884 .update(cx, |workspace, _window, cx| {
20885 workspace.project().update(cx, |project, cx| {
20886 project.worktrees(cx).next().unwrap().read(cx).id()
20887 })
20888 })
20889 .unwrap();
20890
20891 let buffer = project
20892 .update(cx, |project, cx| {
20893 project.open_buffer((worktree_id, "main.rs"), cx)
20894 })
20895 .await
20896 .unwrap();
20897
20898 let (editor, cx) = cx.add_window_view(|window, cx| {
20899 Editor::new(
20900 EditorMode::full(),
20901 MultiBuffer::build_from_buffer(buffer, cx),
20902 Some(project.clone()),
20903 window,
20904 cx,
20905 )
20906 });
20907
20908 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20909 let abs_path = project.read_with(cx, |project, cx| {
20910 project
20911 .absolute_path(&project_path, cx)
20912 .map(|path_buf| Arc::from(path_buf.to_owned()))
20913 .unwrap()
20914 });
20915
20916 // assert we can add breakpoint on the first line
20917 editor.update_in(cx, |editor, window, cx| {
20918 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20919 editor.move_to_end(&MoveToEnd, window, cx);
20920 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20921 });
20922
20923 let breakpoints = editor.update(cx, |editor, cx| {
20924 editor
20925 .breakpoint_store()
20926 .as_ref()
20927 .unwrap()
20928 .read(cx)
20929 .all_source_breakpoints(cx)
20930 .clone()
20931 });
20932
20933 assert_eq!(1, breakpoints.len());
20934 assert_breakpoint(
20935 &breakpoints,
20936 &abs_path,
20937 vec![
20938 (0, Breakpoint::new_standard()),
20939 (3, Breakpoint::new_standard()),
20940 ],
20941 );
20942
20943 editor.update_in(cx, |editor, window, cx| {
20944 editor.move_to_beginning(&MoveToBeginning, window, cx);
20945 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20946 });
20947
20948 let breakpoints = editor.update(cx, |editor, cx| {
20949 editor
20950 .breakpoint_store()
20951 .as_ref()
20952 .unwrap()
20953 .read(cx)
20954 .all_source_breakpoints(cx)
20955 .clone()
20956 });
20957
20958 assert_eq!(1, breakpoints.len());
20959 assert_breakpoint(
20960 &breakpoints,
20961 &abs_path,
20962 vec![(3, Breakpoint::new_standard())],
20963 );
20964
20965 editor.update_in(cx, |editor, window, cx| {
20966 editor.move_to_end(&MoveToEnd, window, cx);
20967 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20968 });
20969
20970 let breakpoints = editor.update(cx, |editor, cx| {
20971 editor
20972 .breakpoint_store()
20973 .as_ref()
20974 .unwrap()
20975 .read(cx)
20976 .all_source_breakpoints(cx)
20977 .clone()
20978 });
20979
20980 assert_eq!(0, breakpoints.len());
20981 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20982}
20983
20984#[gpui::test]
20985async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
20986 init_test(cx, |_| {});
20987
20988 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20989
20990 let fs = FakeFs::new(cx.executor());
20991 fs.insert_tree(
20992 path!("/a"),
20993 json!({
20994 "main.rs": sample_text,
20995 }),
20996 )
20997 .await;
20998 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20999 let (workspace, cx) =
21000 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21001
21002 let worktree_id = workspace.update(cx, |workspace, cx| {
21003 workspace.project().update(cx, |project, cx| {
21004 project.worktrees(cx).next().unwrap().read(cx).id()
21005 })
21006 });
21007
21008 let buffer = project
21009 .update(cx, |project, cx| {
21010 project.open_buffer((worktree_id, "main.rs"), cx)
21011 })
21012 .await
21013 .unwrap();
21014
21015 let (editor, cx) = cx.add_window_view(|window, cx| {
21016 Editor::new(
21017 EditorMode::full(),
21018 MultiBuffer::build_from_buffer(buffer, cx),
21019 Some(project.clone()),
21020 window,
21021 cx,
21022 )
21023 });
21024
21025 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
21026 let abs_path = project.read_with(cx, |project, cx| {
21027 project
21028 .absolute_path(&project_path, cx)
21029 .map(|path_buf| Arc::from(path_buf.to_owned()))
21030 .unwrap()
21031 });
21032
21033 editor.update_in(cx, |editor, window, cx| {
21034 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
21035 });
21036
21037 let breakpoints = editor.update(cx, |editor, cx| {
21038 editor
21039 .breakpoint_store()
21040 .as_ref()
21041 .unwrap()
21042 .read(cx)
21043 .all_source_breakpoints(cx)
21044 .clone()
21045 });
21046
21047 assert_breakpoint(
21048 &breakpoints,
21049 &abs_path,
21050 vec![(0, Breakpoint::new_log("hello world"))],
21051 );
21052
21053 // Removing a log message from a log breakpoint should remove it
21054 editor.update_in(cx, |editor, window, cx| {
21055 add_log_breakpoint_at_cursor(editor, "", window, cx);
21056 });
21057
21058 let breakpoints = editor.update(cx, |editor, cx| {
21059 editor
21060 .breakpoint_store()
21061 .as_ref()
21062 .unwrap()
21063 .read(cx)
21064 .all_source_breakpoints(cx)
21065 .clone()
21066 });
21067
21068 assert_breakpoint(&breakpoints, &abs_path, vec![]);
21069
21070 editor.update_in(cx, |editor, window, cx| {
21071 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21072 editor.move_to_end(&MoveToEnd, window, cx);
21073 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21074 // Not adding a log message to a standard breakpoint shouldn't remove it
21075 add_log_breakpoint_at_cursor(editor, "", window, cx);
21076 });
21077
21078 let breakpoints = editor.update(cx, |editor, cx| {
21079 editor
21080 .breakpoint_store()
21081 .as_ref()
21082 .unwrap()
21083 .read(cx)
21084 .all_source_breakpoints(cx)
21085 .clone()
21086 });
21087
21088 assert_breakpoint(
21089 &breakpoints,
21090 &abs_path,
21091 vec![
21092 (0, Breakpoint::new_standard()),
21093 (3, Breakpoint::new_standard()),
21094 ],
21095 );
21096
21097 editor.update_in(cx, |editor, window, cx| {
21098 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
21099 });
21100
21101 let breakpoints = editor.update(cx, |editor, cx| {
21102 editor
21103 .breakpoint_store()
21104 .as_ref()
21105 .unwrap()
21106 .read(cx)
21107 .all_source_breakpoints(cx)
21108 .clone()
21109 });
21110
21111 assert_breakpoint(
21112 &breakpoints,
21113 &abs_path,
21114 vec![
21115 (0, Breakpoint::new_standard()),
21116 (3, Breakpoint::new_log("hello world")),
21117 ],
21118 );
21119
21120 editor.update_in(cx, |editor, window, cx| {
21121 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
21122 });
21123
21124 let breakpoints = editor.update(cx, |editor, cx| {
21125 editor
21126 .breakpoint_store()
21127 .as_ref()
21128 .unwrap()
21129 .read(cx)
21130 .all_source_breakpoints(cx)
21131 .clone()
21132 });
21133
21134 assert_breakpoint(
21135 &breakpoints,
21136 &abs_path,
21137 vec![
21138 (0, Breakpoint::new_standard()),
21139 (3, Breakpoint::new_log("hello Earth!!")),
21140 ],
21141 );
21142}
21143
21144/// This also tests that Editor::breakpoint_at_cursor_head is working properly
21145/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
21146/// or when breakpoints were placed out of order. This tests for a regression too
21147#[gpui::test]
21148async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
21149 init_test(cx, |_| {});
21150
21151 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
21152 let fs = FakeFs::new(cx.executor());
21153 fs.insert_tree(
21154 path!("/a"),
21155 json!({
21156 "main.rs": sample_text,
21157 }),
21158 )
21159 .await;
21160 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21161 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21162 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21163
21164 let fs = FakeFs::new(cx.executor());
21165 fs.insert_tree(
21166 path!("/a"),
21167 json!({
21168 "main.rs": sample_text,
21169 }),
21170 )
21171 .await;
21172 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21173 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21174 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21175 let worktree_id = workspace
21176 .update(cx, |workspace, _window, cx| {
21177 workspace.project().update(cx, |project, cx| {
21178 project.worktrees(cx).next().unwrap().read(cx).id()
21179 })
21180 })
21181 .unwrap();
21182
21183 let buffer = project
21184 .update(cx, |project, cx| {
21185 project.open_buffer((worktree_id, "main.rs"), cx)
21186 })
21187 .await
21188 .unwrap();
21189
21190 let (editor, cx) = cx.add_window_view(|window, cx| {
21191 Editor::new(
21192 EditorMode::full(),
21193 MultiBuffer::build_from_buffer(buffer, cx),
21194 Some(project.clone()),
21195 window,
21196 cx,
21197 )
21198 });
21199
21200 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
21201 let abs_path = project.read_with(cx, |project, cx| {
21202 project
21203 .absolute_path(&project_path, cx)
21204 .map(|path_buf| Arc::from(path_buf.to_owned()))
21205 .unwrap()
21206 });
21207
21208 // assert we can add breakpoint on the first line
21209 editor.update_in(cx, |editor, window, cx| {
21210 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21211 editor.move_to_end(&MoveToEnd, window, cx);
21212 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21213 editor.move_up(&MoveUp, window, cx);
21214 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21215 });
21216
21217 let breakpoints = editor.update(cx, |editor, cx| {
21218 editor
21219 .breakpoint_store()
21220 .as_ref()
21221 .unwrap()
21222 .read(cx)
21223 .all_source_breakpoints(cx)
21224 .clone()
21225 });
21226
21227 assert_eq!(1, breakpoints.len());
21228 assert_breakpoint(
21229 &breakpoints,
21230 &abs_path,
21231 vec![
21232 (0, Breakpoint::new_standard()),
21233 (2, Breakpoint::new_standard()),
21234 (3, Breakpoint::new_standard()),
21235 ],
21236 );
21237
21238 editor.update_in(cx, |editor, window, cx| {
21239 editor.move_to_beginning(&MoveToBeginning, window, cx);
21240 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21241 editor.move_to_end(&MoveToEnd, window, cx);
21242 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21243 // Disabling a breakpoint that doesn't exist should do nothing
21244 editor.move_up(&MoveUp, window, cx);
21245 editor.move_up(&MoveUp, window, cx);
21246 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21247 });
21248
21249 let breakpoints = editor.update(cx, |editor, cx| {
21250 editor
21251 .breakpoint_store()
21252 .as_ref()
21253 .unwrap()
21254 .read(cx)
21255 .all_source_breakpoints(cx)
21256 .clone()
21257 });
21258
21259 let disable_breakpoint = {
21260 let mut bp = Breakpoint::new_standard();
21261 bp.state = BreakpointState::Disabled;
21262 bp
21263 };
21264
21265 assert_eq!(1, breakpoints.len());
21266 assert_breakpoint(
21267 &breakpoints,
21268 &abs_path,
21269 vec![
21270 (0, disable_breakpoint.clone()),
21271 (2, Breakpoint::new_standard()),
21272 (3, disable_breakpoint.clone()),
21273 ],
21274 );
21275
21276 editor.update_in(cx, |editor, window, cx| {
21277 editor.move_to_beginning(&MoveToBeginning, window, cx);
21278 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
21279 editor.move_to_end(&MoveToEnd, window, cx);
21280 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
21281 editor.move_up(&MoveUp, window, cx);
21282 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21283 });
21284
21285 let breakpoints = editor.update(cx, |editor, cx| {
21286 editor
21287 .breakpoint_store()
21288 .as_ref()
21289 .unwrap()
21290 .read(cx)
21291 .all_source_breakpoints(cx)
21292 .clone()
21293 });
21294
21295 assert_eq!(1, breakpoints.len());
21296 assert_breakpoint(
21297 &breakpoints,
21298 &abs_path,
21299 vec![
21300 (0, Breakpoint::new_standard()),
21301 (2, disable_breakpoint),
21302 (3, Breakpoint::new_standard()),
21303 ],
21304 );
21305}
21306
21307#[gpui::test]
21308async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
21309 init_test(cx, |_| {});
21310 let capabilities = lsp::ServerCapabilities {
21311 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
21312 prepare_provider: Some(true),
21313 work_done_progress_options: Default::default(),
21314 })),
21315 ..Default::default()
21316 };
21317 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21318
21319 cx.set_state(indoc! {"
21320 struct Fˇoo {}
21321 "});
21322
21323 cx.update_editor(|editor, _, cx| {
21324 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21325 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21326 editor.highlight_background::<DocumentHighlightRead>(
21327 &[highlight_range],
21328 |theme| theme.colors().editor_document_highlight_read_background,
21329 cx,
21330 );
21331 });
21332
21333 let mut prepare_rename_handler = cx
21334 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
21335 move |_, _, _| async move {
21336 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
21337 start: lsp::Position {
21338 line: 0,
21339 character: 7,
21340 },
21341 end: lsp::Position {
21342 line: 0,
21343 character: 10,
21344 },
21345 })))
21346 },
21347 );
21348 let prepare_rename_task = cx
21349 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21350 .expect("Prepare rename was not started");
21351 prepare_rename_handler.next().await.unwrap();
21352 prepare_rename_task.await.expect("Prepare rename failed");
21353
21354 let mut rename_handler =
21355 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21356 let edit = lsp::TextEdit {
21357 range: lsp::Range {
21358 start: lsp::Position {
21359 line: 0,
21360 character: 7,
21361 },
21362 end: lsp::Position {
21363 line: 0,
21364 character: 10,
21365 },
21366 },
21367 new_text: "FooRenamed".to_string(),
21368 };
21369 Ok(Some(lsp::WorkspaceEdit::new(
21370 // Specify the same edit twice
21371 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
21372 )))
21373 });
21374 let rename_task = cx
21375 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21376 .expect("Confirm rename was not started");
21377 rename_handler.next().await.unwrap();
21378 rename_task.await.expect("Confirm rename failed");
21379 cx.run_until_parked();
21380
21381 // Despite two edits, only one is actually applied as those are identical
21382 cx.assert_editor_state(indoc! {"
21383 struct FooRenamedˇ {}
21384 "});
21385}
21386
21387#[gpui::test]
21388async fn test_rename_without_prepare(cx: &mut TestAppContext) {
21389 init_test(cx, |_| {});
21390 // These capabilities indicate that the server does not support prepare rename.
21391 let capabilities = lsp::ServerCapabilities {
21392 rename_provider: Some(lsp::OneOf::Left(true)),
21393 ..Default::default()
21394 };
21395 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21396
21397 cx.set_state(indoc! {"
21398 struct Fˇoo {}
21399 "});
21400
21401 cx.update_editor(|editor, _window, cx| {
21402 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21403 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21404 editor.highlight_background::<DocumentHighlightRead>(
21405 &[highlight_range],
21406 |theme| theme.colors().editor_document_highlight_read_background,
21407 cx,
21408 );
21409 });
21410
21411 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21412 .expect("Prepare rename was not started")
21413 .await
21414 .expect("Prepare rename failed");
21415
21416 let mut rename_handler =
21417 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21418 let edit = lsp::TextEdit {
21419 range: lsp::Range {
21420 start: lsp::Position {
21421 line: 0,
21422 character: 7,
21423 },
21424 end: lsp::Position {
21425 line: 0,
21426 character: 10,
21427 },
21428 },
21429 new_text: "FooRenamed".to_string(),
21430 };
21431 Ok(Some(lsp::WorkspaceEdit::new(
21432 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
21433 )))
21434 });
21435 let rename_task = cx
21436 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21437 .expect("Confirm rename was not started");
21438 rename_handler.next().await.unwrap();
21439 rename_task.await.expect("Confirm rename failed");
21440 cx.run_until_parked();
21441
21442 // Correct range is renamed, as `surrounding_word` is used to find it.
21443 cx.assert_editor_state(indoc! {"
21444 struct FooRenamedˇ {}
21445 "});
21446}
21447
21448#[gpui::test]
21449async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
21450 init_test(cx, |_| {});
21451 let mut cx = EditorTestContext::new(cx).await;
21452
21453 let language = Arc::new(
21454 Language::new(
21455 LanguageConfig::default(),
21456 Some(tree_sitter_html::LANGUAGE.into()),
21457 )
21458 .with_brackets_query(
21459 r#"
21460 ("<" @open "/>" @close)
21461 ("</" @open ">" @close)
21462 ("<" @open ">" @close)
21463 ("\"" @open "\"" @close)
21464 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
21465 "#,
21466 )
21467 .unwrap(),
21468 );
21469 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21470
21471 cx.set_state(indoc! {"
21472 <span>ˇ</span>
21473 "});
21474 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21475 cx.assert_editor_state(indoc! {"
21476 <span>
21477 ˇ
21478 </span>
21479 "});
21480
21481 cx.set_state(indoc! {"
21482 <span><span></span>ˇ</span>
21483 "});
21484 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21485 cx.assert_editor_state(indoc! {"
21486 <span><span></span>
21487 ˇ</span>
21488 "});
21489
21490 cx.set_state(indoc! {"
21491 <span>ˇ
21492 </span>
21493 "});
21494 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21495 cx.assert_editor_state(indoc! {"
21496 <span>
21497 ˇ
21498 </span>
21499 "});
21500}
21501
21502#[gpui::test(iterations = 10)]
21503async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
21504 init_test(cx, |_| {});
21505
21506 let fs = FakeFs::new(cx.executor());
21507 fs.insert_tree(
21508 path!("/dir"),
21509 json!({
21510 "a.ts": "a",
21511 }),
21512 )
21513 .await;
21514
21515 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
21516 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21517 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21518
21519 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21520 language_registry.add(Arc::new(Language::new(
21521 LanguageConfig {
21522 name: "TypeScript".into(),
21523 matcher: LanguageMatcher {
21524 path_suffixes: vec!["ts".to_string()],
21525 ..Default::default()
21526 },
21527 ..Default::default()
21528 },
21529 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
21530 )));
21531 let mut fake_language_servers = language_registry.register_fake_lsp(
21532 "TypeScript",
21533 FakeLspAdapter {
21534 capabilities: lsp::ServerCapabilities {
21535 code_lens_provider: Some(lsp::CodeLensOptions {
21536 resolve_provider: Some(true),
21537 }),
21538 execute_command_provider: Some(lsp::ExecuteCommandOptions {
21539 commands: vec!["_the/command".to_string()],
21540 ..lsp::ExecuteCommandOptions::default()
21541 }),
21542 ..lsp::ServerCapabilities::default()
21543 },
21544 ..FakeLspAdapter::default()
21545 },
21546 );
21547
21548 let editor = workspace
21549 .update(cx, |workspace, window, cx| {
21550 workspace.open_abs_path(
21551 PathBuf::from(path!("/dir/a.ts")),
21552 OpenOptions::default(),
21553 window,
21554 cx,
21555 )
21556 })
21557 .unwrap()
21558 .await
21559 .unwrap()
21560 .downcast::<Editor>()
21561 .unwrap();
21562 cx.executor().run_until_parked();
21563
21564 let fake_server = fake_language_servers.next().await.unwrap();
21565
21566 let buffer = editor.update(cx, |editor, cx| {
21567 editor
21568 .buffer()
21569 .read(cx)
21570 .as_singleton()
21571 .expect("have opened a single file by path")
21572 });
21573
21574 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
21575 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
21576 drop(buffer_snapshot);
21577 let actions = cx
21578 .update_window(*workspace, |_, window, cx| {
21579 project.code_actions(&buffer, anchor..anchor, window, cx)
21580 })
21581 .unwrap();
21582
21583 fake_server
21584 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21585 Ok(Some(vec![
21586 lsp::CodeLens {
21587 range: lsp::Range::default(),
21588 command: Some(lsp::Command {
21589 title: "Code lens command".to_owned(),
21590 command: "_the/command".to_owned(),
21591 arguments: None,
21592 }),
21593 data: None,
21594 },
21595 lsp::CodeLens {
21596 range: lsp::Range::default(),
21597 command: Some(lsp::Command {
21598 title: "Command not in capabilities".to_owned(),
21599 command: "not in capabilities".to_owned(),
21600 arguments: None,
21601 }),
21602 data: None,
21603 },
21604 lsp::CodeLens {
21605 range: lsp::Range {
21606 start: lsp::Position {
21607 line: 1,
21608 character: 1,
21609 },
21610 end: lsp::Position {
21611 line: 1,
21612 character: 1,
21613 },
21614 },
21615 command: Some(lsp::Command {
21616 title: "Command not in range".to_owned(),
21617 command: "_the/command".to_owned(),
21618 arguments: None,
21619 }),
21620 data: None,
21621 },
21622 ]))
21623 })
21624 .next()
21625 .await;
21626
21627 let actions = actions.await.unwrap();
21628 assert_eq!(
21629 actions.len(),
21630 1,
21631 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
21632 );
21633 let action = actions[0].clone();
21634 let apply = project.update(cx, |project, cx| {
21635 project.apply_code_action(buffer.clone(), action, true, cx)
21636 });
21637
21638 // Resolving the code action does not populate its edits. In absence of
21639 // edits, we must execute the given command.
21640 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
21641 |mut lens, _| async move {
21642 let lens_command = lens.command.as_mut().expect("should have a command");
21643 assert_eq!(lens_command.title, "Code lens command");
21644 lens_command.arguments = Some(vec![json!("the-argument")]);
21645 Ok(lens)
21646 },
21647 );
21648
21649 // While executing the command, the language server sends the editor
21650 // a `workspaceEdit` request.
21651 fake_server
21652 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
21653 let fake = fake_server.clone();
21654 move |params, _| {
21655 assert_eq!(params.command, "_the/command");
21656 let fake = fake.clone();
21657 async move {
21658 fake.server
21659 .request::<lsp::request::ApplyWorkspaceEdit>(
21660 lsp::ApplyWorkspaceEditParams {
21661 label: None,
21662 edit: lsp::WorkspaceEdit {
21663 changes: Some(
21664 [(
21665 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
21666 vec![lsp::TextEdit {
21667 range: lsp::Range::new(
21668 lsp::Position::new(0, 0),
21669 lsp::Position::new(0, 0),
21670 ),
21671 new_text: "X".into(),
21672 }],
21673 )]
21674 .into_iter()
21675 .collect(),
21676 ),
21677 ..lsp::WorkspaceEdit::default()
21678 },
21679 },
21680 )
21681 .await
21682 .into_response()
21683 .unwrap();
21684 Ok(Some(json!(null)))
21685 }
21686 }
21687 })
21688 .next()
21689 .await;
21690
21691 // Applying the code lens command returns a project transaction containing the edits
21692 // sent by the language server in its `workspaceEdit` request.
21693 let transaction = apply.await.unwrap();
21694 assert!(transaction.0.contains_key(&buffer));
21695 buffer.update(cx, |buffer, cx| {
21696 assert_eq!(buffer.text(), "Xa");
21697 buffer.undo(cx);
21698 assert_eq!(buffer.text(), "a");
21699 });
21700
21701 let actions_after_edits = cx
21702 .update_window(*workspace, |_, window, cx| {
21703 project.code_actions(&buffer, anchor..anchor, window, cx)
21704 })
21705 .unwrap()
21706 .await
21707 .unwrap();
21708 assert_eq!(
21709 actions, actions_after_edits,
21710 "For the same selection, same code lens actions should be returned"
21711 );
21712
21713 let _responses =
21714 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21715 panic!("No more code lens requests are expected");
21716 });
21717 editor.update_in(cx, |editor, window, cx| {
21718 editor.select_all(&SelectAll, window, cx);
21719 });
21720 cx.executor().run_until_parked();
21721 let new_actions = cx
21722 .update_window(*workspace, |_, window, cx| {
21723 project.code_actions(&buffer, anchor..anchor, window, cx)
21724 })
21725 .unwrap()
21726 .await
21727 .unwrap();
21728 assert_eq!(
21729 actions, new_actions,
21730 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
21731 );
21732}
21733
21734#[gpui::test]
21735async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
21736 init_test(cx, |_| {});
21737
21738 let fs = FakeFs::new(cx.executor());
21739 let main_text = r#"fn main() {
21740println!("1");
21741println!("2");
21742println!("3");
21743println!("4");
21744println!("5");
21745}"#;
21746 let lib_text = "mod foo {}";
21747 fs.insert_tree(
21748 path!("/a"),
21749 json!({
21750 "lib.rs": lib_text,
21751 "main.rs": main_text,
21752 }),
21753 )
21754 .await;
21755
21756 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21757 let (workspace, cx) =
21758 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21759 let worktree_id = workspace.update(cx, |workspace, cx| {
21760 workspace.project().update(cx, |project, cx| {
21761 project.worktrees(cx).next().unwrap().read(cx).id()
21762 })
21763 });
21764
21765 let expected_ranges = vec![
21766 Point::new(0, 0)..Point::new(0, 0),
21767 Point::new(1, 0)..Point::new(1, 1),
21768 Point::new(2, 0)..Point::new(2, 2),
21769 Point::new(3, 0)..Point::new(3, 3),
21770 ];
21771
21772 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21773 let editor_1 = workspace
21774 .update_in(cx, |workspace, window, cx| {
21775 workspace.open_path(
21776 (worktree_id, "main.rs"),
21777 Some(pane_1.downgrade()),
21778 true,
21779 window,
21780 cx,
21781 )
21782 })
21783 .unwrap()
21784 .await
21785 .downcast::<Editor>()
21786 .unwrap();
21787 pane_1.update(cx, |pane, cx| {
21788 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21789 open_editor.update(cx, |editor, cx| {
21790 assert_eq!(
21791 editor.display_text(cx),
21792 main_text,
21793 "Original main.rs text on initial open",
21794 );
21795 assert_eq!(
21796 editor
21797 .selections
21798 .all::<Point>(cx)
21799 .into_iter()
21800 .map(|s| s.range())
21801 .collect::<Vec<_>>(),
21802 vec![Point::zero()..Point::zero()],
21803 "Default selections on initial open",
21804 );
21805 })
21806 });
21807 editor_1.update_in(cx, |editor, window, cx| {
21808 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21809 s.select_ranges(expected_ranges.clone());
21810 });
21811 });
21812
21813 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
21814 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
21815 });
21816 let editor_2 = workspace
21817 .update_in(cx, |workspace, window, cx| {
21818 workspace.open_path(
21819 (worktree_id, "main.rs"),
21820 Some(pane_2.downgrade()),
21821 true,
21822 window,
21823 cx,
21824 )
21825 })
21826 .unwrap()
21827 .await
21828 .downcast::<Editor>()
21829 .unwrap();
21830 pane_2.update(cx, |pane, cx| {
21831 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21832 open_editor.update(cx, |editor, cx| {
21833 assert_eq!(
21834 editor.display_text(cx),
21835 main_text,
21836 "Original main.rs text on initial open in another panel",
21837 );
21838 assert_eq!(
21839 editor
21840 .selections
21841 .all::<Point>(cx)
21842 .into_iter()
21843 .map(|s| s.range())
21844 .collect::<Vec<_>>(),
21845 vec![Point::zero()..Point::zero()],
21846 "Default selections on initial open in another panel",
21847 );
21848 })
21849 });
21850
21851 editor_2.update_in(cx, |editor, window, cx| {
21852 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
21853 });
21854
21855 let _other_editor_1 = workspace
21856 .update_in(cx, |workspace, window, cx| {
21857 workspace.open_path(
21858 (worktree_id, "lib.rs"),
21859 Some(pane_1.downgrade()),
21860 true,
21861 window,
21862 cx,
21863 )
21864 })
21865 .unwrap()
21866 .await
21867 .downcast::<Editor>()
21868 .unwrap();
21869 pane_1
21870 .update_in(cx, |pane, window, cx| {
21871 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
21872 })
21873 .await
21874 .unwrap();
21875 drop(editor_1);
21876 pane_1.update(cx, |pane, cx| {
21877 pane.active_item()
21878 .unwrap()
21879 .downcast::<Editor>()
21880 .unwrap()
21881 .update(cx, |editor, cx| {
21882 assert_eq!(
21883 editor.display_text(cx),
21884 lib_text,
21885 "Other file should be open and active",
21886 );
21887 });
21888 assert_eq!(pane.items().count(), 1, "No other editors should be open");
21889 });
21890
21891 let _other_editor_2 = workspace
21892 .update_in(cx, |workspace, window, cx| {
21893 workspace.open_path(
21894 (worktree_id, "lib.rs"),
21895 Some(pane_2.downgrade()),
21896 true,
21897 window,
21898 cx,
21899 )
21900 })
21901 .unwrap()
21902 .await
21903 .downcast::<Editor>()
21904 .unwrap();
21905 pane_2
21906 .update_in(cx, |pane, window, cx| {
21907 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
21908 })
21909 .await
21910 .unwrap();
21911 drop(editor_2);
21912 pane_2.update(cx, |pane, cx| {
21913 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21914 open_editor.update(cx, |editor, cx| {
21915 assert_eq!(
21916 editor.display_text(cx),
21917 lib_text,
21918 "Other file should be open and active in another panel too",
21919 );
21920 });
21921 assert_eq!(
21922 pane.items().count(),
21923 1,
21924 "No other editors should be open in another pane",
21925 );
21926 });
21927
21928 let _editor_1_reopened = workspace
21929 .update_in(cx, |workspace, window, cx| {
21930 workspace.open_path(
21931 (worktree_id, "main.rs"),
21932 Some(pane_1.downgrade()),
21933 true,
21934 window,
21935 cx,
21936 )
21937 })
21938 .unwrap()
21939 .await
21940 .downcast::<Editor>()
21941 .unwrap();
21942 let _editor_2_reopened = workspace
21943 .update_in(cx, |workspace, window, cx| {
21944 workspace.open_path(
21945 (worktree_id, "main.rs"),
21946 Some(pane_2.downgrade()),
21947 true,
21948 window,
21949 cx,
21950 )
21951 })
21952 .unwrap()
21953 .await
21954 .downcast::<Editor>()
21955 .unwrap();
21956 pane_1.update(cx, |pane, cx| {
21957 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21958 open_editor.update(cx, |editor, cx| {
21959 assert_eq!(
21960 editor.display_text(cx),
21961 main_text,
21962 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
21963 );
21964 assert_eq!(
21965 editor
21966 .selections
21967 .all::<Point>(cx)
21968 .into_iter()
21969 .map(|s| s.range())
21970 .collect::<Vec<_>>(),
21971 expected_ranges,
21972 "Previous editor in the 1st panel had selections and should get them restored on reopen",
21973 );
21974 })
21975 });
21976 pane_2.update(cx, |pane, cx| {
21977 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21978 open_editor.update(cx, |editor, cx| {
21979 assert_eq!(
21980 editor.display_text(cx),
21981 r#"fn main() {
21982⋯rintln!("1");
21983⋯intln!("2");
21984⋯ntln!("3");
21985println!("4");
21986println!("5");
21987}"#,
21988 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
21989 );
21990 assert_eq!(
21991 editor
21992 .selections
21993 .all::<Point>(cx)
21994 .into_iter()
21995 .map(|s| s.range())
21996 .collect::<Vec<_>>(),
21997 vec![Point::zero()..Point::zero()],
21998 "Previous editor in the 2nd pane had no selections changed hence should restore none",
21999 );
22000 })
22001 });
22002}
22003
22004#[gpui::test]
22005async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
22006 init_test(cx, |_| {});
22007
22008 let fs = FakeFs::new(cx.executor());
22009 let main_text = r#"fn main() {
22010println!("1");
22011println!("2");
22012println!("3");
22013println!("4");
22014println!("5");
22015}"#;
22016 let lib_text = "mod foo {}";
22017 fs.insert_tree(
22018 path!("/a"),
22019 json!({
22020 "lib.rs": lib_text,
22021 "main.rs": main_text,
22022 }),
22023 )
22024 .await;
22025
22026 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22027 let (workspace, cx) =
22028 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22029 let worktree_id = workspace.update(cx, |workspace, cx| {
22030 workspace.project().update(cx, |project, cx| {
22031 project.worktrees(cx).next().unwrap().read(cx).id()
22032 })
22033 });
22034
22035 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22036 let editor = workspace
22037 .update_in(cx, |workspace, window, cx| {
22038 workspace.open_path(
22039 (worktree_id, "main.rs"),
22040 Some(pane.downgrade()),
22041 true,
22042 window,
22043 cx,
22044 )
22045 })
22046 .unwrap()
22047 .await
22048 .downcast::<Editor>()
22049 .unwrap();
22050 pane.update(cx, |pane, cx| {
22051 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22052 open_editor.update(cx, |editor, cx| {
22053 assert_eq!(
22054 editor.display_text(cx),
22055 main_text,
22056 "Original main.rs text on initial open",
22057 );
22058 })
22059 });
22060 editor.update_in(cx, |editor, window, cx| {
22061 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
22062 });
22063
22064 cx.update_global(|store: &mut SettingsStore, cx| {
22065 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
22066 s.restore_on_file_reopen = Some(false);
22067 });
22068 });
22069 editor.update_in(cx, |editor, window, cx| {
22070 editor.fold_ranges(
22071 vec![
22072 Point::new(1, 0)..Point::new(1, 1),
22073 Point::new(2, 0)..Point::new(2, 2),
22074 Point::new(3, 0)..Point::new(3, 3),
22075 ],
22076 false,
22077 window,
22078 cx,
22079 );
22080 });
22081 pane.update_in(cx, |pane, window, cx| {
22082 pane.close_all_items(&CloseAllItems::default(), window, cx)
22083 })
22084 .await
22085 .unwrap();
22086 pane.update(cx, |pane, _| {
22087 assert!(pane.active_item().is_none());
22088 });
22089 cx.update_global(|store: &mut SettingsStore, cx| {
22090 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
22091 s.restore_on_file_reopen = Some(true);
22092 });
22093 });
22094
22095 let _editor_reopened = workspace
22096 .update_in(cx, |workspace, window, cx| {
22097 workspace.open_path(
22098 (worktree_id, "main.rs"),
22099 Some(pane.downgrade()),
22100 true,
22101 window,
22102 cx,
22103 )
22104 })
22105 .unwrap()
22106 .await
22107 .downcast::<Editor>()
22108 .unwrap();
22109 pane.update(cx, |pane, cx| {
22110 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22111 open_editor.update(cx, |editor, cx| {
22112 assert_eq!(
22113 editor.display_text(cx),
22114 main_text,
22115 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
22116 );
22117 })
22118 });
22119}
22120
22121#[gpui::test]
22122async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
22123 struct EmptyModalView {
22124 focus_handle: gpui::FocusHandle,
22125 }
22126 impl EventEmitter<DismissEvent> for EmptyModalView {}
22127 impl Render for EmptyModalView {
22128 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
22129 div()
22130 }
22131 }
22132 impl Focusable for EmptyModalView {
22133 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
22134 self.focus_handle.clone()
22135 }
22136 }
22137 impl workspace::ModalView for EmptyModalView {}
22138 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
22139 EmptyModalView {
22140 focus_handle: cx.focus_handle(),
22141 }
22142 }
22143
22144 init_test(cx, |_| {});
22145
22146 let fs = FakeFs::new(cx.executor());
22147 let project = Project::test(fs, [], cx).await;
22148 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22149 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
22150 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22151 let editor = cx.new_window_entity(|window, cx| {
22152 Editor::new(
22153 EditorMode::full(),
22154 buffer,
22155 Some(project.clone()),
22156 window,
22157 cx,
22158 )
22159 });
22160 workspace
22161 .update(cx, |workspace, window, cx| {
22162 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
22163 })
22164 .unwrap();
22165 editor.update_in(cx, |editor, window, cx| {
22166 editor.open_context_menu(&OpenContextMenu, window, cx);
22167 assert!(editor.mouse_context_menu.is_some());
22168 });
22169 workspace
22170 .update(cx, |workspace, window, cx| {
22171 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
22172 })
22173 .unwrap();
22174 cx.read(|cx| {
22175 assert!(editor.read(cx).mouse_context_menu.is_none());
22176 });
22177}
22178
22179#[gpui::test]
22180async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
22181 init_test(cx, |_| {});
22182
22183 let fs = FakeFs::new(cx.executor());
22184 fs.insert_file(path!("/file.html"), Default::default())
22185 .await;
22186
22187 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
22188
22189 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22190 let html_language = Arc::new(Language::new(
22191 LanguageConfig {
22192 name: "HTML".into(),
22193 matcher: LanguageMatcher {
22194 path_suffixes: vec!["html".to_string()],
22195 ..LanguageMatcher::default()
22196 },
22197 brackets: BracketPairConfig {
22198 pairs: vec![BracketPair {
22199 start: "<".into(),
22200 end: ">".into(),
22201 close: true,
22202 ..Default::default()
22203 }],
22204 ..Default::default()
22205 },
22206 ..Default::default()
22207 },
22208 Some(tree_sitter_html::LANGUAGE.into()),
22209 ));
22210 language_registry.add(html_language);
22211 let mut fake_servers = language_registry.register_fake_lsp(
22212 "HTML",
22213 FakeLspAdapter {
22214 capabilities: lsp::ServerCapabilities {
22215 completion_provider: Some(lsp::CompletionOptions {
22216 resolve_provider: Some(true),
22217 ..Default::default()
22218 }),
22219 ..Default::default()
22220 },
22221 ..Default::default()
22222 },
22223 );
22224
22225 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22226 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22227
22228 let worktree_id = workspace
22229 .update(cx, |workspace, _window, cx| {
22230 workspace.project().update(cx, |project, cx| {
22231 project.worktrees(cx).next().unwrap().read(cx).id()
22232 })
22233 })
22234 .unwrap();
22235 project
22236 .update(cx, |project, cx| {
22237 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
22238 })
22239 .await
22240 .unwrap();
22241 let editor = workspace
22242 .update(cx, |workspace, window, cx| {
22243 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
22244 })
22245 .unwrap()
22246 .await
22247 .unwrap()
22248 .downcast::<Editor>()
22249 .unwrap();
22250
22251 let fake_server = fake_servers.next().await.unwrap();
22252 editor.update_in(cx, |editor, window, cx| {
22253 editor.set_text("<ad></ad>", window, cx);
22254 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22255 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
22256 });
22257 let Some((buffer, _)) = editor
22258 .buffer
22259 .read(cx)
22260 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
22261 else {
22262 panic!("Failed to get buffer for selection position");
22263 };
22264 let buffer = buffer.read(cx);
22265 let buffer_id = buffer.remote_id();
22266 let opening_range =
22267 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
22268 let closing_range =
22269 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
22270 let mut linked_ranges = HashMap::default();
22271 linked_ranges.insert(
22272 buffer_id,
22273 vec![(opening_range.clone(), vec![closing_range.clone()])],
22274 );
22275 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
22276 });
22277 let mut completion_handle =
22278 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
22279 Ok(Some(lsp::CompletionResponse::Array(vec![
22280 lsp::CompletionItem {
22281 label: "head".to_string(),
22282 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22283 lsp::InsertReplaceEdit {
22284 new_text: "head".to_string(),
22285 insert: lsp::Range::new(
22286 lsp::Position::new(0, 1),
22287 lsp::Position::new(0, 3),
22288 ),
22289 replace: lsp::Range::new(
22290 lsp::Position::new(0, 1),
22291 lsp::Position::new(0, 3),
22292 ),
22293 },
22294 )),
22295 ..Default::default()
22296 },
22297 ])))
22298 });
22299 editor.update_in(cx, |editor, window, cx| {
22300 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
22301 });
22302 cx.run_until_parked();
22303 completion_handle.next().await.unwrap();
22304 editor.update(cx, |editor, _| {
22305 assert!(
22306 editor.context_menu_visible(),
22307 "Completion menu should be visible"
22308 );
22309 });
22310 editor.update_in(cx, |editor, window, cx| {
22311 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
22312 });
22313 cx.executor().run_until_parked();
22314 editor.update(cx, |editor, cx| {
22315 assert_eq!(editor.text(cx), "<head></head>");
22316 });
22317}
22318
22319#[gpui::test]
22320async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
22321 init_test(cx, |_| {});
22322
22323 let fs = FakeFs::new(cx.executor());
22324 fs.insert_tree(
22325 path!("/root"),
22326 json!({
22327 "a": {
22328 "main.rs": "fn main() {}",
22329 },
22330 "foo": {
22331 "bar": {
22332 "external_file.rs": "pub mod external {}",
22333 }
22334 }
22335 }),
22336 )
22337 .await;
22338
22339 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
22340 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22341 language_registry.add(rust_lang());
22342 let _fake_servers = language_registry.register_fake_lsp(
22343 "Rust",
22344 FakeLspAdapter {
22345 ..FakeLspAdapter::default()
22346 },
22347 );
22348 let (workspace, cx) =
22349 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22350 let worktree_id = workspace.update(cx, |workspace, cx| {
22351 workspace.project().update(cx, |project, cx| {
22352 project.worktrees(cx).next().unwrap().read(cx).id()
22353 })
22354 });
22355
22356 let assert_language_servers_count =
22357 |expected: usize, context: &str, cx: &mut VisualTestContext| {
22358 project.update(cx, |project, cx| {
22359 let current = project
22360 .lsp_store()
22361 .read(cx)
22362 .as_local()
22363 .unwrap()
22364 .language_servers
22365 .len();
22366 assert_eq!(expected, current, "{context}");
22367 });
22368 };
22369
22370 assert_language_servers_count(
22371 0,
22372 "No servers should be running before any file is open",
22373 cx,
22374 );
22375 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22376 let main_editor = workspace
22377 .update_in(cx, |workspace, window, cx| {
22378 workspace.open_path(
22379 (worktree_id, "main.rs"),
22380 Some(pane.downgrade()),
22381 true,
22382 window,
22383 cx,
22384 )
22385 })
22386 .unwrap()
22387 .await
22388 .downcast::<Editor>()
22389 .unwrap();
22390 pane.update(cx, |pane, cx| {
22391 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22392 open_editor.update(cx, |editor, cx| {
22393 assert_eq!(
22394 editor.display_text(cx),
22395 "fn main() {}",
22396 "Original main.rs text on initial open",
22397 );
22398 });
22399 assert_eq!(open_editor, main_editor);
22400 });
22401 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
22402
22403 let external_editor = workspace
22404 .update_in(cx, |workspace, window, cx| {
22405 workspace.open_abs_path(
22406 PathBuf::from("/root/foo/bar/external_file.rs"),
22407 OpenOptions::default(),
22408 window,
22409 cx,
22410 )
22411 })
22412 .await
22413 .expect("opening external file")
22414 .downcast::<Editor>()
22415 .expect("downcasted external file's open element to editor");
22416 pane.update(cx, |pane, cx| {
22417 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22418 open_editor.update(cx, |editor, cx| {
22419 assert_eq!(
22420 editor.display_text(cx),
22421 "pub mod external {}",
22422 "External file is open now",
22423 );
22424 });
22425 assert_eq!(open_editor, external_editor);
22426 });
22427 assert_language_servers_count(
22428 1,
22429 "Second, external, *.rs file should join the existing server",
22430 cx,
22431 );
22432
22433 pane.update_in(cx, |pane, window, cx| {
22434 pane.close_active_item(&CloseActiveItem::default(), window, cx)
22435 })
22436 .await
22437 .unwrap();
22438 pane.update_in(cx, |pane, window, cx| {
22439 pane.navigate_backward(window, cx);
22440 });
22441 cx.run_until_parked();
22442 pane.update(cx, |pane, cx| {
22443 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22444 open_editor.update(cx, |editor, cx| {
22445 assert_eq!(
22446 editor.display_text(cx),
22447 "pub mod external {}",
22448 "External file is open now",
22449 );
22450 });
22451 });
22452 assert_language_servers_count(
22453 1,
22454 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
22455 cx,
22456 );
22457
22458 cx.update(|_, cx| {
22459 workspace::reload(&workspace::Reload::default(), cx);
22460 });
22461 assert_language_servers_count(
22462 1,
22463 "After reloading the worktree with local and external files opened, only one project should be started",
22464 cx,
22465 );
22466}
22467
22468#[gpui::test]
22469async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
22470 init_test(cx, |_| {});
22471
22472 let mut cx = EditorTestContext::new(cx).await;
22473 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22474 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22475
22476 // test cursor move to start of each line on tab
22477 // for `if`, `elif`, `else`, `while`, `with` and `for`
22478 cx.set_state(indoc! {"
22479 def main():
22480 ˇ for item in items:
22481 ˇ while item.active:
22482 ˇ if item.value > 10:
22483 ˇ continue
22484 ˇ elif item.value < 0:
22485 ˇ break
22486 ˇ else:
22487 ˇ with item.context() as ctx:
22488 ˇ yield count
22489 ˇ else:
22490 ˇ log('while else')
22491 ˇ else:
22492 ˇ log('for else')
22493 "});
22494 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22495 cx.assert_editor_state(indoc! {"
22496 def main():
22497 ˇfor item in items:
22498 ˇwhile item.active:
22499 ˇif item.value > 10:
22500 ˇcontinue
22501 ˇelif item.value < 0:
22502 ˇbreak
22503 ˇelse:
22504 ˇwith item.context() as ctx:
22505 ˇyield count
22506 ˇelse:
22507 ˇlog('while else')
22508 ˇelse:
22509 ˇlog('for else')
22510 "});
22511 // test relative indent is preserved when tab
22512 // for `if`, `elif`, `else`, `while`, `with` and `for`
22513 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22514 cx.assert_editor_state(indoc! {"
22515 def main():
22516 ˇfor item in items:
22517 ˇwhile item.active:
22518 ˇif item.value > 10:
22519 ˇcontinue
22520 ˇelif item.value < 0:
22521 ˇbreak
22522 ˇelse:
22523 ˇwith item.context() as ctx:
22524 ˇyield count
22525 ˇelse:
22526 ˇlog('while else')
22527 ˇelse:
22528 ˇlog('for else')
22529 "});
22530
22531 // test cursor move to start of each line on tab
22532 // for `try`, `except`, `else`, `finally`, `match` and `def`
22533 cx.set_state(indoc! {"
22534 def main():
22535 ˇ try:
22536 ˇ fetch()
22537 ˇ except ValueError:
22538 ˇ handle_error()
22539 ˇ else:
22540 ˇ match value:
22541 ˇ case _:
22542 ˇ finally:
22543 ˇ def status():
22544 ˇ return 0
22545 "});
22546 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22547 cx.assert_editor_state(indoc! {"
22548 def main():
22549 ˇtry:
22550 ˇfetch()
22551 ˇexcept ValueError:
22552 ˇhandle_error()
22553 ˇelse:
22554 ˇmatch value:
22555 ˇcase _:
22556 ˇfinally:
22557 ˇdef status():
22558 ˇreturn 0
22559 "});
22560 // test relative indent is preserved when tab
22561 // for `try`, `except`, `else`, `finally`, `match` and `def`
22562 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22563 cx.assert_editor_state(indoc! {"
22564 def main():
22565 ˇtry:
22566 ˇfetch()
22567 ˇexcept ValueError:
22568 ˇhandle_error()
22569 ˇelse:
22570 ˇmatch value:
22571 ˇcase _:
22572 ˇfinally:
22573 ˇdef status():
22574 ˇreturn 0
22575 "});
22576}
22577
22578#[gpui::test]
22579async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
22580 init_test(cx, |_| {});
22581
22582 let mut cx = EditorTestContext::new(cx).await;
22583 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22584 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22585
22586 // test `else` auto outdents when typed inside `if` block
22587 cx.set_state(indoc! {"
22588 def main():
22589 if i == 2:
22590 return
22591 ˇ
22592 "});
22593 cx.update_editor(|editor, window, cx| {
22594 editor.handle_input("else:", window, cx);
22595 });
22596 cx.assert_editor_state(indoc! {"
22597 def main():
22598 if i == 2:
22599 return
22600 else:ˇ
22601 "});
22602
22603 // test `except` auto outdents when typed inside `try` block
22604 cx.set_state(indoc! {"
22605 def main():
22606 try:
22607 i = 2
22608 ˇ
22609 "});
22610 cx.update_editor(|editor, window, cx| {
22611 editor.handle_input("except:", window, cx);
22612 });
22613 cx.assert_editor_state(indoc! {"
22614 def main():
22615 try:
22616 i = 2
22617 except:ˇ
22618 "});
22619
22620 // test `else` auto outdents when typed inside `except` block
22621 cx.set_state(indoc! {"
22622 def main():
22623 try:
22624 i = 2
22625 except:
22626 j = 2
22627 ˇ
22628 "});
22629 cx.update_editor(|editor, window, cx| {
22630 editor.handle_input("else:", window, cx);
22631 });
22632 cx.assert_editor_state(indoc! {"
22633 def main():
22634 try:
22635 i = 2
22636 except:
22637 j = 2
22638 else:ˇ
22639 "});
22640
22641 // test `finally` auto outdents when typed inside `else` block
22642 cx.set_state(indoc! {"
22643 def main():
22644 try:
22645 i = 2
22646 except:
22647 j = 2
22648 else:
22649 k = 2
22650 ˇ
22651 "});
22652 cx.update_editor(|editor, window, cx| {
22653 editor.handle_input("finally:", window, cx);
22654 });
22655 cx.assert_editor_state(indoc! {"
22656 def main():
22657 try:
22658 i = 2
22659 except:
22660 j = 2
22661 else:
22662 k = 2
22663 finally:ˇ
22664 "});
22665
22666 // test `else` does not outdents when typed inside `except` block right after for block
22667 cx.set_state(indoc! {"
22668 def main():
22669 try:
22670 i = 2
22671 except:
22672 for i in range(n):
22673 pass
22674 ˇ
22675 "});
22676 cx.update_editor(|editor, window, cx| {
22677 editor.handle_input("else:", window, cx);
22678 });
22679 cx.assert_editor_state(indoc! {"
22680 def main():
22681 try:
22682 i = 2
22683 except:
22684 for i in range(n):
22685 pass
22686 else:ˇ
22687 "});
22688
22689 // test `finally` auto outdents when typed inside `else` block right after for block
22690 cx.set_state(indoc! {"
22691 def main():
22692 try:
22693 i = 2
22694 except:
22695 j = 2
22696 else:
22697 for i in range(n):
22698 pass
22699 ˇ
22700 "});
22701 cx.update_editor(|editor, window, cx| {
22702 editor.handle_input("finally:", window, cx);
22703 });
22704 cx.assert_editor_state(indoc! {"
22705 def main():
22706 try:
22707 i = 2
22708 except:
22709 j = 2
22710 else:
22711 for i in range(n):
22712 pass
22713 finally:ˇ
22714 "});
22715
22716 // test `except` outdents to inner "try" block
22717 cx.set_state(indoc! {"
22718 def main():
22719 try:
22720 i = 2
22721 if i == 2:
22722 try:
22723 i = 3
22724 ˇ
22725 "});
22726 cx.update_editor(|editor, window, cx| {
22727 editor.handle_input("except:", window, cx);
22728 });
22729 cx.assert_editor_state(indoc! {"
22730 def main():
22731 try:
22732 i = 2
22733 if i == 2:
22734 try:
22735 i = 3
22736 except:ˇ
22737 "});
22738
22739 // test `except` outdents to outer "try" block
22740 cx.set_state(indoc! {"
22741 def main():
22742 try:
22743 i = 2
22744 if i == 2:
22745 try:
22746 i = 3
22747 ˇ
22748 "});
22749 cx.update_editor(|editor, window, cx| {
22750 editor.handle_input("except:", window, cx);
22751 });
22752 cx.assert_editor_state(indoc! {"
22753 def main():
22754 try:
22755 i = 2
22756 if i == 2:
22757 try:
22758 i = 3
22759 except:ˇ
22760 "});
22761
22762 // test `else` stays at correct indent when typed after `for` block
22763 cx.set_state(indoc! {"
22764 def main():
22765 for i in range(10):
22766 if i == 3:
22767 break
22768 ˇ
22769 "});
22770 cx.update_editor(|editor, window, cx| {
22771 editor.handle_input("else:", window, cx);
22772 });
22773 cx.assert_editor_state(indoc! {"
22774 def main():
22775 for i in range(10):
22776 if i == 3:
22777 break
22778 else:ˇ
22779 "});
22780
22781 // test does not outdent on typing after line with square brackets
22782 cx.set_state(indoc! {"
22783 def f() -> list[str]:
22784 ˇ
22785 "});
22786 cx.update_editor(|editor, window, cx| {
22787 editor.handle_input("a", window, cx);
22788 });
22789 cx.assert_editor_state(indoc! {"
22790 def f() -> list[str]:
22791 aˇ
22792 "});
22793
22794 // test does not outdent on typing : after case keyword
22795 cx.set_state(indoc! {"
22796 match 1:
22797 caseˇ
22798 "});
22799 cx.update_editor(|editor, window, cx| {
22800 editor.handle_input(":", window, cx);
22801 });
22802 cx.assert_editor_state(indoc! {"
22803 match 1:
22804 case:ˇ
22805 "});
22806}
22807
22808#[gpui::test]
22809async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
22810 init_test(cx, |_| {});
22811 update_test_language_settings(cx, |settings| {
22812 settings.defaults.extend_comment_on_newline = Some(false);
22813 });
22814 let mut cx = EditorTestContext::new(cx).await;
22815 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22816 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22817
22818 // test correct indent after newline on comment
22819 cx.set_state(indoc! {"
22820 # COMMENT:ˇ
22821 "});
22822 cx.update_editor(|editor, window, cx| {
22823 editor.newline(&Newline, window, cx);
22824 });
22825 cx.assert_editor_state(indoc! {"
22826 # COMMENT:
22827 ˇ
22828 "});
22829
22830 // test correct indent after newline in brackets
22831 cx.set_state(indoc! {"
22832 {ˇ}
22833 "});
22834 cx.update_editor(|editor, window, cx| {
22835 editor.newline(&Newline, window, cx);
22836 });
22837 cx.run_until_parked();
22838 cx.assert_editor_state(indoc! {"
22839 {
22840 ˇ
22841 }
22842 "});
22843
22844 cx.set_state(indoc! {"
22845 (ˇ)
22846 "});
22847 cx.update_editor(|editor, window, cx| {
22848 editor.newline(&Newline, window, cx);
22849 });
22850 cx.run_until_parked();
22851 cx.assert_editor_state(indoc! {"
22852 (
22853 ˇ
22854 )
22855 "});
22856
22857 // do not indent after empty lists or dictionaries
22858 cx.set_state(indoc! {"
22859 a = []ˇ
22860 "});
22861 cx.update_editor(|editor, window, cx| {
22862 editor.newline(&Newline, window, cx);
22863 });
22864 cx.run_until_parked();
22865 cx.assert_editor_state(indoc! {"
22866 a = []
22867 ˇ
22868 "});
22869}
22870
22871#[gpui::test]
22872async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
22873 init_test(cx, |_| {});
22874
22875 let mut cx = EditorTestContext::new(cx).await;
22876 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
22877 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22878
22879 // test cursor move to start of each line on tab
22880 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
22881 cx.set_state(indoc! {"
22882 function main() {
22883 ˇ for item in $items; do
22884 ˇ while [ -n \"$item\" ]; do
22885 ˇ if [ \"$value\" -gt 10 ]; then
22886 ˇ continue
22887 ˇ elif [ \"$value\" -lt 0 ]; then
22888 ˇ break
22889 ˇ else
22890 ˇ echo \"$item\"
22891 ˇ fi
22892 ˇ done
22893 ˇ done
22894 ˇ}
22895 "});
22896 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22897 cx.assert_editor_state(indoc! {"
22898 function main() {
22899 ˇfor item in $items; do
22900 ˇwhile [ -n \"$item\" ]; do
22901 ˇif [ \"$value\" -gt 10 ]; then
22902 ˇcontinue
22903 ˇelif [ \"$value\" -lt 0 ]; then
22904 ˇbreak
22905 ˇelse
22906 ˇecho \"$item\"
22907 ˇfi
22908 ˇdone
22909 ˇdone
22910 ˇ}
22911 "});
22912 // test relative indent is preserved when tab
22913 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22914 cx.assert_editor_state(indoc! {"
22915 function main() {
22916 ˇfor item in $items; do
22917 ˇwhile [ -n \"$item\" ]; do
22918 ˇif [ \"$value\" -gt 10 ]; then
22919 ˇcontinue
22920 ˇelif [ \"$value\" -lt 0 ]; then
22921 ˇbreak
22922 ˇelse
22923 ˇecho \"$item\"
22924 ˇfi
22925 ˇdone
22926 ˇdone
22927 ˇ}
22928 "});
22929
22930 // test cursor move to start of each line on tab
22931 // for `case` statement with patterns
22932 cx.set_state(indoc! {"
22933 function handle() {
22934 ˇ case \"$1\" in
22935 ˇ start)
22936 ˇ echo \"a\"
22937 ˇ ;;
22938 ˇ stop)
22939 ˇ echo \"b\"
22940 ˇ ;;
22941 ˇ *)
22942 ˇ echo \"c\"
22943 ˇ ;;
22944 ˇ esac
22945 ˇ}
22946 "});
22947 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22948 cx.assert_editor_state(indoc! {"
22949 function handle() {
22950 ˇcase \"$1\" in
22951 ˇstart)
22952 ˇecho \"a\"
22953 ˇ;;
22954 ˇstop)
22955 ˇecho \"b\"
22956 ˇ;;
22957 ˇ*)
22958 ˇecho \"c\"
22959 ˇ;;
22960 ˇesac
22961 ˇ}
22962 "});
22963}
22964
22965#[gpui::test]
22966async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
22967 init_test(cx, |_| {});
22968
22969 let mut cx = EditorTestContext::new(cx).await;
22970 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
22971 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22972
22973 // test indents on comment insert
22974 cx.set_state(indoc! {"
22975 function main() {
22976 ˇ for item in $items; do
22977 ˇ while [ -n \"$item\" ]; do
22978 ˇ if [ \"$value\" -gt 10 ]; then
22979 ˇ continue
22980 ˇ elif [ \"$value\" -lt 0 ]; then
22981 ˇ break
22982 ˇ else
22983 ˇ echo \"$item\"
22984 ˇ fi
22985 ˇ done
22986 ˇ done
22987 ˇ}
22988 "});
22989 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
22990 cx.assert_editor_state(indoc! {"
22991 function main() {
22992 #ˇ for item in $items; do
22993 #ˇ while [ -n \"$item\" ]; do
22994 #ˇ if [ \"$value\" -gt 10 ]; then
22995 #ˇ continue
22996 #ˇ elif [ \"$value\" -lt 0 ]; then
22997 #ˇ break
22998 #ˇ else
22999 #ˇ echo \"$item\"
23000 #ˇ fi
23001 #ˇ done
23002 #ˇ done
23003 #ˇ}
23004 "});
23005}
23006
23007#[gpui::test]
23008async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
23009 init_test(cx, |_| {});
23010
23011 let mut cx = EditorTestContext::new(cx).await;
23012 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
23013 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23014
23015 // test `else` auto outdents when typed inside `if` block
23016 cx.set_state(indoc! {"
23017 if [ \"$1\" = \"test\" ]; then
23018 echo \"foo bar\"
23019 ˇ
23020 "});
23021 cx.update_editor(|editor, window, cx| {
23022 editor.handle_input("else", window, cx);
23023 });
23024 cx.assert_editor_state(indoc! {"
23025 if [ \"$1\" = \"test\" ]; then
23026 echo \"foo bar\"
23027 elseˇ
23028 "});
23029
23030 // test `elif` auto outdents when typed inside `if` block
23031 cx.set_state(indoc! {"
23032 if [ \"$1\" = \"test\" ]; then
23033 echo \"foo bar\"
23034 ˇ
23035 "});
23036 cx.update_editor(|editor, window, cx| {
23037 editor.handle_input("elif", window, cx);
23038 });
23039 cx.assert_editor_state(indoc! {"
23040 if [ \"$1\" = \"test\" ]; then
23041 echo \"foo bar\"
23042 elifˇ
23043 "});
23044
23045 // test `fi` auto outdents when typed inside `else` block
23046 cx.set_state(indoc! {"
23047 if [ \"$1\" = \"test\" ]; then
23048 echo \"foo bar\"
23049 else
23050 echo \"bar baz\"
23051 ˇ
23052 "});
23053 cx.update_editor(|editor, window, cx| {
23054 editor.handle_input("fi", window, cx);
23055 });
23056 cx.assert_editor_state(indoc! {"
23057 if [ \"$1\" = \"test\" ]; then
23058 echo \"foo bar\"
23059 else
23060 echo \"bar baz\"
23061 fiˇ
23062 "});
23063
23064 // test `done` auto outdents when typed inside `while` block
23065 cx.set_state(indoc! {"
23066 while read line; do
23067 echo \"$line\"
23068 ˇ
23069 "});
23070 cx.update_editor(|editor, window, cx| {
23071 editor.handle_input("done", window, cx);
23072 });
23073 cx.assert_editor_state(indoc! {"
23074 while read line; do
23075 echo \"$line\"
23076 doneˇ
23077 "});
23078
23079 // test `done` auto outdents when typed inside `for` block
23080 cx.set_state(indoc! {"
23081 for file in *.txt; do
23082 cat \"$file\"
23083 ˇ
23084 "});
23085 cx.update_editor(|editor, window, cx| {
23086 editor.handle_input("done", window, cx);
23087 });
23088 cx.assert_editor_state(indoc! {"
23089 for file in *.txt; do
23090 cat \"$file\"
23091 doneˇ
23092 "});
23093
23094 // test `esac` auto outdents when typed inside `case` block
23095 cx.set_state(indoc! {"
23096 case \"$1\" in
23097 start)
23098 echo \"foo bar\"
23099 ;;
23100 stop)
23101 echo \"bar baz\"
23102 ;;
23103 ˇ
23104 "});
23105 cx.update_editor(|editor, window, cx| {
23106 editor.handle_input("esac", window, cx);
23107 });
23108 cx.assert_editor_state(indoc! {"
23109 case \"$1\" in
23110 start)
23111 echo \"foo bar\"
23112 ;;
23113 stop)
23114 echo \"bar baz\"
23115 ;;
23116 esacˇ
23117 "});
23118
23119 // test `*)` auto outdents when typed inside `case` block
23120 cx.set_state(indoc! {"
23121 case \"$1\" in
23122 start)
23123 echo \"foo bar\"
23124 ;;
23125 ˇ
23126 "});
23127 cx.update_editor(|editor, window, cx| {
23128 editor.handle_input("*)", window, cx);
23129 });
23130 cx.assert_editor_state(indoc! {"
23131 case \"$1\" in
23132 start)
23133 echo \"foo bar\"
23134 ;;
23135 *)ˇ
23136 "});
23137
23138 // test `fi` outdents to correct level with nested if blocks
23139 cx.set_state(indoc! {"
23140 if [ \"$1\" = \"test\" ]; then
23141 echo \"outer if\"
23142 if [ \"$2\" = \"debug\" ]; then
23143 echo \"inner if\"
23144 ˇ
23145 "});
23146 cx.update_editor(|editor, window, cx| {
23147 editor.handle_input("fi", window, cx);
23148 });
23149 cx.assert_editor_state(indoc! {"
23150 if [ \"$1\" = \"test\" ]; then
23151 echo \"outer if\"
23152 if [ \"$2\" = \"debug\" ]; then
23153 echo \"inner if\"
23154 fiˇ
23155 "});
23156}
23157
23158#[gpui::test]
23159async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
23160 init_test(cx, |_| {});
23161 update_test_language_settings(cx, |settings| {
23162 settings.defaults.extend_comment_on_newline = Some(false);
23163 });
23164 let mut cx = EditorTestContext::new(cx).await;
23165 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
23166 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23167
23168 // test correct indent after newline on comment
23169 cx.set_state(indoc! {"
23170 # COMMENT:ˇ
23171 "});
23172 cx.update_editor(|editor, window, cx| {
23173 editor.newline(&Newline, window, cx);
23174 });
23175 cx.assert_editor_state(indoc! {"
23176 # COMMENT:
23177 ˇ
23178 "});
23179
23180 // test correct indent after newline after `then`
23181 cx.set_state(indoc! {"
23182
23183 if [ \"$1\" = \"test\" ]; thenˇ
23184 "});
23185 cx.update_editor(|editor, window, cx| {
23186 editor.newline(&Newline, window, cx);
23187 });
23188 cx.run_until_parked();
23189 cx.assert_editor_state(indoc! {"
23190
23191 if [ \"$1\" = \"test\" ]; then
23192 ˇ
23193 "});
23194
23195 // test correct indent after newline after `else`
23196 cx.set_state(indoc! {"
23197 if [ \"$1\" = \"test\" ]; then
23198 elseˇ
23199 "});
23200 cx.update_editor(|editor, window, cx| {
23201 editor.newline(&Newline, window, cx);
23202 });
23203 cx.run_until_parked();
23204 cx.assert_editor_state(indoc! {"
23205 if [ \"$1\" = \"test\" ]; then
23206 else
23207 ˇ
23208 "});
23209
23210 // test correct indent after newline after `elif`
23211 cx.set_state(indoc! {"
23212 if [ \"$1\" = \"test\" ]; then
23213 elifˇ
23214 "});
23215 cx.update_editor(|editor, window, cx| {
23216 editor.newline(&Newline, window, cx);
23217 });
23218 cx.run_until_parked();
23219 cx.assert_editor_state(indoc! {"
23220 if [ \"$1\" = \"test\" ]; then
23221 elif
23222 ˇ
23223 "});
23224
23225 // test correct indent after newline after `do`
23226 cx.set_state(indoc! {"
23227 for file in *.txt; doˇ
23228 "});
23229 cx.update_editor(|editor, window, cx| {
23230 editor.newline(&Newline, window, cx);
23231 });
23232 cx.run_until_parked();
23233 cx.assert_editor_state(indoc! {"
23234 for file in *.txt; do
23235 ˇ
23236 "});
23237
23238 // test correct indent after newline after case pattern
23239 cx.set_state(indoc! {"
23240 case \"$1\" in
23241 start)ˇ
23242 "});
23243 cx.update_editor(|editor, window, cx| {
23244 editor.newline(&Newline, window, cx);
23245 });
23246 cx.run_until_parked();
23247 cx.assert_editor_state(indoc! {"
23248 case \"$1\" in
23249 start)
23250 ˇ
23251 "});
23252
23253 // test correct indent after newline after case pattern
23254 cx.set_state(indoc! {"
23255 case \"$1\" in
23256 start)
23257 ;;
23258 *)ˇ
23259 "});
23260 cx.update_editor(|editor, window, cx| {
23261 editor.newline(&Newline, window, cx);
23262 });
23263 cx.run_until_parked();
23264 cx.assert_editor_state(indoc! {"
23265 case \"$1\" in
23266 start)
23267 ;;
23268 *)
23269 ˇ
23270 "});
23271
23272 // test correct indent after newline after function opening brace
23273 cx.set_state(indoc! {"
23274 function test() {ˇ}
23275 "});
23276 cx.update_editor(|editor, window, cx| {
23277 editor.newline(&Newline, window, cx);
23278 });
23279 cx.run_until_parked();
23280 cx.assert_editor_state(indoc! {"
23281 function test() {
23282 ˇ
23283 }
23284 "});
23285
23286 // test no extra indent after semicolon on same line
23287 cx.set_state(indoc! {"
23288 echo \"test\";ˇ
23289 "});
23290 cx.update_editor(|editor, window, cx| {
23291 editor.newline(&Newline, window, cx);
23292 });
23293 cx.run_until_parked();
23294 cx.assert_editor_state(indoc! {"
23295 echo \"test\";
23296 ˇ
23297 "});
23298}
23299
23300fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
23301 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
23302 point..point
23303}
23304
23305#[track_caller]
23306fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
23307 let (text, ranges) = marked_text_ranges(marked_text, true);
23308 assert_eq!(editor.text(cx), text);
23309 assert_eq!(
23310 editor.selections.ranges(cx),
23311 ranges,
23312 "Assert selections are {}",
23313 marked_text
23314 );
23315}
23316
23317pub fn handle_signature_help_request(
23318 cx: &mut EditorLspTestContext,
23319 mocked_response: lsp::SignatureHelp,
23320) -> impl Future<Output = ()> + use<> {
23321 let mut request =
23322 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
23323 let mocked_response = mocked_response.clone();
23324 async move { Ok(Some(mocked_response)) }
23325 });
23326
23327 async move {
23328 request.next().await;
23329 }
23330}
23331
23332#[track_caller]
23333pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
23334 cx.update_editor(|editor, _, _| {
23335 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
23336 let entries = menu.entries.borrow();
23337 let entries = entries
23338 .iter()
23339 .map(|entry| entry.string.as_str())
23340 .collect::<Vec<_>>();
23341 assert_eq!(entries, expected);
23342 } else {
23343 panic!("Expected completions menu");
23344 }
23345 });
23346}
23347
23348/// Handle completion request passing a marked string specifying where the completion
23349/// should be triggered from using '|' character, what range should be replaced, and what completions
23350/// should be returned using '<' and '>' to delimit the range.
23351///
23352/// Also see `handle_completion_request_with_insert_and_replace`.
23353#[track_caller]
23354pub fn handle_completion_request(
23355 marked_string: &str,
23356 completions: Vec<&'static str>,
23357 is_incomplete: bool,
23358 counter: Arc<AtomicUsize>,
23359 cx: &mut EditorLspTestContext,
23360) -> impl Future<Output = ()> {
23361 let complete_from_marker: TextRangeMarker = '|'.into();
23362 let replace_range_marker: TextRangeMarker = ('<', '>').into();
23363 let (_, mut marked_ranges) = marked_text_ranges_by(
23364 marked_string,
23365 vec![complete_from_marker.clone(), replace_range_marker.clone()],
23366 );
23367
23368 let complete_from_position =
23369 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
23370 let replace_range =
23371 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
23372
23373 let mut request =
23374 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
23375 let completions = completions.clone();
23376 counter.fetch_add(1, atomic::Ordering::Release);
23377 async move {
23378 assert_eq!(params.text_document_position.text_document.uri, url.clone());
23379 assert_eq!(
23380 params.text_document_position.position,
23381 complete_from_position
23382 );
23383 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
23384 is_incomplete: is_incomplete,
23385 item_defaults: None,
23386 items: completions
23387 .iter()
23388 .map(|completion_text| lsp::CompletionItem {
23389 label: completion_text.to_string(),
23390 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
23391 range: replace_range,
23392 new_text: completion_text.to_string(),
23393 })),
23394 ..Default::default()
23395 })
23396 .collect(),
23397 })))
23398 }
23399 });
23400
23401 async move {
23402 request.next().await;
23403 }
23404}
23405
23406/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
23407/// given instead, which also contains an `insert` range.
23408///
23409/// This function uses markers to define ranges:
23410/// - `|` marks the cursor position
23411/// - `<>` marks the replace range
23412/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
23413pub fn handle_completion_request_with_insert_and_replace(
23414 cx: &mut EditorLspTestContext,
23415 marked_string: &str,
23416 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
23417 counter: Arc<AtomicUsize>,
23418) -> impl Future<Output = ()> {
23419 let complete_from_marker: TextRangeMarker = '|'.into();
23420 let replace_range_marker: TextRangeMarker = ('<', '>').into();
23421 let insert_range_marker: TextRangeMarker = ('{', '}').into();
23422
23423 let (_, mut marked_ranges) = marked_text_ranges_by(
23424 marked_string,
23425 vec![
23426 complete_from_marker.clone(),
23427 replace_range_marker.clone(),
23428 insert_range_marker.clone(),
23429 ],
23430 );
23431
23432 let complete_from_position =
23433 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
23434 let replace_range =
23435 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
23436
23437 let insert_range = match marked_ranges.remove(&insert_range_marker) {
23438 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
23439 _ => lsp::Range {
23440 start: replace_range.start,
23441 end: complete_from_position,
23442 },
23443 };
23444
23445 let mut request =
23446 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
23447 let completions = completions.clone();
23448 counter.fetch_add(1, atomic::Ordering::Release);
23449 async move {
23450 assert_eq!(params.text_document_position.text_document.uri, url.clone());
23451 assert_eq!(
23452 params.text_document_position.position, complete_from_position,
23453 "marker `|` position doesn't match",
23454 );
23455 Ok(Some(lsp::CompletionResponse::Array(
23456 completions
23457 .iter()
23458 .map(|(label, new_text)| lsp::CompletionItem {
23459 label: label.to_string(),
23460 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
23461 lsp::InsertReplaceEdit {
23462 insert: insert_range,
23463 replace: replace_range,
23464 new_text: new_text.to_string(),
23465 },
23466 )),
23467 ..Default::default()
23468 })
23469 .collect(),
23470 )))
23471 }
23472 });
23473
23474 async move {
23475 request.next().await;
23476 }
23477}
23478
23479fn handle_resolve_completion_request(
23480 cx: &mut EditorLspTestContext,
23481 edits: Option<Vec<(&'static str, &'static str)>>,
23482) -> impl Future<Output = ()> {
23483 let edits = edits.map(|edits| {
23484 edits
23485 .iter()
23486 .map(|(marked_string, new_text)| {
23487 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
23488 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
23489 lsp::TextEdit::new(replace_range, new_text.to_string())
23490 })
23491 .collect::<Vec<_>>()
23492 });
23493
23494 let mut request =
23495 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
23496 let edits = edits.clone();
23497 async move {
23498 Ok(lsp::CompletionItem {
23499 additional_text_edits: edits,
23500 ..Default::default()
23501 })
23502 }
23503 });
23504
23505 async move {
23506 request.next().await;
23507 }
23508}
23509
23510pub(crate) fn update_test_language_settings(
23511 cx: &mut TestAppContext,
23512 f: impl Fn(&mut AllLanguageSettingsContent),
23513) {
23514 cx.update(|cx| {
23515 SettingsStore::update_global(cx, |store, cx| {
23516 store.update_user_settings::<AllLanguageSettings>(cx, f);
23517 });
23518 });
23519}
23520
23521pub(crate) fn update_test_project_settings(
23522 cx: &mut TestAppContext,
23523 f: impl Fn(&mut ProjectSettings),
23524) {
23525 cx.update(|cx| {
23526 SettingsStore::update_global(cx, |store, cx| {
23527 store.update_user_settings::<ProjectSettings>(cx, f);
23528 });
23529 });
23530}
23531
23532pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
23533 cx.update(|cx| {
23534 assets::Assets.load_test_fonts(cx);
23535 let store = SettingsStore::test(cx);
23536 cx.set_global(store);
23537 theme::init(theme::LoadThemes::JustBase, cx);
23538 release_channel::init(SemanticVersion::default(), cx);
23539 client::init_settings(cx);
23540 language::init(cx);
23541 Project::init_settings(cx);
23542 workspace::init_settings(cx);
23543 crate::init(cx);
23544 });
23545 zlog::init_test();
23546 update_test_language_settings(cx, f);
23547}
23548
23549#[track_caller]
23550fn assert_hunk_revert(
23551 not_reverted_text_with_selections: &str,
23552 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
23553 expected_reverted_text_with_selections: &str,
23554 base_text: &str,
23555 cx: &mut EditorLspTestContext,
23556) {
23557 cx.set_state(not_reverted_text_with_selections);
23558 cx.set_head_text(base_text);
23559 cx.executor().run_until_parked();
23560
23561 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
23562 let snapshot = editor.snapshot(window, cx);
23563 let reverted_hunk_statuses = snapshot
23564 .buffer_snapshot
23565 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
23566 .map(|hunk| hunk.status().kind)
23567 .collect::<Vec<_>>();
23568
23569 editor.git_restore(&Default::default(), window, cx);
23570 reverted_hunk_statuses
23571 });
23572 cx.executor().run_until_parked();
23573 cx.assert_editor_state(expected_reverted_text_with_selections);
23574 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
23575}
23576
23577#[gpui::test(iterations = 10)]
23578async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
23579 init_test(cx, |_| {});
23580
23581 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
23582 let counter = diagnostic_requests.clone();
23583
23584 let fs = FakeFs::new(cx.executor());
23585 fs.insert_tree(
23586 path!("/a"),
23587 json!({
23588 "first.rs": "fn main() { let a = 5; }",
23589 "second.rs": "// Test file",
23590 }),
23591 )
23592 .await;
23593
23594 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23595 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23596 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23597
23598 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23599 language_registry.add(rust_lang());
23600 let mut fake_servers = language_registry.register_fake_lsp(
23601 "Rust",
23602 FakeLspAdapter {
23603 capabilities: lsp::ServerCapabilities {
23604 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
23605 lsp::DiagnosticOptions {
23606 identifier: None,
23607 inter_file_dependencies: true,
23608 workspace_diagnostics: true,
23609 work_done_progress_options: Default::default(),
23610 },
23611 )),
23612 ..Default::default()
23613 },
23614 ..Default::default()
23615 },
23616 );
23617
23618 let editor = workspace
23619 .update(cx, |workspace, window, cx| {
23620 workspace.open_abs_path(
23621 PathBuf::from(path!("/a/first.rs")),
23622 OpenOptions::default(),
23623 window,
23624 cx,
23625 )
23626 })
23627 .unwrap()
23628 .await
23629 .unwrap()
23630 .downcast::<Editor>()
23631 .unwrap();
23632 let fake_server = fake_servers.next().await.unwrap();
23633 let server_id = fake_server.server.server_id();
23634 let mut first_request = fake_server
23635 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
23636 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
23637 let result_id = Some(new_result_id.to_string());
23638 assert_eq!(
23639 params.text_document.uri,
23640 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
23641 );
23642 async move {
23643 Ok(lsp::DocumentDiagnosticReportResult::Report(
23644 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
23645 related_documents: None,
23646 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
23647 items: Vec::new(),
23648 result_id,
23649 },
23650 }),
23651 ))
23652 }
23653 });
23654
23655 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
23656 project.update(cx, |project, cx| {
23657 let buffer_id = editor
23658 .read(cx)
23659 .buffer()
23660 .read(cx)
23661 .as_singleton()
23662 .expect("created a singleton buffer")
23663 .read(cx)
23664 .remote_id();
23665 let buffer_result_id = project
23666 .lsp_store()
23667 .read(cx)
23668 .result_id(server_id, buffer_id, cx);
23669 assert_eq!(expected, buffer_result_id);
23670 });
23671 };
23672
23673 ensure_result_id(None, cx);
23674 cx.executor().advance_clock(Duration::from_millis(60));
23675 cx.executor().run_until_parked();
23676 assert_eq!(
23677 diagnostic_requests.load(atomic::Ordering::Acquire),
23678 1,
23679 "Opening file should trigger diagnostic request"
23680 );
23681 first_request
23682 .next()
23683 .await
23684 .expect("should have sent the first diagnostics pull request");
23685 ensure_result_id(Some("1".to_string()), cx);
23686
23687 // Editing should trigger diagnostics
23688 editor.update_in(cx, |editor, window, cx| {
23689 editor.handle_input("2", window, cx)
23690 });
23691 cx.executor().advance_clock(Duration::from_millis(60));
23692 cx.executor().run_until_parked();
23693 assert_eq!(
23694 diagnostic_requests.load(atomic::Ordering::Acquire),
23695 2,
23696 "Editing should trigger diagnostic request"
23697 );
23698 ensure_result_id(Some("2".to_string()), cx);
23699
23700 // Moving cursor should not trigger diagnostic request
23701 editor.update_in(cx, |editor, window, cx| {
23702 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23703 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
23704 });
23705 });
23706 cx.executor().advance_clock(Duration::from_millis(60));
23707 cx.executor().run_until_parked();
23708 assert_eq!(
23709 diagnostic_requests.load(atomic::Ordering::Acquire),
23710 2,
23711 "Cursor movement should not trigger diagnostic request"
23712 );
23713 ensure_result_id(Some("2".to_string()), cx);
23714 // Multiple rapid edits should be debounced
23715 for _ in 0..5 {
23716 editor.update_in(cx, |editor, window, cx| {
23717 editor.handle_input("x", window, cx)
23718 });
23719 }
23720 cx.executor().advance_clock(Duration::from_millis(60));
23721 cx.executor().run_until_parked();
23722
23723 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
23724 assert!(
23725 final_requests <= 4,
23726 "Multiple rapid edits should be debounced (got {final_requests} requests)",
23727 );
23728 ensure_result_id(Some(final_requests.to_string()), cx);
23729}
23730
23731#[gpui::test]
23732async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
23733 // Regression test for issue #11671
23734 // Previously, adding a cursor after moving multiple cursors would reset
23735 // the cursor count instead of adding to the existing cursors.
23736 init_test(cx, |_| {});
23737 let mut cx = EditorTestContext::new(cx).await;
23738
23739 // Create a simple buffer with cursor at start
23740 cx.set_state(indoc! {"
23741 ˇaaaa
23742 bbbb
23743 cccc
23744 dddd
23745 eeee
23746 ffff
23747 gggg
23748 hhhh"});
23749
23750 // Add 2 cursors below (so we have 3 total)
23751 cx.update_editor(|editor, window, cx| {
23752 editor.add_selection_below(&Default::default(), window, cx);
23753 editor.add_selection_below(&Default::default(), window, cx);
23754 });
23755
23756 // Verify we have 3 cursors
23757 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
23758 assert_eq!(
23759 initial_count, 3,
23760 "Should have 3 cursors after adding 2 below"
23761 );
23762
23763 // Move down one line
23764 cx.update_editor(|editor, window, cx| {
23765 editor.move_down(&MoveDown, window, cx);
23766 });
23767
23768 // Add another cursor below
23769 cx.update_editor(|editor, window, cx| {
23770 editor.add_selection_below(&Default::default(), window, cx);
23771 });
23772
23773 // Should now have 4 cursors (3 original + 1 new)
23774 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
23775 assert_eq!(
23776 final_count, 4,
23777 "Should have 4 cursors after moving and adding another"
23778 );
23779}
23780
23781#[gpui::test(iterations = 10)]
23782async fn test_document_colors(cx: &mut TestAppContext) {
23783 let expected_color = Rgba {
23784 r: 0.33,
23785 g: 0.33,
23786 b: 0.33,
23787 a: 0.33,
23788 };
23789
23790 init_test(cx, |_| {});
23791
23792 let fs = FakeFs::new(cx.executor());
23793 fs.insert_tree(
23794 path!("/a"),
23795 json!({
23796 "first.rs": "fn main() { let a = 5; }",
23797 }),
23798 )
23799 .await;
23800
23801 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23802 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23803 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23804
23805 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23806 language_registry.add(rust_lang());
23807 let mut fake_servers = language_registry.register_fake_lsp(
23808 "Rust",
23809 FakeLspAdapter {
23810 capabilities: lsp::ServerCapabilities {
23811 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
23812 ..lsp::ServerCapabilities::default()
23813 },
23814 name: "rust-analyzer",
23815 ..FakeLspAdapter::default()
23816 },
23817 );
23818 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
23819 "Rust",
23820 FakeLspAdapter {
23821 capabilities: lsp::ServerCapabilities {
23822 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
23823 ..lsp::ServerCapabilities::default()
23824 },
23825 name: "not-rust-analyzer",
23826 ..FakeLspAdapter::default()
23827 },
23828 );
23829
23830 let editor = workspace
23831 .update(cx, |workspace, window, cx| {
23832 workspace.open_abs_path(
23833 PathBuf::from(path!("/a/first.rs")),
23834 OpenOptions::default(),
23835 window,
23836 cx,
23837 )
23838 })
23839 .unwrap()
23840 .await
23841 .unwrap()
23842 .downcast::<Editor>()
23843 .unwrap();
23844 let fake_language_server = fake_servers.next().await.unwrap();
23845 let fake_language_server_without_capabilities =
23846 fake_servers_without_capabilities.next().await.unwrap();
23847 let requests_made = Arc::new(AtomicUsize::new(0));
23848 let closure_requests_made = Arc::clone(&requests_made);
23849 let mut color_request_handle = fake_language_server
23850 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
23851 let requests_made = Arc::clone(&closure_requests_made);
23852 async move {
23853 assert_eq!(
23854 params.text_document.uri,
23855 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
23856 );
23857 requests_made.fetch_add(1, atomic::Ordering::Release);
23858 Ok(vec![
23859 lsp::ColorInformation {
23860 range: lsp::Range {
23861 start: lsp::Position {
23862 line: 0,
23863 character: 0,
23864 },
23865 end: lsp::Position {
23866 line: 0,
23867 character: 1,
23868 },
23869 },
23870 color: lsp::Color {
23871 red: 0.33,
23872 green: 0.33,
23873 blue: 0.33,
23874 alpha: 0.33,
23875 },
23876 },
23877 lsp::ColorInformation {
23878 range: lsp::Range {
23879 start: lsp::Position {
23880 line: 0,
23881 character: 0,
23882 },
23883 end: lsp::Position {
23884 line: 0,
23885 character: 1,
23886 },
23887 },
23888 color: lsp::Color {
23889 red: 0.33,
23890 green: 0.33,
23891 blue: 0.33,
23892 alpha: 0.33,
23893 },
23894 },
23895 ])
23896 }
23897 });
23898
23899 let _handle = fake_language_server_without_capabilities
23900 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
23901 panic!("Should not be called");
23902 });
23903 cx.executor().advance_clock(Duration::from_millis(100));
23904 color_request_handle.next().await.unwrap();
23905 cx.run_until_parked();
23906 assert_eq!(
23907 1,
23908 requests_made.load(atomic::Ordering::Acquire),
23909 "Should query for colors once per editor open"
23910 );
23911 editor.update_in(cx, |editor, _, cx| {
23912 assert_eq!(
23913 vec![expected_color],
23914 extract_color_inlays(editor, cx),
23915 "Should have an initial inlay"
23916 );
23917 });
23918
23919 // opening another file in a split should not influence the LSP query counter
23920 workspace
23921 .update(cx, |workspace, window, cx| {
23922 assert_eq!(
23923 workspace.panes().len(),
23924 1,
23925 "Should have one pane with one editor"
23926 );
23927 workspace.move_item_to_pane_in_direction(
23928 &MoveItemToPaneInDirection {
23929 direction: SplitDirection::Right,
23930 focus: false,
23931 clone: true,
23932 },
23933 window,
23934 cx,
23935 );
23936 })
23937 .unwrap();
23938 cx.run_until_parked();
23939 workspace
23940 .update(cx, |workspace, _, cx| {
23941 let panes = workspace.panes();
23942 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
23943 for pane in panes {
23944 let editor = pane
23945 .read(cx)
23946 .active_item()
23947 .and_then(|item| item.downcast::<Editor>())
23948 .expect("Should have opened an editor in each split");
23949 let editor_file = editor
23950 .read(cx)
23951 .buffer()
23952 .read(cx)
23953 .as_singleton()
23954 .expect("test deals with singleton buffers")
23955 .read(cx)
23956 .file()
23957 .expect("test buffese should have a file")
23958 .path();
23959 assert_eq!(
23960 editor_file.as_ref(),
23961 Path::new("first.rs"),
23962 "Both editors should be opened for the same file"
23963 )
23964 }
23965 })
23966 .unwrap();
23967
23968 cx.executor().advance_clock(Duration::from_millis(500));
23969 let save = editor.update_in(cx, |editor, window, cx| {
23970 editor.move_to_end(&MoveToEnd, window, cx);
23971 editor.handle_input("dirty", window, cx);
23972 editor.save(
23973 SaveOptions {
23974 format: true,
23975 autosave: true,
23976 },
23977 project.clone(),
23978 window,
23979 cx,
23980 )
23981 });
23982 save.await.unwrap();
23983
23984 color_request_handle.next().await.unwrap();
23985 cx.run_until_parked();
23986 assert_eq!(
23987 3,
23988 requests_made.load(atomic::Ordering::Acquire),
23989 "Should query for colors once per save and once per formatting after save"
23990 );
23991
23992 drop(editor);
23993 let close = workspace
23994 .update(cx, |workspace, window, cx| {
23995 workspace.active_pane().update(cx, |pane, cx| {
23996 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23997 })
23998 })
23999 .unwrap();
24000 close.await.unwrap();
24001 let close = workspace
24002 .update(cx, |workspace, window, cx| {
24003 workspace.active_pane().update(cx, |pane, cx| {
24004 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24005 })
24006 })
24007 .unwrap();
24008 close.await.unwrap();
24009 assert_eq!(
24010 3,
24011 requests_made.load(atomic::Ordering::Acquire),
24012 "After saving and closing all editors, no extra requests should be made"
24013 );
24014 workspace
24015 .update(cx, |workspace, _, cx| {
24016 assert!(
24017 workspace.active_item(cx).is_none(),
24018 "Should close all editors"
24019 )
24020 })
24021 .unwrap();
24022
24023 workspace
24024 .update(cx, |workspace, window, cx| {
24025 workspace.active_pane().update(cx, |pane, cx| {
24026 pane.navigate_backward(window, cx);
24027 })
24028 })
24029 .unwrap();
24030 cx.executor().advance_clock(Duration::from_millis(100));
24031 cx.run_until_parked();
24032 let editor = workspace
24033 .update(cx, |workspace, _, cx| {
24034 workspace
24035 .active_item(cx)
24036 .expect("Should have reopened the editor again after navigating back")
24037 .downcast::<Editor>()
24038 .expect("Should be an editor")
24039 })
24040 .unwrap();
24041 color_request_handle.next().await.unwrap();
24042 assert_eq!(
24043 3,
24044 requests_made.load(atomic::Ordering::Acquire),
24045 "Cache should be reused on buffer close and reopen"
24046 );
24047 editor.update(cx, |editor, cx| {
24048 assert_eq!(
24049 vec![expected_color],
24050 extract_color_inlays(editor, cx),
24051 "Should have an initial inlay"
24052 );
24053 });
24054}
24055
24056#[gpui::test]
24057async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
24058 init_test(cx, |_| {});
24059 let (editor, cx) = cx.add_window_view(Editor::single_line);
24060 editor.update_in(cx, |editor, window, cx| {
24061 editor.set_text("oops\n\nwow\n", window, cx)
24062 });
24063 cx.run_until_parked();
24064 editor.update(cx, |editor, cx| {
24065 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
24066 });
24067 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
24068 cx.run_until_parked();
24069 editor.update(cx, |editor, cx| {
24070 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
24071 });
24072}
24073
24074#[track_caller]
24075fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
24076 editor
24077 .all_inlays(cx)
24078 .into_iter()
24079 .filter_map(|inlay| inlay.get_color())
24080 .map(Rgba::from)
24081 .collect()
24082}