1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 inline_completion_tests::FakeInlineCompletionProvider,
6 linked_editing_ranges::LinkedEditingRanges,
7 scroll::scroll_amount::ScrollAmount,
8 test::{
9 assert_text_with_selections, build_editor,
10 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
11 editor_test_context::EditorTestContext,
12 select_ranges,
13 },
14};
15use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
16use futures::StreamExt;
17use gpui::{
18 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
19 VisualTestContext, WindowBounds, WindowOptions, div,
20};
21use indoc::indoc;
22use language::{
23 BracketPairConfig,
24 Capability::ReadWrite,
25 DiagnosticSourceKind, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher,
26 LanguageName, Override, Point,
27 language_settings::{
28 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings, FormatterList,
29 LanguageSettingsContent, LspInsertMode, PrettierSettings, SelectedFormatter,
30 },
31 tree_sitter_python,
32};
33use language_settings::{Formatter, IndentGuideSettings};
34use lsp::CompletionParams;
35use multi_buffer::{IndentGuide, PathKey};
36use parking_lot::Mutex;
37use pretty_assertions::{assert_eq, assert_ne};
38use project::{
39 FakeFs,
40 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
41 project_settings::{LspSettings, ProjectSettings},
42};
43use serde_json::{self, json};
44use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
45use std::{
46 iter,
47 sync::atomic::{self, AtomicUsize},
48};
49use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
50use text::ToPoint as _;
51use unindent::Unindent;
52use util::{
53 assert_set_eq, path,
54 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
55 uri,
56};
57use workspace::{
58 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
59 OpenOptions, ViewId,
60 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
61};
62
63#[gpui::test]
64fn test_edit_events(cx: &mut TestAppContext) {
65 init_test(cx, |_| {});
66
67 let buffer = cx.new(|cx| {
68 let mut buffer = language::Buffer::local("123456", cx);
69 buffer.set_group_interval(Duration::from_secs(1));
70 buffer
71 });
72
73 let events = Rc::new(RefCell::new(Vec::new()));
74 let editor1 = cx.add_window({
75 let events = events.clone();
76 |window, cx| {
77 let entity = cx.entity().clone();
78 cx.subscribe_in(
79 &entity,
80 window,
81 move |_, _, event: &EditorEvent, _, _| match event {
82 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
83 EditorEvent::BufferEdited => {
84 events.borrow_mut().push(("editor1", "buffer edited"))
85 }
86 _ => {}
87 },
88 )
89 .detach();
90 Editor::for_buffer(buffer.clone(), None, window, cx)
91 }
92 });
93
94 let editor2 = cx.add_window({
95 let events = events.clone();
96 |window, cx| {
97 cx.subscribe_in(
98 &cx.entity().clone(),
99 window,
100 move |_, _, event: &EditorEvent, _, _| match event {
101 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
102 EditorEvent::BufferEdited => {
103 events.borrow_mut().push(("editor2", "buffer edited"))
104 }
105 _ => {}
106 },
107 )
108 .detach();
109 Editor::for_buffer(buffer.clone(), None, window, cx)
110 }
111 });
112
113 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
114
115 // Mutating editor 1 will emit an `Edited` event only for that editor.
116 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
117 assert_eq!(
118 mem::take(&mut *events.borrow_mut()),
119 [
120 ("editor1", "edited"),
121 ("editor1", "buffer edited"),
122 ("editor2", "buffer edited"),
123 ]
124 );
125
126 // Mutating editor 2 will emit an `Edited` event only for that editor.
127 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
128 assert_eq!(
129 mem::take(&mut *events.borrow_mut()),
130 [
131 ("editor2", "edited"),
132 ("editor1", "buffer edited"),
133 ("editor2", "buffer edited"),
134 ]
135 );
136
137 // Undoing on editor 1 will emit an `Edited` event only for that editor.
138 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
139 assert_eq!(
140 mem::take(&mut *events.borrow_mut()),
141 [
142 ("editor1", "edited"),
143 ("editor1", "buffer edited"),
144 ("editor2", "buffer edited"),
145 ]
146 );
147
148 // Redoing on editor 1 will emit an `Edited` event only for that editor.
149 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
150 assert_eq!(
151 mem::take(&mut *events.borrow_mut()),
152 [
153 ("editor1", "edited"),
154 ("editor1", "buffer edited"),
155 ("editor2", "buffer edited"),
156 ]
157 );
158
159 // Undoing on editor 2 will emit an `Edited` event only for that editor.
160 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
161 assert_eq!(
162 mem::take(&mut *events.borrow_mut()),
163 [
164 ("editor2", "edited"),
165 ("editor1", "buffer edited"),
166 ("editor2", "buffer edited"),
167 ]
168 );
169
170 // Redoing on editor 2 will emit an `Edited` event only for that editor.
171 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
172 assert_eq!(
173 mem::take(&mut *events.borrow_mut()),
174 [
175 ("editor2", "edited"),
176 ("editor1", "buffer edited"),
177 ("editor2", "buffer edited"),
178 ]
179 );
180
181 // No event is emitted when the mutation is a no-op.
182 _ = editor2.update(cx, |editor, window, cx| {
183 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
184 s.select_ranges([0..0])
185 });
186
187 editor.backspace(&Backspace, window, cx);
188 });
189 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
190}
191
192#[gpui::test]
193fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
194 init_test(cx, |_| {});
195
196 let mut now = Instant::now();
197 let group_interval = Duration::from_millis(1);
198 let buffer = cx.new(|cx| {
199 let mut buf = language::Buffer::local("123456", cx);
200 buf.set_group_interval(group_interval);
201 buf
202 });
203 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
204 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
205
206 _ = editor.update(cx, |editor, window, cx| {
207 editor.start_transaction_at(now, window, cx);
208 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
209 s.select_ranges([2..4])
210 });
211
212 editor.insert("cd", window, cx);
213 editor.end_transaction_at(now, cx);
214 assert_eq!(editor.text(cx), "12cd56");
215 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
216
217 editor.start_transaction_at(now, window, cx);
218 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
219 s.select_ranges([4..5])
220 });
221 editor.insert("e", window, cx);
222 editor.end_transaction_at(now, cx);
223 assert_eq!(editor.text(cx), "12cde6");
224 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
225
226 now += group_interval + Duration::from_millis(1);
227 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
228 s.select_ranges([2..2])
229 });
230
231 // Simulate an edit in another editor
232 buffer.update(cx, |buffer, cx| {
233 buffer.start_transaction_at(now, cx);
234 buffer.edit([(0..1, "a")], None, cx);
235 buffer.edit([(1..1, "b")], None, cx);
236 buffer.end_transaction_at(now, cx);
237 });
238
239 assert_eq!(editor.text(cx), "ab2cde6");
240 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
241
242 // Last transaction happened past the group interval in a different editor.
243 // Undo it individually and don't restore selections.
244 editor.undo(&Undo, window, cx);
245 assert_eq!(editor.text(cx), "12cde6");
246 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
247
248 // First two transactions happened within the group interval in this editor.
249 // Undo them together and restore selections.
250 editor.undo(&Undo, window, cx);
251 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
252 assert_eq!(editor.text(cx), "123456");
253 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
254
255 // Redo the first two transactions together.
256 editor.redo(&Redo, window, cx);
257 assert_eq!(editor.text(cx), "12cde6");
258 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
259
260 // Redo the last transaction on its own.
261 editor.redo(&Redo, window, cx);
262 assert_eq!(editor.text(cx), "ab2cde6");
263 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
264
265 // Test empty transactions.
266 editor.start_transaction_at(now, window, cx);
267 editor.end_transaction_at(now, cx);
268 editor.undo(&Undo, window, cx);
269 assert_eq!(editor.text(cx), "12cde6");
270 });
271}
272
273#[gpui::test]
274fn test_ime_composition(cx: &mut TestAppContext) {
275 init_test(cx, |_| {});
276
277 let buffer = cx.new(|cx| {
278 let mut buffer = language::Buffer::local("abcde", cx);
279 // Ensure automatic grouping doesn't occur.
280 buffer.set_group_interval(Duration::ZERO);
281 buffer
282 });
283
284 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
285 cx.add_window(|window, cx| {
286 let mut editor = build_editor(buffer.clone(), window, cx);
287
288 // Start a new IME composition.
289 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
290 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
291 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
292 assert_eq!(editor.text(cx), "äbcde");
293 assert_eq!(
294 editor.marked_text_ranges(cx),
295 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
296 );
297
298 // Finalize IME composition.
299 editor.replace_text_in_range(None, "ā", window, cx);
300 assert_eq!(editor.text(cx), "ābcde");
301 assert_eq!(editor.marked_text_ranges(cx), None);
302
303 // IME composition edits are grouped and are undone/redone at once.
304 editor.undo(&Default::default(), window, cx);
305 assert_eq!(editor.text(cx), "abcde");
306 assert_eq!(editor.marked_text_ranges(cx), None);
307 editor.redo(&Default::default(), window, cx);
308 assert_eq!(editor.text(cx), "ābcde");
309 assert_eq!(editor.marked_text_ranges(cx), None);
310
311 // Start a new IME composition.
312 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
313 assert_eq!(
314 editor.marked_text_ranges(cx),
315 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
316 );
317
318 // Undoing during an IME composition cancels it.
319 editor.undo(&Default::default(), window, cx);
320 assert_eq!(editor.text(cx), "ābcde");
321 assert_eq!(editor.marked_text_ranges(cx), None);
322
323 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
324 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
325 assert_eq!(editor.text(cx), "ābcdè");
326 assert_eq!(
327 editor.marked_text_ranges(cx),
328 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
329 );
330
331 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
332 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
333 assert_eq!(editor.text(cx), "ābcdę");
334 assert_eq!(editor.marked_text_ranges(cx), None);
335
336 // Start a new IME composition with multiple cursors.
337 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
338 s.select_ranges([
339 OffsetUtf16(1)..OffsetUtf16(1),
340 OffsetUtf16(3)..OffsetUtf16(3),
341 OffsetUtf16(5)..OffsetUtf16(5),
342 ])
343 });
344 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
345 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
346 assert_eq!(
347 editor.marked_text_ranges(cx),
348 Some(vec![
349 OffsetUtf16(0)..OffsetUtf16(3),
350 OffsetUtf16(4)..OffsetUtf16(7),
351 OffsetUtf16(8)..OffsetUtf16(11)
352 ])
353 );
354
355 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
356 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
357 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
358 assert_eq!(
359 editor.marked_text_ranges(cx),
360 Some(vec![
361 OffsetUtf16(1)..OffsetUtf16(2),
362 OffsetUtf16(5)..OffsetUtf16(6),
363 OffsetUtf16(9)..OffsetUtf16(10)
364 ])
365 );
366
367 // Finalize IME composition with multiple cursors.
368 editor.replace_text_in_range(Some(9..10), "2", window, cx);
369 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
370 assert_eq!(editor.marked_text_ranges(cx), None);
371
372 editor
373 });
374}
375
376#[gpui::test]
377fn test_selection_with_mouse(cx: &mut TestAppContext) {
378 init_test(cx, |_| {});
379
380 let editor = cx.add_window(|window, cx| {
381 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
382 build_editor(buffer, window, cx)
383 });
384
385 _ = editor.update(cx, |editor, window, cx| {
386 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
387 });
388 assert_eq!(
389 editor
390 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
391 .unwrap(),
392 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
393 );
394
395 _ = editor.update(cx, |editor, window, cx| {
396 editor.update_selection(
397 DisplayPoint::new(DisplayRow(3), 3),
398 0,
399 gpui::Point::<f32>::default(),
400 window,
401 cx,
402 );
403 });
404
405 assert_eq!(
406 editor
407 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
408 .unwrap(),
409 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
410 );
411
412 _ = editor.update(cx, |editor, window, cx| {
413 editor.update_selection(
414 DisplayPoint::new(DisplayRow(1), 1),
415 0,
416 gpui::Point::<f32>::default(),
417 window,
418 cx,
419 );
420 });
421
422 assert_eq!(
423 editor
424 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
425 .unwrap(),
426 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
427 );
428
429 _ = editor.update(cx, |editor, window, cx| {
430 editor.end_selection(window, cx);
431 editor.update_selection(
432 DisplayPoint::new(DisplayRow(3), 3),
433 0,
434 gpui::Point::<f32>::default(),
435 window,
436 cx,
437 );
438 });
439
440 assert_eq!(
441 editor
442 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
443 .unwrap(),
444 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
445 );
446
447 _ = editor.update(cx, |editor, window, cx| {
448 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
449 editor.update_selection(
450 DisplayPoint::new(DisplayRow(0), 0),
451 0,
452 gpui::Point::<f32>::default(),
453 window,
454 cx,
455 );
456 });
457
458 assert_eq!(
459 editor
460 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
461 .unwrap(),
462 [
463 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
464 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
465 ]
466 );
467
468 _ = editor.update(cx, |editor, window, cx| {
469 editor.end_selection(window, cx);
470 });
471
472 assert_eq!(
473 editor
474 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
475 .unwrap(),
476 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
477 );
478}
479
480#[gpui::test]
481fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
482 init_test(cx, |_| {});
483
484 let editor = cx.add_window(|window, cx| {
485 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
486 build_editor(buffer, window, cx)
487 });
488
489 _ = editor.update(cx, |editor, window, cx| {
490 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
491 });
492
493 _ = editor.update(cx, |editor, window, cx| {
494 editor.end_selection(window, cx);
495 });
496
497 _ = editor.update(cx, |editor, window, cx| {
498 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
499 });
500
501 _ = editor.update(cx, |editor, window, cx| {
502 editor.end_selection(window, cx);
503 });
504
505 assert_eq!(
506 editor
507 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
508 .unwrap(),
509 [
510 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
511 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
512 ]
513 );
514
515 _ = editor.update(cx, |editor, window, cx| {
516 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
517 });
518
519 _ = editor.update(cx, |editor, window, cx| {
520 editor.end_selection(window, cx);
521 });
522
523 assert_eq!(
524 editor
525 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
526 .unwrap(),
527 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
528 );
529}
530
531#[gpui::test]
532fn test_canceling_pending_selection(cx: &mut TestAppContext) {
533 init_test(cx, |_| {});
534
535 let editor = cx.add_window(|window, cx| {
536 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
537 build_editor(buffer, window, cx)
538 });
539
540 _ = editor.update(cx, |editor, window, cx| {
541 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
542 assert_eq!(
543 editor.selections.display_ranges(cx),
544 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
545 );
546 });
547
548 _ = editor.update(cx, |editor, window, cx| {
549 editor.update_selection(
550 DisplayPoint::new(DisplayRow(3), 3),
551 0,
552 gpui::Point::<f32>::default(),
553 window,
554 cx,
555 );
556 assert_eq!(
557 editor.selections.display_ranges(cx),
558 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
559 );
560 });
561
562 _ = editor.update(cx, |editor, window, cx| {
563 editor.cancel(&Cancel, window, cx);
564 editor.update_selection(
565 DisplayPoint::new(DisplayRow(1), 1),
566 0,
567 gpui::Point::<f32>::default(),
568 window,
569 cx,
570 );
571 assert_eq!(
572 editor.selections.display_ranges(cx),
573 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
574 );
575 });
576}
577
578#[gpui::test]
579fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
580 init_test(cx, |_| {});
581
582 let editor = cx.add_window(|window, cx| {
583 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
584 build_editor(buffer, window, cx)
585 });
586
587 _ = editor.update(cx, |editor, window, cx| {
588 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
589 assert_eq!(
590 editor.selections.display_ranges(cx),
591 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
592 );
593
594 editor.move_down(&Default::default(), window, cx);
595 assert_eq!(
596 editor.selections.display_ranges(cx),
597 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
598 );
599
600 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
601 assert_eq!(
602 editor.selections.display_ranges(cx),
603 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
604 );
605
606 editor.move_up(&Default::default(), window, cx);
607 assert_eq!(
608 editor.selections.display_ranges(cx),
609 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
610 );
611 });
612}
613
614#[gpui::test]
615fn test_clone(cx: &mut TestAppContext) {
616 init_test(cx, |_| {});
617
618 let (text, selection_ranges) = marked_text_ranges(
619 indoc! {"
620 one
621 two
622 threeˇ
623 four
624 fiveˇ
625 "},
626 true,
627 );
628
629 let editor = cx.add_window(|window, cx| {
630 let buffer = MultiBuffer::build_simple(&text, cx);
631 build_editor(buffer, window, cx)
632 });
633
634 _ = editor.update(cx, |editor, window, cx| {
635 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
636 s.select_ranges(selection_ranges.clone())
637 });
638 editor.fold_creases(
639 vec![
640 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
641 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
642 ],
643 true,
644 window,
645 cx,
646 );
647 });
648
649 let cloned_editor = editor
650 .update(cx, |editor, _, cx| {
651 cx.open_window(Default::default(), |window, cx| {
652 cx.new(|cx| editor.clone(window, cx))
653 })
654 })
655 .unwrap()
656 .unwrap();
657
658 let snapshot = editor
659 .update(cx, |e, window, cx| e.snapshot(window, cx))
660 .unwrap();
661 let cloned_snapshot = cloned_editor
662 .update(cx, |e, window, cx| e.snapshot(window, cx))
663 .unwrap();
664
665 assert_eq!(
666 cloned_editor
667 .update(cx, |e, _, cx| e.display_text(cx))
668 .unwrap(),
669 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
670 );
671 assert_eq!(
672 cloned_snapshot
673 .folds_in_range(0..text.len())
674 .collect::<Vec<_>>(),
675 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
676 );
677 assert_set_eq!(
678 cloned_editor
679 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
680 .unwrap(),
681 editor
682 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
683 .unwrap()
684 );
685 assert_set_eq!(
686 cloned_editor
687 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
688 .unwrap(),
689 editor
690 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
691 .unwrap()
692 );
693}
694
695#[gpui::test]
696async fn test_navigation_history(cx: &mut TestAppContext) {
697 init_test(cx, |_| {});
698
699 use workspace::item::Item;
700
701 let fs = FakeFs::new(cx.executor());
702 let project = Project::test(fs, [], cx).await;
703 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
704 let pane = workspace
705 .update(cx, |workspace, _, _| workspace.active_pane().clone())
706 .unwrap();
707
708 _ = workspace.update(cx, |_v, window, cx| {
709 cx.new(|cx| {
710 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
711 let mut editor = build_editor(buffer.clone(), window, cx);
712 let handle = cx.entity();
713 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
714
715 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
716 editor.nav_history.as_mut().unwrap().pop_backward(cx)
717 }
718
719 // Move the cursor a small distance.
720 // Nothing is added to the navigation history.
721 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
722 s.select_display_ranges([
723 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
724 ])
725 });
726 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
727 s.select_display_ranges([
728 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
729 ])
730 });
731 assert!(pop_history(&mut editor, cx).is_none());
732
733 // Move the cursor a large distance.
734 // The history can jump back to the previous position.
735 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
736 s.select_display_ranges([
737 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
738 ])
739 });
740 let nav_entry = pop_history(&mut editor, cx).unwrap();
741 editor.navigate(nav_entry.data.unwrap(), window, cx);
742 assert_eq!(nav_entry.item.id(), cx.entity_id());
743 assert_eq!(
744 editor.selections.display_ranges(cx),
745 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
746 );
747 assert!(pop_history(&mut editor, cx).is_none());
748
749 // Move the cursor a small distance via the mouse.
750 // Nothing is added to the navigation history.
751 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
752 editor.end_selection(window, cx);
753 assert_eq!(
754 editor.selections.display_ranges(cx),
755 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
756 );
757 assert!(pop_history(&mut editor, cx).is_none());
758
759 // Move the cursor a large distance via the mouse.
760 // The history can jump back to the previous position.
761 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
762 editor.end_selection(window, cx);
763 assert_eq!(
764 editor.selections.display_ranges(cx),
765 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
766 );
767 let nav_entry = pop_history(&mut editor, cx).unwrap();
768 editor.navigate(nav_entry.data.unwrap(), window, cx);
769 assert_eq!(nav_entry.item.id(), cx.entity_id());
770 assert_eq!(
771 editor.selections.display_ranges(cx),
772 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
773 );
774 assert!(pop_history(&mut editor, cx).is_none());
775
776 // Set scroll position to check later
777 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
778 let original_scroll_position = editor.scroll_manager.anchor();
779
780 // Jump to the end of the document and adjust scroll
781 editor.move_to_end(&MoveToEnd, window, cx);
782 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
783 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
784
785 let nav_entry = pop_history(&mut editor, cx).unwrap();
786 editor.navigate(nav_entry.data.unwrap(), window, cx);
787 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
788
789 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
790 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
791 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
792 let invalid_point = Point::new(9999, 0);
793 editor.navigate(
794 Box::new(NavigationData {
795 cursor_anchor: invalid_anchor,
796 cursor_position: invalid_point,
797 scroll_anchor: ScrollAnchor {
798 anchor: invalid_anchor,
799 offset: Default::default(),
800 },
801 scroll_top_row: invalid_point.row,
802 }),
803 window,
804 cx,
805 );
806 assert_eq!(
807 editor.selections.display_ranges(cx),
808 &[editor.max_point(cx)..editor.max_point(cx)]
809 );
810 assert_eq!(
811 editor.scroll_position(cx),
812 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
813 );
814
815 editor
816 })
817 });
818}
819
820#[gpui::test]
821fn test_cancel(cx: &mut TestAppContext) {
822 init_test(cx, |_| {});
823
824 let editor = cx.add_window(|window, cx| {
825 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
826 build_editor(buffer, window, cx)
827 });
828
829 _ = editor.update(cx, |editor, window, cx| {
830 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
831 editor.update_selection(
832 DisplayPoint::new(DisplayRow(1), 1),
833 0,
834 gpui::Point::<f32>::default(),
835 window,
836 cx,
837 );
838 editor.end_selection(window, cx);
839
840 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
841 editor.update_selection(
842 DisplayPoint::new(DisplayRow(0), 3),
843 0,
844 gpui::Point::<f32>::default(),
845 window,
846 cx,
847 );
848 editor.end_selection(window, cx);
849 assert_eq!(
850 editor.selections.display_ranges(cx),
851 [
852 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
853 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
854 ]
855 );
856 });
857
858 _ = editor.update(cx, |editor, window, cx| {
859 editor.cancel(&Cancel, window, cx);
860 assert_eq!(
861 editor.selections.display_ranges(cx),
862 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
863 );
864 });
865
866 _ = editor.update(cx, |editor, window, cx| {
867 editor.cancel(&Cancel, window, cx);
868 assert_eq!(
869 editor.selections.display_ranges(cx),
870 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
871 );
872 });
873}
874
875#[gpui::test]
876fn test_fold_action(cx: &mut TestAppContext) {
877 init_test(cx, |_| {});
878
879 let editor = cx.add_window(|window, cx| {
880 let buffer = MultiBuffer::build_simple(
881 &"
882 impl Foo {
883 // Hello!
884
885 fn a() {
886 1
887 }
888
889 fn b() {
890 2
891 }
892
893 fn c() {
894 3
895 }
896 }
897 "
898 .unindent(),
899 cx,
900 );
901 build_editor(buffer.clone(), window, cx)
902 });
903
904 _ = editor.update(cx, |editor, window, cx| {
905 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
906 s.select_display_ranges([
907 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
908 ]);
909 });
910 editor.fold(&Fold, window, cx);
911 assert_eq!(
912 editor.display_text(cx),
913 "
914 impl Foo {
915 // Hello!
916
917 fn a() {
918 1
919 }
920
921 fn b() {⋯
922 }
923
924 fn c() {⋯
925 }
926 }
927 "
928 .unindent(),
929 );
930
931 editor.fold(&Fold, window, cx);
932 assert_eq!(
933 editor.display_text(cx),
934 "
935 impl Foo {⋯
936 }
937 "
938 .unindent(),
939 );
940
941 editor.unfold_lines(&UnfoldLines, window, cx);
942 assert_eq!(
943 editor.display_text(cx),
944 "
945 impl Foo {
946 // Hello!
947
948 fn a() {
949 1
950 }
951
952 fn b() {⋯
953 }
954
955 fn c() {⋯
956 }
957 }
958 "
959 .unindent(),
960 );
961
962 editor.unfold_lines(&UnfoldLines, window, cx);
963 assert_eq!(
964 editor.display_text(cx),
965 editor.buffer.read(cx).read(cx).text()
966 );
967 });
968}
969
970#[gpui::test]
971fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
972 init_test(cx, |_| {});
973
974 let editor = cx.add_window(|window, cx| {
975 let buffer = MultiBuffer::build_simple(
976 &"
977 class Foo:
978 # Hello!
979
980 def a():
981 print(1)
982
983 def b():
984 print(2)
985
986 def c():
987 print(3)
988 "
989 .unindent(),
990 cx,
991 );
992 build_editor(buffer.clone(), window, cx)
993 });
994
995 _ = editor.update(cx, |editor, window, cx| {
996 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
997 s.select_display_ranges([
998 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
999 ]);
1000 });
1001 editor.fold(&Fold, window, cx);
1002 assert_eq!(
1003 editor.display_text(cx),
1004 "
1005 class Foo:
1006 # Hello!
1007
1008 def a():
1009 print(1)
1010
1011 def b():⋯
1012
1013 def c():⋯
1014 "
1015 .unindent(),
1016 );
1017
1018 editor.fold(&Fold, window, cx);
1019 assert_eq!(
1020 editor.display_text(cx),
1021 "
1022 class Foo:⋯
1023 "
1024 .unindent(),
1025 );
1026
1027 editor.unfold_lines(&UnfoldLines, window, cx);
1028 assert_eq!(
1029 editor.display_text(cx),
1030 "
1031 class Foo:
1032 # Hello!
1033
1034 def a():
1035 print(1)
1036
1037 def b():⋯
1038
1039 def c():⋯
1040 "
1041 .unindent(),
1042 );
1043
1044 editor.unfold_lines(&UnfoldLines, window, cx);
1045 assert_eq!(
1046 editor.display_text(cx),
1047 editor.buffer.read(cx).read(cx).text()
1048 );
1049 });
1050}
1051
1052#[gpui::test]
1053fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1054 init_test(cx, |_| {});
1055
1056 let editor = cx.add_window(|window, cx| {
1057 let buffer = MultiBuffer::build_simple(
1058 &"
1059 class Foo:
1060 # Hello!
1061
1062 def a():
1063 print(1)
1064
1065 def b():
1066 print(2)
1067
1068
1069 def c():
1070 print(3)
1071
1072
1073 "
1074 .unindent(),
1075 cx,
1076 );
1077 build_editor(buffer.clone(), window, cx)
1078 });
1079
1080 _ = editor.update(cx, |editor, window, cx| {
1081 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1082 s.select_display_ranges([
1083 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1084 ]);
1085 });
1086 editor.fold(&Fold, window, cx);
1087 assert_eq!(
1088 editor.display_text(cx),
1089 "
1090 class Foo:
1091 # Hello!
1092
1093 def a():
1094 print(1)
1095
1096 def b():⋯
1097
1098
1099 def c():⋯
1100
1101
1102 "
1103 .unindent(),
1104 );
1105
1106 editor.fold(&Fold, window, cx);
1107 assert_eq!(
1108 editor.display_text(cx),
1109 "
1110 class Foo:⋯
1111
1112
1113 "
1114 .unindent(),
1115 );
1116
1117 editor.unfold_lines(&UnfoldLines, window, cx);
1118 assert_eq!(
1119 editor.display_text(cx),
1120 "
1121 class Foo:
1122 # Hello!
1123
1124 def a():
1125 print(1)
1126
1127 def b():⋯
1128
1129
1130 def c():⋯
1131
1132
1133 "
1134 .unindent(),
1135 );
1136
1137 editor.unfold_lines(&UnfoldLines, window, cx);
1138 assert_eq!(
1139 editor.display_text(cx),
1140 editor.buffer.read(cx).read(cx).text()
1141 );
1142 });
1143}
1144
1145#[gpui::test]
1146fn test_fold_at_level(cx: &mut TestAppContext) {
1147 init_test(cx, |_| {});
1148
1149 let editor = cx.add_window(|window, cx| {
1150 let buffer = MultiBuffer::build_simple(
1151 &"
1152 class Foo:
1153 # Hello!
1154
1155 def a():
1156 print(1)
1157
1158 def b():
1159 print(2)
1160
1161
1162 class Bar:
1163 # World!
1164
1165 def a():
1166 print(1)
1167
1168 def b():
1169 print(2)
1170
1171
1172 "
1173 .unindent(),
1174 cx,
1175 );
1176 build_editor(buffer.clone(), window, cx)
1177 });
1178
1179 _ = editor.update(cx, |editor, window, cx| {
1180 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1181 assert_eq!(
1182 editor.display_text(cx),
1183 "
1184 class Foo:
1185 # Hello!
1186
1187 def a():⋯
1188
1189 def b():⋯
1190
1191
1192 class Bar:
1193 # World!
1194
1195 def a():⋯
1196
1197 def b():⋯
1198
1199
1200 "
1201 .unindent(),
1202 );
1203
1204 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1205 assert_eq!(
1206 editor.display_text(cx),
1207 "
1208 class Foo:⋯
1209
1210
1211 class Bar:⋯
1212
1213
1214 "
1215 .unindent(),
1216 );
1217
1218 editor.unfold_all(&UnfoldAll, window, cx);
1219 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1220 assert_eq!(
1221 editor.display_text(cx),
1222 "
1223 class Foo:
1224 # Hello!
1225
1226 def a():
1227 print(1)
1228
1229 def b():
1230 print(2)
1231
1232
1233 class Bar:
1234 # World!
1235
1236 def a():
1237 print(1)
1238
1239 def b():
1240 print(2)
1241
1242
1243 "
1244 .unindent(),
1245 );
1246
1247 assert_eq!(
1248 editor.display_text(cx),
1249 editor.buffer.read(cx).read(cx).text()
1250 );
1251 });
1252}
1253
1254#[gpui::test]
1255fn test_move_cursor(cx: &mut TestAppContext) {
1256 init_test(cx, |_| {});
1257
1258 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1259 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1260
1261 buffer.update(cx, |buffer, cx| {
1262 buffer.edit(
1263 vec![
1264 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1265 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1266 ],
1267 None,
1268 cx,
1269 );
1270 });
1271 _ = editor.update(cx, |editor, window, cx| {
1272 assert_eq!(
1273 editor.selections.display_ranges(cx),
1274 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1275 );
1276
1277 editor.move_down(&MoveDown, window, cx);
1278 assert_eq!(
1279 editor.selections.display_ranges(cx),
1280 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1281 );
1282
1283 editor.move_right(&MoveRight, window, cx);
1284 assert_eq!(
1285 editor.selections.display_ranges(cx),
1286 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1287 );
1288
1289 editor.move_left(&MoveLeft, window, cx);
1290 assert_eq!(
1291 editor.selections.display_ranges(cx),
1292 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1293 );
1294
1295 editor.move_up(&MoveUp, window, cx);
1296 assert_eq!(
1297 editor.selections.display_ranges(cx),
1298 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1299 );
1300
1301 editor.move_to_end(&MoveToEnd, window, cx);
1302 assert_eq!(
1303 editor.selections.display_ranges(cx),
1304 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1305 );
1306
1307 editor.move_to_beginning(&MoveToBeginning, window, cx);
1308 assert_eq!(
1309 editor.selections.display_ranges(cx),
1310 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1311 );
1312
1313 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1314 s.select_display_ranges([
1315 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1316 ]);
1317 });
1318 editor.select_to_beginning(&SelectToBeginning, window, cx);
1319 assert_eq!(
1320 editor.selections.display_ranges(cx),
1321 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1322 );
1323
1324 editor.select_to_end(&SelectToEnd, window, cx);
1325 assert_eq!(
1326 editor.selections.display_ranges(cx),
1327 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1328 );
1329 });
1330}
1331
1332#[gpui::test]
1333fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1334 init_test(cx, |_| {});
1335
1336 let editor = cx.add_window(|window, cx| {
1337 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1338 build_editor(buffer.clone(), window, cx)
1339 });
1340
1341 assert_eq!('🟥'.len_utf8(), 4);
1342 assert_eq!('α'.len_utf8(), 2);
1343
1344 _ = editor.update(cx, |editor, window, cx| {
1345 editor.fold_creases(
1346 vec![
1347 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1348 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1349 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1350 ],
1351 true,
1352 window,
1353 cx,
1354 );
1355 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1356
1357 editor.move_right(&MoveRight, window, cx);
1358 assert_eq!(
1359 editor.selections.display_ranges(cx),
1360 &[empty_range(0, "🟥".len())]
1361 );
1362 editor.move_right(&MoveRight, window, cx);
1363 assert_eq!(
1364 editor.selections.display_ranges(cx),
1365 &[empty_range(0, "🟥🟧".len())]
1366 );
1367 editor.move_right(&MoveRight, window, cx);
1368 assert_eq!(
1369 editor.selections.display_ranges(cx),
1370 &[empty_range(0, "🟥🟧⋯".len())]
1371 );
1372
1373 editor.move_down(&MoveDown, window, cx);
1374 assert_eq!(
1375 editor.selections.display_ranges(cx),
1376 &[empty_range(1, "ab⋯e".len())]
1377 );
1378 editor.move_left(&MoveLeft, window, cx);
1379 assert_eq!(
1380 editor.selections.display_ranges(cx),
1381 &[empty_range(1, "ab⋯".len())]
1382 );
1383 editor.move_left(&MoveLeft, window, cx);
1384 assert_eq!(
1385 editor.selections.display_ranges(cx),
1386 &[empty_range(1, "ab".len())]
1387 );
1388 editor.move_left(&MoveLeft, window, cx);
1389 assert_eq!(
1390 editor.selections.display_ranges(cx),
1391 &[empty_range(1, "a".len())]
1392 );
1393
1394 editor.move_down(&MoveDown, window, cx);
1395 assert_eq!(
1396 editor.selections.display_ranges(cx),
1397 &[empty_range(2, "α".len())]
1398 );
1399 editor.move_right(&MoveRight, window, cx);
1400 assert_eq!(
1401 editor.selections.display_ranges(cx),
1402 &[empty_range(2, "αβ".len())]
1403 );
1404 editor.move_right(&MoveRight, window, cx);
1405 assert_eq!(
1406 editor.selections.display_ranges(cx),
1407 &[empty_range(2, "αβ⋯".len())]
1408 );
1409 editor.move_right(&MoveRight, window, cx);
1410 assert_eq!(
1411 editor.selections.display_ranges(cx),
1412 &[empty_range(2, "αβ⋯ε".len())]
1413 );
1414
1415 editor.move_up(&MoveUp, window, cx);
1416 assert_eq!(
1417 editor.selections.display_ranges(cx),
1418 &[empty_range(1, "ab⋯e".len())]
1419 );
1420 editor.move_down(&MoveDown, window, cx);
1421 assert_eq!(
1422 editor.selections.display_ranges(cx),
1423 &[empty_range(2, "αβ⋯ε".len())]
1424 );
1425 editor.move_up(&MoveUp, window, cx);
1426 assert_eq!(
1427 editor.selections.display_ranges(cx),
1428 &[empty_range(1, "ab⋯e".len())]
1429 );
1430
1431 editor.move_up(&MoveUp, window, cx);
1432 assert_eq!(
1433 editor.selections.display_ranges(cx),
1434 &[empty_range(0, "🟥🟧".len())]
1435 );
1436 editor.move_left(&MoveLeft, window, cx);
1437 assert_eq!(
1438 editor.selections.display_ranges(cx),
1439 &[empty_range(0, "🟥".len())]
1440 );
1441 editor.move_left(&MoveLeft, window, cx);
1442 assert_eq!(
1443 editor.selections.display_ranges(cx),
1444 &[empty_range(0, "".len())]
1445 );
1446 });
1447}
1448
1449#[gpui::test]
1450fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1451 init_test(cx, |_| {});
1452
1453 let editor = cx.add_window(|window, cx| {
1454 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1455 build_editor(buffer.clone(), window, cx)
1456 });
1457 _ = editor.update(cx, |editor, window, cx| {
1458 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1459 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1460 });
1461
1462 // moving above start of document should move selection to start of document,
1463 // but the next move down should still be at the original goal_x
1464 editor.move_up(&MoveUp, window, cx);
1465 assert_eq!(
1466 editor.selections.display_ranges(cx),
1467 &[empty_range(0, "".len())]
1468 );
1469
1470 editor.move_down(&MoveDown, window, cx);
1471 assert_eq!(
1472 editor.selections.display_ranges(cx),
1473 &[empty_range(1, "abcd".len())]
1474 );
1475
1476 editor.move_down(&MoveDown, window, cx);
1477 assert_eq!(
1478 editor.selections.display_ranges(cx),
1479 &[empty_range(2, "αβγ".len())]
1480 );
1481
1482 editor.move_down(&MoveDown, window, cx);
1483 assert_eq!(
1484 editor.selections.display_ranges(cx),
1485 &[empty_range(3, "abcd".len())]
1486 );
1487
1488 editor.move_down(&MoveDown, window, cx);
1489 assert_eq!(
1490 editor.selections.display_ranges(cx),
1491 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1492 );
1493
1494 // moving past end of document should not change goal_x
1495 editor.move_down(&MoveDown, window, cx);
1496 assert_eq!(
1497 editor.selections.display_ranges(cx),
1498 &[empty_range(5, "".len())]
1499 );
1500
1501 editor.move_down(&MoveDown, window, cx);
1502 assert_eq!(
1503 editor.selections.display_ranges(cx),
1504 &[empty_range(5, "".len())]
1505 );
1506
1507 editor.move_up(&MoveUp, window, cx);
1508 assert_eq!(
1509 editor.selections.display_ranges(cx),
1510 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1511 );
1512
1513 editor.move_up(&MoveUp, window, cx);
1514 assert_eq!(
1515 editor.selections.display_ranges(cx),
1516 &[empty_range(3, "abcd".len())]
1517 );
1518
1519 editor.move_up(&MoveUp, window, cx);
1520 assert_eq!(
1521 editor.selections.display_ranges(cx),
1522 &[empty_range(2, "αβγ".len())]
1523 );
1524 });
1525}
1526
1527#[gpui::test]
1528fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1529 init_test(cx, |_| {});
1530 let move_to_beg = MoveToBeginningOfLine {
1531 stop_at_soft_wraps: true,
1532 stop_at_indent: true,
1533 };
1534
1535 let delete_to_beg = DeleteToBeginningOfLine {
1536 stop_at_indent: false,
1537 };
1538
1539 let move_to_end = MoveToEndOfLine {
1540 stop_at_soft_wraps: true,
1541 };
1542
1543 let editor = cx.add_window(|window, cx| {
1544 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1545 build_editor(buffer, window, cx)
1546 });
1547 _ = editor.update(cx, |editor, window, cx| {
1548 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1549 s.select_display_ranges([
1550 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1551 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1552 ]);
1553 });
1554 });
1555
1556 _ = editor.update(cx, |editor, window, cx| {
1557 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1558 assert_eq!(
1559 editor.selections.display_ranges(cx),
1560 &[
1561 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1562 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1563 ]
1564 );
1565 });
1566
1567 _ = editor.update(cx, |editor, window, cx| {
1568 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1569 assert_eq!(
1570 editor.selections.display_ranges(cx),
1571 &[
1572 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1573 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1574 ]
1575 );
1576 });
1577
1578 _ = editor.update(cx, |editor, window, cx| {
1579 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1580 assert_eq!(
1581 editor.selections.display_ranges(cx),
1582 &[
1583 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1584 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1585 ]
1586 );
1587 });
1588
1589 _ = editor.update(cx, |editor, window, cx| {
1590 editor.move_to_end_of_line(&move_to_end, window, cx);
1591 assert_eq!(
1592 editor.selections.display_ranges(cx),
1593 &[
1594 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1595 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1596 ]
1597 );
1598 });
1599
1600 // Moving to the end of line again is a no-op.
1601 _ = editor.update(cx, |editor, window, cx| {
1602 editor.move_to_end_of_line(&move_to_end, window, cx);
1603 assert_eq!(
1604 editor.selections.display_ranges(cx),
1605 &[
1606 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1607 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1608 ]
1609 );
1610 });
1611
1612 _ = editor.update(cx, |editor, window, cx| {
1613 editor.move_left(&MoveLeft, window, cx);
1614 editor.select_to_beginning_of_line(
1615 &SelectToBeginningOfLine {
1616 stop_at_soft_wraps: true,
1617 stop_at_indent: true,
1618 },
1619 window,
1620 cx,
1621 );
1622 assert_eq!(
1623 editor.selections.display_ranges(cx),
1624 &[
1625 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1626 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1627 ]
1628 );
1629 });
1630
1631 _ = editor.update(cx, |editor, window, cx| {
1632 editor.select_to_beginning_of_line(
1633 &SelectToBeginningOfLine {
1634 stop_at_soft_wraps: true,
1635 stop_at_indent: true,
1636 },
1637 window,
1638 cx,
1639 );
1640 assert_eq!(
1641 editor.selections.display_ranges(cx),
1642 &[
1643 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1644 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1645 ]
1646 );
1647 });
1648
1649 _ = editor.update(cx, |editor, window, cx| {
1650 editor.select_to_beginning_of_line(
1651 &SelectToBeginningOfLine {
1652 stop_at_soft_wraps: true,
1653 stop_at_indent: true,
1654 },
1655 window,
1656 cx,
1657 );
1658 assert_eq!(
1659 editor.selections.display_ranges(cx),
1660 &[
1661 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1662 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1663 ]
1664 );
1665 });
1666
1667 _ = editor.update(cx, |editor, window, cx| {
1668 editor.select_to_end_of_line(
1669 &SelectToEndOfLine {
1670 stop_at_soft_wraps: true,
1671 },
1672 window,
1673 cx,
1674 );
1675 assert_eq!(
1676 editor.selections.display_ranges(cx),
1677 &[
1678 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1679 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1680 ]
1681 );
1682 });
1683
1684 _ = editor.update(cx, |editor, window, cx| {
1685 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1686 assert_eq!(editor.display_text(cx), "ab\n de");
1687 assert_eq!(
1688 editor.selections.display_ranges(cx),
1689 &[
1690 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1691 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1692 ]
1693 );
1694 });
1695
1696 _ = editor.update(cx, |editor, window, cx| {
1697 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1698 assert_eq!(editor.display_text(cx), "\n");
1699 assert_eq!(
1700 editor.selections.display_ranges(cx),
1701 &[
1702 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1703 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1704 ]
1705 );
1706 });
1707}
1708
1709#[gpui::test]
1710fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1711 init_test(cx, |_| {});
1712 let move_to_beg = MoveToBeginningOfLine {
1713 stop_at_soft_wraps: false,
1714 stop_at_indent: false,
1715 };
1716
1717 let move_to_end = MoveToEndOfLine {
1718 stop_at_soft_wraps: false,
1719 };
1720
1721 let editor = cx.add_window(|window, cx| {
1722 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1723 build_editor(buffer, window, cx)
1724 });
1725
1726 _ = editor.update(cx, |editor, window, cx| {
1727 editor.set_wrap_width(Some(140.0.into()), cx);
1728
1729 // We expect the following lines after wrapping
1730 // ```
1731 // thequickbrownfox
1732 // jumpedoverthelazydo
1733 // gs
1734 // ```
1735 // The final `gs` was soft-wrapped onto a new line.
1736 assert_eq!(
1737 "thequickbrownfox\njumpedoverthelaz\nydogs",
1738 editor.display_text(cx),
1739 );
1740
1741 // First, let's assert behavior on the first line, that was not soft-wrapped.
1742 // Start the cursor at the `k` on the first line
1743 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1744 s.select_display_ranges([
1745 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1746 ]);
1747 });
1748
1749 // Moving to the beginning of the line should put us at the beginning of the line.
1750 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1751 assert_eq!(
1752 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1753 editor.selections.display_ranges(cx)
1754 );
1755
1756 // Moving to the end of the line should put us at the end of the line.
1757 editor.move_to_end_of_line(&move_to_end, window, cx);
1758 assert_eq!(
1759 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1760 editor.selections.display_ranges(cx)
1761 );
1762
1763 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1764 // Start the cursor at the last line (`y` that was wrapped to a new line)
1765 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1766 s.select_display_ranges([
1767 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1768 ]);
1769 });
1770
1771 // Moving to the beginning of the line should put us at the start of the second line of
1772 // display text, i.e., the `j`.
1773 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1774 assert_eq!(
1775 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1776 editor.selections.display_ranges(cx)
1777 );
1778
1779 // Moving to the beginning of the line again should be a no-op.
1780 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1781 assert_eq!(
1782 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1783 editor.selections.display_ranges(cx)
1784 );
1785
1786 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1787 // next display line.
1788 editor.move_to_end_of_line(&move_to_end, window, cx);
1789 assert_eq!(
1790 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1791 editor.selections.display_ranges(cx)
1792 );
1793
1794 // Moving to the end of the line again should be a no-op.
1795 editor.move_to_end_of_line(&move_to_end, window, cx);
1796 assert_eq!(
1797 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1798 editor.selections.display_ranges(cx)
1799 );
1800 });
1801}
1802
1803#[gpui::test]
1804fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1805 init_test(cx, |_| {});
1806
1807 let move_to_beg = MoveToBeginningOfLine {
1808 stop_at_soft_wraps: true,
1809 stop_at_indent: true,
1810 };
1811
1812 let select_to_beg = SelectToBeginningOfLine {
1813 stop_at_soft_wraps: true,
1814 stop_at_indent: true,
1815 };
1816
1817 let delete_to_beg = DeleteToBeginningOfLine {
1818 stop_at_indent: true,
1819 };
1820
1821 let move_to_end = MoveToEndOfLine {
1822 stop_at_soft_wraps: false,
1823 };
1824
1825 let editor = cx.add_window(|window, cx| {
1826 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1827 build_editor(buffer, window, cx)
1828 });
1829
1830 _ = editor.update(cx, |editor, window, cx| {
1831 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1832 s.select_display_ranges([
1833 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1834 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1835 ]);
1836 });
1837
1838 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1839 // and the second cursor at the first non-whitespace character in the line.
1840 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1841 assert_eq!(
1842 editor.selections.display_ranges(cx),
1843 &[
1844 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1845 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1846 ]
1847 );
1848
1849 // Moving to the beginning of the line again should be a no-op for the first cursor,
1850 // and should move the second cursor to the beginning of the line.
1851 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1852 assert_eq!(
1853 editor.selections.display_ranges(cx),
1854 &[
1855 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1856 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1857 ]
1858 );
1859
1860 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1861 // and should move the second cursor back to the first non-whitespace character in the line.
1862 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1863 assert_eq!(
1864 editor.selections.display_ranges(cx),
1865 &[
1866 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1867 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1868 ]
1869 );
1870
1871 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1872 // and to the first non-whitespace character in the line for the second cursor.
1873 editor.move_to_end_of_line(&move_to_end, window, cx);
1874 editor.move_left(&MoveLeft, window, cx);
1875 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1876 assert_eq!(
1877 editor.selections.display_ranges(cx),
1878 &[
1879 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1880 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1881 ]
1882 );
1883
1884 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1885 // and should select to the beginning of the line for the second cursor.
1886 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1887 assert_eq!(
1888 editor.selections.display_ranges(cx),
1889 &[
1890 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1891 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1892 ]
1893 );
1894
1895 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1896 // and should delete to the first non-whitespace character in the line for the second cursor.
1897 editor.move_to_end_of_line(&move_to_end, window, cx);
1898 editor.move_left(&MoveLeft, window, cx);
1899 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1900 assert_eq!(editor.text(cx), "c\n f");
1901 });
1902}
1903
1904#[gpui::test]
1905fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1906 init_test(cx, |_| {});
1907
1908 let editor = cx.add_window(|window, cx| {
1909 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1910 build_editor(buffer, window, cx)
1911 });
1912 _ = editor.update(cx, |editor, window, cx| {
1913 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1914 s.select_display_ranges([
1915 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1916 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1917 ])
1918 });
1919 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1920 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1921
1922 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1923 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1924
1925 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1926 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1927
1928 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1929 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1930
1931 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1932 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
1933
1934 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1935 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1936
1937 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1938 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1939
1940 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1941 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1942
1943 editor.move_right(&MoveRight, window, cx);
1944 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1945 assert_selection_ranges(
1946 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
1947 editor,
1948 cx,
1949 );
1950
1951 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1952 assert_selection_ranges(
1953 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
1954 editor,
1955 cx,
1956 );
1957
1958 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1959 assert_selection_ranges(
1960 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
1961 editor,
1962 cx,
1963 );
1964 });
1965}
1966
1967#[gpui::test]
1968fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1969 init_test(cx, |_| {});
1970
1971 let editor = cx.add_window(|window, cx| {
1972 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1973 build_editor(buffer, window, cx)
1974 });
1975
1976 _ = editor.update(cx, |editor, window, cx| {
1977 editor.set_wrap_width(Some(140.0.into()), cx);
1978 assert_eq!(
1979 editor.display_text(cx),
1980 "use one::{\n two::three::\n four::five\n};"
1981 );
1982
1983 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1984 s.select_display_ranges([
1985 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1986 ]);
1987 });
1988
1989 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1990 assert_eq!(
1991 editor.selections.display_ranges(cx),
1992 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1993 );
1994
1995 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1996 assert_eq!(
1997 editor.selections.display_ranges(cx),
1998 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1999 );
2000
2001 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2002 assert_eq!(
2003 editor.selections.display_ranges(cx),
2004 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2005 );
2006
2007 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2008 assert_eq!(
2009 editor.selections.display_ranges(cx),
2010 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2011 );
2012
2013 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2014 assert_eq!(
2015 editor.selections.display_ranges(cx),
2016 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2017 );
2018
2019 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2020 assert_eq!(
2021 editor.selections.display_ranges(cx),
2022 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2023 );
2024 });
2025}
2026
2027#[gpui::test]
2028async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2029 init_test(cx, |_| {});
2030 let mut cx = EditorTestContext::new(cx).await;
2031
2032 let line_height = cx.editor(|editor, window, _| {
2033 editor
2034 .style()
2035 .unwrap()
2036 .text
2037 .line_height_in_pixels(window.rem_size())
2038 });
2039 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2040
2041 cx.set_state(
2042 &r#"ˇone
2043 two
2044
2045 three
2046 fourˇ
2047 five
2048
2049 six"#
2050 .unindent(),
2051 );
2052
2053 cx.update_editor(|editor, window, cx| {
2054 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2055 });
2056 cx.assert_editor_state(
2057 &r#"one
2058 two
2059 ˇ
2060 three
2061 four
2062 five
2063 ˇ
2064 six"#
2065 .unindent(),
2066 );
2067
2068 cx.update_editor(|editor, window, cx| {
2069 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2070 });
2071 cx.assert_editor_state(
2072 &r#"one
2073 two
2074
2075 three
2076 four
2077 five
2078 ˇ
2079 sixˇ"#
2080 .unindent(),
2081 );
2082
2083 cx.update_editor(|editor, window, cx| {
2084 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2085 });
2086 cx.assert_editor_state(
2087 &r#"one
2088 two
2089
2090 three
2091 four
2092 five
2093
2094 sixˇ"#
2095 .unindent(),
2096 );
2097
2098 cx.update_editor(|editor, window, cx| {
2099 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2100 });
2101 cx.assert_editor_state(
2102 &r#"one
2103 two
2104
2105 three
2106 four
2107 five
2108 ˇ
2109 six"#
2110 .unindent(),
2111 );
2112
2113 cx.update_editor(|editor, window, cx| {
2114 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2115 });
2116 cx.assert_editor_state(
2117 &r#"one
2118 two
2119 ˇ
2120 three
2121 four
2122 five
2123
2124 six"#
2125 .unindent(),
2126 );
2127
2128 cx.update_editor(|editor, window, cx| {
2129 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2130 });
2131 cx.assert_editor_state(
2132 &r#"ˇone
2133 two
2134
2135 three
2136 four
2137 five
2138
2139 six"#
2140 .unindent(),
2141 );
2142}
2143
2144#[gpui::test]
2145async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2146 init_test(cx, |_| {});
2147 let mut cx = EditorTestContext::new(cx).await;
2148 let line_height = cx.editor(|editor, window, _| {
2149 editor
2150 .style()
2151 .unwrap()
2152 .text
2153 .line_height_in_pixels(window.rem_size())
2154 });
2155 let window = cx.window;
2156 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2157
2158 cx.set_state(
2159 r#"ˇone
2160 two
2161 three
2162 four
2163 five
2164 six
2165 seven
2166 eight
2167 nine
2168 ten
2169 "#,
2170 );
2171
2172 cx.update_editor(|editor, window, cx| {
2173 assert_eq!(
2174 editor.snapshot(window, cx).scroll_position(),
2175 gpui::Point::new(0., 0.)
2176 );
2177 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2178 assert_eq!(
2179 editor.snapshot(window, cx).scroll_position(),
2180 gpui::Point::new(0., 3.)
2181 );
2182 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2183 assert_eq!(
2184 editor.snapshot(window, cx).scroll_position(),
2185 gpui::Point::new(0., 6.)
2186 );
2187 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2188 assert_eq!(
2189 editor.snapshot(window, cx).scroll_position(),
2190 gpui::Point::new(0., 3.)
2191 );
2192
2193 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2194 assert_eq!(
2195 editor.snapshot(window, cx).scroll_position(),
2196 gpui::Point::new(0., 1.)
2197 );
2198 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2199 assert_eq!(
2200 editor.snapshot(window, cx).scroll_position(),
2201 gpui::Point::new(0., 3.)
2202 );
2203 });
2204}
2205
2206#[gpui::test]
2207async fn test_autoscroll(cx: &mut TestAppContext) {
2208 init_test(cx, |_| {});
2209 let mut cx = EditorTestContext::new(cx).await;
2210
2211 let line_height = cx.update_editor(|editor, window, cx| {
2212 editor.set_vertical_scroll_margin(2, cx);
2213 editor
2214 .style()
2215 .unwrap()
2216 .text
2217 .line_height_in_pixels(window.rem_size())
2218 });
2219 let window = cx.window;
2220 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2221
2222 cx.set_state(
2223 r#"ˇone
2224 two
2225 three
2226 four
2227 five
2228 six
2229 seven
2230 eight
2231 nine
2232 ten
2233 "#,
2234 );
2235 cx.update_editor(|editor, window, cx| {
2236 assert_eq!(
2237 editor.snapshot(window, cx).scroll_position(),
2238 gpui::Point::new(0., 0.0)
2239 );
2240 });
2241
2242 // Add a cursor below the visible area. Since both cursors cannot fit
2243 // on screen, the editor autoscrolls to reveal the newest cursor, and
2244 // allows the vertical scroll margin below that cursor.
2245 cx.update_editor(|editor, window, cx| {
2246 editor.change_selections(Default::default(), window, cx, |selections| {
2247 selections.select_ranges([
2248 Point::new(0, 0)..Point::new(0, 0),
2249 Point::new(6, 0)..Point::new(6, 0),
2250 ]);
2251 })
2252 });
2253 cx.update_editor(|editor, window, cx| {
2254 assert_eq!(
2255 editor.snapshot(window, cx).scroll_position(),
2256 gpui::Point::new(0., 3.0)
2257 );
2258 });
2259
2260 // Move down. The editor cursor scrolls down to track the newest cursor.
2261 cx.update_editor(|editor, window, cx| {
2262 editor.move_down(&Default::default(), window, cx);
2263 });
2264 cx.update_editor(|editor, window, cx| {
2265 assert_eq!(
2266 editor.snapshot(window, cx).scroll_position(),
2267 gpui::Point::new(0., 4.0)
2268 );
2269 });
2270
2271 // Add a cursor above the visible area. Since both cursors fit on screen,
2272 // the editor scrolls to show both.
2273 cx.update_editor(|editor, window, cx| {
2274 editor.change_selections(Default::default(), window, cx, |selections| {
2275 selections.select_ranges([
2276 Point::new(1, 0)..Point::new(1, 0),
2277 Point::new(6, 0)..Point::new(6, 0),
2278 ]);
2279 })
2280 });
2281 cx.update_editor(|editor, window, cx| {
2282 assert_eq!(
2283 editor.snapshot(window, cx).scroll_position(),
2284 gpui::Point::new(0., 1.0)
2285 );
2286 });
2287}
2288
2289#[gpui::test]
2290async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2291 init_test(cx, |_| {});
2292 let mut cx = EditorTestContext::new(cx).await;
2293
2294 let line_height = cx.editor(|editor, window, _cx| {
2295 editor
2296 .style()
2297 .unwrap()
2298 .text
2299 .line_height_in_pixels(window.rem_size())
2300 });
2301 let window = cx.window;
2302 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2303 cx.set_state(
2304 &r#"
2305 ˇone
2306 two
2307 threeˇ
2308 four
2309 five
2310 six
2311 seven
2312 eight
2313 nine
2314 ten
2315 "#
2316 .unindent(),
2317 );
2318
2319 cx.update_editor(|editor, window, cx| {
2320 editor.move_page_down(&MovePageDown::default(), window, cx)
2321 });
2322 cx.assert_editor_state(
2323 &r#"
2324 one
2325 two
2326 three
2327 ˇfour
2328 five
2329 sixˇ
2330 seven
2331 eight
2332 nine
2333 ten
2334 "#
2335 .unindent(),
2336 );
2337
2338 cx.update_editor(|editor, window, cx| {
2339 editor.move_page_down(&MovePageDown::default(), window, cx)
2340 });
2341 cx.assert_editor_state(
2342 &r#"
2343 one
2344 two
2345 three
2346 four
2347 five
2348 six
2349 ˇseven
2350 eight
2351 nineˇ
2352 ten
2353 "#
2354 .unindent(),
2355 );
2356
2357 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2358 cx.assert_editor_state(
2359 &r#"
2360 one
2361 two
2362 three
2363 ˇfour
2364 five
2365 sixˇ
2366 seven
2367 eight
2368 nine
2369 ten
2370 "#
2371 .unindent(),
2372 );
2373
2374 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2375 cx.assert_editor_state(
2376 &r#"
2377 ˇone
2378 two
2379 threeˇ
2380 four
2381 five
2382 six
2383 seven
2384 eight
2385 nine
2386 ten
2387 "#
2388 .unindent(),
2389 );
2390
2391 // Test select collapsing
2392 cx.update_editor(|editor, window, cx| {
2393 editor.move_page_down(&MovePageDown::default(), window, cx);
2394 editor.move_page_down(&MovePageDown::default(), window, cx);
2395 editor.move_page_down(&MovePageDown::default(), window, cx);
2396 });
2397 cx.assert_editor_state(
2398 &r#"
2399 one
2400 two
2401 three
2402 four
2403 five
2404 six
2405 seven
2406 eight
2407 nine
2408 ˇten
2409 ˇ"#
2410 .unindent(),
2411 );
2412}
2413
2414#[gpui::test]
2415async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2416 init_test(cx, |_| {});
2417 let mut cx = EditorTestContext::new(cx).await;
2418 cx.set_state("one «two threeˇ» four");
2419 cx.update_editor(|editor, window, cx| {
2420 editor.delete_to_beginning_of_line(
2421 &DeleteToBeginningOfLine {
2422 stop_at_indent: false,
2423 },
2424 window,
2425 cx,
2426 );
2427 assert_eq!(editor.text(cx), " four");
2428 });
2429}
2430
2431#[gpui::test]
2432fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2433 init_test(cx, |_| {});
2434
2435 let editor = cx.add_window(|window, cx| {
2436 let buffer = MultiBuffer::build_simple("one two three four", cx);
2437 build_editor(buffer.clone(), window, cx)
2438 });
2439
2440 _ = editor.update(cx, |editor, window, cx| {
2441 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2442 s.select_display_ranges([
2443 // an empty selection - the preceding word fragment is deleted
2444 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2445 // characters selected - they are deleted
2446 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2447 ])
2448 });
2449 editor.delete_to_previous_word_start(
2450 &DeleteToPreviousWordStart {
2451 ignore_newlines: false,
2452 },
2453 window,
2454 cx,
2455 );
2456 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2457 });
2458
2459 _ = editor.update(cx, |editor, window, cx| {
2460 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2461 s.select_display_ranges([
2462 // an empty selection - the following word fragment is deleted
2463 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2464 // characters selected - they are deleted
2465 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2466 ])
2467 });
2468 editor.delete_to_next_word_end(
2469 &DeleteToNextWordEnd {
2470 ignore_newlines: false,
2471 },
2472 window,
2473 cx,
2474 );
2475 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2476 });
2477}
2478
2479#[gpui::test]
2480fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2481 init_test(cx, |_| {});
2482
2483 let editor = cx.add_window(|window, cx| {
2484 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2485 build_editor(buffer.clone(), window, cx)
2486 });
2487 let del_to_prev_word_start = DeleteToPreviousWordStart {
2488 ignore_newlines: false,
2489 };
2490 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2491 ignore_newlines: true,
2492 };
2493
2494 _ = editor.update(cx, |editor, window, cx| {
2495 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2496 s.select_display_ranges([
2497 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2498 ])
2499 });
2500 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2501 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2502 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2503 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2504 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2505 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2506 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2507 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2508 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2509 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2510 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2511 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2512 });
2513}
2514
2515#[gpui::test]
2516fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2517 init_test(cx, |_| {});
2518
2519 let editor = cx.add_window(|window, cx| {
2520 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2521 build_editor(buffer.clone(), window, cx)
2522 });
2523 let del_to_next_word_end = DeleteToNextWordEnd {
2524 ignore_newlines: false,
2525 };
2526 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2527 ignore_newlines: true,
2528 };
2529
2530 _ = editor.update(cx, |editor, window, cx| {
2531 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2532 s.select_display_ranges([
2533 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2534 ])
2535 });
2536 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2537 assert_eq!(
2538 editor.buffer.read(cx).read(cx).text(),
2539 "one\n two\nthree\n four"
2540 );
2541 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2542 assert_eq!(
2543 editor.buffer.read(cx).read(cx).text(),
2544 "\n two\nthree\n four"
2545 );
2546 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2547 assert_eq!(
2548 editor.buffer.read(cx).read(cx).text(),
2549 "two\nthree\n four"
2550 );
2551 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2552 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2553 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2554 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2555 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2556 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2557 });
2558}
2559
2560#[gpui::test]
2561fn test_newline(cx: &mut TestAppContext) {
2562 init_test(cx, |_| {});
2563
2564 let editor = cx.add_window(|window, cx| {
2565 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2566 build_editor(buffer.clone(), window, cx)
2567 });
2568
2569 _ = editor.update(cx, |editor, window, cx| {
2570 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2571 s.select_display_ranges([
2572 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2573 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2574 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2575 ])
2576 });
2577
2578 editor.newline(&Newline, window, cx);
2579 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2580 });
2581}
2582
2583#[gpui::test]
2584fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2585 init_test(cx, |_| {});
2586
2587 let editor = cx.add_window(|window, cx| {
2588 let buffer = MultiBuffer::build_simple(
2589 "
2590 a
2591 b(
2592 X
2593 )
2594 c(
2595 X
2596 )
2597 "
2598 .unindent()
2599 .as_str(),
2600 cx,
2601 );
2602 let mut editor = build_editor(buffer.clone(), window, cx);
2603 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2604 s.select_ranges([
2605 Point::new(2, 4)..Point::new(2, 5),
2606 Point::new(5, 4)..Point::new(5, 5),
2607 ])
2608 });
2609 editor
2610 });
2611
2612 _ = editor.update(cx, |editor, window, cx| {
2613 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2614 editor.buffer.update(cx, |buffer, cx| {
2615 buffer.edit(
2616 [
2617 (Point::new(1, 2)..Point::new(3, 0), ""),
2618 (Point::new(4, 2)..Point::new(6, 0), ""),
2619 ],
2620 None,
2621 cx,
2622 );
2623 assert_eq!(
2624 buffer.read(cx).text(),
2625 "
2626 a
2627 b()
2628 c()
2629 "
2630 .unindent()
2631 );
2632 });
2633 assert_eq!(
2634 editor.selections.ranges(cx),
2635 &[
2636 Point::new(1, 2)..Point::new(1, 2),
2637 Point::new(2, 2)..Point::new(2, 2),
2638 ],
2639 );
2640
2641 editor.newline(&Newline, window, cx);
2642 assert_eq!(
2643 editor.text(cx),
2644 "
2645 a
2646 b(
2647 )
2648 c(
2649 )
2650 "
2651 .unindent()
2652 );
2653
2654 // The selections are moved after the inserted newlines
2655 assert_eq!(
2656 editor.selections.ranges(cx),
2657 &[
2658 Point::new(2, 0)..Point::new(2, 0),
2659 Point::new(4, 0)..Point::new(4, 0),
2660 ],
2661 );
2662 });
2663}
2664
2665#[gpui::test]
2666async fn test_newline_above(cx: &mut TestAppContext) {
2667 init_test(cx, |settings| {
2668 settings.defaults.tab_size = NonZeroU32::new(4)
2669 });
2670
2671 let language = Arc::new(
2672 Language::new(
2673 LanguageConfig::default(),
2674 Some(tree_sitter_rust::LANGUAGE.into()),
2675 )
2676 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2677 .unwrap(),
2678 );
2679
2680 let mut cx = EditorTestContext::new(cx).await;
2681 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2682 cx.set_state(indoc! {"
2683 const a: ˇA = (
2684 (ˇ
2685 «const_functionˇ»(ˇ),
2686 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2687 )ˇ
2688 ˇ);ˇ
2689 "});
2690
2691 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2692 cx.assert_editor_state(indoc! {"
2693 ˇ
2694 const a: A = (
2695 ˇ
2696 (
2697 ˇ
2698 ˇ
2699 const_function(),
2700 ˇ
2701 ˇ
2702 ˇ
2703 ˇ
2704 something_else,
2705 ˇ
2706 )
2707 ˇ
2708 ˇ
2709 );
2710 "});
2711}
2712
2713#[gpui::test]
2714async fn test_newline_below(cx: &mut TestAppContext) {
2715 init_test(cx, |settings| {
2716 settings.defaults.tab_size = NonZeroU32::new(4)
2717 });
2718
2719 let language = Arc::new(
2720 Language::new(
2721 LanguageConfig::default(),
2722 Some(tree_sitter_rust::LANGUAGE.into()),
2723 )
2724 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2725 .unwrap(),
2726 );
2727
2728 let mut cx = EditorTestContext::new(cx).await;
2729 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2730 cx.set_state(indoc! {"
2731 const a: ˇA = (
2732 (ˇ
2733 «const_functionˇ»(ˇ),
2734 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2735 )ˇ
2736 ˇ);ˇ
2737 "});
2738
2739 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2740 cx.assert_editor_state(indoc! {"
2741 const a: A = (
2742 ˇ
2743 (
2744 ˇ
2745 const_function(),
2746 ˇ
2747 ˇ
2748 something_else,
2749 ˇ
2750 ˇ
2751 ˇ
2752 ˇ
2753 )
2754 ˇ
2755 );
2756 ˇ
2757 ˇ
2758 "});
2759}
2760
2761#[gpui::test]
2762async fn test_newline_comments(cx: &mut TestAppContext) {
2763 init_test(cx, |settings| {
2764 settings.defaults.tab_size = NonZeroU32::new(4)
2765 });
2766
2767 let language = Arc::new(Language::new(
2768 LanguageConfig {
2769 line_comments: vec!["// ".into()],
2770 ..LanguageConfig::default()
2771 },
2772 None,
2773 ));
2774 {
2775 let mut cx = EditorTestContext::new(cx).await;
2776 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2777 cx.set_state(indoc! {"
2778 // Fooˇ
2779 "});
2780
2781 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2782 cx.assert_editor_state(indoc! {"
2783 // Foo
2784 // ˇ
2785 "});
2786 // Ensure that we add comment prefix when existing line contains space
2787 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2788 cx.assert_editor_state(
2789 indoc! {"
2790 // Foo
2791 //s
2792 // ˇ
2793 "}
2794 .replace("s", " ") // s is used as space placeholder to prevent format on save
2795 .as_str(),
2796 );
2797 // Ensure that we add comment prefix when existing line does not contain space
2798 cx.set_state(indoc! {"
2799 // Foo
2800 //ˇ
2801 "});
2802 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2803 cx.assert_editor_state(indoc! {"
2804 // Foo
2805 //
2806 // ˇ
2807 "});
2808 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2809 cx.set_state(indoc! {"
2810 ˇ// Foo
2811 "});
2812 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2813 cx.assert_editor_state(indoc! {"
2814
2815 ˇ// Foo
2816 "});
2817 }
2818 // Ensure that comment continuations can be disabled.
2819 update_test_language_settings(cx, |settings| {
2820 settings.defaults.extend_comment_on_newline = Some(false);
2821 });
2822 let mut cx = EditorTestContext::new(cx).await;
2823 cx.set_state(indoc! {"
2824 // Fooˇ
2825 "});
2826 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2827 cx.assert_editor_state(indoc! {"
2828 // Foo
2829 ˇ
2830 "});
2831}
2832
2833#[gpui::test]
2834async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
2835 init_test(cx, |settings| {
2836 settings.defaults.tab_size = NonZeroU32::new(4)
2837 });
2838
2839 let language = Arc::new(Language::new(
2840 LanguageConfig {
2841 line_comments: vec!["// ".into(), "/// ".into()],
2842 ..LanguageConfig::default()
2843 },
2844 None,
2845 ));
2846 {
2847 let mut cx = EditorTestContext::new(cx).await;
2848 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2849 cx.set_state(indoc! {"
2850 //ˇ
2851 "});
2852 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2853 cx.assert_editor_state(indoc! {"
2854 //
2855 // ˇ
2856 "});
2857
2858 cx.set_state(indoc! {"
2859 ///ˇ
2860 "});
2861 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2862 cx.assert_editor_state(indoc! {"
2863 ///
2864 /// ˇ
2865 "});
2866 }
2867}
2868
2869#[gpui::test]
2870async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
2871 init_test(cx, |settings| {
2872 settings.defaults.tab_size = NonZeroU32::new(4)
2873 });
2874
2875 let language = Arc::new(
2876 Language::new(
2877 LanguageConfig {
2878 documentation_comment: Some(language::BlockCommentConfig {
2879 start: "/**".into(),
2880 end: "*/".into(),
2881 prefix: "* ".into(),
2882 tab_size: 1,
2883 }),
2884
2885 ..LanguageConfig::default()
2886 },
2887 Some(tree_sitter_rust::LANGUAGE.into()),
2888 )
2889 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
2890 .unwrap(),
2891 );
2892
2893 {
2894 let mut cx = EditorTestContext::new(cx).await;
2895 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2896 cx.set_state(indoc! {"
2897 /**ˇ
2898 "});
2899
2900 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2901 cx.assert_editor_state(indoc! {"
2902 /**
2903 * ˇ
2904 "});
2905 // Ensure that if cursor is before the comment start,
2906 // we do not actually insert a comment prefix.
2907 cx.set_state(indoc! {"
2908 ˇ/**
2909 "});
2910 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2911 cx.assert_editor_state(indoc! {"
2912
2913 ˇ/**
2914 "});
2915 // Ensure that if cursor is between it doesn't add comment prefix.
2916 cx.set_state(indoc! {"
2917 /*ˇ*
2918 "});
2919 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2920 cx.assert_editor_state(indoc! {"
2921 /*
2922 ˇ*
2923 "});
2924 // Ensure that if suffix exists on same line after cursor it adds new line.
2925 cx.set_state(indoc! {"
2926 /**ˇ*/
2927 "});
2928 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2929 cx.assert_editor_state(indoc! {"
2930 /**
2931 * ˇ
2932 */
2933 "});
2934 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2935 cx.set_state(indoc! {"
2936 /**ˇ */
2937 "});
2938 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2939 cx.assert_editor_state(indoc! {"
2940 /**
2941 * ˇ
2942 */
2943 "});
2944 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2945 cx.set_state(indoc! {"
2946 /** ˇ*/
2947 "});
2948 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2949 cx.assert_editor_state(
2950 indoc! {"
2951 /**s
2952 * ˇ
2953 */
2954 "}
2955 .replace("s", " ") // s is used as space placeholder to prevent format on save
2956 .as_str(),
2957 );
2958 // Ensure that delimiter space is preserved when newline on already
2959 // spaced delimiter.
2960 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2961 cx.assert_editor_state(
2962 indoc! {"
2963 /**s
2964 *s
2965 * ˇ
2966 */
2967 "}
2968 .replace("s", " ") // s is used as space placeholder to prevent format on save
2969 .as_str(),
2970 );
2971 // Ensure that delimiter space is preserved when space is not
2972 // on existing delimiter.
2973 cx.set_state(indoc! {"
2974 /**
2975 *ˇ
2976 */
2977 "});
2978 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2979 cx.assert_editor_state(indoc! {"
2980 /**
2981 *
2982 * ˇ
2983 */
2984 "});
2985 // Ensure that if suffix exists on same line after cursor it
2986 // doesn't add extra new line if prefix is not on same line.
2987 cx.set_state(indoc! {"
2988 /**
2989 ˇ*/
2990 "});
2991 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2992 cx.assert_editor_state(indoc! {"
2993 /**
2994
2995 ˇ*/
2996 "});
2997 // Ensure that it detects suffix after existing prefix.
2998 cx.set_state(indoc! {"
2999 /**ˇ/
3000 "});
3001 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3002 cx.assert_editor_state(indoc! {"
3003 /**
3004 ˇ/
3005 "});
3006 // Ensure that if suffix exists on same line before
3007 // cursor it does not add comment prefix.
3008 cx.set_state(indoc! {"
3009 /** */ˇ
3010 "});
3011 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3012 cx.assert_editor_state(indoc! {"
3013 /** */
3014 ˇ
3015 "});
3016 // Ensure that if suffix exists on same line before
3017 // cursor it does not add comment prefix.
3018 cx.set_state(indoc! {"
3019 /**
3020 *
3021 */ˇ
3022 "});
3023 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3024 cx.assert_editor_state(indoc! {"
3025 /**
3026 *
3027 */
3028 ˇ
3029 "});
3030
3031 // Ensure that inline comment followed by code
3032 // doesn't add comment prefix on newline
3033 cx.set_state(indoc! {"
3034 /** */ textˇ
3035 "});
3036 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3037 cx.assert_editor_state(indoc! {"
3038 /** */ text
3039 ˇ
3040 "});
3041
3042 // Ensure that text after comment end tag
3043 // doesn't add comment prefix on newline
3044 cx.set_state(indoc! {"
3045 /**
3046 *
3047 */ˇtext
3048 "});
3049 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3050 cx.assert_editor_state(indoc! {"
3051 /**
3052 *
3053 */
3054 ˇtext
3055 "});
3056
3057 // Ensure if not comment block it doesn't
3058 // add comment prefix on newline
3059 cx.set_state(indoc! {"
3060 * textˇ
3061 "});
3062 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3063 cx.assert_editor_state(indoc! {"
3064 * text
3065 ˇ
3066 "});
3067 }
3068 // Ensure that comment continuations can be disabled.
3069 update_test_language_settings(cx, |settings| {
3070 settings.defaults.extend_comment_on_newline = Some(false);
3071 });
3072 let mut cx = EditorTestContext::new(cx).await;
3073 cx.set_state(indoc! {"
3074 /**ˇ
3075 "});
3076 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3077 cx.assert_editor_state(indoc! {"
3078 /**
3079 ˇ
3080 "});
3081}
3082
3083#[gpui::test]
3084async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3085 init_test(cx, |settings| {
3086 settings.defaults.tab_size = NonZeroU32::new(4)
3087 });
3088
3089 let lua_language = Arc::new(Language::new(
3090 LanguageConfig {
3091 line_comments: vec!["--".into()],
3092 block_comment: Some(language::BlockCommentConfig {
3093 start: "--[[".into(),
3094 prefix: "".into(),
3095 end: "]]".into(),
3096 tab_size: 0,
3097 }),
3098 ..LanguageConfig::default()
3099 },
3100 None,
3101 ));
3102
3103 let mut cx = EditorTestContext::new(cx).await;
3104 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3105
3106 // Line with line comment should extend
3107 cx.set_state(indoc! {"
3108 --ˇ
3109 "});
3110 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3111 cx.assert_editor_state(indoc! {"
3112 --
3113 --ˇ
3114 "});
3115
3116 // Line with block comment that matches line comment should not extend
3117 cx.set_state(indoc! {"
3118 --[[ˇ
3119 "});
3120 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3121 cx.assert_editor_state(indoc! {"
3122 --[[
3123 ˇ
3124 "});
3125}
3126
3127#[gpui::test]
3128fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3129 init_test(cx, |_| {});
3130
3131 let editor = cx.add_window(|window, cx| {
3132 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3133 let mut editor = build_editor(buffer.clone(), window, cx);
3134 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3135 s.select_ranges([3..4, 11..12, 19..20])
3136 });
3137 editor
3138 });
3139
3140 _ = editor.update(cx, |editor, window, cx| {
3141 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3142 editor.buffer.update(cx, |buffer, cx| {
3143 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3144 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3145 });
3146 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3147
3148 editor.insert("Z", window, cx);
3149 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3150
3151 // The selections are moved after the inserted characters
3152 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3153 });
3154}
3155
3156#[gpui::test]
3157async fn test_tab(cx: &mut TestAppContext) {
3158 init_test(cx, |settings| {
3159 settings.defaults.tab_size = NonZeroU32::new(3)
3160 });
3161
3162 let mut cx = EditorTestContext::new(cx).await;
3163 cx.set_state(indoc! {"
3164 ˇabˇc
3165 ˇ🏀ˇ🏀ˇefg
3166 dˇ
3167 "});
3168 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3169 cx.assert_editor_state(indoc! {"
3170 ˇab ˇc
3171 ˇ🏀 ˇ🏀 ˇefg
3172 d ˇ
3173 "});
3174
3175 cx.set_state(indoc! {"
3176 a
3177 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3178 "});
3179 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3180 cx.assert_editor_state(indoc! {"
3181 a
3182 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3183 "});
3184}
3185
3186#[gpui::test]
3187async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3188 init_test(cx, |_| {});
3189
3190 let mut cx = EditorTestContext::new(cx).await;
3191 let language = Arc::new(
3192 Language::new(
3193 LanguageConfig::default(),
3194 Some(tree_sitter_rust::LANGUAGE.into()),
3195 )
3196 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3197 .unwrap(),
3198 );
3199 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3200
3201 // test when all cursors are not at suggested indent
3202 // then simply move to their suggested indent location
3203 cx.set_state(indoc! {"
3204 const a: B = (
3205 c(
3206 ˇ
3207 ˇ )
3208 );
3209 "});
3210 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3211 cx.assert_editor_state(indoc! {"
3212 const a: B = (
3213 c(
3214 ˇ
3215 ˇ)
3216 );
3217 "});
3218
3219 // test cursor already at suggested indent not moving when
3220 // other cursors are yet to reach their suggested indents
3221 cx.set_state(indoc! {"
3222 ˇ
3223 const a: B = (
3224 c(
3225 d(
3226 ˇ
3227 )
3228 ˇ
3229 ˇ )
3230 );
3231 "});
3232 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3233 cx.assert_editor_state(indoc! {"
3234 ˇ
3235 const a: B = (
3236 c(
3237 d(
3238 ˇ
3239 )
3240 ˇ
3241 ˇ)
3242 );
3243 "});
3244 // test when all cursors are at suggested indent then tab is inserted
3245 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3246 cx.assert_editor_state(indoc! {"
3247 ˇ
3248 const a: B = (
3249 c(
3250 d(
3251 ˇ
3252 )
3253 ˇ
3254 ˇ)
3255 );
3256 "});
3257
3258 // test when current indent is less than suggested indent,
3259 // we adjust line to match suggested indent and move cursor to it
3260 //
3261 // when no other cursor is at word boundary, all of them should move
3262 cx.set_state(indoc! {"
3263 const a: B = (
3264 c(
3265 d(
3266 ˇ
3267 ˇ )
3268 ˇ )
3269 );
3270 "});
3271 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3272 cx.assert_editor_state(indoc! {"
3273 const a: B = (
3274 c(
3275 d(
3276 ˇ
3277 ˇ)
3278 ˇ)
3279 );
3280 "});
3281
3282 // test when current indent is less than suggested indent,
3283 // we adjust line to match suggested indent and move cursor to it
3284 //
3285 // when some other cursor is at word boundary, it should not move
3286 cx.set_state(indoc! {"
3287 const a: B = (
3288 c(
3289 d(
3290 ˇ
3291 ˇ )
3292 ˇ)
3293 );
3294 "});
3295 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3296 cx.assert_editor_state(indoc! {"
3297 const a: B = (
3298 c(
3299 d(
3300 ˇ
3301 ˇ)
3302 ˇ)
3303 );
3304 "});
3305
3306 // test when current indent is more than suggested indent,
3307 // we just move cursor to current indent instead of suggested indent
3308 //
3309 // when no other cursor is at word boundary, all of them should move
3310 cx.set_state(indoc! {"
3311 const a: B = (
3312 c(
3313 d(
3314 ˇ
3315 ˇ )
3316 ˇ )
3317 );
3318 "});
3319 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3320 cx.assert_editor_state(indoc! {"
3321 const a: B = (
3322 c(
3323 d(
3324 ˇ
3325 ˇ)
3326 ˇ)
3327 );
3328 "});
3329 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3330 cx.assert_editor_state(indoc! {"
3331 const a: B = (
3332 c(
3333 d(
3334 ˇ
3335 ˇ)
3336 ˇ)
3337 );
3338 "});
3339
3340 // test when current indent is more than suggested indent,
3341 // we just move cursor to current indent instead of suggested indent
3342 //
3343 // when some other cursor is at word boundary, it doesn't move
3344 cx.set_state(indoc! {"
3345 const a: B = (
3346 c(
3347 d(
3348 ˇ
3349 ˇ )
3350 ˇ)
3351 );
3352 "});
3353 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3354 cx.assert_editor_state(indoc! {"
3355 const a: B = (
3356 c(
3357 d(
3358 ˇ
3359 ˇ)
3360 ˇ)
3361 );
3362 "});
3363
3364 // handle auto-indent when there are multiple cursors on the same line
3365 cx.set_state(indoc! {"
3366 const a: B = (
3367 c(
3368 ˇ ˇ
3369 ˇ )
3370 );
3371 "});
3372 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3373 cx.assert_editor_state(indoc! {"
3374 const a: B = (
3375 c(
3376 ˇ
3377 ˇ)
3378 );
3379 "});
3380}
3381
3382#[gpui::test]
3383async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3384 init_test(cx, |settings| {
3385 settings.defaults.tab_size = NonZeroU32::new(3)
3386 });
3387
3388 let mut cx = EditorTestContext::new(cx).await;
3389 cx.set_state(indoc! {"
3390 ˇ
3391 \t ˇ
3392 \t ˇ
3393 \t ˇ
3394 \t \t\t \t \t\t \t\t \t \t ˇ
3395 "});
3396
3397 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3398 cx.assert_editor_state(indoc! {"
3399 ˇ
3400 \t ˇ
3401 \t ˇ
3402 \t ˇ
3403 \t \t\t \t \t\t \t\t \t \t ˇ
3404 "});
3405}
3406
3407#[gpui::test]
3408async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3409 init_test(cx, |settings| {
3410 settings.defaults.tab_size = NonZeroU32::new(4)
3411 });
3412
3413 let language = Arc::new(
3414 Language::new(
3415 LanguageConfig::default(),
3416 Some(tree_sitter_rust::LANGUAGE.into()),
3417 )
3418 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3419 .unwrap(),
3420 );
3421
3422 let mut cx = EditorTestContext::new(cx).await;
3423 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3424 cx.set_state(indoc! {"
3425 fn a() {
3426 if b {
3427 \t ˇc
3428 }
3429 }
3430 "});
3431
3432 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3433 cx.assert_editor_state(indoc! {"
3434 fn a() {
3435 if b {
3436 ˇc
3437 }
3438 }
3439 "});
3440}
3441
3442#[gpui::test]
3443async fn test_indent_outdent(cx: &mut TestAppContext) {
3444 init_test(cx, |settings| {
3445 settings.defaults.tab_size = NonZeroU32::new(4);
3446 });
3447
3448 let mut cx = EditorTestContext::new(cx).await;
3449
3450 cx.set_state(indoc! {"
3451 «oneˇ» «twoˇ»
3452 three
3453 four
3454 "});
3455 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3456 cx.assert_editor_state(indoc! {"
3457 «oneˇ» «twoˇ»
3458 three
3459 four
3460 "});
3461
3462 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3463 cx.assert_editor_state(indoc! {"
3464 «oneˇ» «twoˇ»
3465 three
3466 four
3467 "});
3468
3469 // select across line ending
3470 cx.set_state(indoc! {"
3471 one two
3472 t«hree
3473 ˇ» four
3474 "});
3475 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3476 cx.assert_editor_state(indoc! {"
3477 one two
3478 t«hree
3479 ˇ» four
3480 "});
3481
3482 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3483 cx.assert_editor_state(indoc! {"
3484 one two
3485 t«hree
3486 ˇ» four
3487 "});
3488
3489 // Ensure that indenting/outdenting works when the cursor is at column 0.
3490 cx.set_state(indoc! {"
3491 one two
3492 ˇthree
3493 four
3494 "});
3495 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3496 cx.assert_editor_state(indoc! {"
3497 one two
3498 ˇthree
3499 four
3500 "});
3501
3502 cx.set_state(indoc! {"
3503 one two
3504 ˇ three
3505 four
3506 "});
3507 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3508 cx.assert_editor_state(indoc! {"
3509 one two
3510 ˇthree
3511 four
3512 "});
3513}
3514
3515#[gpui::test]
3516async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3517 // This is a regression test for issue #33761
3518 init_test(cx, |_| {});
3519
3520 let mut cx = EditorTestContext::new(cx).await;
3521 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3522 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3523
3524 cx.set_state(
3525 r#"ˇ# ingress:
3526ˇ# api:
3527ˇ# enabled: false
3528ˇ# pathType: Prefix
3529ˇ# console:
3530ˇ# enabled: false
3531ˇ# pathType: Prefix
3532"#,
3533 );
3534
3535 // Press tab to indent all lines
3536 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3537
3538 cx.assert_editor_state(
3539 r#" ˇ# ingress:
3540 ˇ# api:
3541 ˇ# enabled: false
3542 ˇ# pathType: Prefix
3543 ˇ# console:
3544 ˇ# enabled: false
3545 ˇ# pathType: Prefix
3546"#,
3547 );
3548}
3549
3550#[gpui::test]
3551async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3552 // This is a test to make sure our fix for issue #33761 didn't break anything
3553 init_test(cx, |_| {});
3554
3555 let mut cx = EditorTestContext::new(cx).await;
3556 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3557 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3558
3559 cx.set_state(
3560 r#"ˇingress:
3561ˇ api:
3562ˇ enabled: false
3563ˇ pathType: Prefix
3564"#,
3565 );
3566
3567 // Press tab to indent all lines
3568 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3569
3570 cx.assert_editor_state(
3571 r#"ˇingress:
3572 ˇapi:
3573 ˇenabled: false
3574 ˇpathType: Prefix
3575"#,
3576 );
3577}
3578
3579#[gpui::test]
3580async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3581 init_test(cx, |settings| {
3582 settings.defaults.hard_tabs = Some(true);
3583 });
3584
3585 let mut cx = EditorTestContext::new(cx).await;
3586
3587 // select two ranges on one line
3588 cx.set_state(indoc! {"
3589 «oneˇ» «twoˇ»
3590 three
3591 four
3592 "});
3593 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3594 cx.assert_editor_state(indoc! {"
3595 \t«oneˇ» «twoˇ»
3596 three
3597 four
3598 "});
3599 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3600 cx.assert_editor_state(indoc! {"
3601 \t\t«oneˇ» «twoˇ»
3602 three
3603 four
3604 "});
3605 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3606 cx.assert_editor_state(indoc! {"
3607 \t«oneˇ» «twoˇ»
3608 three
3609 four
3610 "});
3611 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3612 cx.assert_editor_state(indoc! {"
3613 «oneˇ» «twoˇ»
3614 three
3615 four
3616 "});
3617
3618 // select across a line ending
3619 cx.set_state(indoc! {"
3620 one two
3621 t«hree
3622 ˇ»four
3623 "});
3624 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3625 cx.assert_editor_state(indoc! {"
3626 one two
3627 \tt«hree
3628 ˇ»four
3629 "});
3630 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3631 cx.assert_editor_state(indoc! {"
3632 one two
3633 \t\tt«hree
3634 ˇ»four
3635 "});
3636 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3637 cx.assert_editor_state(indoc! {"
3638 one two
3639 \tt«hree
3640 ˇ»four
3641 "});
3642 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3643 cx.assert_editor_state(indoc! {"
3644 one two
3645 t«hree
3646 ˇ»four
3647 "});
3648
3649 // Ensure that indenting/outdenting works when the cursor is at column 0.
3650 cx.set_state(indoc! {"
3651 one two
3652 ˇthree
3653 four
3654 "});
3655 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3656 cx.assert_editor_state(indoc! {"
3657 one two
3658 ˇthree
3659 four
3660 "});
3661 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3662 cx.assert_editor_state(indoc! {"
3663 one two
3664 \tˇthree
3665 four
3666 "});
3667 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3668 cx.assert_editor_state(indoc! {"
3669 one two
3670 ˇthree
3671 four
3672 "});
3673}
3674
3675#[gpui::test]
3676fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3677 init_test(cx, |settings| {
3678 settings.languages.0.extend([
3679 (
3680 "TOML".into(),
3681 LanguageSettingsContent {
3682 tab_size: NonZeroU32::new(2),
3683 ..Default::default()
3684 },
3685 ),
3686 (
3687 "Rust".into(),
3688 LanguageSettingsContent {
3689 tab_size: NonZeroU32::new(4),
3690 ..Default::default()
3691 },
3692 ),
3693 ]);
3694 });
3695
3696 let toml_language = Arc::new(Language::new(
3697 LanguageConfig {
3698 name: "TOML".into(),
3699 ..Default::default()
3700 },
3701 None,
3702 ));
3703 let rust_language = Arc::new(Language::new(
3704 LanguageConfig {
3705 name: "Rust".into(),
3706 ..Default::default()
3707 },
3708 None,
3709 ));
3710
3711 let toml_buffer =
3712 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3713 let rust_buffer =
3714 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3715 let multibuffer = cx.new(|cx| {
3716 let mut multibuffer = MultiBuffer::new(ReadWrite);
3717 multibuffer.push_excerpts(
3718 toml_buffer.clone(),
3719 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3720 cx,
3721 );
3722 multibuffer.push_excerpts(
3723 rust_buffer.clone(),
3724 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3725 cx,
3726 );
3727 multibuffer
3728 });
3729
3730 cx.add_window(|window, cx| {
3731 let mut editor = build_editor(multibuffer, window, cx);
3732
3733 assert_eq!(
3734 editor.text(cx),
3735 indoc! {"
3736 a = 1
3737 b = 2
3738
3739 const c: usize = 3;
3740 "}
3741 );
3742
3743 select_ranges(
3744 &mut editor,
3745 indoc! {"
3746 «aˇ» = 1
3747 b = 2
3748
3749 «const c:ˇ» usize = 3;
3750 "},
3751 window,
3752 cx,
3753 );
3754
3755 editor.tab(&Tab, window, cx);
3756 assert_text_with_selections(
3757 &mut editor,
3758 indoc! {"
3759 «aˇ» = 1
3760 b = 2
3761
3762 «const c:ˇ» usize = 3;
3763 "},
3764 cx,
3765 );
3766 editor.backtab(&Backtab, window, cx);
3767 assert_text_with_selections(
3768 &mut editor,
3769 indoc! {"
3770 «aˇ» = 1
3771 b = 2
3772
3773 «const c:ˇ» usize = 3;
3774 "},
3775 cx,
3776 );
3777
3778 editor
3779 });
3780}
3781
3782#[gpui::test]
3783async fn test_backspace(cx: &mut TestAppContext) {
3784 init_test(cx, |_| {});
3785
3786 let mut cx = EditorTestContext::new(cx).await;
3787
3788 // Basic backspace
3789 cx.set_state(indoc! {"
3790 onˇe two three
3791 fou«rˇ» five six
3792 seven «ˇeight nine
3793 »ten
3794 "});
3795 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3796 cx.assert_editor_state(indoc! {"
3797 oˇe two three
3798 fouˇ five six
3799 seven ˇten
3800 "});
3801
3802 // Test backspace inside and around indents
3803 cx.set_state(indoc! {"
3804 zero
3805 ˇone
3806 ˇtwo
3807 ˇ ˇ ˇ three
3808 ˇ ˇ four
3809 "});
3810 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3811 cx.assert_editor_state(indoc! {"
3812 zero
3813 ˇone
3814 ˇtwo
3815 ˇ threeˇ four
3816 "});
3817}
3818
3819#[gpui::test]
3820async fn test_delete(cx: &mut TestAppContext) {
3821 init_test(cx, |_| {});
3822
3823 let mut cx = EditorTestContext::new(cx).await;
3824 cx.set_state(indoc! {"
3825 onˇe two three
3826 fou«rˇ» five six
3827 seven «ˇeight nine
3828 »ten
3829 "});
3830 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3831 cx.assert_editor_state(indoc! {"
3832 onˇ two three
3833 fouˇ five six
3834 seven ˇten
3835 "});
3836}
3837
3838#[gpui::test]
3839fn test_delete_line(cx: &mut TestAppContext) {
3840 init_test(cx, |_| {});
3841
3842 let editor = cx.add_window(|window, cx| {
3843 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3844 build_editor(buffer, window, cx)
3845 });
3846 _ = editor.update(cx, |editor, window, cx| {
3847 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3848 s.select_display_ranges([
3849 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3850 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3851 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3852 ])
3853 });
3854 editor.delete_line(&DeleteLine, window, cx);
3855 assert_eq!(editor.display_text(cx), "ghi");
3856 assert_eq!(
3857 editor.selections.display_ranges(cx),
3858 vec![
3859 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3860 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3861 ]
3862 );
3863 });
3864
3865 let editor = cx.add_window(|window, cx| {
3866 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3867 build_editor(buffer, window, cx)
3868 });
3869 _ = editor.update(cx, |editor, window, cx| {
3870 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3871 s.select_display_ranges([
3872 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3873 ])
3874 });
3875 editor.delete_line(&DeleteLine, window, cx);
3876 assert_eq!(editor.display_text(cx), "ghi\n");
3877 assert_eq!(
3878 editor.selections.display_ranges(cx),
3879 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3880 );
3881 });
3882}
3883
3884#[gpui::test]
3885fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3886 init_test(cx, |_| {});
3887
3888 cx.add_window(|window, cx| {
3889 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3890 let mut editor = build_editor(buffer.clone(), window, cx);
3891 let buffer = buffer.read(cx).as_singleton().unwrap();
3892
3893 assert_eq!(
3894 editor.selections.ranges::<Point>(cx),
3895 &[Point::new(0, 0)..Point::new(0, 0)]
3896 );
3897
3898 // When on single line, replace newline at end by space
3899 editor.join_lines(&JoinLines, window, cx);
3900 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3901 assert_eq!(
3902 editor.selections.ranges::<Point>(cx),
3903 &[Point::new(0, 3)..Point::new(0, 3)]
3904 );
3905
3906 // When multiple lines are selected, remove newlines that are spanned by the selection
3907 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3908 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3909 });
3910 editor.join_lines(&JoinLines, window, cx);
3911 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3912 assert_eq!(
3913 editor.selections.ranges::<Point>(cx),
3914 &[Point::new(0, 11)..Point::new(0, 11)]
3915 );
3916
3917 // Undo should be transactional
3918 editor.undo(&Undo, window, cx);
3919 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3920 assert_eq!(
3921 editor.selections.ranges::<Point>(cx),
3922 &[Point::new(0, 5)..Point::new(2, 2)]
3923 );
3924
3925 // When joining an empty line don't insert a space
3926 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3927 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3928 });
3929 editor.join_lines(&JoinLines, window, cx);
3930 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3931 assert_eq!(
3932 editor.selections.ranges::<Point>(cx),
3933 [Point::new(2, 3)..Point::new(2, 3)]
3934 );
3935
3936 // We can remove trailing newlines
3937 editor.join_lines(&JoinLines, window, cx);
3938 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3939 assert_eq!(
3940 editor.selections.ranges::<Point>(cx),
3941 [Point::new(2, 3)..Point::new(2, 3)]
3942 );
3943
3944 // We don't blow up on the last line
3945 editor.join_lines(&JoinLines, window, cx);
3946 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3947 assert_eq!(
3948 editor.selections.ranges::<Point>(cx),
3949 [Point::new(2, 3)..Point::new(2, 3)]
3950 );
3951
3952 // reset to test indentation
3953 editor.buffer.update(cx, |buffer, cx| {
3954 buffer.edit(
3955 [
3956 (Point::new(1, 0)..Point::new(1, 2), " "),
3957 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3958 ],
3959 None,
3960 cx,
3961 )
3962 });
3963
3964 // We remove any leading spaces
3965 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3966 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3967 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3968 });
3969 editor.join_lines(&JoinLines, window, cx);
3970 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3971
3972 // We don't insert a space for a line containing only spaces
3973 editor.join_lines(&JoinLines, window, cx);
3974 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3975
3976 // We ignore any leading tabs
3977 editor.join_lines(&JoinLines, window, cx);
3978 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3979
3980 editor
3981 });
3982}
3983
3984#[gpui::test]
3985fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3986 init_test(cx, |_| {});
3987
3988 cx.add_window(|window, cx| {
3989 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3990 let mut editor = build_editor(buffer.clone(), window, cx);
3991 let buffer = buffer.read(cx).as_singleton().unwrap();
3992
3993 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3994 s.select_ranges([
3995 Point::new(0, 2)..Point::new(1, 1),
3996 Point::new(1, 2)..Point::new(1, 2),
3997 Point::new(3, 1)..Point::new(3, 2),
3998 ])
3999 });
4000
4001 editor.join_lines(&JoinLines, window, cx);
4002 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4003
4004 assert_eq!(
4005 editor.selections.ranges::<Point>(cx),
4006 [
4007 Point::new(0, 7)..Point::new(0, 7),
4008 Point::new(1, 3)..Point::new(1, 3)
4009 ]
4010 );
4011 editor
4012 });
4013}
4014
4015#[gpui::test]
4016async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4017 init_test(cx, |_| {});
4018
4019 let mut cx = EditorTestContext::new(cx).await;
4020
4021 let diff_base = r#"
4022 Line 0
4023 Line 1
4024 Line 2
4025 Line 3
4026 "#
4027 .unindent();
4028
4029 cx.set_state(
4030 &r#"
4031 ˇLine 0
4032 Line 1
4033 Line 2
4034 Line 3
4035 "#
4036 .unindent(),
4037 );
4038
4039 cx.set_head_text(&diff_base);
4040 executor.run_until_parked();
4041
4042 // Join lines
4043 cx.update_editor(|editor, window, cx| {
4044 editor.join_lines(&JoinLines, window, cx);
4045 });
4046 executor.run_until_parked();
4047
4048 cx.assert_editor_state(
4049 &r#"
4050 Line 0ˇ Line 1
4051 Line 2
4052 Line 3
4053 "#
4054 .unindent(),
4055 );
4056 // Join again
4057 cx.update_editor(|editor, window, cx| {
4058 editor.join_lines(&JoinLines, window, cx);
4059 });
4060 executor.run_until_parked();
4061
4062 cx.assert_editor_state(
4063 &r#"
4064 Line 0 Line 1ˇ Line 2
4065 Line 3
4066 "#
4067 .unindent(),
4068 );
4069}
4070
4071#[gpui::test]
4072async fn test_custom_newlines_cause_no_false_positive_diffs(
4073 executor: BackgroundExecutor,
4074 cx: &mut TestAppContext,
4075) {
4076 init_test(cx, |_| {});
4077 let mut cx = EditorTestContext::new(cx).await;
4078 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4079 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4080 executor.run_until_parked();
4081
4082 cx.update_editor(|editor, window, cx| {
4083 let snapshot = editor.snapshot(window, cx);
4084 assert_eq!(
4085 snapshot
4086 .buffer_snapshot
4087 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
4088 .collect::<Vec<_>>(),
4089 Vec::new(),
4090 "Should not have any diffs for files with custom newlines"
4091 );
4092 });
4093}
4094
4095#[gpui::test]
4096async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4097 init_test(cx, |_| {});
4098
4099 let mut cx = EditorTestContext::new(cx).await;
4100
4101 // Test sort_lines_case_insensitive()
4102 cx.set_state(indoc! {"
4103 «z
4104 y
4105 x
4106 Z
4107 Y
4108 Xˇ»
4109 "});
4110 cx.update_editor(|e, window, cx| {
4111 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4112 });
4113 cx.assert_editor_state(indoc! {"
4114 «x
4115 X
4116 y
4117 Y
4118 z
4119 Zˇ»
4120 "});
4121
4122 // Test sort_lines_by_length()
4123 //
4124 // Demonstrates:
4125 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4126 // - sort is stable
4127 cx.set_state(indoc! {"
4128 «123
4129 æ
4130 12
4131 ∞
4132 1
4133 æˇ»
4134 "});
4135 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4136 cx.assert_editor_state(indoc! {"
4137 «æ
4138 ∞
4139 1
4140 æ
4141 12
4142 123ˇ»
4143 "});
4144
4145 // Test reverse_lines()
4146 cx.set_state(indoc! {"
4147 «5
4148 4
4149 3
4150 2
4151 1ˇ»
4152 "});
4153 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4154 cx.assert_editor_state(indoc! {"
4155 «1
4156 2
4157 3
4158 4
4159 5ˇ»
4160 "});
4161
4162 // Skip testing shuffle_line()
4163
4164 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4165 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4166
4167 // Don't manipulate when cursor is on single line, but expand the selection
4168 cx.set_state(indoc! {"
4169 ddˇdd
4170 ccc
4171 bb
4172 a
4173 "});
4174 cx.update_editor(|e, window, cx| {
4175 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4176 });
4177 cx.assert_editor_state(indoc! {"
4178 «ddddˇ»
4179 ccc
4180 bb
4181 a
4182 "});
4183
4184 // Basic manipulate case
4185 // Start selection moves to column 0
4186 // End of selection shrinks to fit shorter line
4187 cx.set_state(indoc! {"
4188 dd«d
4189 ccc
4190 bb
4191 aaaaaˇ»
4192 "});
4193 cx.update_editor(|e, window, cx| {
4194 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4195 });
4196 cx.assert_editor_state(indoc! {"
4197 «aaaaa
4198 bb
4199 ccc
4200 dddˇ»
4201 "});
4202
4203 // Manipulate case with newlines
4204 cx.set_state(indoc! {"
4205 dd«d
4206 ccc
4207
4208 bb
4209 aaaaa
4210
4211 ˇ»
4212 "});
4213 cx.update_editor(|e, window, cx| {
4214 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4215 });
4216 cx.assert_editor_state(indoc! {"
4217 «
4218
4219 aaaaa
4220 bb
4221 ccc
4222 dddˇ»
4223
4224 "});
4225
4226 // Adding new line
4227 cx.set_state(indoc! {"
4228 aa«a
4229 bbˇ»b
4230 "});
4231 cx.update_editor(|e, window, cx| {
4232 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4233 });
4234 cx.assert_editor_state(indoc! {"
4235 «aaa
4236 bbb
4237 added_lineˇ»
4238 "});
4239
4240 // Removing line
4241 cx.set_state(indoc! {"
4242 aa«a
4243 bbbˇ»
4244 "});
4245 cx.update_editor(|e, window, cx| {
4246 e.manipulate_immutable_lines(window, cx, |lines| {
4247 lines.pop();
4248 })
4249 });
4250 cx.assert_editor_state(indoc! {"
4251 «aaaˇ»
4252 "});
4253
4254 // Removing all lines
4255 cx.set_state(indoc! {"
4256 aa«a
4257 bbbˇ»
4258 "});
4259 cx.update_editor(|e, window, cx| {
4260 e.manipulate_immutable_lines(window, cx, |lines| {
4261 lines.drain(..);
4262 })
4263 });
4264 cx.assert_editor_state(indoc! {"
4265 ˇ
4266 "});
4267}
4268
4269#[gpui::test]
4270async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4271 init_test(cx, |_| {});
4272
4273 let mut cx = EditorTestContext::new(cx).await;
4274
4275 // Consider continuous selection as single selection
4276 cx.set_state(indoc! {"
4277 Aaa«aa
4278 cˇ»c«c
4279 bb
4280 aaaˇ»aa
4281 "});
4282 cx.update_editor(|e, window, cx| {
4283 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4284 });
4285 cx.assert_editor_state(indoc! {"
4286 «Aaaaa
4287 ccc
4288 bb
4289 aaaaaˇ»
4290 "});
4291
4292 cx.set_state(indoc! {"
4293 Aaa«aa
4294 cˇ»c«c
4295 bb
4296 aaaˇ»aa
4297 "});
4298 cx.update_editor(|e, window, cx| {
4299 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4300 });
4301 cx.assert_editor_state(indoc! {"
4302 «Aaaaa
4303 ccc
4304 bbˇ»
4305 "});
4306
4307 // Consider non continuous selection as distinct dedup operations
4308 cx.set_state(indoc! {"
4309 «aaaaa
4310 bb
4311 aaaaa
4312 aaaaaˇ»
4313
4314 aaa«aaˇ»
4315 "});
4316 cx.update_editor(|e, window, cx| {
4317 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4318 });
4319 cx.assert_editor_state(indoc! {"
4320 «aaaaa
4321 bbˇ»
4322
4323 «aaaaaˇ»
4324 "});
4325}
4326
4327#[gpui::test]
4328async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4329 init_test(cx, |_| {});
4330
4331 let mut cx = EditorTestContext::new(cx).await;
4332
4333 cx.set_state(indoc! {"
4334 «Aaa
4335 aAa
4336 Aaaˇ»
4337 "});
4338 cx.update_editor(|e, window, cx| {
4339 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4340 });
4341 cx.assert_editor_state(indoc! {"
4342 «Aaa
4343 aAaˇ»
4344 "});
4345
4346 cx.set_state(indoc! {"
4347 «Aaa
4348 aAa
4349 aaAˇ»
4350 "});
4351 cx.update_editor(|e, window, cx| {
4352 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4353 });
4354 cx.assert_editor_state(indoc! {"
4355 «Aaaˇ»
4356 "});
4357}
4358
4359#[gpui::test]
4360async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4361 init_test(cx, |_| {});
4362
4363 let mut cx = EditorTestContext::new(cx).await;
4364
4365 // Manipulate with multiple selections on a single line
4366 cx.set_state(indoc! {"
4367 dd«dd
4368 cˇ»c«c
4369 bb
4370 aaaˇ»aa
4371 "});
4372 cx.update_editor(|e, window, cx| {
4373 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4374 });
4375 cx.assert_editor_state(indoc! {"
4376 «aaaaa
4377 bb
4378 ccc
4379 ddddˇ»
4380 "});
4381
4382 // Manipulate with multiple disjoin selections
4383 cx.set_state(indoc! {"
4384 5«
4385 4
4386 3
4387 2
4388 1ˇ»
4389
4390 dd«dd
4391 ccc
4392 bb
4393 aaaˇ»aa
4394 "});
4395 cx.update_editor(|e, window, cx| {
4396 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4397 });
4398 cx.assert_editor_state(indoc! {"
4399 «1
4400 2
4401 3
4402 4
4403 5ˇ»
4404
4405 «aaaaa
4406 bb
4407 ccc
4408 ddddˇ»
4409 "});
4410
4411 // Adding lines on each selection
4412 cx.set_state(indoc! {"
4413 2«
4414 1ˇ»
4415
4416 bb«bb
4417 aaaˇ»aa
4418 "});
4419 cx.update_editor(|e, window, cx| {
4420 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4421 });
4422 cx.assert_editor_state(indoc! {"
4423 «2
4424 1
4425 added lineˇ»
4426
4427 «bbbb
4428 aaaaa
4429 added lineˇ»
4430 "});
4431
4432 // Removing lines on each selection
4433 cx.set_state(indoc! {"
4434 2«
4435 1ˇ»
4436
4437 bb«bb
4438 aaaˇ»aa
4439 "});
4440 cx.update_editor(|e, window, cx| {
4441 e.manipulate_immutable_lines(window, cx, |lines| {
4442 lines.pop();
4443 })
4444 });
4445 cx.assert_editor_state(indoc! {"
4446 «2ˇ»
4447
4448 «bbbbˇ»
4449 "});
4450}
4451
4452#[gpui::test]
4453async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4454 init_test(cx, |settings| {
4455 settings.defaults.tab_size = NonZeroU32::new(3)
4456 });
4457
4458 let mut cx = EditorTestContext::new(cx).await;
4459
4460 // MULTI SELECTION
4461 // Ln.1 "«" tests empty lines
4462 // Ln.9 tests just leading whitespace
4463 cx.set_state(indoc! {"
4464 «
4465 abc // No indentationˇ»
4466 «\tabc // 1 tabˇ»
4467 \t\tabc « ˇ» // 2 tabs
4468 \t ab«c // Tab followed by space
4469 \tabc // Space followed by tab (3 spaces should be the result)
4470 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4471 abˇ»ˇc ˇ ˇ // Already space indented«
4472 \t
4473 \tabc\tdef // Only the leading tab is manipulatedˇ»
4474 "});
4475 cx.update_editor(|e, window, cx| {
4476 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4477 });
4478 cx.assert_editor_state(
4479 indoc! {"
4480 «
4481 abc // No indentation
4482 abc // 1 tab
4483 abc // 2 tabs
4484 abc // Tab followed by space
4485 abc // Space followed by tab (3 spaces should be the result)
4486 abc // Mixed indentation (tab conversion depends on the column)
4487 abc // Already space indented
4488 ·
4489 abc\tdef // Only the leading tab is manipulatedˇ»
4490 "}
4491 .replace("·", "")
4492 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4493 );
4494
4495 // Test on just a few lines, the others should remain unchanged
4496 // Only lines (3, 5, 10, 11) should change
4497 cx.set_state(
4498 indoc! {"
4499 ·
4500 abc // No indentation
4501 \tabcˇ // 1 tab
4502 \t\tabc // 2 tabs
4503 \t abcˇ // Tab followed by space
4504 \tabc // Space followed by tab (3 spaces should be the result)
4505 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4506 abc // Already space indented
4507 «\t
4508 \tabc\tdef // Only the leading tab is manipulatedˇ»
4509 "}
4510 .replace("·", "")
4511 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4512 );
4513 cx.update_editor(|e, window, cx| {
4514 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4515 });
4516 cx.assert_editor_state(
4517 indoc! {"
4518 ·
4519 abc // No indentation
4520 « abc // 1 tabˇ»
4521 \t\tabc // 2 tabs
4522 « abc // Tab followed by spaceˇ»
4523 \tabc // Space followed by tab (3 spaces should be the result)
4524 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4525 abc // Already space indented
4526 « ·
4527 abc\tdef // Only the leading tab is manipulatedˇ»
4528 "}
4529 .replace("·", "")
4530 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4531 );
4532
4533 // SINGLE SELECTION
4534 // Ln.1 "«" tests empty lines
4535 // Ln.9 tests just leading whitespace
4536 cx.set_state(indoc! {"
4537 «
4538 abc // No indentation
4539 \tabc // 1 tab
4540 \t\tabc // 2 tabs
4541 \t abc // Tab followed by space
4542 \tabc // Space followed by tab (3 spaces should be the result)
4543 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4544 abc // Already space indented
4545 \t
4546 \tabc\tdef // Only the leading tab is manipulatedˇ»
4547 "});
4548 cx.update_editor(|e, window, cx| {
4549 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4550 });
4551 cx.assert_editor_state(
4552 indoc! {"
4553 «
4554 abc // No indentation
4555 abc // 1 tab
4556 abc // 2 tabs
4557 abc // Tab followed by space
4558 abc // Space followed by tab (3 spaces should be the result)
4559 abc // Mixed indentation (tab conversion depends on the column)
4560 abc // Already space indented
4561 ·
4562 abc\tdef // Only the leading tab is manipulatedˇ»
4563 "}
4564 .replace("·", "")
4565 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4566 );
4567}
4568
4569#[gpui::test]
4570async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
4571 init_test(cx, |settings| {
4572 settings.defaults.tab_size = NonZeroU32::new(3)
4573 });
4574
4575 let mut cx = EditorTestContext::new(cx).await;
4576
4577 // MULTI SELECTION
4578 // Ln.1 "«" tests empty lines
4579 // Ln.11 tests just leading whitespace
4580 cx.set_state(indoc! {"
4581 «
4582 abˇ»ˇc // No indentation
4583 abc ˇ ˇ // 1 space (< 3 so dont convert)
4584 abc « // 2 spaces (< 3 so dont convert)
4585 abc // 3 spaces (convert)
4586 abc ˇ» // 5 spaces (1 tab + 2 spaces)
4587 «\tˇ»\t«\tˇ»abc // Already tab indented
4588 «\t abc // Tab followed by space
4589 \tabc // Space followed by tab (should be consumed due to tab)
4590 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4591 \tˇ» «\t
4592 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
4593 "});
4594 cx.update_editor(|e, window, cx| {
4595 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4596 });
4597 cx.assert_editor_state(indoc! {"
4598 «
4599 abc // No indentation
4600 abc // 1 space (< 3 so dont convert)
4601 abc // 2 spaces (< 3 so dont convert)
4602 \tabc // 3 spaces (convert)
4603 \t abc // 5 spaces (1 tab + 2 spaces)
4604 \t\t\tabc // Already tab indented
4605 \t abc // Tab followed by space
4606 \tabc // Space followed by tab (should be consumed due to tab)
4607 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4608 \t\t\t
4609 \tabc \t // Only the leading spaces should be convertedˇ»
4610 "});
4611
4612 // Test on just a few lines, the other should remain unchanged
4613 // Only lines (4, 8, 11, 12) should change
4614 cx.set_state(
4615 indoc! {"
4616 ·
4617 abc // No indentation
4618 abc // 1 space (< 3 so dont convert)
4619 abc // 2 spaces (< 3 so dont convert)
4620 « abc // 3 spaces (convert)ˇ»
4621 abc // 5 spaces (1 tab + 2 spaces)
4622 \t\t\tabc // Already tab indented
4623 \t abc // Tab followed by space
4624 \tabc ˇ // Space followed by tab (should be consumed due to tab)
4625 \t\t \tabc // Mixed indentation
4626 \t \t \t \tabc // Mixed indentation
4627 \t \tˇ
4628 « abc \t // Only the leading spaces should be convertedˇ»
4629 "}
4630 .replace("·", "")
4631 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4632 );
4633 cx.update_editor(|e, window, cx| {
4634 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4635 });
4636 cx.assert_editor_state(
4637 indoc! {"
4638 ·
4639 abc // No indentation
4640 abc // 1 space (< 3 so dont convert)
4641 abc // 2 spaces (< 3 so dont convert)
4642 «\tabc // 3 spaces (convert)ˇ»
4643 abc // 5 spaces (1 tab + 2 spaces)
4644 \t\t\tabc // Already tab indented
4645 \t abc // Tab followed by space
4646 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
4647 \t\t \tabc // Mixed indentation
4648 \t \t \t \tabc // Mixed indentation
4649 «\t\t\t
4650 \tabc \t // Only the leading spaces should be convertedˇ»
4651 "}
4652 .replace("·", "")
4653 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4654 );
4655
4656 // SINGLE SELECTION
4657 // Ln.1 "«" tests empty lines
4658 // Ln.11 tests just leading whitespace
4659 cx.set_state(indoc! {"
4660 «
4661 abc // No indentation
4662 abc // 1 space (< 3 so dont convert)
4663 abc // 2 spaces (< 3 so dont convert)
4664 abc // 3 spaces (convert)
4665 abc // 5 spaces (1 tab + 2 spaces)
4666 \t\t\tabc // Already tab indented
4667 \t abc // Tab followed by space
4668 \tabc // Space followed by tab (should be consumed due to tab)
4669 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4670 \t \t
4671 abc \t // Only the leading spaces should be convertedˇ»
4672 "});
4673 cx.update_editor(|e, window, cx| {
4674 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4675 });
4676 cx.assert_editor_state(indoc! {"
4677 «
4678 abc // No indentation
4679 abc // 1 space (< 3 so dont convert)
4680 abc // 2 spaces (< 3 so dont convert)
4681 \tabc // 3 spaces (convert)
4682 \t abc // 5 spaces (1 tab + 2 spaces)
4683 \t\t\tabc // Already tab indented
4684 \t abc // Tab followed by space
4685 \tabc // Space followed by tab (should be consumed due to tab)
4686 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4687 \t\t\t
4688 \tabc \t // Only the leading spaces should be convertedˇ»
4689 "});
4690}
4691
4692#[gpui::test]
4693async fn test_toggle_case(cx: &mut TestAppContext) {
4694 init_test(cx, |_| {});
4695
4696 let mut cx = EditorTestContext::new(cx).await;
4697
4698 // If all lower case -> upper case
4699 cx.set_state(indoc! {"
4700 «hello worldˇ»
4701 "});
4702 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4703 cx.assert_editor_state(indoc! {"
4704 «HELLO WORLDˇ»
4705 "});
4706
4707 // If all upper case -> lower case
4708 cx.set_state(indoc! {"
4709 «HELLO WORLDˇ»
4710 "});
4711 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4712 cx.assert_editor_state(indoc! {"
4713 «hello worldˇ»
4714 "});
4715
4716 // If any upper case characters are identified -> lower case
4717 // This matches JetBrains IDEs
4718 cx.set_state(indoc! {"
4719 «hEllo worldˇ»
4720 "});
4721 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4722 cx.assert_editor_state(indoc! {"
4723 «hello worldˇ»
4724 "});
4725}
4726
4727#[gpui::test]
4728async fn test_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_inline_completion_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(|_| FakeInlineCompletionProvider::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_inline_completion(Some(inline_completion::InlineCompletion {
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_inline_completion(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_fold_function_bodies(cx: &mut TestAppContext) {
7974 init_test(cx, |_| {});
7975
7976 let base_text = r#"
7977 impl A {
7978 // this is an uncommitted comment
7979
7980 fn b() {
7981 c();
7982 }
7983
7984 // this is another uncommitted comment
7985
7986 fn d() {
7987 // e
7988 // f
7989 }
7990 }
7991
7992 fn g() {
7993 // h
7994 }
7995 "#
7996 .unindent();
7997
7998 let text = r#"
7999 ˇimpl A {
8000
8001 fn b() {
8002 c();
8003 }
8004
8005 fn d() {
8006 // e
8007 // f
8008 }
8009 }
8010
8011 fn g() {
8012 // h
8013 }
8014 "#
8015 .unindent();
8016
8017 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8018 cx.set_state(&text);
8019 cx.set_head_text(&base_text);
8020 cx.update_editor(|editor, window, cx| {
8021 editor.expand_all_diff_hunks(&Default::default(), window, cx);
8022 });
8023
8024 cx.assert_state_with_diff(
8025 "
8026 ˇimpl A {
8027 - // this is an uncommitted comment
8028
8029 fn b() {
8030 c();
8031 }
8032
8033 - // this is another uncommitted comment
8034 -
8035 fn d() {
8036 // e
8037 // f
8038 }
8039 }
8040
8041 fn g() {
8042 // h
8043 }
8044 "
8045 .unindent(),
8046 );
8047
8048 let expected_display_text = "
8049 impl A {
8050 // this is an uncommitted comment
8051
8052 fn b() {
8053 ⋯
8054 }
8055
8056 // this is another uncommitted comment
8057
8058 fn d() {
8059 ⋯
8060 }
8061 }
8062
8063 fn g() {
8064 ⋯
8065 }
8066 "
8067 .unindent();
8068
8069 cx.update_editor(|editor, window, cx| {
8070 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
8071 assert_eq!(editor.display_text(cx), expected_display_text);
8072 });
8073}
8074
8075#[gpui::test]
8076async fn test_autoindent(cx: &mut TestAppContext) {
8077 init_test(cx, |_| {});
8078
8079 let language = Arc::new(
8080 Language::new(
8081 LanguageConfig {
8082 brackets: BracketPairConfig {
8083 pairs: vec![
8084 BracketPair {
8085 start: "{".to_string(),
8086 end: "}".to_string(),
8087 close: false,
8088 surround: false,
8089 newline: true,
8090 },
8091 BracketPair {
8092 start: "(".to_string(),
8093 end: ")".to_string(),
8094 close: false,
8095 surround: false,
8096 newline: true,
8097 },
8098 ],
8099 ..Default::default()
8100 },
8101 ..Default::default()
8102 },
8103 Some(tree_sitter_rust::LANGUAGE.into()),
8104 )
8105 .with_indents_query(
8106 r#"
8107 (_ "(" ")" @end) @indent
8108 (_ "{" "}" @end) @indent
8109 "#,
8110 )
8111 .unwrap(),
8112 );
8113
8114 let text = "fn a() {}";
8115
8116 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8117 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8118 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8119 editor
8120 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8121 .await;
8122
8123 editor.update_in(cx, |editor, window, cx| {
8124 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8125 s.select_ranges([5..5, 8..8, 9..9])
8126 });
8127 editor.newline(&Newline, window, cx);
8128 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
8129 assert_eq!(
8130 editor.selections.ranges(cx),
8131 &[
8132 Point::new(1, 4)..Point::new(1, 4),
8133 Point::new(3, 4)..Point::new(3, 4),
8134 Point::new(5, 0)..Point::new(5, 0)
8135 ]
8136 );
8137 });
8138}
8139
8140#[gpui::test]
8141async fn test_autoindent_selections(cx: &mut TestAppContext) {
8142 init_test(cx, |_| {});
8143
8144 {
8145 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8146 cx.set_state(indoc! {"
8147 impl A {
8148
8149 fn b() {}
8150
8151 «fn c() {
8152
8153 }ˇ»
8154 }
8155 "});
8156
8157 cx.update_editor(|editor, window, cx| {
8158 editor.autoindent(&Default::default(), window, cx);
8159 });
8160
8161 cx.assert_editor_state(indoc! {"
8162 impl A {
8163
8164 fn b() {}
8165
8166 «fn c() {
8167
8168 }ˇ»
8169 }
8170 "});
8171 }
8172
8173 {
8174 let mut cx = EditorTestContext::new_multibuffer(
8175 cx,
8176 [indoc! { "
8177 impl A {
8178 «
8179 // a
8180 fn b(){}
8181 »
8182 «
8183 }
8184 fn c(){}
8185 »
8186 "}],
8187 );
8188
8189 let buffer = cx.update_editor(|editor, _, cx| {
8190 let buffer = editor.buffer().update(cx, |buffer, _| {
8191 buffer.all_buffers().iter().next().unwrap().clone()
8192 });
8193 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
8194 buffer
8195 });
8196
8197 cx.run_until_parked();
8198 cx.update_editor(|editor, window, cx| {
8199 editor.select_all(&Default::default(), window, cx);
8200 editor.autoindent(&Default::default(), window, cx)
8201 });
8202 cx.run_until_parked();
8203
8204 cx.update(|_, cx| {
8205 assert_eq!(
8206 buffer.read(cx).text(),
8207 indoc! { "
8208 impl A {
8209
8210 // a
8211 fn b(){}
8212
8213
8214 }
8215 fn c(){}
8216
8217 " }
8218 )
8219 });
8220 }
8221}
8222
8223#[gpui::test]
8224async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
8225 init_test(cx, |_| {});
8226
8227 let mut cx = EditorTestContext::new(cx).await;
8228
8229 let language = Arc::new(Language::new(
8230 LanguageConfig {
8231 brackets: BracketPairConfig {
8232 pairs: vec![
8233 BracketPair {
8234 start: "{".to_string(),
8235 end: "}".to_string(),
8236 close: true,
8237 surround: true,
8238 newline: true,
8239 },
8240 BracketPair {
8241 start: "(".to_string(),
8242 end: ")".to_string(),
8243 close: true,
8244 surround: true,
8245 newline: true,
8246 },
8247 BracketPair {
8248 start: "/*".to_string(),
8249 end: " */".to_string(),
8250 close: true,
8251 surround: true,
8252 newline: true,
8253 },
8254 BracketPair {
8255 start: "[".to_string(),
8256 end: "]".to_string(),
8257 close: false,
8258 surround: false,
8259 newline: true,
8260 },
8261 BracketPair {
8262 start: "\"".to_string(),
8263 end: "\"".to_string(),
8264 close: true,
8265 surround: true,
8266 newline: false,
8267 },
8268 BracketPair {
8269 start: "<".to_string(),
8270 end: ">".to_string(),
8271 close: false,
8272 surround: true,
8273 newline: true,
8274 },
8275 ],
8276 ..Default::default()
8277 },
8278 autoclose_before: "})]".to_string(),
8279 ..Default::default()
8280 },
8281 Some(tree_sitter_rust::LANGUAGE.into()),
8282 ));
8283
8284 cx.language_registry().add(language.clone());
8285 cx.update_buffer(|buffer, cx| {
8286 buffer.set_language(Some(language), cx);
8287 });
8288
8289 cx.set_state(
8290 &r#"
8291 🏀ˇ
8292 εˇ
8293 ❤️ˇ
8294 "#
8295 .unindent(),
8296 );
8297
8298 // autoclose multiple nested brackets at multiple cursors
8299 cx.update_editor(|editor, window, cx| {
8300 editor.handle_input("{", window, cx);
8301 editor.handle_input("{", window, cx);
8302 editor.handle_input("{", window, cx);
8303 });
8304 cx.assert_editor_state(
8305 &"
8306 🏀{{{ˇ}}}
8307 ε{{{ˇ}}}
8308 ❤️{{{ˇ}}}
8309 "
8310 .unindent(),
8311 );
8312
8313 // insert a different closing bracket
8314 cx.update_editor(|editor, window, cx| {
8315 editor.handle_input(")", window, cx);
8316 });
8317 cx.assert_editor_state(
8318 &"
8319 🏀{{{)ˇ}}}
8320 ε{{{)ˇ}}}
8321 ❤️{{{)ˇ}}}
8322 "
8323 .unindent(),
8324 );
8325
8326 // skip over the auto-closed brackets when typing a closing bracket
8327 cx.update_editor(|editor, window, cx| {
8328 editor.move_right(&MoveRight, window, cx);
8329 editor.handle_input("}", window, cx);
8330 editor.handle_input("}", window, cx);
8331 editor.handle_input("}", window, cx);
8332 });
8333 cx.assert_editor_state(
8334 &"
8335 🏀{{{)}}}}ˇ
8336 ε{{{)}}}}ˇ
8337 ❤️{{{)}}}}ˇ
8338 "
8339 .unindent(),
8340 );
8341
8342 // autoclose multi-character pairs
8343 cx.set_state(
8344 &"
8345 ˇ
8346 ˇ
8347 "
8348 .unindent(),
8349 );
8350 cx.update_editor(|editor, window, cx| {
8351 editor.handle_input("/", window, cx);
8352 editor.handle_input("*", window, cx);
8353 });
8354 cx.assert_editor_state(
8355 &"
8356 /*ˇ */
8357 /*ˇ */
8358 "
8359 .unindent(),
8360 );
8361
8362 // one cursor autocloses a multi-character pair, one cursor
8363 // does not autoclose.
8364 cx.set_state(
8365 &"
8366 /ˇ
8367 ˇ
8368 "
8369 .unindent(),
8370 );
8371 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
8372 cx.assert_editor_state(
8373 &"
8374 /*ˇ */
8375 *ˇ
8376 "
8377 .unindent(),
8378 );
8379
8380 // Don't autoclose if the next character isn't whitespace and isn't
8381 // listed in the language's "autoclose_before" section.
8382 cx.set_state("ˇa b");
8383 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8384 cx.assert_editor_state("{ˇa b");
8385
8386 // Don't autoclose if `close` is false for the bracket pair
8387 cx.set_state("ˇ");
8388 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
8389 cx.assert_editor_state("[ˇ");
8390
8391 // Surround with brackets if text is selected
8392 cx.set_state("«aˇ» b");
8393 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8394 cx.assert_editor_state("{«aˇ»} b");
8395
8396 // Autoclose when not immediately after a word character
8397 cx.set_state("a ˇ");
8398 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8399 cx.assert_editor_state("a \"ˇ\"");
8400
8401 // Autoclose pair where the start and end characters are the same
8402 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8403 cx.assert_editor_state("a \"\"ˇ");
8404
8405 // Don't autoclose when immediately after a word character
8406 cx.set_state("aˇ");
8407 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8408 cx.assert_editor_state("a\"ˇ");
8409
8410 // Do autoclose when after a non-word character
8411 cx.set_state("{ˇ");
8412 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8413 cx.assert_editor_state("{\"ˇ\"");
8414
8415 // Non identical pairs autoclose regardless of preceding character
8416 cx.set_state("aˇ");
8417 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8418 cx.assert_editor_state("a{ˇ}");
8419
8420 // Don't autoclose pair if autoclose is disabled
8421 cx.set_state("ˇ");
8422 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8423 cx.assert_editor_state("<ˇ");
8424
8425 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
8426 cx.set_state("«aˇ» b");
8427 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8428 cx.assert_editor_state("<«aˇ»> b");
8429}
8430
8431#[gpui::test]
8432async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
8433 init_test(cx, |settings| {
8434 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8435 });
8436
8437 let mut cx = EditorTestContext::new(cx).await;
8438
8439 let language = Arc::new(Language::new(
8440 LanguageConfig {
8441 brackets: BracketPairConfig {
8442 pairs: vec![
8443 BracketPair {
8444 start: "{".to_string(),
8445 end: "}".to_string(),
8446 close: true,
8447 surround: true,
8448 newline: true,
8449 },
8450 BracketPair {
8451 start: "(".to_string(),
8452 end: ")".to_string(),
8453 close: true,
8454 surround: true,
8455 newline: true,
8456 },
8457 BracketPair {
8458 start: "[".to_string(),
8459 end: "]".to_string(),
8460 close: false,
8461 surround: false,
8462 newline: true,
8463 },
8464 ],
8465 ..Default::default()
8466 },
8467 autoclose_before: "})]".to_string(),
8468 ..Default::default()
8469 },
8470 Some(tree_sitter_rust::LANGUAGE.into()),
8471 ));
8472
8473 cx.language_registry().add(language.clone());
8474 cx.update_buffer(|buffer, cx| {
8475 buffer.set_language(Some(language), cx);
8476 });
8477
8478 cx.set_state(
8479 &"
8480 ˇ
8481 ˇ
8482 ˇ
8483 "
8484 .unindent(),
8485 );
8486
8487 // ensure only matching closing brackets are skipped over
8488 cx.update_editor(|editor, window, cx| {
8489 editor.handle_input("}", window, cx);
8490 editor.move_left(&MoveLeft, window, cx);
8491 editor.handle_input(")", window, cx);
8492 editor.move_left(&MoveLeft, window, cx);
8493 });
8494 cx.assert_editor_state(
8495 &"
8496 ˇ)}
8497 ˇ)}
8498 ˇ)}
8499 "
8500 .unindent(),
8501 );
8502
8503 // skip-over closing brackets at multiple cursors
8504 cx.update_editor(|editor, window, cx| {
8505 editor.handle_input(")", window, cx);
8506 editor.handle_input("}", window, cx);
8507 });
8508 cx.assert_editor_state(
8509 &"
8510 )}ˇ
8511 )}ˇ
8512 )}ˇ
8513 "
8514 .unindent(),
8515 );
8516
8517 // ignore non-close brackets
8518 cx.update_editor(|editor, window, cx| {
8519 editor.handle_input("]", window, cx);
8520 editor.move_left(&MoveLeft, window, cx);
8521 editor.handle_input("]", window, cx);
8522 });
8523 cx.assert_editor_state(
8524 &"
8525 )}]ˇ]
8526 )}]ˇ]
8527 )}]ˇ]
8528 "
8529 .unindent(),
8530 );
8531}
8532
8533#[gpui::test]
8534async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8535 init_test(cx, |_| {});
8536
8537 let mut cx = EditorTestContext::new(cx).await;
8538
8539 let html_language = Arc::new(
8540 Language::new(
8541 LanguageConfig {
8542 name: "HTML".into(),
8543 brackets: BracketPairConfig {
8544 pairs: vec![
8545 BracketPair {
8546 start: "<".into(),
8547 end: ">".into(),
8548 close: true,
8549 ..Default::default()
8550 },
8551 BracketPair {
8552 start: "{".into(),
8553 end: "}".into(),
8554 close: true,
8555 ..Default::default()
8556 },
8557 BracketPair {
8558 start: "(".into(),
8559 end: ")".into(),
8560 close: true,
8561 ..Default::default()
8562 },
8563 ],
8564 ..Default::default()
8565 },
8566 autoclose_before: "})]>".into(),
8567 ..Default::default()
8568 },
8569 Some(tree_sitter_html::LANGUAGE.into()),
8570 )
8571 .with_injection_query(
8572 r#"
8573 (script_element
8574 (raw_text) @injection.content
8575 (#set! injection.language "javascript"))
8576 "#,
8577 )
8578 .unwrap(),
8579 );
8580
8581 let javascript_language = Arc::new(Language::new(
8582 LanguageConfig {
8583 name: "JavaScript".into(),
8584 brackets: BracketPairConfig {
8585 pairs: vec![
8586 BracketPair {
8587 start: "/*".into(),
8588 end: " */".into(),
8589 close: true,
8590 ..Default::default()
8591 },
8592 BracketPair {
8593 start: "{".into(),
8594 end: "}".into(),
8595 close: true,
8596 ..Default::default()
8597 },
8598 BracketPair {
8599 start: "(".into(),
8600 end: ")".into(),
8601 close: true,
8602 ..Default::default()
8603 },
8604 ],
8605 ..Default::default()
8606 },
8607 autoclose_before: "})]>".into(),
8608 ..Default::default()
8609 },
8610 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8611 ));
8612
8613 cx.language_registry().add(html_language.clone());
8614 cx.language_registry().add(javascript_language.clone());
8615
8616 cx.update_buffer(|buffer, cx| {
8617 buffer.set_language(Some(html_language), cx);
8618 });
8619
8620 cx.set_state(
8621 &r#"
8622 <body>ˇ
8623 <script>
8624 var x = 1;ˇ
8625 </script>
8626 </body>ˇ
8627 "#
8628 .unindent(),
8629 );
8630
8631 // Precondition: different languages are active at different locations.
8632 cx.update_editor(|editor, window, cx| {
8633 let snapshot = editor.snapshot(window, cx);
8634 let cursors = editor.selections.ranges::<usize>(cx);
8635 let languages = cursors
8636 .iter()
8637 .map(|c| snapshot.language_at(c.start).unwrap().name())
8638 .collect::<Vec<_>>();
8639 assert_eq!(
8640 languages,
8641 &["HTML".into(), "JavaScript".into(), "HTML".into()]
8642 );
8643 });
8644
8645 // Angle brackets autoclose in HTML, but not JavaScript.
8646 cx.update_editor(|editor, window, cx| {
8647 editor.handle_input("<", window, cx);
8648 editor.handle_input("a", window, cx);
8649 });
8650 cx.assert_editor_state(
8651 &r#"
8652 <body><aˇ>
8653 <script>
8654 var x = 1;<aˇ
8655 </script>
8656 </body><aˇ>
8657 "#
8658 .unindent(),
8659 );
8660
8661 // Curly braces and parens autoclose in both HTML and JavaScript.
8662 cx.update_editor(|editor, window, cx| {
8663 editor.handle_input(" b=", window, cx);
8664 editor.handle_input("{", window, cx);
8665 editor.handle_input("c", window, cx);
8666 editor.handle_input("(", window, cx);
8667 });
8668 cx.assert_editor_state(
8669 &r#"
8670 <body><a b={c(ˇ)}>
8671 <script>
8672 var x = 1;<a b={c(ˇ)}
8673 </script>
8674 </body><a b={c(ˇ)}>
8675 "#
8676 .unindent(),
8677 );
8678
8679 // Brackets that were already autoclosed are skipped.
8680 cx.update_editor(|editor, window, cx| {
8681 editor.handle_input(")", window, cx);
8682 editor.handle_input("d", window, cx);
8683 editor.handle_input("}", window, cx);
8684 });
8685 cx.assert_editor_state(
8686 &r#"
8687 <body><a b={c()d}ˇ>
8688 <script>
8689 var x = 1;<a b={c()d}ˇ
8690 </script>
8691 </body><a b={c()d}ˇ>
8692 "#
8693 .unindent(),
8694 );
8695 cx.update_editor(|editor, window, cx| {
8696 editor.handle_input(">", window, cx);
8697 });
8698 cx.assert_editor_state(
8699 &r#"
8700 <body><a b={c()d}>ˇ
8701 <script>
8702 var x = 1;<a b={c()d}>ˇ
8703 </script>
8704 </body><a b={c()d}>ˇ
8705 "#
8706 .unindent(),
8707 );
8708
8709 // Reset
8710 cx.set_state(
8711 &r#"
8712 <body>ˇ
8713 <script>
8714 var x = 1;ˇ
8715 </script>
8716 </body>ˇ
8717 "#
8718 .unindent(),
8719 );
8720
8721 cx.update_editor(|editor, window, cx| {
8722 editor.handle_input("<", window, cx);
8723 });
8724 cx.assert_editor_state(
8725 &r#"
8726 <body><ˇ>
8727 <script>
8728 var x = 1;<ˇ
8729 </script>
8730 </body><ˇ>
8731 "#
8732 .unindent(),
8733 );
8734
8735 // When backspacing, the closing angle brackets are removed.
8736 cx.update_editor(|editor, window, cx| {
8737 editor.backspace(&Backspace, window, cx);
8738 });
8739 cx.assert_editor_state(
8740 &r#"
8741 <body>ˇ
8742 <script>
8743 var x = 1;ˇ
8744 </script>
8745 </body>ˇ
8746 "#
8747 .unindent(),
8748 );
8749
8750 // Block comments autoclose in JavaScript, but not HTML.
8751 cx.update_editor(|editor, window, cx| {
8752 editor.handle_input("/", window, cx);
8753 editor.handle_input("*", window, cx);
8754 });
8755 cx.assert_editor_state(
8756 &r#"
8757 <body>/*ˇ
8758 <script>
8759 var x = 1;/*ˇ */
8760 </script>
8761 </body>/*ˇ
8762 "#
8763 .unindent(),
8764 );
8765}
8766
8767#[gpui::test]
8768async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
8769 init_test(cx, |_| {});
8770
8771 let mut cx = EditorTestContext::new(cx).await;
8772
8773 let rust_language = Arc::new(
8774 Language::new(
8775 LanguageConfig {
8776 name: "Rust".into(),
8777 brackets: serde_json::from_value(json!([
8778 { "start": "{", "end": "}", "close": true, "newline": true },
8779 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
8780 ]))
8781 .unwrap(),
8782 autoclose_before: "})]>".into(),
8783 ..Default::default()
8784 },
8785 Some(tree_sitter_rust::LANGUAGE.into()),
8786 )
8787 .with_override_query("(string_literal) @string")
8788 .unwrap(),
8789 );
8790
8791 cx.language_registry().add(rust_language.clone());
8792 cx.update_buffer(|buffer, cx| {
8793 buffer.set_language(Some(rust_language), cx);
8794 });
8795
8796 cx.set_state(
8797 &r#"
8798 let x = ˇ
8799 "#
8800 .unindent(),
8801 );
8802
8803 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
8804 cx.update_editor(|editor, window, cx| {
8805 editor.handle_input("\"", window, cx);
8806 });
8807 cx.assert_editor_state(
8808 &r#"
8809 let x = "ˇ"
8810 "#
8811 .unindent(),
8812 );
8813
8814 // Inserting another quotation mark. The cursor moves across the existing
8815 // automatically-inserted quotation mark.
8816 cx.update_editor(|editor, window, cx| {
8817 editor.handle_input("\"", window, cx);
8818 });
8819 cx.assert_editor_state(
8820 &r#"
8821 let x = ""ˇ
8822 "#
8823 .unindent(),
8824 );
8825
8826 // Reset
8827 cx.set_state(
8828 &r#"
8829 let x = ˇ
8830 "#
8831 .unindent(),
8832 );
8833
8834 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
8835 cx.update_editor(|editor, window, cx| {
8836 editor.handle_input("\"", window, cx);
8837 editor.handle_input(" ", window, cx);
8838 editor.move_left(&Default::default(), window, cx);
8839 editor.handle_input("\\", window, cx);
8840 editor.handle_input("\"", window, cx);
8841 });
8842 cx.assert_editor_state(
8843 &r#"
8844 let x = "\"ˇ "
8845 "#
8846 .unindent(),
8847 );
8848
8849 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8850 // mark. Nothing is inserted.
8851 cx.update_editor(|editor, window, cx| {
8852 editor.move_right(&Default::default(), window, cx);
8853 editor.handle_input("\"", window, cx);
8854 });
8855 cx.assert_editor_state(
8856 &r#"
8857 let x = "\" "ˇ
8858 "#
8859 .unindent(),
8860 );
8861}
8862
8863#[gpui::test]
8864async fn test_surround_with_pair(cx: &mut TestAppContext) {
8865 init_test(cx, |_| {});
8866
8867 let language = Arc::new(Language::new(
8868 LanguageConfig {
8869 brackets: BracketPairConfig {
8870 pairs: vec![
8871 BracketPair {
8872 start: "{".to_string(),
8873 end: "}".to_string(),
8874 close: true,
8875 surround: true,
8876 newline: true,
8877 },
8878 BracketPair {
8879 start: "/* ".to_string(),
8880 end: "*/".to_string(),
8881 close: true,
8882 surround: true,
8883 ..Default::default()
8884 },
8885 ],
8886 ..Default::default()
8887 },
8888 ..Default::default()
8889 },
8890 Some(tree_sitter_rust::LANGUAGE.into()),
8891 ));
8892
8893 let text = r#"
8894 a
8895 b
8896 c
8897 "#
8898 .unindent();
8899
8900 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8901 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8902 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8903 editor
8904 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8905 .await;
8906
8907 editor.update_in(cx, |editor, window, cx| {
8908 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8909 s.select_display_ranges([
8910 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8911 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8912 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8913 ])
8914 });
8915
8916 editor.handle_input("{", window, cx);
8917 editor.handle_input("{", window, cx);
8918 editor.handle_input("{", window, cx);
8919 assert_eq!(
8920 editor.text(cx),
8921 "
8922 {{{a}}}
8923 {{{b}}}
8924 {{{c}}}
8925 "
8926 .unindent()
8927 );
8928 assert_eq!(
8929 editor.selections.display_ranges(cx),
8930 [
8931 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
8932 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
8933 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
8934 ]
8935 );
8936
8937 editor.undo(&Undo, window, cx);
8938 editor.undo(&Undo, window, cx);
8939 editor.undo(&Undo, window, cx);
8940 assert_eq!(
8941 editor.text(cx),
8942 "
8943 a
8944 b
8945 c
8946 "
8947 .unindent()
8948 );
8949 assert_eq!(
8950 editor.selections.display_ranges(cx),
8951 [
8952 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8953 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8954 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8955 ]
8956 );
8957
8958 // Ensure inserting the first character of a multi-byte bracket pair
8959 // doesn't surround the selections with the bracket.
8960 editor.handle_input("/", window, cx);
8961 assert_eq!(
8962 editor.text(cx),
8963 "
8964 /
8965 /
8966 /
8967 "
8968 .unindent()
8969 );
8970 assert_eq!(
8971 editor.selections.display_ranges(cx),
8972 [
8973 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8974 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8975 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8976 ]
8977 );
8978
8979 editor.undo(&Undo, window, cx);
8980 assert_eq!(
8981 editor.text(cx),
8982 "
8983 a
8984 b
8985 c
8986 "
8987 .unindent()
8988 );
8989 assert_eq!(
8990 editor.selections.display_ranges(cx),
8991 [
8992 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8993 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8994 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8995 ]
8996 );
8997
8998 // Ensure inserting the last character of a multi-byte bracket pair
8999 // doesn't surround the selections with the bracket.
9000 editor.handle_input("*", window, cx);
9001 assert_eq!(
9002 editor.text(cx),
9003 "
9004 *
9005 *
9006 *
9007 "
9008 .unindent()
9009 );
9010 assert_eq!(
9011 editor.selections.display_ranges(cx),
9012 [
9013 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
9014 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
9015 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
9016 ]
9017 );
9018 });
9019}
9020
9021#[gpui::test]
9022async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
9023 init_test(cx, |_| {});
9024
9025 let language = Arc::new(Language::new(
9026 LanguageConfig {
9027 brackets: BracketPairConfig {
9028 pairs: vec![BracketPair {
9029 start: "{".to_string(),
9030 end: "}".to_string(),
9031 close: true,
9032 surround: true,
9033 newline: true,
9034 }],
9035 ..Default::default()
9036 },
9037 autoclose_before: "}".to_string(),
9038 ..Default::default()
9039 },
9040 Some(tree_sitter_rust::LANGUAGE.into()),
9041 ));
9042
9043 let text = r#"
9044 a
9045 b
9046 c
9047 "#
9048 .unindent();
9049
9050 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9051 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9052 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9053 editor
9054 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9055 .await;
9056
9057 editor.update_in(cx, |editor, window, cx| {
9058 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9059 s.select_ranges([
9060 Point::new(0, 1)..Point::new(0, 1),
9061 Point::new(1, 1)..Point::new(1, 1),
9062 Point::new(2, 1)..Point::new(2, 1),
9063 ])
9064 });
9065
9066 editor.handle_input("{", window, cx);
9067 editor.handle_input("{", window, cx);
9068 editor.handle_input("_", window, cx);
9069 assert_eq!(
9070 editor.text(cx),
9071 "
9072 a{{_}}
9073 b{{_}}
9074 c{{_}}
9075 "
9076 .unindent()
9077 );
9078 assert_eq!(
9079 editor.selections.ranges::<Point>(cx),
9080 [
9081 Point::new(0, 4)..Point::new(0, 4),
9082 Point::new(1, 4)..Point::new(1, 4),
9083 Point::new(2, 4)..Point::new(2, 4)
9084 ]
9085 );
9086
9087 editor.backspace(&Default::default(), window, cx);
9088 editor.backspace(&Default::default(), window, cx);
9089 assert_eq!(
9090 editor.text(cx),
9091 "
9092 a{}
9093 b{}
9094 c{}
9095 "
9096 .unindent()
9097 );
9098 assert_eq!(
9099 editor.selections.ranges::<Point>(cx),
9100 [
9101 Point::new(0, 2)..Point::new(0, 2),
9102 Point::new(1, 2)..Point::new(1, 2),
9103 Point::new(2, 2)..Point::new(2, 2)
9104 ]
9105 );
9106
9107 editor.delete_to_previous_word_start(&Default::default(), window, cx);
9108 assert_eq!(
9109 editor.text(cx),
9110 "
9111 a
9112 b
9113 c
9114 "
9115 .unindent()
9116 );
9117 assert_eq!(
9118 editor.selections.ranges::<Point>(cx),
9119 [
9120 Point::new(0, 1)..Point::new(0, 1),
9121 Point::new(1, 1)..Point::new(1, 1),
9122 Point::new(2, 1)..Point::new(2, 1)
9123 ]
9124 );
9125 });
9126}
9127
9128#[gpui::test]
9129async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
9130 init_test(cx, |settings| {
9131 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9132 });
9133
9134 let mut cx = EditorTestContext::new(cx).await;
9135
9136 let language = Arc::new(Language::new(
9137 LanguageConfig {
9138 brackets: BracketPairConfig {
9139 pairs: vec![
9140 BracketPair {
9141 start: "{".to_string(),
9142 end: "}".to_string(),
9143 close: true,
9144 surround: true,
9145 newline: true,
9146 },
9147 BracketPair {
9148 start: "(".to_string(),
9149 end: ")".to_string(),
9150 close: true,
9151 surround: true,
9152 newline: true,
9153 },
9154 BracketPair {
9155 start: "[".to_string(),
9156 end: "]".to_string(),
9157 close: false,
9158 surround: true,
9159 newline: true,
9160 },
9161 ],
9162 ..Default::default()
9163 },
9164 autoclose_before: "})]".to_string(),
9165 ..Default::default()
9166 },
9167 Some(tree_sitter_rust::LANGUAGE.into()),
9168 ));
9169
9170 cx.language_registry().add(language.clone());
9171 cx.update_buffer(|buffer, cx| {
9172 buffer.set_language(Some(language), cx);
9173 });
9174
9175 cx.set_state(
9176 &"
9177 {(ˇ)}
9178 [[ˇ]]
9179 {(ˇ)}
9180 "
9181 .unindent(),
9182 );
9183
9184 cx.update_editor(|editor, window, cx| {
9185 editor.backspace(&Default::default(), window, cx);
9186 editor.backspace(&Default::default(), window, cx);
9187 });
9188
9189 cx.assert_editor_state(
9190 &"
9191 ˇ
9192 ˇ]]
9193 ˇ
9194 "
9195 .unindent(),
9196 );
9197
9198 cx.update_editor(|editor, window, cx| {
9199 editor.handle_input("{", window, cx);
9200 editor.handle_input("{", window, cx);
9201 editor.move_right(&MoveRight, window, cx);
9202 editor.move_right(&MoveRight, window, cx);
9203 editor.move_left(&MoveLeft, window, cx);
9204 editor.move_left(&MoveLeft, window, cx);
9205 editor.backspace(&Default::default(), window, cx);
9206 });
9207
9208 cx.assert_editor_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 });
9220
9221 cx.assert_editor_state(
9222 &"
9223 ˇ
9224 ˇ]]
9225 ˇ
9226 "
9227 .unindent(),
9228 );
9229}
9230
9231#[gpui::test]
9232async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
9233 init_test(cx, |_| {});
9234
9235 let language = Arc::new(Language::new(
9236 LanguageConfig::default(),
9237 Some(tree_sitter_rust::LANGUAGE.into()),
9238 ));
9239
9240 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
9241 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9242 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9243 editor
9244 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9245 .await;
9246
9247 editor.update_in(cx, |editor, window, cx| {
9248 editor.set_auto_replace_emoji_shortcode(true);
9249
9250 editor.handle_input("Hello ", window, cx);
9251 editor.handle_input(":wave", window, cx);
9252 assert_eq!(editor.text(cx), "Hello :wave".unindent());
9253
9254 editor.handle_input(":", window, cx);
9255 assert_eq!(editor.text(cx), "Hello 👋".unindent());
9256
9257 editor.handle_input(" :smile", window, cx);
9258 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
9259
9260 editor.handle_input(":", window, cx);
9261 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
9262
9263 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
9264 editor.handle_input(":wave", window, cx);
9265 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
9266
9267 editor.handle_input(":", window, cx);
9268 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
9269
9270 editor.handle_input(":1", window, cx);
9271 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
9272
9273 editor.handle_input(":", window, cx);
9274 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
9275
9276 // Ensure shortcode does not get replaced when it is part of a word
9277 editor.handle_input(" Test:wave", window, cx);
9278 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
9279
9280 editor.handle_input(":", window, cx);
9281 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
9282
9283 editor.set_auto_replace_emoji_shortcode(false);
9284
9285 // Ensure shortcode does not get replaced when auto replace is off
9286 editor.handle_input(" :wave", window, cx);
9287 assert_eq!(
9288 editor.text(cx),
9289 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
9290 );
9291
9292 editor.handle_input(":", window, cx);
9293 assert_eq!(
9294 editor.text(cx),
9295 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
9296 );
9297 });
9298}
9299
9300#[gpui::test]
9301async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
9302 init_test(cx, |_| {});
9303
9304 let (text, insertion_ranges) = marked_text_ranges(
9305 indoc! {"
9306 ˇ
9307 "},
9308 false,
9309 );
9310
9311 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
9312 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9313
9314 _ = editor.update_in(cx, |editor, window, cx| {
9315 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
9316
9317 editor
9318 .insert_snippet(&insertion_ranges, snippet, window, cx)
9319 .unwrap();
9320
9321 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
9322 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
9323 assert_eq!(editor.text(cx), expected_text);
9324 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
9325 }
9326
9327 assert(
9328 editor,
9329 cx,
9330 indoc! {"
9331 type «» =•
9332 "},
9333 );
9334
9335 assert!(editor.context_menu_visible(), "There should be a matches");
9336 });
9337}
9338
9339#[gpui::test]
9340async fn test_snippets(cx: &mut TestAppContext) {
9341 init_test(cx, |_| {});
9342
9343 let mut cx = EditorTestContext::new(cx).await;
9344
9345 cx.set_state(indoc! {"
9346 a.ˇ b
9347 a.ˇ b
9348 a.ˇ b
9349 "});
9350
9351 cx.update_editor(|editor, window, cx| {
9352 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
9353 let insertion_ranges = editor
9354 .selections
9355 .all(cx)
9356 .iter()
9357 .map(|s| s.range().clone())
9358 .collect::<Vec<_>>();
9359 editor
9360 .insert_snippet(&insertion_ranges, snippet, window, cx)
9361 .unwrap();
9362 });
9363
9364 cx.assert_editor_state(indoc! {"
9365 a.f(«oneˇ», two, «threeˇ») b
9366 a.f(«oneˇ», two, «threeˇ») b
9367 a.f(«oneˇ», two, «threeˇ») b
9368 "});
9369
9370 // Can't move earlier than the first tab stop
9371 cx.update_editor(|editor, window, cx| {
9372 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9373 });
9374 cx.assert_editor_state(indoc! {"
9375 a.f(«oneˇ», two, «threeˇ») b
9376 a.f(«oneˇ», two, «threeˇ») b
9377 a.f(«oneˇ», two, «threeˇ») b
9378 "});
9379
9380 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9381 cx.assert_editor_state(indoc! {"
9382 a.f(one, «twoˇ», three) b
9383 a.f(one, «twoˇ», three) b
9384 a.f(one, «twoˇ», three) b
9385 "});
9386
9387 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
9388 cx.assert_editor_state(indoc! {"
9389 a.f(«oneˇ», two, «threeˇ») b
9390 a.f(«oneˇ», two, «threeˇ») b
9391 a.f(«oneˇ», two, «threeˇ») b
9392 "});
9393
9394 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9395 cx.assert_editor_state(indoc! {"
9396 a.f(one, «twoˇ», three) b
9397 a.f(one, «twoˇ», three) b
9398 a.f(one, «twoˇ», three) b
9399 "});
9400 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9401 cx.assert_editor_state(indoc! {"
9402 a.f(one, two, three)ˇ b
9403 a.f(one, two, three)ˇ b
9404 a.f(one, two, three)ˇ b
9405 "});
9406
9407 // As soon as the last tab stop is reached, snippet state is gone
9408 cx.update_editor(|editor, window, cx| {
9409 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9410 });
9411 cx.assert_editor_state(indoc! {"
9412 a.f(one, two, three)ˇ b
9413 a.f(one, two, three)ˇ b
9414 a.f(one, two, three)ˇ b
9415 "});
9416}
9417
9418#[gpui::test]
9419async fn test_snippet_indentation(cx: &mut TestAppContext) {
9420 init_test(cx, |_| {});
9421
9422 let mut cx = EditorTestContext::new(cx).await;
9423
9424 cx.update_editor(|editor, window, cx| {
9425 let snippet = Snippet::parse(indoc! {"
9426 /*
9427 * Multiline comment with leading indentation
9428 *
9429 * $1
9430 */
9431 $0"})
9432 .unwrap();
9433 let insertion_ranges = editor
9434 .selections
9435 .all(cx)
9436 .iter()
9437 .map(|s| s.range().clone())
9438 .collect::<Vec<_>>();
9439 editor
9440 .insert_snippet(&insertion_ranges, snippet, window, cx)
9441 .unwrap();
9442 });
9443
9444 cx.assert_editor_state(indoc! {"
9445 /*
9446 * Multiline comment with leading indentation
9447 *
9448 * ˇ
9449 */
9450 "});
9451
9452 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9453 cx.assert_editor_state(indoc! {"
9454 /*
9455 * Multiline comment with leading indentation
9456 *
9457 *•
9458 */
9459 ˇ"});
9460}
9461
9462#[gpui::test]
9463async fn test_document_format_during_save(cx: &mut TestAppContext) {
9464 init_test(cx, |_| {});
9465
9466 let fs = FakeFs::new(cx.executor());
9467 fs.insert_file(path!("/file.rs"), Default::default()).await;
9468
9469 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
9470
9471 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9472 language_registry.add(rust_lang());
9473 let mut fake_servers = language_registry.register_fake_lsp(
9474 "Rust",
9475 FakeLspAdapter {
9476 capabilities: lsp::ServerCapabilities {
9477 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9478 ..Default::default()
9479 },
9480 ..Default::default()
9481 },
9482 );
9483
9484 let buffer = project
9485 .update(cx, |project, cx| {
9486 project.open_local_buffer(path!("/file.rs"), cx)
9487 })
9488 .await
9489 .unwrap();
9490
9491 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9492 let (editor, cx) = cx.add_window_view(|window, cx| {
9493 build_editor_with_project(project.clone(), buffer, window, cx)
9494 });
9495 editor.update_in(cx, |editor, window, cx| {
9496 editor.set_text("one\ntwo\nthree\n", window, cx)
9497 });
9498 assert!(cx.read(|cx| editor.is_dirty(cx)));
9499
9500 cx.executor().start_waiting();
9501 let fake_server = fake_servers.next().await.unwrap();
9502
9503 {
9504 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9505 move |params, _| async move {
9506 assert_eq!(
9507 params.text_document.uri,
9508 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9509 );
9510 assert_eq!(params.options.tab_size, 4);
9511 Ok(Some(vec![lsp::TextEdit::new(
9512 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9513 ", ".to_string(),
9514 )]))
9515 },
9516 );
9517 let save = editor
9518 .update_in(cx, |editor, window, cx| {
9519 editor.save(
9520 SaveOptions {
9521 format: true,
9522 autosave: false,
9523 },
9524 project.clone(),
9525 window,
9526 cx,
9527 )
9528 })
9529 .unwrap();
9530 cx.executor().start_waiting();
9531 save.await;
9532
9533 assert_eq!(
9534 editor.update(cx, |editor, cx| editor.text(cx)),
9535 "one, two\nthree\n"
9536 );
9537 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9538 }
9539
9540 {
9541 editor.update_in(cx, |editor, window, cx| {
9542 editor.set_text("one\ntwo\nthree\n", window, cx)
9543 });
9544 assert!(cx.read(|cx| editor.is_dirty(cx)));
9545
9546 // Ensure we can still save even if formatting hangs.
9547 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9548 move |params, _| async move {
9549 assert_eq!(
9550 params.text_document.uri,
9551 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9552 );
9553 futures::future::pending::<()>().await;
9554 unreachable!()
9555 },
9556 );
9557 let save = editor
9558 .update_in(cx, |editor, window, cx| {
9559 editor.save(
9560 SaveOptions {
9561 format: true,
9562 autosave: false,
9563 },
9564 project.clone(),
9565 window,
9566 cx,
9567 )
9568 })
9569 .unwrap();
9570 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9571 cx.executor().start_waiting();
9572 save.await;
9573 assert_eq!(
9574 editor.update(cx, |editor, cx| editor.text(cx)),
9575 "one\ntwo\nthree\n"
9576 );
9577 }
9578
9579 // Set rust language override and assert overridden tabsize is sent to language server
9580 update_test_language_settings(cx, |settings| {
9581 settings.languages.0.insert(
9582 "Rust".into(),
9583 LanguageSettingsContent {
9584 tab_size: NonZeroU32::new(8),
9585 ..Default::default()
9586 },
9587 );
9588 });
9589
9590 {
9591 editor.update_in(cx, |editor, window, cx| {
9592 editor.set_text("somehting_new\n", window, cx)
9593 });
9594 assert!(cx.read(|cx| editor.is_dirty(cx)));
9595 let _formatting_request_signal = fake_server
9596 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9597 assert_eq!(
9598 params.text_document.uri,
9599 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9600 );
9601 assert_eq!(params.options.tab_size, 8);
9602 Ok(Some(vec![]))
9603 });
9604 let save = editor
9605 .update_in(cx, |editor, window, cx| {
9606 editor.save(
9607 SaveOptions {
9608 format: true,
9609 autosave: false,
9610 },
9611 project.clone(),
9612 window,
9613 cx,
9614 )
9615 })
9616 .unwrap();
9617 cx.executor().start_waiting();
9618 save.await;
9619 }
9620}
9621
9622#[gpui::test]
9623async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
9624 init_test(cx, |settings| {
9625 settings.defaults.ensure_final_newline_on_save = Some(false);
9626 });
9627
9628 let fs = FakeFs::new(cx.executor());
9629 fs.insert_file(path!("/file.txt"), "foo".into()).await;
9630
9631 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
9632
9633 let buffer = project
9634 .update(cx, |project, cx| {
9635 project.open_local_buffer(path!("/file.txt"), cx)
9636 })
9637 .await
9638 .unwrap();
9639
9640 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9641 let (editor, cx) = cx.add_window_view(|window, cx| {
9642 build_editor_with_project(project.clone(), buffer, window, cx)
9643 });
9644 editor.update_in(cx, |editor, window, cx| {
9645 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9646 s.select_ranges([0..0])
9647 });
9648 });
9649 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9650
9651 editor.update_in(cx, |editor, window, cx| {
9652 editor.handle_input("\n", window, cx)
9653 });
9654 cx.run_until_parked();
9655 save(&editor, &project, cx).await;
9656 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9657
9658 editor.update_in(cx, |editor, window, cx| {
9659 editor.undo(&Default::default(), window, cx);
9660 });
9661 save(&editor, &project, cx).await;
9662 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9663
9664 editor.update_in(cx, |editor, window, cx| {
9665 editor.redo(&Default::default(), window, cx);
9666 });
9667 cx.run_until_parked();
9668 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9669
9670 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
9671 let save = editor
9672 .update_in(cx, |editor, window, cx| {
9673 editor.save(
9674 SaveOptions {
9675 format: true,
9676 autosave: false,
9677 },
9678 project.clone(),
9679 window,
9680 cx,
9681 )
9682 })
9683 .unwrap();
9684 cx.executor().start_waiting();
9685 save.await;
9686 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9687 }
9688}
9689
9690#[gpui::test]
9691async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9692 init_test(cx, |_| {});
9693
9694 let cols = 4;
9695 let rows = 10;
9696 let sample_text_1 = sample_text(rows, cols, 'a');
9697 assert_eq!(
9698 sample_text_1,
9699 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9700 );
9701 let sample_text_2 = sample_text(rows, cols, 'l');
9702 assert_eq!(
9703 sample_text_2,
9704 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9705 );
9706 let sample_text_3 = sample_text(rows, cols, 'v');
9707 assert_eq!(
9708 sample_text_3,
9709 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9710 );
9711
9712 let fs = FakeFs::new(cx.executor());
9713 fs.insert_tree(
9714 path!("/a"),
9715 json!({
9716 "main.rs": sample_text_1,
9717 "other.rs": sample_text_2,
9718 "lib.rs": sample_text_3,
9719 }),
9720 )
9721 .await;
9722
9723 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9724 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9725 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9726
9727 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9728 language_registry.add(rust_lang());
9729 let mut fake_servers = language_registry.register_fake_lsp(
9730 "Rust",
9731 FakeLspAdapter {
9732 capabilities: lsp::ServerCapabilities {
9733 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9734 ..Default::default()
9735 },
9736 ..Default::default()
9737 },
9738 );
9739
9740 let worktree = project.update(cx, |project, cx| {
9741 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
9742 assert_eq!(worktrees.len(), 1);
9743 worktrees.pop().unwrap()
9744 });
9745 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9746
9747 let buffer_1 = project
9748 .update(cx, |project, cx| {
9749 project.open_buffer((worktree_id, "main.rs"), cx)
9750 })
9751 .await
9752 .unwrap();
9753 let buffer_2 = project
9754 .update(cx, |project, cx| {
9755 project.open_buffer((worktree_id, "other.rs"), cx)
9756 })
9757 .await
9758 .unwrap();
9759 let buffer_3 = project
9760 .update(cx, |project, cx| {
9761 project.open_buffer((worktree_id, "lib.rs"), cx)
9762 })
9763 .await
9764 .unwrap();
9765
9766 let multi_buffer = cx.new(|cx| {
9767 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9768 multi_buffer.push_excerpts(
9769 buffer_1.clone(),
9770 [
9771 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9772 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9773 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9774 ],
9775 cx,
9776 );
9777 multi_buffer.push_excerpts(
9778 buffer_2.clone(),
9779 [
9780 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9781 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9782 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9783 ],
9784 cx,
9785 );
9786 multi_buffer.push_excerpts(
9787 buffer_3.clone(),
9788 [
9789 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9790 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9791 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9792 ],
9793 cx,
9794 );
9795 multi_buffer
9796 });
9797 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
9798 Editor::new(
9799 EditorMode::full(),
9800 multi_buffer,
9801 Some(project.clone()),
9802 window,
9803 cx,
9804 )
9805 });
9806
9807 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9808 editor.change_selections(
9809 SelectionEffects::scroll(Autoscroll::Next),
9810 window,
9811 cx,
9812 |s| s.select_ranges(Some(1..2)),
9813 );
9814 editor.insert("|one|two|three|", window, cx);
9815 });
9816 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9817 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9818 editor.change_selections(
9819 SelectionEffects::scroll(Autoscroll::Next),
9820 window,
9821 cx,
9822 |s| s.select_ranges(Some(60..70)),
9823 );
9824 editor.insert("|four|five|six|", window, cx);
9825 });
9826 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9827
9828 // First two buffers should be edited, but not the third one.
9829 assert_eq!(
9830 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9831 "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}",
9832 );
9833 buffer_1.update(cx, |buffer, _| {
9834 assert!(buffer.is_dirty());
9835 assert_eq!(
9836 buffer.text(),
9837 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
9838 )
9839 });
9840 buffer_2.update(cx, |buffer, _| {
9841 assert!(buffer.is_dirty());
9842 assert_eq!(
9843 buffer.text(),
9844 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
9845 )
9846 });
9847 buffer_3.update(cx, |buffer, _| {
9848 assert!(!buffer.is_dirty());
9849 assert_eq!(buffer.text(), sample_text_3,)
9850 });
9851 cx.executor().run_until_parked();
9852
9853 cx.executor().start_waiting();
9854 let save = multi_buffer_editor
9855 .update_in(cx, |editor, window, cx| {
9856 editor.save(
9857 SaveOptions {
9858 format: true,
9859 autosave: false,
9860 },
9861 project.clone(),
9862 window,
9863 cx,
9864 )
9865 })
9866 .unwrap();
9867
9868 let fake_server = fake_servers.next().await.unwrap();
9869 fake_server
9870 .server
9871 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
9872 Ok(Some(vec![lsp::TextEdit::new(
9873 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9874 format!("[{} formatted]", params.text_document.uri),
9875 )]))
9876 })
9877 .detach();
9878 save.await;
9879
9880 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
9881 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
9882 assert_eq!(
9883 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9884 uri!(
9885 "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}"
9886 ),
9887 );
9888 buffer_1.update(cx, |buffer, _| {
9889 assert!(!buffer.is_dirty());
9890 assert_eq!(
9891 buffer.text(),
9892 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
9893 )
9894 });
9895 buffer_2.update(cx, |buffer, _| {
9896 assert!(!buffer.is_dirty());
9897 assert_eq!(
9898 buffer.text(),
9899 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
9900 )
9901 });
9902 buffer_3.update(cx, |buffer, _| {
9903 assert!(!buffer.is_dirty());
9904 assert_eq!(buffer.text(), sample_text_3,)
9905 });
9906}
9907
9908#[gpui::test]
9909async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
9910 init_test(cx, |_| {});
9911
9912 let fs = FakeFs::new(cx.executor());
9913 fs.insert_tree(
9914 path!("/dir"),
9915 json!({
9916 "file1.rs": "fn main() { println!(\"hello\"); }",
9917 "file2.rs": "fn test() { println!(\"test\"); }",
9918 "file3.rs": "fn other() { println!(\"other\"); }\n",
9919 }),
9920 )
9921 .await;
9922
9923 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
9924 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9925 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9926
9927 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9928 language_registry.add(rust_lang());
9929
9930 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
9931 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9932
9933 // Open three buffers
9934 let buffer_1 = project
9935 .update(cx, |project, cx| {
9936 project.open_buffer((worktree_id, "file1.rs"), cx)
9937 })
9938 .await
9939 .unwrap();
9940 let buffer_2 = project
9941 .update(cx, |project, cx| {
9942 project.open_buffer((worktree_id, "file2.rs"), cx)
9943 })
9944 .await
9945 .unwrap();
9946 let buffer_3 = project
9947 .update(cx, |project, cx| {
9948 project.open_buffer((worktree_id, "file3.rs"), cx)
9949 })
9950 .await
9951 .unwrap();
9952
9953 // Create a multi-buffer with all three buffers
9954 let multi_buffer = cx.new(|cx| {
9955 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9956 multi_buffer.push_excerpts(
9957 buffer_1.clone(),
9958 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9959 cx,
9960 );
9961 multi_buffer.push_excerpts(
9962 buffer_2.clone(),
9963 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9964 cx,
9965 );
9966 multi_buffer.push_excerpts(
9967 buffer_3.clone(),
9968 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9969 cx,
9970 );
9971 multi_buffer
9972 });
9973
9974 let editor = cx.new_window_entity(|window, cx| {
9975 Editor::new(
9976 EditorMode::full(),
9977 multi_buffer,
9978 Some(project.clone()),
9979 window,
9980 cx,
9981 )
9982 });
9983
9984 // Edit only the first buffer
9985 editor.update_in(cx, |editor, window, cx| {
9986 editor.change_selections(
9987 SelectionEffects::scroll(Autoscroll::Next),
9988 window,
9989 cx,
9990 |s| s.select_ranges(Some(10..10)),
9991 );
9992 editor.insert("// edited", window, cx);
9993 });
9994
9995 // Verify that only buffer 1 is dirty
9996 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
9997 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9998 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9999
10000 // Get write counts after file creation (files were created with initial content)
10001 // We expect each file to have been written once during creation
10002 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
10003 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
10004 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
10005
10006 // Perform autosave
10007 let save_task = editor.update_in(cx, |editor, window, cx| {
10008 editor.save(
10009 SaveOptions {
10010 format: true,
10011 autosave: true,
10012 },
10013 project.clone(),
10014 window,
10015 cx,
10016 )
10017 });
10018 save_task.await.unwrap();
10019
10020 // Only the dirty buffer should have been saved
10021 assert_eq!(
10022 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
10023 1,
10024 "Buffer 1 was dirty, so it should have been written once during autosave"
10025 );
10026 assert_eq!(
10027 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
10028 0,
10029 "Buffer 2 was clean, so it should not have been written during autosave"
10030 );
10031 assert_eq!(
10032 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
10033 0,
10034 "Buffer 3 was clean, so it should not have been written during autosave"
10035 );
10036
10037 // Verify buffer states after autosave
10038 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10039 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10040 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
10041
10042 // Now perform a manual save (format = true)
10043 let save_task = editor.update_in(cx, |editor, window, cx| {
10044 editor.save(
10045 SaveOptions {
10046 format: true,
10047 autosave: false,
10048 },
10049 project.clone(),
10050 window,
10051 cx,
10052 )
10053 });
10054 save_task.await.unwrap();
10055
10056 // During manual save, clean buffers don't get written to disk
10057 // They just get did_save called for language server notifications
10058 assert_eq!(
10059 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
10060 1,
10061 "Buffer 1 should only have been written once total (during autosave, not manual save)"
10062 );
10063 assert_eq!(
10064 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
10065 0,
10066 "Buffer 2 should not have been written at all"
10067 );
10068 assert_eq!(
10069 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
10070 0,
10071 "Buffer 3 should not have been written at all"
10072 );
10073}
10074
10075async fn setup_range_format_test(
10076 cx: &mut TestAppContext,
10077) -> (
10078 Entity<Project>,
10079 Entity<Editor>,
10080 &mut gpui::VisualTestContext,
10081 lsp::FakeLanguageServer,
10082) {
10083 init_test(cx, |_| {});
10084
10085 let fs = FakeFs::new(cx.executor());
10086 fs.insert_file(path!("/file.rs"), Default::default()).await;
10087
10088 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10089
10090 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10091 language_registry.add(rust_lang());
10092 let mut fake_servers = language_registry.register_fake_lsp(
10093 "Rust",
10094 FakeLspAdapter {
10095 capabilities: lsp::ServerCapabilities {
10096 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
10097 ..lsp::ServerCapabilities::default()
10098 },
10099 ..FakeLspAdapter::default()
10100 },
10101 );
10102
10103 let buffer = project
10104 .update(cx, |project, cx| {
10105 project.open_local_buffer(path!("/file.rs"), cx)
10106 })
10107 .await
10108 .unwrap();
10109
10110 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10111 let (editor, cx) = cx.add_window_view(|window, cx| {
10112 build_editor_with_project(project.clone(), buffer, window, cx)
10113 });
10114
10115 cx.executor().start_waiting();
10116 let fake_server = fake_servers.next().await.unwrap();
10117
10118 (project, editor, cx, fake_server)
10119}
10120
10121#[gpui::test]
10122async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
10123 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10124
10125 editor.update_in(cx, |editor, window, cx| {
10126 editor.set_text("one\ntwo\nthree\n", window, cx)
10127 });
10128 assert!(cx.read(|cx| editor.is_dirty(cx)));
10129
10130 let save = editor
10131 .update_in(cx, |editor, window, cx| {
10132 editor.save(
10133 SaveOptions {
10134 format: true,
10135 autosave: false,
10136 },
10137 project.clone(),
10138 window,
10139 cx,
10140 )
10141 })
10142 .unwrap();
10143 fake_server
10144 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10145 assert_eq!(
10146 params.text_document.uri,
10147 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10148 );
10149 assert_eq!(params.options.tab_size, 4);
10150 Ok(Some(vec![lsp::TextEdit::new(
10151 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10152 ", ".to_string(),
10153 )]))
10154 })
10155 .next()
10156 .await;
10157 cx.executor().start_waiting();
10158 save.await;
10159 assert_eq!(
10160 editor.update(cx, |editor, cx| editor.text(cx)),
10161 "one, two\nthree\n"
10162 );
10163 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10164}
10165
10166#[gpui::test]
10167async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
10168 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10169
10170 editor.update_in(cx, |editor, window, cx| {
10171 editor.set_text("one\ntwo\nthree\n", window, cx)
10172 });
10173 assert!(cx.read(|cx| editor.is_dirty(cx)));
10174
10175 // Test that save still works when formatting hangs
10176 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
10177 move |params, _| async move {
10178 assert_eq!(
10179 params.text_document.uri,
10180 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10181 );
10182 futures::future::pending::<()>().await;
10183 unreachable!()
10184 },
10185 );
10186 let save = editor
10187 .update_in(cx, |editor, window, cx| {
10188 editor.save(
10189 SaveOptions {
10190 format: true,
10191 autosave: false,
10192 },
10193 project.clone(),
10194 window,
10195 cx,
10196 )
10197 })
10198 .unwrap();
10199 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10200 cx.executor().start_waiting();
10201 save.await;
10202 assert_eq!(
10203 editor.update(cx, |editor, cx| editor.text(cx)),
10204 "one\ntwo\nthree\n"
10205 );
10206 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10207}
10208
10209#[gpui::test]
10210async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
10211 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10212
10213 // Buffer starts clean, no formatting should be requested
10214 let save = editor
10215 .update_in(cx, |editor, window, cx| {
10216 editor.save(
10217 SaveOptions {
10218 format: false,
10219 autosave: false,
10220 },
10221 project.clone(),
10222 window,
10223 cx,
10224 )
10225 })
10226 .unwrap();
10227 let _pending_format_request = fake_server
10228 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
10229 panic!("Should not be invoked");
10230 })
10231 .next();
10232 cx.executor().start_waiting();
10233 save.await;
10234 cx.run_until_parked();
10235}
10236
10237#[gpui::test]
10238async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
10239 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10240
10241 // Set Rust language override and assert overridden tabsize is sent to language server
10242 update_test_language_settings(cx, |settings| {
10243 settings.languages.0.insert(
10244 "Rust".into(),
10245 LanguageSettingsContent {
10246 tab_size: NonZeroU32::new(8),
10247 ..Default::default()
10248 },
10249 );
10250 });
10251
10252 editor.update_in(cx, |editor, window, cx| {
10253 editor.set_text("something_new\n", window, cx)
10254 });
10255 assert!(cx.read(|cx| editor.is_dirty(cx)));
10256 let save = editor
10257 .update_in(cx, |editor, window, cx| {
10258 editor.save(
10259 SaveOptions {
10260 format: true,
10261 autosave: false,
10262 },
10263 project.clone(),
10264 window,
10265 cx,
10266 )
10267 })
10268 .unwrap();
10269 fake_server
10270 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10271 assert_eq!(
10272 params.text_document.uri,
10273 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10274 );
10275 assert_eq!(params.options.tab_size, 8);
10276 Ok(Some(Vec::new()))
10277 })
10278 .next()
10279 .await;
10280 save.await;
10281}
10282
10283#[gpui::test]
10284async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
10285 init_test(cx, |settings| {
10286 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
10287 Formatter::LanguageServer { name: None },
10288 )))
10289 });
10290
10291 let fs = FakeFs::new(cx.executor());
10292 fs.insert_file(path!("/file.rs"), Default::default()).await;
10293
10294 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10295
10296 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10297 language_registry.add(Arc::new(Language::new(
10298 LanguageConfig {
10299 name: "Rust".into(),
10300 matcher: LanguageMatcher {
10301 path_suffixes: vec!["rs".to_string()],
10302 ..Default::default()
10303 },
10304 ..LanguageConfig::default()
10305 },
10306 Some(tree_sitter_rust::LANGUAGE.into()),
10307 )));
10308 update_test_language_settings(cx, |settings| {
10309 // Enable Prettier formatting for the same buffer, and ensure
10310 // LSP is called instead of Prettier.
10311 settings.defaults.prettier = Some(PrettierSettings {
10312 allowed: true,
10313 ..PrettierSettings::default()
10314 });
10315 });
10316 let mut fake_servers = language_registry.register_fake_lsp(
10317 "Rust",
10318 FakeLspAdapter {
10319 capabilities: lsp::ServerCapabilities {
10320 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10321 ..Default::default()
10322 },
10323 ..Default::default()
10324 },
10325 );
10326
10327 let buffer = project
10328 .update(cx, |project, cx| {
10329 project.open_local_buffer(path!("/file.rs"), cx)
10330 })
10331 .await
10332 .unwrap();
10333
10334 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10335 let (editor, cx) = cx.add_window_view(|window, cx| {
10336 build_editor_with_project(project.clone(), buffer, window, cx)
10337 });
10338 editor.update_in(cx, |editor, window, cx| {
10339 editor.set_text("one\ntwo\nthree\n", window, cx)
10340 });
10341
10342 cx.executor().start_waiting();
10343 let fake_server = fake_servers.next().await.unwrap();
10344
10345 let format = editor
10346 .update_in(cx, |editor, window, cx| {
10347 editor.perform_format(
10348 project.clone(),
10349 FormatTrigger::Manual,
10350 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10351 window,
10352 cx,
10353 )
10354 })
10355 .unwrap();
10356 fake_server
10357 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10358 assert_eq!(
10359 params.text_document.uri,
10360 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10361 );
10362 assert_eq!(params.options.tab_size, 4);
10363 Ok(Some(vec![lsp::TextEdit::new(
10364 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10365 ", ".to_string(),
10366 )]))
10367 })
10368 .next()
10369 .await;
10370 cx.executor().start_waiting();
10371 format.await;
10372 assert_eq!(
10373 editor.update(cx, |editor, cx| editor.text(cx)),
10374 "one, two\nthree\n"
10375 );
10376
10377 editor.update_in(cx, |editor, window, cx| {
10378 editor.set_text("one\ntwo\nthree\n", window, cx)
10379 });
10380 // Ensure we don't lock if formatting hangs.
10381 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10382 move |params, _| async move {
10383 assert_eq!(
10384 params.text_document.uri,
10385 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10386 );
10387 futures::future::pending::<()>().await;
10388 unreachable!()
10389 },
10390 );
10391 let format = editor
10392 .update_in(cx, |editor, window, cx| {
10393 editor.perform_format(
10394 project,
10395 FormatTrigger::Manual,
10396 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10397 window,
10398 cx,
10399 )
10400 })
10401 .unwrap();
10402 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10403 cx.executor().start_waiting();
10404 format.await;
10405 assert_eq!(
10406 editor.update(cx, |editor, cx| editor.text(cx)),
10407 "one\ntwo\nthree\n"
10408 );
10409}
10410
10411#[gpui::test]
10412async fn test_multiple_formatters(cx: &mut TestAppContext) {
10413 init_test(cx, |settings| {
10414 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
10415 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10416 Formatter::LanguageServer { name: None },
10417 Formatter::CodeActions(
10418 [
10419 ("code-action-1".into(), true),
10420 ("code-action-2".into(), true),
10421 ]
10422 .into_iter()
10423 .collect(),
10424 ),
10425 ])))
10426 });
10427
10428 let fs = FakeFs::new(cx.executor());
10429 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
10430 .await;
10431
10432 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10433 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10434 language_registry.add(rust_lang());
10435
10436 let mut fake_servers = language_registry.register_fake_lsp(
10437 "Rust",
10438 FakeLspAdapter {
10439 capabilities: lsp::ServerCapabilities {
10440 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10441 execute_command_provider: Some(lsp::ExecuteCommandOptions {
10442 commands: vec!["the-command-for-code-action-1".into()],
10443 ..Default::default()
10444 }),
10445 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10446 ..Default::default()
10447 },
10448 ..Default::default()
10449 },
10450 );
10451
10452 let buffer = project
10453 .update(cx, |project, cx| {
10454 project.open_local_buffer(path!("/file.rs"), cx)
10455 })
10456 .await
10457 .unwrap();
10458
10459 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10460 let (editor, cx) = cx.add_window_view(|window, cx| {
10461 build_editor_with_project(project.clone(), buffer, window, cx)
10462 });
10463
10464 cx.executor().start_waiting();
10465
10466 let fake_server = fake_servers.next().await.unwrap();
10467 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10468 move |_params, _| async move {
10469 Ok(Some(vec![lsp::TextEdit::new(
10470 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10471 "applied-formatting\n".to_string(),
10472 )]))
10473 },
10474 );
10475 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10476 move |params, _| async move {
10477 assert_eq!(
10478 params.context.only,
10479 Some(vec!["code-action-1".into(), "code-action-2".into()])
10480 );
10481 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
10482 Ok(Some(vec![
10483 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10484 kind: Some("code-action-1".into()),
10485 edit: Some(lsp::WorkspaceEdit::new(
10486 [(
10487 uri.clone(),
10488 vec![lsp::TextEdit::new(
10489 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10490 "applied-code-action-1-edit\n".to_string(),
10491 )],
10492 )]
10493 .into_iter()
10494 .collect(),
10495 )),
10496 command: Some(lsp::Command {
10497 command: "the-command-for-code-action-1".into(),
10498 ..Default::default()
10499 }),
10500 ..Default::default()
10501 }),
10502 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10503 kind: Some("code-action-2".into()),
10504 edit: Some(lsp::WorkspaceEdit::new(
10505 [(
10506 uri.clone(),
10507 vec![lsp::TextEdit::new(
10508 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10509 "applied-code-action-2-edit\n".to_string(),
10510 )],
10511 )]
10512 .into_iter()
10513 .collect(),
10514 )),
10515 ..Default::default()
10516 }),
10517 ]))
10518 },
10519 );
10520
10521 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
10522 move |params, _| async move { Ok(params) }
10523 });
10524
10525 let command_lock = Arc::new(futures::lock::Mutex::new(()));
10526 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
10527 let fake = fake_server.clone();
10528 let lock = command_lock.clone();
10529 move |params, _| {
10530 assert_eq!(params.command, "the-command-for-code-action-1");
10531 let fake = fake.clone();
10532 let lock = lock.clone();
10533 async move {
10534 lock.lock().await;
10535 fake.server
10536 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
10537 label: None,
10538 edit: lsp::WorkspaceEdit {
10539 changes: Some(
10540 [(
10541 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
10542 vec![lsp::TextEdit {
10543 range: lsp::Range::new(
10544 lsp::Position::new(0, 0),
10545 lsp::Position::new(0, 0),
10546 ),
10547 new_text: "applied-code-action-1-command\n".into(),
10548 }],
10549 )]
10550 .into_iter()
10551 .collect(),
10552 ),
10553 ..Default::default()
10554 },
10555 })
10556 .await
10557 .into_response()
10558 .unwrap();
10559 Ok(Some(json!(null)))
10560 }
10561 }
10562 });
10563
10564 cx.executor().start_waiting();
10565 editor
10566 .update_in(cx, |editor, window, cx| {
10567 editor.perform_format(
10568 project.clone(),
10569 FormatTrigger::Manual,
10570 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10571 window,
10572 cx,
10573 )
10574 })
10575 .unwrap()
10576 .await;
10577 editor.update(cx, |editor, cx| {
10578 assert_eq!(
10579 editor.text(cx),
10580 r#"
10581 applied-code-action-2-edit
10582 applied-code-action-1-command
10583 applied-code-action-1-edit
10584 applied-formatting
10585 one
10586 two
10587 three
10588 "#
10589 .unindent()
10590 );
10591 });
10592
10593 editor.update_in(cx, |editor, window, cx| {
10594 editor.undo(&Default::default(), window, cx);
10595 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10596 });
10597
10598 // Perform a manual edit while waiting for an LSP command
10599 // that's being run as part of a formatting code action.
10600 let lock_guard = command_lock.lock().await;
10601 let format = editor
10602 .update_in(cx, |editor, window, cx| {
10603 editor.perform_format(
10604 project.clone(),
10605 FormatTrigger::Manual,
10606 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10607 window,
10608 cx,
10609 )
10610 })
10611 .unwrap();
10612 cx.run_until_parked();
10613 editor.update(cx, |editor, cx| {
10614 assert_eq!(
10615 editor.text(cx),
10616 r#"
10617 applied-code-action-1-edit
10618 applied-formatting
10619 one
10620 two
10621 three
10622 "#
10623 .unindent()
10624 );
10625
10626 editor.buffer.update(cx, |buffer, cx| {
10627 let ix = buffer.len(cx);
10628 buffer.edit([(ix..ix, "edited\n")], None, cx);
10629 });
10630 });
10631
10632 // Allow the LSP command to proceed. Because the buffer was edited,
10633 // the second code action will not be run.
10634 drop(lock_guard);
10635 format.await;
10636 editor.update_in(cx, |editor, window, cx| {
10637 assert_eq!(
10638 editor.text(cx),
10639 r#"
10640 applied-code-action-1-command
10641 applied-code-action-1-edit
10642 applied-formatting
10643 one
10644 two
10645 three
10646 edited
10647 "#
10648 .unindent()
10649 );
10650
10651 // The manual edit is undone first, because it is the last thing the user did
10652 // (even though the command completed afterwards).
10653 editor.undo(&Default::default(), window, cx);
10654 assert_eq!(
10655 editor.text(cx),
10656 r#"
10657 applied-code-action-1-command
10658 applied-code-action-1-edit
10659 applied-formatting
10660 one
10661 two
10662 three
10663 "#
10664 .unindent()
10665 );
10666
10667 // All the formatting (including the command, which completed after the manual edit)
10668 // is undone together.
10669 editor.undo(&Default::default(), window, cx);
10670 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10671 });
10672}
10673
10674#[gpui::test]
10675async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
10676 init_test(cx, |settings| {
10677 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10678 Formatter::LanguageServer { name: None },
10679 ])))
10680 });
10681
10682 let fs = FakeFs::new(cx.executor());
10683 fs.insert_file(path!("/file.ts"), Default::default()).await;
10684
10685 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10686
10687 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10688 language_registry.add(Arc::new(Language::new(
10689 LanguageConfig {
10690 name: "TypeScript".into(),
10691 matcher: LanguageMatcher {
10692 path_suffixes: vec!["ts".to_string()],
10693 ..Default::default()
10694 },
10695 ..LanguageConfig::default()
10696 },
10697 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10698 )));
10699 update_test_language_settings(cx, |settings| {
10700 settings.defaults.prettier = Some(PrettierSettings {
10701 allowed: true,
10702 ..PrettierSettings::default()
10703 });
10704 });
10705 let mut fake_servers = language_registry.register_fake_lsp(
10706 "TypeScript",
10707 FakeLspAdapter {
10708 capabilities: lsp::ServerCapabilities {
10709 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10710 ..Default::default()
10711 },
10712 ..Default::default()
10713 },
10714 );
10715
10716 let buffer = project
10717 .update(cx, |project, cx| {
10718 project.open_local_buffer(path!("/file.ts"), cx)
10719 })
10720 .await
10721 .unwrap();
10722
10723 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10724 let (editor, cx) = cx.add_window_view(|window, cx| {
10725 build_editor_with_project(project.clone(), buffer, window, cx)
10726 });
10727 editor.update_in(cx, |editor, window, cx| {
10728 editor.set_text(
10729 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10730 window,
10731 cx,
10732 )
10733 });
10734
10735 cx.executor().start_waiting();
10736 let fake_server = fake_servers.next().await.unwrap();
10737
10738 let format = editor
10739 .update_in(cx, |editor, window, cx| {
10740 editor.perform_code_action_kind(
10741 project.clone(),
10742 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10743 window,
10744 cx,
10745 )
10746 })
10747 .unwrap();
10748 fake_server
10749 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
10750 assert_eq!(
10751 params.text_document.uri,
10752 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10753 );
10754 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
10755 lsp::CodeAction {
10756 title: "Organize Imports".to_string(),
10757 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
10758 edit: Some(lsp::WorkspaceEdit {
10759 changes: Some(
10760 [(
10761 params.text_document.uri.clone(),
10762 vec![lsp::TextEdit::new(
10763 lsp::Range::new(
10764 lsp::Position::new(1, 0),
10765 lsp::Position::new(2, 0),
10766 ),
10767 "".to_string(),
10768 )],
10769 )]
10770 .into_iter()
10771 .collect(),
10772 ),
10773 ..Default::default()
10774 }),
10775 ..Default::default()
10776 },
10777 )]))
10778 })
10779 .next()
10780 .await;
10781 cx.executor().start_waiting();
10782 format.await;
10783 assert_eq!(
10784 editor.update(cx, |editor, cx| editor.text(cx)),
10785 "import { a } from 'module';\n\nconst x = a;\n"
10786 );
10787
10788 editor.update_in(cx, |editor, window, cx| {
10789 editor.set_text(
10790 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10791 window,
10792 cx,
10793 )
10794 });
10795 // Ensure we don't lock if code action hangs.
10796 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10797 move |params, _| async move {
10798 assert_eq!(
10799 params.text_document.uri,
10800 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10801 );
10802 futures::future::pending::<()>().await;
10803 unreachable!()
10804 },
10805 );
10806 let format = editor
10807 .update_in(cx, |editor, window, cx| {
10808 editor.perform_code_action_kind(
10809 project,
10810 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10811 window,
10812 cx,
10813 )
10814 })
10815 .unwrap();
10816 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
10817 cx.executor().start_waiting();
10818 format.await;
10819 assert_eq!(
10820 editor.update(cx, |editor, cx| editor.text(cx)),
10821 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
10822 );
10823}
10824
10825#[gpui::test]
10826async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
10827 init_test(cx, |_| {});
10828
10829 let mut cx = EditorLspTestContext::new_rust(
10830 lsp::ServerCapabilities {
10831 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10832 ..Default::default()
10833 },
10834 cx,
10835 )
10836 .await;
10837
10838 cx.set_state(indoc! {"
10839 one.twoˇ
10840 "});
10841
10842 // The format request takes a long time. When it completes, it inserts
10843 // a newline and an indent before the `.`
10844 cx.lsp
10845 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
10846 let executor = cx.background_executor().clone();
10847 async move {
10848 executor.timer(Duration::from_millis(100)).await;
10849 Ok(Some(vec![lsp::TextEdit {
10850 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
10851 new_text: "\n ".into(),
10852 }]))
10853 }
10854 });
10855
10856 // Submit a format request.
10857 let format_1 = cx
10858 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10859 .unwrap();
10860 cx.executor().run_until_parked();
10861
10862 // Submit a second format request.
10863 let format_2 = cx
10864 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10865 .unwrap();
10866 cx.executor().run_until_parked();
10867
10868 // Wait for both format requests to complete
10869 cx.executor().advance_clock(Duration::from_millis(200));
10870 cx.executor().start_waiting();
10871 format_1.await.unwrap();
10872 cx.executor().start_waiting();
10873 format_2.await.unwrap();
10874
10875 // The formatting edits only happens once.
10876 cx.assert_editor_state(indoc! {"
10877 one
10878 .twoˇ
10879 "});
10880}
10881
10882#[gpui::test]
10883async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
10884 init_test(cx, |settings| {
10885 settings.defaults.formatter = Some(SelectedFormatter::Auto)
10886 });
10887
10888 let mut cx = EditorLspTestContext::new_rust(
10889 lsp::ServerCapabilities {
10890 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10891 ..Default::default()
10892 },
10893 cx,
10894 )
10895 .await;
10896
10897 // Set up a buffer white some trailing whitespace and no trailing newline.
10898 cx.set_state(
10899 &[
10900 "one ", //
10901 "twoˇ", //
10902 "three ", //
10903 "four", //
10904 ]
10905 .join("\n"),
10906 );
10907
10908 // Submit a format request.
10909 let format = cx
10910 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10911 .unwrap();
10912
10913 // Record which buffer changes have been sent to the language server
10914 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
10915 cx.lsp
10916 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
10917 let buffer_changes = buffer_changes.clone();
10918 move |params, _| {
10919 buffer_changes.lock().extend(
10920 params
10921 .content_changes
10922 .into_iter()
10923 .map(|e| (e.range.unwrap(), e.text)),
10924 );
10925 }
10926 });
10927
10928 // Handle formatting requests to the language server.
10929 cx.lsp
10930 .set_request_handler::<lsp::request::Formatting, _, _>({
10931 let buffer_changes = buffer_changes.clone();
10932 move |_, _| {
10933 // When formatting is requested, trailing whitespace has already been stripped,
10934 // and the trailing newline has already been added.
10935 assert_eq!(
10936 &buffer_changes.lock()[1..],
10937 &[
10938 (
10939 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
10940 "".into()
10941 ),
10942 (
10943 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
10944 "".into()
10945 ),
10946 (
10947 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
10948 "\n".into()
10949 ),
10950 ]
10951 );
10952
10953 // Insert blank lines between each line of the buffer.
10954 async move {
10955 Ok(Some(vec![
10956 lsp::TextEdit {
10957 range: lsp::Range::new(
10958 lsp::Position::new(1, 0),
10959 lsp::Position::new(1, 0),
10960 ),
10961 new_text: "\n".into(),
10962 },
10963 lsp::TextEdit {
10964 range: lsp::Range::new(
10965 lsp::Position::new(2, 0),
10966 lsp::Position::new(2, 0),
10967 ),
10968 new_text: "\n".into(),
10969 },
10970 ]))
10971 }
10972 }
10973 });
10974
10975 // After formatting the buffer, the trailing whitespace is stripped,
10976 // a newline is appended, and the edits provided by the language server
10977 // have been applied.
10978 format.await.unwrap();
10979 cx.assert_editor_state(
10980 &[
10981 "one", //
10982 "", //
10983 "twoˇ", //
10984 "", //
10985 "three", //
10986 "four", //
10987 "", //
10988 ]
10989 .join("\n"),
10990 );
10991
10992 // Undoing the formatting undoes the trailing whitespace removal, the
10993 // trailing newline, and the LSP edits.
10994 cx.update_buffer(|buffer, cx| buffer.undo(cx));
10995 cx.assert_editor_state(
10996 &[
10997 "one ", //
10998 "twoˇ", //
10999 "three ", //
11000 "four", //
11001 ]
11002 .join("\n"),
11003 );
11004}
11005
11006#[gpui::test]
11007async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
11008 cx: &mut TestAppContext,
11009) {
11010 init_test(cx, |_| {});
11011
11012 cx.update(|cx| {
11013 cx.update_global::<SettingsStore, _>(|settings, cx| {
11014 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11015 settings.auto_signature_help = Some(true);
11016 });
11017 });
11018 });
11019
11020 let mut cx = EditorLspTestContext::new_rust(
11021 lsp::ServerCapabilities {
11022 signature_help_provider: Some(lsp::SignatureHelpOptions {
11023 ..Default::default()
11024 }),
11025 ..Default::default()
11026 },
11027 cx,
11028 )
11029 .await;
11030
11031 let language = Language::new(
11032 LanguageConfig {
11033 name: "Rust".into(),
11034 brackets: BracketPairConfig {
11035 pairs: vec![
11036 BracketPair {
11037 start: "{".to_string(),
11038 end: "}".to_string(),
11039 close: true,
11040 surround: true,
11041 newline: true,
11042 },
11043 BracketPair {
11044 start: "(".to_string(),
11045 end: ")".to_string(),
11046 close: true,
11047 surround: true,
11048 newline: true,
11049 },
11050 BracketPair {
11051 start: "/*".to_string(),
11052 end: " */".to_string(),
11053 close: true,
11054 surround: true,
11055 newline: true,
11056 },
11057 BracketPair {
11058 start: "[".to_string(),
11059 end: "]".to_string(),
11060 close: false,
11061 surround: false,
11062 newline: true,
11063 },
11064 BracketPair {
11065 start: "\"".to_string(),
11066 end: "\"".to_string(),
11067 close: true,
11068 surround: true,
11069 newline: false,
11070 },
11071 BracketPair {
11072 start: "<".to_string(),
11073 end: ">".to_string(),
11074 close: false,
11075 surround: true,
11076 newline: true,
11077 },
11078 ],
11079 ..Default::default()
11080 },
11081 autoclose_before: "})]".to_string(),
11082 ..Default::default()
11083 },
11084 Some(tree_sitter_rust::LANGUAGE.into()),
11085 );
11086 let language = Arc::new(language);
11087
11088 cx.language_registry().add(language.clone());
11089 cx.update_buffer(|buffer, cx| {
11090 buffer.set_language(Some(language), cx);
11091 });
11092
11093 cx.set_state(
11094 &r#"
11095 fn main() {
11096 sampleˇ
11097 }
11098 "#
11099 .unindent(),
11100 );
11101
11102 cx.update_editor(|editor, window, cx| {
11103 editor.handle_input("(", window, cx);
11104 });
11105 cx.assert_editor_state(
11106 &"
11107 fn main() {
11108 sample(ˇ)
11109 }
11110 "
11111 .unindent(),
11112 );
11113
11114 let mocked_response = lsp::SignatureHelp {
11115 signatures: vec![lsp::SignatureInformation {
11116 label: "fn sample(param1: u8, param2: u8)".to_string(),
11117 documentation: None,
11118 parameters: Some(vec![
11119 lsp::ParameterInformation {
11120 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11121 documentation: None,
11122 },
11123 lsp::ParameterInformation {
11124 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11125 documentation: None,
11126 },
11127 ]),
11128 active_parameter: None,
11129 }],
11130 active_signature: Some(0),
11131 active_parameter: Some(0),
11132 };
11133 handle_signature_help_request(&mut cx, mocked_response).await;
11134
11135 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11136 .await;
11137
11138 cx.editor(|editor, _, _| {
11139 let signature_help_state = editor.signature_help_state.popover().cloned();
11140 let signature = signature_help_state.unwrap();
11141 assert_eq!(
11142 signature.signatures[signature.current_signature].label,
11143 "fn sample(param1: u8, param2: u8)"
11144 );
11145 });
11146}
11147
11148#[gpui::test]
11149async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
11150 init_test(cx, |_| {});
11151
11152 cx.update(|cx| {
11153 cx.update_global::<SettingsStore, _>(|settings, cx| {
11154 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11155 settings.auto_signature_help = Some(false);
11156 settings.show_signature_help_after_edits = Some(false);
11157 });
11158 });
11159 });
11160
11161 let mut cx = EditorLspTestContext::new_rust(
11162 lsp::ServerCapabilities {
11163 signature_help_provider: Some(lsp::SignatureHelpOptions {
11164 ..Default::default()
11165 }),
11166 ..Default::default()
11167 },
11168 cx,
11169 )
11170 .await;
11171
11172 let language = Language::new(
11173 LanguageConfig {
11174 name: "Rust".into(),
11175 brackets: BracketPairConfig {
11176 pairs: vec![
11177 BracketPair {
11178 start: "{".to_string(),
11179 end: "}".to_string(),
11180 close: true,
11181 surround: true,
11182 newline: true,
11183 },
11184 BracketPair {
11185 start: "(".to_string(),
11186 end: ")".to_string(),
11187 close: true,
11188 surround: true,
11189 newline: true,
11190 },
11191 BracketPair {
11192 start: "/*".to_string(),
11193 end: " */".to_string(),
11194 close: true,
11195 surround: true,
11196 newline: true,
11197 },
11198 BracketPair {
11199 start: "[".to_string(),
11200 end: "]".to_string(),
11201 close: false,
11202 surround: false,
11203 newline: true,
11204 },
11205 BracketPair {
11206 start: "\"".to_string(),
11207 end: "\"".to_string(),
11208 close: true,
11209 surround: true,
11210 newline: false,
11211 },
11212 BracketPair {
11213 start: "<".to_string(),
11214 end: ">".to_string(),
11215 close: false,
11216 surround: true,
11217 newline: true,
11218 },
11219 ],
11220 ..Default::default()
11221 },
11222 autoclose_before: "})]".to_string(),
11223 ..Default::default()
11224 },
11225 Some(tree_sitter_rust::LANGUAGE.into()),
11226 );
11227 let language = Arc::new(language);
11228
11229 cx.language_registry().add(language.clone());
11230 cx.update_buffer(|buffer, cx| {
11231 buffer.set_language(Some(language), cx);
11232 });
11233
11234 // Ensure that signature_help is not called when no signature help is enabled.
11235 cx.set_state(
11236 &r#"
11237 fn main() {
11238 sampleˇ
11239 }
11240 "#
11241 .unindent(),
11242 );
11243 cx.update_editor(|editor, window, cx| {
11244 editor.handle_input("(", window, cx);
11245 });
11246 cx.assert_editor_state(
11247 &"
11248 fn main() {
11249 sample(ˇ)
11250 }
11251 "
11252 .unindent(),
11253 );
11254 cx.editor(|editor, _, _| {
11255 assert!(editor.signature_help_state.task().is_none());
11256 });
11257
11258 let mocked_response = lsp::SignatureHelp {
11259 signatures: vec![lsp::SignatureInformation {
11260 label: "fn sample(param1: u8, param2: u8)".to_string(),
11261 documentation: None,
11262 parameters: Some(vec![
11263 lsp::ParameterInformation {
11264 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11265 documentation: None,
11266 },
11267 lsp::ParameterInformation {
11268 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11269 documentation: None,
11270 },
11271 ]),
11272 active_parameter: None,
11273 }],
11274 active_signature: Some(0),
11275 active_parameter: Some(0),
11276 };
11277
11278 // Ensure that signature_help is called when enabled afte edits
11279 cx.update(|_, cx| {
11280 cx.update_global::<SettingsStore, _>(|settings, cx| {
11281 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11282 settings.auto_signature_help = Some(false);
11283 settings.show_signature_help_after_edits = Some(true);
11284 });
11285 });
11286 });
11287 cx.set_state(
11288 &r#"
11289 fn main() {
11290 sampleˇ
11291 }
11292 "#
11293 .unindent(),
11294 );
11295 cx.update_editor(|editor, window, cx| {
11296 editor.handle_input("(", window, cx);
11297 });
11298 cx.assert_editor_state(
11299 &"
11300 fn main() {
11301 sample(ˇ)
11302 }
11303 "
11304 .unindent(),
11305 );
11306 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11307 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11308 .await;
11309 cx.update_editor(|editor, _, _| {
11310 let signature_help_state = editor.signature_help_state.popover().cloned();
11311 assert!(signature_help_state.is_some());
11312 let signature = signature_help_state.unwrap();
11313 assert_eq!(
11314 signature.signatures[signature.current_signature].label,
11315 "fn sample(param1: u8, param2: u8)"
11316 );
11317 editor.signature_help_state = SignatureHelpState::default();
11318 });
11319
11320 // Ensure that signature_help is called when auto signature help override is enabled
11321 cx.update(|_, cx| {
11322 cx.update_global::<SettingsStore, _>(|settings, cx| {
11323 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11324 settings.auto_signature_help = Some(true);
11325 settings.show_signature_help_after_edits = Some(false);
11326 });
11327 });
11328 });
11329 cx.set_state(
11330 &r#"
11331 fn main() {
11332 sampleˇ
11333 }
11334 "#
11335 .unindent(),
11336 );
11337 cx.update_editor(|editor, window, cx| {
11338 editor.handle_input("(", window, cx);
11339 });
11340 cx.assert_editor_state(
11341 &"
11342 fn main() {
11343 sample(ˇ)
11344 }
11345 "
11346 .unindent(),
11347 );
11348 handle_signature_help_request(&mut cx, mocked_response).await;
11349 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11350 .await;
11351 cx.editor(|editor, _, _| {
11352 let signature_help_state = editor.signature_help_state.popover().cloned();
11353 assert!(signature_help_state.is_some());
11354 let signature = signature_help_state.unwrap();
11355 assert_eq!(
11356 signature.signatures[signature.current_signature].label,
11357 "fn sample(param1: u8, param2: u8)"
11358 );
11359 });
11360}
11361
11362#[gpui::test]
11363async fn test_signature_help(cx: &mut TestAppContext) {
11364 init_test(cx, |_| {});
11365 cx.update(|cx| {
11366 cx.update_global::<SettingsStore, _>(|settings, cx| {
11367 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11368 settings.auto_signature_help = Some(true);
11369 });
11370 });
11371 });
11372
11373 let mut cx = EditorLspTestContext::new_rust(
11374 lsp::ServerCapabilities {
11375 signature_help_provider: Some(lsp::SignatureHelpOptions {
11376 ..Default::default()
11377 }),
11378 ..Default::default()
11379 },
11380 cx,
11381 )
11382 .await;
11383
11384 // A test that directly calls `show_signature_help`
11385 cx.update_editor(|editor, window, cx| {
11386 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11387 });
11388
11389 let mocked_response = lsp::SignatureHelp {
11390 signatures: vec![lsp::SignatureInformation {
11391 label: "fn sample(param1: u8, param2: u8)".to_string(),
11392 documentation: None,
11393 parameters: Some(vec![
11394 lsp::ParameterInformation {
11395 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11396 documentation: None,
11397 },
11398 lsp::ParameterInformation {
11399 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11400 documentation: None,
11401 },
11402 ]),
11403 active_parameter: None,
11404 }],
11405 active_signature: Some(0),
11406 active_parameter: Some(0),
11407 };
11408 handle_signature_help_request(&mut cx, mocked_response).await;
11409
11410 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11411 .await;
11412
11413 cx.editor(|editor, _, _| {
11414 let signature_help_state = editor.signature_help_state.popover().cloned();
11415 assert!(signature_help_state.is_some());
11416 let signature = signature_help_state.unwrap();
11417 assert_eq!(
11418 signature.signatures[signature.current_signature].label,
11419 "fn sample(param1: u8, param2: u8)"
11420 );
11421 });
11422
11423 // When exiting outside from inside the brackets, `signature_help` is closed.
11424 cx.set_state(indoc! {"
11425 fn main() {
11426 sample(ˇ);
11427 }
11428
11429 fn sample(param1: u8, param2: u8) {}
11430 "});
11431
11432 cx.update_editor(|editor, window, cx| {
11433 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11434 s.select_ranges([0..0])
11435 });
11436 });
11437
11438 let mocked_response = lsp::SignatureHelp {
11439 signatures: Vec::new(),
11440 active_signature: None,
11441 active_parameter: None,
11442 };
11443 handle_signature_help_request(&mut cx, mocked_response).await;
11444
11445 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11446 .await;
11447
11448 cx.editor(|editor, _, _| {
11449 assert!(!editor.signature_help_state.is_shown());
11450 });
11451
11452 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
11453 cx.set_state(indoc! {"
11454 fn main() {
11455 sample(ˇ);
11456 }
11457
11458 fn sample(param1: u8, param2: u8) {}
11459 "});
11460
11461 let mocked_response = lsp::SignatureHelp {
11462 signatures: vec![lsp::SignatureInformation {
11463 label: "fn sample(param1: u8, param2: u8)".to_string(),
11464 documentation: None,
11465 parameters: Some(vec![
11466 lsp::ParameterInformation {
11467 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11468 documentation: None,
11469 },
11470 lsp::ParameterInformation {
11471 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11472 documentation: None,
11473 },
11474 ]),
11475 active_parameter: None,
11476 }],
11477 active_signature: Some(0),
11478 active_parameter: Some(0),
11479 };
11480 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11481 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11482 .await;
11483 cx.editor(|editor, _, _| {
11484 assert!(editor.signature_help_state.is_shown());
11485 });
11486
11487 // Restore the popover with more parameter input
11488 cx.set_state(indoc! {"
11489 fn main() {
11490 sample(param1, param2ˇ);
11491 }
11492
11493 fn sample(param1: u8, param2: u8) {}
11494 "});
11495
11496 let mocked_response = lsp::SignatureHelp {
11497 signatures: vec![lsp::SignatureInformation {
11498 label: "fn sample(param1: u8, param2: u8)".to_string(),
11499 documentation: None,
11500 parameters: Some(vec![
11501 lsp::ParameterInformation {
11502 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11503 documentation: None,
11504 },
11505 lsp::ParameterInformation {
11506 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11507 documentation: None,
11508 },
11509 ]),
11510 active_parameter: None,
11511 }],
11512 active_signature: Some(0),
11513 active_parameter: Some(1),
11514 };
11515 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11516 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11517 .await;
11518
11519 // When selecting a range, the popover is gone.
11520 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
11521 cx.update_editor(|editor, window, cx| {
11522 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11523 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11524 })
11525 });
11526 cx.assert_editor_state(indoc! {"
11527 fn main() {
11528 sample(param1, «ˇparam2»);
11529 }
11530
11531 fn sample(param1: u8, param2: u8) {}
11532 "});
11533 cx.editor(|editor, _, _| {
11534 assert!(!editor.signature_help_state.is_shown());
11535 });
11536
11537 // When unselecting again, the popover is back if within the brackets.
11538 cx.update_editor(|editor, window, cx| {
11539 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11540 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11541 })
11542 });
11543 cx.assert_editor_state(indoc! {"
11544 fn main() {
11545 sample(param1, ˇparam2);
11546 }
11547
11548 fn sample(param1: u8, param2: u8) {}
11549 "});
11550 handle_signature_help_request(&mut cx, mocked_response).await;
11551 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11552 .await;
11553 cx.editor(|editor, _, _| {
11554 assert!(editor.signature_help_state.is_shown());
11555 });
11556
11557 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
11558 cx.update_editor(|editor, window, cx| {
11559 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11560 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
11561 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11562 })
11563 });
11564 cx.assert_editor_state(indoc! {"
11565 fn main() {
11566 sample(param1, ˇparam2);
11567 }
11568
11569 fn sample(param1: u8, param2: u8) {}
11570 "});
11571
11572 let mocked_response = lsp::SignatureHelp {
11573 signatures: vec![lsp::SignatureInformation {
11574 label: "fn sample(param1: u8, param2: u8)".to_string(),
11575 documentation: None,
11576 parameters: Some(vec![
11577 lsp::ParameterInformation {
11578 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11579 documentation: None,
11580 },
11581 lsp::ParameterInformation {
11582 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11583 documentation: None,
11584 },
11585 ]),
11586 active_parameter: None,
11587 }],
11588 active_signature: Some(0),
11589 active_parameter: Some(1),
11590 };
11591 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11592 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11593 .await;
11594 cx.update_editor(|editor, _, cx| {
11595 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
11596 });
11597 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11598 .await;
11599 cx.update_editor(|editor, window, cx| {
11600 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11601 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11602 })
11603 });
11604 cx.assert_editor_state(indoc! {"
11605 fn main() {
11606 sample(param1, «ˇparam2»);
11607 }
11608
11609 fn sample(param1: u8, param2: u8) {}
11610 "});
11611 cx.update_editor(|editor, window, cx| {
11612 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11613 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11614 })
11615 });
11616 cx.assert_editor_state(indoc! {"
11617 fn main() {
11618 sample(param1, ˇparam2);
11619 }
11620
11621 fn sample(param1: u8, param2: u8) {}
11622 "});
11623 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
11624 .await;
11625}
11626
11627#[gpui::test]
11628async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
11629 init_test(cx, |_| {});
11630
11631 let mut cx = EditorLspTestContext::new_rust(
11632 lsp::ServerCapabilities {
11633 signature_help_provider: Some(lsp::SignatureHelpOptions {
11634 ..Default::default()
11635 }),
11636 ..Default::default()
11637 },
11638 cx,
11639 )
11640 .await;
11641
11642 cx.set_state(indoc! {"
11643 fn main() {
11644 overloadedˇ
11645 }
11646 "});
11647
11648 cx.update_editor(|editor, window, cx| {
11649 editor.handle_input("(", window, cx);
11650 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11651 });
11652
11653 // Mock response with 3 signatures
11654 let mocked_response = lsp::SignatureHelp {
11655 signatures: vec![
11656 lsp::SignatureInformation {
11657 label: "fn overloaded(x: i32)".to_string(),
11658 documentation: None,
11659 parameters: Some(vec![lsp::ParameterInformation {
11660 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11661 documentation: None,
11662 }]),
11663 active_parameter: None,
11664 },
11665 lsp::SignatureInformation {
11666 label: "fn overloaded(x: i32, y: i32)".to_string(),
11667 documentation: None,
11668 parameters: Some(vec![
11669 lsp::ParameterInformation {
11670 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11671 documentation: None,
11672 },
11673 lsp::ParameterInformation {
11674 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11675 documentation: None,
11676 },
11677 ]),
11678 active_parameter: None,
11679 },
11680 lsp::SignatureInformation {
11681 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
11682 documentation: None,
11683 parameters: Some(vec![
11684 lsp::ParameterInformation {
11685 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11686 documentation: None,
11687 },
11688 lsp::ParameterInformation {
11689 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11690 documentation: None,
11691 },
11692 lsp::ParameterInformation {
11693 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
11694 documentation: None,
11695 },
11696 ]),
11697 active_parameter: None,
11698 },
11699 ],
11700 active_signature: Some(1),
11701 active_parameter: Some(0),
11702 };
11703 handle_signature_help_request(&mut cx, mocked_response).await;
11704
11705 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11706 .await;
11707
11708 // Verify we have multiple signatures and the right one is selected
11709 cx.editor(|editor, _, _| {
11710 let popover = editor.signature_help_state.popover().cloned().unwrap();
11711 assert_eq!(popover.signatures.len(), 3);
11712 // active_signature was 1, so that should be the current
11713 assert_eq!(popover.current_signature, 1);
11714 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
11715 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
11716 assert_eq!(
11717 popover.signatures[2].label,
11718 "fn overloaded(x: i32, y: i32, z: i32)"
11719 );
11720 });
11721
11722 // Test navigation functionality
11723 cx.update_editor(|editor, window, cx| {
11724 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11725 });
11726
11727 cx.editor(|editor, _, _| {
11728 let popover = editor.signature_help_state.popover().cloned().unwrap();
11729 assert_eq!(popover.current_signature, 2);
11730 });
11731
11732 // Test wrap around
11733 cx.update_editor(|editor, window, cx| {
11734 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11735 });
11736
11737 cx.editor(|editor, _, _| {
11738 let popover = editor.signature_help_state.popover().cloned().unwrap();
11739 assert_eq!(popover.current_signature, 0);
11740 });
11741
11742 // Test previous navigation
11743 cx.update_editor(|editor, window, cx| {
11744 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
11745 });
11746
11747 cx.editor(|editor, _, _| {
11748 let popover = editor.signature_help_state.popover().cloned().unwrap();
11749 assert_eq!(popover.current_signature, 2);
11750 });
11751}
11752
11753#[gpui::test]
11754async fn test_completion_mode(cx: &mut TestAppContext) {
11755 init_test(cx, |_| {});
11756 let mut cx = EditorLspTestContext::new_rust(
11757 lsp::ServerCapabilities {
11758 completion_provider: Some(lsp::CompletionOptions {
11759 resolve_provider: Some(true),
11760 ..Default::default()
11761 }),
11762 ..Default::default()
11763 },
11764 cx,
11765 )
11766 .await;
11767
11768 struct Run {
11769 run_description: &'static str,
11770 initial_state: String,
11771 buffer_marked_text: String,
11772 completion_label: &'static str,
11773 completion_text: &'static str,
11774 expected_with_insert_mode: String,
11775 expected_with_replace_mode: String,
11776 expected_with_replace_subsequence_mode: String,
11777 expected_with_replace_suffix_mode: String,
11778 }
11779
11780 let runs = [
11781 Run {
11782 run_description: "Start of word matches completion text",
11783 initial_state: "before ediˇ after".into(),
11784 buffer_marked_text: "before <edi|> after".into(),
11785 completion_label: "editor",
11786 completion_text: "editor",
11787 expected_with_insert_mode: "before editorˇ after".into(),
11788 expected_with_replace_mode: "before editorˇ after".into(),
11789 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11790 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11791 },
11792 Run {
11793 run_description: "Accept same text at the middle of the word",
11794 initial_state: "before ediˇtor after".into(),
11795 buffer_marked_text: "before <edi|tor> after".into(),
11796 completion_label: "editor",
11797 completion_text: "editor",
11798 expected_with_insert_mode: "before editorˇtor after".into(),
11799 expected_with_replace_mode: "before editorˇ after".into(),
11800 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11801 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11802 },
11803 Run {
11804 run_description: "End of word matches completion text -- cursor at end",
11805 initial_state: "before torˇ after".into(),
11806 buffer_marked_text: "before <tor|> after".into(),
11807 completion_label: "editor",
11808 completion_text: "editor",
11809 expected_with_insert_mode: "before editorˇ after".into(),
11810 expected_with_replace_mode: "before editorˇ after".into(),
11811 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11812 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11813 },
11814 Run {
11815 run_description: "End of word matches completion text -- cursor at start",
11816 initial_state: "before ˇtor after".into(),
11817 buffer_marked_text: "before <|tor> after".into(),
11818 completion_label: "editor",
11819 completion_text: "editor",
11820 expected_with_insert_mode: "before editorˇtor 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: "Prepend text containing whitespace",
11827 initial_state: "pˇfield: bool".into(),
11828 buffer_marked_text: "<p|field>: bool".into(),
11829 completion_label: "pub ",
11830 completion_text: "pub ",
11831 expected_with_insert_mode: "pub ˇfield: bool".into(),
11832 expected_with_replace_mode: "pub ˇ: bool".into(),
11833 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
11834 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
11835 },
11836 Run {
11837 run_description: "Add element to start of list",
11838 initial_state: "[element_ˇelement_2]".into(),
11839 buffer_marked_text: "[<element_|element_2>]".into(),
11840 completion_label: "element_1",
11841 completion_text: "element_1",
11842 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
11843 expected_with_replace_mode: "[element_1ˇ]".into(),
11844 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
11845 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
11846 },
11847 Run {
11848 run_description: "Add element to start of list -- first and second elements are equal",
11849 initial_state: "[elˇelement]".into(),
11850 buffer_marked_text: "[<el|element>]".into(),
11851 completion_label: "element",
11852 completion_text: "element",
11853 expected_with_insert_mode: "[elementˇelement]".into(),
11854 expected_with_replace_mode: "[elementˇ]".into(),
11855 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
11856 expected_with_replace_suffix_mode: "[elementˇ]".into(),
11857 },
11858 Run {
11859 run_description: "Ends with matching suffix",
11860 initial_state: "SubˇError".into(),
11861 buffer_marked_text: "<Sub|Error>".into(),
11862 completion_label: "SubscriptionError",
11863 completion_text: "SubscriptionError",
11864 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
11865 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11866 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11867 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
11868 },
11869 Run {
11870 run_description: "Suffix is a subsequence -- contiguous",
11871 initial_state: "SubˇErr".into(),
11872 buffer_marked_text: "<Sub|Err>".into(),
11873 completion_label: "SubscriptionError",
11874 completion_text: "SubscriptionError",
11875 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
11876 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11877 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11878 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
11879 },
11880 Run {
11881 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
11882 initial_state: "Suˇscrirr".into(),
11883 buffer_marked_text: "<Su|scrirr>".into(),
11884 completion_label: "SubscriptionError",
11885 completion_text: "SubscriptionError",
11886 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
11887 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11888 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11889 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
11890 },
11891 Run {
11892 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
11893 initial_state: "foo(indˇix)".into(),
11894 buffer_marked_text: "foo(<ind|ix>)".into(),
11895 completion_label: "node_index",
11896 completion_text: "node_index",
11897 expected_with_insert_mode: "foo(node_indexˇix)".into(),
11898 expected_with_replace_mode: "foo(node_indexˇ)".into(),
11899 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
11900 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
11901 },
11902 Run {
11903 run_description: "Replace range ends before cursor - should extend to cursor",
11904 initial_state: "before editˇo after".into(),
11905 buffer_marked_text: "before <{ed}>it|o after".into(),
11906 completion_label: "editor",
11907 completion_text: "editor",
11908 expected_with_insert_mode: "before editorˇo after".into(),
11909 expected_with_replace_mode: "before editorˇo after".into(),
11910 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
11911 expected_with_replace_suffix_mode: "before editorˇo after".into(),
11912 },
11913 Run {
11914 run_description: "Uses label for suffix matching",
11915 initial_state: "before ediˇtor after".into(),
11916 buffer_marked_text: "before <edi|tor> after".into(),
11917 completion_label: "editor",
11918 completion_text: "editor()",
11919 expected_with_insert_mode: "before editor()ˇtor after".into(),
11920 expected_with_replace_mode: "before editor()ˇ after".into(),
11921 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
11922 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
11923 },
11924 Run {
11925 run_description: "Case insensitive subsequence and suffix matching",
11926 initial_state: "before EDiˇtoR after".into(),
11927 buffer_marked_text: "before <EDi|toR> after".into(),
11928 completion_label: "editor",
11929 completion_text: "editor",
11930 expected_with_insert_mode: "before editorˇtoR after".into(),
11931 expected_with_replace_mode: "before editorˇ after".into(),
11932 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11933 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11934 },
11935 ];
11936
11937 for run in runs {
11938 let run_variations = [
11939 (LspInsertMode::Insert, run.expected_with_insert_mode),
11940 (LspInsertMode::Replace, run.expected_with_replace_mode),
11941 (
11942 LspInsertMode::ReplaceSubsequence,
11943 run.expected_with_replace_subsequence_mode,
11944 ),
11945 (
11946 LspInsertMode::ReplaceSuffix,
11947 run.expected_with_replace_suffix_mode,
11948 ),
11949 ];
11950
11951 for (lsp_insert_mode, expected_text) in run_variations {
11952 eprintln!(
11953 "run = {:?}, mode = {lsp_insert_mode:.?}",
11954 run.run_description,
11955 );
11956
11957 update_test_language_settings(&mut cx, |settings| {
11958 settings.defaults.completions = Some(CompletionSettings {
11959 lsp_insert_mode,
11960 words: WordsCompletionMode::Disabled,
11961 lsp: true,
11962 lsp_fetch_timeout_ms: 0,
11963 });
11964 });
11965
11966 cx.set_state(&run.initial_state);
11967 cx.update_editor(|editor, window, cx| {
11968 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11969 });
11970
11971 let counter = Arc::new(AtomicUsize::new(0));
11972 handle_completion_request_with_insert_and_replace(
11973 &mut cx,
11974 &run.buffer_marked_text,
11975 vec![(run.completion_label, run.completion_text)],
11976 counter.clone(),
11977 )
11978 .await;
11979 cx.condition(|editor, _| editor.context_menu_visible())
11980 .await;
11981 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11982
11983 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11984 editor
11985 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11986 .unwrap()
11987 });
11988 cx.assert_editor_state(&expected_text);
11989 handle_resolve_completion_request(&mut cx, None).await;
11990 apply_additional_edits.await.unwrap();
11991 }
11992 }
11993}
11994
11995#[gpui::test]
11996async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
11997 init_test(cx, |_| {});
11998 let mut cx = EditorLspTestContext::new_rust(
11999 lsp::ServerCapabilities {
12000 completion_provider: Some(lsp::CompletionOptions {
12001 resolve_provider: Some(true),
12002 ..Default::default()
12003 }),
12004 ..Default::default()
12005 },
12006 cx,
12007 )
12008 .await;
12009
12010 let initial_state = "SubˇError";
12011 let buffer_marked_text = "<Sub|Error>";
12012 let completion_text = "SubscriptionError";
12013 let expected_with_insert_mode = "SubscriptionErrorˇError";
12014 let expected_with_replace_mode = "SubscriptionErrorˇ";
12015
12016 update_test_language_settings(&mut cx, |settings| {
12017 settings.defaults.completions = Some(CompletionSettings {
12018 words: WordsCompletionMode::Disabled,
12019 // set the opposite here to ensure that the action is overriding the default behavior
12020 lsp_insert_mode: LspInsertMode::Insert,
12021 lsp: true,
12022 lsp_fetch_timeout_ms: 0,
12023 });
12024 });
12025
12026 cx.set_state(initial_state);
12027 cx.update_editor(|editor, window, cx| {
12028 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12029 });
12030
12031 let counter = Arc::new(AtomicUsize::new(0));
12032 handle_completion_request_with_insert_and_replace(
12033 &mut cx,
12034 &buffer_marked_text,
12035 vec![(completion_text, completion_text)],
12036 counter.clone(),
12037 )
12038 .await;
12039 cx.condition(|editor, _| editor.context_menu_visible())
12040 .await;
12041 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12042
12043 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12044 editor
12045 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12046 .unwrap()
12047 });
12048 cx.assert_editor_state(&expected_with_replace_mode);
12049 handle_resolve_completion_request(&mut cx, None).await;
12050 apply_additional_edits.await.unwrap();
12051
12052 update_test_language_settings(&mut cx, |settings| {
12053 settings.defaults.completions = Some(CompletionSettings {
12054 words: WordsCompletionMode::Disabled,
12055 // set the opposite here to ensure that the action is overriding the default behavior
12056 lsp_insert_mode: LspInsertMode::Replace,
12057 lsp: true,
12058 lsp_fetch_timeout_ms: 0,
12059 });
12060 });
12061
12062 cx.set_state(initial_state);
12063 cx.update_editor(|editor, window, cx| {
12064 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12065 });
12066 handle_completion_request_with_insert_and_replace(
12067 &mut cx,
12068 &buffer_marked_text,
12069 vec![(completion_text, completion_text)],
12070 counter.clone(),
12071 )
12072 .await;
12073 cx.condition(|editor, _| editor.context_menu_visible())
12074 .await;
12075 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12076
12077 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12078 editor
12079 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
12080 .unwrap()
12081 });
12082 cx.assert_editor_state(&expected_with_insert_mode);
12083 handle_resolve_completion_request(&mut cx, None).await;
12084 apply_additional_edits.await.unwrap();
12085}
12086
12087#[gpui::test]
12088async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
12089 init_test(cx, |_| {});
12090 let mut cx = EditorLspTestContext::new_rust(
12091 lsp::ServerCapabilities {
12092 completion_provider: Some(lsp::CompletionOptions {
12093 resolve_provider: Some(true),
12094 ..Default::default()
12095 }),
12096 ..Default::default()
12097 },
12098 cx,
12099 )
12100 .await;
12101
12102 // scenario: surrounding text matches completion text
12103 let completion_text = "to_offset";
12104 let initial_state = indoc! {"
12105 1. buf.to_offˇsuffix
12106 2. buf.to_offˇsuf
12107 3. buf.to_offˇfix
12108 4. buf.to_offˇ
12109 5. into_offˇensive
12110 6. ˇsuffix
12111 7. let ˇ //
12112 8. aaˇzz
12113 9. buf.to_off«zzzzzˇ»suffix
12114 10. buf.«ˇzzzzz»suffix
12115 11. to_off«ˇzzzzz»
12116
12117 buf.to_offˇsuffix // newest cursor
12118 "};
12119 let completion_marked_buffer = indoc! {"
12120 1. buf.to_offsuffix
12121 2. buf.to_offsuf
12122 3. buf.to_offfix
12123 4. buf.to_off
12124 5. into_offensive
12125 6. suffix
12126 7. let //
12127 8. aazz
12128 9. buf.to_offzzzzzsuffix
12129 10. buf.zzzzzsuffix
12130 11. to_offzzzzz
12131
12132 buf.<to_off|suffix> // newest cursor
12133 "};
12134 let expected = indoc! {"
12135 1. buf.to_offsetˇ
12136 2. buf.to_offsetˇsuf
12137 3. buf.to_offsetˇfix
12138 4. buf.to_offsetˇ
12139 5. into_offsetˇensive
12140 6. to_offsetˇsuffix
12141 7. let to_offsetˇ //
12142 8. aato_offsetˇzz
12143 9. buf.to_offsetˇ
12144 10. buf.to_offsetˇsuffix
12145 11. to_offsetˇ
12146
12147 buf.to_offsetˇ // newest cursor
12148 "};
12149 cx.set_state(initial_state);
12150 cx.update_editor(|editor, window, cx| {
12151 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12152 });
12153 handle_completion_request_with_insert_and_replace(
12154 &mut cx,
12155 completion_marked_buffer,
12156 vec![(completion_text, completion_text)],
12157 Arc::new(AtomicUsize::new(0)),
12158 )
12159 .await;
12160 cx.condition(|editor, _| editor.context_menu_visible())
12161 .await;
12162 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12163 editor
12164 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12165 .unwrap()
12166 });
12167 cx.assert_editor_state(expected);
12168 handle_resolve_completion_request(&mut cx, None).await;
12169 apply_additional_edits.await.unwrap();
12170
12171 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
12172 let completion_text = "foo_and_bar";
12173 let initial_state = indoc! {"
12174 1. ooanbˇ
12175 2. zooanbˇ
12176 3. ooanbˇz
12177 4. zooanbˇz
12178 5. ooanˇ
12179 6. oanbˇ
12180
12181 ooanbˇ
12182 "};
12183 let completion_marked_buffer = indoc! {"
12184 1. ooanb
12185 2. zooanb
12186 3. ooanbz
12187 4. zooanbz
12188 5. ooan
12189 6. oanb
12190
12191 <ooanb|>
12192 "};
12193 let expected = indoc! {"
12194 1. foo_and_barˇ
12195 2. zfoo_and_barˇ
12196 3. foo_and_barˇz
12197 4. zfoo_and_barˇz
12198 5. ooanfoo_and_barˇ
12199 6. oanbfoo_and_barˇ
12200
12201 foo_and_barˇ
12202 "};
12203 cx.set_state(initial_state);
12204 cx.update_editor(|editor, window, cx| {
12205 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12206 });
12207 handle_completion_request_with_insert_and_replace(
12208 &mut cx,
12209 completion_marked_buffer,
12210 vec![(completion_text, completion_text)],
12211 Arc::new(AtomicUsize::new(0)),
12212 )
12213 .await;
12214 cx.condition(|editor, _| editor.context_menu_visible())
12215 .await;
12216 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12217 editor
12218 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12219 .unwrap()
12220 });
12221 cx.assert_editor_state(expected);
12222 handle_resolve_completion_request(&mut cx, None).await;
12223 apply_additional_edits.await.unwrap();
12224
12225 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
12226 // (expects the same as if it was inserted at the end)
12227 let completion_text = "foo_and_bar";
12228 let initial_state = indoc! {"
12229 1. ooˇanb
12230 2. zooˇanb
12231 3. ooˇanbz
12232 4. zooˇanbz
12233
12234 ooˇanb
12235 "};
12236 let completion_marked_buffer = indoc! {"
12237 1. ooanb
12238 2. zooanb
12239 3. ooanbz
12240 4. zooanbz
12241
12242 <oo|anb>
12243 "};
12244 let expected = indoc! {"
12245 1. foo_and_barˇ
12246 2. zfoo_and_barˇ
12247 3. foo_and_barˇz
12248 4. zfoo_and_barˇz
12249
12250 foo_and_barˇ
12251 "};
12252 cx.set_state(initial_state);
12253 cx.update_editor(|editor, window, cx| {
12254 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12255 });
12256 handle_completion_request_with_insert_and_replace(
12257 &mut cx,
12258 completion_marked_buffer,
12259 vec![(completion_text, completion_text)],
12260 Arc::new(AtomicUsize::new(0)),
12261 )
12262 .await;
12263 cx.condition(|editor, _| editor.context_menu_visible())
12264 .await;
12265 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12266 editor
12267 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12268 .unwrap()
12269 });
12270 cx.assert_editor_state(expected);
12271 handle_resolve_completion_request(&mut cx, None).await;
12272 apply_additional_edits.await.unwrap();
12273}
12274
12275// This used to crash
12276#[gpui::test]
12277async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
12278 init_test(cx, |_| {});
12279
12280 let buffer_text = indoc! {"
12281 fn main() {
12282 10.satu;
12283
12284 //
12285 // separate cursors so they open in different excerpts (manually reproducible)
12286 //
12287
12288 10.satu20;
12289 }
12290 "};
12291 let multibuffer_text_with_selections = indoc! {"
12292 fn main() {
12293 10.satuˇ;
12294
12295 //
12296
12297 //
12298
12299 10.satuˇ20;
12300 }
12301 "};
12302 let expected_multibuffer = indoc! {"
12303 fn main() {
12304 10.saturating_sub()ˇ;
12305
12306 //
12307
12308 //
12309
12310 10.saturating_sub()ˇ;
12311 }
12312 "};
12313
12314 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
12315 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
12316
12317 let fs = FakeFs::new(cx.executor());
12318 fs.insert_tree(
12319 path!("/a"),
12320 json!({
12321 "main.rs": buffer_text,
12322 }),
12323 )
12324 .await;
12325
12326 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12327 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12328 language_registry.add(rust_lang());
12329 let mut fake_servers = language_registry.register_fake_lsp(
12330 "Rust",
12331 FakeLspAdapter {
12332 capabilities: lsp::ServerCapabilities {
12333 completion_provider: Some(lsp::CompletionOptions {
12334 resolve_provider: None,
12335 ..lsp::CompletionOptions::default()
12336 }),
12337 ..lsp::ServerCapabilities::default()
12338 },
12339 ..FakeLspAdapter::default()
12340 },
12341 );
12342 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12343 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12344 let buffer = project
12345 .update(cx, |project, cx| {
12346 project.open_local_buffer(path!("/a/main.rs"), cx)
12347 })
12348 .await
12349 .unwrap();
12350
12351 let multi_buffer = cx.new(|cx| {
12352 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
12353 multi_buffer.push_excerpts(
12354 buffer.clone(),
12355 [ExcerptRange::new(0..first_excerpt_end)],
12356 cx,
12357 );
12358 multi_buffer.push_excerpts(
12359 buffer.clone(),
12360 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
12361 cx,
12362 );
12363 multi_buffer
12364 });
12365
12366 let editor = workspace
12367 .update(cx, |_, window, cx| {
12368 cx.new(|cx| {
12369 Editor::new(
12370 EditorMode::Full {
12371 scale_ui_elements_with_buffer_font_size: false,
12372 show_active_line_background: false,
12373 sized_by_content: false,
12374 },
12375 multi_buffer.clone(),
12376 Some(project.clone()),
12377 window,
12378 cx,
12379 )
12380 })
12381 })
12382 .unwrap();
12383
12384 let pane = workspace
12385 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12386 .unwrap();
12387 pane.update_in(cx, |pane, window, cx| {
12388 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
12389 });
12390
12391 let fake_server = fake_servers.next().await.unwrap();
12392
12393 editor.update_in(cx, |editor, window, cx| {
12394 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12395 s.select_ranges([
12396 Point::new(1, 11)..Point::new(1, 11),
12397 Point::new(7, 11)..Point::new(7, 11),
12398 ])
12399 });
12400
12401 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
12402 });
12403
12404 editor.update_in(cx, |editor, window, cx| {
12405 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12406 });
12407
12408 fake_server
12409 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12410 let completion_item = lsp::CompletionItem {
12411 label: "saturating_sub()".into(),
12412 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
12413 lsp::InsertReplaceEdit {
12414 new_text: "saturating_sub()".to_owned(),
12415 insert: lsp::Range::new(
12416 lsp::Position::new(7, 7),
12417 lsp::Position::new(7, 11),
12418 ),
12419 replace: lsp::Range::new(
12420 lsp::Position::new(7, 7),
12421 lsp::Position::new(7, 13),
12422 ),
12423 },
12424 )),
12425 ..lsp::CompletionItem::default()
12426 };
12427
12428 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
12429 })
12430 .next()
12431 .await
12432 .unwrap();
12433
12434 cx.condition(&editor, |editor, _| editor.context_menu_visible())
12435 .await;
12436
12437 editor
12438 .update_in(cx, |editor, window, cx| {
12439 editor
12440 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12441 .unwrap()
12442 })
12443 .await
12444 .unwrap();
12445
12446 editor.update(cx, |editor, cx| {
12447 assert_text_with_selections(editor, expected_multibuffer, cx);
12448 })
12449}
12450
12451#[gpui::test]
12452async fn test_completion(cx: &mut TestAppContext) {
12453 init_test(cx, |_| {});
12454
12455 let mut cx = EditorLspTestContext::new_rust(
12456 lsp::ServerCapabilities {
12457 completion_provider: Some(lsp::CompletionOptions {
12458 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12459 resolve_provider: Some(true),
12460 ..Default::default()
12461 }),
12462 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12463 ..Default::default()
12464 },
12465 cx,
12466 )
12467 .await;
12468 let counter = Arc::new(AtomicUsize::new(0));
12469
12470 cx.set_state(indoc! {"
12471 oneˇ
12472 two
12473 three
12474 "});
12475 cx.simulate_keystroke(".");
12476 handle_completion_request(
12477 indoc! {"
12478 one.|<>
12479 two
12480 three
12481 "},
12482 vec!["first_completion", "second_completion"],
12483 true,
12484 counter.clone(),
12485 &mut cx,
12486 )
12487 .await;
12488 cx.condition(|editor, _| editor.context_menu_visible())
12489 .await;
12490 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12491
12492 let _handler = handle_signature_help_request(
12493 &mut cx,
12494 lsp::SignatureHelp {
12495 signatures: vec![lsp::SignatureInformation {
12496 label: "test signature".to_string(),
12497 documentation: None,
12498 parameters: Some(vec![lsp::ParameterInformation {
12499 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
12500 documentation: None,
12501 }]),
12502 active_parameter: None,
12503 }],
12504 active_signature: None,
12505 active_parameter: None,
12506 },
12507 );
12508 cx.update_editor(|editor, window, cx| {
12509 assert!(
12510 !editor.signature_help_state.is_shown(),
12511 "No signature help was called for"
12512 );
12513 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12514 });
12515 cx.run_until_parked();
12516 cx.update_editor(|editor, _, _| {
12517 assert!(
12518 !editor.signature_help_state.is_shown(),
12519 "No signature help should be shown when completions menu is open"
12520 );
12521 });
12522
12523 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12524 editor.context_menu_next(&Default::default(), window, cx);
12525 editor
12526 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12527 .unwrap()
12528 });
12529 cx.assert_editor_state(indoc! {"
12530 one.second_completionˇ
12531 two
12532 three
12533 "});
12534
12535 handle_resolve_completion_request(
12536 &mut cx,
12537 Some(vec![
12538 (
12539 //This overlaps with the primary completion edit which is
12540 //misbehavior from the LSP spec, test that we filter it out
12541 indoc! {"
12542 one.second_ˇcompletion
12543 two
12544 threeˇ
12545 "},
12546 "overlapping additional edit",
12547 ),
12548 (
12549 indoc! {"
12550 one.second_completion
12551 two
12552 threeˇ
12553 "},
12554 "\nadditional edit",
12555 ),
12556 ]),
12557 )
12558 .await;
12559 apply_additional_edits.await.unwrap();
12560 cx.assert_editor_state(indoc! {"
12561 one.second_completionˇ
12562 two
12563 three
12564 additional edit
12565 "});
12566
12567 cx.set_state(indoc! {"
12568 one.second_completion
12569 twoˇ
12570 threeˇ
12571 additional edit
12572 "});
12573 cx.simulate_keystroke(" ");
12574 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12575 cx.simulate_keystroke("s");
12576 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12577
12578 cx.assert_editor_state(indoc! {"
12579 one.second_completion
12580 two sˇ
12581 three sˇ
12582 additional edit
12583 "});
12584 handle_completion_request(
12585 indoc! {"
12586 one.second_completion
12587 two s
12588 three <s|>
12589 additional edit
12590 "},
12591 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12592 true,
12593 counter.clone(),
12594 &mut cx,
12595 )
12596 .await;
12597 cx.condition(|editor, _| editor.context_menu_visible())
12598 .await;
12599 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12600
12601 cx.simulate_keystroke("i");
12602
12603 handle_completion_request(
12604 indoc! {"
12605 one.second_completion
12606 two si
12607 three <si|>
12608 additional edit
12609 "},
12610 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12611 true,
12612 counter.clone(),
12613 &mut cx,
12614 )
12615 .await;
12616 cx.condition(|editor, _| editor.context_menu_visible())
12617 .await;
12618 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12619
12620 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12621 editor
12622 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12623 .unwrap()
12624 });
12625 cx.assert_editor_state(indoc! {"
12626 one.second_completion
12627 two sixth_completionˇ
12628 three sixth_completionˇ
12629 additional edit
12630 "});
12631
12632 apply_additional_edits.await.unwrap();
12633
12634 update_test_language_settings(&mut cx, |settings| {
12635 settings.defaults.show_completions_on_input = Some(false);
12636 });
12637 cx.set_state("editorˇ");
12638 cx.simulate_keystroke(".");
12639 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12640 cx.simulate_keystrokes("c l o");
12641 cx.assert_editor_state("editor.cloˇ");
12642 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12643 cx.update_editor(|editor, window, cx| {
12644 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12645 });
12646 handle_completion_request(
12647 "editor.<clo|>",
12648 vec!["close", "clobber"],
12649 true,
12650 counter.clone(),
12651 &mut cx,
12652 )
12653 .await;
12654 cx.condition(|editor, _| editor.context_menu_visible())
12655 .await;
12656 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
12657
12658 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12659 editor
12660 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12661 .unwrap()
12662 });
12663 cx.assert_editor_state("editor.clobberˇ");
12664 handle_resolve_completion_request(&mut cx, None).await;
12665 apply_additional_edits.await.unwrap();
12666}
12667
12668#[gpui::test]
12669async fn test_completion_reuse(cx: &mut TestAppContext) {
12670 init_test(cx, |_| {});
12671
12672 let mut cx = EditorLspTestContext::new_rust(
12673 lsp::ServerCapabilities {
12674 completion_provider: Some(lsp::CompletionOptions {
12675 trigger_characters: Some(vec![".".to_string()]),
12676 ..Default::default()
12677 }),
12678 ..Default::default()
12679 },
12680 cx,
12681 )
12682 .await;
12683
12684 let counter = Arc::new(AtomicUsize::new(0));
12685 cx.set_state("objˇ");
12686 cx.simulate_keystroke(".");
12687
12688 // Initial completion request returns complete results
12689 let is_incomplete = false;
12690 handle_completion_request(
12691 "obj.|<>",
12692 vec!["a", "ab", "abc"],
12693 is_incomplete,
12694 counter.clone(),
12695 &mut cx,
12696 )
12697 .await;
12698 cx.run_until_parked();
12699 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12700 cx.assert_editor_state("obj.ˇ");
12701 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12702
12703 // Type "a" - filters existing completions
12704 cx.simulate_keystroke("a");
12705 cx.run_until_parked();
12706 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12707 cx.assert_editor_state("obj.aˇ");
12708 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12709
12710 // Type "b" - filters existing completions
12711 cx.simulate_keystroke("b");
12712 cx.run_until_parked();
12713 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12714 cx.assert_editor_state("obj.abˇ");
12715 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12716
12717 // Type "c" - filters existing completions
12718 cx.simulate_keystroke("c");
12719 cx.run_until_parked();
12720 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12721 cx.assert_editor_state("obj.abcˇ");
12722 check_displayed_completions(vec!["abc"], &mut cx);
12723
12724 // Backspace to delete "c" - filters existing completions
12725 cx.update_editor(|editor, window, cx| {
12726 editor.backspace(&Backspace, window, cx);
12727 });
12728 cx.run_until_parked();
12729 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12730 cx.assert_editor_state("obj.abˇ");
12731 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12732
12733 // Moving cursor to the left dismisses menu.
12734 cx.update_editor(|editor, window, cx| {
12735 editor.move_left(&MoveLeft, window, cx);
12736 });
12737 cx.run_until_parked();
12738 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12739 cx.assert_editor_state("obj.aˇb");
12740 cx.update_editor(|editor, _, _| {
12741 assert_eq!(editor.context_menu_visible(), false);
12742 });
12743
12744 // Type "b" - new request
12745 cx.simulate_keystroke("b");
12746 let is_incomplete = false;
12747 handle_completion_request(
12748 "obj.<ab|>a",
12749 vec!["ab", "abc"],
12750 is_incomplete,
12751 counter.clone(),
12752 &mut cx,
12753 )
12754 .await;
12755 cx.run_until_parked();
12756 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12757 cx.assert_editor_state("obj.abˇb");
12758 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12759
12760 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
12761 cx.update_editor(|editor, window, cx| {
12762 editor.backspace(&Backspace, window, cx);
12763 });
12764 let is_incomplete = false;
12765 handle_completion_request(
12766 "obj.<a|>b",
12767 vec!["a", "ab", "abc"],
12768 is_incomplete,
12769 counter.clone(),
12770 &mut cx,
12771 )
12772 .await;
12773 cx.run_until_parked();
12774 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12775 cx.assert_editor_state("obj.aˇb");
12776 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12777
12778 // Backspace to delete "a" - dismisses menu.
12779 cx.update_editor(|editor, window, cx| {
12780 editor.backspace(&Backspace, window, cx);
12781 });
12782 cx.run_until_parked();
12783 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12784 cx.assert_editor_state("obj.ˇb");
12785 cx.update_editor(|editor, _, _| {
12786 assert_eq!(editor.context_menu_visible(), false);
12787 });
12788}
12789
12790#[gpui::test]
12791async fn test_word_completion(cx: &mut TestAppContext) {
12792 let lsp_fetch_timeout_ms = 10;
12793 init_test(cx, |language_settings| {
12794 language_settings.defaults.completions = Some(CompletionSettings {
12795 words: WordsCompletionMode::Fallback,
12796 lsp: true,
12797 lsp_fetch_timeout_ms: 10,
12798 lsp_insert_mode: LspInsertMode::Insert,
12799 });
12800 });
12801
12802 let mut cx = EditorLspTestContext::new_rust(
12803 lsp::ServerCapabilities {
12804 completion_provider: Some(lsp::CompletionOptions {
12805 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12806 ..lsp::CompletionOptions::default()
12807 }),
12808 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12809 ..lsp::ServerCapabilities::default()
12810 },
12811 cx,
12812 )
12813 .await;
12814
12815 let throttle_completions = Arc::new(AtomicBool::new(false));
12816
12817 let lsp_throttle_completions = throttle_completions.clone();
12818 let _completion_requests_handler =
12819 cx.lsp
12820 .server
12821 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
12822 let lsp_throttle_completions = lsp_throttle_completions.clone();
12823 let cx = cx.clone();
12824 async move {
12825 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
12826 cx.background_executor()
12827 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
12828 .await;
12829 }
12830 Ok(Some(lsp::CompletionResponse::Array(vec![
12831 lsp::CompletionItem {
12832 label: "first".into(),
12833 ..lsp::CompletionItem::default()
12834 },
12835 lsp::CompletionItem {
12836 label: "last".into(),
12837 ..lsp::CompletionItem::default()
12838 },
12839 ])))
12840 }
12841 });
12842
12843 cx.set_state(indoc! {"
12844 oneˇ
12845 two
12846 three
12847 "});
12848 cx.simulate_keystroke(".");
12849 cx.executor().run_until_parked();
12850 cx.condition(|editor, _| editor.context_menu_visible())
12851 .await;
12852 cx.update_editor(|editor, window, cx| {
12853 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12854 {
12855 assert_eq!(
12856 completion_menu_entries(&menu),
12857 &["first", "last"],
12858 "When LSP server is fast to reply, no fallback word completions are used"
12859 );
12860 } else {
12861 panic!("expected completion menu to be open");
12862 }
12863 editor.cancel(&Cancel, window, cx);
12864 });
12865 cx.executor().run_until_parked();
12866 cx.condition(|editor, _| !editor.context_menu_visible())
12867 .await;
12868
12869 throttle_completions.store(true, atomic::Ordering::Release);
12870 cx.simulate_keystroke(".");
12871 cx.executor()
12872 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
12873 cx.executor().run_until_parked();
12874 cx.condition(|editor, _| editor.context_menu_visible())
12875 .await;
12876 cx.update_editor(|editor, _, _| {
12877 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12878 {
12879 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
12880 "When LSP server is slow, document words can be shown instead, if configured accordingly");
12881 } else {
12882 panic!("expected completion menu to be open");
12883 }
12884 });
12885}
12886
12887#[gpui::test]
12888async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
12889 init_test(cx, |language_settings| {
12890 language_settings.defaults.completions = Some(CompletionSettings {
12891 words: WordsCompletionMode::Enabled,
12892 lsp: true,
12893 lsp_fetch_timeout_ms: 0,
12894 lsp_insert_mode: LspInsertMode::Insert,
12895 });
12896 });
12897
12898 let mut cx = EditorLspTestContext::new_rust(
12899 lsp::ServerCapabilities {
12900 completion_provider: Some(lsp::CompletionOptions {
12901 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12902 ..lsp::CompletionOptions::default()
12903 }),
12904 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12905 ..lsp::ServerCapabilities::default()
12906 },
12907 cx,
12908 )
12909 .await;
12910
12911 let _completion_requests_handler =
12912 cx.lsp
12913 .server
12914 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12915 Ok(Some(lsp::CompletionResponse::Array(vec![
12916 lsp::CompletionItem {
12917 label: "first".into(),
12918 ..lsp::CompletionItem::default()
12919 },
12920 lsp::CompletionItem {
12921 label: "last".into(),
12922 ..lsp::CompletionItem::default()
12923 },
12924 ])))
12925 });
12926
12927 cx.set_state(indoc! {"ˇ
12928 first
12929 last
12930 second
12931 "});
12932 cx.simulate_keystroke(".");
12933 cx.executor().run_until_parked();
12934 cx.condition(|editor, _| editor.context_menu_visible())
12935 .await;
12936 cx.update_editor(|editor, _, _| {
12937 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12938 {
12939 assert_eq!(
12940 completion_menu_entries(&menu),
12941 &["first", "last", "second"],
12942 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
12943 );
12944 } else {
12945 panic!("expected completion menu to be open");
12946 }
12947 });
12948}
12949
12950#[gpui::test]
12951async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
12952 init_test(cx, |language_settings| {
12953 language_settings.defaults.completions = Some(CompletionSettings {
12954 words: WordsCompletionMode::Disabled,
12955 lsp: true,
12956 lsp_fetch_timeout_ms: 0,
12957 lsp_insert_mode: LspInsertMode::Insert,
12958 });
12959 });
12960
12961 let mut cx = EditorLspTestContext::new_rust(
12962 lsp::ServerCapabilities {
12963 completion_provider: Some(lsp::CompletionOptions {
12964 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12965 ..lsp::CompletionOptions::default()
12966 }),
12967 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12968 ..lsp::ServerCapabilities::default()
12969 },
12970 cx,
12971 )
12972 .await;
12973
12974 let _completion_requests_handler =
12975 cx.lsp
12976 .server
12977 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12978 panic!("LSP completions should not be queried when dealing with word completions")
12979 });
12980
12981 cx.set_state(indoc! {"ˇ
12982 first
12983 last
12984 second
12985 "});
12986 cx.update_editor(|editor, window, cx| {
12987 editor.show_word_completions(&ShowWordCompletions, window, cx);
12988 });
12989 cx.executor().run_until_parked();
12990 cx.condition(|editor, _| editor.context_menu_visible())
12991 .await;
12992 cx.update_editor(|editor, _, _| {
12993 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12994 {
12995 assert_eq!(
12996 completion_menu_entries(&menu),
12997 &["first", "last", "second"],
12998 "`ShowWordCompletions` action should show word completions"
12999 );
13000 } else {
13001 panic!("expected completion menu to be open");
13002 }
13003 });
13004
13005 cx.simulate_keystroke("l");
13006 cx.executor().run_until_parked();
13007 cx.condition(|editor, _| editor.context_menu_visible())
13008 .await;
13009 cx.update_editor(|editor, _, _| {
13010 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13011 {
13012 assert_eq!(
13013 completion_menu_entries(&menu),
13014 &["last"],
13015 "After showing word completions, further editing should filter them and not query the LSP"
13016 );
13017 } else {
13018 panic!("expected completion menu to be open");
13019 }
13020 });
13021}
13022
13023#[gpui::test]
13024async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
13025 init_test(cx, |language_settings| {
13026 language_settings.defaults.completions = Some(CompletionSettings {
13027 words: WordsCompletionMode::Fallback,
13028 lsp: false,
13029 lsp_fetch_timeout_ms: 0,
13030 lsp_insert_mode: LspInsertMode::Insert,
13031 });
13032 });
13033
13034 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13035
13036 cx.set_state(indoc! {"ˇ
13037 0_usize
13038 let
13039 33
13040 4.5f32
13041 "});
13042 cx.update_editor(|editor, window, cx| {
13043 editor.show_completions(&ShowCompletions::default(), window, cx);
13044 });
13045 cx.executor().run_until_parked();
13046 cx.condition(|editor, _| editor.context_menu_visible())
13047 .await;
13048 cx.update_editor(|editor, window, cx| {
13049 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13050 {
13051 assert_eq!(
13052 completion_menu_entries(&menu),
13053 &["let"],
13054 "With no digits in the completion query, no digits should be in the word completions"
13055 );
13056 } else {
13057 panic!("expected completion menu to be open");
13058 }
13059 editor.cancel(&Cancel, window, cx);
13060 });
13061
13062 cx.set_state(indoc! {"3ˇ
13063 0_usize
13064 let
13065 3
13066 33.35f32
13067 "});
13068 cx.update_editor(|editor, window, cx| {
13069 editor.show_completions(&ShowCompletions::default(), window, cx);
13070 });
13071 cx.executor().run_until_parked();
13072 cx.condition(|editor, _| editor.context_menu_visible())
13073 .await;
13074 cx.update_editor(|editor, _, _| {
13075 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13076 {
13077 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
13078 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
13079 } else {
13080 panic!("expected completion menu to be open");
13081 }
13082 });
13083}
13084
13085fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
13086 let position = || lsp::Position {
13087 line: params.text_document_position.position.line,
13088 character: params.text_document_position.position.character,
13089 };
13090 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13091 range: lsp::Range {
13092 start: position(),
13093 end: position(),
13094 },
13095 new_text: text.to_string(),
13096 }))
13097}
13098
13099#[gpui::test]
13100async fn test_multiline_completion(cx: &mut TestAppContext) {
13101 init_test(cx, |_| {});
13102
13103 let fs = FakeFs::new(cx.executor());
13104 fs.insert_tree(
13105 path!("/a"),
13106 json!({
13107 "main.ts": "a",
13108 }),
13109 )
13110 .await;
13111
13112 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13113 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13114 let typescript_language = Arc::new(Language::new(
13115 LanguageConfig {
13116 name: "TypeScript".into(),
13117 matcher: LanguageMatcher {
13118 path_suffixes: vec!["ts".to_string()],
13119 ..LanguageMatcher::default()
13120 },
13121 line_comments: vec!["// ".into()],
13122 ..LanguageConfig::default()
13123 },
13124 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13125 ));
13126 language_registry.add(typescript_language.clone());
13127 let mut fake_servers = language_registry.register_fake_lsp(
13128 "TypeScript",
13129 FakeLspAdapter {
13130 capabilities: lsp::ServerCapabilities {
13131 completion_provider: Some(lsp::CompletionOptions {
13132 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13133 ..lsp::CompletionOptions::default()
13134 }),
13135 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13136 ..lsp::ServerCapabilities::default()
13137 },
13138 // Emulate vtsls label generation
13139 label_for_completion: Some(Box::new(|item, _| {
13140 let text = if let Some(description) = item
13141 .label_details
13142 .as_ref()
13143 .and_then(|label_details| label_details.description.as_ref())
13144 {
13145 format!("{} {}", item.label, description)
13146 } else if let Some(detail) = &item.detail {
13147 format!("{} {}", item.label, detail)
13148 } else {
13149 item.label.clone()
13150 };
13151 let len = text.len();
13152 Some(language::CodeLabel {
13153 text,
13154 runs: Vec::new(),
13155 filter_range: 0..len,
13156 })
13157 })),
13158 ..FakeLspAdapter::default()
13159 },
13160 );
13161 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13162 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13163 let worktree_id = workspace
13164 .update(cx, |workspace, _window, cx| {
13165 workspace.project().update(cx, |project, cx| {
13166 project.worktrees(cx).next().unwrap().read(cx).id()
13167 })
13168 })
13169 .unwrap();
13170 let _buffer = project
13171 .update(cx, |project, cx| {
13172 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
13173 })
13174 .await
13175 .unwrap();
13176 let editor = workspace
13177 .update(cx, |workspace, window, cx| {
13178 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
13179 })
13180 .unwrap()
13181 .await
13182 .unwrap()
13183 .downcast::<Editor>()
13184 .unwrap();
13185 let fake_server = fake_servers.next().await.unwrap();
13186
13187 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
13188 let multiline_label_2 = "a\nb\nc\n";
13189 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
13190 let multiline_description = "d\ne\nf\n";
13191 let multiline_detail_2 = "g\nh\ni\n";
13192
13193 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
13194 move |params, _| async move {
13195 Ok(Some(lsp::CompletionResponse::Array(vec![
13196 lsp::CompletionItem {
13197 label: multiline_label.to_string(),
13198 text_edit: gen_text_edit(¶ms, "new_text_1"),
13199 ..lsp::CompletionItem::default()
13200 },
13201 lsp::CompletionItem {
13202 label: "single line label 1".to_string(),
13203 detail: Some(multiline_detail.to_string()),
13204 text_edit: gen_text_edit(¶ms, "new_text_2"),
13205 ..lsp::CompletionItem::default()
13206 },
13207 lsp::CompletionItem {
13208 label: "single line label 2".to_string(),
13209 label_details: Some(lsp::CompletionItemLabelDetails {
13210 description: Some(multiline_description.to_string()),
13211 detail: None,
13212 }),
13213 text_edit: gen_text_edit(¶ms, "new_text_2"),
13214 ..lsp::CompletionItem::default()
13215 },
13216 lsp::CompletionItem {
13217 label: multiline_label_2.to_string(),
13218 detail: Some(multiline_detail_2.to_string()),
13219 text_edit: gen_text_edit(¶ms, "new_text_3"),
13220 ..lsp::CompletionItem::default()
13221 },
13222 lsp::CompletionItem {
13223 label: "Label with many spaces and \t but without newlines".to_string(),
13224 detail: Some(
13225 "Details with many spaces and \t but without newlines".to_string(),
13226 ),
13227 text_edit: gen_text_edit(¶ms, "new_text_4"),
13228 ..lsp::CompletionItem::default()
13229 },
13230 ])))
13231 },
13232 );
13233
13234 editor.update_in(cx, |editor, window, cx| {
13235 cx.focus_self(window);
13236 editor.move_to_end(&MoveToEnd, window, cx);
13237 editor.handle_input(".", window, cx);
13238 });
13239 cx.run_until_parked();
13240 completion_handle.next().await.unwrap();
13241
13242 editor.update(cx, |editor, _| {
13243 assert!(editor.context_menu_visible());
13244 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13245 {
13246 let completion_labels = menu
13247 .completions
13248 .borrow()
13249 .iter()
13250 .map(|c| c.label.text.clone())
13251 .collect::<Vec<_>>();
13252 assert_eq!(
13253 completion_labels,
13254 &[
13255 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
13256 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
13257 "single line label 2 d e f ",
13258 "a b c g h i ",
13259 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
13260 ],
13261 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
13262 );
13263
13264 for completion in menu
13265 .completions
13266 .borrow()
13267 .iter() {
13268 assert_eq!(
13269 completion.label.filter_range,
13270 0..completion.label.text.len(),
13271 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
13272 );
13273 }
13274 } else {
13275 panic!("expected completion menu to be open");
13276 }
13277 });
13278}
13279
13280#[gpui::test]
13281async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
13282 init_test(cx, |_| {});
13283 let mut cx = EditorLspTestContext::new_rust(
13284 lsp::ServerCapabilities {
13285 completion_provider: Some(lsp::CompletionOptions {
13286 trigger_characters: Some(vec![".".to_string()]),
13287 ..Default::default()
13288 }),
13289 ..Default::default()
13290 },
13291 cx,
13292 )
13293 .await;
13294 cx.lsp
13295 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13296 Ok(Some(lsp::CompletionResponse::Array(vec![
13297 lsp::CompletionItem {
13298 label: "first".into(),
13299 ..Default::default()
13300 },
13301 lsp::CompletionItem {
13302 label: "last".into(),
13303 ..Default::default()
13304 },
13305 ])))
13306 });
13307 cx.set_state("variableˇ");
13308 cx.simulate_keystroke(".");
13309 cx.executor().run_until_parked();
13310
13311 cx.update_editor(|editor, _, _| {
13312 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13313 {
13314 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
13315 } else {
13316 panic!("expected completion menu to be open");
13317 }
13318 });
13319
13320 cx.update_editor(|editor, window, cx| {
13321 editor.move_page_down(&MovePageDown::default(), window, cx);
13322 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13323 {
13324 assert!(
13325 menu.selected_item == 1,
13326 "expected PageDown to select the last item from the context menu"
13327 );
13328 } else {
13329 panic!("expected completion menu to stay open after PageDown");
13330 }
13331 });
13332
13333 cx.update_editor(|editor, window, cx| {
13334 editor.move_page_up(&MovePageUp::default(), window, cx);
13335 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13336 {
13337 assert!(
13338 menu.selected_item == 0,
13339 "expected PageUp to select the first item from the context menu"
13340 );
13341 } else {
13342 panic!("expected completion menu to stay open after PageUp");
13343 }
13344 });
13345}
13346
13347#[gpui::test]
13348async fn test_as_is_completions(cx: &mut TestAppContext) {
13349 init_test(cx, |_| {});
13350 let mut cx = EditorLspTestContext::new_rust(
13351 lsp::ServerCapabilities {
13352 completion_provider: Some(lsp::CompletionOptions {
13353 ..Default::default()
13354 }),
13355 ..Default::default()
13356 },
13357 cx,
13358 )
13359 .await;
13360 cx.lsp
13361 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13362 Ok(Some(lsp::CompletionResponse::Array(vec![
13363 lsp::CompletionItem {
13364 label: "unsafe".into(),
13365 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13366 range: lsp::Range {
13367 start: lsp::Position {
13368 line: 1,
13369 character: 2,
13370 },
13371 end: lsp::Position {
13372 line: 1,
13373 character: 3,
13374 },
13375 },
13376 new_text: "unsafe".to_string(),
13377 })),
13378 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
13379 ..Default::default()
13380 },
13381 ])))
13382 });
13383 cx.set_state("fn a() {}\n nˇ");
13384 cx.executor().run_until_parked();
13385 cx.update_editor(|editor, window, cx| {
13386 editor.show_completions(
13387 &ShowCompletions {
13388 trigger: Some("\n".into()),
13389 },
13390 window,
13391 cx,
13392 );
13393 });
13394 cx.executor().run_until_parked();
13395
13396 cx.update_editor(|editor, window, cx| {
13397 editor.confirm_completion(&Default::default(), window, cx)
13398 });
13399 cx.executor().run_until_parked();
13400 cx.assert_editor_state("fn a() {}\n unsafeˇ");
13401}
13402
13403#[gpui::test]
13404async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
13405 init_test(cx, |_| {});
13406
13407 let mut cx = EditorLspTestContext::new_rust(
13408 lsp::ServerCapabilities {
13409 completion_provider: Some(lsp::CompletionOptions {
13410 trigger_characters: Some(vec![".".to_string()]),
13411 resolve_provider: Some(true),
13412 ..Default::default()
13413 }),
13414 ..Default::default()
13415 },
13416 cx,
13417 )
13418 .await;
13419
13420 cx.set_state("fn main() { let a = 2ˇ; }");
13421 cx.simulate_keystroke(".");
13422 let completion_item = lsp::CompletionItem {
13423 label: "Some".into(),
13424 kind: Some(lsp::CompletionItemKind::SNIPPET),
13425 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13426 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13427 kind: lsp::MarkupKind::Markdown,
13428 value: "```rust\nSome(2)\n```".to_string(),
13429 })),
13430 deprecated: Some(false),
13431 sort_text: Some("Some".to_string()),
13432 filter_text: Some("Some".to_string()),
13433 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13434 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13435 range: lsp::Range {
13436 start: lsp::Position {
13437 line: 0,
13438 character: 22,
13439 },
13440 end: lsp::Position {
13441 line: 0,
13442 character: 22,
13443 },
13444 },
13445 new_text: "Some(2)".to_string(),
13446 })),
13447 additional_text_edits: Some(vec![lsp::TextEdit {
13448 range: lsp::Range {
13449 start: lsp::Position {
13450 line: 0,
13451 character: 20,
13452 },
13453 end: lsp::Position {
13454 line: 0,
13455 character: 22,
13456 },
13457 },
13458 new_text: "".to_string(),
13459 }]),
13460 ..Default::default()
13461 };
13462
13463 let closure_completion_item = completion_item.clone();
13464 let counter = Arc::new(AtomicUsize::new(0));
13465 let counter_clone = counter.clone();
13466 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13467 let task_completion_item = closure_completion_item.clone();
13468 counter_clone.fetch_add(1, atomic::Ordering::Release);
13469 async move {
13470 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13471 is_incomplete: true,
13472 item_defaults: None,
13473 items: vec![task_completion_item],
13474 })))
13475 }
13476 });
13477
13478 cx.condition(|editor, _| editor.context_menu_visible())
13479 .await;
13480 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
13481 assert!(request.next().await.is_some());
13482 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13483
13484 cx.simulate_keystrokes("S o m");
13485 cx.condition(|editor, _| editor.context_menu_visible())
13486 .await;
13487 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
13488 assert!(request.next().await.is_some());
13489 assert!(request.next().await.is_some());
13490 assert!(request.next().await.is_some());
13491 request.close();
13492 assert!(request.next().await.is_none());
13493 assert_eq!(
13494 counter.load(atomic::Ordering::Acquire),
13495 4,
13496 "With the completions menu open, only one LSP request should happen per input"
13497 );
13498}
13499
13500#[gpui::test]
13501async fn test_toggle_comment(cx: &mut TestAppContext) {
13502 init_test(cx, |_| {});
13503 let mut cx = EditorTestContext::new(cx).await;
13504 let language = Arc::new(Language::new(
13505 LanguageConfig {
13506 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13507 ..Default::default()
13508 },
13509 Some(tree_sitter_rust::LANGUAGE.into()),
13510 ));
13511 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13512
13513 // If multiple selections intersect a line, the line is only toggled once.
13514 cx.set_state(indoc! {"
13515 fn a() {
13516 «//b();
13517 ˇ»// «c();
13518 //ˇ» d();
13519 }
13520 "});
13521
13522 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13523
13524 cx.assert_editor_state(indoc! {"
13525 fn a() {
13526 «b();
13527 c();
13528 ˇ» d();
13529 }
13530 "});
13531
13532 // The comment prefix is inserted at the same column for every line in a
13533 // selection.
13534 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13535
13536 cx.assert_editor_state(indoc! {"
13537 fn a() {
13538 // «b();
13539 // c();
13540 ˇ»// d();
13541 }
13542 "});
13543
13544 // If a selection ends at the beginning of a line, that line is not toggled.
13545 cx.set_selections_state(indoc! {"
13546 fn a() {
13547 // b();
13548 «// c();
13549 ˇ» // d();
13550 }
13551 "});
13552
13553 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13554
13555 cx.assert_editor_state(indoc! {"
13556 fn a() {
13557 // b();
13558 «c();
13559 ˇ» // d();
13560 }
13561 "});
13562
13563 // If a selection span a single line and is empty, the line is toggled.
13564 cx.set_state(indoc! {"
13565 fn a() {
13566 a();
13567 b();
13568 ˇ
13569 }
13570 "});
13571
13572 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13573
13574 cx.assert_editor_state(indoc! {"
13575 fn a() {
13576 a();
13577 b();
13578 //•ˇ
13579 }
13580 "});
13581
13582 // If a selection span multiple lines, empty lines are not toggled.
13583 cx.set_state(indoc! {"
13584 fn a() {
13585 «a();
13586
13587 c();ˇ»
13588 }
13589 "});
13590
13591 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13592
13593 cx.assert_editor_state(indoc! {"
13594 fn a() {
13595 // «a();
13596
13597 // c();ˇ»
13598 }
13599 "});
13600
13601 // If a selection includes multiple comment prefixes, all lines are uncommented.
13602 cx.set_state(indoc! {"
13603 fn a() {
13604 «// a();
13605 /// b();
13606 //! c();ˇ»
13607 }
13608 "});
13609
13610 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13611
13612 cx.assert_editor_state(indoc! {"
13613 fn a() {
13614 «a();
13615 b();
13616 c();ˇ»
13617 }
13618 "});
13619}
13620
13621#[gpui::test]
13622async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
13623 init_test(cx, |_| {});
13624 let mut cx = EditorTestContext::new(cx).await;
13625 let language = Arc::new(Language::new(
13626 LanguageConfig {
13627 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13628 ..Default::default()
13629 },
13630 Some(tree_sitter_rust::LANGUAGE.into()),
13631 ));
13632 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13633
13634 let toggle_comments = &ToggleComments {
13635 advance_downwards: false,
13636 ignore_indent: true,
13637 };
13638
13639 // If multiple selections intersect a line, the line is only toggled once.
13640 cx.set_state(indoc! {"
13641 fn a() {
13642 // «b();
13643 // c();
13644 // ˇ» d();
13645 }
13646 "});
13647
13648 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13649
13650 cx.assert_editor_state(indoc! {"
13651 fn a() {
13652 «b();
13653 c();
13654 ˇ» d();
13655 }
13656 "});
13657
13658 // The comment prefix is inserted at the beginning of each line
13659 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13660
13661 cx.assert_editor_state(indoc! {"
13662 fn a() {
13663 // «b();
13664 // c();
13665 // ˇ» d();
13666 }
13667 "});
13668
13669 // If a selection ends at the beginning of a line, that line is not toggled.
13670 cx.set_selections_state(indoc! {"
13671 fn a() {
13672 // b();
13673 // «c();
13674 ˇ»// d();
13675 }
13676 "});
13677
13678 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13679
13680 cx.assert_editor_state(indoc! {"
13681 fn a() {
13682 // b();
13683 «c();
13684 ˇ»// d();
13685 }
13686 "});
13687
13688 // If a selection span a single line and is empty, the line is toggled.
13689 cx.set_state(indoc! {"
13690 fn a() {
13691 a();
13692 b();
13693 ˇ
13694 }
13695 "});
13696
13697 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13698
13699 cx.assert_editor_state(indoc! {"
13700 fn a() {
13701 a();
13702 b();
13703 //ˇ
13704 }
13705 "});
13706
13707 // If a selection span multiple lines, empty lines are not toggled.
13708 cx.set_state(indoc! {"
13709 fn a() {
13710 «a();
13711
13712 c();ˇ»
13713 }
13714 "});
13715
13716 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13717
13718 cx.assert_editor_state(indoc! {"
13719 fn a() {
13720 // «a();
13721
13722 // c();ˇ»
13723 }
13724 "});
13725
13726 // If a selection includes multiple comment prefixes, all lines are uncommented.
13727 cx.set_state(indoc! {"
13728 fn a() {
13729 // «a();
13730 /// b();
13731 //! c();ˇ»
13732 }
13733 "});
13734
13735 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13736
13737 cx.assert_editor_state(indoc! {"
13738 fn a() {
13739 «a();
13740 b();
13741 c();ˇ»
13742 }
13743 "});
13744}
13745
13746#[gpui::test]
13747async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
13748 init_test(cx, |_| {});
13749
13750 let language = Arc::new(Language::new(
13751 LanguageConfig {
13752 line_comments: vec!["// ".into()],
13753 ..Default::default()
13754 },
13755 Some(tree_sitter_rust::LANGUAGE.into()),
13756 ));
13757
13758 let mut cx = EditorTestContext::new(cx).await;
13759
13760 cx.language_registry().add(language.clone());
13761 cx.update_buffer(|buffer, cx| {
13762 buffer.set_language(Some(language), cx);
13763 });
13764
13765 let toggle_comments = &ToggleComments {
13766 advance_downwards: true,
13767 ignore_indent: false,
13768 };
13769
13770 // Single cursor on one line -> advance
13771 // Cursor moves horizontally 3 characters as well on non-blank line
13772 cx.set_state(indoc!(
13773 "fn a() {
13774 ˇdog();
13775 cat();
13776 }"
13777 ));
13778 cx.update_editor(|editor, window, cx| {
13779 editor.toggle_comments(toggle_comments, window, cx);
13780 });
13781 cx.assert_editor_state(indoc!(
13782 "fn a() {
13783 // dog();
13784 catˇ();
13785 }"
13786 ));
13787
13788 // Single selection on one line -> don't advance
13789 cx.set_state(indoc!(
13790 "fn a() {
13791 «dog()ˇ»;
13792 cat();
13793 }"
13794 ));
13795 cx.update_editor(|editor, window, cx| {
13796 editor.toggle_comments(toggle_comments, window, cx);
13797 });
13798 cx.assert_editor_state(indoc!(
13799 "fn a() {
13800 // «dog()ˇ»;
13801 cat();
13802 }"
13803 ));
13804
13805 // Multiple cursors on one line -> advance
13806 cx.set_state(indoc!(
13807 "fn a() {
13808 ˇdˇog();
13809 cat();
13810 }"
13811 ));
13812 cx.update_editor(|editor, window, cx| {
13813 editor.toggle_comments(toggle_comments, window, cx);
13814 });
13815 cx.assert_editor_state(indoc!(
13816 "fn a() {
13817 // dog();
13818 catˇ(ˇ);
13819 }"
13820 ));
13821
13822 // Multiple cursors on one line, with selection -> don't advance
13823 cx.set_state(indoc!(
13824 "fn a() {
13825 ˇdˇog«()ˇ»;
13826 cat();
13827 }"
13828 ));
13829 cx.update_editor(|editor, window, cx| {
13830 editor.toggle_comments(toggle_comments, window, cx);
13831 });
13832 cx.assert_editor_state(indoc!(
13833 "fn a() {
13834 // ˇdˇog«()ˇ»;
13835 cat();
13836 }"
13837 ));
13838
13839 // Single cursor on one line -> advance
13840 // Cursor moves to column 0 on blank line
13841 cx.set_state(indoc!(
13842 "fn a() {
13843 ˇdog();
13844
13845 cat();
13846 }"
13847 ));
13848 cx.update_editor(|editor, window, cx| {
13849 editor.toggle_comments(toggle_comments, window, cx);
13850 });
13851 cx.assert_editor_state(indoc!(
13852 "fn a() {
13853 // dog();
13854 ˇ
13855 cat();
13856 }"
13857 ));
13858
13859 // Single cursor on one line -> advance
13860 // Cursor starts and ends at column 0
13861 cx.set_state(indoc!(
13862 "fn a() {
13863 ˇ dog();
13864 cat();
13865 }"
13866 ));
13867 cx.update_editor(|editor, window, cx| {
13868 editor.toggle_comments(toggle_comments, window, cx);
13869 });
13870 cx.assert_editor_state(indoc!(
13871 "fn a() {
13872 // dog();
13873 ˇ cat();
13874 }"
13875 ));
13876}
13877
13878#[gpui::test]
13879async fn test_toggle_block_comment(cx: &mut TestAppContext) {
13880 init_test(cx, |_| {});
13881
13882 let mut cx = EditorTestContext::new(cx).await;
13883
13884 let html_language = Arc::new(
13885 Language::new(
13886 LanguageConfig {
13887 name: "HTML".into(),
13888 block_comment: Some(BlockCommentConfig {
13889 start: "<!-- ".into(),
13890 prefix: "".into(),
13891 end: " -->".into(),
13892 tab_size: 0,
13893 }),
13894 ..Default::default()
13895 },
13896 Some(tree_sitter_html::LANGUAGE.into()),
13897 )
13898 .with_injection_query(
13899 r#"
13900 (script_element
13901 (raw_text) @injection.content
13902 (#set! injection.language "javascript"))
13903 "#,
13904 )
13905 .unwrap(),
13906 );
13907
13908 let javascript_language = Arc::new(Language::new(
13909 LanguageConfig {
13910 name: "JavaScript".into(),
13911 line_comments: vec!["// ".into()],
13912 ..Default::default()
13913 },
13914 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13915 ));
13916
13917 cx.language_registry().add(html_language.clone());
13918 cx.language_registry().add(javascript_language.clone());
13919 cx.update_buffer(|buffer, cx| {
13920 buffer.set_language(Some(html_language), cx);
13921 });
13922
13923 // Toggle comments for empty selections
13924 cx.set_state(
13925 &r#"
13926 <p>A</p>ˇ
13927 <p>B</p>ˇ
13928 <p>C</p>ˇ
13929 "#
13930 .unindent(),
13931 );
13932 cx.update_editor(|editor, window, cx| {
13933 editor.toggle_comments(&ToggleComments::default(), window, cx)
13934 });
13935 cx.assert_editor_state(
13936 &r#"
13937 <!-- <p>A</p>ˇ -->
13938 <!-- <p>B</p>ˇ -->
13939 <!-- <p>C</p>ˇ -->
13940 "#
13941 .unindent(),
13942 );
13943 cx.update_editor(|editor, window, cx| {
13944 editor.toggle_comments(&ToggleComments::default(), window, cx)
13945 });
13946 cx.assert_editor_state(
13947 &r#"
13948 <p>A</p>ˇ
13949 <p>B</p>ˇ
13950 <p>C</p>ˇ
13951 "#
13952 .unindent(),
13953 );
13954
13955 // Toggle comments for mixture of empty and non-empty selections, where
13956 // multiple selections occupy a given line.
13957 cx.set_state(
13958 &r#"
13959 <p>A«</p>
13960 <p>ˇ»B</p>ˇ
13961 <p>C«</p>
13962 <p>ˇ»D</p>ˇ
13963 "#
13964 .unindent(),
13965 );
13966
13967 cx.update_editor(|editor, window, cx| {
13968 editor.toggle_comments(&ToggleComments::default(), window, cx)
13969 });
13970 cx.assert_editor_state(
13971 &r#"
13972 <!-- <p>A«</p>
13973 <p>ˇ»B</p>ˇ -->
13974 <!-- <p>C«</p>
13975 <p>ˇ»D</p>ˇ -->
13976 "#
13977 .unindent(),
13978 );
13979 cx.update_editor(|editor, window, cx| {
13980 editor.toggle_comments(&ToggleComments::default(), window, cx)
13981 });
13982 cx.assert_editor_state(
13983 &r#"
13984 <p>A«</p>
13985 <p>ˇ»B</p>ˇ
13986 <p>C«</p>
13987 <p>ˇ»D</p>ˇ
13988 "#
13989 .unindent(),
13990 );
13991
13992 // Toggle comments when different languages are active for different
13993 // selections.
13994 cx.set_state(
13995 &r#"
13996 ˇ<script>
13997 ˇvar x = new Y();
13998 ˇ</script>
13999 "#
14000 .unindent(),
14001 );
14002 cx.executor().run_until_parked();
14003 cx.update_editor(|editor, window, cx| {
14004 editor.toggle_comments(&ToggleComments::default(), window, cx)
14005 });
14006 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
14007 // Uncommenting and commenting from this position brings in even more wrong artifacts.
14008 cx.assert_editor_state(
14009 &r#"
14010 <!-- ˇ<script> -->
14011 // ˇvar x = new Y();
14012 <!-- ˇ</script> -->
14013 "#
14014 .unindent(),
14015 );
14016}
14017
14018#[gpui::test]
14019fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
14020 init_test(cx, |_| {});
14021
14022 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14023 let multibuffer = cx.new(|cx| {
14024 let mut multibuffer = MultiBuffer::new(ReadWrite);
14025 multibuffer.push_excerpts(
14026 buffer.clone(),
14027 [
14028 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
14029 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
14030 ],
14031 cx,
14032 );
14033 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
14034 multibuffer
14035 });
14036
14037 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
14038 editor.update_in(cx, |editor, window, cx| {
14039 assert_eq!(editor.text(cx), "aaaa\nbbbb");
14040 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14041 s.select_ranges([
14042 Point::new(0, 0)..Point::new(0, 0),
14043 Point::new(1, 0)..Point::new(1, 0),
14044 ])
14045 });
14046
14047 editor.handle_input("X", window, cx);
14048 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
14049 assert_eq!(
14050 editor.selections.ranges(cx),
14051 [
14052 Point::new(0, 1)..Point::new(0, 1),
14053 Point::new(1, 1)..Point::new(1, 1),
14054 ]
14055 );
14056
14057 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
14058 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14059 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
14060 });
14061 editor.backspace(&Default::default(), window, cx);
14062 assert_eq!(editor.text(cx), "Xa\nbbb");
14063 assert_eq!(
14064 editor.selections.ranges(cx),
14065 [Point::new(1, 0)..Point::new(1, 0)]
14066 );
14067
14068 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14069 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
14070 });
14071 editor.backspace(&Default::default(), window, cx);
14072 assert_eq!(editor.text(cx), "X\nbb");
14073 assert_eq!(
14074 editor.selections.ranges(cx),
14075 [Point::new(0, 1)..Point::new(0, 1)]
14076 );
14077 });
14078}
14079
14080#[gpui::test]
14081fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
14082 init_test(cx, |_| {});
14083
14084 let markers = vec![('[', ']').into(), ('(', ')').into()];
14085 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
14086 indoc! {"
14087 [aaaa
14088 (bbbb]
14089 cccc)",
14090 },
14091 markers.clone(),
14092 );
14093 let excerpt_ranges = markers.into_iter().map(|marker| {
14094 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
14095 ExcerptRange::new(context.clone())
14096 });
14097 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
14098 let multibuffer = cx.new(|cx| {
14099 let mut multibuffer = MultiBuffer::new(ReadWrite);
14100 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
14101 multibuffer
14102 });
14103
14104 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
14105 editor.update_in(cx, |editor, window, cx| {
14106 let (expected_text, selection_ranges) = marked_text_ranges(
14107 indoc! {"
14108 aaaa
14109 bˇbbb
14110 bˇbbˇb
14111 cccc"
14112 },
14113 true,
14114 );
14115 assert_eq!(editor.text(cx), expected_text);
14116 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14117 s.select_ranges(selection_ranges)
14118 });
14119
14120 editor.handle_input("X", window, cx);
14121
14122 let (expected_text, expected_selections) = marked_text_ranges(
14123 indoc! {"
14124 aaaa
14125 bXˇbbXb
14126 bXˇbbXˇb
14127 cccc"
14128 },
14129 false,
14130 );
14131 assert_eq!(editor.text(cx), expected_text);
14132 assert_eq!(editor.selections.ranges(cx), expected_selections);
14133
14134 editor.newline(&Newline, window, cx);
14135 let (expected_text, expected_selections) = marked_text_ranges(
14136 indoc! {"
14137 aaaa
14138 bX
14139 ˇbbX
14140 b
14141 bX
14142 ˇbbX
14143 ˇb
14144 cccc"
14145 },
14146 false,
14147 );
14148 assert_eq!(editor.text(cx), expected_text);
14149 assert_eq!(editor.selections.ranges(cx), expected_selections);
14150 });
14151}
14152
14153#[gpui::test]
14154fn test_refresh_selections(cx: &mut TestAppContext) {
14155 init_test(cx, |_| {});
14156
14157 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14158 let mut excerpt1_id = None;
14159 let multibuffer = cx.new(|cx| {
14160 let mut multibuffer = MultiBuffer::new(ReadWrite);
14161 excerpt1_id = multibuffer
14162 .push_excerpts(
14163 buffer.clone(),
14164 [
14165 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14166 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14167 ],
14168 cx,
14169 )
14170 .into_iter()
14171 .next();
14172 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14173 multibuffer
14174 });
14175
14176 let editor = cx.add_window(|window, cx| {
14177 let mut editor = build_editor(multibuffer.clone(), window, cx);
14178 let snapshot = editor.snapshot(window, cx);
14179 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14180 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
14181 });
14182 editor.begin_selection(
14183 Point::new(2, 1).to_display_point(&snapshot),
14184 true,
14185 1,
14186 window,
14187 cx,
14188 );
14189 assert_eq!(
14190 editor.selections.ranges(cx),
14191 [
14192 Point::new(1, 3)..Point::new(1, 3),
14193 Point::new(2, 1)..Point::new(2, 1),
14194 ]
14195 );
14196 editor
14197 });
14198
14199 // Refreshing selections is a no-op when excerpts haven't changed.
14200 _ = editor.update(cx, |editor, window, cx| {
14201 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14202 assert_eq!(
14203 editor.selections.ranges(cx),
14204 [
14205 Point::new(1, 3)..Point::new(1, 3),
14206 Point::new(2, 1)..Point::new(2, 1),
14207 ]
14208 );
14209 });
14210
14211 multibuffer.update(cx, |multibuffer, cx| {
14212 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14213 });
14214 _ = editor.update(cx, |editor, window, cx| {
14215 // Removing an excerpt causes the first selection to become degenerate.
14216 assert_eq!(
14217 editor.selections.ranges(cx),
14218 [
14219 Point::new(0, 0)..Point::new(0, 0),
14220 Point::new(0, 1)..Point::new(0, 1)
14221 ]
14222 );
14223
14224 // Refreshing selections will relocate the first selection to the original buffer
14225 // location.
14226 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14227 assert_eq!(
14228 editor.selections.ranges(cx),
14229 [
14230 Point::new(0, 1)..Point::new(0, 1),
14231 Point::new(0, 3)..Point::new(0, 3)
14232 ]
14233 );
14234 assert!(editor.selections.pending_anchor().is_some());
14235 });
14236}
14237
14238#[gpui::test]
14239fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
14240 init_test(cx, |_| {});
14241
14242 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14243 let mut excerpt1_id = None;
14244 let multibuffer = cx.new(|cx| {
14245 let mut multibuffer = MultiBuffer::new(ReadWrite);
14246 excerpt1_id = multibuffer
14247 .push_excerpts(
14248 buffer.clone(),
14249 [
14250 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14251 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14252 ],
14253 cx,
14254 )
14255 .into_iter()
14256 .next();
14257 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14258 multibuffer
14259 });
14260
14261 let editor = cx.add_window(|window, cx| {
14262 let mut editor = build_editor(multibuffer.clone(), window, cx);
14263 let snapshot = editor.snapshot(window, cx);
14264 editor.begin_selection(
14265 Point::new(1, 3).to_display_point(&snapshot),
14266 false,
14267 1,
14268 window,
14269 cx,
14270 );
14271 assert_eq!(
14272 editor.selections.ranges(cx),
14273 [Point::new(1, 3)..Point::new(1, 3)]
14274 );
14275 editor
14276 });
14277
14278 multibuffer.update(cx, |multibuffer, cx| {
14279 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14280 });
14281 _ = editor.update(cx, |editor, window, cx| {
14282 assert_eq!(
14283 editor.selections.ranges(cx),
14284 [Point::new(0, 0)..Point::new(0, 0)]
14285 );
14286
14287 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
14288 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14289 assert_eq!(
14290 editor.selections.ranges(cx),
14291 [Point::new(0, 3)..Point::new(0, 3)]
14292 );
14293 assert!(editor.selections.pending_anchor().is_some());
14294 });
14295}
14296
14297#[gpui::test]
14298async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
14299 init_test(cx, |_| {});
14300
14301 let language = Arc::new(
14302 Language::new(
14303 LanguageConfig {
14304 brackets: BracketPairConfig {
14305 pairs: vec![
14306 BracketPair {
14307 start: "{".to_string(),
14308 end: "}".to_string(),
14309 close: true,
14310 surround: true,
14311 newline: true,
14312 },
14313 BracketPair {
14314 start: "/* ".to_string(),
14315 end: " */".to_string(),
14316 close: true,
14317 surround: true,
14318 newline: true,
14319 },
14320 ],
14321 ..Default::default()
14322 },
14323 ..Default::default()
14324 },
14325 Some(tree_sitter_rust::LANGUAGE.into()),
14326 )
14327 .with_indents_query("")
14328 .unwrap(),
14329 );
14330
14331 let text = concat!(
14332 "{ }\n", //
14333 " x\n", //
14334 " /* */\n", //
14335 "x\n", //
14336 "{{} }\n", //
14337 );
14338
14339 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
14340 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14341 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14342 editor
14343 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
14344 .await;
14345
14346 editor.update_in(cx, |editor, window, cx| {
14347 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14348 s.select_display_ranges([
14349 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
14350 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
14351 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
14352 ])
14353 });
14354 editor.newline(&Newline, window, cx);
14355
14356 assert_eq!(
14357 editor.buffer().read(cx).read(cx).text(),
14358 concat!(
14359 "{ \n", // Suppress rustfmt
14360 "\n", //
14361 "}\n", //
14362 " x\n", //
14363 " /* \n", //
14364 " \n", //
14365 " */\n", //
14366 "x\n", //
14367 "{{} \n", //
14368 "}\n", //
14369 )
14370 );
14371 });
14372}
14373
14374#[gpui::test]
14375fn test_highlighted_ranges(cx: &mut TestAppContext) {
14376 init_test(cx, |_| {});
14377
14378 let editor = cx.add_window(|window, cx| {
14379 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
14380 build_editor(buffer.clone(), window, cx)
14381 });
14382
14383 _ = editor.update(cx, |editor, window, cx| {
14384 struct Type1;
14385 struct Type2;
14386
14387 let buffer = editor.buffer.read(cx).snapshot(cx);
14388
14389 let anchor_range =
14390 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
14391
14392 editor.highlight_background::<Type1>(
14393 &[
14394 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
14395 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
14396 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
14397 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
14398 ],
14399 |_| Hsla::red(),
14400 cx,
14401 );
14402 editor.highlight_background::<Type2>(
14403 &[
14404 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
14405 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
14406 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
14407 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
14408 ],
14409 |_| Hsla::green(),
14410 cx,
14411 );
14412
14413 let snapshot = editor.snapshot(window, cx);
14414 let mut highlighted_ranges = editor.background_highlights_in_range(
14415 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
14416 &snapshot,
14417 cx.theme(),
14418 );
14419 // Enforce a consistent ordering based on color without relying on the ordering of the
14420 // highlight's `TypeId` which is non-executor.
14421 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
14422 assert_eq!(
14423 highlighted_ranges,
14424 &[
14425 (
14426 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
14427 Hsla::red(),
14428 ),
14429 (
14430 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14431 Hsla::red(),
14432 ),
14433 (
14434 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
14435 Hsla::green(),
14436 ),
14437 (
14438 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
14439 Hsla::green(),
14440 ),
14441 ]
14442 );
14443 assert_eq!(
14444 editor.background_highlights_in_range(
14445 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
14446 &snapshot,
14447 cx.theme(),
14448 ),
14449 &[(
14450 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14451 Hsla::red(),
14452 )]
14453 );
14454 });
14455}
14456
14457#[gpui::test]
14458async fn test_following(cx: &mut TestAppContext) {
14459 init_test(cx, |_| {});
14460
14461 let fs = FakeFs::new(cx.executor());
14462 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14463
14464 let buffer = project.update(cx, |project, cx| {
14465 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
14466 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
14467 });
14468 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
14469 let follower = cx.update(|cx| {
14470 cx.open_window(
14471 WindowOptions {
14472 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
14473 gpui::Point::new(px(0.), px(0.)),
14474 gpui::Point::new(px(10.), px(80.)),
14475 ))),
14476 ..Default::default()
14477 },
14478 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
14479 )
14480 .unwrap()
14481 });
14482
14483 let is_still_following = Rc::new(RefCell::new(true));
14484 let follower_edit_event_count = Rc::new(RefCell::new(0));
14485 let pending_update = Rc::new(RefCell::new(None));
14486 let leader_entity = leader.root(cx).unwrap();
14487 let follower_entity = follower.root(cx).unwrap();
14488 _ = follower.update(cx, {
14489 let update = pending_update.clone();
14490 let is_still_following = is_still_following.clone();
14491 let follower_edit_event_count = follower_edit_event_count.clone();
14492 |_, window, cx| {
14493 cx.subscribe_in(
14494 &leader_entity,
14495 window,
14496 move |_, leader, event, window, cx| {
14497 leader.read(cx).add_event_to_update_proto(
14498 event,
14499 &mut update.borrow_mut(),
14500 window,
14501 cx,
14502 );
14503 },
14504 )
14505 .detach();
14506
14507 cx.subscribe_in(
14508 &follower_entity,
14509 window,
14510 move |_, _, event: &EditorEvent, _window, _cx| {
14511 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
14512 *is_still_following.borrow_mut() = false;
14513 }
14514
14515 if let EditorEvent::BufferEdited = event {
14516 *follower_edit_event_count.borrow_mut() += 1;
14517 }
14518 },
14519 )
14520 .detach();
14521 }
14522 });
14523
14524 // Update the selections only
14525 _ = leader.update(cx, |leader, window, cx| {
14526 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14527 s.select_ranges([1..1])
14528 });
14529 });
14530 follower
14531 .update(cx, |follower, window, cx| {
14532 follower.apply_update_proto(
14533 &project,
14534 pending_update.borrow_mut().take().unwrap(),
14535 window,
14536 cx,
14537 )
14538 })
14539 .unwrap()
14540 .await
14541 .unwrap();
14542 _ = follower.update(cx, |follower, _, cx| {
14543 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
14544 });
14545 assert!(*is_still_following.borrow());
14546 assert_eq!(*follower_edit_event_count.borrow(), 0);
14547
14548 // Update the scroll position only
14549 _ = leader.update(cx, |leader, window, cx| {
14550 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14551 });
14552 follower
14553 .update(cx, |follower, window, cx| {
14554 follower.apply_update_proto(
14555 &project,
14556 pending_update.borrow_mut().take().unwrap(),
14557 window,
14558 cx,
14559 )
14560 })
14561 .unwrap()
14562 .await
14563 .unwrap();
14564 assert_eq!(
14565 follower
14566 .update(cx, |follower, _, cx| follower.scroll_position(cx))
14567 .unwrap(),
14568 gpui::Point::new(1.5, 3.5)
14569 );
14570 assert!(*is_still_following.borrow());
14571 assert_eq!(*follower_edit_event_count.borrow(), 0);
14572
14573 // Update the selections and scroll position. The follower's scroll position is updated
14574 // via autoscroll, not via the leader's exact scroll position.
14575 _ = leader.update(cx, |leader, window, cx| {
14576 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14577 s.select_ranges([0..0])
14578 });
14579 leader.request_autoscroll(Autoscroll::newest(), cx);
14580 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14581 });
14582 follower
14583 .update(cx, |follower, window, cx| {
14584 follower.apply_update_proto(
14585 &project,
14586 pending_update.borrow_mut().take().unwrap(),
14587 window,
14588 cx,
14589 )
14590 })
14591 .unwrap()
14592 .await
14593 .unwrap();
14594 _ = follower.update(cx, |follower, _, cx| {
14595 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
14596 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
14597 });
14598 assert!(*is_still_following.borrow());
14599
14600 // Creating a pending selection that precedes another selection
14601 _ = leader.update(cx, |leader, window, cx| {
14602 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14603 s.select_ranges([1..1])
14604 });
14605 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
14606 });
14607 follower
14608 .update(cx, |follower, window, cx| {
14609 follower.apply_update_proto(
14610 &project,
14611 pending_update.borrow_mut().take().unwrap(),
14612 window,
14613 cx,
14614 )
14615 })
14616 .unwrap()
14617 .await
14618 .unwrap();
14619 _ = follower.update(cx, |follower, _, cx| {
14620 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
14621 });
14622 assert!(*is_still_following.borrow());
14623
14624 // Extend the pending selection so that it surrounds another selection
14625 _ = leader.update(cx, |leader, window, cx| {
14626 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
14627 });
14628 follower
14629 .update(cx, |follower, window, cx| {
14630 follower.apply_update_proto(
14631 &project,
14632 pending_update.borrow_mut().take().unwrap(),
14633 window,
14634 cx,
14635 )
14636 })
14637 .unwrap()
14638 .await
14639 .unwrap();
14640 _ = follower.update(cx, |follower, _, cx| {
14641 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
14642 });
14643
14644 // Scrolling locally breaks the follow
14645 _ = follower.update(cx, |follower, window, cx| {
14646 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
14647 follower.set_scroll_anchor(
14648 ScrollAnchor {
14649 anchor: top_anchor,
14650 offset: gpui::Point::new(0.0, 0.5),
14651 },
14652 window,
14653 cx,
14654 );
14655 });
14656 assert!(!(*is_still_following.borrow()));
14657}
14658
14659#[gpui::test]
14660async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
14661 init_test(cx, |_| {});
14662
14663 let fs = FakeFs::new(cx.executor());
14664 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14665 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14666 let pane = workspace
14667 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14668 .unwrap();
14669
14670 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14671
14672 let leader = pane.update_in(cx, |_, window, cx| {
14673 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
14674 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
14675 });
14676
14677 // Start following the editor when it has no excerpts.
14678 let mut state_message =
14679 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14680 let workspace_entity = workspace.root(cx).unwrap();
14681 let follower_1 = cx
14682 .update_window(*workspace.deref(), |_, window, cx| {
14683 Editor::from_state_proto(
14684 workspace_entity,
14685 ViewId {
14686 creator: CollaboratorId::PeerId(PeerId::default()),
14687 id: 0,
14688 },
14689 &mut state_message,
14690 window,
14691 cx,
14692 )
14693 })
14694 .unwrap()
14695 .unwrap()
14696 .await
14697 .unwrap();
14698
14699 let update_message = Rc::new(RefCell::new(None));
14700 follower_1.update_in(cx, {
14701 let update = update_message.clone();
14702 |_, window, cx| {
14703 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
14704 leader.read(cx).add_event_to_update_proto(
14705 event,
14706 &mut update.borrow_mut(),
14707 window,
14708 cx,
14709 );
14710 })
14711 .detach();
14712 }
14713 });
14714
14715 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
14716 (
14717 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
14718 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
14719 )
14720 });
14721
14722 // Insert some excerpts.
14723 leader.update(cx, |leader, cx| {
14724 leader.buffer.update(cx, |multibuffer, cx| {
14725 multibuffer.set_excerpts_for_path(
14726 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
14727 buffer_1.clone(),
14728 vec![
14729 Point::row_range(0..3),
14730 Point::row_range(1..6),
14731 Point::row_range(12..15),
14732 ],
14733 0,
14734 cx,
14735 );
14736 multibuffer.set_excerpts_for_path(
14737 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
14738 buffer_2.clone(),
14739 vec![Point::row_range(0..6), Point::row_range(8..12)],
14740 0,
14741 cx,
14742 );
14743 });
14744 });
14745
14746 // Apply the update of adding the excerpts.
14747 follower_1
14748 .update_in(cx, |follower, window, cx| {
14749 follower.apply_update_proto(
14750 &project,
14751 update_message.borrow().clone().unwrap(),
14752 window,
14753 cx,
14754 )
14755 })
14756 .await
14757 .unwrap();
14758 assert_eq!(
14759 follower_1.update(cx, |editor, cx| editor.text(cx)),
14760 leader.update(cx, |editor, cx| editor.text(cx))
14761 );
14762 update_message.borrow_mut().take();
14763
14764 // Start following separately after it already has excerpts.
14765 let mut state_message =
14766 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14767 let workspace_entity = workspace.root(cx).unwrap();
14768 let follower_2 = cx
14769 .update_window(*workspace.deref(), |_, window, cx| {
14770 Editor::from_state_proto(
14771 workspace_entity,
14772 ViewId {
14773 creator: CollaboratorId::PeerId(PeerId::default()),
14774 id: 0,
14775 },
14776 &mut state_message,
14777 window,
14778 cx,
14779 )
14780 })
14781 .unwrap()
14782 .unwrap()
14783 .await
14784 .unwrap();
14785 assert_eq!(
14786 follower_2.update(cx, |editor, cx| editor.text(cx)),
14787 leader.update(cx, |editor, cx| editor.text(cx))
14788 );
14789
14790 // Remove some excerpts.
14791 leader.update(cx, |leader, cx| {
14792 leader.buffer.update(cx, |multibuffer, cx| {
14793 let excerpt_ids = multibuffer.excerpt_ids();
14794 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
14795 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
14796 });
14797 });
14798
14799 // Apply the update of removing the excerpts.
14800 follower_1
14801 .update_in(cx, |follower, window, cx| {
14802 follower.apply_update_proto(
14803 &project,
14804 update_message.borrow().clone().unwrap(),
14805 window,
14806 cx,
14807 )
14808 })
14809 .await
14810 .unwrap();
14811 follower_2
14812 .update_in(cx, |follower, window, cx| {
14813 follower.apply_update_proto(
14814 &project,
14815 update_message.borrow().clone().unwrap(),
14816 window,
14817 cx,
14818 )
14819 })
14820 .await
14821 .unwrap();
14822 update_message.borrow_mut().take();
14823 assert_eq!(
14824 follower_1.update(cx, |editor, cx| editor.text(cx)),
14825 leader.update(cx, |editor, cx| editor.text(cx))
14826 );
14827}
14828
14829#[gpui::test]
14830async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14831 init_test(cx, |_| {});
14832
14833 let mut cx = EditorTestContext::new(cx).await;
14834 let lsp_store =
14835 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
14836
14837 cx.set_state(indoc! {"
14838 ˇfn func(abc def: i32) -> u32 {
14839 }
14840 "});
14841
14842 cx.update(|_, cx| {
14843 lsp_store.update(cx, |lsp_store, cx| {
14844 lsp_store
14845 .update_diagnostics(
14846 LanguageServerId(0),
14847 lsp::PublishDiagnosticsParams {
14848 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
14849 version: None,
14850 diagnostics: vec![
14851 lsp::Diagnostic {
14852 range: lsp::Range::new(
14853 lsp::Position::new(0, 11),
14854 lsp::Position::new(0, 12),
14855 ),
14856 severity: Some(lsp::DiagnosticSeverity::ERROR),
14857 ..Default::default()
14858 },
14859 lsp::Diagnostic {
14860 range: lsp::Range::new(
14861 lsp::Position::new(0, 12),
14862 lsp::Position::new(0, 15),
14863 ),
14864 severity: Some(lsp::DiagnosticSeverity::ERROR),
14865 ..Default::default()
14866 },
14867 lsp::Diagnostic {
14868 range: lsp::Range::new(
14869 lsp::Position::new(0, 25),
14870 lsp::Position::new(0, 28),
14871 ),
14872 severity: Some(lsp::DiagnosticSeverity::ERROR),
14873 ..Default::default()
14874 },
14875 ],
14876 },
14877 None,
14878 DiagnosticSourceKind::Pushed,
14879 &[],
14880 cx,
14881 )
14882 .unwrap()
14883 });
14884 });
14885
14886 executor.run_until_parked();
14887
14888 cx.update_editor(|editor, window, cx| {
14889 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14890 });
14891
14892 cx.assert_editor_state(indoc! {"
14893 fn func(abc def: i32) -> ˇu32 {
14894 }
14895 "});
14896
14897 cx.update_editor(|editor, window, cx| {
14898 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14899 });
14900
14901 cx.assert_editor_state(indoc! {"
14902 fn func(abc ˇdef: i32) -> u32 {
14903 }
14904 "});
14905
14906 cx.update_editor(|editor, window, cx| {
14907 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14908 });
14909
14910 cx.assert_editor_state(indoc! {"
14911 fn func(abcˇ def: i32) -> u32 {
14912 }
14913 "});
14914
14915 cx.update_editor(|editor, window, cx| {
14916 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14917 });
14918
14919 cx.assert_editor_state(indoc! {"
14920 fn func(abc def: i32) -> ˇu32 {
14921 }
14922 "});
14923}
14924
14925#[gpui::test]
14926async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14927 init_test(cx, |_| {});
14928
14929 let mut cx = EditorTestContext::new(cx).await;
14930
14931 let diff_base = r#"
14932 use some::mod;
14933
14934 const A: u32 = 42;
14935
14936 fn main() {
14937 println!("hello");
14938
14939 println!("world");
14940 }
14941 "#
14942 .unindent();
14943
14944 // Edits are modified, removed, modified, added
14945 cx.set_state(
14946 &r#"
14947 use some::modified;
14948
14949 ˇ
14950 fn main() {
14951 println!("hello there");
14952
14953 println!("around the");
14954 println!("world");
14955 }
14956 "#
14957 .unindent(),
14958 );
14959
14960 cx.set_head_text(&diff_base);
14961 executor.run_until_parked();
14962
14963 cx.update_editor(|editor, window, cx| {
14964 //Wrap around the bottom of the buffer
14965 for _ in 0..3 {
14966 editor.go_to_next_hunk(&GoToHunk, window, cx);
14967 }
14968 });
14969
14970 cx.assert_editor_state(
14971 &r#"
14972 ˇuse some::modified;
14973
14974
14975 fn main() {
14976 println!("hello there");
14977
14978 println!("around the");
14979 println!("world");
14980 }
14981 "#
14982 .unindent(),
14983 );
14984
14985 cx.update_editor(|editor, window, cx| {
14986 //Wrap around the top of the buffer
14987 for _ in 0..2 {
14988 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14989 }
14990 });
14991
14992 cx.assert_editor_state(
14993 &r#"
14994 use some::modified;
14995
14996
14997 fn main() {
14998 ˇ println!("hello there");
14999
15000 println!("around the");
15001 println!("world");
15002 }
15003 "#
15004 .unindent(),
15005 );
15006
15007 cx.update_editor(|editor, window, cx| {
15008 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15009 });
15010
15011 cx.assert_editor_state(
15012 &r#"
15013 use some::modified;
15014
15015 ˇ
15016 fn main() {
15017 println!("hello there");
15018
15019 println!("around the");
15020 println!("world");
15021 }
15022 "#
15023 .unindent(),
15024 );
15025
15026 cx.update_editor(|editor, window, cx| {
15027 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15028 });
15029
15030 cx.assert_editor_state(
15031 &r#"
15032 ˇuse some::modified;
15033
15034
15035 fn main() {
15036 println!("hello there");
15037
15038 println!("around the");
15039 println!("world");
15040 }
15041 "#
15042 .unindent(),
15043 );
15044
15045 cx.update_editor(|editor, window, cx| {
15046 for _ in 0..2 {
15047 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
15048 }
15049 });
15050
15051 cx.assert_editor_state(
15052 &r#"
15053 use some::modified;
15054
15055
15056 fn main() {
15057 ˇ println!("hello there");
15058
15059 println!("around the");
15060 println!("world");
15061 }
15062 "#
15063 .unindent(),
15064 );
15065
15066 cx.update_editor(|editor, window, cx| {
15067 editor.fold(&Fold, window, cx);
15068 });
15069
15070 cx.update_editor(|editor, window, cx| {
15071 editor.go_to_next_hunk(&GoToHunk, window, cx);
15072 });
15073
15074 cx.assert_editor_state(
15075 &r#"
15076 ˇuse some::modified;
15077
15078
15079 fn main() {
15080 println!("hello there");
15081
15082 println!("around the");
15083 println!("world");
15084 }
15085 "#
15086 .unindent(),
15087 );
15088}
15089
15090#[test]
15091fn test_split_words() {
15092 fn split(text: &str) -> Vec<&str> {
15093 split_words(text).collect()
15094 }
15095
15096 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
15097 assert_eq!(split("hello_world"), &["hello_", "world"]);
15098 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
15099 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
15100 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
15101 assert_eq!(split("helloworld"), &["helloworld"]);
15102
15103 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
15104}
15105
15106#[gpui::test]
15107async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
15108 init_test(cx, |_| {});
15109
15110 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
15111 let mut assert = |before, after| {
15112 let _state_context = cx.set_state(before);
15113 cx.run_until_parked();
15114 cx.update_editor(|editor, window, cx| {
15115 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
15116 });
15117 cx.run_until_parked();
15118 cx.assert_editor_state(after);
15119 };
15120
15121 // Outside bracket jumps to outside of matching bracket
15122 assert("console.logˇ(var);", "console.log(var)ˇ;");
15123 assert("console.log(var)ˇ;", "console.logˇ(var);");
15124
15125 // Inside bracket jumps to inside of matching bracket
15126 assert("console.log(ˇvar);", "console.log(varˇ);");
15127 assert("console.log(varˇ);", "console.log(ˇvar);");
15128
15129 // When outside a bracket and inside, favor jumping to the inside bracket
15130 assert(
15131 "console.log('foo', [1, 2, 3]ˇ);",
15132 "console.log(ˇ'foo', [1, 2, 3]);",
15133 );
15134 assert(
15135 "console.log(ˇ'foo', [1, 2, 3]);",
15136 "console.log('foo', [1, 2, 3]ˇ);",
15137 );
15138
15139 // Bias forward if two options are equally likely
15140 assert(
15141 "let result = curried_fun()ˇ();",
15142 "let result = curried_fun()()ˇ;",
15143 );
15144
15145 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
15146 assert(
15147 indoc! {"
15148 function test() {
15149 console.log('test')ˇ
15150 }"},
15151 indoc! {"
15152 function test() {
15153 console.logˇ('test')
15154 }"},
15155 );
15156}
15157
15158#[gpui::test]
15159async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
15160 init_test(cx, |_| {});
15161
15162 let fs = FakeFs::new(cx.executor());
15163 fs.insert_tree(
15164 path!("/a"),
15165 json!({
15166 "main.rs": "fn main() { let a = 5; }",
15167 "other.rs": "// Test file",
15168 }),
15169 )
15170 .await;
15171 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15172
15173 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15174 language_registry.add(Arc::new(Language::new(
15175 LanguageConfig {
15176 name: "Rust".into(),
15177 matcher: LanguageMatcher {
15178 path_suffixes: vec!["rs".to_string()],
15179 ..Default::default()
15180 },
15181 brackets: BracketPairConfig {
15182 pairs: vec![BracketPair {
15183 start: "{".to_string(),
15184 end: "}".to_string(),
15185 close: true,
15186 surround: true,
15187 newline: true,
15188 }],
15189 disabled_scopes_by_bracket_ix: Vec::new(),
15190 },
15191 ..Default::default()
15192 },
15193 Some(tree_sitter_rust::LANGUAGE.into()),
15194 )));
15195 let mut fake_servers = language_registry.register_fake_lsp(
15196 "Rust",
15197 FakeLspAdapter {
15198 capabilities: lsp::ServerCapabilities {
15199 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15200 first_trigger_character: "{".to_string(),
15201 more_trigger_character: None,
15202 }),
15203 ..Default::default()
15204 },
15205 ..Default::default()
15206 },
15207 );
15208
15209 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15210
15211 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15212
15213 let worktree_id = workspace
15214 .update(cx, |workspace, _, cx| {
15215 workspace.project().update(cx, |project, cx| {
15216 project.worktrees(cx).next().unwrap().read(cx).id()
15217 })
15218 })
15219 .unwrap();
15220
15221 let buffer = project
15222 .update(cx, |project, cx| {
15223 project.open_local_buffer(path!("/a/main.rs"), cx)
15224 })
15225 .await
15226 .unwrap();
15227 let editor_handle = workspace
15228 .update(cx, |workspace, window, cx| {
15229 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
15230 })
15231 .unwrap()
15232 .await
15233 .unwrap()
15234 .downcast::<Editor>()
15235 .unwrap();
15236
15237 cx.executor().start_waiting();
15238 let fake_server = fake_servers.next().await.unwrap();
15239
15240 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
15241 |params, _| async move {
15242 assert_eq!(
15243 params.text_document_position.text_document.uri,
15244 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
15245 );
15246 assert_eq!(
15247 params.text_document_position.position,
15248 lsp::Position::new(0, 21),
15249 );
15250
15251 Ok(Some(vec![lsp::TextEdit {
15252 new_text: "]".to_string(),
15253 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15254 }]))
15255 },
15256 );
15257
15258 editor_handle.update_in(cx, |editor, window, cx| {
15259 window.focus(&editor.focus_handle(cx));
15260 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15261 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
15262 });
15263 editor.handle_input("{", window, cx);
15264 });
15265
15266 cx.executor().run_until_parked();
15267
15268 buffer.update(cx, |buffer, _| {
15269 assert_eq!(
15270 buffer.text(),
15271 "fn main() { let a = {5}; }",
15272 "No extra braces from on type formatting should appear in the buffer"
15273 )
15274 });
15275}
15276
15277#[gpui::test(iterations = 20, seeds(31))]
15278async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
15279 init_test(cx, |_| {});
15280
15281 let mut cx = EditorLspTestContext::new_rust(
15282 lsp::ServerCapabilities {
15283 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15284 first_trigger_character: ".".to_string(),
15285 more_trigger_character: None,
15286 }),
15287 ..Default::default()
15288 },
15289 cx,
15290 )
15291 .await;
15292
15293 cx.update_buffer(|buffer, _| {
15294 // This causes autoindent to be async.
15295 buffer.set_sync_parse_timeout(Duration::ZERO)
15296 });
15297
15298 cx.set_state("fn c() {\n d()ˇ\n}\n");
15299 cx.simulate_keystroke("\n");
15300 cx.run_until_parked();
15301
15302 let buffer_cloned =
15303 cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap().clone());
15304 let mut request =
15305 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
15306 let buffer_cloned = buffer_cloned.clone();
15307 async move {
15308 buffer_cloned.update(&mut cx, |buffer, _| {
15309 assert_eq!(
15310 buffer.text(),
15311 "fn c() {\n d()\n .\n}\n",
15312 "OnTypeFormatting should triggered after autoindent applied"
15313 )
15314 })?;
15315
15316 Ok(Some(vec![]))
15317 }
15318 });
15319
15320 cx.simulate_keystroke(".");
15321 cx.run_until_parked();
15322
15323 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
15324 assert!(request.next().await.is_some());
15325 request.close();
15326 assert!(request.next().await.is_none());
15327}
15328
15329#[gpui::test]
15330async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
15331 init_test(cx, |_| {});
15332
15333 let fs = FakeFs::new(cx.executor());
15334 fs.insert_tree(
15335 path!("/a"),
15336 json!({
15337 "main.rs": "fn main() { let a = 5; }",
15338 "other.rs": "// Test file",
15339 }),
15340 )
15341 .await;
15342
15343 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15344
15345 let server_restarts = Arc::new(AtomicUsize::new(0));
15346 let closure_restarts = Arc::clone(&server_restarts);
15347 let language_server_name = "test language server";
15348 let language_name: LanguageName = "Rust".into();
15349
15350 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15351 language_registry.add(Arc::new(Language::new(
15352 LanguageConfig {
15353 name: language_name.clone(),
15354 matcher: LanguageMatcher {
15355 path_suffixes: vec!["rs".to_string()],
15356 ..Default::default()
15357 },
15358 ..Default::default()
15359 },
15360 Some(tree_sitter_rust::LANGUAGE.into()),
15361 )));
15362 let mut fake_servers = language_registry.register_fake_lsp(
15363 "Rust",
15364 FakeLspAdapter {
15365 name: language_server_name,
15366 initialization_options: Some(json!({
15367 "testOptionValue": true
15368 })),
15369 initializer: Some(Box::new(move |fake_server| {
15370 let task_restarts = Arc::clone(&closure_restarts);
15371 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
15372 task_restarts.fetch_add(1, atomic::Ordering::Release);
15373 futures::future::ready(Ok(()))
15374 });
15375 })),
15376 ..Default::default()
15377 },
15378 );
15379
15380 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15381 let _buffer = project
15382 .update(cx, |project, cx| {
15383 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
15384 })
15385 .await
15386 .unwrap();
15387 let _fake_server = fake_servers.next().await.unwrap();
15388 update_test_language_settings(cx, |language_settings| {
15389 language_settings.languages.0.insert(
15390 language_name.clone(),
15391 LanguageSettingsContent {
15392 tab_size: NonZeroU32::new(8),
15393 ..Default::default()
15394 },
15395 );
15396 });
15397 cx.executor().run_until_parked();
15398 assert_eq!(
15399 server_restarts.load(atomic::Ordering::Acquire),
15400 0,
15401 "Should not restart LSP server on an unrelated change"
15402 );
15403
15404 update_test_project_settings(cx, |project_settings| {
15405 project_settings.lsp.insert(
15406 "Some other server name".into(),
15407 LspSettings {
15408 binary: None,
15409 settings: None,
15410 initialization_options: Some(json!({
15411 "some other init value": false
15412 })),
15413 enable_lsp_tasks: false,
15414 },
15415 );
15416 });
15417 cx.executor().run_until_parked();
15418 assert_eq!(
15419 server_restarts.load(atomic::Ordering::Acquire),
15420 0,
15421 "Should not restart LSP server on an unrelated LSP settings change"
15422 );
15423
15424 update_test_project_settings(cx, |project_settings| {
15425 project_settings.lsp.insert(
15426 language_server_name.into(),
15427 LspSettings {
15428 binary: None,
15429 settings: None,
15430 initialization_options: Some(json!({
15431 "anotherInitValue": false
15432 })),
15433 enable_lsp_tasks: false,
15434 },
15435 );
15436 });
15437 cx.executor().run_until_parked();
15438 assert_eq!(
15439 server_restarts.load(atomic::Ordering::Acquire),
15440 1,
15441 "Should restart LSP server on a related LSP settings change"
15442 );
15443
15444 update_test_project_settings(cx, |project_settings| {
15445 project_settings.lsp.insert(
15446 language_server_name.into(),
15447 LspSettings {
15448 binary: None,
15449 settings: None,
15450 initialization_options: Some(json!({
15451 "anotherInitValue": false
15452 })),
15453 enable_lsp_tasks: false,
15454 },
15455 );
15456 });
15457 cx.executor().run_until_parked();
15458 assert_eq!(
15459 server_restarts.load(atomic::Ordering::Acquire),
15460 1,
15461 "Should not restart LSP server on a related LSP settings change that is the same"
15462 );
15463
15464 update_test_project_settings(cx, |project_settings| {
15465 project_settings.lsp.insert(
15466 language_server_name.into(),
15467 LspSettings {
15468 binary: None,
15469 settings: None,
15470 initialization_options: None,
15471 enable_lsp_tasks: false,
15472 },
15473 );
15474 });
15475 cx.executor().run_until_parked();
15476 assert_eq!(
15477 server_restarts.load(atomic::Ordering::Acquire),
15478 2,
15479 "Should restart LSP server on another related LSP settings change"
15480 );
15481}
15482
15483#[gpui::test]
15484async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
15485 init_test(cx, |_| {});
15486
15487 let mut cx = EditorLspTestContext::new_rust(
15488 lsp::ServerCapabilities {
15489 completion_provider: Some(lsp::CompletionOptions {
15490 trigger_characters: Some(vec![".".to_string()]),
15491 resolve_provider: Some(true),
15492 ..Default::default()
15493 }),
15494 ..Default::default()
15495 },
15496 cx,
15497 )
15498 .await;
15499
15500 cx.set_state("fn main() { let a = 2ˇ; }");
15501 cx.simulate_keystroke(".");
15502 let completion_item = lsp::CompletionItem {
15503 label: "some".into(),
15504 kind: Some(lsp::CompletionItemKind::SNIPPET),
15505 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15506 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15507 kind: lsp::MarkupKind::Markdown,
15508 value: "```rust\nSome(2)\n```".to_string(),
15509 })),
15510 deprecated: Some(false),
15511 sort_text: Some("fffffff2".to_string()),
15512 filter_text: Some("some".to_string()),
15513 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15514 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15515 range: lsp::Range {
15516 start: lsp::Position {
15517 line: 0,
15518 character: 22,
15519 },
15520 end: lsp::Position {
15521 line: 0,
15522 character: 22,
15523 },
15524 },
15525 new_text: "Some(2)".to_string(),
15526 })),
15527 additional_text_edits: Some(vec![lsp::TextEdit {
15528 range: lsp::Range {
15529 start: lsp::Position {
15530 line: 0,
15531 character: 20,
15532 },
15533 end: lsp::Position {
15534 line: 0,
15535 character: 22,
15536 },
15537 },
15538 new_text: "".to_string(),
15539 }]),
15540 ..Default::default()
15541 };
15542
15543 let closure_completion_item = completion_item.clone();
15544 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15545 let task_completion_item = closure_completion_item.clone();
15546 async move {
15547 Ok(Some(lsp::CompletionResponse::Array(vec![
15548 task_completion_item,
15549 ])))
15550 }
15551 });
15552
15553 request.next().await;
15554
15555 cx.condition(|editor, _| editor.context_menu_visible())
15556 .await;
15557 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15558 editor
15559 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15560 .unwrap()
15561 });
15562 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
15563
15564 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
15565 let task_completion_item = completion_item.clone();
15566 async move { Ok(task_completion_item) }
15567 })
15568 .next()
15569 .await
15570 .unwrap();
15571 apply_additional_edits.await.unwrap();
15572 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
15573}
15574
15575#[gpui::test]
15576async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
15577 init_test(cx, |_| {});
15578
15579 let mut cx = EditorLspTestContext::new_rust(
15580 lsp::ServerCapabilities {
15581 completion_provider: Some(lsp::CompletionOptions {
15582 trigger_characters: Some(vec![".".to_string()]),
15583 resolve_provider: Some(true),
15584 ..Default::default()
15585 }),
15586 ..Default::default()
15587 },
15588 cx,
15589 )
15590 .await;
15591
15592 cx.set_state("fn main() { let a = 2ˇ; }");
15593 cx.simulate_keystroke(".");
15594
15595 let item1 = lsp::CompletionItem {
15596 label: "method id()".to_string(),
15597 filter_text: Some("id".to_string()),
15598 detail: None,
15599 documentation: None,
15600 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15601 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15602 new_text: ".id".to_string(),
15603 })),
15604 ..lsp::CompletionItem::default()
15605 };
15606
15607 let item2 = lsp::CompletionItem {
15608 label: "other".to_string(),
15609 filter_text: Some("other".to_string()),
15610 detail: None,
15611 documentation: None,
15612 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15613 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15614 new_text: ".other".to_string(),
15615 })),
15616 ..lsp::CompletionItem::default()
15617 };
15618
15619 let item1 = item1.clone();
15620 cx.set_request_handler::<lsp::request::Completion, _, _>({
15621 let item1 = item1.clone();
15622 move |_, _, _| {
15623 let item1 = item1.clone();
15624 let item2 = item2.clone();
15625 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
15626 }
15627 })
15628 .next()
15629 .await;
15630
15631 cx.condition(|editor, _| editor.context_menu_visible())
15632 .await;
15633 cx.update_editor(|editor, _, _| {
15634 let context_menu = editor.context_menu.borrow_mut();
15635 let context_menu = context_menu
15636 .as_ref()
15637 .expect("Should have the context menu deployed");
15638 match context_menu {
15639 CodeContextMenu::Completions(completions_menu) => {
15640 let completions = completions_menu.completions.borrow_mut();
15641 assert_eq!(
15642 completions
15643 .iter()
15644 .map(|completion| &completion.label.text)
15645 .collect::<Vec<_>>(),
15646 vec!["method id()", "other"]
15647 )
15648 }
15649 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15650 }
15651 });
15652
15653 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
15654 let item1 = item1.clone();
15655 move |_, item_to_resolve, _| {
15656 let item1 = item1.clone();
15657 async move {
15658 if item1 == item_to_resolve {
15659 Ok(lsp::CompletionItem {
15660 label: "method id()".to_string(),
15661 filter_text: Some("id".to_string()),
15662 detail: Some("Now resolved!".to_string()),
15663 documentation: Some(lsp::Documentation::String("Docs".to_string())),
15664 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15665 range: lsp::Range::new(
15666 lsp::Position::new(0, 22),
15667 lsp::Position::new(0, 22),
15668 ),
15669 new_text: ".id".to_string(),
15670 })),
15671 ..lsp::CompletionItem::default()
15672 })
15673 } else {
15674 Ok(item_to_resolve)
15675 }
15676 }
15677 }
15678 })
15679 .next()
15680 .await
15681 .unwrap();
15682 cx.run_until_parked();
15683
15684 cx.update_editor(|editor, window, cx| {
15685 editor.context_menu_next(&Default::default(), window, cx);
15686 });
15687
15688 cx.update_editor(|editor, _, _| {
15689 let context_menu = editor.context_menu.borrow_mut();
15690 let context_menu = context_menu
15691 .as_ref()
15692 .expect("Should have the context menu deployed");
15693 match context_menu {
15694 CodeContextMenu::Completions(completions_menu) => {
15695 let completions = completions_menu.completions.borrow_mut();
15696 assert_eq!(
15697 completions
15698 .iter()
15699 .map(|completion| &completion.label.text)
15700 .collect::<Vec<_>>(),
15701 vec!["method id() Now resolved!", "other"],
15702 "Should update first completion label, but not second as the filter text did not match."
15703 );
15704 }
15705 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15706 }
15707 });
15708}
15709
15710#[gpui::test]
15711async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
15712 init_test(cx, |_| {});
15713 let mut cx = EditorLspTestContext::new_rust(
15714 lsp::ServerCapabilities {
15715 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
15716 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
15717 completion_provider: Some(lsp::CompletionOptions {
15718 resolve_provider: Some(true),
15719 ..Default::default()
15720 }),
15721 ..Default::default()
15722 },
15723 cx,
15724 )
15725 .await;
15726 cx.set_state(indoc! {"
15727 struct TestStruct {
15728 field: i32
15729 }
15730
15731 fn mainˇ() {
15732 let unused_var = 42;
15733 let test_struct = TestStruct { field: 42 };
15734 }
15735 "});
15736 let symbol_range = cx.lsp_range(indoc! {"
15737 struct TestStruct {
15738 field: i32
15739 }
15740
15741 «fn main»() {
15742 let unused_var = 42;
15743 let test_struct = TestStruct { field: 42 };
15744 }
15745 "});
15746 let mut hover_requests =
15747 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
15748 Ok(Some(lsp::Hover {
15749 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
15750 kind: lsp::MarkupKind::Markdown,
15751 value: "Function documentation".to_string(),
15752 }),
15753 range: Some(symbol_range),
15754 }))
15755 });
15756
15757 // Case 1: Test that code action menu hide hover popover
15758 cx.dispatch_action(Hover);
15759 hover_requests.next().await;
15760 cx.condition(|editor, _| editor.hover_state.visible()).await;
15761 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
15762 move |_, _, _| async move {
15763 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
15764 lsp::CodeAction {
15765 title: "Remove unused variable".to_string(),
15766 kind: Some(CodeActionKind::QUICKFIX),
15767 edit: Some(lsp::WorkspaceEdit {
15768 changes: Some(
15769 [(
15770 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
15771 vec![lsp::TextEdit {
15772 range: lsp::Range::new(
15773 lsp::Position::new(5, 4),
15774 lsp::Position::new(5, 27),
15775 ),
15776 new_text: "".to_string(),
15777 }],
15778 )]
15779 .into_iter()
15780 .collect(),
15781 ),
15782 ..Default::default()
15783 }),
15784 ..Default::default()
15785 },
15786 )]))
15787 },
15788 );
15789 cx.update_editor(|editor, window, cx| {
15790 editor.toggle_code_actions(
15791 &ToggleCodeActions {
15792 deployed_from: None,
15793 quick_launch: false,
15794 },
15795 window,
15796 cx,
15797 );
15798 });
15799 code_action_requests.next().await;
15800 cx.run_until_parked();
15801 cx.condition(|editor, _| editor.context_menu_visible())
15802 .await;
15803 cx.update_editor(|editor, _, _| {
15804 assert!(
15805 !editor.hover_state.visible(),
15806 "Hover popover should be hidden when code action menu is shown"
15807 );
15808 // Hide code actions
15809 editor.context_menu.take();
15810 });
15811
15812 // Case 2: Test that code completions hide hover popover
15813 cx.dispatch_action(Hover);
15814 hover_requests.next().await;
15815 cx.condition(|editor, _| editor.hover_state.visible()).await;
15816 let counter = Arc::new(AtomicUsize::new(0));
15817 let mut completion_requests =
15818 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15819 let counter = counter.clone();
15820 async move {
15821 counter.fetch_add(1, atomic::Ordering::Release);
15822 Ok(Some(lsp::CompletionResponse::Array(vec![
15823 lsp::CompletionItem {
15824 label: "main".into(),
15825 kind: Some(lsp::CompletionItemKind::FUNCTION),
15826 detail: Some("() -> ()".to_string()),
15827 ..Default::default()
15828 },
15829 lsp::CompletionItem {
15830 label: "TestStruct".into(),
15831 kind: Some(lsp::CompletionItemKind::STRUCT),
15832 detail: Some("struct TestStruct".to_string()),
15833 ..Default::default()
15834 },
15835 ])))
15836 }
15837 });
15838 cx.update_editor(|editor, window, cx| {
15839 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15840 });
15841 completion_requests.next().await;
15842 cx.condition(|editor, _| editor.context_menu_visible())
15843 .await;
15844 cx.update_editor(|editor, _, _| {
15845 assert!(
15846 !editor.hover_state.visible(),
15847 "Hover popover should be hidden when completion menu is shown"
15848 );
15849 });
15850}
15851
15852#[gpui::test]
15853async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
15854 init_test(cx, |_| {});
15855
15856 let mut cx = EditorLspTestContext::new_rust(
15857 lsp::ServerCapabilities {
15858 completion_provider: Some(lsp::CompletionOptions {
15859 trigger_characters: Some(vec![".".to_string()]),
15860 resolve_provider: Some(true),
15861 ..Default::default()
15862 }),
15863 ..Default::default()
15864 },
15865 cx,
15866 )
15867 .await;
15868
15869 cx.set_state("fn main() { let a = 2ˇ; }");
15870 cx.simulate_keystroke(".");
15871
15872 let unresolved_item_1 = lsp::CompletionItem {
15873 label: "id".to_string(),
15874 filter_text: Some("id".to_string()),
15875 detail: None,
15876 documentation: None,
15877 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15878 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15879 new_text: ".id".to_string(),
15880 })),
15881 ..lsp::CompletionItem::default()
15882 };
15883 let resolved_item_1 = lsp::CompletionItem {
15884 additional_text_edits: Some(vec![lsp::TextEdit {
15885 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15886 new_text: "!!".to_string(),
15887 }]),
15888 ..unresolved_item_1.clone()
15889 };
15890 let unresolved_item_2 = lsp::CompletionItem {
15891 label: "other".to_string(),
15892 filter_text: Some("other".to_string()),
15893 detail: None,
15894 documentation: None,
15895 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15896 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15897 new_text: ".other".to_string(),
15898 })),
15899 ..lsp::CompletionItem::default()
15900 };
15901 let resolved_item_2 = lsp::CompletionItem {
15902 additional_text_edits: Some(vec![lsp::TextEdit {
15903 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15904 new_text: "??".to_string(),
15905 }]),
15906 ..unresolved_item_2.clone()
15907 };
15908
15909 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
15910 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
15911 cx.lsp
15912 .server
15913 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15914 let unresolved_item_1 = unresolved_item_1.clone();
15915 let resolved_item_1 = resolved_item_1.clone();
15916 let unresolved_item_2 = unresolved_item_2.clone();
15917 let resolved_item_2 = resolved_item_2.clone();
15918 let resolve_requests_1 = resolve_requests_1.clone();
15919 let resolve_requests_2 = resolve_requests_2.clone();
15920 move |unresolved_request, _| {
15921 let unresolved_item_1 = unresolved_item_1.clone();
15922 let resolved_item_1 = resolved_item_1.clone();
15923 let unresolved_item_2 = unresolved_item_2.clone();
15924 let resolved_item_2 = resolved_item_2.clone();
15925 let resolve_requests_1 = resolve_requests_1.clone();
15926 let resolve_requests_2 = resolve_requests_2.clone();
15927 async move {
15928 if unresolved_request == unresolved_item_1 {
15929 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
15930 Ok(resolved_item_1.clone())
15931 } else if unresolved_request == unresolved_item_2 {
15932 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
15933 Ok(resolved_item_2.clone())
15934 } else {
15935 panic!("Unexpected completion item {unresolved_request:?}")
15936 }
15937 }
15938 }
15939 })
15940 .detach();
15941
15942 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15943 let unresolved_item_1 = unresolved_item_1.clone();
15944 let unresolved_item_2 = unresolved_item_2.clone();
15945 async move {
15946 Ok(Some(lsp::CompletionResponse::Array(vec![
15947 unresolved_item_1,
15948 unresolved_item_2,
15949 ])))
15950 }
15951 })
15952 .next()
15953 .await;
15954
15955 cx.condition(|editor, _| editor.context_menu_visible())
15956 .await;
15957 cx.update_editor(|editor, _, _| {
15958 let context_menu = editor.context_menu.borrow_mut();
15959 let context_menu = context_menu
15960 .as_ref()
15961 .expect("Should have the context menu deployed");
15962 match context_menu {
15963 CodeContextMenu::Completions(completions_menu) => {
15964 let completions = completions_menu.completions.borrow_mut();
15965 assert_eq!(
15966 completions
15967 .iter()
15968 .map(|completion| &completion.label.text)
15969 .collect::<Vec<_>>(),
15970 vec!["id", "other"]
15971 )
15972 }
15973 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15974 }
15975 });
15976 cx.run_until_parked();
15977
15978 cx.update_editor(|editor, window, cx| {
15979 editor.context_menu_next(&ContextMenuNext, window, cx);
15980 });
15981 cx.run_until_parked();
15982 cx.update_editor(|editor, window, cx| {
15983 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15984 });
15985 cx.run_until_parked();
15986 cx.update_editor(|editor, window, cx| {
15987 editor.context_menu_next(&ContextMenuNext, window, cx);
15988 });
15989 cx.run_until_parked();
15990 cx.update_editor(|editor, window, cx| {
15991 editor
15992 .compose_completion(&ComposeCompletion::default(), window, cx)
15993 .expect("No task returned")
15994 })
15995 .await
15996 .expect("Completion failed");
15997 cx.run_until_parked();
15998
15999 cx.update_editor(|editor, _, cx| {
16000 assert_eq!(
16001 resolve_requests_1.load(atomic::Ordering::Acquire),
16002 1,
16003 "Should always resolve once despite multiple selections"
16004 );
16005 assert_eq!(
16006 resolve_requests_2.load(atomic::Ordering::Acquire),
16007 1,
16008 "Should always resolve once after multiple selections and applying the completion"
16009 );
16010 assert_eq!(
16011 editor.text(cx),
16012 "fn main() { let a = ??.other; }",
16013 "Should use resolved data when applying the completion"
16014 );
16015 });
16016}
16017
16018#[gpui::test]
16019async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
16020 init_test(cx, |_| {});
16021
16022 let item_0 = lsp::CompletionItem {
16023 label: "abs".into(),
16024 insert_text: Some("abs".into()),
16025 data: Some(json!({ "very": "special"})),
16026 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
16027 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
16028 lsp::InsertReplaceEdit {
16029 new_text: "abs".to_string(),
16030 insert: lsp::Range::default(),
16031 replace: lsp::Range::default(),
16032 },
16033 )),
16034 ..lsp::CompletionItem::default()
16035 };
16036 let items = iter::once(item_0.clone())
16037 .chain((11..51).map(|i| lsp::CompletionItem {
16038 label: format!("item_{}", i),
16039 insert_text: Some(format!("item_{}", i)),
16040 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
16041 ..lsp::CompletionItem::default()
16042 }))
16043 .collect::<Vec<_>>();
16044
16045 let default_commit_characters = vec!["?".to_string()];
16046 let default_data = json!({ "default": "data"});
16047 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
16048 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
16049 let default_edit_range = lsp::Range {
16050 start: lsp::Position {
16051 line: 0,
16052 character: 5,
16053 },
16054 end: lsp::Position {
16055 line: 0,
16056 character: 5,
16057 },
16058 };
16059
16060 let mut cx = EditorLspTestContext::new_rust(
16061 lsp::ServerCapabilities {
16062 completion_provider: Some(lsp::CompletionOptions {
16063 trigger_characters: Some(vec![".".to_string()]),
16064 resolve_provider: Some(true),
16065 ..Default::default()
16066 }),
16067 ..Default::default()
16068 },
16069 cx,
16070 )
16071 .await;
16072
16073 cx.set_state("fn main() { let a = 2ˇ; }");
16074 cx.simulate_keystroke(".");
16075
16076 let completion_data = default_data.clone();
16077 let completion_characters = default_commit_characters.clone();
16078 let completion_items = items.clone();
16079 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16080 let default_data = completion_data.clone();
16081 let default_commit_characters = completion_characters.clone();
16082 let items = completion_items.clone();
16083 async move {
16084 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16085 items,
16086 item_defaults: Some(lsp::CompletionListItemDefaults {
16087 data: Some(default_data.clone()),
16088 commit_characters: Some(default_commit_characters.clone()),
16089 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
16090 default_edit_range,
16091 )),
16092 insert_text_format: Some(default_insert_text_format),
16093 insert_text_mode: Some(default_insert_text_mode),
16094 }),
16095 ..lsp::CompletionList::default()
16096 })))
16097 }
16098 })
16099 .next()
16100 .await;
16101
16102 let resolved_items = Arc::new(Mutex::new(Vec::new()));
16103 cx.lsp
16104 .server
16105 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
16106 let closure_resolved_items = resolved_items.clone();
16107 move |item_to_resolve, _| {
16108 let closure_resolved_items = closure_resolved_items.clone();
16109 async move {
16110 closure_resolved_items.lock().push(item_to_resolve.clone());
16111 Ok(item_to_resolve)
16112 }
16113 }
16114 })
16115 .detach();
16116
16117 cx.condition(|editor, _| editor.context_menu_visible())
16118 .await;
16119 cx.run_until_parked();
16120 cx.update_editor(|editor, _, _| {
16121 let menu = editor.context_menu.borrow_mut();
16122 match menu.as_ref().expect("should have the completions menu") {
16123 CodeContextMenu::Completions(completions_menu) => {
16124 assert_eq!(
16125 completions_menu
16126 .entries
16127 .borrow()
16128 .iter()
16129 .map(|mat| mat.string.clone())
16130 .collect::<Vec<String>>(),
16131 items
16132 .iter()
16133 .map(|completion| completion.label.clone())
16134 .collect::<Vec<String>>()
16135 );
16136 }
16137 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
16138 }
16139 });
16140 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
16141 // with 4 from the end.
16142 assert_eq!(
16143 *resolved_items.lock(),
16144 [&items[0..16], &items[items.len() - 4..items.len()]]
16145 .concat()
16146 .iter()
16147 .cloned()
16148 .map(|mut item| {
16149 if item.data.is_none() {
16150 item.data = Some(default_data.clone());
16151 }
16152 item
16153 })
16154 .collect::<Vec<lsp::CompletionItem>>(),
16155 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
16156 );
16157 resolved_items.lock().clear();
16158
16159 cx.update_editor(|editor, window, cx| {
16160 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
16161 });
16162 cx.run_until_parked();
16163 // Completions that have already been resolved are skipped.
16164 assert_eq!(
16165 *resolved_items.lock(),
16166 items[items.len() - 17..items.len() - 4]
16167 .iter()
16168 .cloned()
16169 .map(|mut item| {
16170 if item.data.is_none() {
16171 item.data = Some(default_data.clone());
16172 }
16173 item
16174 })
16175 .collect::<Vec<lsp::CompletionItem>>()
16176 );
16177 resolved_items.lock().clear();
16178}
16179
16180#[gpui::test]
16181async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
16182 init_test(cx, |_| {});
16183
16184 let mut cx = EditorLspTestContext::new(
16185 Language::new(
16186 LanguageConfig {
16187 matcher: LanguageMatcher {
16188 path_suffixes: vec!["jsx".into()],
16189 ..Default::default()
16190 },
16191 overrides: [(
16192 "element".into(),
16193 LanguageConfigOverride {
16194 completion_query_characters: Override::Set(['-'].into_iter().collect()),
16195 ..Default::default()
16196 },
16197 )]
16198 .into_iter()
16199 .collect(),
16200 ..Default::default()
16201 },
16202 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16203 )
16204 .with_override_query("(jsx_self_closing_element) @element")
16205 .unwrap(),
16206 lsp::ServerCapabilities {
16207 completion_provider: Some(lsp::CompletionOptions {
16208 trigger_characters: Some(vec![":".to_string()]),
16209 ..Default::default()
16210 }),
16211 ..Default::default()
16212 },
16213 cx,
16214 )
16215 .await;
16216
16217 cx.lsp
16218 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16219 Ok(Some(lsp::CompletionResponse::Array(vec![
16220 lsp::CompletionItem {
16221 label: "bg-blue".into(),
16222 ..Default::default()
16223 },
16224 lsp::CompletionItem {
16225 label: "bg-red".into(),
16226 ..Default::default()
16227 },
16228 lsp::CompletionItem {
16229 label: "bg-yellow".into(),
16230 ..Default::default()
16231 },
16232 ])))
16233 });
16234
16235 cx.set_state(r#"<p class="bgˇ" />"#);
16236
16237 // Trigger completion when typing a dash, because the dash is an extra
16238 // word character in the 'element' scope, which contains the cursor.
16239 cx.simulate_keystroke("-");
16240 cx.executor().run_until_parked();
16241 cx.update_editor(|editor, _, _| {
16242 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16243 {
16244 assert_eq!(
16245 completion_menu_entries(&menu),
16246 &["bg-blue", "bg-red", "bg-yellow"]
16247 );
16248 } else {
16249 panic!("expected completion menu to be open");
16250 }
16251 });
16252
16253 cx.simulate_keystroke("l");
16254 cx.executor().run_until_parked();
16255 cx.update_editor(|editor, _, _| {
16256 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16257 {
16258 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
16259 } else {
16260 panic!("expected completion menu to be open");
16261 }
16262 });
16263
16264 // When filtering completions, consider the character after the '-' to
16265 // be the start of a subword.
16266 cx.set_state(r#"<p class="yelˇ" />"#);
16267 cx.simulate_keystroke("l");
16268 cx.executor().run_until_parked();
16269 cx.update_editor(|editor, _, _| {
16270 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16271 {
16272 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
16273 } else {
16274 panic!("expected completion menu to be open");
16275 }
16276 });
16277}
16278
16279fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
16280 let entries = menu.entries.borrow();
16281 entries.iter().map(|mat| mat.string.clone()).collect()
16282}
16283
16284#[gpui::test]
16285async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
16286 init_test(cx, |settings| {
16287 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
16288 Formatter::Prettier,
16289 )))
16290 });
16291
16292 let fs = FakeFs::new(cx.executor());
16293 fs.insert_file(path!("/file.ts"), Default::default()).await;
16294
16295 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
16296 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16297
16298 language_registry.add(Arc::new(Language::new(
16299 LanguageConfig {
16300 name: "TypeScript".into(),
16301 matcher: LanguageMatcher {
16302 path_suffixes: vec!["ts".to_string()],
16303 ..Default::default()
16304 },
16305 ..Default::default()
16306 },
16307 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
16308 )));
16309 update_test_language_settings(cx, |settings| {
16310 settings.defaults.prettier = Some(PrettierSettings {
16311 allowed: true,
16312 ..PrettierSettings::default()
16313 });
16314 });
16315
16316 let test_plugin = "test_plugin";
16317 let _ = language_registry.register_fake_lsp(
16318 "TypeScript",
16319 FakeLspAdapter {
16320 prettier_plugins: vec![test_plugin],
16321 ..Default::default()
16322 },
16323 );
16324
16325 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
16326 let buffer = project
16327 .update(cx, |project, cx| {
16328 project.open_local_buffer(path!("/file.ts"), cx)
16329 })
16330 .await
16331 .unwrap();
16332
16333 let buffer_text = "one\ntwo\nthree\n";
16334 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16335 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16336 editor.update_in(cx, |editor, window, cx| {
16337 editor.set_text(buffer_text, window, cx)
16338 });
16339
16340 editor
16341 .update_in(cx, |editor, window, cx| {
16342 editor.perform_format(
16343 project.clone(),
16344 FormatTrigger::Manual,
16345 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16346 window,
16347 cx,
16348 )
16349 })
16350 .unwrap()
16351 .await;
16352 assert_eq!(
16353 editor.update(cx, |editor, cx| editor.text(cx)),
16354 buffer_text.to_string() + prettier_format_suffix,
16355 "Test prettier formatting was not applied to the original buffer text",
16356 );
16357
16358 update_test_language_settings(cx, |settings| {
16359 settings.defaults.formatter = Some(SelectedFormatter::Auto)
16360 });
16361 let format = editor.update_in(cx, |editor, window, cx| {
16362 editor.perform_format(
16363 project.clone(),
16364 FormatTrigger::Manual,
16365 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16366 window,
16367 cx,
16368 )
16369 });
16370 format.await.unwrap();
16371 assert_eq!(
16372 editor.update(cx, |editor, cx| editor.text(cx)),
16373 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
16374 "Autoformatting (via test prettier) was not applied to the original buffer text",
16375 );
16376}
16377
16378#[gpui::test]
16379async fn test_addition_reverts(cx: &mut TestAppContext) {
16380 init_test(cx, |_| {});
16381 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16382 let base_text = indoc! {r#"
16383 struct Row;
16384 struct Row1;
16385 struct Row2;
16386
16387 struct Row4;
16388 struct Row5;
16389 struct Row6;
16390
16391 struct Row8;
16392 struct Row9;
16393 struct Row10;"#};
16394
16395 // When addition hunks are not adjacent to carets, no hunk revert is performed
16396 assert_hunk_revert(
16397 indoc! {r#"struct Row;
16398 struct Row1;
16399 struct Row1.1;
16400 struct Row1.2;
16401 struct Row2;ˇ
16402
16403 struct Row4;
16404 struct Row5;
16405 struct Row6;
16406
16407 struct Row8;
16408 ˇstruct Row9;
16409 struct Row9.1;
16410 struct Row9.2;
16411 struct Row9.3;
16412 struct Row10;"#},
16413 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16414 indoc! {r#"struct Row;
16415 struct Row1;
16416 struct Row1.1;
16417 struct Row1.2;
16418 struct Row2;ˇ
16419
16420 struct Row4;
16421 struct Row5;
16422 struct Row6;
16423
16424 struct Row8;
16425 ˇstruct Row9;
16426 struct Row9.1;
16427 struct Row9.2;
16428 struct Row9.3;
16429 struct Row10;"#},
16430 base_text,
16431 &mut cx,
16432 );
16433 // Same for selections
16434 assert_hunk_revert(
16435 indoc! {r#"struct Row;
16436 struct Row1;
16437 struct Row2;
16438 struct Row2.1;
16439 struct Row2.2;
16440 «ˇ
16441 struct Row4;
16442 struct» Row5;
16443 «struct Row6;
16444 ˇ»
16445 struct Row9.1;
16446 struct Row9.2;
16447 struct Row9.3;
16448 struct Row8;
16449 struct Row9;
16450 struct Row10;"#},
16451 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16452 indoc! {r#"struct Row;
16453 struct Row1;
16454 struct Row2;
16455 struct Row2.1;
16456 struct Row2.2;
16457 «ˇ
16458 struct Row4;
16459 struct» Row5;
16460 «struct Row6;
16461 ˇ»
16462 struct Row9.1;
16463 struct Row9.2;
16464 struct Row9.3;
16465 struct Row8;
16466 struct Row9;
16467 struct Row10;"#},
16468 base_text,
16469 &mut cx,
16470 );
16471
16472 // When carets and selections intersect the addition hunks, those are reverted.
16473 // Adjacent carets got merged.
16474 assert_hunk_revert(
16475 indoc! {r#"struct Row;
16476 ˇ// something on the top
16477 struct Row1;
16478 struct Row2;
16479 struct Roˇw3.1;
16480 struct Row2.2;
16481 struct Row2.3;ˇ
16482
16483 struct Row4;
16484 struct ˇRow5.1;
16485 struct Row5.2;
16486 struct «Rowˇ»5.3;
16487 struct Row5;
16488 struct Row6;
16489 ˇ
16490 struct Row9.1;
16491 struct «Rowˇ»9.2;
16492 struct «ˇRow»9.3;
16493 struct Row8;
16494 struct Row9;
16495 «ˇ// something on bottom»
16496 struct Row10;"#},
16497 vec![
16498 DiffHunkStatusKind::Added,
16499 DiffHunkStatusKind::Added,
16500 DiffHunkStatusKind::Added,
16501 DiffHunkStatusKind::Added,
16502 DiffHunkStatusKind::Added,
16503 ],
16504 indoc! {r#"struct Row;
16505 ˇstruct Row1;
16506 struct Row2;
16507 ˇ
16508 struct Row4;
16509 ˇstruct Row5;
16510 struct Row6;
16511 ˇ
16512 ˇstruct Row8;
16513 struct Row9;
16514 ˇstruct Row10;"#},
16515 base_text,
16516 &mut cx,
16517 );
16518}
16519
16520#[gpui::test]
16521async fn test_modification_reverts(cx: &mut TestAppContext) {
16522 init_test(cx, |_| {});
16523 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16524 let base_text = indoc! {r#"
16525 struct Row;
16526 struct Row1;
16527 struct Row2;
16528
16529 struct Row4;
16530 struct Row5;
16531 struct Row6;
16532
16533 struct Row8;
16534 struct Row9;
16535 struct Row10;"#};
16536
16537 // Modification hunks behave the same as the addition ones.
16538 assert_hunk_revert(
16539 indoc! {r#"struct Row;
16540 struct Row1;
16541 struct Row33;
16542 ˇ
16543 struct Row4;
16544 struct Row5;
16545 struct Row6;
16546 ˇ
16547 struct Row99;
16548 struct Row9;
16549 struct Row10;"#},
16550 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16551 indoc! {r#"struct Row;
16552 struct Row1;
16553 struct Row33;
16554 ˇ
16555 struct Row4;
16556 struct Row5;
16557 struct Row6;
16558 ˇ
16559 struct Row99;
16560 struct Row9;
16561 struct Row10;"#},
16562 base_text,
16563 &mut cx,
16564 );
16565 assert_hunk_revert(
16566 indoc! {r#"struct Row;
16567 struct Row1;
16568 struct Row33;
16569 «ˇ
16570 struct Row4;
16571 struct» Row5;
16572 «struct Row6;
16573 ˇ»
16574 struct Row99;
16575 struct Row9;
16576 struct Row10;"#},
16577 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16578 indoc! {r#"struct Row;
16579 struct Row1;
16580 struct Row33;
16581 «ˇ
16582 struct Row4;
16583 struct» Row5;
16584 «struct Row6;
16585 ˇ»
16586 struct Row99;
16587 struct Row9;
16588 struct Row10;"#},
16589 base_text,
16590 &mut cx,
16591 );
16592
16593 assert_hunk_revert(
16594 indoc! {r#"ˇstruct Row1.1;
16595 struct Row1;
16596 «ˇstr»uct Row22;
16597
16598 struct ˇRow44;
16599 struct Row5;
16600 struct «Rˇ»ow66;ˇ
16601
16602 «struˇ»ct Row88;
16603 struct Row9;
16604 struct Row1011;ˇ"#},
16605 vec![
16606 DiffHunkStatusKind::Modified,
16607 DiffHunkStatusKind::Modified,
16608 DiffHunkStatusKind::Modified,
16609 DiffHunkStatusKind::Modified,
16610 DiffHunkStatusKind::Modified,
16611 DiffHunkStatusKind::Modified,
16612 ],
16613 indoc! {r#"struct Row;
16614 ˇstruct Row1;
16615 struct Row2;
16616 ˇ
16617 struct Row4;
16618 ˇstruct Row5;
16619 struct Row6;
16620 ˇ
16621 struct Row8;
16622 ˇstruct Row9;
16623 struct Row10;ˇ"#},
16624 base_text,
16625 &mut cx,
16626 );
16627}
16628
16629#[gpui::test]
16630async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
16631 init_test(cx, |_| {});
16632 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16633 let base_text = indoc! {r#"
16634 one
16635
16636 two
16637 three
16638 "#};
16639
16640 cx.set_head_text(base_text);
16641 cx.set_state("\nˇ\n");
16642 cx.executor().run_until_parked();
16643 cx.update_editor(|editor, _window, cx| {
16644 editor.expand_selected_diff_hunks(cx);
16645 });
16646 cx.executor().run_until_parked();
16647 cx.update_editor(|editor, window, cx| {
16648 editor.backspace(&Default::default(), window, cx);
16649 });
16650 cx.run_until_parked();
16651 cx.assert_state_with_diff(
16652 indoc! {r#"
16653
16654 - two
16655 - threeˇ
16656 +
16657 "#}
16658 .to_string(),
16659 );
16660}
16661
16662#[gpui::test]
16663async fn test_deletion_reverts(cx: &mut TestAppContext) {
16664 init_test(cx, |_| {});
16665 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16666 let base_text = indoc! {r#"struct Row;
16667struct Row1;
16668struct Row2;
16669
16670struct Row4;
16671struct Row5;
16672struct Row6;
16673
16674struct Row8;
16675struct Row9;
16676struct Row10;"#};
16677
16678 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
16679 assert_hunk_revert(
16680 indoc! {r#"struct Row;
16681 struct Row2;
16682
16683 ˇstruct Row4;
16684 struct Row5;
16685 struct Row6;
16686 ˇ
16687 struct Row8;
16688 struct Row10;"#},
16689 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16690 indoc! {r#"struct Row;
16691 struct Row2;
16692
16693 ˇstruct Row4;
16694 struct Row5;
16695 struct Row6;
16696 ˇ
16697 struct Row8;
16698 struct Row10;"#},
16699 base_text,
16700 &mut cx,
16701 );
16702 assert_hunk_revert(
16703 indoc! {r#"struct Row;
16704 struct Row2;
16705
16706 «ˇstruct Row4;
16707 struct» Row5;
16708 «struct Row6;
16709 ˇ»
16710 struct Row8;
16711 struct Row10;"#},
16712 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16713 indoc! {r#"struct Row;
16714 struct Row2;
16715
16716 «ˇstruct Row4;
16717 struct» Row5;
16718 «struct Row6;
16719 ˇ»
16720 struct Row8;
16721 struct Row10;"#},
16722 base_text,
16723 &mut cx,
16724 );
16725
16726 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
16727 assert_hunk_revert(
16728 indoc! {r#"struct Row;
16729 ˇstruct Row2;
16730
16731 struct Row4;
16732 struct Row5;
16733 struct Row6;
16734
16735 struct Row8;ˇ
16736 struct Row10;"#},
16737 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16738 indoc! {r#"struct Row;
16739 struct Row1;
16740 ˇstruct Row2;
16741
16742 struct Row4;
16743 struct Row5;
16744 struct Row6;
16745
16746 struct Row8;ˇ
16747 struct Row9;
16748 struct Row10;"#},
16749 base_text,
16750 &mut cx,
16751 );
16752 assert_hunk_revert(
16753 indoc! {r#"struct Row;
16754 struct Row2«ˇ;
16755 struct Row4;
16756 struct» Row5;
16757 «struct Row6;
16758
16759 struct Row8;ˇ»
16760 struct Row10;"#},
16761 vec![
16762 DiffHunkStatusKind::Deleted,
16763 DiffHunkStatusKind::Deleted,
16764 DiffHunkStatusKind::Deleted,
16765 ],
16766 indoc! {r#"struct Row;
16767 struct Row1;
16768 struct Row2«ˇ;
16769
16770 struct Row4;
16771 struct» Row5;
16772 «struct Row6;
16773
16774 struct Row8;ˇ»
16775 struct Row9;
16776 struct Row10;"#},
16777 base_text,
16778 &mut cx,
16779 );
16780}
16781
16782#[gpui::test]
16783async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
16784 init_test(cx, |_| {});
16785
16786 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
16787 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
16788 let base_text_3 =
16789 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
16790
16791 let text_1 = edit_first_char_of_every_line(base_text_1);
16792 let text_2 = edit_first_char_of_every_line(base_text_2);
16793 let text_3 = edit_first_char_of_every_line(base_text_3);
16794
16795 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
16796 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
16797 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
16798
16799 let multibuffer = cx.new(|cx| {
16800 let mut multibuffer = MultiBuffer::new(ReadWrite);
16801 multibuffer.push_excerpts(
16802 buffer_1.clone(),
16803 [
16804 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16805 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16806 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16807 ],
16808 cx,
16809 );
16810 multibuffer.push_excerpts(
16811 buffer_2.clone(),
16812 [
16813 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16814 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16815 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16816 ],
16817 cx,
16818 );
16819 multibuffer.push_excerpts(
16820 buffer_3.clone(),
16821 [
16822 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16823 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16824 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16825 ],
16826 cx,
16827 );
16828 multibuffer
16829 });
16830
16831 let fs = FakeFs::new(cx.executor());
16832 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
16833 let (editor, cx) = cx
16834 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
16835 editor.update_in(cx, |editor, _window, cx| {
16836 for (buffer, diff_base) in [
16837 (buffer_1.clone(), base_text_1),
16838 (buffer_2.clone(), base_text_2),
16839 (buffer_3.clone(), base_text_3),
16840 ] {
16841 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16842 editor
16843 .buffer
16844 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16845 }
16846 });
16847 cx.executor().run_until_parked();
16848
16849 editor.update_in(cx, |editor, window, cx| {
16850 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}");
16851 editor.select_all(&SelectAll, window, cx);
16852 editor.git_restore(&Default::default(), window, cx);
16853 });
16854 cx.executor().run_until_parked();
16855
16856 // When all ranges are selected, all buffer hunks are reverted.
16857 editor.update(cx, |editor, cx| {
16858 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");
16859 });
16860 buffer_1.update(cx, |buffer, _| {
16861 assert_eq!(buffer.text(), base_text_1);
16862 });
16863 buffer_2.update(cx, |buffer, _| {
16864 assert_eq!(buffer.text(), base_text_2);
16865 });
16866 buffer_3.update(cx, |buffer, _| {
16867 assert_eq!(buffer.text(), base_text_3);
16868 });
16869
16870 editor.update_in(cx, |editor, window, cx| {
16871 editor.undo(&Default::default(), window, cx);
16872 });
16873
16874 editor.update_in(cx, |editor, window, cx| {
16875 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16876 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
16877 });
16878 editor.git_restore(&Default::default(), window, cx);
16879 });
16880
16881 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
16882 // but not affect buffer_2 and its related excerpts.
16883 editor.update(cx, |editor, cx| {
16884 assert_eq!(
16885 editor.text(cx),
16886 "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}"
16887 );
16888 });
16889 buffer_1.update(cx, |buffer, _| {
16890 assert_eq!(buffer.text(), base_text_1);
16891 });
16892 buffer_2.update(cx, |buffer, _| {
16893 assert_eq!(
16894 buffer.text(),
16895 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
16896 );
16897 });
16898 buffer_3.update(cx, |buffer, _| {
16899 assert_eq!(
16900 buffer.text(),
16901 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
16902 );
16903 });
16904
16905 fn edit_first_char_of_every_line(text: &str) -> String {
16906 text.split('\n')
16907 .map(|line| format!("X{}", &line[1..]))
16908 .collect::<Vec<_>>()
16909 .join("\n")
16910 }
16911}
16912
16913#[gpui::test]
16914async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
16915 init_test(cx, |_| {});
16916
16917 let cols = 4;
16918 let rows = 10;
16919 let sample_text_1 = sample_text(rows, cols, 'a');
16920 assert_eq!(
16921 sample_text_1,
16922 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
16923 );
16924 let sample_text_2 = sample_text(rows, cols, 'l');
16925 assert_eq!(
16926 sample_text_2,
16927 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
16928 );
16929 let sample_text_3 = sample_text(rows, cols, 'v');
16930 assert_eq!(
16931 sample_text_3,
16932 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
16933 );
16934
16935 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
16936 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
16937 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
16938
16939 let multi_buffer = cx.new(|cx| {
16940 let mut multibuffer = MultiBuffer::new(ReadWrite);
16941 multibuffer.push_excerpts(
16942 buffer_1.clone(),
16943 [
16944 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16945 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16946 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16947 ],
16948 cx,
16949 );
16950 multibuffer.push_excerpts(
16951 buffer_2.clone(),
16952 [
16953 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16954 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16955 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16956 ],
16957 cx,
16958 );
16959 multibuffer.push_excerpts(
16960 buffer_3.clone(),
16961 [
16962 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16963 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16964 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16965 ],
16966 cx,
16967 );
16968 multibuffer
16969 });
16970
16971 let fs = FakeFs::new(cx.executor());
16972 fs.insert_tree(
16973 "/a",
16974 json!({
16975 "main.rs": sample_text_1,
16976 "other.rs": sample_text_2,
16977 "lib.rs": sample_text_3,
16978 }),
16979 )
16980 .await;
16981 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16982 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16983 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16984 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16985 Editor::new(
16986 EditorMode::full(),
16987 multi_buffer,
16988 Some(project.clone()),
16989 window,
16990 cx,
16991 )
16992 });
16993 let multibuffer_item_id = workspace
16994 .update(cx, |workspace, window, cx| {
16995 assert!(
16996 workspace.active_item(cx).is_none(),
16997 "active item should be None before the first item is added"
16998 );
16999 workspace.add_item_to_active_pane(
17000 Box::new(multi_buffer_editor.clone()),
17001 None,
17002 true,
17003 window,
17004 cx,
17005 );
17006 let active_item = workspace
17007 .active_item(cx)
17008 .expect("should have an active item after adding the multi buffer");
17009 assert!(
17010 !active_item.is_singleton(cx),
17011 "A multi buffer was expected to active after adding"
17012 );
17013 active_item.item_id()
17014 })
17015 .unwrap();
17016 cx.executor().run_until_parked();
17017
17018 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17019 editor.change_selections(
17020 SelectionEffects::scroll(Autoscroll::Next),
17021 window,
17022 cx,
17023 |s| s.select_ranges(Some(1..2)),
17024 );
17025 editor.open_excerpts(&OpenExcerpts, window, cx);
17026 });
17027 cx.executor().run_until_parked();
17028 let first_item_id = workspace
17029 .update(cx, |workspace, window, cx| {
17030 let active_item = workspace
17031 .active_item(cx)
17032 .expect("should have an active item after navigating into the 1st buffer");
17033 let first_item_id = active_item.item_id();
17034 assert_ne!(
17035 first_item_id, multibuffer_item_id,
17036 "Should navigate into the 1st buffer and activate it"
17037 );
17038 assert!(
17039 active_item.is_singleton(cx),
17040 "New active item should be a singleton buffer"
17041 );
17042 assert_eq!(
17043 active_item
17044 .act_as::<Editor>(cx)
17045 .expect("should have navigated into an editor for the 1st buffer")
17046 .read(cx)
17047 .text(cx),
17048 sample_text_1
17049 );
17050
17051 workspace
17052 .go_back(workspace.active_pane().downgrade(), window, cx)
17053 .detach_and_log_err(cx);
17054
17055 first_item_id
17056 })
17057 .unwrap();
17058 cx.executor().run_until_parked();
17059 workspace
17060 .update(cx, |workspace, _, cx| {
17061 let active_item = workspace
17062 .active_item(cx)
17063 .expect("should have an active item after navigating back");
17064 assert_eq!(
17065 active_item.item_id(),
17066 multibuffer_item_id,
17067 "Should navigate back to the multi buffer"
17068 );
17069 assert!(!active_item.is_singleton(cx));
17070 })
17071 .unwrap();
17072
17073 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17074 editor.change_selections(
17075 SelectionEffects::scroll(Autoscroll::Next),
17076 window,
17077 cx,
17078 |s| s.select_ranges(Some(39..40)),
17079 );
17080 editor.open_excerpts(&OpenExcerpts, window, cx);
17081 });
17082 cx.executor().run_until_parked();
17083 let second_item_id = workspace
17084 .update(cx, |workspace, window, cx| {
17085 let active_item = workspace
17086 .active_item(cx)
17087 .expect("should have an active item after navigating into the 2nd buffer");
17088 let second_item_id = active_item.item_id();
17089 assert_ne!(
17090 second_item_id, multibuffer_item_id,
17091 "Should navigate away from the multibuffer"
17092 );
17093 assert_ne!(
17094 second_item_id, first_item_id,
17095 "Should navigate into the 2nd buffer and activate it"
17096 );
17097 assert!(
17098 active_item.is_singleton(cx),
17099 "New active item should be a singleton buffer"
17100 );
17101 assert_eq!(
17102 active_item
17103 .act_as::<Editor>(cx)
17104 .expect("should have navigated into an editor")
17105 .read(cx)
17106 .text(cx),
17107 sample_text_2
17108 );
17109
17110 workspace
17111 .go_back(workspace.active_pane().downgrade(), window, cx)
17112 .detach_and_log_err(cx);
17113
17114 second_item_id
17115 })
17116 .unwrap();
17117 cx.executor().run_until_parked();
17118 workspace
17119 .update(cx, |workspace, _, cx| {
17120 let active_item = workspace
17121 .active_item(cx)
17122 .expect("should have an active item after navigating back from the 2nd buffer");
17123 assert_eq!(
17124 active_item.item_id(),
17125 multibuffer_item_id,
17126 "Should navigate back from the 2nd buffer to the multi buffer"
17127 );
17128 assert!(!active_item.is_singleton(cx));
17129 })
17130 .unwrap();
17131
17132 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17133 editor.change_selections(
17134 SelectionEffects::scroll(Autoscroll::Next),
17135 window,
17136 cx,
17137 |s| s.select_ranges(Some(70..70)),
17138 );
17139 editor.open_excerpts(&OpenExcerpts, window, cx);
17140 });
17141 cx.executor().run_until_parked();
17142 workspace
17143 .update(cx, |workspace, window, cx| {
17144 let active_item = workspace
17145 .active_item(cx)
17146 .expect("should have an active item after navigating into the 3rd buffer");
17147 let third_item_id = active_item.item_id();
17148 assert_ne!(
17149 third_item_id, multibuffer_item_id,
17150 "Should navigate into the 3rd buffer and activate it"
17151 );
17152 assert_ne!(third_item_id, first_item_id);
17153 assert_ne!(third_item_id, second_item_id);
17154 assert!(
17155 active_item.is_singleton(cx),
17156 "New active item should be a singleton buffer"
17157 );
17158 assert_eq!(
17159 active_item
17160 .act_as::<Editor>(cx)
17161 .expect("should have navigated into an editor")
17162 .read(cx)
17163 .text(cx),
17164 sample_text_3
17165 );
17166
17167 workspace
17168 .go_back(workspace.active_pane().downgrade(), window, cx)
17169 .detach_and_log_err(cx);
17170 })
17171 .unwrap();
17172 cx.executor().run_until_parked();
17173 workspace
17174 .update(cx, |workspace, _, cx| {
17175 let active_item = workspace
17176 .active_item(cx)
17177 .expect("should have an active item after navigating back from the 3rd buffer");
17178 assert_eq!(
17179 active_item.item_id(),
17180 multibuffer_item_id,
17181 "Should navigate back from the 3rd buffer to the multi buffer"
17182 );
17183 assert!(!active_item.is_singleton(cx));
17184 })
17185 .unwrap();
17186}
17187
17188#[gpui::test]
17189async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17190 init_test(cx, |_| {});
17191
17192 let mut cx = EditorTestContext::new(cx).await;
17193
17194 let diff_base = r#"
17195 use some::mod;
17196
17197 const A: u32 = 42;
17198
17199 fn main() {
17200 println!("hello");
17201
17202 println!("world");
17203 }
17204 "#
17205 .unindent();
17206
17207 cx.set_state(
17208 &r#"
17209 use some::modified;
17210
17211 ˇ
17212 fn main() {
17213 println!("hello there");
17214
17215 println!("around the");
17216 println!("world");
17217 }
17218 "#
17219 .unindent(),
17220 );
17221
17222 cx.set_head_text(&diff_base);
17223 executor.run_until_parked();
17224
17225 cx.update_editor(|editor, window, cx| {
17226 editor.go_to_next_hunk(&GoToHunk, window, cx);
17227 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17228 });
17229 executor.run_until_parked();
17230 cx.assert_state_with_diff(
17231 r#"
17232 use some::modified;
17233
17234
17235 fn main() {
17236 - println!("hello");
17237 + ˇ println!("hello there");
17238
17239 println!("around the");
17240 println!("world");
17241 }
17242 "#
17243 .unindent(),
17244 );
17245
17246 cx.update_editor(|editor, window, cx| {
17247 for _ in 0..2 {
17248 editor.go_to_next_hunk(&GoToHunk, window, cx);
17249 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17250 }
17251 });
17252 executor.run_until_parked();
17253 cx.assert_state_with_diff(
17254 r#"
17255 - use some::mod;
17256 + ˇuse some::modified;
17257
17258
17259 fn main() {
17260 - println!("hello");
17261 + println!("hello there");
17262
17263 + println!("around the");
17264 println!("world");
17265 }
17266 "#
17267 .unindent(),
17268 );
17269
17270 cx.update_editor(|editor, window, cx| {
17271 editor.go_to_next_hunk(&GoToHunk, window, cx);
17272 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17273 });
17274 executor.run_until_parked();
17275 cx.assert_state_with_diff(
17276 r#"
17277 - use some::mod;
17278 + use some::modified;
17279
17280 - const A: u32 = 42;
17281 ˇ
17282 fn main() {
17283 - println!("hello");
17284 + println!("hello there");
17285
17286 + println!("around the");
17287 println!("world");
17288 }
17289 "#
17290 .unindent(),
17291 );
17292
17293 cx.update_editor(|editor, window, cx| {
17294 editor.cancel(&Cancel, window, cx);
17295 });
17296
17297 cx.assert_state_with_diff(
17298 r#"
17299 use some::modified;
17300
17301 ˇ
17302 fn main() {
17303 println!("hello there");
17304
17305 println!("around the");
17306 println!("world");
17307 }
17308 "#
17309 .unindent(),
17310 );
17311}
17312
17313#[gpui::test]
17314async fn test_diff_base_change_with_expanded_diff_hunks(
17315 executor: BackgroundExecutor,
17316 cx: &mut TestAppContext,
17317) {
17318 init_test(cx, |_| {});
17319
17320 let mut cx = EditorTestContext::new(cx).await;
17321
17322 let diff_base = r#"
17323 use some::mod1;
17324 use some::mod2;
17325
17326 const A: u32 = 42;
17327 const B: u32 = 42;
17328 const C: u32 = 42;
17329
17330 fn main() {
17331 println!("hello");
17332
17333 println!("world");
17334 }
17335 "#
17336 .unindent();
17337
17338 cx.set_state(
17339 &r#"
17340 use some::mod2;
17341
17342 const A: u32 = 42;
17343 const C: u32 = 42;
17344
17345 fn main(ˇ) {
17346 //println!("hello");
17347
17348 println!("world");
17349 //
17350 //
17351 }
17352 "#
17353 .unindent(),
17354 );
17355
17356 cx.set_head_text(&diff_base);
17357 executor.run_until_parked();
17358
17359 cx.update_editor(|editor, window, cx| {
17360 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17361 });
17362 executor.run_until_parked();
17363 cx.assert_state_with_diff(
17364 r#"
17365 - use some::mod1;
17366 use some::mod2;
17367
17368 const A: u32 = 42;
17369 - const B: u32 = 42;
17370 const C: u32 = 42;
17371
17372 fn main(ˇ) {
17373 - println!("hello");
17374 + //println!("hello");
17375
17376 println!("world");
17377 + //
17378 + //
17379 }
17380 "#
17381 .unindent(),
17382 );
17383
17384 cx.set_head_text("new diff base!");
17385 executor.run_until_parked();
17386 cx.assert_state_with_diff(
17387 r#"
17388 - new diff base!
17389 + use some::mod2;
17390 +
17391 + const A: u32 = 42;
17392 + const C: u32 = 42;
17393 +
17394 + fn main(ˇ) {
17395 + //println!("hello");
17396 +
17397 + println!("world");
17398 + //
17399 + //
17400 + }
17401 "#
17402 .unindent(),
17403 );
17404}
17405
17406#[gpui::test]
17407async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
17408 init_test(cx, |_| {});
17409
17410 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17411 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17412 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17413 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17414 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
17415 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
17416
17417 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
17418 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
17419 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
17420
17421 let multi_buffer = cx.new(|cx| {
17422 let mut multibuffer = MultiBuffer::new(ReadWrite);
17423 multibuffer.push_excerpts(
17424 buffer_1.clone(),
17425 [
17426 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17427 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17428 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17429 ],
17430 cx,
17431 );
17432 multibuffer.push_excerpts(
17433 buffer_2.clone(),
17434 [
17435 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17436 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17437 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17438 ],
17439 cx,
17440 );
17441 multibuffer.push_excerpts(
17442 buffer_3.clone(),
17443 [
17444 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17445 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17446 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17447 ],
17448 cx,
17449 );
17450 multibuffer
17451 });
17452
17453 let editor =
17454 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17455 editor
17456 .update(cx, |editor, _window, cx| {
17457 for (buffer, diff_base) in [
17458 (buffer_1.clone(), file_1_old),
17459 (buffer_2.clone(), file_2_old),
17460 (buffer_3.clone(), file_3_old),
17461 ] {
17462 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
17463 editor
17464 .buffer
17465 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
17466 }
17467 })
17468 .unwrap();
17469
17470 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17471 cx.run_until_parked();
17472
17473 cx.assert_editor_state(
17474 &"
17475 ˇaaa
17476 ccc
17477 ddd
17478
17479 ggg
17480 hhh
17481
17482
17483 lll
17484 mmm
17485 NNN
17486
17487 qqq
17488 rrr
17489
17490 uuu
17491 111
17492 222
17493 333
17494
17495 666
17496 777
17497
17498 000
17499 !!!"
17500 .unindent(),
17501 );
17502
17503 cx.update_editor(|editor, window, cx| {
17504 editor.select_all(&SelectAll, window, cx);
17505 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17506 });
17507 cx.executor().run_until_parked();
17508
17509 cx.assert_state_with_diff(
17510 "
17511 «aaa
17512 - bbb
17513 ccc
17514 ddd
17515
17516 ggg
17517 hhh
17518
17519
17520 lll
17521 mmm
17522 - nnn
17523 + NNN
17524
17525 qqq
17526 rrr
17527
17528 uuu
17529 111
17530 222
17531 333
17532
17533 + 666
17534 777
17535
17536 000
17537 !!!ˇ»"
17538 .unindent(),
17539 );
17540}
17541
17542#[gpui::test]
17543async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
17544 init_test(cx, |_| {});
17545
17546 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
17547 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
17548
17549 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
17550 let multi_buffer = cx.new(|cx| {
17551 let mut multibuffer = MultiBuffer::new(ReadWrite);
17552 multibuffer.push_excerpts(
17553 buffer.clone(),
17554 [
17555 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
17556 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
17557 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
17558 ],
17559 cx,
17560 );
17561 multibuffer
17562 });
17563
17564 let editor =
17565 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17566 editor
17567 .update(cx, |editor, _window, cx| {
17568 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
17569 editor
17570 .buffer
17571 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
17572 })
17573 .unwrap();
17574
17575 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17576 cx.run_until_parked();
17577
17578 cx.update_editor(|editor, window, cx| {
17579 editor.expand_all_diff_hunks(&Default::default(), window, cx)
17580 });
17581 cx.executor().run_until_parked();
17582
17583 // When the start of a hunk coincides with the start of its excerpt,
17584 // the hunk is expanded. When the start of a a hunk is earlier than
17585 // the start of its excerpt, the hunk is not expanded.
17586 cx.assert_state_with_diff(
17587 "
17588 ˇaaa
17589 - bbb
17590 + BBB
17591
17592 - ddd
17593 - eee
17594 + DDD
17595 + EEE
17596 fff
17597
17598 iii
17599 "
17600 .unindent(),
17601 );
17602}
17603
17604#[gpui::test]
17605async fn test_edits_around_expanded_insertion_hunks(
17606 executor: BackgroundExecutor,
17607 cx: &mut TestAppContext,
17608) {
17609 init_test(cx, |_| {});
17610
17611 let mut cx = EditorTestContext::new(cx).await;
17612
17613 let diff_base = r#"
17614 use some::mod1;
17615 use some::mod2;
17616
17617 const A: u32 = 42;
17618
17619 fn main() {
17620 println!("hello");
17621
17622 println!("world");
17623 }
17624 "#
17625 .unindent();
17626 executor.run_until_parked();
17627 cx.set_state(
17628 &r#"
17629 use some::mod1;
17630 use some::mod2;
17631
17632 const A: u32 = 42;
17633 const B: u32 = 42;
17634 const C: u32 = 42;
17635 ˇ
17636
17637 fn main() {
17638 println!("hello");
17639
17640 println!("world");
17641 }
17642 "#
17643 .unindent(),
17644 );
17645
17646 cx.set_head_text(&diff_base);
17647 executor.run_until_parked();
17648
17649 cx.update_editor(|editor, window, cx| {
17650 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17651 });
17652 executor.run_until_parked();
17653
17654 cx.assert_state_with_diff(
17655 r#"
17656 use some::mod1;
17657 use some::mod2;
17658
17659 const A: u32 = 42;
17660 + const B: u32 = 42;
17661 + const C: u32 = 42;
17662 + ˇ
17663
17664 fn main() {
17665 println!("hello");
17666
17667 println!("world");
17668 }
17669 "#
17670 .unindent(),
17671 );
17672
17673 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
17674 executor.run_until_parked();
17675
17676 cx.assert_state_with_diff(
17677 r#"
17678 use some::mod1;
17679 use some::mod2;
17680
17681 const A: u32 = 42;
17682 + const B: u32 = 42;
17683 + const C: u32 = 42;
17684 + const D: u32 = 42;
17685 + ˇ
17686
17687 fn main() {
17688 println!("hello");
17689
17690 println!("world");
17691 }
17692 "#
17693 .unindent(),
17694 );
17695
17696 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
17697 executor.run_until_parked();
17698
17699 cx.assert_state_with_diff(
17700 r#"
17701 use some::mod1;
17702 use some::mod2;
17703
17704 const A: u32 = 42;
17705 + const B: u32 = 42;
17706 + const C: u32 = 42;
17707 + const D: u32 = 42;
17708 + const E: u32 = 42;
17709 + ˇ
17710
17711 fn main() {
17712 println!("hello");
17713
17714 println!("world");
17715 }
17716 "#
17717 .unindent(),
17718 );
17719
17720 cx.update_editor(|editor, window, cx| {
17721 editor.delete_line(&DeleteLine, window, cx);
17722 });
17723 executor.run_until_parked();
17724
17725 cx.assert_state_with_diff(
17726 r#"
17727 use some::mod1;
17728 use some::mod2;
17729
17730 const A: u32 = 42;
17731 + const B: u32 = 42;
17732 + const C: u32 = 42;
17733 + const D: u32 = 42;
17734 + const E: u32 = 42;
17735 ˇ
17736 fn main() {
17737 println!("hello");
17738
17739 println!("world");
17740 }
17741 "#
17742 .unindent(),
17743 );
17744
17745 cx.update_editor(|editor, window, cx| {
17746 editor.move_up(&MoveUp, window, cx);
17747 editor.delete_line(&DeleteLine, window, cx);
17748 editor.move_up(&MoveUp, window, cx);
17749 editor.delete_line(&DeleteLine, window, cx);
17750 editor.move_up(&MoveUp, window, cx);
17751 editor.delete_line(&DeleteLine, window, cx);
17752 });
17753 executor.run_until_parked();
17754 cx.assert_state_with_diff(
17755 r#"
17756 use some::mod1;
17757 use some::mod2;
17758
17759 const A: u32 = 42;
17760 + const B: u32 = 42;
17761 ˇ
17762 fn main() {
17763 println!("hello");
17764
17765 println!("world");
17766 }
17767 "#
17768 .unindent(),
17769 );
17770
17771 cx.update_editor(|editor, window, cx| {
17772 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
17773 editor.delete_line(&DeleteLine, window, cx);
17774 });
17775 executor.run_until_parked();
17776 cx.assert_state_with_diff(
17777 r#"
17778 ˇ
17779 fn main() {
17780 println!("hello");
17781
17782 println!("world");
17783 }
17784 "#
17785 .unindent(),
17786 );
17787}
17788
17789#[gpui::test]
17790async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
17791 init_test(cx, |_| {});
17792
17793 let mut cx = EditorTestContext::new(cx).await;
17794 cx.set_head_text(indoc! { "
17795 one
17796 two
17797 three
17798 four
17799 five
17800 "
17801 });
17802 cx.set_state(indoc! { "
17803 one
17804 ˇthree
17805 five
17806 "});
17807 cx.run_until_parked();
17808 cx.update_editor(|editor, window, cx| {
17809 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17810 });
17811 cx.assert_state_with_diff(
17812 indoc! { "
17813 one
17814 - two
17815 ˇthree
17816 - four
17817 five
17818 "}
17819 .to_string(),
17820 );
17821 cx.update_editor(|editor, window, cx| {
17822 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17823 });
17824
17825 cx.assert_state_with_diff(
17826 indoc! { "
17827 one
17828 ˇthree
17829 five
17830 "}
17831 .to_string(),
17832 );
17833
17834 cx.set_state(indoc! { "
17835 one
17836 ˇTWO
17837 three
17838 four
17839 five
17840 "});
17841 cx.run_until_parked();
17842 cx.update_editor(|editor, window, cx| {
17843 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17844 });
17845
17846 cx.assert_state_with_diff(
17847 indoc! { "
17848 one
17849 - two
17850 + ˇTWO
17851 three
17852 four
17853 five
17854 "}
17855 .to_string(),
17856 );
17857 cx.update_editor(|editor, window, cx| {
17858 editor.move_up(&Default::default(), window, cx);
17859 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17860 });
17861 cx.assert_state_with_diff(
17862 indoc! { "
17863 one
17864 ˇTWO
17865 three
17866 four
17867 five
17868 "}
17869 .to_string(),
17870 );
17871}
17872
17873#[gpui::test]
17874async fn test_edits_around_expanded_deletion_hunks(
17875 executor: BackgroundExecutor,
17876 cx: &mut TestAppContext,
17877) {
17878 init_test(cx, |_| {});
17879
17880 let mut cx = EditorTestContext::new(cx).await;
17881
17882 let diff_base = r#"
17883 use some::mod1;
17884 use some::mod2;
17885
17886 const A: u32 = 42;
17887 const B: u32 = 42;
17888 const C: u32 = 42;
17889
17890
17891 fn main() {
17892 println!("hello");
17893
17894 println!("world");
17895 }
17896 "#
17897 .unindent();
17898 executor.run_until_parked();
17899 cx.set_state(
17900 &r#"
17901 use some::mod1;
17902 use some::mod2;
17903
17904 ˇconst B: u32 = 42;
17905 const C: u32 = 42;
17906
17907
17908 fn main() {
17909 println!("hello");
17910
17911 println!("world");
17912 }
17913 "#
17914 .unindent(),
17915 );
17916
17917 cx.set_head_text(&diff_base);
17918 executor.run_until_parked();
17919
17920 cx.update_editor(|editor, window, cx| {
17921 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17922 });
17923 executor.run_until_parked();
17924
17925 cx.assert_state_with_diff(
17926 r#"
17927 use some::mod1;
17928 use some::mod2;
17929
17930 - const A: u32 = 42;
17931 ˇconst B: u32 = 42;
17932 const C: u32 = 42;
17933
17934
17935 fn main() {
17936 println!("hello");
17937
17938 println!("world");
17939 }
17940 "#
17941 .unindent(),
17942 );
17943
17944 cx.update_editor(|editor, window, cx| {
17945 editor.delete_line(&DeleteLine, window, cx);
17946 });
17947 executor.run_until_parked();
17948 cx.assert_state_with_diff(
17949 r#"
17950 use some::mod1;
17951 use some::mod2;
17952
17953 - const A: u32 = 42;
17954 - const B: u32 = 42;
17955 ˇconst C: u32 = 42;
17956
17957
17958 fn main() {
17959 println!("hello");
17960
17961 println!("world");
17962 }
17963 "#
17964 .unindent(),
17965 );
17966
17967 cx.update_editor(|editor, window, cx| {
17968 editor.delete_line(&DeleteLine, window, cx);
17969 });
17970 executor.run_until_parked();
17971 cx.assert_state_with_diff(
17972 r#"
17973 use some::mod1;
17974 use some::mod2;
17975
17976 - const A: u32 = 42;
17977 - const B: u32 = 42;
17978 - const C: u32 = 42;
17979 ˇ
17980
17981 fn main() {
17982 println!("hello");
17983
17984 println!("world");
17985 }
17986 "#
17987 .unindent(),
17988 );
17989
17990 cx.update_editor(|editor, window, cx| {
17991 editor.handle_input("replacement", window, cx);
17992 });
17993 executor.run_until_parked();
17994 cx.assert_state_with_diff(
17995 r#"
17996 use some::mod1;
17997 use some::mod2;
17998
17999 - const A: u32 = 42;
18000 - const B: u32 = 42;
18001 - const C: u32 = 42;
18002 -
18003 + replacementˇ
18004
18005 fn main() {
18006 println!("hello");
18007
18008 println!("world");
18009 }
18010 "#
18011 .unindent(),
18012 );
18013}
18014
18015#[gpui::test]
18016async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18017 init_test(cx, |_| {});
18018
18019 let mut cx = EditorTestContext::new(cx).await;
18020
18021 let base_text = r#"
18022 one
18023 two
18024 three
18025 four
18026 five
18027 "#
18028 .unindent();
18029 executor.run_until_parked();
18030 cx.set_state(
18031 &r#"
18032 one
18033 two
18034 fˇour
18035 five
18036 "#
18037 .unindent(),
18038 );
18039
18040 cx.set_head_text(&base_text);
18041 executor.run_until_parked();
18042
18043 cx.update_editor(|editor, window, cx| {
18044 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18045 });
18046 executor.run_until_parked();
18047
18048 cx.assert_state_with_diff(
18049 r#"
18050 one
18051 two
18052 - three
18053 fˇour
18054 five
18055 "#
18056 .unindent(),
18057 );
18058
18059 cx.update_editor(|editor, window, cx| {
18060 editor.backspace(&Backspace, window, cx);
18061 editor.backspace(&Backspace, window, cx);
18062 });
18063 executor.run_until_parked();
18064 cx.assert_state_with_diff(
18065 r#"
18066 one
18067 two
18068 - threeˇ
18069 - four
18070 + our
18071 five
18072 "#
18073 .unindent(),
18074 );
18075}
18076
18077#[gpui::test]
18078async fn test_edit_after_expanded_modification_hunk(
18079 executor: BackgroundExecutor,
18080 cx: &mut TestAppContext,
18081) {
18082 init_test(cx, |_| {});
18083
18084 let mut cx = EditorTestContext::new(cx).await;
18085
18086 let diff_base = r#"
18087 use some::mod1;
18088 use some::mod2;
18089
18090 const A: u32 = 42;
18091 const B: u32 = 42;
18092 const C: u32 = 42;
18093 const D: u32 = 42;
18094
18095
18096 fn main() {
18097 println!("hello");
18098
18099 println!("world");
18100 }"#
18101 .unindent();
18102
18103 cx.set_state(
18104 &r#"
18105 use some::mod1;
18106 use some::mod2;
18107
18108 const A: u32 = 42;
18109 const B: u32 = 42;
18110 const C: u32 = 43ˇ
18111 const D: u32 = 42;
18112
18113
18114 fn main() {
18115 println!("hello");
18116
18117 println!("world");
18118 }"#
18119 .unindent(),
18120 );
18121
18122 cx.set_head_text(&diff_base);
18123 executor.run_until_parked();
18124 cx.update_editor(|editor, window, cx| {
18125 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18126 });
18127 executor.run_until_parked();
18128
18129 cx.assert_state_with_diff(
18130 r#"
18131 use some::mod1;
18132 use some::mod2;
18133
18134 const A: u32 = 42;
18135 const B: u32 = 42;
18136 - const C: u32 = 42;
18137 + const C: u32 = 43ˇ
18138 const D: u32 = 42;
18139
18140
18141 fn main() {
18142 println!("hello");
18143
18144 println!("world");
18145 }"#
18146 .unindent(),
18147 );
18148
18149 cx.update_editor(|editor, window, cx| {
18150 editor.handle_input("\nnew_line\n", window, cx);
18151 });
18152 executor.run_until_parked();
18153
18154 cx.assert_state_with_diff(
18155 r#"
18156 use some::mod1;
18157 use some::mod2;
18158
18159 const A: u32 = 42;
18160 const B: u32 = 42;
18161 - const C: u32 = 42;
18162 + const C: u32 = 43
18163 + new_line
18164 + ˇ
18165 const D: u32 = 42;
18166
18167
18168 fn main() {
18169 println!("hello");
18170
18171 println!("world");
18172 }"#
18173 .unindent(),
18174 );
18175}
18176
18177#[gpui::test]
18178async fn test_stage_and_unstage_added_file_hunk(
18179 executor: BackgroundExecutor,
18180 cx: &mut TestAppContext,
18181) {
18182 init_test(cx, |_| {});
18183
18184 let mut cx = EditorTestContext::new(cx).await;
18185 cx.update_editor(|editor, _, cx| {
18186 editor.set_expand_all_diff_hunks(cx);
18187 });
18188
18189 let working_copy = r#"
18190 ˇfn main() {
18191 println!("hello, world!");
18192 }
18193 "#
18194 .unindent();
18195
18196 cx.set_state(&working_copy);
18197 executor.run_until_parked();
18198
18199 cx.assert_state_with_diff(
18200 r#"
18201 + ˇfn main() {
18202 + println!("hello, world!");
18203 + }
18204 "#
18205 .unindent(),
18206 );
18207 cx.assert_index_text(None);
18208
18209 cx.update_editor(|editor, window, cx| {
18210 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18211 });
18212 executor.run_until_parked();
18213 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
18214 cx.assert_state_with_diff(
18215 r#"
18216 + ˇfn main() {
18217 + println!("hello, world!");
18218 + }
18219 "#
18220 .unindent(),
18221 );
18222
18223 cx.update_editor(|editor, window, cx| {
18224 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18225 });
18226 executor.run_until_parked();
18227 cx.assert_index_text(None);
18228}
18229
18230async fn setup_indent_guides_editor(
18231 text: &str,
18232 cx: &mut TestAppContext,
18233) -> (BufferId, EditorTestContext) {
18234 init_test(cx, |_| {});
18235
18236 let mut cx = EditorTestContext::new(cx).await;
18237
18238 let buffer_id = cx.update_editor(|editor, window, cx| {
18239 editor.set_text(text, window, cx);
18240 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
18241
18242 buffer_ids[0]
18243 });
18244
18245 (buffer_id, cx)
18246}
18247
18248fn assert_indent_guides(
18249 range: Range<u32>,
18250 expected: Vec<IndentGuide>,
18251 active_indices: Option<Vec<usize>>,
18252 cx: &mut EditorTestContext,
18253) {
18254 let indent_guides = cx.update_editor(|editor, window, cx| {
18255 let snapshot = editor.snapshot(window, cx).display_snapshot;
18256 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
18257 editor,
18258 MultiBufferRow(range.start)..MultiBufferRow(range.end),
18259 true,
18260 &snapshot,
18261 cx,
18262 );
18263
18264 indent_guides.sort_by(|a, b| {
18265 a.depth.cmp(&b.depth).then(
18266 a.start_row
18267 .cmp(&b.start_row)
18268 .then(a.end_row.cmp(&b.end_row)),
18269 )
18270 });
18271 indent_guides
18272 });
18273
18274 if let Some(expected) = active_indices {
18275 let active_indices = cx.update_editor(|editor, window, cx| {
18276 let snapshot = editor.snapshot(window, cx).display_snapshot;
18277 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
18278 });
18279
18280 assert_eq!(
18281 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
18282 expected,
18283 "Active indent guide indices do not match"
18284 );
18285 }
18286
18287 assert_eq!(indent_guides, expected, "Indent guides do not match");
18288}
18289
18290fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
18291 IndentGuide {
18292 buffer_id,
18293 start_row: MultiBufferRow(start_row),
18294 end_row: MultiBufferRow(end_row),
18295 depth,
18296 tab_size: 4,
18297 settings: IndentGuideSettings {
18298 enabled: true,
18299 line_width: 1,
18300 active_line_width: 1,
18301 ..Default::default()
18302 },
18303 }
18304}
18305
18306#[gpui::test]
18307async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
18308 let (buffer_id, mut cx) = setup_indent_guides_editor(
18309 &"
18310 fn main() {
18311 let a = 1;
18312 }"
18313 .unindent(),
18314 cx,
18315 )
18316 .await;
18317
18318 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18319}
18320
18321#[gpui::test]
18322async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
18323 let (buffer_id, mut cx) = setup_indent_guides_editor(
18324 &"
18325 fn main() {
18326 let a = 1;
18327 let b = 2;
18328 }"
18329 .unindent(),
18330 cx,
18331 )
18332 .await;
18333
18334 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
18335}
18336
18337#[gpui::test]
18338async fn test_indent_guide_nested(cx: &mut TestAppContext) {
18339 let (buffer_id, mut cx) = setup_indent_guides_editor(
18340 &"
18341 fn main() {
18342 let a = 1;
18343 if a == 3 {
18344 let b = 2;
18345 } else {
18346 let c = 3;
18347 }
18348 }"
18349 .unindent(),
18350 cx,
18351 )
18352 .await;
18353
18354 assert_indent_guides(
18355 0..8,
18356 vec![
18357 indent_guide(buffer_id, 1, 6, 0),
18358 indent_guide(buffer_id, 3, 3, 1),
18359 indent_guide(buffer_id, 5, 5, 1),
18360 ],
18361 None,
18362 &mut cx,
18363 );
18364}
18365
18366#[gpui::test]
18367async fn test_indent_guide_tab(cx: &mut TestAppContext) {
18368 let (buffer_id, mut cx) = setup_indent_guides_editor(
18369 &"
18370 fn main() {
18371 let a = 1;
18372 let b = 2;
18373 let c = 3;
18374 }"
18375 .unindent(),
18376 cx,
18377 )
18378 .await;
18379
18380 assert_indent_guides(
18381 0..5,
18382 vec![
18383 indent_guide(buffer_id, 1, 3, 0),
18384 indent_guide(buffer_id, 2, 2, 1),
18385 ],
18386 None,
18387 &mut cx,
18388 );
18389}
18390
18391#[gpui::test]
18392async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
18393 let (buffer_id, mut cx) = setup_indent_guides_editor(
18394 &"
18395 fn main() {
18396 let a = 1;
18397
18398 let c = 3;
18399 }"
18400 .unindent(),
18401 cx,
18402 )
18403 .await;
18404
18405 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
18406}
18407
18408#[gpui::test]
18409async fn test_indent_guide_complex(cx: &mut TestAppContext) {
18410 let (buffer_id, mut cx) = setup_indent_guides_editor(
18411 &"
18412 fn main() {
18413 let a = 1;
18414
18415 let c = 3;
18416
18417 if a == 3 {
18418 let b = 2;
18419 } else {
18420 let c = 3;
18421 }
18422 }"
18423 .unindent(),
18424 cx,
18425 )
18426 .await;
18427
18428 assert_indent_guides(
18429 0..11,
18430 vec![
18431 indent_guide(buffer_id, 1, 9, 0),
18432 indent_guide(buffer_id, 6, 6, 1),
18433 indent_guide(buffer_id, 8, 8, 1),
18434 ],
18435 None,
18436 &mut cx,
18437 );
18438}
18439
18440#[gpui::test]
18441async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
18442 let (buffer_id, mut cx) = setup_indent_guides_editor(
18443 &"
18444 fn main() {
18445 let a = 1;
18446
18447 let c = 3;
18448
18449 if a == 3 {
18450 let b = 2;
18451 } else {
18452 let c = 3;
18453 }
18454 }"
18455 .unindent(),
18456 cx,
18457 )
18458 .await;
18459
18460 assert_indent_guides(
18461 1..11,
18462 vec![
18463 indent_guide(buffer_id, 1, 9, 0),
18464 indent_guide(buffer_id, 6, 6, 1),
18465 indent_guide(buffer_id, 8, 8, 1),
18466 ],
18467 None,
18468 &mut cx,
18469 );
18470}
18471
18472#[gpui::test]
18473async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
18474 let (buffer_id, mut cx) = setup_indent_guides_editor(
18475 &"
18476 fn main() {
18477 let a = 1;
18478
18479 let c = 3;
18480
18481 if a == 3 {
18482 let b = 2;
18483 } else {
18484 let c = 3;
18485 }
18486 }"
18487 .unindent(),
18488 cx,
18489 )
18490 .await;
18491
18492 assert_indent_guides(
18493 1..10,
18494 vec![
18495 indent_guide(buffer_id, 1, 9, 0),
18496 indent_guide(buffer_id, 6, 6, 1),
18497 indent_guide(buffer_id, 8, 8, 1),
18498 ],
18499 None,
18500 &mut cx,
18501 );
18502}
18503
18504#[gpui::test]
18505async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
18506 let (buffer_id, mut cx) = setup_indent_guides_editor(
18507 &"
18508 fn main() {
18509 if a {
18510 b(
18511 c,
18512 d,
18513 )
18514 } else {
18515 e(
18516 f
18517 )
18518 }
18519 }"
18520 .unindent(),
18521 cx,
18522 )
18523 .await;
18524
18525 assert_indent_guides(
18526 0..11,
18527 vec![
18528 indent_guide(buffer_id, 1, 10, 0),
18529 indent_guide(buffer_id, 2, 5, 1),
18530 indent_guide(buffer_id, 7, 9, 1),
18531 indent_guide(buffer_id, 3, 4, 2),
18532 indent_guide(buffer_id, 8, 8, 2),
18533 ],
18534 None,
18535 &mut cx,
18536 );
18537
18538 cx.update_editor(|editor, window, cx| {
18539 editor.fold_at(MultiBufferRow(2), window, cx);
18540 assert_eq!(
18541 editor.display_text(cx),
18542 "
18543 fn main() {
18544 if a {
18545 b(⋯
18546 )
18547 } else {
18548 e(
18549 f
18550 )
18551 }
18552 }"
18553 .unindent()
18554 );
18555 });
18556
18557 assert_indent_guides(
18558 0..11,
18559 vec![
18560 indent_guide(buffer_id, 1, 10, 0),
18561 indent_guide(buffer_id, 2, 5, 1),
18562 indent_guide(buffer_id, 7, 9, 1),
18563 indent_guide(buffer_id, 8, 8, 2),
18564 ],
18565 None,
18566 &mut cx,
18567 );
18568}
18569
18570#[gpui::test]
18571async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
18572 let (buffer_id, mut cx) = setup_indent_guides_editor(
18573 &"
18574 block1
18575 block2
18576 block3
18577 block4
18578 block2
18579 block1
18580 block1"
18581 .unindent(),
18582 cx,
18583 )
18584 .await;
18585
18586 assert_indent_guides(
18587 1..10,
18588 vec![
18589 indent_guide(buffer_id, 1, 4, 0),
18590 indent_guide(buffer_id, 2, 3, 1),
18591 indent_guide(buffer_id, 3, 3, 2),
18592 ],
18593 None,
18594 &mut cx,
18595 );
18596}
18597
18598#[gpui::test]
18599async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
18600 let (buffer_id, mut cx) = setup_indent_guides_editor(
18601 &"
18602 block1
18603 block2
18604 block3
18605
18606 block1
18607 block1"
18608 .unindent(),
18609 cx,
18610 )
18611 .await;
18612
18613 assert_indent_guides(
18614 0..6,
18615 vec![
18616 indent_guide(buffer_id, 1, 2, 0),
18617 indent_guide(buffer_id, 2, 2, 1),
18618 ],
18619 None,
18620 &mut cx,
18621 );
18622}
18623
18624#[gpui::test]
18625async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
18626 let (buffer_id, mut cx) = setup_indent_guides_editor(
18627 &"
18628 function component() {
18629 \treturn (
18630 \t\t\t
18631 \t\t<div>
18632 \t\t\t<abc></abc>
18633 \t\t</div>
18634 \t)
18635 }"
18636 .unindent(),
18637 cx,
18638 )
18639 .await;
18640
18641 assert_indent_guides(
18642 0..8,
18643 vec![
18644 indent_guide(buffer_id, 1, 6, 0),
18645 indent_guide(buffer_id, 2, 5, 1),
18646 indent_guide(buffer_id, 4, 4, 2),
18647 ],
18648 None,
18649 &mut cx,
18650 );
18651}
18652
18653#[gpui::test]
18654async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
18655 let (buffer_id, mut cx) = setup_indent_guides_editor(
18656 &"
18657 function component() {
18658 \treturn (
18659 \t
18660 \t\t<div>
18661 \t\t\t<abc></abc>
18662 \t\t</div>
18663 \t)
18664 }"
18665 .unindent(),
18666 cx,
18667 )
18668 .await;
18669
18670 assert_indent_guides(
18671 0..8,
18672 vec![
18673 indent_guide(buffer_id, 1, 6, 0),
18674 indent_guide(buffer_id, 2, 5, 1),
18675 indent_guide(buffer_id, 4, 4, 2),
18676 ],
18677 None,
18678 &mut cx,
18679 );
18680}
18681
18682#[gpui::test]
18683async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
18684 let (buffer_id, mut cx) = setup_indent_guides_editor(
18685 &"
18686 block1
18687
18688
18689
18690 block2
18691 "
18692 .unindent(),
18693 cx,
18694 )
18695 .await;
18696
18697 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18698}
18699
18700#[gpui::test]
18701async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
18702 let (buffer_id, mut cx) = setup_indent_guides_editor(
18703 &"
18704 def a:
18705 \tb = 3
18706 \tif True:
18707 \t\tc = 4
18708 \t\td = 5
18709 \tprint(b)
18710 "
18711 .unindent(),
18712 cx,
18713 )
18714 .await;
18715
18716 assert_indent_guides(
18717 0..6,
18718 vec![
18719 indent_guide(buffer_id, 1, 5, 0),
18720 indent_guide(buffer_id, 3, 4, 1),
18721 ],
18722 None,
18723 &mut cx,
18724 );
18725}
18726
18727#[gpui::test]
18728async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
18729 let (buffer_id, mut cx) = setup_indent_guides_editor(
18730 &"
18731 fn main() {
18732 let a = 1;
18733 }"
18734 .unindent(),
18735 cx,
18736 )
18737 .await;
18738
18739 cx.update_editor(|editor, window, cx| {
18740 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18741 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18742 });
18743 });
18744
18745 assert_indent_guides(
18746 0..3,
18747 vec![indent_guide(buffer_id, 1, 1, 0)],
18748 Some(vec![0]),
18749 &mut cx,
18750 );
18751}
18752
18753#[gpui::test]
18754async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
18755 let (buffer_id, mut cx) = setup_indent_guides_editor(
18756 &"
18757 fn main() {
18758 if 1 == 2 {
18759 let a = 1;
18760 }
18761 }"
18762 .unindent(),
18763 cx,
18764 )
18765 .await;
18766
18767 cx.update_editor(|editor, window, cx| {
18768 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18769 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18770 });
18771 });
18772
18773 assert_indent_guides(
18774 0..4,
18775 vec![
18776 indent_guide(buffer_id, 1, 3, 0),
18777 indent_guide(buffer_id, 2, 2, 1),
18778 ],
18779 Some(vec![1]),
18780 &mut cx,
18781 );
18782
18783 cx.update_editor(|editor, window, cx| {
18784 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18785 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18786 });
18787 });
18788
18789 assert_indent_guides(
18790 0..4,
18791 vec![
18792 indent_guide(buffer_id, 1, 3, 0),
18793 indent_guide(buffer_id, 2, 2, 1),
18794 ],
18795 Some(vec![1]),
18796 &mut cx,
18797 );
18798
18799 cx.update_editor(|editor, window, cx| {
18800 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18801 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
18802 });
18803 });
18804
18805 assert_indent_guides(
18806 0..4,
18807 vec![
18808 indent_guide(buffer_id, 1, 3, 0),
18809 indent_guide(buffer_id, 2, 2, 1),
18810 ],
18811 Some(vec![0]),
18812 &mut cx,
18813 );
18814}
18815
18816#[gpui::test]
18817async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
18818 let (buffer_id, mut cx) = setup_indent_guides_editor(
18819 &"
18820 fn main() {
18821 let a = 1;
18822
18823 let b = 2;
18824 }"
18825 .unindent(),
18826 cx,
18827 )
18828 .await;
18829
18830 cx.update_editor(|editor, window, cx| {
18831 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18832 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18833 });
18834 });
18835
18836 assert_indent_guides(
18837 0..5,
18838 vec![indent_guide(buffer_id, 1, 3, 0)],
18839 Some(vec![0]),
18840 &mut cx,
18841 );
18842}
18843
18844#[gpui::test]
18845async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
18846 let (buffer_id, mut cx) = setup_indent_guides_editor(
18847 &"
18848 def m:
18849 a = 1
18850 pass"
18851 .unindent(),
18852 cx,
18853 )
18854 .await;
18855
18856 cx.update_editor(|editor, window, cx| {
18857 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18858 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18859 });
18860 });
18861
18862 assert_indent_guides(
18863 0..3,
18864 vec![indent_guide(buffer_id, 1, 2, 0)],
18865 Some(vec![0]),
18866 &mut cx,
18867 );
18868}
18869
18870#[gpui::test]
18871async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
18872 init_test(cx, |_| {});
18873 let mut cx = EditorTestContext::new(cx).await;
18874 let text = indoc! {
18875 "
18876 impl A {
18877 fn b() {
18878 0;
18879 3;
18880 5;
18881 6;
18882 7;
18883 }
18884 }
18885 "
18886 };
18887 let base_text = indoc! {
18888 "
18889 impl A {
18890 fn b() {
18891 0;
18892 1;
18893 2;
18894 3;
18895 4;
18896 }
18897 fn c() {
18898 5;
18899 6;
18900 7;
18901 }
18902 }
18903 "
18904 };
18905
18906 cx.update_editor(|editor, window, cx| {
18907 editor.set_text(text, window, cx);
18908
18909 editor.buffer().update(cx, |multibuffer, cx| {
18910 let buffer = multibuffer.as_singleton().unwrap();
18911 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
18912
18913 multibuffer.set_all_diff_hunks_expanded(cx);
18914 multibuffer.add_diff(diff, cx);
18915
18916 buffer.read(cx).remote_id()
18917 })
18918 });
18919 cx.run_until_parked();
18920
18921 cx.assert_state_with_diff(
18922 indoc! { "
18923 impl A {
18924 fn b() {
18925 0;
18926 - 1;
18927 - 2;
18928 3;
18929 - 4;
18930 - }
18931 - fn c() {
18932 5;
18933 6;
18934 7;
18935 }
18936 }
18937 ˇ"
18938 }
18939 .to_string(),
18940 );
18941
18942 let mut actual_guides = cx.update_editor(|editor, window, cx| {
18943 editor
18944 .snapshot(window, cx)
18945 .buffer_snapshot
18946 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
18947 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
18948 .collect::<Vec<_>>()
18949 });
18950 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
18951 assert_eq!(
18952 actual_guides,
18953 vec![
18954 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
18955 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
18956 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
18957 ]
18958 );
18959}
18960
18961#[gpui::test]
18962async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18963 init_test(cx, |_| {});
18964 let mut cx = EditorTestContext::new(cx).await;
18965
18966 let diff_base = r#"
18967 a
18968 b
18969 c
18970 "#
18971 .unindent();
18972
18973 cx.set_state(
18974 &r#"
18975 ˇA
18976 b
18977 C
18978 "#
18979 .unindent(),
18980 );
18981 cx.set_head_text(&diff_base);
18982 cx.update_editor(|editor, window, cx| {
18983 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18984 });
18985 executor.run_until_parked();
18986
18987 let both_hunks_expanded = r#"
18988 - a
18989 + ˇA
18990 b
18991 - c
18992 + C
18993 "#
18994 .unindent();
18995
18996 cx.assert_state_with_diff(both_hunks_expanded.clone());
18997
18998 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18999 let snapshot = editor.snapshot(window, cx);
19000 let hunks = editor
19001 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19002 .collect::<Vec<_>>();
19003 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19004 let buffer_id = hunks[0].buffer_id;
19005 hunks
19006 .into_iter()
19007 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19008 .collect::<Vec<_>>()
19009 });
19010 assert_eq!(hunk_ranges.len(), 2);
19011
19012 cx.update_editor(|editor, _, cx| {
19013 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19014 });
19015 executor.run_until_parked();
19016
19017 let second_hunk_expanded = r#"
19018 ˇA
19019 b
19020 - c
19021 + C
19022 "#
19023 .unindent();
19024
19025 cx.assert_state_with_diff(second_hunk_expanded);
19026
19027 cx.update_editor(|editor, _, cx| {
19028 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19029 });
19030 executor.run_until_parked();
19031
19032 cx.assert_state_with_diff(both_hunks_expanded.clone());
19033
19034 cx.update_editor(|editor, _, cx| {
19035 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19036 });
19037 executor.run_until_parked();
19038
19039 let first_hunk_expanded = r#"
19040 - a
19041 + ˇA
19042 b
19043 C
19044 "#
19045 .unindent();
19046
19047 cx.assert_state_with_diff(first_hunk_expanded);
19048
19049 cx.update_editor(|editor, _, cx| {
19050 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19051 });
19052 executor.run_until_parked();
19053
19054 cx.assert_state_with_diff(both_hunks_expanded);
19055
19056 cx.set_state(
19057 &r#"
19058 ˇA
19059 b
19060 "#
19061 .unindent(),
19062 );
19063 cx.run_until_parked();
19064
19065 // TODO this cursor position seems bad
19066 cx.assert_state_with_diff(
19067 r#"
19068 - ˇa
19069 + A
19070 b
19071 "#
19072 .unindent(),
19073 );
19074
19075 cx.update_editor(|editor, window, cx| {
19076 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19077 });
19078
19079 cx.assert_state_with_diff(
19080 r#"
19081 - ˇa
19082 + A
19083 b
19084 - c
19085 "#
19086 .unindent(),
19087 );
19088
19089 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19090 let snapshot = editor.snapshot(window, cx);
19091 let hunks = editor
19092 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19093 .collect::<Vec<_>>();
19094 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19095 let buffer_id = hunks[0].buffer_id;
19096 hunks
19097 .into_iter()
19098 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19099 .collect::<Vec<_>>()
19100 });
19101 assert_eq!(hunk_ranges.len(), 2);
19102
19103 cx.update_editor(|editor, _, cx| {
19104 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19105 });
19106 executor.run_until_parked();
19107
19108 cx.assert_state_with_diff(
19109 r#"
19110 - ˇa
19111 + A
19112 b
19113 "#
19114 .unindent(),
19115 );
19116}
19117
19118#[gpui::test]
19119async fn test_toggle_deletion_hunk_at_start_of_file(
19120 executor: BackgroundExecutor,
19121 cx: &mut TestAppContext,
19122) {
19123 init_test(cx, |_| {});
19124 let mut cx = EditorTestContext::new(cx).await;
19125
19126 let diff_base = r#"
19127 a
19128 b
19129 c
19130 "#
19131 .unindent();
19132
19133 cx.set_state(
19134 &r#"
19135 ˇb
19136 c
19137 "#
19138 .unindent(),
19139 );
19140 cx.set_head_text(&diff_base);
19141 cx.update_editor(|editor, window, cx| {
19142 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19143 });
19144 executor.run_until_parked();
19145
19146 let hunk_expanded = r#"
19147 - a
19148 ˇb
19149 c
19150 "#
19151 .unindent();
19152
19153 cx.assert_state_with_diff(hunk_expanded.clone());
19154
19155 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19156 let snapshot = editor.snapshot(window, cx);
19157 let hunks = editor
19158 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19159 .collect::<Vec<_>>();
19160 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19161 let buffer_id = hunks[0].buffer_id;
19162 hunks
19163 .into_iter()
19164 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19165 .collect::<Vec<_>>()
19166 });
19167 assert_eq!(hunk_ranges.len(), 1);
19168
19169 cx.update_editor(|editor, _, cx| {
19170 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19171 });
19172 executor.run_until_parked();
19173
19174 let hunk_collapsed = r#"
19175 ˇb
19176 c
19177 "#
19178 .unindent();
19179
19180 cx.assert_state_with_diff(hunk_collapsed);
19181
19182 cx.update_editor(|editor, _, cx| {
19183 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19184 });
19185 executor.run_until_parked();
19186
19187 cx.assert_state_with_diff(hunk_expanded.clone());
19188}
19189
19190#[gpui::test]
19191async fn test_display_diff_hunks(cx: &mut TestAppContext) {
19192 init_test(cx, |_| {});
19193
19194 let fs = FakeFs::new(cx.executor());
19195 fs.insert_tree(
19196 path!("/test"),
19197 json!({
19198 ".git": {},
19199 "file-1": "ONE\n",
19200 "file-2": "TWO\n",
19201 "file-3": "THREE\n",
19202 }),
19203 )
19204 .await;
19205
19206 fs.set_head_for_repo(
19207 path!("/test/.git").as_ref(),
19208 &[
19209 ("file-1".into(), "one\n".into()),
19210 ("file-2".into(), "two\n".into()),
19211 ("file-3".into(), "three\n".into()),
19212 ],
19213 "deadbeef",
19214 );
19215
19216 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
19217 let mut buffers = vec![];
19218 for i in 1..=3 {
19219 let buffer = project
19220 .update(cx, |project, cx| {
19221 let path = format!(path!("/test/file-{}"), i);
19222 project.open_local_buffer(path, cx)
19223 })
19224 .await
19225 .unwrap();
19226 buffers.push(buffer);
19227 }
19228
19229 let multibuffer = cx.new(|cx| {
19230 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
19231 multibuffer.set_all_diff_hunks_expanded(cx);
19232 for buffer in &buffers {
19233 let snapshot = buffer.read(cx).snapshot();
19234 multibuffer.set_excerpts_for_path(
19235 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
19236 buffer.clone(),
19237 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
19238 DEFAULT_MULTIBUFFER_CONTEXT,
19239 cx,
19240 );
19241 }
19242 multibuffer
19243 });
19244
19245 let editor = cx.add_window(|window, cx| {
19246 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
19247 });
19248 cx.run_until_parked();
19249
19250 let snapshot = editor
19251 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19252 .unwrap();
19253 let hunks = snapshot
19254 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
19255 .map(|hunk| match hunk {
19256 DisplayDiffHunk::Unfolded {
19257 display_row_range, ..
19258 } => display_row_range,
19259 DisplayDiffHunk::Folded { .. } => unreachable!(),
19260 })
19261 .collect::<Vec<_>>();
19262 assert_eq!(
19263 hunks,
19264 [
19265 DisplayRow(2)..DisplayRow(4),
19266 DisplayRow(7)..DisplayRow(9),
19267 DisplayRow(12)..DisplayRow(14),
19268 ]
19269 );
19270}
19271
19272#[gpui::test]
19273async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
19274 init_test(cx, |_| {});
19275
19276 let mut cx = EditorTestContext::new(cx).await;
19277 cx.set_head_text(indoc! { "
19278 one
19279 two
19280 three
19281 four
19282 five
19283 "
19284 });
19285 cx.set_index_text(indoc! { "
19286 one
19287 two
19288 three
19289 four
19290 five
19291 "
19292 });
19293 cx.set_state(indoc! {"
19294 one
19295 TWO
19296 ˇTHREE
19297 FOUR
19298 five
19299 "});
19300 cx.run_until_parked();
19301 cx.update_editor(|editor, window, cx| {
19302 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19303 });
19304 cx.run_until_parked();
19305 cx.assert_index_text(Some(indoc! {"
19306 one
19307 TWO
19308 THREE
19309 FOUR
19310 five
19311 "}));
19312 cx.set_state(indoc! { "
19313 one
19314 TWO
19315 ˇTHREE-HUNDRED
19316 FOUR
19317 five
19318 "});
19319 cx.run_until_parked();
19320 cx.update_editor(|editor, window, cx| {
19321 let snapshot = editor.snapshot(window, cx);
19322 let hunks = editor
19323 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19324 .collect::<Vec<_>>();
19325 assert_eq!(hunks.len(), 1);
19326 assert_eq!(
19327 hunks[0].status(),
19328 DiffHunkStatus {
19329 kind: DiffHunkStatusKind::Modified,
19330 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
19331 }
19332 );
19333
19334 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19335 });
19336 cx.run_until_parked();
19337 cx.assert_index_text(Some(indoc! {"
19338 one
19339 TWO
19340 THREE-HUNDRED
19341 FOUR
19342 five
19343 "}));
19344}
19345
19346#[gpui::test]
19347fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
19348 init_test(cx, |_| {});
19349
19350 let editor = cx.add_window(|window, cx| {
19351 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
19352 build_editor(buffer, window, cx)
19353 });
19354
19355 let render_args = Arc::new(Mutex::new(None));
19356 let snapshot = editor
19357 .update(cx, |editor, window, cx| {
19358 let snapshot = editor.buffer().read(cx).snapshot(cx);
19359 let range =
19360 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
19361
19362 struct RenderArgs {
19363 row: MultiBufferRow,
19364 folded: bool,
19365 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
19366 }
19367
19368 let crease = Crease::inline(
19369 range,
19370 FoldPlaceholder::test(),
19371 {
19372 let toggle_callback = render_args.clone();
19373 move |row, folded, callback, _window, _cx| {
19374 *toggle_callback.lock() = Some(RenderArgs {
19375 row,
19376 folded,
19377 callback,
19378 });
19379 div()
19380 }
19381 },
19382 |_row, _folded, _window, _cx| div(),
19383 );
19384
19385 editor.insert_creases(Some(crease), cx);
19386 let snapshot = editor.snapshot(window, cx);
19387 let _div = snapshot.render_crease_toggle(
19388 MultiBufferRow(1),
19389 false,
19390 cx.entity().clone(),
19391 window,
19392 cx,
19393 );
19394 snapshot
19395 })
19396 .unwrap();
19397
19398 let render_args = render_args.lock().take().unwrap();
19399 assert_eq!(render_args.row, MultiBufferRow(1));
19400 assert!(!render_args.folded);
19401 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19402
19403 cx.update_window(*editor, |_, window, cx| {
19404 (render_args.callback)(true, window, cx)
19405 })
19406 .unwrap();
19407 let snapshot = editor
19408 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19409 .unwrap();
19410 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
19411
19412 cx.update_window(*editor, |_, window, cx| {
19413 (render_args.callback)(false, window, cx)
19414 })
19415 .unwrap();
19416 let snapshot = editor
19417 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19418 .unwrap();
19419 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19420}
19421
19422#[gpui::test]
19423async fn test_input_text(cx: &mut TestAppContext) {
19424 init_test(cx, |_| {});
19425 let mut cx = EditorTestContext::new(cx).await;
19426
19427 cx.set_state(
19428 &r#"ˇone
19429 two
19430
19431 three
19432 fourˇ
19433 five
19434
19435 siˇx"#
19436 .unindent(),
19437 );
19438
19439 cx.dispatch_action(HandleInput(String::new()));
19440 cx.assert_editor_state(
19441 &r#"ˇone
19442 two
19443
19444 three
19445 fourˇ
19446 five
19447
19448 siˇx"#
19449 .unindent(),
19450 );
19451
19452 cx.dispatch_action(HandleInput("AAAA".to_string()));
19453 cx.assert_editor_state(
19454 &r#"AAAAˇone
19455 two
19456
19457 three
19458 fourAAAAˇ
19459 five
19460
19461 siAAAAˇx"#
19462 .unindent(),
19463 );
19464}
19465
19466#[gpui::test]
19467async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
19468 init_test(cx, |_| {});
19469
19470 let mut cx = EditorTestContext::new(cx).await;
19471 cx.set_state(
19472 r#"let foo = 1;
19473let foo = 2;
19474let foo = 3;
19475let fooˇ = 4;
19476let foo = 5;
19477let foo = 6;
19478let foo = 7;
19479let foo = 8;
19480let foo = 9;
19481let foo = 10;
19482let foo = 11;
19483let foo = 12;
19484let foo = 13;
19485let foo = 14;
19486let foo = 15;"#,
19487 );
19488
19489 cx.update_editor(|e, window, cx| {
19490 assert_eq!(
19491 e.next_scroll_position,
19492 NextScrollCursorCenterTopBottom::Center,
19493 "Default next scroll direction is center",
19494 );
19495
19496 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19497 assert_eq!(
19498 e.next_scroll_position,
19499 NextScrollCursorCenterTopBottom::Top,
19500 "After center, next scroll direction should be top",
19501 );
19502
19503 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19504 assert_eq!(
19505 e.next_scroll_position,
19506 NextScrollCursorCenterTopBottom::Bottom,
19507 "After top, next scroll direction should be bottom",
19508 );
19509
19510 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19511 assert_eq!(
19512 e.next_scroll_position,
19513 NextScrollCursorCenterTopBottom::Center,
19514 "After bottom, scrolling should start over",
19515 );
19516
19517 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19518 assert_eq!(
19519 e.next_scroll_position,
19520 NextScrollCursorCenterTopBottom::Top,
19521 "Scrolling continues if retriggered fast enough"
19522 );
19523 });
19524
19525 cx.executor()
19526 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
19527 cx.executor().run_until_parked();
19528 cx.update_editor(|e, _, _| {
19529 assert_eq!(
19530 e.next_scroll_position,
19531 NextScrollCursorCenterTopBottom::Center,
19532 "If scrolling is not triggered fast enough, it should reset"
19533 );
19534 });
19535}
19536
19537#[gpui::test]
19538async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
19539 init_test(cx, |_| {});
19540 let mut cx = EditorLspTestContext::new_rust(
19541 lsp::ServerCapabilities {
19542 definition_provider: Some(lsp::OneOf::Left(true)),
19543 references_provider: Some(lsp::OneOf::Left(true)),
19544 ..lsp::ServerCapabilities::default()
19545 },
19546 cx,
19547 )
19548 .await;
19549
19550 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
19551 let go_to_definition = cx
19552 .lsp
19553 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19554 move |params, _| async move {
19555 if empty_go_to_definition {
19556 Ok(None)
19557 } else {
19558 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
19559 uri: params.text_document_position_params.text_document.uri,
19560 range: lsp::Range::new(
19561 lsp::Position::new(4, 3),
19562 lsp::Position::new(4, 6),
19563 ),
19564 })))
19565 }
19566 },
19567 );
19568 let references = cx
19569 .lsp
19570 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
19571 Ok(Some(vec![lsp::Location {
19572 uri: params.text_document_position.text_document.uri,
19573 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
19574 }]))
19575 });
19576 (go_to_definition, references)
19577 };
19578
19579 cx.set_state(
19580 &r#"fn one() {
19581 let mut a = ˇtwo();
19582 }
19583
19584 fn two() {}"#
19585 .unindent(),
19586 );
19587 set_up_lsp_handlers(false, &mut cx);
19588 let navigated = cx
19589 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19590 .await
19591 .expect("Failed to navigate to definition");
19592 assert_eq!(
19593 navigated,
19594 Navigated::Yes,
19595 "Should have navigated to definition from the GetDefinition response"
19596 );
19597 cx.assert_editor_state(
19598 &r#"fn one() {
19599 let mut a = two();
19600 }
19601
19602 fn «twoˇ»() {}"#
19603 .unindent(),
19604 );
19605
19606 let editors = cx.update_workspace(|workspace, _, cx| {
19607 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19608 });
19609 cx.update_editor(|_, _, test_editor_cx| {
19610 assert_eq!(
19611 editors.len(),
19612 1,
19613 "Initially, only one, test, editor should be open in the workspace"
19614 );
19615 assert_eq!(
19616 test_editor_cx.entity(),
19617 editors.last().expect("Asserted len is 1").clone()
19618 );
19619 });
19620
19621 set_up_lsp_handlers(true, &mut cx);
19622 let navigated = cx
19623 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19624 .await
19625 .expect("Failed to navigate to lookup references");
19626 assert_eq!(
19627 navigated,
19628 Navigated::Yes,
19629 "Should have navigated to references as a fallback after empty GoToDefinition response"
19630 );
19631 // We should not change the selections in the existing file,
19632 // if opening another milti buffer with the references
19633 cx.assert_editor_state(
19634 &r#"fn one() {
19635 let mut a = two();
19636 }
19637
19638 fn «twoˇ»() {}"#
19639 .unindent(),
19640 );
19641 let editors = cx.update_workspace(|workspace, _, cx| {
19642 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19643 });
19644 cx.update_editor(|_, _, test_editor_cx| {
19645 assert_eq!(
19646 editors.len(),
19647 2,
19648 "After falling back to references search, we open a new editor with the results"
19649 );
19650 let references_fallback_text = editors
19651 .into_iter()
19652 .find(|new_editor| *new_editor != test_editor_cx.entity())
19653 .expect("Should have one non-test editor now")
19654 .read(test_editor_cx)
19655 .text(test_editor_cx);
19656 assert_eq!(
19657 references_fallback_text, "fn one() {\n let mut a = two();\n}",
19658 "Should use the range from the references response and not the GoToDefinition one"
19659 );
19660 });
19661}
19662
19663#[gpui::test]
19664async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
19665 init_test(cx, |_| {});
19666 cx.update(|cx| {
19667 let mut editor_settings = EditorSettings::get_global(cx).clone();
19668 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
19669 EditorSettings::override_global(editor_settings, cx);
19670 });
19671 let mut cx = EditorLspTestContext::new_rust(
19672 lsp::ServerCapabilities {
19673 definition_provider: Some(lsp::OneOf::Left(true)),
19674 references_provider: Some(lsp::OneOf::Left(true)),
19675 ..lsp::ServerCapabilities::default()
19676 },
19677 cx,
19678 )
19679 .await;
19680 let original_state = r#"fn one() {
19681 let mut a = ˇtwo();
19682 }
19683
19684 fn two() {}"#
19685 .unindent();
19686 cx.set_state(&original_state);
19687
19688 let mut go_to_definition = cx
19689 .lsp
19690 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19691 move |_, _| async move { Ok(None) },
19692 );
19693 let _references = cx
19694 .lsp
19695 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
19696 panic!("Should not call for references with no go to definition fallback")
19697 });
19698
19699 let navigated = cx
19700 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19701 .await
19702 .expect("Failed to navigate to lookup references");
19703 go_to_definition
19704 .next()
19705 .await
19706 .expect("Should have called the go_to_definition handler");
19707
19708 assert_eq!(
19709 navigated,
19710 Navigated::No,
19711 "Should have navigated to references as a fallback after empty GoToDefinition response"
19712 );
19713 cx.assert_editor_state(&original_state);
19714 let editors = cx.update_workspace(|workspace, _, cx| {
19715 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19716 });
19717 cx.update_editor(|_, _, _| {
19718 assert_eq!(
19719 editors.len(),
19720 1,
19721 "After unsuccessful fallback, no other editor should have been opened"
19722 );
19723 });
19724}
19725
19726#[gpui::test]
19727async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
19728 init_test(cx, |_| {});
19729
19730 let language = Arc::new(Language::new(
19731 LanguageConfig::default(),
19732 Some(tree_sitter_rust::LANGUAGE.into()),
19733 ));
19734
19735 let text = r#"
19736 #[cfg(test)]
19737 mod tests() {
19738 #[test]
19739 fn runnable_1() {
19740 let a = 1;
19741 }
19742
19743 #[test]
19744 fn runnable_2() {
19745 let a = 1;
19746 let b = 2;
19747 }
19748 }
19749 "#
19750 .unindent();
19751
19752 let fs = FakeFs::new(cx.executor());
19753 fs.insert_file("/file.rs", Default::default()).await;
19754
19755 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19756 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19757 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19758 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
19759 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
19760
19761 let editor = cx.new_window_entity(|window, cx| {
19762 Editor::new(
19763 EditorMode::full(),
19764 multi_buffer,
19765 Some(project.clone()),
19766 window,
19767 cx,
19768 )
19769 });
19770
19771 editor.update_in(cx, |editor, window, cx| {
19772 let snapshot = editor.buffer().read(cx).snapshot(cx);
19773 editor.tasks.insert(
19774 (buffer.read(cx).remote_id(), 3),
19775 RunnableTasks {
19776 templates: vec![],
19777 offset: snapshot.anchor_before(43),
19778 column: 0,
19779 extra_variables: HashMap::default(),
19780 context_range: BufferOffset(43)..BufferOffset(85),
19781 },
19782 );
19783 editor.tasks.insert(
19784 (buffer.read(cx).remote_id(), 8),
19785 RunnableTasks {
19786 templates: vec![],
19787 offset: snapshot.anchor_before(86),
19788 column: 0,
19789 extra_variables: HashMap::default(),
19790 context_range: BufferOffset(86)..BufferOffset(191),
19791 },
19792 );
19793
19794 // Test finding task when cursor is inside function body
19795 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19796 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
19797 });
19798 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19799 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
19800
19801 // Test finding task when cursor is on function name
19802 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19803 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
19804 });
19805 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19806 assert_eq!(row, 8, "Should find task when cursor is on function name");
19807 });
19808}
19809
19810#[gpui::test]
19811async fn test_folding_buffers(cx: &mut TestAppContext) {
19812 init_test(cx, |_| {});
19813
19814 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19815 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
19816 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
19817
19818 let fs = FakeFs::new(cx.executor());
19819 fs.insert_tree(
19820 path!("/a"),
19821 json!({
19822 "first.rs": sample_text_1,
19823 "second.rs": sample_text_2,
19824 "third.rs": sample_text_3,
19825 }),
19826 )
19827 .await;
19828 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19829 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19830 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19831 let worktree = project.update(cx, |project, cx| {
19832 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19833 assert_eq!(worktrees.len(), 1);
19834 worktrees.pop().unwrap()
19835 });
19836 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19837
19838 let buffer_1 = project
19839 .update(cx, |project, cx| {
19840 project.open_buffer((worktree_id, "first.rs"), cx)
19841 })
19842 .await
19843 .unwrap();
19844 let buffer_2 = project
19845 .update(cx, |project, cx| {
19846 project.open_buffer((worktree_id, "second.rs"), cx)
19847 })
19848 .await
19849 .unwrap();
19850 let buffer_3 = project
19851 .update(cx, |project, cx| {
19852 project.open_buffer((worktree_id, "third.rs"), cx)
19853 })
19854 .await
19855 .unwrap();
19856
19857 let multi_buffer = cx.new(|cx| {
19858 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19859 multi_buffer.push_excerpts(
19860 buffer_1.clone(),
19861 [
19862 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19863 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19864 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19865 ],
19866 cx,
19867 );
19868 multi_buffer.push_excerpts(
19869 buffer_2.clone(),
19870 [
19871 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19872 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19873 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19874 ],
19875 cx,
19876 );
19877 multi_buffer.push_excerpts(
19878 buffer_3.clone(),
19879 [
19880 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19881 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19882 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19883 ],
19884 cx,
19885 );
19886 multi_buffer
19887 });
19888 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19889 Editor::new(
19890 EditorMode::full(),
19891 multi_buffer.clone(),
19892 Some(project.clone()),
19893 window,
19894 cx,
19895 )
19896 });
19897
19898 assert_eq!(
19899 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19900 "\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",
19901 );
19902
19903 multi_buffer_editor.update(cx, |editor, cx| {
19904 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19905 });
19906 assert_eq!(
19907 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19908 "\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",
19909 "After folding the first buffer, its text should not be displayed"
19910 );
19911
19912 multi_buffer_editor.update(cx, |editor, cx| {
19913 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19914 });
19915 assert_eq!(
19916 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19917 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
19918 "After folding the second buffer, its text should not be displayed"
19919 );
19920
19921 multi_buffer_editor.update(cx, |editor, cx| {
19922 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19923 });
19924 assert_eq!(
19925 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19926 "\n\n\n\n\n",
19927 "After folding the third buffer, its text should not be displayed"
19928 );
19929
19930 // Emulate selection inside the fold logic, that should work
19931 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19932 editor
19933 .snapshot(window, cx)
19934 .next_line_boundary(Point::new(0, 4));
19935 });
19936
19937 multi_buffer_editor.update(cx, |editor, cx| {
19938 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19939 });
19940 assert_eq!(
19941 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19942 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19943 "After unfolding the second buffer, its text should be displayed"
19944 );
19945
19946 // Typing inside of buffer 1 causes that buffer to be unfolded.
19947 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19948 assert_eq!(
19949 multi_buffer
19950 .read(cx)
19951 .snapshot(cx)
19952 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
19953 .collect::<String>(),
19954 "bbbb"
19955 );
19956 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
19957 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
19958 });
19959 editor.handle_input("B", window, cx);
19960 });
19961
19962 assert_eq!(
19963 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19964 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19965 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
19966 );
19967
19968 multi_buffer_editor.update(cx, |editor, cx| {
19969 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19970 });
19971 assert_eq!(
19972 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19973 "\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",
19974 "After unfolding the all buffers, all original text should be displayed"
19975 );
19976}
19977
19978#[gpui::test]
19979async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
19980 init_test(cx, |_| {});
19981
19982 let sample_text_1 = "1111\n2222\n3333".to_string();
19983 let sample_text_2 = "4444\n5555\n6666".to_string();
19984 let sample_text_3 = "7777\n8888\n9999".to_string();
19985
19986 let fs = FakeFs::new(cx.executor());
19987 fs.insert_tree(
19988 path!("/a"),
19989 json!({
19990 "first.rs": sample_text_1,
19991 "second.rs": sample_text_2,
19992 "third.rs": sample_text_3,
19993 }),
19994 )
19995 .await;
19996 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19997 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19998 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19999 let worktree = project.update(cx, |project, cx| {
20000 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20001 assert_eq!(worktrees.len(), 1);
20002 worktrees.pop().unwrap()
20003 });
20004 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20005
20006 let buffer_1 = project
20007 .update(cx, |project, cx| {
20008 project.open_buffer((worktree_id, "first.rs"), cx)
20009 })
20010 .await
20011 .unwrap();
20012 let buffer_2 = project
20013 .update(cx, |project, cx| {
20014 project.open_buffer((worktree_id, "second.rs"), cx)
20015 })
20016 .await
20017 .unwrap();
20018 let buffer_3 = project
20019 .update(cx, |project, cx| {
20020 project.open_buffer((worktree_id, "third.rs"), cx)
20021 })
20022 .await
20023 .unwrap();
20024
20025 let multi_buffer = cx.new(|cx| {
20026 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20027 multi_buffer.push_excerpts(
20028 buffer_1.clone(),
20029 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20030 cx,
20031 );
20032 multi_buffer.push_excerpts(
20033 buffer_2.clone(),
20034 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20035 cx,
20036 );
20037 multi_buffer.push_excerpts(
20038 buffer_3.clone(),
20039 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
20040 cx,
20041 );
20042 multi_buffer
20043 });
20044
20045 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20046 Editor::new(
20047 EditorMode::full(),
20048 multi_buffer,
20049 Some(project.clone()),
20050 window,
20051 cx,
20052 )
20053 });
20054
20055 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
20056 assert_eq!(
20057 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20058 full_text,
20059 );
20060
20061 multi_buffer_editor.update(cx, |editor, cx| {
20062 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
20063 });
20064 assert_eq!(
20065 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20066 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
20067 "After folding the first buffer, its text should not be displayed"
20068 );
20069
20070 multi_buffer_editor.update(cx, |editor, cx| {
20071 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
20072 });
20073
20074 assert_eq!(
20075 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20076 "\n\n\n\n\n\n7777\n8888\n9999",
20077 "After folding the second buffer, its text should not be displayed"
20078 );
20079
20080 multi_buffer_editor.update(cx, |editor, cx| {
20081 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
20082 });
20083 assert_eq!(
20084 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20085 "\n\n\n\n\n",
20086 "After folding the third buffer, its text should not be displayed"
20087 );
20088
20089 multi_buffer_editor.update(cx, |editor, cx| {
20090 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
20091 });
20092 assert_eq!(
20093 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20094 "\n\n\n\n4444\n5555\n6666\n\n",
20095 "After unfolding the second buffer, its text should be displayed"
20096 );
20097
20098 multi_buffer_editor.update(cx, |editor, cx| {
20099 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
20100 });
20101 assert_eq!(
20102 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20103 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
20104 "After unfolding the first buffer, its text should be displayed"
20105 );
20106
20107 multi_buffer_editor.update(cx, |editor, cx| {
20108 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
20109 });
20110 assert_eq!(
20111 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20112 full_text,
20113 "After unfolding all buffers, all original text should be displayed"
20114 );
20115}
20116
20117#[gpui::test]
20118async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
20119 init_test(cx, |_| {});
20120
20121 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
20122
20123 let fs = FakeFs::new(cx.executor());
20124 fs.insert_tree(
20125 path!("/a"),
20126 json!({
20127 "main.rs": sample_text,
20128 }),
20129 )
20130 .await;
20131 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20132 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20133 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20134 let worktree = project.update(cx, |project, cx| {
20135 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20136 assert_eq!(worktrees.len(), 1);
20137 worktrees.pop().unwrap()
20138 });
20139 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20140
20141 let buffer_1 = project
20142 .update(cx, |project, cx| {
20143 project.open_buffer((worktree_id, "main.rs"), cx)
20144 })
20145 .await
20146 .unwrap();
20147
20148 let multi_buffer = cx.new(|cx| {
20149 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20150 multi_buffer.push_excerpts(
20151 buffer_1.clone(),
20152 [ExcerptRange::new(
20153 Point::new(0, 0)
20154 ..Point::new(
20155 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
20156 0,
20157 ),
20158 )],
20159 cx,
20160 );
20161 multi_buffer
20162 });
20163 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20164 Editor::new(
20165 EditorMode::full(),
20166 multi_buffer,
20167 Some(project.clone()),
20168 window,
20169 cx,
20170 )
20171 });
20172
20173 let selection_range = Point::new(1, 0)..Point::new(2, 0);
20174 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20175 enum TestHighlight {}
20176 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
20177 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
20178 editor.highlight_text::<TestHighlight>(
20179 vec![highlight_range.clone()],
20180 HighlightStyle::color(Hsla::green()),
20181 cx,
20182 );
20183 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20184 s.select_ranges(Some(highlight_range))
20185 });
20186 });
20187
20188 let full_text = format!("\n\n{sample_text}");
20189 assert_eq!(
20190 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20191 full_text,
20192 );
20193}
20194
20195#[gpui::test]
20196async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
20197 init_test(cx, |_| {});
20198 cx.update(|cx| {
20199 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
20200 "keymaps/default-linux.json",
20201 cx,
20202 )
20203 .unwrap();
20204 cx.bind_keys(default_key_bindings);
20205 });
20206
20207 let (editor, cx) = cx.add_window_view(|window, cx| {
20208 let multi_buffer = MultiBuffer::build_multi(
20209 [
20210 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
20211 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
20212 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
20213 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
20214 ],
20215 cx,
20216 );
20217 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
20218
20219 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
20220 // fold all but the second buffer, so that we test navigating between two
20221 // adjacent folded buffers, as well as folded buffers at the start and
20222 // end the multibuffer
20223 editor.fold_buffer(buffer_ids[0], cx);
20224 editor.fold_buffer(buffer_ids[2], cx);
20225 editor.fold_buffer(buffer_ids[3], cx);
20226
20227 editor
20228 });
20229 cx.simulate_resize(size(px(1000.), px(1000.)));
20230
20231 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
20232 cx.assert_excerpts_with_selections(indoc! {"
20233 [EXCERPT]
20234 ˇ[FOLDED]
20235 [EXCERPT]
20236 a1
20237 b1
20238 [EXCERPT]
20239 [FOLDED]
20240 [EXCERPT]
20241 [FOLDED]
20242 "
20243 });
20244 cx.simulate_keystroke("down");
20245 cx.assert_excerpts_with_selections(indoc! {"
20246 [EXCERPT]
20247 [FOLDED]
20248 [EXCERPT]
20249 ˇa1
20250 b1
20251 [EXCERPT]
20252 [FOLDED]
20253 [EXCERPT]
20254 [FOLDED]
20255 "
20256 });
20257 cx.simulate_keystroke("down");
20258 cx.assert_excerpts_with_selections(indoc! {"
20259 [EXCERPT]
20260 [FOLDED]
20261 [EXCERPT]
20262 a1
20263 ˇb1
20264 [EXCERPT]
20265 [FOLDED]
20266 [EXCERPT]
20267 [FOLDED]
20268 "
20269 });
20270 cx.simulate_keystroke("down");
20271 cx.assert_excerpts_with_selections(indoc! {"
20272 [EXCERPT]
20273 [FOLDED]
20274 [EXCERPT]
20275 a1
20276 b1
20277 ˇ[EXCERPT]
20278 [FOLDED]
20279 [EXCERPT]
20280 [FOLDED]
20281 "
20282 });
20283 cx.simulate_keystroke("down");
20284 cx.assert_excerpts_with_selections(indoc! {"
20285 [EXCERPT]
20286 [FOLDED]
20287 [EXCERPT]
20288 a1
20289 b1
20290 [EXCERPT]
20291 ˇ[FOLDED]
20292 [EXCERPT]
20293 [FOLDED]
20294 "
20295 });
20296 for _ in 0..5 {
20297 cx.simulate_keystroke("down");
20298 cx.assert_excerpts_with_selections(indoc! {"
20299 [EXCERPT]
20300 [FOLDED]
20301 [EXCERPT]
20302 a1
20303 b1
20304 [EXCERPT]
20305 [FOLDED]
20306 [EXCERPT]
20307 ˇ[FOLDED]
20308 "
20309 });
20310 }
20311
20312 cx.simulate_keystroke("up");
20313 cx.assert_excerpts_with_selections(indoc! {"
20314 [EXCERPT]
20315 [FOLDED]
20316 [EXCERPT]
20317 a1
20318 b1
20319 [EXCERPT]
20320 ˇ[FOLDED]
20321 [EXCERPT]
20322 [FOLDED]
20323 "
20324 });
20325 cx.simulate_keystroke("up");
20326 cx.assert_excerpts_with_selections(indoc! {"
20327 [EXCERPT]
20328 [FOLDED]
20329 [EXCERPT]
20330 a1
20331 b1
20332 ˇ[EXCERPT]
20333 [FOLDED]
20334 [EXCERPT]
20335 [FOLDED]
20336 "
20337 });
20338 cx.simulate_keystroke("up");
20339 cx.assert_excerpts_with_selections(indoc! {"
20340 [EXCERPT]
20341 [FOLDED]
20342 [EXCERPT]
20343 a1
20344 ˇb1
20345 [EXCERPT]
20346 [FOLDED]
20347 [EXCERPT]
20348 [FOLDED]
20349 "
20350 });
20351 cx.simulate_keystroke("up");
20352 cx.assert_excerpts_with_selections(indoc! {"
20353 [EXCERPT]
20354 [FOLDED]
20355 [EXCERPT]
20356 ˇa1
20357 b1
20358 [EXCERPT]
20359 [FOLDED]
20360 [EXCERPT]
20361 [FOLDED]
20362 "
20363 });
20364 for _ in 0..5 {
20365 cx.simulate_keystroke("up");
20366 cx.assert_excerpts_with_selections(indoc! {"
20367 [EXCERPT]
20368 ˇ[FOLDED]
20369 [EXCERPT]
20370 a1
20371 b1
20372 [EXCERPT]
20373 [FOLDED]
20374 [EXCERPT]
20375 [FOLDED]
20376 "
20377 });
20378 }
20379}
20380
20381#[gpui::test]
20382async fn test_inline_completion_text(cx: &mut TestAppContext) {
20383 init_test(cx, |_| {});
20384
20385 // Simple insertion
20386 assert_highlighted_edits(
20387 "Hello, world!",
20388 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
20389 true,
20390 cx,
20391 |highlighted_edits, cx| {
20392 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
20393 assert_eq!(highlighted_edits.highlights.len(), 1);
20394 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
20395 assert_eq!(
20396 highlighted_edits.highlights[0].1.background_color,
20397 Some(cx.theme().status().created_background)
20398 );
20399 },
20400 )
20401 .await;
20402
20403 // Replacement
20404 assert_highlighted_edits(
20405 "This is a test.",
20406 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
20407 false,
20408 cx,
20409 |highlighted_edits, cx| {
20410 assert_eq!(highlighted_edits.text, "That is a test.");
20411 assert_eq!(highlighted_edits.highlights.len(), 1);
20412 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
20413 assert_eq!(
20414 highlighted_edits.highlights[0].1.background_color,
20415 Some(cx.theme().status().created_background)
20416 );
20417 },
20418 )
20419 .await;
20420
20421 // Multiple edits
20422 assert_highlighted_edits(
20423 "Hello, world!",
20424 vec![
20425 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
20426 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
20427 ],
20428 false,
20429 cx,
20430 |highlighted_edits, cx| {
20431 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
20432 assert_eq!(highlighted_edits.highlights.len(), 2);
20433 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
20434 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
20435 assert_eq!(
20436 highlighted_edits.highlights[0].1.background_color,
20437 Some(cx.theme().status().created_background)
20438 );
20439 assert_eq!(
20440 highlighted_edits.highlights[1].1.background_color,
20441 Some(cx.theme().status().created_background)
20442 );
20443 },
20444 )
20445 .await;
20446
20447 // Multiple lines with edits
20448 assert_highlighted_edits(
20449 "First line\nSecond line\nThird line\nFourth line",
20450 vec![
20451 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
20452 (
20453 Point::new(2, 0)..Point::new(2, 10),
20454 "New third line".to_string(),
20455 ),
20456 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
20457 ],
20458 false,
20459 cx,
20460 |highlighted_edits, cx| {
20461 assert_eq!(
20462 highlighted_edits.text,
20463 "Second modified\nNew third line\nFourth updated line"
20464 );
20465 assert_eq!(highlighted_edits.highlights.len(), 3);
20466 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
20467 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
20468 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
20469 for highlight in &highlighted_edits.highlights {
20470 assert_eq!(
20471 highlight.1.background_color,
20472 Some(cx.theme().status().created_background)
20473 );
20474 }
20475 },
20476 )
20477 .await;
20478}
20479
20480#[gpui::test]
20481async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
20482 init_test(cx, |_| {});
20483
20484 // Deletion
20485 assert_highlighted_edits(
20486 "Hello, world!",
20487 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
20488 true,
20489 cx,
20490 |highlighted_edits, cx| {
20491 assert_eq!(highlighted_edits.text, "Hello, world!");
20492 assert_eq!(highlighted_edits.highlights.len(), 1);
20493 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
20494 assert_eq!(
20495 highlighted_edits.highlights[0].1.background_color,
20496 Some(cx.theme().status().deleted_background)
20497 );
20498 },
20499 )
20500 .await;
20501
20502 // Insertion
20503 assert_highlighted_edits(
20504 "Hello, world!",
20505 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
20506 true,
20507 cx,
20508 |highlighted_edits, cx| {
20509 assert_eq!(highlighted_edits.highlights.len(), 1);
20510 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
20511 assert_eq!(
20512 highlighted_edits.highlights[0].1.background_color,
20513 Some(cx.theme().status().created_background)
20514 );
20515 },
20516 )
20517 .await;
20518}
20519
20520async fn assert_highlighted_edits(
20521 text: &str,
20522 edits: Vec<(Range<Point>, String)>,
20523 include_deletions: bool,
20524 cx: &mut TestAppContext,
20525 assertion_fn: impl Fn(HighlightedText, &App),
20526) {
20527 let window = cx.add_window(|window, cx| {
20528 let buffer = MultiBuffer::build_simple(text, cx);
20529 Editor::new(EditorMode::full(), buffer, None, window, cx)
20530 });
20531 let cx = &mut VisualTestContext::from_window(*window, cx);
20532
20533 let (buffer, snapshot) = window
20534 .update(cx, |editor, _window, cx| {
20535 (
20536 editor.buffer().clone(),
20537 editor.buffer().read(cx).snapshot(cx),
20538 )
20539 })
20540 .unwrap();
20541
20542 let edits = edits
20543 .into_iter()
20544 .map(|(range, edit)| {
20545 (
20546 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
20547 edit,
20548 )
20549 })
20550 .collect::<Vec<_>>();
20551
20552 let text_anchor_edits = edits
20553 .clone()
20554 .into_iter()
20555 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
20556 .collect::<Vec<_>>();
20557
20558 let edit_preview = window
20559 .update(cx, |_, _window, cx| {
20560 buffer
20561 .read(cx)
20562 .as_singleton()
20563 .unwrap()
20564 .read(cx)
20565 .preview_edits(text_anchor_edits.into(), cx)
20566 })
20567 .unwrap()
20568 .await;
20569
20570 cx.update(|_window, cx| {
20571 let highlighted_edits = inline_completion_edit_text(
20572 &snapshot.as_singleton().unwrap().2,
20573 &edits,
20574 &edit_preview,
20575 include_deletions,
20576 cx,
20577 );
20578 assertion_fn(highlighted_edits, cx)
20579 });
20580}
20581
20582#[track_caller]
20583fn assert_breakpoint(
20584 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
20585 path: &Arc<Path>,
20586 expected: Vec<(u32, Breakpoint)>,
20587) {
20588 if expected.len() == 0usize {
20589 assert!(!breakpoints.contains_key(path), "{}", path.display());
20590 } else {
20591 let mut breakpoint = breakpoints
20592 .get(path)
20593 .unwrap()
20594 .into_iter()
20595 .map(|breakpoint| {
20596 (
20597 breakpoint.row,
20598 Breakpoint {
20599 message: breakpoint.message.clone(),
20600 state: breakpoint.state,
20601 condition: breakpoint.condition.clone(),
20602 hit_condition: breakpoint.hit_condition.clone(),
20603 },
20604 )
20605 })
20606 .collect::<Vec<_>>();
20607
20608 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
20609
20610 assert_eq!(expected, breakpoint);
20611 }
20612}
20613
20614fn add_log_breakpoint_at_cursor(
20615 editor: &mut Editor,
20616 log_message: &str,
20617 window: &mut Window,
20618 cx: &mut Context<Editor>,
20619) {
20620 let (anchor, bp) = editor
20621 .breakpoints_at_cursors(window, cx)
20622 .first()
20623 .and_then(|(anchor, bp)| {
20624 if let Some(bp) = bp {
20625 Some((*anchor, bp.clone()))
20626 } else {
20627 None
20628 }
20629 })
20630 .unwrap_or_else(|| {
20631 let cursor_position: Point = editor.selections.newest(cx).head();
20632
20633 let breakpoint_position = editor
20634 .snapshot(window, cx)
20635 .display_snapshot
20636 .buffer_snapshot
20637 .anchor_before(Point::new(cursor_position.row, 0));
20638
20639 (breakpoint_position, Breakpoint::new_log(&log_message))
20640 });
20641
20642 editor.edit_breakpoint_at_anchor(
20643 anchor,
20644 bp,
20645 BreakpointEditAction::EditLogMessage(log_message.into()),
20646 cx,
20647 );
20648}
20649
20650#[gpui::test]
20651async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
20652 init_test(cx, |_| {});
20653
20654 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20655 let fs = FakeFs::new(cx.executor());
20656 fs.insert_tree(
20657 path!("/a"),
20658 json!({
20659 "main.rs": sample_text,
20660 }),
20661 )
20662 .await;
20663 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20664 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20665 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20666
20667 let fs = FakeFs::new(cx.executor());
20668 fs.insert_tree(
20669 path!("/a"),
20670 json!({
20671 "main.rs": sample_text,
20672 }),
20673 )
20674 .await;
20675 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20676 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20677 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20678 let worktree_id = workspace
20679 .update(cx, |workspace, _window, cx| {
20680 workspace.project().update(cx, |project, cx| {
20681 project.worktrees(cx).next().unwrap().read(cx).id()
20682 })
20683 })
20684 .unwrap();
20685
20686 let buffer = project
20687 .update(cx, |project, cx| {
20688 project.open_buffer((worktree_id, "main.rs"), cx)
20689 })
20690 .await
20691 .unwrap();
20692
20693 let (editor, cx) = cx.add_window_view(|window, cx| {
20694 Editor::new(
20695 EditorMode::full(),
20696 MultiBuffer::build_from_buffer(buffer, cx),
20697 Some(project.clone()),
20698 window,
20699 cx,
20700 )
20701 });
20702
20703 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20704 let abs_path = project.read_with(cx, |project, cx| {
20705 project
20706 .absolute_path(&project_path, cx)
20707 .map(|path_buf| Arc::from(path_buf.to_owned()))
20708 .unwrap()
20709 });
20710
20711 // assert we can add breakpoint on the first line
20712 editor.update_in(cx, |editor, window, cx| {
20713 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20714 editor.move_to_end(&MoveToEnd, window, cx);
20715 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20716 });
20717
20718 let breakpoints = editor.update(cx, |editor, cx| {
20719 editor
20720 .breakpoint_store()
20721 .as_ref()
20722 .unwrap()
20723 .read(cx)
20724 .all_source_breakpoints(cx)
20725 .clone()
20726 });
20727
20728 assert_eq!(1, breakpoints.len());
20729 assert_breakpoint(
20730 &breakpoints,
20731 &abs_path,
20732 vec![
20733 (0, Breakpoint::new_standard()),
20734 (3, Breakpoint::new_standard()),
20735 ],
20736 );
20737
20738 editor.update_in(cx, |editor, window, cx| {
20739 editor.move_to_beginning(&MoveToBeginning, window, cx);
20740 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20741 });
20742
20743 let breakpoints = editor.update(cx, |editor, cx| {
20744 editor
20745 .breakpoint_store()
20746 .as_ref()
20747 .unwrap()
20748 .read(cx)
20749 .all_source_breakpoints(cx)
20750 .clone()
20751 });
20752
20753 assert_eq!(1, breakpoints.len());
20754 assert_breakpoint(
20755 &breakpoints,
20756 &abs_path,
20757 vec![(3, Breakpoint::new_standard())],
20758 );
20759
20760 editor.update_in(cx, |editor, window, cx| {
20761 editor.move_to_end(&MoveToEnd, window, cx);
20762 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20763 });
20764
20765 let breakpoints = editor.update(cx, |editor, cx| {
20766 editor
20767 .breakpoint_store()
20768 .as_ref()
20769 .unwrap()
20770 .read(cx)
20771 .all_source_breakpoints(cx)
20772 .clone()
20773 });
20774
20775 assert_eq!(0, breakpoints.len());
20776 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20777}
20778
20779#[gpui::test]
20780async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
20781 init_test(cx, |_| {});
20782
20783 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20784
20785 let fs = FakeFs::new(cx.executor());
20786 fs.insert_tree(
20787 path!("/a"),
20788 json!({
20789 "main.rs": sample_text,
20790 }),
20791 )
20792 .await;
20793 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20794 let (workspace, cx) =
20795 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20796
20797 let worktree_id = workspace.update(cx, |workspace, cx| {
20798 workspace.project().update(cx, |project, cx| {
20799 project.worktrees(cx).next().unwrap().read(cx).id()
20800 })
20801 });
20802
20803 let buffer = project
20804 .update(cx, |project, cx| {
20805 project.open_buffer((worktree_id, "main.rs"), cx)
20806 })
20807 .await
20808 .unwrap();
20809
20810 let (editor, cx) = cx.add_window_view(|window, cx| {
20811 Editor::new(
20812 EditorMode::full(),
20813 MultiBuffer::build_from_buffer(buffer, cx),
20814 Some(project.clone()),
20815 window,
20816 cx,
20817 )
20818 });
20819
20820 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20821 let abs_path = project.read_with(cx, |project, cx| {
20822 project
20823 .absolute_path(&project_path, cx)
20824 .map(|path_buf| Arc::from(path_buf.to_owned()))
20825 .unwrap()
20826 });
20827
20828 editor.update_in(cx, |editor, window, cx| {
20829 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20830 });
20831
20832 let breakpoints = editor.update(cx, |editor, cx| {
20833 editor
20834 .breakpoint_store()
20835 .as_ref()
20836 .unwrap()
20837 .read(cx)
20838 .all_source_breakpoints(cx)
20839 .clone()
20840 });
20841
20842 assert_breakpoint(
20843 &breakpoints,
20844 &abs_path,
20845 vec![(0, Breakpoint::new_log("hello world"))],
20846 );
20847
20848 // Removing a log message from a log breakpoint should remove it
20849 editor.update_in(cx, |editor, window, cx| {
20850 add_log_breakpoint_at_cursor(editor, "", window, cx);
20851 });
20852
20853 let breakpoints = editor.update(cx, |editor, cx| {
20854 editor
20855 .breakpoint_store()
20856 .as_ref()
20857 .unwrap()
20858 .read(cx)
20859 .all_source_breakpoints(cx)
20860 .clone()
20861 });
20862
20863 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20864
20865 editor.update_in(cx, |editor, window, cx| {
20866 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20867 editor.move_to_end(&MoveToEnd, window, cx);
20868 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20869 // Not adding a log message to a standard breakpoint shouldn't remove it
20870 add_log_breakpoint_at_cursor(editor, "", window, cx);
20871 });
20872
20873 let breakpoints = editor.update(cx, |editor, cx| {
20874 editor
20875 .breakpoint_store()
20876 .as_ref()
20877 .unwrap()
20878 .read(cx)
20879 .all_source_breakpoints(cx)
20880 .clone()
20881 });
20882
20883 assert_breakpoint(
20884 &breakpoints,
20885 &abs_path,
20886 vec![
20887 (0, Breakpoint::new_standard()),
20888 (3, Breakpoint::new_standard()),
20889 ],
20890 );
20891
20892 editor.update_in(cx, |editor, window, cx| {
20893 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20894 });
20895
20896 let breakpoints = editor.update(cx, |editor, cx| {
20897 editor
20898 .breakpoint_store()
20899 .as_ref()
20900 .unwrap()
20901 .read(cx)
20902 .all_source_breakpoints(cx)
20903 .clone()
20904 });
20905
20906 assert_breakpoint(
20907 &breakpoints,
20908 &abs_path,
20909 vec![
20910 (0, Breakpoint::new_standard()),
20911 (3, Breakpoint::new_log("hello world")),
20912 ],
20913 );
20914
20915 editor.update_in(cx, |editor, window, cx| {
20916 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
20917 });
20918
20919 let breakpoints = editor.update(cx, |editor, cx| {
20920 editor
20921 .breakpoint_store()
20922 .as_ref()
20923 .unwrap()
20924 .read(cx)
20925 .all_source_breakpoints(cx)
20926 .clone()
20927 });
20928
20929 assert_breakpoint(
20930 &breakpoints,
20931 &abs_path,
20932 vec![
20933 (0, Breakpoint::new_standard()),
20934 (3, Breakpoint::new_log("hello Earth!!")),
20935 ],
20936 );
20937}
20938
20939/// This also tests that Editor::breakpoint_at_cursor_head is working properly
20940/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
20941/// or when breakpoints were placed out of order. This tests for a regression too
20942#[gpui::test]
20943async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
20944 init_test(cx, |_| {});
20945
20946 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20947 let fs = FakeFs::new(cx.executor());
20948 fs.insert_tree(
20949 path!("/a"),
20950 json!({
20951 "main.rs": sample_text,
20952 }),
20953 )
20954 .await;
20955 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20956 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20957 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20958
20959 let fs = FakeFs::new(cx.executor());
20960 fs.insert_tree(
20961 path!("/a"),
20962 json!({
20963 "main.rs": sample_text,
20964 }),
20965 )
20966 .await;
20967 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20968 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20969 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20970 let worktree_id = workspace
20971 .update(cx, |workspace, _window, cx| {
20972 workspace.project().update(cx, |project, cx| {
20973 project.worktrees(cx).next().unwrap().read(cx).id()
20974 })
20975 })
20976 .unwrap();
20977
20978 let buffer = project
20979 .update(cx, |project, cx| {
20980 project.open_buffer((worktree_id, "main.rs"), cx)
20981 })
20982 .await
20983 .unwrap();
20984
20985 let (editor, cx) = cx.add_window_view(|window, cx| {
20986 Editor::new(
20987 EditorMode::full(),
20988 MultiBuffer::build_from_buffer(buffer, cx),
20989 Some(project.clone()),
20990 window,
20991 cx,
20992 )
20993 });
20994
20995 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20996 let abs_path = project.read_with(cx, |project, cx| {
20997 project
20998 .absolute_path(&project_path, cx)
20999 .map(|path_buf| Arc::from(path_buf.to_owned()))
21000 .unwrap()
21001 });
21002
21003 // assert we can add breakpoint on the first line
21004 editor.update_in(cx, |editor, window, cx| {
21005 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21006 editor.move_to_end(&MoveToEnd, window, cx);
21007 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21008 editor.move_up(&MoveUp, window, cx);
21009 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
21010 });
21011
21012 let breakpoints = editor.update(cx, |editor, cx| {
21013 editor
21014 .breakpoint_store()
21015 .as_ref()
21016 .unwrap()
21017 .read(cx)
21018 .all_source_breakpoints(cx)
21019 .clone()
21020 });
21021
21022 assert_eq!(1, breakpoints.len());
21023 assert_breakpoint(
21024 &breakpoints,
21025 &abs_path,
21026 vec![
21027 (0, Breakpoint::new_standard()),
21028 (2, Breakpoint::new_standard()),
21029 (3, Breakpoint::new_standard()),
21030 ],
21031 );
21032
21033 editor.update_in(cx, |editor, window, cx| {
21034 editor.move_to_beginning(&MoveToBeginning, window, cx);
21035 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21036 editor.move_to_end(&MoveToEnd, window, cx);
21037 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21038 // Disabling a breakpoint that doesn't exist should do nothing
21039 editor.move_up(&MoveUp, window, cx);
21040 editor.move_up(&MoveUp, window, cx);
21041 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21042 });
21043
21044 let breakpoints = editor.update(cx, |editor, cx| {
21045 editor
21046 .breakpoint_store()
21047 .as_ref()
21048 .unwrap()
21049 .read(cx)
21050 .all_source_breakpoints(cx)
21051 .clone()
21052 });
21053
21054 let disable_breakpoint = {
21055 let mut bp = Breakpoint::new_standard();
21056 bp.state = BreakpointState::Disabled;
21057 bp
21058 };
21059
21060 assert_eq!(1, breakpoints.len());
21061 assert_breakpoint(
21062 &breakpoints,
21063 &abs_path,
21064 vec![
21065 (0, disable_breakpoint.clone()),
21066 (2, Breakpoint::new_standard()),
21067 (3, disable_breakpoint.clone()),
21068 ],
21069 );
21070
21071 editor.update_in(cx, |editor, window, cx| {
21072 editor.move_to_beginning(&MoveToBeginning, window, cx);
21073 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
21074 editor.move_to_end(&MoveToEnd, window, cx);
21075 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
21076 editor.move_up(&MoveUp, window, cx);
21077 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21078 });
21079
21080 let breakpoints = editor.update(cx, |editor, cx| {
21081 editor
21082 .breakpoint_store()
21083 .as_ref()
21084 .unwrap()
21085 .read(cx)
21086 .all_source_breakpoints(cx)
21087 .clone()
21088 });
21089
21090 assert_eq!(1, breakpoints.len());
21091 assert_breakpoint(
21092 &breakpoints,
21093 &abs_path,
21094 vec![
21095 (0, Breakpoint::new_standard()),
21096 (2, disable_breakpoint),
21097 (3, Breakpoint::new_standard()),
21098 ],
21099 );
21100}
21101
21102#[gpui::test]
21103async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
21104 init_test(cx, |_| {});
21105 let capabilities = lsp::ServerCapabilities {
21106 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
21107 prepare_provider: Some(true),
21108 work_done_progress_options: Default::default(),
21109 })),
21110 ..Default::default()
21111 };
21112 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21113
21114 cx.set_state(indoc! {"
21115 struct Fˇoo {}
21116 "});
21117
21118 cx.update_editor(|editor, _, cx| {
21119 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21120 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21121 editor.highlight_background::<DocumentHighlightRead>(
21122 &[highlight_range],
21123 |theme| theme.colors().editor_document_highlight_read_background,
21124 cx,
21125 );
21126 });
21127
21128 let mut prepare_rename_handler = cx
21129 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
21130 move |_, _, _| async move {
21131 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
21132 start: lsp::Position {
21133 line: 0,
21134 character: 7,
21135 },
21136 end: lsp::Position {
21137 line: 0,
21138 character: 10,
21139 },
21140 })))
21141 },
21142 );
21143 let prepare_rename_task = cx
21144 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21145 .expect("Prepare rename was not started");
21146 prepare_rename_handler.next().await.unwrap();
21147 prepare_rename_task.await.expect("Prepare rename failed");
21148
21149 let mut rename_handler =
21150 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21151 let edit = lsp::TextEdit {
21152 range: lsp::Range {
21153 start: lsp::Position {
21154 line: 0,
21155 character: 7,
21156 },
21157 end: lsp::Position {
21158 line: 0,
21159 character: 10,
21160 },
21161 },
21162 new_text: "FooRenamed".to_string(),
21163 };
21164 Ok(Some(lsp::WorkspaceEdit::new(
21165 // Specify the same edit twice
21166 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
21167 )))
21168 });
21169 let rename_task = cx
21170 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21171 .expect("Confirm rename was not started");
21172 rename_handler.next().await.unwrap();
21173 rename_task.await.expect("Confirm rename failed");
21174 cx.run_until_parked();
21175
21176 // Despite two edits, only one is actually applied as those are identical
21177 cx.assert_editor_state(indoc! {"
21178 struct FooRenamedˇ {}
21179 "});
21180}
21181
21182#[gpui::test]
21183async fn test_rename_without_prepare(cx: &mut TestAppContext) {
21184 init_test(cx, |_| {});
21185 // These capabilities indicate that the server does not support prepare rename.
21186 let capabilities = lsp::ServerCapabilities {
21187 rename_provider: Some(lsp::OneOf::Left(true)),
21188 ..Default::default()
21189 };
21190 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21191
21192 cx.set_state(indoc! {"
21193 struct Fˇoo {}
21194 "});
21195
21196 cx.update_editor(|editor, _window, cx| {
21197 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21198 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21199 editor.highlight_background::<DocumentHighlightRead>(
21200 &[highlight_range],
21201 |theme| theme.colors().editor_document_highlight_read_background,
21202 cx,
21203 );
21204 });
21205
21206 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21207 .expect("Prepare rename was not started")
21208 .await
21209 .expect("Prepare rename failed");
21210
21211 let mut rename_handler =
21212 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21213 let edit = lsp::TextEdit {
21214 range: lsp::Range {
21215 start: lsp::Position {
21216 line: 0,
21217 character: 7,
21218 },
21219 end: lsp::Position {
21220 line: 0,
21221 character: 10,
21222 },
21223 },
21224 new_text: "FooRenamed".to_string(),
21225 };
21226 Ok(Some(lsp::WorkspaceEdit::new(
21227 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
21228 )))
21229 });
21230 let rename_task = cx
21231 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21232 .expect("Confirm rename was not started");
21233 rename_handler.next().await.unwrap();
21234 rename_task.await.expect("Confirm rename failed");
21235 cx.run_until_parked();
21236
21237 // Correct range is renamed, as `surrounding_word` is used to find it.
21238 cx.assert_editor_state(indoc! {"
21239 struct FooRenamedˇ {}
21240 "});
21241}
21242
21243#[gpui::test]
21244async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
21245 init_test(cx, |_| {});
21246 let mut cx = EditorTestContext::new(cx).await;
21247
21248 let language = Arc::new(
21249 Language::new(
21250 LanguageConfig::default(),
21251 Some(tree_sitter_html::LANGUAGE.into()),
21252 )
21253 .with_brackets_query(
21254 r#"
21255 ("<" @open "/>" @close)
21256 ("</" @open ">" @close)
21257 ("<" @open ">" @close)
21258 ("\"" @open "\"" @close)
21259 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
21260 "#,
21261 )
21262 .unwrap(),
21263 );
21264 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21265
21266 cx.set_state(indoc! {"
21267 <span>ˇ</span>
21268 "});
21269 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21270 cx.assert_editor_state(indoc! {"
21271 <span>
21272 ˇ
21273 </span>
21274 "});
21275
21276 cx.set_state(indoc! {"
21277 <span><span></span>ˇ</span>
21278 "});
21279 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21280 cx.assert_editor_state(indoc! {"
21281 <span><span></span>
21282 ˇ</span>
21283 "});
21284
21285 cx.set_state(indoc! {"
21286 <span>ˇ
21287 </span>
21288 "});
21289 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21290 cx.assert_editor_state(indoc! {"
21291 <span>
21292 ˇ
21293 </span>
21294 "});
21295}
21296
21297#[gpui::test(iterations = 10)]
21298async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
21299 init_test(cx, |_| {});
21300
21301 let fs = FakeFs::new(cx.executor());
21302 fs.insert_tree(
21303 path!("/dir"),
21304 json!({
21305 "a.ts": "a",
21306 }),
21307 )
21308 .await;
21309
21310 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
21311 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21312 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21313
21314 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21315 language_registry.add(Arc::new(Language::new(
21316 LanguageConfig {
21317 name: "TypeScript".into(),
21318 matcher: LanguageMatcher {
21319 path_suffixes: vec!["ts".to_string()],
21320 ..Default::default()
21321 },
21322 ..Default::default()
21323 },
21324 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
21325 )));
21326 let mut fake_language_servers = language_registry.register_fake_lsp(
21327 "TypeScript",
21328 FakeLspAdapter {
21329 capabilities: lsp::ServerCapabilities {
21330 code_lens_provider: Some(lsp::CodeLensOptions {
21331 resolve_provider: Some(true),
21332 }),
21333 execute_command_provider: Some(lsp::ExecuteCommandOptions {
21334 commands: vec!["_the/command".to_string()],
21335 ..lsp::ExecuteCommandOptions::default()
21336 }),
21337 ..lsp::ServerCapabilities::default()
21338 },
21339 ..FakeLspAdapter::default()
21340 },
21341 );
21342
21343 let editor = workspace
21344 .update(cx, |workspace, window, cx| {
21345 workspace.open_abs_path(
21346 PathBuf::from(path!("/dir/a.ts")),
21347 OpenOptions::default(),
21348 window,
21349 cx,
21350 )
21351 })
21352 .unwrap()
21353 .await
21354 .unwrap()
21355 .downcast::<Editor>()
21356 .unwrap();
21357 cx.executor().run_until_parked();
21358
21359 let fake_server = fake_language_servers.next().await.unwrap();
21360
21361 let buffer = editor.update(cx, |editor, cx| {
21362 editor
21363 .buffer()
21364 .read(cx)
21365 .as_singleton()
21366 .expect("have opened a single file by path")
21367 });
21368
21369 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
21370 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
21371 drop(buffer_snapshot);
21372 let actions = cx
21373 .update_window(*workspace, |_, window, cx| {
21374 project.code_actions(&buffer, anchor..anchor, window, cx)
21375 })
21376 .unwrap();
21377
21378 fake_server
21379 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21380 Ok(Some(vec![
21381 lsp::CodeLens {
21382 range: lsp::Range::default(),
21383 command: Some(lsp::Command {
21384 title: "Code lens command".to_owned(),
21385 command: "_the/command".to_owned(),
21386 arguments: None,
21387 }),
21388 data: None,
21389 },
21390 lsp::CodeLens {
21391 range: lsp::Range::default(),
21392 command: Some(lsp::Command {
21393 title: "Command not in capabilities".to_owned(),
21394 command: "not in capabilities".to_owned(),
21395 arguments: None,
21396 }),
21397 data: None,
21398 },
21399 lsp::CodeLens {
21400 range: lsp::Range {
21401 start: lsp::Position {
21402 line: 1,
21403 character: 1,
21404 },
21405 end: lsp::Position {
21406 line: 1,
21407 character: 1,
21408 },
21409 },
21410 command: Some(lsp::Command {
21411 title: "Command not in range".to_owned(),
21412 command: "_the/command".to_owned(),
21413 arguments: None,
21414 }),
21415 data: None,
21416 },
21417 ]))
21418 })
21419 .next()
21420 .await;
21421
21422 let actions = actions.await.unwrap();
21423 assert_eq!(
21424 actions.len(),
21425 1,
21426 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
21427 );
21428 let action = actions[0].clone();
21429 let apply = project.update(cx, |project, cx| {
21430 project.apply_code_action(buffer.clone(), action, true, cx)
21431 });
21432
21433 // Resolving the code action does not populate its edits. In absence of
21434 // edits, we must execute the given command.
21435 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
21436 |mut lens, _| async move {
21437 let lens_command = lens.command.as_mut().expect("should have a command");
21438 assert_eq!(lens_command.title, "Code lens command");
21439 lens_command.arguments = Some(vec![json!("the-argument")]);
21440 Ok(lens)
21441 },
21442 );
21443
21444 // While executing the command, the language server sends the editor
21445 // a `workspaceEdit` request.
21446 fake_server
21447 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
21448 let fake = fake_server.clone();
21449 move |params, _| {
21450 assert_eq!(params.command, "_the/command");
21451 let fake = fake.clone();
21452 async move {
21453 fake.server
21454 .request::<lsp::request::ApplyWorkspaceEdit>(
21455 lsp::ApplyWorkspaceEditParams {
21456 label: None,
21457 edit: lsp::WorkspaceEdit {
21458 changes: Some(
21459 [(
21460 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
21461 vec![lsp::TextEdit {
21462 range: lsp::Range::new(
21463 lsp::Position::new(0, 0),
21464 lsp::Position::new(0, 0),
21465 ),
21466 new_text: "X".into(),
21467 }],
21468 )]
21469 .into_iter()
21470 .collect(),
21471 ),
21472 ..lsp::WorkspaceEdit::default()
21473 },
21474 },
21475 )
21476 .await
21477 .into_response()
21478 .unwrap();
21479 Ok(Some(json!(null)))
21480 }
21481 }
21482 })
21483 .next()
21484 .await;
21485
21486 // Applying the code lens command returns a project transaction containing the edits
21487 // sent by the language server in its `workspaceEdit` request.
21488 let transaction = apply.await.unwrap();
21489 assert!(transaction.0.contains_key(&buffer));
21490 buffer.update(cx, |buffer, cx| {
21491 assert_eq!(buffer.text(), "Xa");
21492 buffer.undo(cx);
21493 assert_eq!(buffer.text(), "a");
21494 });
21495
21496 let actions_after_edits = cx
21497 .update_window(*workspace, |_, window, cx| {
21498 project.code_actions(&buffer, anchor..anchor, window, cx)
21499 })
21500 .unwrap()
21501 .await
21502 .unwrap();
21503 assert_eq!(
21504 actions, actions_after_edits,
21505 "For the same selection, same code lens actions should be returned"
21506 );
21507
21508 let _responses =
21509 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21510 panic!("No more code lens requests are expected");
21511 });
21512 editor.update_in(cx, |editor, window, cx| {
21513 editor.select_all(&SelectAll, window, cx);
21514 });
21515 cx.executor().run_until_parked();
21516 let new_actions = cx
21517 .update_window(*workspace, |_, window, cx| {
21518 project.code_actions(&buffer, anchor..anchor, window, cx)
21519 })
21520 .unwrap()
21521 .await
21522 .unwrap();
21523 assert_eq!(
21524 actions, new_actions,
21525 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
21526 );
21527}
21528
21529#[gpui::test]
21530async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
21531 init_test(cx, |_| {});
21532
21533 let fs = FakeFs::new(cx.executor());
21534 let main_text = r#"fn main() {
21535println!("1");
21536println!("2");
21537println!("3");
21538println!("4");
21539println!("5");
21540}"#;
21541 let lib_text = "mod foo {}";
21542 fs.insert_tree(
21543 path!("/a"),
21544 json!({
21545 "lib.rs": lib_text,
21546 "main.rs": main_text,
21547 }),
21548 )
21549 .await;
21550
21551 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21552 let (workspace, cx) =
21553 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21554 let worktree_id = workspace.update(cx, |workspace, cx| {
21555 workspace.project().update(cx, |project, cx| {
21556 project.worktrees(cx).next().unwrap().read(cx).id()
21557 })
21558 });
21559
21560 let expected_ranges = vec![
21561 Point::new(0, 0)..Point::new(0, 0),
21562 Point::new(1, 0)..Point::new(1, 1),
21563 Point::new(2, 0)..Point::new(2, 2),
21564 Point::new(3, 0)..Point::new(3, 3),
21565 ];
21566
21567 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21568 let editor_1 = workspace
21569 .update_in(cx, |workspace, window, cx| {
21570 workspace.open_path(
21571 (worktree_id, "main.rs"),
21572 Some(pane_1.downgrade()),
21573 true,
21574 window,
21575 cx,
21576 )
21577 })
21578 .unwrap()
21579 .await
21580 .downcast::<Editor>()
21581 .unwrap();
21582 pane_1.update(cx, |pane, cx| {
21583 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21584 open_editor.update(cx, |editor, cx| {
21585 assert_eq!(
21586 editor.display_text(cx),
21587 main_text,
21588 "Original main.rs text on initial open",
21589 );
21590 assert_eq!(
21591 editor
21592 .selections
21593 .all::<Point>(cx)
21594 .into_iter()
21595 .map(|s| s.range())
21596 .collect::<Vec<_>>(),
21597 vec![Point::zero()..Point::zero()],
21598 "Default selections on initial open",
21599 );
21600 })
21601 });
21602 editor_1.update_in(cx, |editor, window, cx| {
21603 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21604 s.select_ranges(expected_ranges.clone());
21605 });
21606 });
21607
21608 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
21609 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
21610 });
21611 let editor_2 = workspace
21612 .update_in(cx, |workspace, window, cx| {
21613 workspace.open_path(
21614 (worktree_id, "main.rs"),
21615 Some(pane_2.downgrade()),
21616 true,
21617 window,
21618 cx,
21619 )
21620 })
21621 .unwrap()
21622 .await
21623 .downcast::<Editor>()
21624 .unwrap();
21625 pane_2.update(cx, |pane, cx| {
21626 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21627 open_editor.update(cx, |editor, cx| {
21628 assert_eq!(
21629 editor.display_text(cx),
21630 main_text,
21631 "Original main.rs text on initial open in another panel",
21632 );
21633 assert_eq!(
21634 editor
21635 .selections
21636 .all::<Point>(cx)
21637 .into_iter()
21638 .map(|s| s.range())
21639 .collect::<Vec<_>>(),
21640 vec![Point::zero()..Point::zero()],
21641 "Default selections on initial open in another panel",
21642 );
21643 })
21644 });
21645
21646 editor_2.update_in(cx, |editor, window, cx| {
21647 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
21648 });
21649
21650 let _other_editor_1 = workspace
21651 .update_in(cx, |workspace, window, cx| {
21652 workspace.open_path(
21653 (worktree_id, "lib.rs"),
21654 Some(pane_1.downgrade()),
21655 true,
21656 window,
21657 cx,
21658 )
21659 })
21660 .unwrap()
21661 .await
21662 .downcast::<Editor>()
21663 .unwrap();
21664 pane_1
21665 .update_in(cx, |pane, window, cx| {
21666 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
21667 })
21668 .await
21669 .unwrap();
21670 drop(editor_1);
21671 pane_1.update(cx, |pane, cx| {
21672 pane.active_item()
21673 .unwrap()
21674 .downcast::<Editor>()
21675 .unwrap()
21676 .update(cx, |editor, cx| {
21677 assert_eq!(
21678 editor.display_text(cx),
21679 lib_text,
21680 "Other file should be open and active",
21681 );
21682 });
21683 assert_eq!(pane.items().count(), 1, "No other editors should be open");
21684 });
21685
21686 let _other_editor_2 = workspace
21687 .update_in(cx, |workspace, window, cx| {
21688 workspace.open_path(
21689 (worktree_id, "lib.rs"),
21690 Some(pane_2.downgrade()),
21691 true,
21692 window,
21693 cx,
21694 )
21695 })
21696 .unwrap()
21697 .await
21698 .downcast::<Editor>()
21699 .unwrap();
21700 pane_2
21701 .update_in(cx, |pane, window, cx| {
21702 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
21703 })
21704 .await
21705 .unwrap();
21706 drop(editor_2);
21707 pane_2.update(cx, |pane, cx| {
21708 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21709 open_editor.update(cx, |editor, cx| {
21710 assert_eq!(
21711 editor.display_text(cx),
21712 lib_text,
21713 "Other file should be open and active in another panel too",
21714 );
21715 });
21716 assert_eq!(
21717 pane.items().count(),
21718 1,
21719 "No other editors should be open in another pane",
21720 );
21721 });
21722
21723 let _editor_1_reopened = workspace
21724 .update_in(cx, |workspace, window, cx| {
21725 workspace.open_path(
21726 (worktree_id, "main.rs"),
21727 Some(pane_1.downgrade()),
21728 true,
21729 window,
21730 cx,
21731 )
21732 })
21733 .unwrap()
21734 .await
21735 .downcast::<Editor>()
21736 .unwrap();
21737 let _editor_2_reopened = workspace
21738 .update_in(cx, |workspace, window, cx| {
21739 workspace.open_path(
21740 (worktree_id, "main.rs"),
21741 Some(pane_2.downgrade()),
21742 true,
21743 window,
21744 cx,
21745 )
21746 })
21747 .unwrap()
21748 .await
21749 .downcast::<Editor>()
21750 .unwrap();
21751 pane_1.update(cx, |pane, cx| {
21752 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21753 open_editor.update(cx, |editor, cx| {
21754 assert_eq!(
21755 editor.display_text(cx),
21756 main_text,
21757 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
21758 );
21759 assert_eq!(
21760 editor
21761 .selections
21762 .all::<Point>(cx)
21763 .into_iter()
21764 .map(|s| s.range())
21765 .collect::<Vec<_>>(),
21766 expected_ranges,
21767 "Previous editor in the 1st panel had selections and should get them restored on reopen",
21768 );
21769 })
21770 });
21771 pane_2.update(cx, |pane, cx| {
21772 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21773 open_editor.update(cx, |editor, cx| {
21774 assert_eq!(
21775 editor.display_text(cx),
21776 r#"fn main() {
21777⋯rintln!("1");
21778⋯intln!("2");
21779⋯ntln!("3");
21780println!("4");
21781println!("5");
21782}"#,
21783 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
21784 );
21785 assert_eq!(
21786 editor
21787 .selections
21788 .all::<Point>(cx)
21789 .into_iter()
21790 .map(|s| s.range())
21791 .collect::<Vec<_>>(),
21792 vec![Point::zero()..Point::zero()],
21793 "Previous editor in the 2nd pane had no selections changed hence should restore none",
21794 );
21795 })
21796 });
21797}
21798
21799#[gpui::test]
21800async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
21801 init_test(cx, |_| {});
21802
21803 let fs = FakeFs::new(cx.executor());
21804 let main_text = r#"fn main() {
21805println!("1");
21806println!("2");
21807println!("3");
21808println!("4");
21809println!("5");
21810}"#;
21811 let lib_text = "mod foo {}";
21812 fs.insert_tree(
21813 path!("/a"),
21814 json!({
21815 "lib.rs": lib_text,
21816 "main.rs": main_text,
21817 }),
21818 )
21819 .await;
21820
21821 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21822 let (workspace, cx) =
21823 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21824 let worktree_id = workspace.update(cx, |workspace, cx| {
21825 workspace.project().update(cx, |project, cx| {
21826 project.worktrees(cx).next().unwrap().read(cx).id()
21827 })
21828 });
21829
21830 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21831 let editor = workspace
21832 .update_in(cx, |workspace, window, cx| {
21833 workspace.open_path(
21834 (worktree_id, "main.rs"),
21835 Some(pane.downgrade()),
21836 true,
21837 window,
21838 cx,
21839 )
21840 })
21841 .unwrap()
21842 .await
21843 .downcast::<Editor>()
21844 .unwrap();
21845 pane.update(cx, |pane, cx| {
21846 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21847 open_editor.update(cx, |editor, cx| {
21848 assert_eq!(
21849 editor.display_text(cx),
21850 main_text,
21851 "Original main.rs text on initial open",
21852 );
21853 })
21854 });
21855 editor.update_in(cx, |editor, window, cx| {
21856 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
21857 });
21858
21859 cx.update_global(|store: &mut SettingsStore, cx| {
21860 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21861 s.restore_on_file_reopen = Some(false);
21862 });
21863 });
21864 editor.update_in(cx, |editor, window, cx| {
21865 editor.fold_ranges(
21866 vec![
21867 Point::new(1, 0)..Point::new(1, 1),
21868 Point::new(2, 0)..Point::new(2, 2),
21869 Point::new(3, 0)..Point::new(3, 3),
21870 ],
21871 false,
21872 window,
21873 cx,
21874 );
21875 });
21876 pane.update_in(cx, |pane, window, cx| {
21877 pane.close_all_items(&CloseAllItems::default(), window, cx)
21878 })
21879 .await
21880 .unwrap();
21881 pane.update(cx, |pane, _| {
21882 assert!(pane.active_item().is_none());
21883 });
21884 cx.update_global(|store: &mut SettingsStore, cx| {
21885 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21886 s.restore_on_file_reopen = Some(true);
21887 });
21888 });
21889
21890 let _editor_reopened = workspace
21891 .update_in(cx, |workspace, window, cx| {
21892 workspace.open_path(
21893 (worktree_id, "main.rs"),
21894 Some(pane.downgrade()),
21895 true,
21896 window,
21897 cx,
21898 )
21899 })
21900 .unwrap()
21901 .await
21902 .downcast::<Editor>()
21903 .unwrap();
21904 pane.update(cx, |pane, cx| {
21905 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21906 open_editor.update(cx, |editor, cx| {
21907 assert_eq!(
21908 editor.display_text(cx),
21909 main_text,
21910 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
21911 );
21912 })
21913 });
21914}
21915
21916#[gpui::test]
21917async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
21918 struct EmptyModalView {
21919 focus_handle: gpui::FocusHandle,
21920 }
21921 impl EventEmitter<DismissEvent> for EmptyModalView {}
21922 impl Render for EmptyModalView {
21923 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
21924 div()
21925 }
21926 }
21927 impl Focusable for EmptyModalView {
21928 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
21929 self.focus_handle.clone()
21930 }
21931 }
21932 impl workspace::ModalView for EmptyModalView {}
21933 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
21934 EmptyModalView {
21935 focus_handle: cx.focus_handle(),
21936 }
21937 }
21938
21939 init_test(cx, |_| {});
21940
21941 let fs = FakeFs::new(cx.executor());
21942 let project = Project::test(fs, [], cx).await;
21943 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21944 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
21945 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21946 let editor = cx.new_window_entity(|window, cx| {
21947 Editor::new(
21948 EditorMode::full(),
21949 buffer,
21950 Some(project.clone()),
21951 window,
21952 cx,
21953 )
21954 });
21955 workspace
21956 .update(cx, |workspace, window, cx| {
21957 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
21958 })
21959 .unwrap();
21960 editor.update_in(cx, |editor, window, cx| {
21961 editor.open_context_menu(&OpenContextMenu, window, cx);
21962 assert!(editor.mouse_context_menu.is_some());
21963 });
21964 workspace
21965 .update(cx, |workspace, window, cx| {
21966 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
21967 })
21968 .unwrap();
21969 cx.read(|cx| {
21970 assert!(editor.read(cx).mouse_context_menu.is_none());
21971 });
21972}
21973
21974#[gpui::test]
21975async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
21976 init_test(cx, |_| {});
21977
21978 let fs = FakeFs::new(cx.executor());
21979 fs.insert_file(path!("/file.html"), Default::default())
21980 .await;
21981
21982 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
21983
21984 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21985 let html_language = Arc::new(Language::new(
21986 LanguageConfig {
21987 name: "HTML".into(),
21988 matcher: LanguageMatcher {
21989 path_suffixes: vec!["html".to_string()],
21990 ..LanguageMatcher::default()
21991 },
21992 brackets: BracketPairConfig {
21993 pairs: vec![BracketPair {
21994 start: "<".into(),
21995 end: ">".into(),
21996 close: true,
21997 ..Default::default()
21998 }],
21999 ..Default::default()
22000 },
22001 ..Default::default()
22002 },
22003 Some(tree_sitter_html::LANGUAGE.into()),
22004 ));
22005 language_registry.add(html_language);
22006 let mut fake_servers = language_registry.register_fake_lsp(
22007 "HTML",
22008 FakeLspAdapter {
22009 capabilities: lsp::ServerCapabilities {
22010 completion_provider: Some(lsp::CompletionOptions {
22011 resolve_provider: Some(true),
22012 ..Default::default()
22013 }),
22014 ..Default::default()
22015 },
22016 ..Default::default()
22017 },
22018 );
22019
22020 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22021 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22022
22023 let worktree_id = workspace
22024 .update(cx, |workspace, _window, cx| {
22025 workspace.project().update(cx, |project, cx| {
22026 project.worktrees(cx).next().unwrap().read(cx).id()
22027 })
22028 })
22029 .unwrap();
22030 project
22031 .update(cx, |project, cx| {
22032 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
22033 })
22034 .await
22035 .unwrap();
22036 let editor = workspace
22037 .update(cx, |workspace, window, cx| {
22038 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
22039 })
22040 .unwrap()
22041 .await
22042 .unwrap()
22043 .downcast::<Editor>()
22044 .unwrap();
22045
22046 let fake_server = fake_servers.next().await.unwrap();
22047 editor.update_in(cx, |editor, window, cx| {
22048 editor.set_text("<ad></ad>", window, cx);
22049 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22050 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
22051 });
22052 let Some((buffer, _)) = editor
22053 .buffer
22054 .read(cx)
22055 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
22056 else {
22057 panic!("Failed to get buffer for selection position");
22058 };
22059 let buffer = buffer.read(cx);
22060 let buffer_id = buffer.remote_id();
22061 let opening_range =
22062 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
22063 let closing_range =
22064 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
22065 let mut linked_ranges = HashMap::default();
22066 linked_ranges.insert(
22067 buffer_id,
22068 vec![(opening_range.clone(), vec![closing_range.clone()])],
22069 );
22070 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
22071 });
22072 let mut completion_handle =
22073 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
22074 Ok(Some(lsp::CompletionResponse::Array(vec![
22075 lsp::CompletionItem {
22076 label: "head".to_string(),
22077 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22078 lsp::InsertReplaceEdit {
22079 new_text: "head".to_string(),
22080 insert: lsp::Range::new(
22081 lsp::Position::new(0, 1),
22082 lsp::Position::new(0, 3),
22083 ),
22084 replace: lsp::Range::new(
22085 lsp::Position::new(0, 1),
22086 lsp::Position::new(0, 3),
22087 ),
22088 },
22089 )),
22090 ..Default::default()
22091 },
22092 ])))
22093 });
22094 editor.update_in(cx, |editor, window, cx| {
22095 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
22096 });
22097 cx.run_until_parked();
22098 completion_handle.next().await.unwrap();
22099 editor.update(cx, |editor, _| {
22100 assert!(
22101 editor.context_menu_visible(),
22102 "Completion menu should be visible"
22103 );
22104 });
22105 editor.update_in(cx, |editor, window, cx| {
22106 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
22107 });
22108 cx.executor().run_until_parked();
22109 editor.update(cx, |editor, cx| {
22110 assert_eq!(editor.text(cx), "<head></head>");
22111 });
22112}
22113
22114#[gpui::test]
22115async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
22116 init_test(cx, |_| {});
22117
22118 let fs = FakeFs::new(cx.executor());
22119 fs.insert_tree(
22120 path!("/root"),
22121 json!({
22122 "a": {
22123 "main.rs": "fn main() {}",
22124 },
22125 "foo": {
22126 "bar": {
22127 "external_file.rs": "pub mod external {}",
22128 }
22129 }
22130 }),
22131 )
22132 .await;
22133
22134 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
22135 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22136 language_registry.add(rust_lang());
22137 let _fake_servers = language_registry.register_fake_lsp(
22138 "Rust",
22139 FakeLspAdapter {
22140 ..FakeLspAdapter::default()
22141 },
22142 );
22143 let (workspace, cx) =
22144 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22145 let worktree_id = workspace.update(cx, |workspace, cx| {
22146 workspace.project().update(cx, |project, cx| {
22147 project.worktrees(cx).next().unwrap().read(cx).id()
22148 })
22149 });
22150
22151 let assert_language_servers_count =
22152 |expected: usize, context: &str, cx: &mut VisualTestContext| {
22153 project.update(cx, |project, cx| {
22154 let current = project
22155 .lsp_store()
22156 .read(cx)
22157 .as_local()
22158 .unwrap()
22159 .language_servers
22160 .len();
22161 assert_eq!(expected, current, "{context}");
22162 });
22163 };
22164
22165 assert_language_servers_count(
22166 0,
22167 "No servers should be running before any file is open",
22168 cx,
22169 );
22170 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22171 let main_editor = workspace
22172 .update_in(cx, |workspace, window, cx| {
22173 workspace.open_path(
22174 (worktree_id, "main.rs"),
22175 Some(pane.downgrade()),
22176 true,
22177 window,
22178 cx,
22179 )
22180 })
22181 .unwrap()
22182 .await
22183 .downcast::<Editor>()
22184 .unwrap();
22185 pane.update(cx, |pane, cx| {
22186 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22187 open_editor.update(cx, |editor, cx| {
22188 assert_eq!(
22189 editor.display_text(cx),
22190 "fn main() {}",
22191 "Original main.rs text on initial open",
22192 );
22193 });
22194 assert_eq!(open_editor, main_editor);
22195 });
22196 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
22197
22198 let external_editor = workspace
22199 .update_in(cx, |workspace, window, cx| {
22200 workspace.open_abs_path(
22201 PathBuf::from("/root/foo/bar/external_file.rs"),
22202 OpenOptions::default(),
22203 window,
22204 cx,
22205 )
22206 })
22207 .await
22208 .expect("opening external file")
22209 .downcast::<Editor>()
22210 .expect("downcasted external file's open element to editor");
22211 pane.update(cx, |pane, cx| {
22212 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22213 open_editor.update(cx, |editor, cx| {
22214 assert_eq!(
22215 editor.display_text(cx),
22216 "pub mod external {}",
22217 "External file is open now",
22218 );
22219 });
22220 assert_eq!(open_editor, external_editor);
22221 });
22222 assert_language_servers_count(
22223 1,
22224 "Second, external, *.rs file should join the existing server",
22225 cx,
22226 );
22227
22228 pane.update_in(cx, |pane, window, cx| {
22229 pane.close_active_item(&CloseActiveItem::default(), window, cx)
22230 })
22231 .await
22232 .unwrap();
22233 pane.update_in(cx, |pane, window, cx| {
22234 pane.navigate_backward(window, cx);
22235 });
22236 cx.run_until_parked();
22237 pane.update(cx, |pane, cx| {
22238 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22239 open_editor.update(cx, |editor, cx| {
22240 assert_eq!(
22241 editor.display_text(cx),
22242 "pub mod external {}",
22243 "External file is open now",
22244 );
22245 });
22246 });
22247 assert_language_servers_count(
22248 1,
22249 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
22250 cx,
22251 );
22252
22253 cx.update(|_, cx| {
22254 workspace::reload(&workspace::Reload::default(), cx);
22255 });
22256 assert_language_servers_count(
22257 1,
22258 "After reloading the worktree with local and external files opened, only one project should be started",
22259 cx,
22260 );
22261}
22262
22263#[gpui::test]
22264async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
22265 init_test(cx, |_| {});
22266
22267 let mut cx = EditorTestContext::new(cx).await;
22268 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22269 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22270
22271 // test cursor move to start of each line on tab
22272 // for `if`, `elif`, `else`, `while`, `with` and `for`
22273 cx.set_state(indoc! {"
22274 def main():
22275 ˇ for item in items:
22276 ˇ while item.active:
22277 ˇ if item.value > 10:
22278 ˇ continue
22279 ˇ elif item.value < 0:
22280 ˇ break
22281 ˇ else:
22282 ˇ with item.context() as ctx:
22283 ˇ yield count
22284 ˇ else:
22285 ˇ log('while else')
22286 ˇ else:
22287 ˇ log('for else')
22288 "});
22289 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22290 cx.assert_editor_state(indoc! {"
22291 def main():
22292 ˇfor item in items:
22293 ˇwhile item.active:
22294 ˇif item.value > 10:
22295 ˇcontinue
22296 ˇelif item.value < 0:
22297 ˇbreak
22298 ˇelse:
22299 ˇwith item.context() as ctx:
22300 ˇyield count
22301 ˇelse:
22302 ˇlog('while else')
22303 ˇelse:
22304 ˇlog('for else')
22305 "});
22306 // test relative indent is preserved when tab
22307 // for `if`, `elif`, `else`, `while`, `with` and `for`
22308 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22309 cx.assert_editor_state(indoc! {"
22310 def main():
22311 ˇfor item in items:
22312 ˇwhile item.active:
22313 ˇif item.value > 10:
22314 ˇcontinue
22315 ˇelif item.value < 0:
22316 ˇbreak
22317 ˇelse:
22318 ˇwith item.context() as ctx:
22319 ˇyield count
22320 ˇelse:
22321 ˇlog('while else')
22322 ˇelse:
22323 ˇlog('for else')
22324 "});
22325
22326 // test cursor move to start of each line on tab
22327 // for `try`, `except`, `else`, `finally`, `match` and `def`
22328 cx.set_state(indoc! {"
22329 def main():
22330 ˇ try:
22331 ˇ fetch()
22332 ˇ except ValueError:
22333 ˇ handle_error()
22334 ˇ else:
22335 ˇ match value:
22336 ˇ case _:
22337 ˇ finally:
22338 ˇ def status():
22339 ˇ return 0
22340 "});
22341 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22342 cx.assert_editor_state(indoc! {"
22343 def main():
22344 ˇtry:
22345 ˇfetch()
22346 ˇexcept ValueError:
22347 ˇhandle_error()
22348 ˇelse:
22349 ˇmatch value:
22350 ˇcase _:
22351 ˇfinally:
22352 ˇdef status():
22353 ˇreturn 0
22354 "});
22355 // test relative indent is preserved when tab
22356 // for `try`, `except`, `else`, `finally`, `match` and `def`
22357 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22358 cx.assert_editor_state(indoc! {"
22359 def main():
22360 ˇtry:
22361 ˇfetch()
22362 ˇexcept ValueError:
22363 ˇhandle_error()
22364 ˇelse:
22365 ˇmatch value:
22366 ˇcase _:
22367 ˇfinally:
22368 ˇdef status():
22369 ˇreturn 0
22370 "});
22371}
22372
22373#[gpui::test]
22374async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
22375 init_test(cx, |_| {});
22376
22377 let mut cx = EditorTestContext::new(cx).await;
22378 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22379 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22380
22381 // test `else` auto outdents when typed inside `if` block
22382 cx.set_state(indoc! {"
22383 def main():
22384 if i == 2:
22385 return
22386 ˇ
22387 "});
22388 cx.update_editor(|editor, window, cx| {
22389 editor.handle_input("else:", window, cx);
22390 });
22391 cx.assert_editor_state(indoc! {"
22392 def main():
22393 if i == 2:
22394 return
22395 else:ˇ
22396 "});
22397
22398 // test `except` auto outdents when typed inside `try` block
22399 cx.set_state(indoc! {"
22400 def main():
22401 try:
22402 i = 2
22403 ˇ
22404 "});
22405 cx.update_editor(|editor, window, cx| {
22406 editor.handle_input("except:", window, cx);
22407 });
22408 cx.assert_editor_state(indoc! {"
22409 def main():
22410 try:
22411 i = 2
22412 except:ˇ
22413 "});
22414
22415 // test `else` auto outdents when typed inside `except` block
22416 cx.set_state(indoc! {"
22417 def main():
22418 try:
22419 i = 2
22420 except:
22421 j = 2
22422 ˇ
22423 "});
22424 cx.update_editor(|editor, window, cx| {
22425 editor.handle_input("else:", window, cx);
22426 });
22427 cx.assert_editor_state(indoc! {"
22428 def main():
22429 try:
22430 i = 2
22431 except:
22432 j = 2
22433 else:ˇ
22434 "});
22435
22436 // test `finally` auto outdents when typed inside `else` block
22437 cx.set_state(indoc! {"
22438 def main():
22439 try:
22440 i = 2
22441 except:
22442 j = 2
22443 else:
22444 k = 2
22445 ˇ
22446 "});
22447 cx.update_editor(|editor, window, cx| {
22448 editor.handle_input("finally:", window, cx);
22449 });
22450 cx.assert_editor_state(indoc! {"
22451 def main():
22452 try:
22453 i = 2
22454 except:
22455 j = 2
22456 else:
22457 k = 2
22458 finally:ˇ
22459 "});
22460
22461 // test `else` does not outdents when typed inside `except` block right after for block
22462 cx.set_state(indoc! {"
22463 def main():
22464 try:
22465 i = 2
22466 except:
22467 for i in range(n):
22468 pass
22469 ˇ
22470 "});
22471 cx.update_editor(|editor, window, cx| {
22472 editor.handle_input("else:", window, cx);
22473 });
22474 cx.assert_editor_state(indoc! {"
22475 def main():
22476 try:
22477 i = 2
22478 except:
22479 for i in range(n):
22480 pass
22481 else:ˇ
22482 "});
22483
22484 // test `finally` auto outdents when typed inside `else` block right after for block
22485 cx.set_state(indoc! {"
22486 def main():
22487 try:
22488 i = 2
22489 except:
22490 j = 2
22491 else:
22492 for i in range(n):
22493 pass
22494 ˇ
22495 "});
22496 cx.update_editor(|editor, window, cx| {
22497 editor.handle_input("finally:", window, cx);
22498 });
22499 cx.assert_editor_state(indoc! {"
22500 def main():
22501 try:
22502 i = 2
22503 except:
22504 j = 2
22505 else:
22506 for i in range(n):
22507 pass
22508 finally:ˇ
22509 "});
22510
22511 // test `except` outdents to inner "try" block
22512 cx.set_state(indoc! {"
22513 def main():
22514 try:
22515 i = 2
22516 if i == 2:
22517 try:
22518 i = 3
22519 ˇ
22520 "});
22521 cx.update_editor(|editor, window, cx| {
22522 editor.handle_input("except:", window, cx);
22523 });
22524 cx.assert_editor_state(indoc! {"
22525 def main():
22526 try:
22527 i = 2
22528 if i == 2:
22529 try:
22530 i = 3
22531 except:ˇ
22532 "});
22533
22534 // test `except` outdents to outer "try" block
22535 cx.set_state(indoc! {"
22536 def main():
22537 try:
22538 i = 2
22539 if i == 2:
22540 try:
22541 i = 3
22542 ˇ
22543 "});
22544 cx.update_editor(|editor, window, cx| {
22545 editor.handle_input("except:", window, cx);
22546 });
22547 cx.assert_editor_state(indoc! {"
22548 def main():
22549 try:
22550 i = 2
22551 if i == 2:
22552 try:
22553 i = 3
22554 except:ˇ
22555 "});
22556
22557 // test `else` stays at correct indent when typed after `for` block
22558 cx.set_state(indoc! {"
22559 def main():
22560 for i in range(10):
22561 if i == 3:
22562 break
22563 ˇ
22564 "});
22565 cx.update_editor(|editor, window, cx| {
22566 editor.handle_input("else:", window, cx);
22567 });
22568 cx.assert_editor_state(indoc! {"
22569 def main():
22570 for i in range(10):
22571 if i == 3:
22572 break
22573 else:ˇ
22574 "});
22575
22576 // test does not outdent on typing after line with square brackets
22577 cx.set_state(indoc! {"
22578 def f() -> list[str]:
22579 ˇ
22580 "});
22581 cx.update_editor(|editor, window, cx| {
22582 editor.handle_input("a", window, cx);
22583 });
22584 cx.assert_editor_state(indoc! {"
22585 def f() -> list[str]:
22586 aˇ
22587 "});
22588
22589 // test does not outdent on typing : after case keyword
22590 cx.set_state(indoc! {"
22591 match 1:
22592 caseˇ
22593 "});
22594 cx.update_editor(|editor, window, cx| {
22595 editor.handle_input(":", window, cx);
22596 });
22597 cx.assert_editor_state(indoc! {"
22598 match 1:
22599 case:ˇ
22600 "});
22601}
22602
22603#[gpui::test]
22604async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
22605 init_test(cx, |_| {});
22606 update_test_language_settings(cx, |settings| {
22607 settings.defaults.extend_comment_on_newline = Some(false);
22608 });
22609 let mut cx = EditorTestContext::new(cx).await;
22610 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22611 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22612
22613 // test correct indent after newline on comment
22614 cx.set_state(indoc! {"
22615 # COMMENT:ˇ
22616 "});
22617 cx.update_editor(|editor, window, cx| {
22618 editor.newline(&Newline, window, cx);
22619 });
22620 cx.assert_editor_state(indoc! {"
22621 # COMMENT:
22622 ˇ
22623 "});
22624
22625 // test correct indent after newline in brackets
22626 cx.set_state(indoc! {"
22627 {ˇ}
22628 "});
22629 cx.update_editor(|editor, window, cx| {
22630 editor.newline(&Newline, window, cx);
22631 });
22632 cx.run_until_parked();
22633 cx.assert_editor_state(indoc! {"
22634 {
22635 ˇ
22636 }
22637 "});
22638
22639 cx.set_state(indoc! {"
22640 (ˇ)
22641 "});
22642 cx.update_editor(|editor, window, cx| {
22643 editor.newline(&Newline, window, cx);
22644 });
22645 cx.run_until_parked();
22646 cx.assert_editor_state(indoc! {"
22647 (
22648 ˇ
22649 )
22650 "});
22651
22652 // do not indent after empty lists or dictionaries
22653 cx.set_state(indoc! {"
22654 a = []ˇ
22655 "});
22656 cx.update_editor(|editor, window, cx| {
22657 editor.newline(&Newline, window, cx);
22658 });
22659 cx.run_until_parked();
22660 cx.assert_editor_state(indoc! {"
22661 a = []
22662 ˇ
22663 "});
22664}
22665
22666#[gpui::test]
22667async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
22668 init_test(cx, |_| {});
22669
22670 let mut cx = EditorTestContext::new(cx).await;
22671 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
22672 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22673
22674 // test cursor move to start of each line on tab
22675 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
22676 cx.set_state(indoc! {"
22677 function main() {
22678 ˇ for item in $items; do
22679 ˇ while [ -n \"$item\" ]; do
22680 ˇ if [ \"$value\" -gt 10 ]; then
22681 ˇ continue
22682 ˇ elif [ \"$value\" -lt 0 ]; then
22683 ˇ break
22684 ˇ else
22685 ˇ echo \"$item\"
22686 ˇ fi
22687 ˇ done
22688 ˇ done
22689 ˇ}
22690 "});
22691 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22692 cx.assert_editor_state(indoc! {"
22693 function main() {
22694 ˇfor item in $items; do
22695 ˇwhile [ -n \"$item\" ]; do
22696 ˇif [ \"$value\" -gt 10 ]; then
22697 ˇcontinue
22698 ˇelif [ \"$value\" -lt 0 ]; then
22699 ˇbreak
22700 ˇelse
22701 ˇecho \"$item\"
22702 ˇfi
22703 ˇdone
22704 ˇdone
22705 ˇ}
22706 "});
22707 // test relative indent is preserved when tab
22708 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22709 cx.assert_editor_state(indoc! {"
22710 function main() {
22711 ˇfor item in $items; do
22712 ˇwhile [ -n \"$item\" ]; do
22713 ˇif [ \"$value\" -gt 10 ]; then
22714 ˇcontinue
22715 ˇelif [ \"$value\" -lt 0 ]; then
22716 ˇbreak
22717 ˇelse
22718 ˇecho \"$item\"
22719 ˇfi
22720 ˇdone
22721 ˇdone
22722 ˇ}
22723 "});
22724
22725 // test cursor move to start of each line on tab
22726 // for `case` statement with patterns
22727 cx.set_state(indoc! {"
22728 function handle() {
22729 ˇ case \"$1\" in
22730 ˇ start)
22731 ˇ echo \"a\"
22732 ˇ ;;
22733 ˇ stop)
22734 ˇ echo \"b\"
22735 ˇ ;;
22736 ˇ *)
22737 ˇ echo \"c\"
22738 ˇ ;;
22739 ˇ esac
22740 ˇ}
22741 "});
22742 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22743 cx.assert_editor_state(indoc! {"
22744 function handle() {
22745 ˇcase \"$1\" in
22746 ˇstart)
22747 ˇecho \"a\"
22748 ˇ;;
22749 ˇstop)
22750 ˇecho \"b\"
22751 ˇ;;
22752 ˇ*)
22753 ˇecho \"c\"
22754 ˇ;;
22755 ˇesac
22756 ˇ}
22757 "});
22758}
22759
22760#[gpui::test]
22761async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
22762 init_test(cx, |_| {});
22763
22764 let mut cx = EditorTestContext::new(cx).await;
22765 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
22766 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22767
22768 // test indents on comment insert
22769 cx.set_state(indoc! {"
22770 function main() {
22771 ˇ for item in $items; do
22772 ˇ while [ -n \"$item\" ]; do
22773 ˇ if [ \"$value\" -gt 10 ]; then
22774 ˇ continue
22775 ˇ elif [ \"$value\" -lt 0 ]; then
22776 ˇ break
22777 ˇ else
22778 ˇ echo \"$item\"
22779 ˇ fi
22780 ˇ done
22781 ˇ done
22782 ˇ}
22783 "});
22784 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
22785 cx.assert_editor_state(indoc! {"
22786 function main() {
22787 #ˇ for item in $items; do
22788 #ˇ while [ -n \"$item\" ]; do
22789 #ˇ if [ \"$value\" -gt 10 ]; then
22790 #ˇ continue
22791 #ˇ elif [ \"$value\" -lt 0 ]; then
22792 #ˇ break
22793 #ˇ else
22794 #ˇ echo \"$item\"
22795 #ˇ fi
22796 #ˇ done
22797 #ˇ done
22798 #ˇ}
22799 "});
22800}
22801
22802#[gpui::test]
22803async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
22804 init_test(cx, |_| {});
22805
22806 let mut cx = EditorTestContext::new(cx).await;
22807 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
22808 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22809
22810 // test `else` auto outdents when typed inside `if` block
22811 cx.set_state(indoc! {"
22812 if [ \"$1\" = \"test\" ]; then
22813 echo \"foo bar\"
22814 ˇ
22815 "});
22816 cx.update_editor(|editor, window, cx| {
22817 editor.handle_input("else", window, cx);
22818 });
22819 cx.assert_editor_state(indoc! {"
22820 if [ \"$1\" = \"test\" ]; then
22821 echo \"foo bar\"
22822 elseˇ
22823 "});
22824
22825 // test `elif` auto outdents when typed inside `if` block
22826 cx.set_state(indoc! {"
22827 if [ \"$1\" = \"test\" ]; then
22828 echo \"foo bar\"
22829 ˇ
22830 "});
22831 cx.update_editor(|editor, window, cx| {
22832 editor.handle_input("elif", window, cx);
22833 });
22834 cx.assert_editor_state(indoc! {"
22835 if [ \"$1\" = \"test\" ]; then
22836 echo \"foo bar\"
22837 elifˇ
22838 "});
22839
22840 // test `fi` auto outdents when typed inside `else` block
22841 cx.set_state(indoc! {"
22842 if [ \"$1\" = \"test\" ]; then
22843 echo \"foo bar\"
22844 else
22845 echo \"bar baz\"
22846 ˇ
22847 "});
22848 cx.update_editor(|editor, window, cx| {
22849 editor.handle_input("fi", window, cx);
22850 });
22851 cx.assert_editor_state(indoc! {"
22852 if [ \"$1\" = \"test\" ]; then
22853 echo \"foo bar\"
22854 else
22855 echo \"bar baz\"
22856 fiˇ
22857 "});
22858
22859 // test `done` auto outdents when typed inside `while` block
22860 cx.set_state(indoc! {"
22861 while read line; do
22862 echo \"$line\"
22863 ˇ
22864 "});
22865 cx.update_editor(|editor, window, cx| {
22866 editor.handle_input("done", window, cx);
22867 });
22868 cx.assert_editor_state(indoc! {"
22869 while read line; do
22870 echo \"$line\"
22871 doneˇ
22872 "});
22873
22874 // test `done` auto outdents when typed inside `for` block
22875 cx.set_state(indoc! {"
22876 for file in *.txt; do
22877 cat \"$file\"
22878 ˇ
22879 "});
22880 cx.update_editor(|editor, window, cx| {
22881 editor.handle_input("done", window, cx);
22882 });
22883 cx.assert_editor_state(indoc! {"
22884 for file in *.txt; do
22885 cat \"$file\"
22886 doneˇ
22887 "});
22888
22889 // test `esac` auto outdents when typed inside `case` block
22890 cx.set_state(indoc! {"
22891 case \"$1\" in
22892 start)
22893 echo \"foo bar\"
22894 ;;
22895 stop)
22896 echo \"bar baz\"
22897 ;;
22898 ˇ
22899 "});
22900 cx.update_editor(|editor, window, cx| {
22901 editor.handle_input("esac", window, cx);
22902 });
22903 cx.assert_editor_state(indoc! {"
22904 case \"$1\" in
22905 start)
22906 echo \"foo bar\"
22907 ;;
22908 stop)
22909 echo \"bar baz\"
22910 ;;
22911 esacˇ
22912 "});
22913
22914 // test `*)` auto outdents when typed inside `case` block
22915 cx.set_state(indoc! {"
22916 case \"$1\" in
22917 start)
22918 echo \"foo bar\"
22919 ;;
22920 ˇ
22921 "});
22922 cx.update_editor(|editor, window, cx| {
22923 editor.handle_input("*)", window, cx);
22924 });
22925 cx.assert_editor_state(indoc! {"
22926 case \"$1\" in
22927 start)
22928 echo \"foo bar\"
22929 ;;
22930 *)ˇ
22931 "});
22932
22933 // test `fi` outdents to correct level with nested if blocks
22934 cx.set_state(indoc! {"
22935 if [ \"$1\" = \"test\" ]; then
22936 echo \"outer if\"
22937 if [ \"$2\" = \"debug\" ]; then
22938 echo \"inner if\"
22939 ˇ
22940 "});
22941 cx.update_editor(|editor, window, cx| {
22942 editor.handle_input("fi", window, cx);
22943 });
22944 cx.assert_editor_state(indoc! {"
22945 if [ \"$1\" = \"test\" ]; then
22946 echo \"outer if\"
22947 if [ \"$2\" = \"debug\" ]; then
22948 echo \"inner if\"
22949 fiˇ
22950 "});
22951}
22952
22953#[gpui::test]
22954async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
22955 init_test(cx, |_| {});
22956 update_test_language_settings(cx, |settings| {
22957 settings.defaults.extend_comment_on_newline = Some(false);
22958 });
22959 let mut cx = EditorTestContext::new(cx).await;
22960 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
22961 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22962
22963 // test correct indent after newline on comment
22964 cx.set_state(indoc! {"
22965 # COMMENT:ˇ
22966 "});
22967 cx.update_editor(|editor, window, cx| {
22968 editor.newline(&Newline, window, cx);
22969 });
22970 cx.assert_editor_state(indoc! {"
22971 # COMMENT:
22972 ˇ
22973 "});
22974
22975 // test correct indent after newline after `then`
22976 cx.set_state(indoc! {"
22977
22978 if [ \"$1\" = \"test\" ]; thenˇ
22979 "});
22980 cx.update_editor(|editor, window, cx| {
22981 editor.newline(&Newline, window, cx);
22982 });
22983 cx.run_until_parked();
22984 cx.assert_editor_state(indoc! {"
22985
22986 if [ \"$1\" = \"test\" ]; then
22987 ˇ
22988 "});
22989
22990 // test correct indent after newline after `else`
22991 cx.set_state(indoc! {"
22992 if [ \"$1\" = \"test\" ]; then
22993 elseˇ
22994 "});
22995 cx.update_editor(|editor, window, cx| {
22996 editor.newline(&Newline, window, cx);
22997 });
22998 cx.run_until_parked();
22999 cx.assert_editor_state(indoc! {"
23000 if [ \"$1\" = \"test\" ]; then
23001 else
23002 ˇ
23003 "});
23004
23005 // test correct indent after newline after `elif`
23006 cx.set_state(indoc! {"
23007 if [ \"$1\" = \"test\" ]; then
23008 elifˇ
23009 "});
23010 cx.update_editor(|editor, window, cx| {
23011 editor.newline(&Newline, window, cx);
23012 });
23013 cx.run_until_parked();
23014 cx.assert_editor_state(indoc! {"
23015 if [ \"$1\" = \"test\" ]; then
23016 elif
23017 ˇ
23018 "});
23019
23020 // test correct indent after newline after `do`
23021 cx.set_state(indoc! {"
23022 for file in *.txt; doˇ
23023 "});
23024 cx.update_editor(|editor, window, cx| {
23025 editor.newline(&Newline, window, cx);
23026 });
23027 cx.run_until_parked();
23028 cx.assert_editor_state(indoc! {"
23029 for file in *.txt; do
23030 ˇ
23031 "});
23032
23033 // test correct indent after newline after case pattern
23034 cx.set_state(indoc! {"
23035 case \"$1\" in
23036 start)ˇ
23037 "});
23038 cx.update_editor(|editor, window, cx| {
23039 editor.newline(&Newline, window, cx);
23040 });
23041 cx.run_until_parked();
23042 cx.assert_editor_state(indoc! {"
23043 case \"$1\" in
23044 start)
23045 ˇ
23046 "});
23047
23048 // test correct indent after newline after case pattern
23049 cx.set_state(indoc! {"
23050 case \"$1\" in
23051 start)
23052 ;;
23053 *)ˇ
23054 "});
23055 cx.update_editor(|editor, window, cx| {
23056 editor.newline(&Newline, window, cx);
23057 });
23058 cx.run_until_parked();
23059 cx.assert_editor_state(indoc! {"
23060 case \"$1\" in
23061 start)
23062 ;;
23063 *)
23064 ˇ
23065 "});
23066
23067 // test correct indent after newline after function opening brace
23068 cx.set_state(indoc! {"
23069 function test() {ˇ}
23070 "});
23071 cx.update_editor(|editor, window, cx| {
23072 editor.newline(&Newline, window, cx);
23073 });
23074 cx.run_until_parked();
23075 cx.assert_editor_state(indoc! {"
23076 function test() {
23077 ˇ
23078 }
23079 "});
23080
23081 // test no extra indent after semicolon on same line
23082 cx.set_state(indoc! {"
23083 echo \"test\";ˇ
23084 "});
23085 cx.update_editor(|editor, window, cx| {
23086 editor.newline(&Newline, window, cx);
23087 });
23088 cx.run_until_parked();
23089 cx.assert_editor_state(indoc! {"
23090 echo \"test\";
23091 ˇ
23092 "});
23093}
23094
23095fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
23096 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
23097 point..point
23098}
23099
23100#[track_caller]
23101fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
23102 let (text, ranges) = marked_text_ranges(marked_text, true);
23103 assert_eq!(editor.text(cx), text);
23104 assert_eq!(
23105 editor.selections.ranges(cx),
23106 ranges,
23107 "Assert selections are {}",
23108 marked_text
23109 );
23110}
23111
23112pub fn handle_signature_help_request(
23113 cx: &mut EditorLspTestContext,
23114 mocked_response: lsp::SignatureHelp,
23115) -> impl Future<Output = ()> + use<> {
23116 let mut request =
23117 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
23118 let mocked_response = mocked_response.clone();
23119 async move { Ok(Some(mocked_response)) }
23120 });
23121
23122 async move {
23123 request.next().await;
23124 }
23125}
23126
23127#[track_caller]
23128pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
23129 cx.update_editor(|editor, _, _| {
23130 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
23131 let entries = menu.entries.borrow();
23132 let entries = entries
23133 .iter()
23134 .map(|entry| entry.string.as_str())
23135 .collect::<Vec<_>>();
23136 assert_eq!(entries, expected);
23137 } else {
23138 panic!("Expected completions menu");
23139 }
23140 });
23141}
23142
23143/// Handle completion request passing a marked string specifying where the completion
23144/// should be triggered from using '|' character, what range should be replaced, and what completions
23145/// should be returned using '<' and '>' to delimit the range.
23146///
23147/// Also see `handle_completion_request_with_insert_and_replace`.
23148#[track_caller]
23149pub fn handle_completion_request(
23150 marked_string: &str,
23151 completions: Vec<&'static str>,
23152 is_incomplete: bool,
23153 counter: Arc<AtomicUsize>,
23154 cx: &mut EditorLspTestContext,
23155) -> impl Future<Output = ()> {
23156 let complete_from_marker: TextRangeMarker = '|'.into();
23157 let replace_range_marker: TextRangeMarker = ('<', '>').into();
23158 let (_, mut marked_ranges) = marked_text_ranges_by(
23159 marked_string,
23160 vec![complete_from_marker.clone(), replace_range_marker.clone()],
23161 );
23162
23163 let complete_from_position =
23164 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
23165 let replace_range =
23166 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
23167
23168 let mut request =
23169 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
23170 let completions = completions.clone();
23171 counter.fetch_add(1, atomic::Ordering::Release);
23172 async move {
23173 assert_eq!(params.text_document_position.text_document.uri, url.clone());
23174 assert_eq!(
23175 params.text_document_position.position,
23176 complete_from_position
23177 );
23178 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
23179 is_incomplete: is_incomplete,
23180 item_defaults: None,
23181 items: completions
23182 .iter()
23183 .map(|completion_text| lsp::CompletionItem {
23184 label: completion_text.to_string(),
23185 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
23186 range: replace_range,
23187 new_text: completion_text.to_string(),
23188 })),
23189 ..Default::default()
23190 })
23191 .collect(),
23192 })))
23193 }
23194 });
23195
23196 async move {
23197 request.next().await;
23198 }
23199}
23200
23201/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
23202/// given instead, which also contains an `insert` range.
23203///
23204/// This function uses markers to define ranges:
23205/// - `|` marks the cursor position
23206/// - `<>` marks the replace range
23207/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
23208pub fn handle_completion_request_with_insert_and_replace(
23209 cx: &mut EditorLspTestContext,
23210 marked_string: &str,
23211 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
23212 counter: Arc<AtomicUsize>,
23213) -> impl Future<Output = ()> {
23214 let complete_from_marker: TextRangeMarker = '|'.into();
23215 let replace_range_marker: TextRangeMarker = ('<', '>').into();
23216 let insert_range_marker: TextRangeMarker = ('{', '}').into();
23217
23218 let (_, mut marked_ranges) = marked_text_ranges_by(
23219 marked_string,
23220 vec![
23221 complete_from_marker.clone(),
23222 replace_range_marker.clone(),
23223 insert_range_marker.clone(),
23224 ],
23225 );
23226
23227 let complete_from_position =
23228 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
23229 let replace_range =
23230 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
23231
23232 let insert_range = match marked_ranges.remove(&insert_range_marker) {
23233 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
23234 _ => lsp::Range {
23235 start: replace_range.start,
23236 end: complete_from_position,
23237 },
23238 };
23239
23240 let mut request =
23241 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
23242 let completions = completions.clone();
23243 counter.fetch_add(1, atomic::Ordering::Release);
23244 async move {
23245 assert_eq!(params.text_document_position.text_document.uri, url.clone());
23246 assert_eq!(
23247 params.text_document_position.position, complete_from_position,
23248 "marker `|` position doesn't match",
23249 );
23250 Ok(Some(lsp::CompletionResponse::Array(
23251 completions
23252 .iter()
23253 .map(|(label, new_text)| lsp::CompletionItem {
23254 label: label.to_string(),
23255 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
23256 lsp::InsertReplaceEdit {
23257 insert: insert_range,
23258 replace: replace_range,
23259 new_text: new_text.to_string(),
23260 },
23261 )),
23262 ..Default::default()
23263 })
23264 .collect(),
23265 )))
23266 }
23267 });
23268
23269 async move {
23270 request.next().await;
23271 }
23272}
23273
23274fn handle_resolve_completion_request(
23275 cx: &mut EditorLspTestContext,
23276 edits: Option<Vec<(&'static str, &'static str)>>,
23277) -> impl Future<Output = ()> {
23278 let edits = edits.map(|edits| {
23279 edits
23280 .iter()
23281 .map(|(marked_string, new_text)| {
23282 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
23283 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
23284 lsp::TextEdit::new(replace_range, new_text.to_string())
23285 })
23286 .collect::<Vec<_>>()
23287 });
23288
23289 let mut request =
23290 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
23291 let edits = edits.clone();
23292 async move {
23293 Ok(lsp::CompletionItem {
23294 additional_text_edits: edits,
23295 ..Default::default()
23296 })
23297 }
23298 });
23299
23300 async move {
23301 request.next().await;
23302 }
23303}
23304
23305pub(crate) fn update_test_language_settings(
23306 cx: &mut TestAppContext,
23307 f: impl Fn(&mut AllLanguageSettingsContent),
23308) {
23309 cx.update(|cx| {
23310 SettingsStore::update_global(cx, |store, cx| {
23311 store.update_user_settings::<AllLanguageSettings>(cx, f);
23312 });
23313 });
23314}
23315
23316pub(crate) fn update_test_project_settings(
23317 cx: &mut TestAppContext,
23318 f: impl Fn(&mut ProjectSettings),
23319) {
23320 cx.update(|cx| {
23321 SettingsStore::update_global(cx, |store, cx| {
23322 store.update_user_settings::<ProjectSettings>(cx, f);
23323 });
23324 });
23325}
23326
23327pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
23328 cx.update(|cx| {
23329 assets::Assets.load_test_fonts(cx);
23330 let store = SettingsStore::test(cx);
23331 cx.set_global(store);
23332 theme::init(theme::LoadThemes::JustBase, cx);
23333 release_channel::init(SemanticVersion::default(), cx);
23334 client::init_settings(cx);
23335 language::init(cx);
23336 Project::init_settings(cx);
23337 workspace::init_settings(cx);
23338 crate::init(cx);
23339 });
23340 zlog::init_test();
23341 update_test_language_settings(cx, f);
23342}
23343
23344#[track_caller]
23345fn assert_hunk_revert(
23346 not_reverted_text_with_selections: &str,
23347 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
23348 expected_reverted_text_with_selections: &str,
23349 base_text: &str,
23350 cx: &mut EditorLspTestContext,
23351) {
23352 cx.set_state(not_reverted_text_with_selections);
23353 cx.set_head_text(base_text);
23354 cx.executor().run_until_parked();
23355
23356 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
23357 let snapshot = editor.snapshot(window, cx);
23358 let reverted_hunk_statuses = snapshot
23359 .buffer_snapshot
23360 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
23361 .map(|hunk| hunk.status().kind)
23362 .collect::<Vec<_>>();
23363
23364 editor.git_restore(&Default::default(), window, cx);
23365 reverted_hunk_statuses
23366 });
23367 cx.executor().run_until_parked();
23368 cx.assert_editor_state(expected_reverted_text_with_selections);
23369 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
23370}
23371
23372#[gpui::test(iterations = 10)]
23373async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
23374 init_test(cx, |_| {});
23375
23376 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
23377 let counter = diagnostic_requests.clone();
23378
23379 let fs = FakeFs::new(cx.executor());
23380 fs.insert_tree(
23381 path!("/a"),
23382 json!({
23383 "first.rs": "fn main() { let a = 5; }",
23384 "second.rs": "// Test file",
23385 }),
23386 )
23387 .await;
23388
23389 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23390 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23391 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23392
23393 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23394 language_registry.add(rust_lang());
23395 let mut fake_servers = language_registry.register_fake_lsp(
23396 "Rust",
23397 FakeLspAdapter {
23398 capabilities: lsp::ServerCapabilities {
23399 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
23400 lsp::DiagnosticOptions {
23401 identifier: None,
23402 inter_file_dependencies: true,
23403 workspace_diagnostics: true,
23404 work_done_progress_options: Default::default(),
23405 },
23406 )),
23407 ..Default::default()
23408 },
23409 ..Default::default()
23410 },
23411 );
23412
23413 let editor = workspace
23414 .update(cx, |workspace, window, cx| {
23415 workspace.open_abs_path(
23416 PathBuf::from(path!("/a/first.rs")),
23417 OpenOptions::default(),
23418 window,
23419 cx,
23420 )
23421 })
23422 .unwrap()
23423 .await
23424 .unwrap()
23425 .downcast::<Editor>()
23426 .unwrap();
23427 let fake_server = fake_servers.next().await.unwrap();
23428 let server_id = fake_server.server.server_id();
23429 let mut first_request = fake_server
23430 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
23431 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
23432 let result_id = Some(new_result_id.to_string());
23433 assert_eq!(
23434 params.text_document.uri,
23435 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
23436 );
23437 async move {
23438 Ok(lsp::DocumentDiagnosticReportResult::Report(
23439 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
23440 related_documents: None,
23441 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
23442 items: Vec::new(),
23443 result_id,
23444 },
23445 }),
23446 ))
23447 }
23448 });
23449
23450 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
23451 project.update(cx, |project, cx| {
23452 let buffer_id = editor
23453 .read(cx)
23454 .buffer()
23455 .read(cx)
23456 .as_singleton()
23457 .expect("created a singleton buffer")
23458 .read(cx)
23459 .remote_id();
23460 let buffer_result_id = project
23461 .lsp_store()
23462 .read(cx)
23463 .result_id(server_id, buffer_id, cx);
23464 assert_eq!(expected, buffer_result_id);
23465 });
23466 };
23467
23468 ensure_result_id(None, cx);
23469 cx.executor().advance_clock(Duration::from_millis(60));
23470 cx.executor().run_until_parked();
23471 assert_eq!(
23472 diagnostic_requests.load(atomic::Ordering::Acquire),
23473 1,
23474 "Opening file should trigger diagnostic request"
23475 );
23476 first_request
23477 .next()
23478 .await
23479 .expect("should have sent the first diagnostics pull request");
23480 ensure_result_id(Some("1".to_string()), cx);
23481
23482 // Editing should trigger diagnostics
23483 editor.update_in(cx, |editor, window, cx| {
23484 editor.handle_input("2", window, cx)
23485 });
23486 cx.executor().advance_clock(Duration::from_millis(60));
23487 cx.executor().run_until_parked();
23488 assert_eq!(
23489 diagnostic_requests.load(atomic::Ordering::Acquire),
23490 2,
23491 "Editing should trigger diagnostic request"
23492 );
23493 ensure_result_id(Some("2".to_string()), cx);
23494
23495 // Moving cursor should not trigger diagnostic request
23496 editor.update_in(cx, |editor, window, cx| {
23497 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23498 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
23499 });
23500 });
23501 cx.executor().advance_clock(Duration::from_millis(60));
23502 cx.executor().run_until_parked();
23503 assert_eq!(
23504 diagnostic_requests.load(atomic::Ordering::Acquire),
23505 2,
23506 "Cursor movement should not trigger diagnostic request"
23507 );
23508 ensure_result_id(Some("2".to_string()), cx);
23509 // Multiple rapid edits should be debounced
23510 for _ in 0..5 {
23511 editor.update_in(cx, |editor, window, cx| {
23512 editor.handle_input("x", window, cx)
23513 });
23514 }
23515 cx.executor().advance_clock(Duration::from_millis(60));
23516 cx.executor().run_until_parked();
23517
23518 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
23519 assert!(
23520 final_requests <= 4,
23521 "Multiple rapid edits should be debounced (got {final_requests} requests)",
23522 );
23523 ensure_result_id(Some(final_requests.to_string()), cx);
23524}
23525
23526#[gpui::test]
23527async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
23528 // Regression test for issue #11671
23529 // Previously, adding a cursor after moving multiple cursors would reset
23530 // the cursor count instead of adding to the existing cursors.
23531 init_test(cx, |_| {});
23532 let mut cx = EditorTestContext::new(cx).await;
23533
23534 // Create a simple buffer with cursor at start
23535 cx.set_state(indoc! {"
23536 ˇaaaa
23537 bbbb
23538 cccc
23539 dddd
23540 eeee
23541 ffff
23542 gggg
23543 hhhh"});
23544
23545 // Add 2 cursors below (so we have 3 total)
23546 cx.update_editor(|editor, window, cx| {
23547 editor.add_selection_below(&Default::default(), window, cx);
23548 editor.add_selection_below(&Default::default(), window, cx);
23549 });
23550
23551 // Verify we have 3 cursors
23552 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
23553 assert_eq!(
23554 initial_count, 3,
23555 "Should have 3 cursors after adding 2 below"
23556 );
23557
23558 // Move down one line
23559 cx.update_editor(|editor, window, cx| {
23560 editor.move_down(&MoveDown, window, cx);
23561 });
23562
23563 // Add another cursor below
23564 cx.update_editor(|editor, window, cx| {
23565 editor.add_selection_below(&Default::default(), window, cx);
23566 });
23567
23568 // Should now have 4 cursors (3 original + 1 new)
23569 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
23570 assert_eq!(
23571 final_count, 4,
23572 "Should have 4 cursors after moving and adding another"
23573 );
23574}
23575
23576#[gpui::test(iterations = 10)]
23577async fn test_document_colors(cx: &mut TestAppContext) {
23578 let expected_color = Rgba {
23579 r: 0.33,
23580 g: 0.33,
23581 b: 0.33,
23582 a: 0.33,
23583 };
23584
23585 init_test(cx, |_| {});
23586
23587 let fs = FakeFs::new(cx.executor());
23588 fs.insert_tree(
23589 path!("/a"),
23590 json!({
23591 "first.rs": "fn main() { let a = 5; }",
23592 }),
23593 )
23594 .await;
23595
23596 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23597 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23598 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23599
23600 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23601 language_registry.add(rust_lang());
23602 let mut fake_servers = language_registry.register_fake_lsp(
23603 "Rust",
23604 FakeLspAdapter {
23605 capabilities: lsp::ServerCapabilities {
23606 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
23607 ..lsp::ServerCapabilities::default()
23608 },
23609 name: "rust-analyzer",
23610 ..FakeLspAdapter::default()
23611 },
23612 );
23613 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
23614 "Rust",
23615 FakeLspAdapter {
23616 capabilities: lsp::ServerCapabilities {
23617 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
23618 ..lsp::ServerCapabilities::default()
23619 },
23620 name: "not-rust-analyzer",
23621 ..FakeLspAdapter::default()
23622 },
23623 );
23624
23625 let editor = workspace
23626 .update(cx, |workspace, window, cx| {
23627 workspace.open_abs_path(
23628 PathBuf::from(path!("/a/first.rs")),
23629 OpenOptions::default(),
23630 window,
23631 cx,
23632 )
23633 })
23634 .unwrap()
23635 .await
23636 .unwrap()
23637 .downcast::<Editor>()
23638 .unwrap();
23639 let fake_language_server = fake_servers.next().await.unwrap();
23640 let fake_language_server_without_capabilities =
23641 fake_servers_without_capabilities.next().await.unwrap();
23642 let requests_made = Arc::new(AtomicUsize::new(0));
23643 let closure_requests_made = Arc::clone(&requests_made);
23644 let mut color_request_handle = fake_language_server
23645 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
23646 let requests_made = Arc::clone(&closure_requests_made);
23647 async move {
23648 assert_eq!(
23649 params.text_document.uri,
23650 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
23651 );
23652 requests_made.fetch_add(1, atomic::Ordering::Release);
23653 Ok(vec![
23654 lsp::ColorInformation {
23655 range: lsp::Range {
23656 start: lsp::Position {
23657 line: 0,
23658 character: 0,
23659 },
23660 end: lsp::Position {
23661 line: 0,
23662 character: 1,
23663 },
23664 },
23665 color: lsp::Color {
23666 red: 0.33,
23667 green: 0.33,
23668 blue: 0.33,
23669 alpha: 0.33,
23670 },
23671 },
23672 lsp::ColorInformation {
23673 range: lsp::Range {
23674 start: lsp::Position {
23675 line: 0,
23676 character: 0,
23677 },
23678 end: lsp::Position {
23679 line: 0,
23680 character: 1,
23681 },
23682 },
23683 color: lsp::Color {
23684 red: 0.33,
23685 green: 0.33,
23686 blue: 0.33,
23687 alpha: 0.33,
23688 },
23689 },
23690 ])
23691 }
23692 });
23693
23694 let _handle = fake_language_server_without_capabilities
23695 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
23696 panic!("Should not be called");
23697 });
23698 cx.executor().advance_clock(Duration::from_millis(100));
23699 color_request_handle.next().await.unwrap();
23700 cx.run_until_parked();
23701 assert_eq!(
23702 1,
23703 requests_made.load(atomic::Ordering::Acquire),
23704 "Should query for colors once per editor open"
23705 );
23706 editor.update_in(cx, |editor, _, cx| {
23707 assert_eq!(
23708 vec![expected_color],
23709 extract_color_inlays(editor, cx),
23710 "Should have an initial inlay"
23711 );
23712 });
23713
23714 // opening another file in a split should not influence the LSP query counter
23715 workspace
23716 .update(cx, |workspace, window, cx| {
23717 assert_eq!(
23718 workspace.panes().len(),
23719 1,
23720 "Should have one pane with one editor"
23721 );
23722 workspace.move_item_to_pane_in_direction(
23723 &MoveItemToPaneInDirection {
23724 direction: SplitDirection::Right,
23725 focus: false,
23726 clone: true,
23727 },
23728 window,
23729 cx,
23730 );
23731 })
23732 .unwrap();
23733 cx.run_until_parked();
23734 workspace
23735 .update(cx, |workspace, _, cx| {
23736 let panes = workspace.panes();
23737 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
23738 for pane in panes {
23739 let editor = pane
23740 .read(cx)
23741 .active_item()
23742 .and_then(|item| item.downcast::<Editor>())
23743 .expect("Should have opened an editor in each split");
23744 let editor_file = editor
23745 .read(cx)
23746 .buffer()
23747 .read(cx)
23748 .as_singleton()
23749 .expect("test deals with singleton buffers")
23750 .read(cx)
23751 .file()
23752 .expect("test buffese should have a file")
23753 .path();
23754 assert_eq!(
23755 editor_file.as_ref(),
23756 Path::new("first.rs"),
23757 "Both editors should be opened for the same file"
23758 )
23759 }
23760 })
23761 .unwrap();
23762
23763 cx.executor().advance_clock(Duration::from_millis(500));
23764 let save = editor.update_in(cx, |editor, window, cx| {
23765 editor.move_to_end(&MoveToEnd, window, cx);
23766 editor.handle_input("dirty", window, cx);
23767 editor.save(
23768 SaveOptions {
23769 format: true,
23770 autosave: true,
23771 },
23772 project.clone(),
23773 window,
23774 cx,
23775 )
23776 });
23777 save.await.unwrap();
23778
23779 color_request_handle.next().await.unwrap();
23780 cx.run_until_parked();
23781 assert_eq!(
23782 3,
23783 requests_made.load(atomic::Ordering::Acquire),
23784 "Should query for colors once per save and once per formatting after save"
23785 );
23786
23787 drop(editor);
23788 let close = workspace
23789 .update(cx, |workspace, window, cx| {
23790 workspace.active_pane().update(cx, |pane, cx| {
23791 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23792 })
23793 })
23794 .unwrap();
23795 close.await.unwrap();
23796 let close = workspace
23797 .update(cx, |workspace, window, cx| {
23798 workspace.active_pane().update(cx, |pane, cx| {
23799 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23800 })
23801 })
23802 .unwrap();
23803 close.await.unwrap();
23804 assert_eq!(
23805 3,
23806 requests_made.load(atomic::Ordering::Acquire),
23807 "After saving and closing all editors, no extra requests should be made"
23808 );
23809 workspace
23810 .update(cx, |workspace, _, cx| {
23811 assert!(
23812 workspace.active_item(cx).is_none(),
23813 "Should close all editors"
23814 )
23815 })
23816 .unwrap();
23817
23818 workspace
23819 .update(cx, |workspace, window, cx| {
23820 workspace.active_pane().update(cx, |pane, cx| {
23821 pane.navigate_backward(window, cx);
23822 })
23823 })
23824 .unwrap();
23825 cx.executor().advance_clock(Duration::from_millis(100));
23826 cx.run_until_parked();
23827 let editor = workspace
23828 .update(cx, |workspace, _, cx| {
23829 workspace
23830 .active_item(cx)
23831 .expect("Should have reopened the editor again after navigating back")
23832 .downcast::<Editor>()
23833 .expect("Should be an editor")
23834 })
23835 .unwrap();
23836 color_request_handle.next().await.unwrap();
23837 assert_eq!(
23838 3,
23839 requests_made.load(atomic::Ordering::Acquire),
23840 "Cache should be reused on buffer close and reopen"
23841 );
23842 editor.update(cx, |editor, cx| {
23843 assert_eq!(
23844 vec![expected_color],
23845 extract_color_inlays(editor, cx),
23846 "Should have an initial inlay"
23847 );
23848 });
23849}
23850
23851#[gpui::test]
23852async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
23853 init_test(cx, |_| {});
23854 let (editor, cx) = cx.add_window_view(Editor::single_line);
23855 editor.update_in(cx, |editor, window, cx| {
23856 editor.set_text("oops\n\nwow\n", window, cx)
23857 });
23858 cx.run_until_parked();
23859 editor.update(cx, |editor, cx| {
23860 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
23861 });
23862 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
23863 cx.run_until_parked();
23864 editor.update(cx, |editor, cx| {
23865 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
23866 });
23867}
23868
23869#[track_caller]
23870fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
23871 editor
23872 .all_inlays(cx)
23873 .into_iter()
23874 .filter_map(|inlay| inlay.get_color())
23875 .map(Rgba::from)
23876 .collect()
23877}