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: Some(language::DocumentationConfig {
2879 start: "/**".into(),
2880 end: "*/".into(),
2881 prefix: "* ".into(),
2882 tab_size: NonZeroU32::new(1).unwrap(),
2883 }),
2884
2885 ..LanguageConfig::default()
2886 },
2887 Some(tree_sitter_rust::LANGUAGE.into()),
2888 )
2889 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
2890 .unwrap(),
2891 );
2892
2893 {
2894 let mut cx = EditorTestContext::new(cx).await;
2895 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2896 cx.set_state(indoc! {"
2897 /**ˇ
2898 "});
2899
2900 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2901 cx.assert_editor_state(indoc! {"
2902 /**
2903 * ˇ
2904 "});
2905 // Ensure that if cursor is before the comment start,
2906 // we do not actually insert a comment prefix.
2907 cx.set_state(indoc! {"
2908 ˇ/**
2909 "});
2910 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2911 cx.assert_editor_state(indoc! {"
2912
2913 ˇ/**
2914 "});
2915 // Ensure that if cursor is between it doesn't add comment prefix.
2916 cx.set_state(indoc! {"
2917 /*ˇ*
2918 "});
2919 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2920 cx.assert_editor_state(indoc! {"
2921 /*
2922 ˇ*
2923 "});
2924 // Ensure that if suffix exists on same line after cursor it adds new line.
2925 cx.set_state(indoc! {"
2926 /**ˇ*/
2927 "});
2928 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2929 cx.assert_editor_state(indoc! {"
2930 /**
2931 * ˇ
2932 */
2933 "});
2934 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2935 cx.set_state(indoc! {"
2936 /**ˇ */
2937 "});
2938 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2939 cx.assert_editor_state(indoc! {"
2940 /**
2941 * ˇ
2942 */
2943 "});
2944 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2945 cx.set_state(indoc! {"
2946 /** ˇ*/
2947 "});
2948 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2949 cx.assert_editor_state(
2950 indoc! {"
2951 /**s
2952 * ˇ
2953 */
2954 "}
2955 .replace("s", " ") // s is used as space placeholder to prevent format on save
2956 .as_str(),
2957 );
2958 // Ensure that delimiter space is preserved when newline on already
2959 // spaced delimiter.
2960 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2961 cx.assert_editor_state(
2962 indoc! {"
2963 /**s
2964 *s
2965 * ˇ
2966 */
2967 "}
2968 .replace("s", " ") // s is used as space placeholder to prevent format on save
2969 .as_str(),
2970 );
2971 // Ensure that delimiter space is preserved when space is not
2972 // on existing delimiter.
2973 cx.set_state(indoc! {"
2974 /**
2975 *ˇ
2976 */
2977 "});
2978 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2979 cx.assert_editor_state(indoc! {"
2980 /**
2981 *
2982 * ˇ
2983 */
2984 "});
2985 // Ensure that if suffix exists on same line after cursor it
2986 // doesn't add extra new line if prefix is not on same line.
2987 cx.set_state(indoc! {"
2988 /**
2989 ˇ*/
2990 "});
2991 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2992 cx.assert_editor_state(indoc! {"
2993 /**
2994
2995 ˇ*/
2996 "});
2997 // Ensure that it detects suffix after existing prefix.
2998 cx.set_state(indoc! {"
2999 /**ˇ/
3000 "});
3001 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3002 cx.assert_editor_state(indoc! {"
3003 /**
3004 ˇ/
3005 "});
3006 // Ensure that if suffix exists on same line before
3007 // cursor it does not add comment prefix.
3008 cx.set_state(indoc! {"
3009 /** */ˇ
3010 "});
3011 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3012 cx.assert_editor_state(indoc! {"
3013 /** */
3014 ˇ
3015 "});
3016 // Ensure that if suffix exists on same line before
3017 // cursor it does not add comment prefix.
3018 cx.set_state(indoc! {"
3019 /**
3020 *
3021 */ˇ
3022 "});
3023 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3024 cx.assert_editor_state(indoc! {"
3025 /**
3026 *
3027 */
3028 ˇ
3029 "});
3030
3031 // Ensure that inline comment followed by code
3032 // doesn't add comment prefix on newline
3033 cx.set_state(indoc! {"
3034 /** */ textˇ
3035 "});
3036 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3037 cx.assert_editor_state(indoc! {"
3038 /** */ text
3039 ˇ
3040 "});
3041
3042 // Ensure that text after comment end tag
3043 // doesn't add comment prefix on newline
3044 cx.set_state(indoc! {"
3045 /**
3046 *
3047 */ˇtext
3048 "});
3049 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3050 cx.assert_editor_state(indoc! {"
3051 /**
3052 *
3053 */
3054 ˇtext
3055 "});
3056
3057 // Ensure if not comment block it doesn't
3058 // add comment prefix on newline
3059 cx.set_state(indoc! {"
3060 * textˇ
3061 "});
3062 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3063 cx.assert_editor_state(indoc! {"
3064 * text
3065 ˇ
3066 "});
3067 }
3068 // Ensure that comment continuations can be disabled.
3069 update_test_language_settings(cx, |settings| {
3070 settings.defaults.extend_comment_on_newline = Some(false);
3071 });
3072 let mut cx = EditorTestContext::new(cx).await;
3073 cx.set_state(indoc! {"
3074 /**ˇ
3075 "});
3076 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3077 cx.assert_editor_state(indoc! {"
3078 /**
3079 ˇ
3080 "});
3081}
3082
3083#[gpui::test]
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(("--[[".into(), "]]".into())),
3093 ..LanguageConfig::default()
3094 },
3095 None,
3096 ));
3097
3098 let mut cx = EditorTestContext::new(cx).await;
3099 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3100
3101 // Line with line comment should extend
3102 cx.set_state(indoc! {"
3103 --ˇ
3104 "});
3105 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3106 cx.assert_editor_state(indoc! {"
3107 --
3108 --ˇ
3109 "});
3110
3111 // Line with block comment that matches line comment should not extend
3112 cx.set_state(indoc! {"
3113 --[[ˇ
3114 "});
3115 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3116 cx.assert_editor_state(indoc! {"
3117 --[[
3118 ˇ
3119 "});
3120}
3121
3122#[gpui::test]
3123fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3124 init_test(cx, |_| {});
3125
3126 let editor = cx.add_window(|window, cx| {
3127 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3128 let mut editor = build_editor(buffer.clone(), window, cx);
3129 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3130 s.select_ranges([3..4, 11..12, 19..20])
3131 });
3132 editor
3133 });
3134
3135 _ = editor.update(cx, |editor, window, cx| {
3136 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3137 editor.buffer.update(cx, |buffer, cx| {
3138 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3139 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3140 });
3141 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3142
3143 editor.insert("Z", window, cx);
3144 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3145
3146 // The selections are moved after the inserted characters
3147 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3148 });
3149}
3150
3151#[gpui::test]
3152async fn test_tab(cx: &mut TestAppContext) {
3153 init_test(cx, |settings| {
3154 settings.defaults.tab_size = NonZeroU32::new(3)
3155 });
3156
3157 let mut cx = EditorTestContext::new(cx).await;
3158 cx.set_state(indoc! {"
3159 ˇabˇc
3160 ˇ🏀ˇ🏀ˇefg
3161 dˇ
3162 "});
3163 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3164 cx.assert_editor_state(indoc! {"
3165 ˇab ˇc
3166 ˇ🏀 ˇ🏀 ˇefg
3167 d ˇ
3168 "});
3169
3170 cx.set_state(indoc! {"
3171 a
3172 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3173 "});
3174 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3175 cx.assert_editor_state(indoc! {"
3176 a
3177 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3178 "});
3179}
3180
3181#[gpui::test]
3182async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3183 init_test(cx, |_| {});
3184
3185 let mut cx = EditorTestContext::new(cx).await;
3186 let language = Arc::new(
3187 Language::new(
3188 LanguageConfig::default(),
3189 Some(tree_sitter_rust::LANGUAGE.into()),
3190 )
3191 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3192 .unwrap(),
3193 );
3194 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3195
3196 // test when all cursors are not at suggested indent
3197 // then simply move to their suggested indent location
3198 cx.set_state(indoc! {"
3199 const a: B = (
3200 c(
3201 ˇ
3202 ˇ )
3203 );
3204 "});
3205 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3206 cx.assert_editor_state(indoc! {"
3207 const a: B = (
3208 c(
3209 ˇ
3210 ˇ)
3211 );
3212 "});
3213
3214 // test cursor already at suggested indent not moving when
3215 // other cursors are yet to reach their suggested indents
3216 cx.set_state(indoc! {"
3217 ˇ
3218 const a: B = (
3219 c(
3220 d(
3221 ˇ
3222 )
3223 ˇ
3224 ˇ )
3225 );
3226 "});
3227 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3228 cx.assert_editor_state(indoc! {"
3229 ˇ
3230 const a: B = (
3231 c(
3232 d(
3233 ˇ
3234 )
3235 ˇ
3236 ˇ)
3237 );
3238 "});
3239 // test when all cursors are at suggested indent then tab is inserted
3240 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3241 cx.assert_editor_state(indoc! {"
3242 ˇ
3243 const a: B = (
3244 c(
3245 d(
3246 ˇ
3247 )
3248 ˇ
3249 ˇ)
3250 );
3251 "});
3252
3253 // test when current indent is less than suggested indent,
3254 // we adjust line to match suggested indent and move cursor to it
3255 //
3256 // when no other cursor is at word boundary, all of them should move
3257 cx.set_state(indoc! {"
3258 const a: B = (
3259 c(
3260 d(
3261 ˇ
3262 ˇ )
3263 ˇ )
3264 );
3265 "});
3266 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3267 cx.assert_editor_state(indoc! {"
3268 const a: B = (
3269 c(
3270 d(
3271 ˇ
3272 ˇ)
3273 ˇ)
3274 );
3275 "});
3276
3277 // test when current indent is less than suggested indent,
3278 // we adjust line to match suggested indent and move cursor to it
3279 //
3280 // when some other cursor is at word boundary, it should not move
3281 cx.set_state(indoc! {"
3282 const a: B = (
3283 c(
3284 d(
3285 ˇ
3286 ˇ )
3287 ˇ)
3288 );
3289 "});
3290 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3291 cx.assert_editor_state(indoc! {"
3292 const a: B = (
3293 c(
3294 d(
3295 ˇ
3296 ˇ)
3297 ˇ)
3298 );
3299 "});
3300
3301 // test when current indent is more than suggested indent,
3302 // we just move cursor to current indent instead of suggested indent
3303 //
3304 // when no other cursor is at word boundary, all of them should move
3305 cx.set_state(indoc! {"
3306 const a: B = (
3307 c(
3308 d(
3309 ˇ
3310 ˇ )
3311 ˇ )
3312 );
3313 "});
3314 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3315 cx.assert_editor_state(indoc! {"
3316 const a: B = (
3317 c(
3318 d(
3319 ˇ
3320 ˇ)
3321 ˇ)
3322 );
3323 "});
3324 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3325 cx.assert_editor_state(indoc! {"
3326 const a: B = (
3327 c(
3328 d(
3329 ˇ
3330 ˇ)
3331 ˇ)
3332 );
3333 "});
3334
3335 // test when current indent is more than suggested indent,
3336 // we just move cursor to current indent instead of suggested indent
3337 //
3338 // when some other cursor is at word boundary, it doesn't move
3339 cx.set_state(indoc! {"
3340 const a: B = (
3341 c(
3342 d(
3343 ˇ
3344 ˇ )
3345 ˇ)
3346 );
3347 "});
3348 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3349 cx.assert_editor_state(indoc! {"
3350 const a: B = (
3351 c(
3352 d(
3353 ˇ
3354 ˇ)
3355 ˇ)
3356 );
3357 "});
3358
3359 // handle auto-indent when there are multiple cursors on the same line
3360 cx.set_state(indoc! {"
3361 const a: B = (
3362 c(
3363 ˇ ˇ
3364 ˇ )
3365 );
3366 "});
3367 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3368 cx.assert_editor_state(indoc! {"
3369 const a: B = (
3370 c(
3371 ˇ
3372 ˇ)
3373 );
3374 "});
3375}
3376
3377#[gpui::test]
3378async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3379 init_test(cx, |settings| {
3380 settings.defaults.tab_size = NonZeroU32::new(3)
3381 });
3382
3383 let mut cx = EditorTestContext::new(cx).await;
3384 cx.set_state(indoc! {"
3385 ˇ
3386 \t ˇ
3387 \t ˇ
3388 \t ˇ
3389 \t \t\t \t \t\t \t\t \t \t ˇ
3390 "});
3391
3392 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3393 cx.assert_editor_state(indoc! {"
3394 ˇ
3395 \t ˇ
3396 \t ˇ
3397 \t ˇ
3398 \t \t\t \t \t\t \t\t \t \t ˇ
3399 "});
3400}
3401
3402#[gpui::test]
3403async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3404 init_test(cx, |settings| {
3405 settings.defaults.tab_size = NonZeroU32::new(4)
3406 });
3407
3408 let language = Arc::new(
3409 Language::new(
3410 LanguageConfig::default(),
3411 Some(tree_sitter_rust::LANGUAGE.into()),
3412 )
3413 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3414 .unwrap(),
3415 );
3416
3417 let mut cx = EditorTestContext::new(cx).await;
3418 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3419 cx.set_state(indoc! {"
3420 fn a() {
3421 if b {
3422 \t ˇc
3423 }
3424 }
3425 "});
3426
3427 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3428 cx.assert_editor_state(indoc! {"
3429 fn a() {
3430 if b {
3431 ˇc
3432 }
3433 }
3434 "});
3435}
3436
3437#[gpui::test]
3438async fn test_indent_outdent(cx: &mut TestAppContext) {
3439 init_test(cx, |settings| {
3440 settings.defaults.tab_size = NonZeroU32::new(4);
3441 });
3442
3443 let mut cx = EditorTestContext::new(cx).await;
3444
3445 cx.set_state(indoc! {"
3446 «oneˇ» «twoˇ»
3447 three
3448 four
3449 "});
3450 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3451 cx.assert_editor_state(indoc! {"
3452 «oneˇ» «twoˇ»
3453 three
3454 four
3455 "});
3456
3457 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3458 cx.assert_editor_state(indoc! {"
3459 «oneˇ» «twoˇ»
3460 three
3461 four
3462 "});
3463
3464 // select across line ending
3465 cx.set_state(indoc! {"
3466 one two
3467 t«hree
3468 ˇ» four
3469 "});
3470 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3471 cx.assert_editor_state(indoc! {"
3472 one two
3473 t«hree
3474 ˇ» four
3475 "});
3476
3477 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3478 cx.assert_editor_state(indoc! {"
3479 one two
3480 t«hree
3481 ˇ» four
3482 "});
3483
3484 // Ensure that indenting/outdenting works when the cursor is at column 0.
3485 cx.set_state(indoc! {"
3486 one two
3487 ˇthree
3488 four
3489 "});
3490 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3491 cx.assert_editor_state(indoc! {"
3492 one two
3493 ˇthree
3494 four
3495 "});
3496
3497 cx.set_state(indoc! {"
3498 one two
3499 ˇ three
3500 four
3501 "});
3502 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3503 cx.assert_editor_state(indoc! {"
3504 one two
3505 ˇthree
3506 four
3507 "});
3508}
3509
3510#[gpui::test]
3511async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3512 // This is a regression test for issue #33761
3513 init_test(cx, |_| {});
3514
3515 let mut cx = EditorTestContext::new(cx).await;
3516 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3517 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3518
3519 cx.set_state(
3520 r#"ˇ# ingress:
3521ˇ# api:
3522ˇ# enabled: false
3523ˇ# pathType: Prefix
3524ˇ# console:
3525ˇ# enabled: false
3526ˇ# pathType: Prefix
3527"#,
3528 );
3529
3530 // Press tab to indent all lines
3531 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3532
3533 cx.assert_editor_state(
3534 r#" ˇ# ingress:
3535 ˇ# api:
3536 ˇ# enabled: false
3537 ˇ# pathType: Prefix
3538 ˇ# console:
3539 ˇ# enabled: false
3540 ˇ# pathType: Prefix
3541"#,
3542 );
3543}
3544
3545#[gpui::test]
3546async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3547 // This is a test to make sure our fix for issue #33761 didn't break anything
3548 init_test(cx, |_| {});
3549
3550 let mut cx = EditorTestContext::new(cx).await;
3551 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3552 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3553
3554 cx.set_state(
3555 r#"ˇingress:
3556ˇ api:
3557ˇ enabled: false
3558ˇ pathType: Prefix
3559"#,
3560 );
3561
3562 // Press tab to indent all lines
3563 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3564
3565 cx.assert_editor_state(
3566 r#"ˇingress:
3567 ˇapi:
3568 ˇenabled: false
3569 ˇpathType: Prefix
3570"#,
3571 );
3572}
3573
3574#[gpui::test]
3575async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3576 init_test(cx, |settings| {
3577 settings.defaults.hard_tabs = Some(true);
3578 });
3579
3580 let mut cx = EditorTestContext::new(cx).await;
3581
3582 // select two ranges on one line
3583 cx.set_state(indoc! {"
3584 «oneˇ» «twoˇ»
3585 three
3586 four
3587 "});
3588 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3589 cx.assert_editor_state(indoc! {"
3590 \t«oneˇ» «twoˇ»
3591 three
3592 four
3593 "});
3594 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3595 cx.assert_editor_state(indoc! {"
3596 \t\t«oneˇ» «twoˇ»
3597 three
3598 four
3599 "});
3600 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3601 cx.assert_editor_state(indoc! {"
3602 \t«oneˇ» «twoˇ»
3603 three
3604 four
3605 "});
3606 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3607 cx.assert_editor_state(indoc! {"
3608 «oneˇ» «twoˇ»
3609 three
3610 four
3611 "});
3612
3613 // select across a line ending
3614 cx.set_state(indoc! {"
3615 one two
3616 t«hree
3617 ˇ»four
3618 "});
3619 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3620 cx.assert_editor_state(indoc! {"
3621 one two
3622 \tt«hree
3623 ˇ»four
3624 "});
3625 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3626 cx.assert_editor_state(indoc! {"
3627 one two
3628 \t\tt«hree
3629 ˇ»four
3630 "});
3631 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3632 cx.assert_editor_state(indoc! {"
3633 one two
3634 \tt«hree
3635 ˇ»four
3636 "});
3637 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3638 cx.assert_editor_state(indoc! {"
3639 one two
3640 t«hree
3641 ˇ»four
3642 "});
3643
3644 // Ensure that indenting/outdenting works when the cursor is at column 0.
3645 cx.set_state(indoc! {"
3646 one two
3647 ˇthree
3648 four
3649 "});
3650 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3651 cx.assert_editor_state(indoc! {"
3652 one two
3653 ˇthree
3654 four
3655 "});
3656 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3657 cx.assert_editor_state(indoc! {"
3658 one two
3659 \tˇthree
3660 four
3661 "});
3662 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3663 cx.assert_editor_state(indoc! {"
3664 one two
3665 ˇthree
3666 four
3667 "});
3668}
3669
3670#[gpui::test]
3671fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3672 init_test(cx, |settings| {
3673 settings.languages.0.extend([
3674 (
3675 "TOML".into(),
3676 LanguageSettingsContent {
3677 tab_size: NonZeroU32::new(2),
3678 ..Default::default()
3679 },
3680 ),
3681 (
3682 "Rust".into(),
3683 LanguageSettingsContent {
3684 tab_size: NonZeroU32::new(4),
3685 ..Default::default()
3686 },
3687 ),
3688 ]);
3689 });
3690
3691 let toml_language = Arc::new(Language::new(
3692 LanguageConfig {
3693 name: "TOML".into(),
3694 ..Default::default()
3695 },
3696 None,
3697 ));
3698 let rust_language = Arc::new(Language::new(
3699 LanguageConfig {
3700 name: "Rust".into(),
3701 ..Default::default()
3702 },
3703 None,
3704 ));
3705
3706 let toml_buffer =
3707 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3708 let rust_buffer =
3709 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3710 let multibuffer = cx.new(|cx| {
3711 let mut multibuffer = MultiBuffer::new(ReadWrite);
3712 multibuffer.push_excerpts(
3713 toml_buffer.clone(),
3714 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3715 cx,
3716 );
3717 multibuffer.push_excerpts(
3718 rust_buffer.clone(),
3719 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3720 cx,
3721 );
3722 multibuffer
3723 });
3724
3725 cx.add_window(|window, cx| {
3726 let mut editor = build_editor(multibuffer, window, cx);
3727
3728 assert_eq!(
3729 editor.text(cx),
3730 indoc! {"
3731 a = 1
3732 b = 2
3733
3734 const c: usize = 3;
3735 "}
3736 );
3737
3738 select_ranges(
3739 &mut editor,
3740 indoc! {"
3741 «aˇ» = 1
3742 b = 2
3743
3744 «const c:ˇ» usize = 3;
3745 "},
3746 window,
3747 cx,
3748 );
3749
3750 editor.tab(&Tab, window, cx);
3751 assert_text_with_selections(
3752 &mut editor,
3753 indoc! {"
3754 «aˇ» = 1
3755 b = 2
3756
3757 «const c:ˇ» usize = 3;
3758 "},
3759 cx,
3760 );
3761 editor.backtab(&Backtab, window, cx);
3762 assert_text_with_selections(
3763 &mut editor,
3764 indoc! {"
3765 «aˇ» = 1
3766 b = 2
3767
3768 «const c:ˇ» usize = 3;
3769 "},
3770 cx,
3771 );
3772
3773 editor
3774 });
3775}
3776
3777#[gpui::test]
3778async fn test_backspace(cx: &mut TestAppContext) {
3779 init_test(cx, |_| {});
3780
3781 let mut cx = EditorTestContext::new(cx).await;
3782
3783 // Basic backspace
3784 cx.set_state(indoc! {"
3785 onˇe two three
3786 fou«rˇ» five six
3787 seven «ˇeight nine
3788 »ten
3789 "});
3790 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3791 cx.assert_editor_state(indoc! {"
3792 oˇe two three
3793 fouˇ five six
3794 seven ˇten
3795 "});
3796
3797 // Test backspace inside and around indents
3798 cx.set_state(indoc! {"
3799 zero
3800 ˇone
3801 ˇtwo
3802 ˇ ˇ ˇ three
3803 ˇ ˇ four
3804 "});
3805 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3806 cx.assert_editor_state(indoc! {"
3807 zero
3808 ˇone
3809 ˇtwo
3810 ˇ threeˇ four
3811 "});
3812}
3813
3814#[gpui::test]
3815async fn test_delete(cx: &mut TestAppContext) {
3816 init_test(cx, |_| {});
3817
3818 let mut cx = EditorTestContext::new(cx).await;
3819 cx.set_state(indoc! {"
3820 onˇe two three
3821 fou«rˇ» five six
3822 seven «ˇeight nine
3823 »ten
3824 "});
3825 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3826 cx.assert_editor_state(indoc! {"
3827 onˇ two three
3828 fouˇ five six
3829 seven ˇten
3830 "});
3831}
3832
3833#[gpui::test]
3834fn test_delete_line(cx: &mut TestAppContext) {
3835 init_test(cx, |_| {});
3836
3837 let editor = cx.add_window(|window, cx| {
3838 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3839 build_editor(buffer, window, cx)
3840 });
3841 _ = editor.update(cx, |editor, window, cx| {
3842 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3843 s.select_display_ranges([
3844 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3845 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3846 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3847 ])
3848 });
3849 editor.delete_line(&DeleteLine, window, cx);
3850 assert_eq!(editor.display_text(cx), "ghi");
3851 assert_eq!(
3852 editor.selections.display_ranges(cx),
3853 vec![
3854 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3855 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3856 ]
3857 );
3858 });
3859
3860 let editor = cx.add_window(|window, cx| {
3861 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3862 build_editor(buffer, window, cx)
3863 });
3864 _ = editor.update(cx, |editor, window, cx| {
3865 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3866 s.select_display_ranges([
3867 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3868 ])
3869 });
3870 editor.delete_line(&DeleteLine, window, cx);
3871 assert_eq!(editor.display_text(cx), "ghi\n");
3872 assert_eq!(
3873 editor.selections.display_ranges(cx),
3874 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3875 );
3876 });
3877}
3878
3879#[gpui::test]
3880fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3881 init_test(cx, |_| {});
3882
3883 cx.add_window(|window, cx| {
3884 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3885 let mut editor = build_editor(buffer.clone(), window, cx);
3886 let buffer = buffer.read(cx).as_singleton().unwrap();
3887
3888 assert_eq!(
3889 editor.selections.ranges::<Point>(cx),
3890 &[Point::new(0, 0)..Point::new(0, 0)]
3891 );
3892
3893 // When on single line, replace newline at end by space
3894 editor.join_lines(&JoinLines, window, cx);
3895 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3896 assert_eq!(
3897 editor.selections.ranges::<Point>(cx),
3898 &[Point::new(0, 3)..Point::new(0, 3)]
3899 );
3900
3901 // When multiple lines are selected, remove newlines that are spanned by the selection
3902 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3903 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3904 });
3905 editor.join_lines(&JoinLines, window, cx);
3906 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3907 assert_eq!(
3908 editor.selections.ranges::<Point>(cx),
3909 &[Point::new(0, 11)..Point::new(0, 11)]
3910 );
3911
3912 // Undo should be transactional
3913 editor.undo(&Undo, window, cx);
3914 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3915 assert_eq!(
3916 editor.selections.ranges::<Point>(cx),
3917 &[Point::new(0, 5)..Point::new(2, 2)]
3918 );
3919
3920 // When joining an empty line don't insert a space
3921 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3922 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3923 });
3924 editor.join_lines(&JoinLines, window, cx);
3925 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3926 assert_eq!(
3927 editor.selections.ranges::<Point>(cx),
3928 [Point::new(2, 3)..Point::new(2, 3)]
3929 );
3930
3931 // We can remove trailing newlines
3932 editor.join_lines(&JoinLines, window, cx);
3933 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3934 assert_eq!(
3935 editor.selections.ranges::<Point>(cx),
3936 [Point::new(2, 3)..Point::new(2, 3)]
3937 );
3938
3939 // We don't blow up on the last line
3940 editor.join_lines(&JoinLines, window, cx);
3941 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3942 assert_eq!(
3943 editor.selections.ranges::<Point>(cx),
3944 [Point::new(2, 3)..Point::new(2, 3)]
3945 );
3946
3947 // reset to test indentation
3948 editor.buffer.update(cx, |buffer, cx| {
3949 buffer.edit(
3950 [
3951 (Point::new(1, 0)..Point::new(1, 2), " "),
3952 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3953 ],
3954 None,
3955 cx,
3956 )
3957 });
3958
3959 // We remove any leading spaces
3960 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3961 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3962 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3963 });
3964 editor.join_lines(&JoinLines, window, cx);
3965 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3966
3967 // We don't insert a space for a line containing only spaces
3968 editor.join_lines(&JoinLines, window, cx);
3969 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3970
3971 // We ignore any leading tabs
3972 editor.join_lines(&JoinLines, window, cx);
3973 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3974
3975 editor
3976 });
3977}
3978
3979#[gpui::test]
3980fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3981 init_test(cx, |_| {});
3982
3983 cx.add_window(|window, cx| {
3984 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3985 let mut editor = build_editor(buffer.clone(), window, cx);
3986 let buffer = buffer.read(cx).as_singleton().unwrap();
3987
3988 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3989 s.select_ranges([
3990 Point::new(0, 2)..Point::new(1, 1),
3991 Point::new(1, 2)..Point::new(1, 2),
3992 Point::new(3, 1)..Point::new(3, 2),
3993 ])
3994 });
3995
3996 editor.join_lines(&JoinLines, window, cx);
3997 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3998
3999 assert_eq!(
4000 editor.selections.ranges::<Point>(cx),
4001 [
4002 Point::new(0, 7)..Point::new(0, 7),
4003 Point::new(1, 3)..Point::new(1, 3)
4004 ]
4005 );
4006 editor
4007 });
4008}
4009
4010#[gpui::test]
4011async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4012 init_test(cx, |_| {});
4013
4014 let mut cx = EditorTestContext::new(cx).await;
4015
4016 let diff_base = r#"
4017 Line 0
4018 Line 1
4019 Line 2
4020 Line 3
4021 "#
4022 .unindent();
4023
4024 cx.set_state(
4025 &r#"
4026 ˇLine 0
4027 Line 1
4028 Line 2
4029 Line 3
4030 "#
4031 .unindent(),
4032 );
4033
4034 cx.set_head_text(&diff_base);
4035 executor.run_until_parked();
4036
4037 // Join lines
4038 cx.update_editor(|editor, window, cx| {
4039 editor.join_lines(&JoinLines, window, cx);
4040 });
4041 executor.run_until_parked();
4042
4043 cx.assert_editor_state(
4044 &r#"
4045 Line 0ˇ Line 1
4046 Line 2
4047 Line 3
4048 "#
4049 .unindent(),
4050 );
4051 // Join again
4052 cx.update_editor(|editor, window, cx| {
4053 editor.join_lines(&JoinLines, window, cx);
4054 });
4055 executor.run_until_parked();
4056
4057 cx.assert_editor_state(
4058 &r#"
4059 Line 0 Line 1ˇ Line 2
4060 Line 3
4061 "#
4062 .unindent(),
4063 );
4064}
4065
4066#[gpui::test]
4067async fn test_custom_newlines_cause_no_false_positive_diffs(
4068 executor: BackgroundExecutor,
4069 cx: &mut TestAppContext,
4070) {
4071 init_test(cx, |_| {});
4072 let mut cx = EditorTestContext::new(cx).await;
4073 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4074 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4075 executor.run_until_parked();
4076
4077 cx.update_editor(|editor, window, cx| {
4078 let snapshot = editor.snapshot(window, cx);
4079 assert_eq!(
4080 snapshot
4081 .buffer_snapshot
4082 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
4083 .collect::<Vec<_>>(),
4084 Vec::new(),
4085 "Should not have any diffs for files with custom newlines"
4086 );
4087 });
4088}
4089
4090#[gpui::test]
4091async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4092 init_test(cx, |_| {});
4093
4094 let mut cx = EditorTestContext::new(cx).await;
4095
4096 // Test sort_lines_case_insensitive()
4097 cx.set_state(indoc! {"
4098 «z
4099 y
4100 x
4101 Z
4102 Y
4103 Xˇ»
4104 "});
4105 cx.update_editor(|e, window, cx| {
4106 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4107 });
4108 cx.assert_editor_state(indoc! {"
4109 «x
4110 X
4111 y
4112 Y
4113 z
4114 Zˇ»
4115 "});
4116
4117 // Test sort_lines_by_length()
4118 //
4119 // Demonstrates:
4120 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4121 // - sort is stable
4122 cx.set_state(indoc! {"
4123 «123
4124 æ
4125 12
4126 ∞
4127 1
4128 æˇ»
4129 "});
4130 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4131 cx.assert_editor_state(indoc! {"
4132 «æ
4133 ∞
4134 1
4135 æ
4136 12
4137 123ˇ»
4138 "});
4139
4140 // Test reverse_lines()
4141 cx.set_state(indoc! {"
4142 «5
4143 4
4144 3
4145 2
4146 1ˇ»
4147 "});
4148 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4149 cx.assert_editor_state(indoc! {"
4150 «1
4151 2
4152 3
4153 4
4154 5ˇ»
4155 "});
4156
4157 // Skip testing shuffle_line()
4158
4159 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4160 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4161
4162 // Don't manipulate when cursor is on single line, but expand the selection
4163 cx.set_state(indoc! {"
4164 ddˇdd
4165 ccc
4166 bb
4167 a
4168 "});
4169 cx.update_editor(|e, window, cx| {
4170 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4171 });
4172 cx.assert_editor_state(indoc! {"
4173 «ddddˇ»
4174 ccc
4175 bb
4176 a
4177 "});
4178
4179 // Basic manipulate case
4180 // Start selection moves to column 0
4181 // End of selection shrinks to fit shorter line
4182 cx.set_state(indoc! {"
4183 dd«d
4184 ccc
4185 bb
4186 aaaaaˇ»
4187 "});
4188 cx.update_editor(|e, window, cx| {
4189 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4190 });
4191 cx.assert_editor_state(indoc! {"
4192 «aaaaa
4193 bb
4194 ccc
4195 dddˇ»
4196 "});
4197
4198 // Manipulate case with newlines
4199 cx.set_state(indoc! {"
4200 dd«d
4201 ccc
4202
4203 bb
4204 aaaaa
4205
4206 ˇ»
4207 "});
4208 cx.update_editor(|e, window, cx| {
4209 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4210 });
4211 cx.assert_editor_state(indoc! {"
4212 «
4213
4214 aaaaa
4215 bb
4216 ccc
4217 dddˇ»
4218
4219 "});
4220
4221 // Adding new line
4222 cx.set_state(indoc! {"
4223 aa«a
4224 bbˇ»b
4225 "});
4226 cx.update_editor(|e, window, cx| {
4227 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4228 });
4229 cx.assert_editor_state(indoc! {"
4230 «aaa
4231 bbb
4232 added_lineˇ»
4233 "});
4234
4235 // Removing line
4236 cx.set_state(indoc! {"
4237 aa«a
4238 bbbˇ»
4239 "});
4240 cx.update_editor(|e, window, cx| {
4241 e.manipulate_immutable_lines(window, cx, |lines| {
4242 lines.pop();
4243 })
4244 });
4245 cx.assert_editor_state(indoc! {"
4246 «aaaˇ»
4247 "});
4248
4249 // Removing all lines
4250 cx.set_state(indoc! {"
4251 aa«a
4252 bbbˇ»
4253 "});
4254 cx.update_editor(|e, window, cx| {
4255 e.manipulate_immutable_lines(window, cx, |lines| {
4256 lines.drain(..);
4257 })
4258 });
4259 cx.assert_editor_state(indoc! {"
4260 ˇ
4261 "});
4262}
4263
4264#[gpui::test]
4265async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4266 init_test(cx, |_| {});
4267
4268 let mut cx = EditorTestContext::new(cx).await;
4269
4270 // Consider continuous selection as single selection
4271 cx.set_state(indoc! {"
4272 Aaa«aa
4273 cˇ»c«c
4274 bb
4275 aaaˇ»aa
4276 "});
4277 cx.update_editor(|e, window, cx| {
4278 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4279 });
4280 cx.assert_editor_state(indoc! {"
4281 «Aaaaa
4282 ccc
4283 bb
4284 aaaaaˇ»
4285 "});
4286
4287 cx.set_state(indoc! {"
4288 Aaa«aa
4289 cˇ»c«c
4290 bb
4291 aaaˇ»aa
4292 "});
4293 cx.update_editor(|e, window, cx| {
4294 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4295 });
4296 cx.assert_editor_state(indoc! {"
4297 «Aaaaa
4298 ccc
4299 bbˇ»
4300 "});
4301
4302 // Consider non continuous selection as distinct dedup operations
4303 cx.set_state(indoc! {"
4304 «aaaaa
4305 bb
4306 aaaaa
4307 aaaaaˇ»
4308
4309 aaa«aaˇ»
4310 "});
4311 cx.update_editor(|e, window, cx| {
4312 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4313 });
4314 cx.assert_editor_state(indoc! {"
4315 «aaaaa
4316 bbˇ»
4317
4318 «aaaaaˇ»
4319 "});
4320}
4321
4322#[gpui::test]
4323async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4324 init_test(cx, |_| {});
4325
4326 let mut cx = EditorTestContext::new(cx).await;
4327
4328 cx.set_state(indoc! {"
4329 «Aaa
4330 aAa
4331 Aaaˇ»
4332 "});
4333 cx.update_editor(|e, window, cx| {
4334 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4335 });
4336 cx.assert_editor_state(indoc! {"
4337 «Aaa
4338 aAaˇ»
4339 "});
4340
4341 cx.set_state(indoc! {"
4342 «Aaa
4343 aAa
4344 aaAˇ»
4345 "});
4346 cx.update_editor(|e, window, cx| {
4347 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4348 });
4349 cx.assert_editor_state(indoc! {"
4350 «Aaaˇ»
4351 "});
4352}
4353
4354#[gpui::test]
4355async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4356 init_test(cx, |_| {});
4357
4358 let mut cx = EditorTestContext::new(cx).await;
4359
4360 // Manipulate with multiple selections on a single line
4361 cx.set_state(indoc! {"
4362 dd«dd
4363 cˇ»c«c
4364 bb
4365 aaaˇ»aa
4366 "});
4367 cx.update_editor(|e, window, cx| {
4368 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4369 });
4370 cx.assert_editor_state(indoc! {"
4371 «aaaaa
4372 bb
4373 ccc
4374 ddddˇ»
4375 "});
4376
4377 // Manipulate with multiple disjoin selections
4378 cx.set_state(indoc! {"
4379 5«
4380 4
4381 3
4382 2
4383 1ˇ»
4384
4385 dd«dd
4386 ccc
4387 bb
4388 aaaˇ»aa
4389 "});
4390 cx.update_editor(|e, window, cx| {
4391 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4392 });
4393 cx.assert_editor_state(indoc! {"
4394 «1
4395 2
4396 3
4397 4
4398 5ˇ»
4399
4400 «aaaaa
4401 bb
4402 ccc
4403 ddddˇ»
4404 "});
4405
4406 // Adding lines on each selection
4407 cx.set_state(indoc! {"
4408 2«
4409 1ˇ»
4410
4411 bb«bb
4412 aaaˇ»aa
4413 "});
4414 cx.update_editor(|e, window, cx| {
4415 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4416 });
4417 cx.assert_editor_state(indoc! {"
4418 «2
4419 1
4420 added lineˇ»
4421
4422 «bbbb
4423 aaaaa
4424 added lineˇ»
4425 "});
4426
4427 // Removing lines on each selection
4428 cx.set_state(indoc! {"
4429 2«
4430 1ˇ»
4431
4432 bb«bb
4433 aaaˇ»aa
4434 "});
4435 cx.update_editor(|e, window, cx| {
4436 e.manipulate_immutable_lines(window, cx, |lines| {
4437 lines.pop();
4438 })
4439 });
4440 cx.assert_editor_state(indoc! {"
4441 «2ˇ»
4442
4443 «bbbbˇ»
4444 "});
4445}
4446
4447#[gpui::test]
4448async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4449 init_test(cx, |settings| {
4450 settings.defaults.tab_size = NonZeroU32::new(3)
4451 });
4452
4453 let mut cx = EditorTestContext::new(cx).await;
4454
4455 // MULTI SELECTION
4456 // Ln.1 "«" tests empty lines
4457 // Ln.9 tests just leading whitespace
4458 cx.set_state(indoc! {"
4459 «
4460 abc // No indentationˇ»
4461 «\tabc // 1 tabˇ»
4462 \t\tabc « ˇ» // 2 tabs
4463 \t ab«c // Tab followed by space
4464 \tabc // Space followed by tab (3 spaces should be the result)
4465 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4466 abˇ»ˇc ˇ ˇ // Already space indented«
4467 \t
4468 \tabc\tdef // Only the leading tab is manipulatedˇ»
4469 "});
4470 cx.update_editor(|e, window, cx| {
4471 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4472 });
4473 cx.assert_editor_state(
4474 indoc! {"
4475 «
4476 abc // No indentation
4477 abc // 1 tab
4478 abc // 2 tabs
4479 abc // Tab followed by space
4480 abc // Space followed by tab (3 spaces should be the result)
4481 abc // Mixed indentation (tab conversion depends on the column)
4482 abc // Already space indented
4483 ·
4484 abc\tdef // Only the leading tab is manipulatedˇ»
4485 "}
4486 .replace("·", "")
4487 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4488 );
4489
4490 // Test on just a few lines, the others should remain unchanged
4491 // Only lines (3, 5, 10, 11) should change
4492 cx.set_state(
4493 indoc! {"
4494 ·
4495 abc // No indentation
4496 \tabcˇ // 1 tab
4497 \t\tabc // 2 tabs
4498 \t abcˇ // Tab followed by space
4499 \tabc // Space followed by tab (3 spaces should be the result)
4500 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4501 abc // Already space indented
4502 «\t
4503 \tabc\tdef // Only the leading tab is manipulatedˇ»
4504 "}
4505 .replace("·", "")
4506 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4507 );
4508 cx.update_editor(|e, window, cx| {
4509 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4510 });
4511 cx.assert_editor_state(
4512 indoc! {"
4513 ·
4514 abc // No indentation
4515 « abc // 1 tabˇ»
4516 \t\tabc // 2 tabs
4517 « abc // Tab followed by spaceˇ»
4518 \tabc // Space followed by tab (3 spaces should be the result)
4519 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4520 abc // Already space indented
4521 « ·
4522 abc\tdef // Only the leading tab is manipulatedˇ»
4523 "}
4524 .replace("·", "")
4525 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4526 );
4527
4528 // SINGLE SELECTION
4529 // Ln.1 "«" tests empty lines
4530 // Ln.9 tests just leading whitespace
4531 cx.set_state(indoc! {"
4532 «
4533 abc // No indentation
4534 \tabc // 1 tab
4535 \t\tabc // 2 tabs
4536 \t abc // Tab followed by space
4537 \tabc // Space followed by tab (3 spaces should be the result)
4538 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4539 abc // Already space indented
4540 \t
4541 \tabc\tdef // Only the leading tab is manipulatedˇ»
4542 "});
4543 cx.update_editor(|e, window, cx| {
4544 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4545 });
4546 cx.assert_editor_state(
4547 indoc! {"
4548 «
4549 abc // No indentation
4550 abc // 1 tab
4551 abc // 2 tabs
4552 abc // Tab followed by space
4553 abc // Space followed by tab (3 spaces should be the result)
4554 abc // Mixed indentation (tab conversion depends on the column)
4555 abc // Already space indented
4556 ·
4557 abc\tdef // Only the leading tab is manipulatedˇ»
4558 "}
4559 .replace("·", "")
4560 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4561 );
4562}
4563
4564#[gpui::test]
4565async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
4566 init_test(cx, |settings| {
4567 settings.defaults.tab_size = NonZeroU32::new(3)
4568 });
4569
4570 let mut cx = EditorTestContext::new(cx).await;
4571
4572 // MULTI SELECTION
4573 // Ln.1 "«" tests empty lines
4574 // Ln.11 tests just leading whitespace
4575 cx.set_state(indoc! {"
4576 «
4577 abˇ»ˇc // No indentation
4578 abc ˇ ˇ // 1 space (< 3 so dont convert)
4579 abc « // 2 spaces (< 3 so dont convert)
4580 abc // 3 spaces (convert)
4581 abc ˇ» // 5 spaces (1 tab + 2 spaces)
4582 «\tˇ»\t«\tˇ»abc // Already tab indented
4583 «\t abc // Tab followed by space
4584 \tabc // Space followed by tab (should be consumed due to tab)
4585 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4586 \tˇ» «\t
4587 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
4588 "});
4589 cx.update_editor(|e, window, cx| {
4590 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4591 });
4592 cx.assert_editor_state(indoc! {"
4593 «
4594 abc // No indentation
4595 abc // 1 space (< 3 so dont convert)
4596 abc // 2 spaces (< 3 so dont convert)
4597 \tabc // 3 spaces (convert)
4598 \t abc // 5 spaces (1 tab + 2 spaces)
4599 \t\t\tabc // Already tab indented
4600 \t abc // Tab followed by space
4601 \tabc // Space followed by tab (should be consumed due to tab)
4602 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4603 \t\t\t
4604 \tabc \t // Only the leading spaces should be convertedˇ»
4605 "});
4606
4607 // Test on just a few lines, the other should remain unchanged
4608 // Only lines (4, 8, 11, 12) should change
4609 cx.set_state(
4610 indoc! {"
4611 ·
4612 abc // No indentation
4613 abc // 1 space (< 3 so dont convert)
4614 abc // 2 spaces (< 3 so dont convert)
4615 « abc // 3 spaces (convert)ˇ»
4616 abc // 5 spaces (1 tab + 2 spaces)
4617 \t\t\tabc // Already tab indented
4618 \t abc // Tab followed by space
4619 \tabc ˇ // Space followed by tab (should be consumed due to tab)
4620 \t\t \tabc // Mixed indentation
4621 \t \t \t \tabc // Mixed indentation
4622 \t \tˇ
4623 « abc \t // Only the leading spaces should be convertedˇ»
4624 "}
4625 .replace("·", "")
4626 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4627 );
4628 cx.update_editor(|e, window, cx| {
4629 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4630 });
4631 cx.assert_editor_state(
4632 indoc! {"
4633 ·
4634 abc // No indentation
4635 abc // 1 space (< 3 so dont convert)
4636 abc // 2 spaces (< 3 so dont convert)
4637 «\tabc // 3 spaces (convert)ˇ»
4638 abc // 5 spaces (1 tab + 2 spaces)
4639 \t\t\tabc // Already tab indented
4640 \t abc // Tab followed by space
4641 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
4642 \t\t \tabc // Mixed indentation
4643 \t \t \t \tabc // Mixed indentation
4644 «\t\t\t
4645 \tabc \t // Only the leading spaces should be convertedˇ»
4646 "}
4647 .replace("·", "")
4648 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4649 );
4650
4651 // SINGLE SELECTION
4652 // Ln.1 "«" tests empty lines
4653 // Ln.11 tests just leading whitespace
4654 cx.set_state(indoc! {"
4655 «
4656 abc // No indentation
4657 abc // 1 space (< 3 so dont convert)
4658 abc // 2 spaces (< 3 so dont convert)
4659 abc // 3 spaces (convert)
4660 abc // 5 spaces (1 tab + 2 spaces)
4661 \t\t\tabc // Already tab indented
4662 \t abc // Tab followed by space
4663 \tabc // Space followed by tab (should be consumed due to tab)
4664 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4665 \t \t
4666 abc \t // Only the leading spaces should be convertedˇ»
4667 "});
4668 cx.update_editor(|e, window, cx| {
4669 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4670 });
4671 cx.assert_editor_state(indoc! {"
4672 «
4673 abc // No indentation
4674 abc // 1 space (< 3 so dont convert)
4675 abc // 2 spaces (< 3 so dont convert)
4676 \tabc // 3 spaces (convert)
4677 \t abc // 5 spaces (1 tab + 2 spaces)
4678 \t\t\tabc // Already tab indented
4679 \t abc // Tab followed by space
4680 \tabc // Space followed by tab (should be consumed due to tab)
4681 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4682 \t\t\t
4683 \tabc \t // Only the leading spaces should be convertedˇ»
4684 "});
4685}
4686
4687#[gpui::test]
4688async fn test_toggle_case(cx: &mut TestAppContext) {
4689 init_test(cx, |_| {});
4690
4691 let mut cx = EditorTestContext::new(cx).await;
4692
4693 // If all lower case -> upper case
4694 cx.set_state(indoc! {"
4695 «hello worldˇ»
4696 "});
4697 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4698 cx.assert_editor_state(indoc! {"
4699 «HELLO WORLDˇ»
4700 "});
4701
4702 // If all upper case -> lower case
4703 cx.set_state(indoc! {"
4704 «HELLO WORLDˇ»
4705 "});
4706 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4707 cx.assert_editor_state(indoc! {"
4708 «hello worldˇ»
4709 "});
4710
4711 // If any upper case characters are identified -> lower case
4712 // This matches JetBrains IDEs
4713 cx.set_state(indoc! {"
4714 «hEllo worldˇ»
4715 "});
4716 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4717 cx.assert_editor_state(indoc! {"
4718 «hello worldˇ»
4719 "});
4720}
4721
4722#[gpui::test]
4723async fn test_manipulate_text(cx: &mut TestAppContext) {
4724 init_test(cx, |_| {});
4725
4726 let mut cx = EditorTestContext::new(cx).await;
4727
4728 // Test convert_to_upper_case()
4729 cx.set_state(indoc! {"
4730 «hello worldˇ»
4731 "});
4732 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4733 cx.assert_editor_state(indoc! {"
4734 «HELLO WORLDˇ»
4735 "});
4736
4737 // Test convert_to_lower_case()
4738 cx.set_state(indoc! {"
4739 «HELLO WORLDˇ»
4740 "});
4741 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4742 cx.assert_editor_state(indoc! {"
4743 «hello worldˇ»
4744 "});
4745
4746 // Test multiple line, single selection case
4747 cx.set_state(indoc! {"
4748 «The quick brown
4749 fox jumps over
4750 the lazy dogˇ»
4751 "});
4752 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4753 cx.assert_editor_state(indoc! {"
4754 «The Quick Brown
4755 Fox Jumps Over
4756 The Lazy Dogˇ»
4757 "});
4758
4759 // Test multiple line, single selection case
4760 cx.set_state(indoc! {"
4761 «The quick brown
4762 fox jumps over
4763 the lazy dogˇ»
4764 "});
4765 cx.update_editor(|e, window, cx| {
4766 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4767 });
4768 cx.assert_editor_state(indoc! {"
4769 «TheQuickBrown
4770 FoxJumpsOver
4771 TheLazyDogˇ»
4772 "});
4773
4774 // From here on out, test more complex cases of manipulate_text()
4775
4776 // Test no selection case - should affect words cursors are in
4777 // Cursor at beginning, middle, and end of word
4778 cx.set_state(indoc! {"
4779 ˇhello big beauˇtiful worldˇ
4780 "});
4781 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4782 cx.assert_editor_state(indoc! {"
4783 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4784 "});
4785
4786 // Test multiple selections on a single line and across multiple lines
4787 cx.set_state(indoc! {"
4788 «Theˇ» quick «brown
4789 foxˇ» jumps «overˇ»
4790 the «lazyˇ» dog
4791 "});
4792 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4793 cx.assert_editor_state(indoc! {"
4794 «THEˇ» quick «BROWN
4795 FOXˇ» jumps «OVERˇ»
4796 the «LAZYˇ» dog
4797 "});
4798
4799 // Test case where text length grows
4800 cx.set_state(indoc! {"
4801 «tschüߡ»
4802 "});
4803 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4804 cx.assert_editor_state(indoc! {"
4805 «TSCHÜSSˇ»
4806 "});
4807
4808 // Test to make sure we don't crash when text shrinks
4809 cx.set_state(indoc! {"
4810 aaa_bbbˇ
4811 "});
4812 cx.update_editor(|e, window, cx| {
4813 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4814 });
4815 cx.assert_editor_state(indoc! {"
4816 «aaaBbbˇ»
4817 "});
4818
4819 // Test to make sure we all aware of the fact that each word can grow and shrink
4820 // Final selections should be aware of this fact
4821 cx.set_state(indoc! {"
4822 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4823 "});
4824 cx.update_editor(|e, window, cx| {
4825 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4826 });
4827 cx.assert_editor_state(indoc! {"
4828 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4829 "});
4830
4831 cx.set_state(indoc! {"
4832 «hElLo, WoRld!ˇ»
4833 "});
4834 cx.update_editor(|e, window, cx| {
4835 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4836 });
4837 cx.assert_editor_state(indoc! {"
4838 «HeLlO, wOrLD!ˇ»
4839 "});
4840}
4841
4842#[gpui::test]
4843fn test_duplicate_line(cx: &mut TestAppContext) {
4844 init_test(cx, |_| {});
4845
4846 let editor = cx.add_window(|window, cx| {
4847 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4848 build_editor(buffer, window, cx)
4849 });
4850 _ = editor.update(cx, |editor, window, cx| {
4851 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4852 s.select_display_ranges([
4853 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4854 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4855 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4856 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4857 ])
4858 });
4859 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4860 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4861 assert_eq!(
4862 editor.selections.display_ranges(cx),
4863 vec![
4864 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4865 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4866 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4867 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4868 ]
4869 );
4870 });
4871
4872 let editor = cx.add_window(|window, cx| {
4873 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4874 build_editor(buffer, window, cx)
4875 });
4876 _ = editor.update(cx, |editor, window, cx| {
4877 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4878 s.select_display_ranges([
4879 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4880 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4881 ])
4882 });
4883 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4884 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4885 assert_eq!(
4886 editor.selections.display_ranges(cx),
4887 vec![
4888 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4889 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4890 ]
4891 );
4892 });
4893
4894 // With `move_upwards` the selections stay in place, except for
4895 // the lines inserted above them
4896 let editor = cx.add_window(|window, cx| {
4897 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4898 build_editor(buffer, window, cx)
4899 });
4900 _ = editor.update(cx, |editor, window, cx| {
4901 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4902 s.select_display_ranges([
4903 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4904 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4905 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4906 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4907 ])
4908 });
4909 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4910 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4911 assert_eq!(
4912 editor.selections.display_ranges(cx),
4913 vec![
4914 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4915 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4916 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4917 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4918 ]
4919 );
4920 });
4921
4922 let editor = cx.add_window(|window, cx| {
4923 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4924 build_editor(buffer, window, cx)
4925 });
4926 _ = editor.update(cx, |editor, window, cx| {
4927 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4928 s.select_display_ranges([
4929 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4930 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4931 ])
4932 });
4933 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4934 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4935 assert_eq!(
4936 editor.selections.display_ranges(cx),
4937 vec![
4938 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4939 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
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_selection(&DuplicateSelection, window, cx);
4956 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\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(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4962 ]
4963 );
4964 });
4965}
4966
4967#[gpui::test]
4968fn test_move_line_up_down(cx: &mut TestAppContext) {
4969 init_test(cx, |_| {});
4970
4971 let editor = cx.add_window(|window, cx| {
4972 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4973 build_editor(buffer, window, cx)
4974 });
4975 _ = editor.update(cx, |editor, window, cx| {
4976 editor.fold_creases(
4977 vec![
4978 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4979 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4980 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4981 ],
4982 true,
4983 window,
4984 cx,
4985 );
4986 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4987 s.select_display_ranges([
4988 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4989 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4990 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4991 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4992 ])
4993 });
4994 assert_eq!(
4995 editor.display_text(cx),
4996 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4997 );
4998
4999 editor.move_line_up(&MoveLineUp, window, cx);
5000 assert_eq!(
5001 editor.display_text(cx),
5002 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5003 );
5004 assert_eq!(
5005 editor.selections.display_ranges(cx),
5006 vec![
5007 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5008 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5009 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5010 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5011 ]
5012 );
5013 });
5014
5015 _ = editor.update(cx, |editor, window, cx| {
5016 editor.move_line_down(&MoveLineDown, window, cx);
5017 assert_eq!(
5018 editor.display_text(cx),
5019 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5020 );
5021 assert_eq!(
5022 editor.selections.display_ranges(cx),
5023 vec![
5024 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5025 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5026 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5027 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5028 ]
5029 );
5030 });
5031
5032 _ = editor.update(cx, |editor, window, cx| {
5033 editor.move_line_down(&MoveLineDown, window, cx);
5034 assert_eq!(
5035 editor.display_text(cx),
5036 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5037 );
5038 assert_eq!(
5039 editor.selections.display_ranges(cx),
5040 vec![
5041 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5042 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5043 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5044 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5045 ]
5046 );
5047 });
5048
5049 _ = editor.update(cx, |editor, window, cx| {
5050 editor.move_line_up(&MoveLineUp, window, cx);
5051 assert_eq!(
5052 editor.display_text(cx),
5053 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5054 );
5055 assert_eq!(
5056 editor.selections.display_ranges(cx),
5057 vec![
5058 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5059 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5060 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5061 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5062 ]
5063 );
5064 });
5065}
5066
5067#[gpui::test]
5068fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5069 init_test(cx, |_| {});
5070
5071 let editor = cx.add_window(|window, cx| {
5072 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5073 build_editor(buffer, window, cx)
5074 });
5075 _ = editor.update(cx, |editor, window, cx| {
5076 let snapshot = editor.buffer.read(cx).snapshot(cx);
5077 editor.insert_blocks(
5078 [BlockProperties {
5079 style: BlockStyle::Fixed,
5080 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5081 height: Some(1),
5082 render: Arc::new(|_| div().into_any()),
5083 priority: 0,
5084 }],
5085 Some(Autoscroll::fit()),
5086 cx,
5087 );
5088 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5089 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5090 });
5091 editor.move_line_down(&MoveLineDown, window, cx);
5092 });
5093}
5094
5095#[gpui::test]
5096async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5097 init_test(cx, |_| {});
5098
5099 let mut cx = EditorTestContext::new(cx).await;
5100 cx.set_state(
5101 &"
5102 ˇzero
5103 one
5104 two
5105 three
5106 four
5107 five
5108 "
5109 .unindent(),
5110 );
5111
5112 // Create a four-line block that replaces three lines of text.
5113 cx.update_editor(|editor, window, cx| {
5114 let snapshot = editor.snapshot(window, cx);
5115 let snapshot = &snapshot.buffer_snapshot;
5116 let placement = BlockPlacement::Replace(
5117 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5118 );
5119 editor.insert_blocks(
5120 [BlockProperties {
5121 placement,
5122 height: Some(4),
5123 style: BlockStyle::Sticky,
5124 render: Arc::new(|_| gpui::div().into_any_element()),
5125 priority: 0,
5126 }],
5127 None,
5128 cx,
5129 );
5130 });
5131
5132 // Move down so that the cursor touches the block.
5133 cx.update_editor(|editor, window, cx| {
5134 editor.move_down(&Default::default(), window, cx);
5135 });
5136 cx.assert_editor_state(
5137 &"
5138 zero
5139 «one
5140 two
5141 threeˇ»
5142 four
5143 five
5144 "
5145 .unindent(),
5146 );
5147
5148 // Move down past the block.
5149 cx.update_editor(|editor, window, cx| {
5150 editor.move_down(&Default::default(), window, cx);
5151 });
5152 cx.assert_editor_state(
5153 &"
5154 zero
5155 one
5156 two
5157 three
5158 ˇfour
5159 five
5160 "
5161 .unindent(),
5162 );
5163}
5164
5165#[gpui::test]
5166fn test_transpose(cx: &mut TestAppContext) {
5167 init_test(cx, |_| {});
5168
5169 _ = cx.add_window(|window, cx| {
5170 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5171 editor.set_style(EditorStyle::default(), window, cx);
5172 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5173 s.select_ranges([1..1])
5174 });
5175 editor.transpose(&Default::default(), window, cx);
5176 assert_eq!(editor.text(cx), "bac");
5177 assert_eq!(editor.selections.ranges(cx), [2..2]);
5178
5179 editor.transpose(&Default::default(), window, cx);
5180 assert_eq!(editor.text(cx), "bca");
5181 assert_eq!(editor.selections.ranges(cx), [3..3]);
5182
5183 editor.transpose(&Default::default(), window, cx);
5184 assert_eq!(editor.text(cx), "bac");
5185 assert_eq!(editor.selections.ranges(cx), [3..3]);
5186
5187 editor
5188 });
5189
5190 _ = cx.add_window(|window, cx| {
5191 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5192 editor.set_style(EditorStyle::default(), window, cx);
5193 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5194 s.select_ranges([3..3])
5195 });
5196 editor.transpose(&Default::default(), window, cx);
5197 assert_eq!(editor.text(cx), "acb\nde");
5198 assert_eq!(editor.selections.ranges(cx), [3..3]);
5199
5200 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5201 s.select_ranges([4..4])
5202 });
5203 editor.transpose(&Default::default(), window, cx);
5204 assert_eq!(editor.text(cx), "acbd\ne");
5205 assert_eq!(editor.selections.ranges(cx), [5..5]);
5206
5207 editor.transpose(&Default::default(), window, cx);
5208 assert_eq!(editor.text(cx), "acbde\n");
5209 assert_eq!(editor.selections.ranges(cx), [6..6]);
5210
5211 editor.transpose(&Default::default(), window, cx);
5212 assert_eq!(editor.text(cx), "acbd\ne");
5213 assert_eq!(editor.selections.ranges(cx), [6..6]);
5214
5215 editor
5216 });
5217
5218 _ = cx.add_window(|window, cx| {
5219 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", 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, 2..2, 4..4])
5223 });
5224 editor.transpose(&Default::default(), window, cx);
5225 assert_eq!(editor.text(cx), "bacd\ne");
5226 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5227
5228 editor.transpose(&Default::default(), window, cx);
5229 assert_eq!(editor.text(cx), "bcade\n");
5230 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5231
5232 editor.transpose(&Default::default(), window, cx);
5233 assert_eq!(editor.text(cx), "bcda\ne");
5234 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5235
5236 editor.transpose(&Default::default(), window, cx);
5237 assert_eq!(editor.text(cx), "bcade\n");
5238 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5239
5240 editor.transpose(&Default::default(), window, cx);
5241 assert_eq!(editor.text(cx), "bcaed\n");
5242 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5243
5244 editor
5245 });
5246
5247 _ = cx.add_window(|window, cx| {
5248 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5249 editor.set_style(EditorStyle::default(), window, cx);
5250 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5251 s.select_ranges([4..4])
5252 });
5253 editor.transpose(&Default::default(), window, cx);
5254 assert_eq!(editor.text(cx), "🏀🍐✋");
5255 assert_eq!(editor.selections.ranges(cx), [8..8]);
5256
5257 editor.transpose(&Default::default(), window, cx);
5258 assert_eq!(editor.text(cx), "🏀✋🍐");
5259 assert_eq!(editor.selections.ranges(cx), [11..11]);
5260
5261 editor.transpose(&Default::default(), window, cx);
5262 assert_eq!(editor.text(cx), "🏀🍐✋");
5263 assert_eq!(editor.selections.ranges(cx), [11..11]);
5264
5265 editor
5266 });
5267}
5268
5269#[gpui::test]
5270async fn test_rewrap(cx: &mut TestAppContext) {
5271 init_test(cx, |settings| {
5272 settings.languages.0.extend([
5273 (
5274 "Markdown".into(),
5275 LanguageSettingsContent {
5276 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5277 preferred_line_length: Some(40),
5278 ..Default::default()
5279 },
5280 ),
5281 (
5282 "Plain Text".into(),
5283 LanguageSettingsContent {
5284 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5285 preferred_line_length: Some(40),
5286 ..Default::default()
5287 },
5288 ),
5289 (
5290 "C++".into(),
5291 LanguageSettingsContent {
5292 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5293 preferred_line_length: Some(40),
5294 ..Default::default()
5295 },
5296 ),
5297 (
5298 "Python".into(),
5299 LanguageSettingsContent {
5300 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5301 preferred_line_length: Some(40),
5302 ..Default::default()
5303 },
5304 ),
5305 (
5306 "Rust".into(),
5307 LanguageSettingsContent {
5308 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5309 preferred_line_length: Some(40),
5310 ..Default::default()
5311 },
5312 ),
5313 ])
5314 });
5315
5316 let mut cx = EditorTestContext::new(cx).await;
5317
5318 let cpp_language = Arc::new(Language::new(
5319 LanguageConfig {
5320 name: "C++".into(),
5321 line_comments: vec!["// ".into()],
5322 ..LanguageConfig::default()
5323 },
5324 None,
5325 ));
5326 let python_language = Arc::new(Language::new(
5327 LanguageConfig {
5328 name: "Python".into(),
5329 line_comments: vec!["# ".into()],
5330 ..LanguageConfig::default()
5331 },
5332 None,
5333 ));
5334 let markdown_language = Arc::new(Language::new(
5335 LanguageConfig {
5336 name: "Markdown".into(),
5337 rewrap_prefixes: vec![
5338 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5339 regex::Regex::new("[-*+]\\s+").unwrap(),
5340 ],
5341 ..LanguageConfig::default()
5342 },
5343 None,
5344 ));
5345 let rust_language = Arc::new(Language::new(
5346 LanguageConfig {
5347 name: "Rust".into(),
5348 line_comments: vec!["// ".into(), "/// ".into()],
5349 ..LanguageConfig::default()
5350 },
5351 Some(tree_sitter_rust::LANGUAGE.into()),
5352 ));
5353
5354 let plaintext_language = Arc::new(Language::new(
5355 LanguageConfig {
5356 name: "Plain Text".into(),
5357 ..LanguageConfig::default()
5358 },
5359 None,
5360 ));
5361
5362 // Test basic rewrapping of a long line with a cursor
5363 assert_rewrap(
5364 indoc! {"
5365 // ˇThis is a long comment that needs to be wrapped.
5366 "},
5367 indoc! {"
5368 // ˇThis is a long comment that needs to
5369 // be wrapped.
5370 "},
5371 cpp_language.clone(),
5372 &mut cx,
5373 );
5374
5375 // Test rewrapping a full selection
5376 assert_rewrap(
5377 indoc! {"
5378 «// This selected long comment needs to be wrapped.ˇ»"
5379 },
5380 indoc! {"
5381 «// This selected long comment needs to
5382 // be wrapped.ˇ»"
5383 },
5384 cpp_language.clone(),
5385 &mut cx,
5386 );
5387
5388 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5389 assert_rewrap(
5390 indoc! {"
5391 // ˇThis is the first line.
5392 // Thisˇ is the second line.
5393 // This is the thirdˇ line, all part of one paragraph.
5394 "},
5395 indoc! {"
5396 // ˇThis is the first line. Thisˇ is the
5397 // second line. This is the thirdˇ line,
5398 // all part of one paragraph.
5399 "},
5400 cpp_language.clone(),
5401 &mut cx,
5402 );
5403
5404 // Test multiple cursors in different paragraphs trigger separate rewraps
5405 assert_rewrap(
5406 indoc! {"
5407 // ˇThis is the first paragraph, first line.
5408 // ˇThis is the first paragraph, second line.
5409
5410 // ˇThis is the second paragraph, first line.
5411 // ˇThis is the second paragraph, second line.
5412 "},
5413 indoc! {"
5414 // ˇThis is the first paragraph, first
5415 // line. ˇThis is the first paragraph,
5416 // second line.
5417
5418 // ˇThis is the second paragraph, first
5419 // line. ˇThis is the second paragraph,
5420 // second line.
5421 "},
5422 cpp_language.clone(),
5423 &mut cx,
5424 );
5425
5426 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
5427 assert_rewrap(
5428 indoc! {"
5429 «// A regular long long comment to be wrapped.
5430 /// A documentation long comment to be wrapped.ˇ»
5431 "},
5432 indoc! {"
5433 «// A regular long long comment to be
5434 // wrapped.
5435 /// A documentation long comment to be
5436 /// wrapped.ˇ»
5437 "},
5438 rust_language.clone(),
5439 &mut cx,
5440 );
5441
5442 // Test that change in indentation level trigger seperate rewraps
5443 assert_rewrap(
5444 indoc! {"
5445 fn foo() {
5446 «// This is a long comment at the base indent.
5447 // This is a long comment at the next indent.ˇ»
5448 }
5449 "},
5450 indoc! {"
5451 fn foo() {
5452 «// This is a long comment at the
5453 // base indent.
5454 // This is a long comment at the
5455 // next indent.ˇ»
5456 }
5457 "},
5458 rust_language.clone(),
5459 &mut cx,
5460 );
5461
5462 // Test that different comment prefix characters (e.g., '#') are handled correctly
5463 assert_rewrap(
5464 indoc! {"
5465 # ˇThis is a long comment using a pound sign.
5466 "},
5467 indoc! {"
5468 # ˇThis is a long comment using a pound
5469 # sign.
5470 "},
5471 python_language.clone(),
5472 &mut cx,
5473 );
5474
5475 // Test rewrapping only affects comments, not code even when selected
5476 assert_rewrap(
5477 indoc! {"
5478 «/// This doc comment is long and should be wrapped.
5479 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5480 "},
5481 indoc! {"
5482 «/// This doc comment is long and should
5483 /// be wrapped.
5484 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5485 "},
5486 rust_language.clone(),
5487 &mut cx,
5488 );
5489
5490 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
5491 assert_rewrap(
5492 indoc! {"
5493 # Header
5494
5495 A long long long line of markdown text to wrap.ˇ
5496 "},
5497 indoc! {"
5498 # Header
5499
5500 A long long long line of markdown text
5501 to wrap.ˇ
5502 "},
5503 markdown_language.clone(),
5504 &mut cx,
5505 );
5506
5507 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
5508 assert_rewrap(
5509 indoc! {"
5510 «1. This is a numbered list item that is very long and needs to be wrapped properly.
5511 2. This is a numbered list item that is very long and needs to be wrapped properly.
5512 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
5513 "},
5514 indoc! {"
5515 «1. This is a numbered list item that is
5516 very long and needs to be wrapped
5517 properly.
5518 2. This is a numbered list item that is
5519 very long and needs to be wrapped
5520 properly.
5521 - This is an unordered list item that is
5522 also very long and should not merge
5523 with the numbered item.ˇ»
5524 "},
5525 markdown_language.clone(),
5526 &mut cx,
5527 );
5528
5529 // Test that rewrapping add indents for rewrapping boundary if not exists already.
5530 assert_rewrap(
5531 indoc! {"
5532 «1. This is a numbered list item that is
5533 very long and needs to be wrapped
5534 properly.
5535 2. This is a numbered list item that is
5536 very long and needs to be wrapped
5537 properly.
5538 - This is an unordered list item that is
5539 also very long and should not merge with
5540 the numbered item.ˇ»
5541 "},
5542 indoc! {"
5543 «1. This is a numbered list item that is
5544 very long and needs to be wrapped
5545 properly.
5546 2. This is a numbered list item that is
5547 very long and needs to be wrapped
5548 properly.
5549 - This is an unordered list item that is
5550 also very long and should not merge
5551 with the numbered item.ˇ»
5552 "},
5553 markdown_language.clone(),
5554 &mut cx,
5555 );
5556
5557 // Test that rewrapping maintain indents even when they already exists.
5558 assert_rewrap(
5559 indoc! {"
5560 «1. This is a numbered list
5561 item that is very long and needs to be wrapped properly.
5562 2. This is a numbered list
5563 item that is very long and needs to be wrapped properly.
5564 - This is an unordered list item that is also very long and
5565 should not merge with the numbered item.ˇ»
5566 "},
5567 indoc! {"
5568 «1. This is a numbered list item that is
5569 very long and needs to be wrapped
5570 properly.
5571 2. This is a numbered list item that is
5572 very long and needs to be wrapped
5573 properly.
5574 - This is an unordered list item that is
5575 also very long and should not merge
5576 with the numbered item.ˇ»
5577 "},
5578 markdown_language.clone(),
5579 &mut cx,
5580 );
5581
5582 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
5583 assert_rewrap(
5584 indoc! {"
5585 ˇThis is a very long line of plain text that will be wrapped.
5586 "},
5587 indoc! {"
5588 ˇThis is a very long line of plain text
5589 that will be wrapped.
5590 "},
5591 plaintext_language.clone(),
5592 &mut cx,
5593 );
5594
5595 // Test that non-commented code acts as a paragraph boundary within a selection
5596 assert_rewrap(
5597 indoc! {"
5598 «// This is the first long comment block to be wrapped.
5599 fn my_func(a: u32);
5600 // This is the second long comment block to be wrapped.ˇ»
5601 "},
5602 indoc! {"
5603 «// This is the first long comment block
5604 // to be wrapped.
5605 fn my_func(a: u32);
5606 // This is the second long comment block
5607 // to be wrapped.ˇ»
5608 "},
5609 rust_language.clone(),
5610 &mut cx,
5611 );
5612
5613 // Test rewrapping multiple selections, including ones with blank lines or tabs
5614 assert_rewrap(
5615 indoc! {"
5616 «ˇThis is a very long line that will be wrapped.
5617
5618 This is another paragraph in the same selection.»
5619
5620 «\tThis is a very long indented line that will be wrapped.ˇ»
5621 "},
5622 indoc! {"
5623 «ˇThis is a very long line that will be
5624 wrapped.
5625
5626 This is another paragraph in the same
5627 selection.»
5628
5629 «\tThis is a very long indented line
5630 \tthat will be wrapped.ˇ»
5631 "},
5632 plaintext_language.clone(),
5633 &mut cx,
5634 );
5635
5636 // Test that an empty comment line acts as a paragraph boundary
5637 assert_rewrap(
5638 indoc! {"
5639 // ˇThis is a long comment that will be wrapped.
5640 //
5641 // And this is another long comment that will also be wrapped.ˇ
5642 "},
5643 indoc! {"
5644 // ˇThis is a long comment that will be
5645 // wrapped.
5646 //
5647 // And this is another long comment that
5648 // will also be wrapped.ˇ
5649 "},
5650 cpp_language,
5651 &mut cx,
5652 );
5653
5654 #[track_caller]
5655 fn assert_rewrap(
5656 unwrapped_text: &str,
5657 wrapped_text: &str,
5658 language: Arc<Language>,
5659 cx: &mut EditorTestContext,
5660 ) {
5661 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5662 cx.set_state(unwrapped_text);
5663 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5664 cx.assert_editor_state(wrapped_text);
5665 }
5666}
5667
5668#[gpui::test]
5669async fn test_hard_wrap(cx: &mut TestAppContext) {
5670 init_test(cx, |_| {});
5671 let mut cx = EditorTestContext::new(cx).await;
5672
5673 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5674 cx.update_editor(|editor, _, cx| {
5675 editor.set_hard_wrap(Some(14), cx);
5676 });
5677
5678 cx.set_state(indoc!(
5679 "
5680 one two three ˇ
5681 "
5682 ));
5683 cx.simulate_input("four");
5684 cx.run_until_parked();
5685
5686 cx.assert_editor_state(indoc!(
5687 "
5688 one two three
5689 fourˇ
5690 "
5691 ));
5692
5693 cx.update_editor(|editor, window, cx| {
5694 editor.newline(&Default::default(), window, cx);
5695 });
5696 cx.run_until_parked();
5697 cx.assert_editor_state(indoc!(
5698 "
5699 one two three
5700 four
5701 ˇ
5702 "
5703 ));
5704
5705 cx.simulate_input("five");
5706 cx.run_until_parked();
5707 cx.assert_editor_state(indoc!(
5708 "
5709 one two three
5710 four
5711 fiveˇ
5712 "
5713 ));
5714
5715 cx.update_editor(|editor, window, cx| {
5716 editor.newline(&Default::default(), window, cx);
5717 });
5718 cx.run_until_parked();
5719 cx.simulate_input("# ");
5720 cx.run_until_parked();
5721 cx.assert_editor_state(indoc!(
5722 "
5723 one two three
5724 four
5725 five
5726 # ˇ
5727 "
5728 ));
5729
5730 cx.update_editor(|editor, window, cx| {
5731 editor.newline(&Default::default(), window, cx);
5732 });
5733 cx.run_until_parked();
5734 cx.assert_editor_state(indoc!(
5735 "
5736 one two three
5737 four
5738 five
5739 #\x20
5740 #ˇ
5741 "
5742 ));
5743
5744 cx.simulate_input(" 6");
5745 cx.run_until_parked();
5746 cx.assert_editor_state(indoc!(
5747 "
5748 one two three
5749 four
5750 five
5751 #
5752 # 6ˇ
5753 "
5754 ));
5755}
5756
5757#[gpui::test]
5758async fn test_clipboard(cx: &mut TestAppContext) {
5759 init_test(cx, |_| {});
5760
5761 let mut cx = EditorTestContext::new(cx).await;
5762
5763 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5764 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5765 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5766
5767 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5768 cx.set_state("two ˇfour ˇsix ˇ");
5769 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5770 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5771
5772 // Paste again but with only two cursors. Since the number of cursors doesn't
5773 // match the number of slices in the clipboard, the entire clipboard text
5774 // is pasted at each cursor.
5775 cx.set_state("ˇtwo one✅ four three six five ˇ");
5776 cx.update_editor(|e, window, cx| {
5777 e.handle_input("( ", window, cx);
5778 e.paste(&Paste, window, cx);
5779 e.handle_input(") ", window, cx);
5780 });
5781 cx.assert_editor_state(
5782 &([
5783 "( one✅ ",
5784 "three ",
5785 "five ) ˇtwo one✅ four three six five ( one✅ ",
5786 "three ",
5787 "five ) ˇ",
5788 ]
5789 .join("\n")),
5790 );
5791
5792 // Cut with three selections, one of which is full-line.
5793 cx.set_state(indoc! {"
5794 1«2ˇ»3
5795 4ˇ567
5796 «8ˇ»9"});
5797 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5798 cx.assert_editor_state(indoc! {"
5799 1ˇ3
5800 ˇ9"});
5801
5802 // Paste with three selections, noticing how the copied selection that was full-line
5803 // gets inserted before the second cursor.
5804 cx.set_state(indoc! {"
5805 1ˇ3
5806 9ˇ
5807 «oˇ»ne"});
5808 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5809 cx.assert_editor_state(indoc! {"
5810 12ˇ3
5811 4567
5812 9ˇ
5813 8ˇne"});
5814
5815 // Copy with a single cursor only, which writes the whole line into the clipboard.
5816 cx.set_state(indoc! {"
5817 The quick brown
5818 fox juˇmps over
5819 the lazy dog"});
5820 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5821 assert_eq!(
5822 cx.read_from_clipboard()
5823 .and_then(|item| item.text().as_deref().map(str::to_string)),
5824 Some("fox jumps over\n".to_string())
5825 );
5826
5827 // Paste with three selections, noticing how the copied full-line selection is inserted
5828 // before the empty selections but replaces the selection that is non-empty.
5829 cx.set_state(indoc! {"
5830 Tˇhe quick brown
5831 «foˇ»x jumps over
5832 tˇhe lazy dog"});
5833 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5834 cx.assert_editor_state(indoc! {"
5835 fox jumps over
5836 Tˇhe quick brown
5837 fox jumps over
5838 ˇx jumps over
5839 fox jumps over
5840 tˇhe lazy dog"});
5841}
5842
5843#[gpui::test]
5844async fn test_copy_trim(cx: &mut TestAppContext) {
5845 init_test(cx, |_| {});
5846
5847 let mut cx = EditorTestContext::new(cx).await;
5848 cx.set_state(
5849 r#" «for selection in selections.iter() {
5850 let mut start = selection.start;
5851 let mut end = selection.end;
5852 let is_entire_line = selection.is_empty();
5853 if is_entire_line {
5854 start = Point::new(start.row, 0);ˇ»
5855 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5856 }
5857 "#,
5858 );
5859 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5860 assert_eq!(
5861 cx.read_from_clipboard()
5862 .and_then(|item| item.text().as_deref().map(str::to_string)),
5863 Some(
5864 "for selection in selections.iter() {
5865 let mut start = selection.start;
5866 let mut end = selection.end;
5867 let is_entire_line = selection.is_empty();
5868 if is_entire_line {
5869 start = Point::new(start.row, 0);"
5870 .to_string()
5871 ),
5872 "Regular copying preserves all indentation selected",
5873 );
5874 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5875 assert_eq!(
5876 cx.read_from_clipboard()
5877 .and_then(|item| item.text().as_deref().map(str::to_string)),
5878 Some(
5879 "for selection in selections.iter() {
5880let mut start = selection.start;
5881let mut end = selection.end;
5882let is_entire_line = selection.is_empty();
5883if is_entire_line {
5884 start = Point::new(start.row, 0);"
5885 .to_string()
5886 ),
5887 "Copying with stripping should strip all leading whitespaces"
5888 );
5889
5890 cx.set_state(
5891 r#" « for selection in selections.iter() {
5892 let mut start = selection.start;
5893 let mut end = selection.end;
5894 let is_entire_line = selection.is_empty();
5895 if is_entire_line {
5896 start = Point::new(start.row, 0);ˇ»
5897 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5898 }
5899 "#,
5900 );
5901 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5902 assert_eq!(
5903 cx.read_from_clipboard()
5904 .and_then(|item| item.text().as_deref().map(str::to_string)),
5905 Some(
5906 " for selection in selections.iter() {
5907 let mut start = selection.start;
5908 let mut end = selection.end;
5909 let is_entire_line = selection.is_empty();
5910 if is_entire_line {
5911 start = Point::new(start.row, 0);"
5912 .to_string()
5913 ),
5914 "Regular copying preserves all indentation selected",
5915 );
5916 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5917 assert_eq!(
5918 cx.read_from_clipboard()
5919 .and_then(|item| item.text().as_deref().map(str::to_string)),
5920 Some(
5921 "for selection in selections.iter() {
5922let mut start = selection.start;
5923let mut end = selection.end;
5924let is_entire_line = selection.is_empty();
5925if is_entire_line {
5926 start = Point::new(start.row, 0);"
5927 .to_string()
5928 ),
5929 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5930 );
5931
5932 cx.set_state(
5933 r#" «ˇ for selection in selections.iter() {
5934 let mut start = selection.start;
5935 let mut end = selection.end;
5936 let is_entire_line = selection.is_empty();
5937 if is_entire_line {
5938 start = Point::new(start.row, 0);»
5939 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5940 }
5941 "#,
5942 );
5943 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5944 assert_eq!(
5945 cx.read_from_clipboard()
5946 .and_then(|item| item.text().as_deref().map(str::to_string)),
5947 Some(
5948 " for selection in selections.iter() {
5949 let mut start = selection.start;
5950 let mut end = selection.end;
5951 let is_entire_line = selection.is_empty();
5952 if is_entire_line {
5953 start = Point::new(start.row, 0);"
5954 .to_string()
5955 ),
5956 "Regular copying for reverse selection works the same",
5957 );
5958 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5959 assert_eq!(
5960 cx.read_from_clipboard()
5961 .and_then(|item| item.text().as_deref().map(str::to_string)),
5962 Some(
5963 "for selection in selections.iter() {
5964let mut start = selection.start;
5965let mut end = selection.end;
5966let is_entire_line = selection.is_empty();
5967if is_entire_line {
5968 start = Point::new(start.row, 0);"
5969 .to_string()
5970 ),
5971 "Copying with stripping for reverse selection works the same"
5972 );
5973
5974 cx.set_state(
5975 r#" for selection «in selections.iter() {
5976 let mut start = selection.start;
5977 let mut end = selection.end;
5978 let is_entire_line = selection.is_empty();
5979 if is_entire_line {
5980 start = Point::new(start.row, 0);ˇ»
5981 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5982 }
5983 "#,
5984 );
5985 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5986 assert_eq!(
5987 cx.read_from_clipboard()
5988 .and_then(|item| item.text().as_deref().map(str::to_string)),
5989 Some(
5990 "in selections.iter() {
5991 let mut start = selection.start;
5992 let mut end = selection.end;
5993 let is_entire_line = selection.is_empty();
5994 if is_entire_line {
5995 start = Point::new(start.row, 0);"
5996 .to_string()
5997 ),
5998 "When selecting past the indent, the copying works as usual",
5999 );
6000 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6001 assert_eq!(
6002 cx.read_from_clipboard()
6003 .and_then(|item| item.text().as_deref().map(str::to_string)),
6004 Some(
6005 "in selections.iter() {
6006 let mut start = selection.start;
6007 let mut end = selection.end;
6008 let is_entire_line = selection.is_empty();
6009 if is_entire_line {
6010 start = Point::new(start.row, 0);"
6011 .to_string()
6012 ),
6013 "When selecting past the indent, nothing is trimmed"
6014 );
6015
6016 cx.set_state(
6017 r#" «for selection in selections.iter() {
6018 let mut start = selection.start;
6019
6020 let mut end = selection.end;
6021 let is_entire_line = selection.is_empty();
6022 if is_entire_line {
6023 start = Point::new(start.row, 0);
6024ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
6025 }
6026 "#,
6027 );
6028 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6029 assert_eq!(
6030 cx.read_from_clipboard()
6031 .and_then(|item| item.text().as_deref().map(str::to_string)),
6032 Some(
6033 "for selection in selections.iter() {
6034let mut start = selection.start;
6035
6036let mut end = selection.end;
6037let is_entire_line = selection.is_empty();
6038if is_entire_line {
6039 start = Point::new(start.row, 0);
6040"
6041 .to_string()
6042 ),
6043 "Copying with stripping should ignore empty lines"
6044 );
6045}
6046
6047#[gpui::test]
6048async fn test_paste_multiline(cx: &mut TestAppContext) {
6049 init_test(cx, |_| {});
6050
6051 let mut cx = EditorTestContext::new(cx).await;
6052 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6053
6054 // Cut an indented block, without the leading whitespace.
6055 cx.set_state(indoc! {"
6056 const a: B = (
6057 c(),
6058 «d(
6059 e,
6060 f
6061 )ˇ»
6062 );
6063 "});
6064 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6065 cx.assert_editor_state(indoc! {"
6066 const a: B = (
6067 c(),
6068 ˇ
6069 );
6070 "});
6071
6072 // Paste it at the same position.
6073 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6074 cx.assert_editor_state(indoc! {"
6075 const a: B = (
6076 c(),
6077 d(
6078 e,
6079 f
6080 )ˇ
6081 );
6082 "});
6083
6084 // Paste it at a line with a lower indent level.
6085 cx.set_state(indoc! {"
6086 ˇ
6087 const a: B = (
6088 c(),
6089 );
6090 "});
6091 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6092 cx.assert_editor_state(indoc! {"
6093 d(
6094 e,
6095 f
6096 )ˇ
6097 const a: B = (
6098 c(),
6099 );
6100 "});
6101
6102 // Cut an indented block, with the leading whitespace.
6103 cx.set_state(indoc! {"
6104 const a: B = (
6105 c(),
6106 « d(
6107 e,
6108 f
6109 )
6110 ˇ»);
6111 "});
6112 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6113 cx.assert_editor_state(indoc! {"
6114 const a: B = (
6115 c(),
6116 ˇ);
6117 "});
6118
6119 // Paste it at the same position.
6120 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6121 cx.assert_editor_state(indoc! {"
6122 const a: B = (
6123 c(),
6124 d(
6125 e,
6126 f
6127 )
6128 ˇ);
6129 "});
6130
6131 // Paste it at a line with a higher indent level.
6132 cx.set_state(indoc! {"
6133 const a: B = (
6134 c(),
6135 d(
6136 e,
6137 fˇ
6138 )
6139 );
6140 "});
6141 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6142 cx.assert_editor_state(indoc! {"
6143 const a: B = (
6144 c(),
6145 d(
6146 e,
6147 f d(
6148 e,
6149 f
6150 )
6151 ˇ
6152 )
6153 );
6154 "});
6155
6156 // Copy an indented block, starting mid-line
6157 cx.set_state(indoc! {"
6158 const a: B = (
6159 c(),
6160 somethin«g(
6161 e,
6162 f
6163 )ˇ»
6164 );
6165 "});
6166 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6167
6168 // Paste it on a line with a lower indent level
6169 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
6170 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6171 cx.assert_editor_state(indoc! {"
6172 const a: B = (
6173 c(),
6174 something(
6175 e,
6176 f
6177 )
6178 );
6179 g(
6180 e,
6181 f
6182 )ˇ"});
6183}
6184
6185#[gpui::test]
6186async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
6187 init_test(cx, |_| {});
6188
6189 cx.write_to_clipboard(ClipboardItem::new_string(
6190 " d(\n e\n );\n".into(),
6191 ));
6192
6193 let mut cx = EditorTestContext::new(cx).await;
6194 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6195
6196 cx.set_state(indoc! {"
6197 fn a() {
6198 b();
6199 if c() {
6200 ˇ
6201 }
6202 }
6203 "});
6204
6205 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6206 cx.assert_editor_state(indoc! {"
6207 fn a() {
6208 b();
6209 if c() {
6210 d(
6211 e
6212 );
6213 ˇ
6214 }
6215 }
6216 "});
6217
6218 cx.set_state(indoc! {"
6219 fn a() {
6220 b();
6221 ˇ
6222 }
6223 "});
6224
6225 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6226 cx.assert_editor_state(indoc! {"
6227 fn a() {
6228 b();
6229 d(
6230 e
6231 );
6232 ˇ
6233 }
6234 "});
6235}
6236
6237#[gpui::test]
6238fn test_select_all(cx: &mut TestAppContext) {
6239 init_test(cx, |_| {});
6240
6241 let editor = cx.add_window(|window, cx| {
6242 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
6243 build_editor(buffer, window, cx)
6244 });
6245 _ = editor.update(cx, |editor, window, cx| {
6246 editor.select_all(&SelectAll, window, cx);
6247 assert_eq!(
6248 editor.selections.display_ranges(cx),
6249 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
6250 );
6251 });
6252}
6253
6254#[gpui::test]
6255fn test_select_line(cx: &mut TestAppContext) {
6256 init_test(cx, |_| {});
6257
6258 let editor = cx.add_window(|window, cx| {
6259 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
6260 build_editor(buffer, window, cx)
6261 });
6262 _ = editor.update(cx, |editor, window, cx| {
6263 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6264 s.select_display_ranges([
6265 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6266 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6267 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6268 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
6269 ])
6270 });
6271 editor.select_line(&SelectLine, window, cx);
6272 assert_eq!(
6273 editor.selections.display_ranges(cx),
6274 vec![
6275 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
6276 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
6277 ]
6278 );
6279 });
6280
6281 _ = editor.update(cx, |editor, window, cx| {
6282 editor.select_line(&SelectLine, window, cx);
6283 assert_eq!(
6284 editor.selections.display_ranges(cx),
6285 vec![
6286 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
6287 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
6288 ]
6289 );
6290 });
6291
6292 _ = editor.update(cx, |editor, window, cx| {
6293 editor.select_line(&SelectLine, window, cx);
6294 assert_eq!(
6295 editor.selections.display_ranges(cx),
6296 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
6297 );
6298 });
6299}
6300
6301#[gpui::test]
6302async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
6303 init_test(cx, |_| {});
6304 let mut cx = EditorTestContext::new(cx).await;
6305
6306 #[track_caller]
6307 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
6308 cx.set_state(initial_state);
6309 cx.update_editor(|e, window, cx| {
6310 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
6311 });
6312 cx.assert_editor_state(expected_state);
6313 }
6314
6315 // Selection starts and ends at the middle of lines, left-to-right
6316 test(
6317 &mut cx,
6318 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
6319 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6320 );
6321 // Same thing, right-to-left
6322 test(
6323 &mut cx,
6324 "aa\nb«b\ncc\ndd\neˇ»e\nff",
6325 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6326 );
6327
6328 // Whole buffer, left-to-right, last line *doesn't* end with newline
6329 test(
6330 &mut cx,
6331 "«ˇaa\nbb\ncc\ndd\nee\nff»",
6332 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6333 );
6334 // Same thing, right-to-left
6335 test(
6336 &mut cx,
6337 "«aa\nbb\ncc\ndd\nee\nffˇ»",
6338 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6339 );
6340
6341 // Whole buffer, left-to-right, last line ends with newline
6342 test(
6343 &mut cx,
6344 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
6345 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6346 );
6347 // Same thing, right-to-left
6348 test(
6349 &mut cx,
6350 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
6351 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6352 );
6353
6354 // Starts at the end of a line, ends at the start of another
6355 test(
6356 &mut cx,
6357 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
6358 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
6359 );
6360}
6361
6362#[gpui::test]
6363async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
6364 init_test(cx, |_| {});
6365
6366 let editor = cx.add_window(|window, cx| {
6367 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
6368 build_editor(buffer, window, cx)
6369 });
6370
6371 // setup
6372 _ = editor.update(cx, |editor, window, cx| {
6373 editor.fold_creases(
6374 vec![
6375 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
6376 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
6377 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
6378 ],
6379 true,
6380 window,
6381 cx,
6382 );
6383 assert_eq!(
6384 editor.display_text(cx),
6385 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6386 );
6387 });
6388
6389 _ = editor.update(cx, |editor, window, cx| {
6390 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6391 s.select_display_ranges([
6392 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6393 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6394 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6395 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
6396 ])
6397 });
6398 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6399 assert_eq!(
6400 editor.display_text(cx),
6401 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6402 );
6403 });
6404 EditorTestContext::for_editor(editor, cx)
6405 .await
6406 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
6407
6408 _ = editor.update(cx, |editor, window, cx| {
6409 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6410 s.select_display_ranges([
6411 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
6412 ])
6413 });
6414 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6415 assert_eq!(
6416 editor.display_text(cx),
6417 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
6418 );
6419 assert_eq!(
6420 editor.selections.display_ranges(cx),
6421 [
6422 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
6423 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
6424 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
6425 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
6426 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
6427 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
6428 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
6429 ]
6430 );
6431 });
6432 EditorTestContext::for_editor(editor, cx)
6433 .await
6434 .assert_editor_state(
6435 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
6436 );
6437}
6438
6439#[gpui::test]
6440async fn test_add_selection_above_below(cx: &mut TestAppContext) {
6441 init_test(cx, |_| {});
6442
6443 let mut cx = EditorTestContext::new(cx).await;
6444
6445 cx.set_state(indoc!(
6446 r#"abc
6447 defˇghi
6448
6449 jk
6450 nlmo
6451 "#
6452 ));
6453
6454 cx.update_editor(|editor, window, cx| {
6455 editor.add_selection_above(&Default::default(), window, cx);
6456 });
6457
6458 cx.assert_editor_state(indoc!(
6459 r#"abcˇ
6460 defˇghi
6461
6462 jk
6463 nlmo
6464 "#
6465 ));
6466
6467 cx.update_editor(|editor, window, cx| {
6468 editor.add_selection_above(&Default::default(), window, cx);
6469 });
6470
6471 cx.assert_editor_state(indoc!(
6472 r#"abcˇ
6473 defˇghi
6474
6475 jk
6476 nlmo
6477 "#
6478 ));
6479
6480 cx.update_editor(|editor, window, cx| {
6481 editor.add_selection_below(&Default::default(), window, cx);
6482 });
6483
6484 cx.assert_editor_state(indoc!(
6485 r#"abc
6486 defˇghi
6487
6488 jk
6489 nlmo
6490 "#
6491 ));
6492
6493 cx.update_editor(|editor, window, cx| {
6494 editor.undo_selection(&Default::default(), window, cx);
6495 });
6496
6497 cx.assert_editor_state(indoc!(
6498 r#"abcˇ
6499 defˇghi
6500
6501 jk
6502 nlmo
6503 "#
6504 ));
6505
6506 cx.update_editor(|editor, window, cx| {
6507 editor.redo_selection(&Default::default(), window, cx);
6508 });
6509
6510 cx.assert_editor_state(indoc!(
6511 r#"abc
6512 defˇghi
6513
6514 jk
6515 nlmo
6516 "#
6517 ));
6518
6519 cx.update_editor(|editor, window, cx| {
6520 editor.add_selection_below(&Default::default(), window, cx);
6521 });
6522
6523 cx.assert_editor_state(indoc!(
6524 r#"abc
6525 defˇghi
6526 ˇ
6527 jk
6528 nlmo
6529 "#
6530 ));
6531
6532 cx.update_editor(|editor, window, cx| {
6533 editor.add_selection_below(&Default::default(), window, cx);
6534 });
6535
6536 cx.assert_editor_state(indoc!(
6537 r#"abc
6538 defˇghi
6539 ˇ
6540 jkˇ
6541 nlmo
6542 "#
6543 ));
6544
6545 cx.update_editor(|editor, window, cx| {
6546 editor.add_selection_below(&Default::default(), window, cx);
6547 });
6548
6549 cx.assert_editor_state(indoc!(
6550 r#"abc
6551 defˇghi
6552 ˇ
6553 jkˇ
6554 nlmˇo
6555 "#
6556 ));
6557
6558 cx.update_editor(|editor, window, cx| {
6559 editor.add_selection_below(&Default::default(), window, cx);
6560 });
6561
6562 cx.assert_editor_state(indoc!(
6563 r#"abc
6564 defˇghi
6565 ˇ
6566 jkˇ
6567 nlmˇo
6568 ˇ"#
6569 ));
6570
6571 // change selections
6572 cx.set_state(indoc!(
6573 r#"abc
6574 def«ˇg»hi
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«ˇg»hi
6588
6589 jk
6590 nlm«ˇo»
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«ˇg»hi
6601
6602 jk
6603 nlm«ˇo»
6604 "#
6605 ));
6606
6607 cx.update_editor(|editor, window, cx| {
6608 editor.add_selection_above(&Default::default(), window, cx);
6609 });
6610
6611 cx.assert_editor_state(indoc!(
6612 r#"abc
6613 def«ˇg»hi
6614
6615 jk
6616 nlmo
6617 "#
6618 ));
6619
6620 cx.update_editor(|editor, window, cx| {
6621 editor.add_selection_above(&Default::default(), window, cx);
6622 });
6623
6624 cx.assert_editor_state(indoc!(
6625 r#"abc
6626 def«ˇg»hi
6627
6628 jk
6629 nlmo
6630 "#
6631 ));
6632
6633 // Change selections again
6634 cx.set_state(indoc!(
6635 r#"a«bc
6636 defgˇ»hi
6637
6638 jk
6639 nlmo
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#"a«bcˇ»
6649 d«efgˇ»hi
6650
6651 j«kˇ»
6652 nlmo
6653 "#
6654 ));
6655
6656 cx.update_editor(|editor, window, cx| {
6657 editor.add_selection_below(&Default::default(), window, cx);
6658 });
6659 cx.assert_editor_state(indoc!(
6660 r#"a«bcˇ»
6661 d«efgˇ»hi
6662
6663 j«kˇ»
6664 n«lmoˇ»
6665 "#
6666 ));
6667 cx.update_editor(|editor, window, cx| {
6668 editor.add_selection_above(&Default::default(), window, cx);
6669 });
6670
6671 cx.assert_editor_state(indoc!(
6672 r#"a«bcˇ»
6673 d«efgˇ»hi
6674
6675 j«kˇ»
6676 nlmo
6677 "#
6678 ));
6679
6680 // Change selections again
6681 cx.set_state(indoc!(
6682 r#"abc
6683 d«ˇefghi
6684
6685 jk
6686 nlm»o
6687 "#
6688 ));
6689
6690 cx.update_editor(|editor, window, cx| {
6691 editor.add_selection_above(&Default::default(), window, cx);
6692 });
6693
6694 cx.assert_editor_state(indoc!(
6695 r#"a«ˇbc»
6696 d«ˇef»ghi
6697
6698 j«ˇk»
6699 n«ˇlm»o
6700 "#
6701 ));
6702
6703 cx.update_editor(|editor, window, cx| {
6704 editor.add_selection_below(&Default::default(), window, cx);
6705 });
6706
6707 cx.assert_editor_state(indoc!(
6708 r#"abc
6709 d«ˇef»ghi
6710
6711 j«ˇk»
6712 n«ˇlm»o
6713 "#
6714 ));
6715}
6716
6717#[gpui::test]
6718async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
6719 init_test(cx, |_| {});
6720 let mut cx = EditorTestContext::new(cx).await;
6721
6722 cx.set_state(indoc!(
6723 r#"line onˇe
6724 liˇne two
6725 line three
6726 line four"#
6727 ));
6728
6729 cx.update_editor(|editor, window, cx| {
6730 editor.add_selection_below(&Default::default(), window, cx);
6731 });
6732
6733 // test multiple cursors expand in the same direction
6734 cx.assert_editor_state(indoc!(
6735 r#"line onˇe
6736 liˇne twˇo
6737 liˇne three
6738 line four"#
6739 ));
6740
6741 cx.update_editor(|editor, window, cx| {
6742 editor.add_selection_below(&Default::default(), window, cx);
6743 });
6744
6745 cx.update_editor(|editor, window, cx| {
6746 editor.add_selection_below(&Default::default(), window, cx);
6747 });
6748
6749 // test multiple cursors expand below overflow
6750 cx.assert_editor_state(indoc!(
6751 r#"line onˇe
6752 liˇne twˇo
6753 liˇne thˇree
6754 liˇne foˇur"#
6755 ));
6756
6757 cx.update_editor(|editor, window, cx| {
6758 editor.add_selection_above(&Default::default(), window, cx);
6759 });
6760
6761 // test multiple cursors retrieves back correctly
6762 cx.assert_editor_state(indoc!(
6763 r#"line onˇe
6764 liˇne twˇo
6765 liˇne thˇree
6766 line four"#
6767 ));
6768
6769 cx.update_editor(|editor, window, cx| {
6770 editor.add_selection_above(&Default::default(), window, cx);
6771 });
6772
6773 cx.update_editor(|editor, window, cx| {
6774 editor.add_selection_above(&Default::default(), window, cx);
6775 });
6776
6777 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
6778 cx.assert_editor_state(indoc!(
6779 r#"liˇne onˇe
6780 liˇne two
6781 line three
6782 line four"#
6783 ));
6784
6785 cx.update_editor(|editor, window, cx| {
6786 editor.undo_selection(&Default::default(), window, cx);
6787 });
6788
6789 // test undo
6790 cx.assert_editor_state(indoc!(
6791 r#"line onˇe
6792 liˇne twˇo
6793 line three
6794 line four"#
6795 ));
6796
6797 cx.update_editor(|editor, window, cx| {
6798 editor.redo_selection(&Default::default(), window, cx);
6799 });
6800
6801 // test redo
6802 cx.assert_editor_state(indoc!(
6803 r#"liˇne onˇe
6804 liˇne two
6805 line three
6806 line four"#
6807 ));
6808
6809 cx.set_state(indoc!(
6810 r#"abcd
6811 ef«ghˇ»
6812 ijkl
6813 «mˇ»nop"#
6814 ));
6815
6816 cx.update_editor(|editor, window, cx| {
6817 editor.add_selection_above(&Default::default(), window, cx);
6818 });
6819
6820 // test multiple selections expand in the same direction
6821 cx.assert_editor_state(indoc!(
6822 r#"ab«cdˇ»
6823 ef«ghˇ»
6824 «iˇ»jkl
6825 «mˇ»nop"#
6826 ));
6827
6828 cx.update_editor(|editor, window, cx| {
6829 editor.add_selection_above(&Default::default(), window, cx);
6830 });
6831
6832 // test multiple selection upward overflow
6833 cx.assert_editor_state(indoc!(
6834 r#"ab«cdˇ»
6835 «eˇ»f«ghˇ»
6836 «iˇ»jkl
6837 «mˇ»nop"#
6838 ));
6839
6840 cx.update_editor(|editor, window, cx| {
6841 editor.add_selection_below(&Default::default(), window, cx);
6842 });
6843
6844 // test multiple selection retrieves back correctly
6845 cx.assert_editor_state(indoc!(
6846 r#"abcd
6847 ef«ghˇ»
6848 «iˇ»jkl
6849 «mˇ»nop"#
6850 ));
6851
6852 cx.update_editor(|editor, window, cx| {
6853 editor.add_selection_below(&Default::default(), window, cx);
6854 });
6855
6856 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
6857 cx.assert_editor_state(indoc!(
6858 r#"abcd
6859 ef«ghˇ»
6860 ij«klˇ»
6861 «mˇ»nop"#
6862 ));
6863
6864 cx.update_editor(|editor, window, cx| {
6865 editor.undo_selection(&Default::default(), window, cx);
6866 });
6867
6868 // test undo
6869 cx.assert_editor_state(indoc!(
6870 r#"abcd
6871 ef«ghˇ»
6872 «iˇ»jkl
6873 «mˇ»nop"#
6874 ));
6875
6876 cx.update_editor(|editor, window, cx| {
6877 editor.redo_selection(&Default::default(), window, cx);
6878 });
6879
6880 // test redo
6881 cx.assert_editor_state(indoc!(
6882 r#"abcd
6883 ef«ghˇ»
6884 ij«klˇ»
6885 «mˇ»nop"#
6886 ));
6887}
6888
6889#[gpui::test]
6890async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
6891 init_test(cx, |_| {});
6892 let mut cx = EditorTestContext::new(cx).await;
6893
6894 cx.set_state(indoc!(
6895 r#"line onˇe
6896 liˇne two
6897 line three
6898 line four"#
6899 ));
6900
6901 cx.update_editor(|editor, window, cx| {
6902 editor.add_selection_below(&Default::default(), window, cx);
6903 editor.add_selection_below(&Default::default(), window, cx);
6904 editor.add_selection_below(&Default::default(), window, cx);
6905 });
6906
6907 // initial state with two multi cursor groups
6908 cx.assert_editor_state(indoc!(
6909 r#"line onˇe
6910 liˇne twˇo
6911 liˇne thˇree
6912 liˇne foˇur"#
6913 ));
6914
6915 // add single cursor in middle - simulate opt click
6916 cx.update_editor(|editor, window, cx| {
6917 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
6918 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6919 editor.end_selection(window, cx);
6920 });
6921
6922 cx.assert_editor_state(indoc!(
6923 r#"line onˇe
6924 liˇne twˇo
6925 liˇneˇ thˇree
6926 liˇne foˇur"#
6927 ));
6928
6929 cx.update_editor(|editor, window, cx| {
6930 editor.add_selection_above(&Default::default(), window, cx);
6931 });
6932
6933 // test new added selection expands above and existing selection shrinks
6934 cx.assert_editor_state(indoc!(
6935 r#"line onˇe
6936 liˇneˇ twˇo
6937 liˇneˇ thˇree
6938 line four"#
6939 ));
6940
6941 cx.update_editor(|editor, window, cx| {
6942 editor.add_selection_above(&Default::default(), window, cx);
6943 });
6944
6945 // test new added selection expands above and existing selection shrinks
6946 cx.assert_editor_state(indoc!(
6947 r#"lineˇ onˇe
6948 liˇneˇ twˇo
6949 lineˇ three
6950 line four"#
6951 ));
6952
6953 // intial state with two selection groups
6954 cx.set_state(indoc!(
6955 r#"abcd
6956 ef«ghˇ»
6957 ijkl
6958 «mˇ»nop"#
6959 ));
6960
6961 cx.update_editor(|editor, window, cx| {
6962 editor.add_selection_above(&Default::default(), window, cx);
6963 editor.add_selection_above(&Default::default(), window, cx);
6964 });
6965
6966 cx.assert_editor_state(indoc!(
6967 r#"ab«cdˇ»
6968 «eˇ»f«ghˇ»
6969 «iˇ»jkl
6970 «mˇ»nop"#
6971 ));
6972
6973 // add single selection in middle - simulate opt drag
6974 cx.update_editor(|editor, window, cx| {
6975 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
6976 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6977 editor.update_selection(
6978 DisplayPoint::new(DisplayRow(2), 4),
6979 0,
6980 gpui::Point::<f32>::default(),
6981 window,
6982 cx,
6983 );
6984 editor.end_selection(window, cx);
6985 });
6986
6987 cx.assert_editor_state(indoc!(
6988 r#"ab«cdˇ»
6989 «eˇ»f«ghˇ»
6990 «iˇ»jk«lˇ»
6991 «mˇ»nop"#
6992 ));
6993
6994 cx.update_editor(|editor, window, cx| {
6995 editor.add_selection_below(&Default::default(), window, cx);
6996 });
6997
6998 // test new added selection expands below, others shrinks from above
6999 cx.assert_editor_state(indoc!(
7000 r#"abcd
7001 ef«ghˇ»
7002 «iˇ»jk«lˇ»
7003 «mˇ»no«pˇ»"#
7004 ));
7005}
7006
7007#[gpui::test]
7008async fn test_select_next(cx: &mut TestAppContext) {
7009 init_test(cx, |_| {});
7010
7011 let mut cx = EditorTestContext::new(cx).await;
7012 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7013
7014 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7015 .unwrap();
7016 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7017
7018 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7019 .unwrap();
7020 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7021
7022 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7023 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7024
7025 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7026 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7027
7028 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7029 .unwrap();
7030 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7031
7032 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7033 .unwrap();
7034 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7035
7036 // Test selection direction should be preserved
7037 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7038
7039 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7040 .unwrap();
7041 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
7042}
7043
7044#[gpui::test]
7045async fn test_select_all_matches(cx: &mut TestAppContext) {
7046 init_test(cx, |_| {});
7047
7048 let mut cx = EditorTestContext::new(cx).await;
7049
7050 // Test caret-only selections
7051 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7052 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7053 .unwrap();
7054 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7055
7056 // Test left-to-right selections
7057 cx.set_state("abc\n«abcˇ»\nabc");
7058 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7059 .unwrap();
7060 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
7061
7062 // Test right-to-left selections
7063 cx.set_state("abc\n«ˇabc»\nabc");
7064 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7065 .unwrap();
7066 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
7067
7068 // Test selecting whitespace with caret selection
7069 cx.set_state("abc\nˇ abc\nabc");
7070 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7071 .unwrap();
7072 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7073
7074 // Test selecting whitespace with left-to-right selection
7075 cx.set_state("abc\n«ˇ »abc\nabc");
7076 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7077 .unwrap();
7078 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
7079
7080 // Test no matches with right-to-left selection
7081 cx.set_state("abc\n« ˇ»abc\nabc");
7082 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7083 .unwrap();
7084 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7085
7086 // Test with a single word and clip_at_line_ends=true (#29823)
7087 cx.set_state("aˇbc");
7088 cx.update_editor(|e, window, cx| {
7089 e.set_clip_at_line_ends(true, cx);
7090 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
7091 e.set_clip_at_line_ends(false, cx);
7092 });
7093 cx.assert_editor_state("«abcˇ»");
7094}
7095
7096#[gpui::test]
7097async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
7098 init_test(cx, |_| {});
7099
7100 let mut cx = EditorTestContext::new(cx).await;
7101
7102 let large_body_1 = "\nd".repeat(200);
7103 let large_body_2 = "\ne".repeat(200);
7104
7105 cx.set_state(&format!(
7106 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
7107 ));
7108 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
7109 let scroll_position = editor.scroll_position(cx);
7110 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
7111 scroll_position
7112 });
7113
7114 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7115 .unwrap();
7116 cx.assert_editor_state(&format!(
7117 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
7118 ));
7119 let scroll_position_after_selection =
7120 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
7121 assert_eq!(
7122 initial_scroll_position, scroll_position_after_selection,
7123 "Scroll position should not change after selecting all matches"
7124 );
7125}
7126
7127#[gpui::test]
7128async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
7129 init_test(cx, |_| {});
7130
7131 let mut cx = EditorLspTestContext::new_rust(
7132 lsp::ServerCapabilities {
7133 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7134 ..Default::default()
7135 },
7136 cx,
7137 )
7138 .await;
7139
7140 cx.set_state(indoc! {"
7141 line 1
7142 line 2
7143 linˇe 3
7144 line 4
7145 line 5
7146 "});
7147
7148 // Make an edit
7149 cx.update_editor(|editor, window, cx| {
7150 editor.handle_input("X", window, cx);
7151 });
7152
7153 // Move cursor to a different position
7154 cx.update_editor(|editor, window, cx| {
7155 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7156 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
7157 });
7158 });
7159
7160 cx.assert_editor_state(indoc! {"
7161 line 1
7162 line 2
7163 linXe 3
7164 line 4
7165 liˇne 5
7166 "});
7167
7168 cx.lsp
7169 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
7170 Ok(Some(vec![lsp::TextEdit::new(
7171 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
7172 "PREFIX ".to_string(),
7173 )]))
7174 });
7175
7176 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
7177 .unwrap()
7178 .await
7179 .unwrap();
7180
7181 cx.assert_editor_state(indoc! {"
7182 PREFIX line 1
7183 line 2
7184 linXe 3
7185 line 4
7186 liˇne 5
7187 "});
7188
7189 // Undo formatting
7190 cx.update_editor(|editor, window, cx| {
7191 editor.undo(&Default::default(), window, cx);
7192 });
7193
7194 // Verify cursor moved back to position after edit
7195 cx.assert_editor_state(indoc! {"
7196 line 1
7197 line 2
7198 linXˇe 3
7199 line 4
7200 line 5
7201 "});
7202}
7203
7204#[gpui::test]
7205async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) {
7206 init_test(cx, |_| {});
7207
7208 let mut cx = EditorTestContext::new(cx).await;
7209
7210 let provider = cx.new(|_| FakeInlineCompletionProvider::default());
7211 cx.update_editor(|editor, window, cx| {
7212 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
7213 });
7214
7215 cx.set_state(indoc! {"
7216 line 1
7217 line 2
7218 linˇe 3
7219 line 4
7220 line 5
7221 line 6
7222 line 7
7223 line 8
7224 line 9
7225 line 10
7226 "});
7227
7228 let snapshot = cx.buffer_snapshot();
7229 let edit_position = snapshot.anchor_after(Point::new(2, 4));
7230
7231 cx.update(|_, cx| {
7232 provider.update(cx, |provider, _| {
7233 provider.set_inline_completion(Some(inline_completion::InlineCompletion {
7234 id: None,
7235 edits: vec![(edit_position..edit_position, "X".into())],
7236 edit_preview: None,
7237 }))
7238 })
7239 });
7240
7241 cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
7242 cx.update_editor(|editor, window, cx| {
7243 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
7244 });
7245
7246 cx.assert_editor_state(indoc! {"
7247 line 1
7248 line 2
7249 lineXˇ 3
7250 line 4
7251 line 5
7252 line 6
7253 line 7
7254 line 8
7255 line 9
7256 line 10
7257 "});
7258
7259 cx.update_editor(|editor, window, cx| {
7260 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7261 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
7262 });
7263 });
7264
7265 cx.assert_editor_state(indoc! {"
7266 line 1
7267 line 2
7268 lineX 3
7269 line 4
7270 line 5
7271 line 6
7272 line 7
7273 line 8
7274 line 9
7275 liˇne 10
7276 "});
7277
7278 cx.update_editor(|editor, window, cx| {
7279 editor.undo(&Default::default(), window, cx);
7280 });
7281
7282 cx.assert_editor_state(indoc! {"
7283 line 1
7284 line 2
7285 lineˇ 3
7286 line 4
7287 line 5
7288 line 6
7289 line 7
7290 line 8
7291 line 9
7292 line 10
7293 "});
7294}
7295
7296#[gpui::test]
7297async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
7298 init_test(cx, |_| {});
7299
7300 let mut cx = EditorTestContext::new(cx).await;
7301 cx.set_state(
7302 r#"let foo = 2;
7303lˇet foo = 2;
7304let fooˇ = 2;
7305let foo = 2;
7306let foo = ˇ2;"#,
7307 );
7308
7309 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7310 .unwrap();
7311 cx.assert_editor_state(
7312 r#"let foo = 2;
7313«letˇ» foo = 2;
7314let «fooˇ» = 2;
7315let foo = 2;
7316let foo = «2ˇ»;"#,
7317 );
7318
7319 // noop for multiple selections with different contents
7320 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7321 .unwrap();
7322 cx.assert_editor_state(
7323 r#"let foo = 2;
7324«letˇ» foo = 2;
7325let «fooˇ» = 2;
7326let foo = 2;
7327let foo = «2ˇ»;"#,
7328 );
7329
7330 // Test last selection direction should be preserved
7331 cx.set_state(
7332 r#"let foo = 2;
7333let foo = 2;
7334let «fooˇ» = 2;
7335let «ˇfoo» = 2;
7336let foo = 2;"#,
7337 );
7338
7339 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7340 .unwrap();
7341 cx.assert_editor_state(
7342 r#"let foo = 2;
7343let foo = 2;
7344let «fooˇ» = 2;
7345let «ˇfoo» = 2;
7346let «ˇfoo» = 2;"#,
7347 );
7348}
7349
7350#[gpui::test]
7351async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
7352 init_test(cx, |_| {});
7353
7354 let mut cx =
7355 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
7356
7357 cx.assert_editor_state(indoc! {"
7358 ˇbbb
7359 ccc
7360
7361 bbb
7362 ccc
7363 "});
7364 cx.dispatch_action(SelectPrevious::default());
7365 cx.assert_editor_state(indoc! {"
7366 «bbbˇ»
7367 ccc
7368
7369 bbb
7370 ccc
7371 "});
7372 cx.dispatch_action(SelectPrevious::default());
7373 cx.assert_editor_state(indoc! {"
7374 «bbbˇ»
7375 ccc
7376
7377 «bbbˇ»
7378 ccc
7379 "});
7380}
7381
7382#[gpui::test]
7383async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
7384 init_test(cx, |_| {});
7385
7386 let mut cx = EditorTestContext::new(cx).await;
7387 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7388
7389 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7390 .unwrap();
7391 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7392
7393 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7394 .unwrap();
7395 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7396
7397 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7398 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7399
7400 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7401 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7402
7403 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7404 .unwrap();
7405 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
7406
7407 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7408 .unwrap();
7409 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7410}
7411
7412#[gpui::test]
7413async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
7414 init_test(cx, |_| {});
7415
7416 let mut cx = EditorTestContext::new(cx).await;
7417 cx.set_state("aˇ");
7418
7419 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7420 .unwrap();
7421 cx.assert_editor_state("«aˇ»");
7422 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7423 .unwrap();
7424 cx.assert_editor_state("«aˇ»");
7425}
7426
7427#[gpui::test]
7428async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
7429 init_test(cx, |_| {});
7430
7431 let mut cx = EditorTestContext::new(cx).await;
7432 cx.set_state(
7433 r#"let foo = 2;
7434lˇet foo = 2;
7435let fooˇ = 2;
7436let foo = 2;
7437let foo = ˇ2;"#,
7438 );
7439
7440 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7441 .unwrap();
7442 cx.assert_editor_state(
7443 r#"let foo = 2;
7444«letˇ» foo = 2;
7445let «fooˇ» = 2;
7446let foo = 2;
7447let foo = «2ˇ»;"#,
7448 );
7449
7450 // noop for multiple selections with different contents
7451 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7452 .unwrap();
7453 cx.assert_editor_state(
7454 r#"let foo = 2;
7455«letˇ» foo = 2;
7456let «fooˇ» = 2;
7457let foo = 2;
7458let foo = «2ˇ»;"#,
7459 );
7460}
7461
7462#[gpui::test]
7463async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
7464 init_test(cx, |_| {});
7465
7466 let mut cx = EditorTestContext::new(cx).await;
7467 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7468
7469 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7470 .unwrap();
7471 // selection direction is preserved
7472 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7473
7474 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7475 .unwrap();
7476 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7477
7478 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7479 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7480
7481 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7482 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7483
7484 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7485 .unwrap();
7486 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
7487
7488 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7489 .unwrap();
7490 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
7491}
7492
7493#[gpui::test]
7494async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
7495 init_test(cx, |_| {});
7496
7497 let language = Arc::new(Language::new(
7498 LanguageConfig::default(),
7499 Some(tree_sitter_rust::LANGUAGE.into()),
7500 ));
7501
7502 let text = r#"
7503 use mod1::mod2::{mod3, mod4};
7504
7505 fn fn_1(param1: bool, param2: &str) {
7506 let var1 = "text";
7507 }
7508 "#
7509 .unindent();
7510
7511 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7512 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7513 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7514
7515 editor
7516 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7517 .await;
7518
7519 editor.update_in(cx, |editor, window, cx| {
7520 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7521 s.select_display_ranges([
7522 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
7523 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
7524 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
7525 ]);
7526 });
7527 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7528 });
7529 editor.update(cx, |editor, cx| {
7530 assert_text_with_selections(
7531 editor,
7532 indoc! {r#"
7533 use mod1::mod2::{mod3, «mod4ˇ»};
7534
7535 fn fn_1«ˇ(param1: bool, param2: &str)» {
7536 let var1 = "«ˇtext»";
7537 }
7538 "#},
7539 cx,
7540 );
7541 });
7542
7543 editor.update_in(cx, |editor, window, cx| {
7544 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7545 });
7546 editor.update(cx, |editor, cx| {
7547 assert_text_with_selections(
7548 editor,
7549 indoc! {r#"
7550 use mod1::mod2::«{mod3, mod4}ˇ»;
7551
7552 «ˇfn fn_1(param1: bool, param2: &str) {
7553 let var1 = "text";
7554 }»
7555 "#},
7556 cx,
7557 );
7558 });
7559
7560 editor.update_in(cx, |editor, window, cx| {
7561 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7562 });
7563 assert_eq!(
7564 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7565 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7566 );
7567
7568 // Trying to expand the selected syntax node one more time has no effect.
7569 editor.update_in(cx, |editor, window, cx| {
7570 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7571 });
7572 assert_eq!(
7573 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7574 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7575 );
7576
7577 editor.update_in(cx, |editor, window, cx| {
7578 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7579 });
7580 editor.update(cx, |editor, cx| {
7581 assert_text_with_selections(
7582 editor,
7583 indoc! {r#"
7584 use mod1::mod2::«{mod3, mod4}ˇ»;
7585
7586 «ˇfn fn_1(param1: bool, param2: &str) {
7587 let var1 = "text";
7588 }»
7589 "#},
7590 cx,
7591 );
7592 });
7593
7594 editor.update_in(cx, |editor, window, cx| {
7595 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7596 });
7597 editor.update(cx, |editor, cx| {
7598 assert_text_with_selections(
7599 editor,
7600 indoc! {r#"
7601 use mod1::mod2::{mod3, «mod4ˇ»};
7602
7603 fn fn_1«ˇ(param1: bool, param2: &str)» {
7604 let var1 = "«ˇtext»";
7605 }
7606 "#},
7607 cx,
7608 );
7609 });
7610
7611 editor.update_in(cx, |editor, window, cx| {
7612 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7613 });
7614 editor.update(cx, |editor, cx| {
7615 assert_text_with_selections(
7616 editor,
7617 indoc! {r#"
7618 use mod1::mod2::{mod3, mo«ˇ»d4};
7619
7620 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7621 let var1 = "te«ˇ»xt";
7622 }
7623 "#},
7624 cx,
7625 );
7626 });
7627
7628 // Trying to shrink the selected syntax node one more time has no effect.
7629 editor.update_in(cx, |editor, window, cx| {
7630 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7631 });
7632 editor.update_in(cx, |editor, _, cx| {
7633 assert_text_with_selections(
7634 editor,
7635 indoc! {r#"
7636 use mod1::mod2::{mod3, mo«ˇ»d4};
7637
7638 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7639 let var1 = "te«ˇ»xt";
7640 }
7641 "#},
7642 cx,
7643 );
7644 });
7645
7646 // Ensure that we keep expanding the selection if the larger selection starts or ends within
7647 // a fold.
7648 editor.update_in(cx, |editor, window, cx| {
7649 editor.fold_creases(
7650 vec![
7651 Crease::simple(
7652 Point::new(0, 21)..Point::new(0, 24),
7653 FoldPlaceholder::test(),
7654 ),
7655 Crease::simple(
7656 Point::new(3, 20)..Point::new(3, 22),
7657 FoldPlaceholder::test(),
7658 ),
7659 ],
7660 true,
7661 window,
7662 cx,
7663 );
7664 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7665 });
7666 editor.update(cx, |editor, cx| {
7667 assert_text_with_selections(
7668 editor,
7669 indoc! {r#"
7670 use mod1::mod2::«{mod3, mod4}ˇ»;
7671
7672 fn fn_1«ˇ(param1: bool, param2: &str)» {
7673 let var1 = "«ˇtext»";
7674 }
7675 "#},
7676 cx,
7677 );
7678 });
7679}
7680
7681#[gpui::test]
7682async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
7683 init_test(cx, |_| {});
7684
7685 let language = Arc::new(Language::new(
7686 LanguageConfig::default(),
7687 Some(tree_sitter_rust::LANGUAGE.into()),
7688 ));
7689
7690 let text = "let a = 2;";
7691
7692 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7693 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7694 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7695
7696 editor
7697 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7698 .await;
7699
7700 // Test case 1: Cursor at end of word
7701 editor.update_in(cx, |editor, window, cx| {
7702 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7703 s.select_display_ranges([
7704 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
7705 ]);
7706 });
7707 });
7708 editor.update(cx, |editor, cx| {
7709 assert_text_with_selections(editor, "let aˇ = 2;", cx);
7710 });
7711 editor.update_in(cx, |editor, window, cx| {
7712 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7713 });
7714 editor.update(cx, |editor, cx| {
7715 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
7716 });
7717 editor.update_in(cx, |editor, window, cx| {
7718 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7719 });
7720 editor.update(cx, |editor, cx| {
7721 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7722 });
7723
7724 // Test case 2: Cursor at end of statement
7725 editor.update_in(cx, |editor, window, cx| {
7726 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7727 s.select_display_ranges([
7728 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
7729 ]);
7730 });
7731 });
7732 editor.update(cx, |editor, cx| {
7733 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
7734 });
7735 editor.update_in(cx, |editor, window, cx| {
7736 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7737 });
7738 editor.update(cx, |editor, cx| {
7739 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7740 });
7741}
7742
7743#[gpui::test]
7744async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
7745 init_test(cx, |_| {});
7746
7747 let language = Arc::new(Language::new(
7748 LanguageConfig::default(),
7749 Some(tree_sitter_rust::LANGUAGE.into()),
7750 ));
7751
7752 let text = r#"
7753 use mod1::mod2::{mod3, mod4};
7754
7755 fn fn_1(param1: bool, param2: &str) {
7756 let var1 = "hello world";
7757 }
7758 "#
7759 .unindent();
7760
7761 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7762 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7763 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7764
7765 editor
7766 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7767 .await;
7768
7769 // Test 1: Cursor on a letter of a string word
7770 editor.update_in(cx, |editor, window, cx| {
7771 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7772 s.select_display_ranges([
7773 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
7774 ]);
7775 });
7776 });
7777 editor.update_in(cx, |editor, window, cx| {
7778 assert_text_with_selections(
7779 editor,
7780 indoc! {r#"
7781 use mod1::mod2::{mod3, mod4};
7782
7783 fn fn_1(param1: bool, param2: &str) {
7784 let var1 = "hˇello world";
7785 }
7786 "#},
7787 cx,
7788 );
7789 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7790 assert_text_with_selections(
7791 editor,
7792 indoc! {r#"
7793 use mod1::mod2::{mod3, mod4};
7794
7795 fn fn_1(param1: bool, param2: &str) {
7796 let var1 = "«ˇhello» world";
7797 }
7798 "#},
7799 cx,
7800 );
7801 });
7802
7803 // Test 2: Partial selection within a word
7804 editor.update_in(cx, |editor, window, cx| {
7805 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7806 s.select_display_ranges([
7807 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7808 ]);
7809 });
7810 });
7811 editor.update_in(cx, |editor, window, cx| {
7812 assert_text_with_selections(
7813 editor,
7814 indoc! {r#"
7815 use mod1::mod2::{mod3, mod4};
7816
7817 fn fn_1(param1: bool, param2: &str) {
7818 let var1 = "h«elˇ»lo world";
7819 }
7820 "#},
7821 cx,
7822 );
7823 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7824 assert_text_with_selections(
7825 editor,
7826 indoc! {r#"
7827 use mod1::mod2::{mod3, mod4};
7828
7829 fn fn_1(param1: bool, param2: &str) {
7830 let var1 = "«ˇhello» world";
7831 }
7832 "#},
7833 cx,
7834 );
7835 });
7836
7837 // Test 3: Complete word already selected
7838 editor.update_in(cx, |editor, window, cx| {
7839 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7840 s.select_display_ranges([
7841 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7842 ]);
7843 });
7844 });
7845 editor.update_in(cx, |editor, window, cx| {
7846 assert_text_with_selections(
7847 editor,
7848 indoc! {r#"
7849 use mod1::mod2::{mod3, mod4};
7850
7851 fn fn_1(param1: bool, param2: &str) {
7852 let var1 = "«helloˇ» world";
7853 }
7854 "#},
7855 cx,
7856 );
7857 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7858 assert_text_with_selections(
7859 editor,
7860 indoc! {r#"
7861 use mod1::mod2::{mod3, mod4};
7862
7863 fn fn_1(param1: bool, param2: &str) {
7864 let var1 = "«hello worldˇ»";
7865 }
7866 "#},
7867 cx,
7868 );
7869 });
7870
7871 // Test 4: Selection spanning across words
7872 editor.update_in(cx, |editor, window, cx| {
7873 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7874 s.select_display_ranges([
7875 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7876 ]);
7877 });
7878 });
7879 editor.update_in(cx, |editor, window, cx| {
7880 assert_text_with_selections(
7881 editor,
7882 indoc! {r#"
7883 use mod1::mod2::{mod3, mod4};
7884
7885 fn fn_1(param1: bool, param2: &str) {
7886 let var1 = "hel«lo woˇ»rld";
7887 }
7888 "#},
7889 cx,
7890 );
7891 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7892 assert_text_with_selections(
7893 editor,
7894 indoc! {r#"
7895 use mod1::mod2::{mod3, mod4};
7896
7897 fn fn_1(param1: bool, param2: &str) {
7898 let var1 = "«ˇhello world»";
7899 }
7900 "#},
7901 cx,
7902 );
7903 });
7904
7905 // Test 5: Expansion beyond string
7906 editor.update_in(cx, |editor, window, cx| {
7907 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7908 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7909 assert_text_with_selections(
7910 editor,
7911 indoc! {r#"
7912 use mod1::mod2::{mod3, mod4};
7913
7914 fn fn_1(param1: bool, param2: &str) {
7915 «ˇlet var1 = "hello world";»
7916 }
7917 "#},
7918 cx,
7919 );
7920 });
7921}
7922
7923#[gpui::test]
7924async fn test_fold_function_bodies(cx: &mut TestAppContext) {
7925 init_test(cx, |_| {});
7926
7927 let base_text = r#"
7928 impl A {
7929 // this is an uncommitted comment
7930
7931 fn b() {
7932 c();
7933 }
7934
7935 // this is another uncommitted comment
7936
7937 fn d() {
7938 // e
7939 // f
7940 }
7941 }
7942
7943 fn g() {
7944 // h
7945 }
7946 "#
7947 .unindent();
7948
7949 let text = r#"
7950 ˇimpl A {
7951
7952 fn b() {
7953 c();
7954 }
7955
7956 fn d() {
7957 // e
7958 // f
7959 }
7960 }
7961
7962 fn g() {
7963 // h
7964 }
7965 "#
7966 .unindent();
7967
7968 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7969 cx.set_state(&text);
7970 cx.set_head_text(&base_text);
7971 cx.update_editor(|editor, window, cx| {
7972 editor.expand_all_diff_hunks(&Default::default(), window, cx);
7973 });
7974
7975 cx.assert_state_with_diff(
7976 "
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
7999 let expected_display_text = "
8000 impl A {
8001 // this is an uncommitted comment
8002
8003 fn b() {
8004 ⋯
8005 }
8006
8007 // this is another uncommitted comment
8008
8009 fn d() {
8010 ⋯
8011 }
8012 }
8013
8014 fn g() {
8015 ⋯
8016 }
8017 "
8018 .unindent();
8019
8020 cx.update_editor(|editor, window, cx| {
8021 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
8022 assert_eq!(editor.display_text(cx), expected_display_text);
8023 });
8024}
8025
8026#[gpui::test]
8027async fn test_autoindent(cx: &mut TestAppContext) {
8028 init_test(cx, |_| {});
8029
8030 let language = Arc::new(
8031 Language::new(
8032 LanguageConfig {
8033 brackets: BracketPairConfig {
8034 pairs: vec![
8035 BracketPair {
8036 start: "{".to_string(),
8037 end: "}".to_string(),
8038 close: false,
8039 surround: false,
8040 newline: true,
8041 },
8042 BracketPair {
8043 start: "(".to_string(),
8044 end: ")".to_string(),
8045 close: false,
8046 surround: false,
8047 newline: true,
8048 },
8049 ],
8050 ..Default::default()
8051 },
8052 ..Default::default()
8053 },
8054 Some(tree_sitter_rust::LANGUAGE.into()),
8055 )
8056 .with_indents_query(
8057 r#"
8058 (_ "(" ")" @end) @indent
8059 (_ "{" "}" @end) @indent
8060 "#,
8061 )
8062 .unwrap(),
8063 );
8064
8065 let text = "fn a() {}";
8066
8067 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8068 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8069 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8070 editor
8071 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8072 .await;
8073
8074 editor.update_in(cx, |editor, window, cx| {
8075 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8076 s.select_ranges([5..5, 8..8, 9..9])
8077 });
8078 editor.newline(&Newline, window, cx);
8079 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
8080 assert_eq!(
8081 editor.selections.ranges(cx),
8082 &[
8083 Point::new(1, 4)..Point::new(1, 4),
8084 Point::new(3, 4)..Point::new(3, 4),
8085 Point::new(5, 0)..Point::new(5, 0)
8086 ]
8087 );
8088 });
8089}
8090
8091#[gpui::test]
8092async fn test_autoindent_selections(cx: &mut TestAppContext) {
8093 init_test(cx, |_| {});
8094
8095 {
8096 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8097 cx.set_state(indoc! {"
8098 impl A {
8099
8100 fn b() {}
8101
8102 «fn c() {
8103
8104 }ˇ»
8105 }
8106 "});
8107
8108 cx.update_editor(|editor, window, cx| {
8109 editor.autoindent(&Default::default(), window, cx);
8110 });
8111
8112 cx.assert_editor_state(indoc! {"
8113 impl A {
8114
8115 fn b() {}
8116
8117 «fn c() {
8118
8119 }ˇ»
8120 }
8121 "});
8122 }
8123
8124 {
8125 let mut cx = EditorTestContext::new_multibuffer(
8126 cx,
8127 [indoc! { "
8128 impl A {
8129 «
8130 // a
8131 fn b(){}
8132 »
8133 «
8134 }
8135 fn c(){}
8136 »
8137 "}],
8138 );
8139
8140 let buffer = cx.update_editor(|editor, _, cx| {
8141 let buffer = editor.buffer().update(cx, |buffer, _| {
8142 buffer.all_buffers().iter().next().unwrap().clone()
8143 });
8144 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
8145 buffer
8146 });
8147
8148 cx.run_until_parked();
8149 cx.update_editor(|editor, window, cx| {
8150 editor.select_all(&Default::default(), window, cx);
8151 editor.autoindent(&Default::default(), window, cx)
8152 });
8153 cx.run_until_parked();
8154
8155 cx.update(|_, cx| {
8156 assert_eq!(
8157 buffer.read(cx).text(),
8158 indoc! { "
8159 impl A {
8160
8161 // a
8162 fn b(){}
8163
8164
8165 }
8166 fn c(){}
8167
8168 " }
8169 )
8170 });
8171 }
8172}
8173
8174#[gpui::test]
8175async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
8176 init_test(cx, |_| {});
8177
8178 let mut cx = EditorTestContext::new(cx).await;
8179
8180 let language = Arc::new(Language::new(
8181 LanguageConfig {
8182 brackets: BracketPairConfig {
8183 pairs: vec![
8184 BracketPair {
8185 start: "{".to_string(),
8186 end: "}".to_string(),
8187 close: true,
8188 surround: true,
8189 newline: true,
8190 },
8191 BracketPair {
8192 start: "(".to_string(),
8193 end: ")".to_string(),
8194 close: true,
8195 surround: true,
8196 newline: true,
8197 },
8198 BracketPair {
8199 start: "/*".to_string(),
8200 end: " */".to_string(),
8201 close: true,
8202 surround: true,
8203 newline: true,
8204 },
8205 BracketPair {
8206 start: "[".to_string(),
8207 end: "]".to_string(),
8208 close: false,
8209 surround: false,
8210 newline: true,
8211 },
8212 BracketPair {
8213 start: "\"".to_string(),
8214 end: "\"".to_string(),
8215 close: true,
8216 surround: true,
8217 newline: false,
8218 },
8219 BracketPair {
8220 start: "<".to_string(),
8221 end: ">".to_string(),
8222 close: false,
8223 surround: true,
8224 newline: true,
8225 },
8226 ],
8227 ..Default::default()
8228 },
8229 autoclose_before: "})]".to_string(),
8230 ..Default::default()
8231 },
8232 Some(tree_sitter_rust::LANGUAGE.into()),
8233 ));
8234
8235 cx.language_registry().add(language.clone());
8236 cx.update_buffer(|buffer, cx| {
8237 buffer.set_language(Some(language), cx);
8238 });
8239
8240 cx.set_state(
8241 &r#"
8242 🏀ˇ
8243 εˇ
8244 ❤️ˇ
8245 "#
8246 .unindent(),
8247 );
8248
8249 // autoclose multiple nested brackets at multiple cursors
8250 cx.update_editor(|editor, window, cx| {
8251 editor.handle_input("{", window, cx);
8252 editor.handle_input("{", window, cx);
8253 editor.handle_input("{", window, cx);
8254 });
8255 cx.assert_editor_state(
8256 &"
8257 🏀{{{ˇ}}}
8258 ε{{{ˇ}}}
8259 ❤️{{{ˇ}}}
8260 "
8261 .unindent(),
8262 );
8263
8264 // insert a different closing bracket
8265 cx.update_editor(|editor, window, cx| {
8266 editor.handle_input(")", window, cx);
8267 });
8268 cx.assert_editor_state(
8269 &"
8270 🏀{{{)ˇ}}}
8271 ε{{{)ˇ}}}
8272 ❤️{{{)ˇ}}}
8273 "
8274 .unindent(),
8275 );
8276
8277 // skip over the auto-closed brackets when typing a closing bracket
8278 cx.update_editor(|editor, window, cx| {
8279 editor.move_right(&MoveRight, window, cx);
8280 editor.handle_input("}", window, cx);
8281 editor.handle_input("}", window, cx);
8282 editor.handle_input("}", window, cx);
8283 });
8284 cx.assert_editor_state(
8285 &"
8286 🏀{{{)}}}}ˇ
8287 ε{{{)}}}}ˇ
8288 ❤️{{{)}}}}ˇ
8289 "
8290 .unindent(),
8291 );
8292
8293 // autoclose multi-character pairs
8294 cx.set_state(
8295 &"
8296 ˇ
8297 ˇ
8298 "
8299 .unindent(),
8300 );
8301 cx.update_editor(|editor, window, cx| {
8302 editor.handle_input("/", window, cx);
8303 editor.handle_input("*", window, cx);
8304 });
8305 cx.assert_editor_state(
8306 &"
8307 /*ˇ */
8308 /*ˇ */
8309 "
8310 .unindent(),
8311 );
8312
8313 // one cursor autocloses a multi-character pair, one cursor
8314 // does not autoclose.
8315 cx.set_state(
8316 &"
8317 /ˇ
8318 ˇ
8319 "
8320 .unindent(),
8321 );
8322 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
8323 cx.assert_editor_state(
8324 &"
8325 /*ˇ */
8326 *ˇ
8327 "
8328 .unindent(),
8329 );
8330
8331 // Don't autoclose if the next character isn't whitespace and isn't
8332 // listed in the language's "autoclose_before" section.
8333 cx.set_state("ˇa b");
8334 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8335 cx.assert_editor_state("{ˇa b");
8336
8337 // Don't autoclose if `close` is false for the bracket pair
8338 cx.set_state("ˇ");
8339 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
8340 cx.assert_editor_state("[ˇ");
8341
8342 // Surround with brackets if text is selected
8343 cx.set_state("«aˇ» b");
8344 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8345 cx.assert_editor_state("{«aˇ»} b");
8346
8347 // Autoclose when not immediately after a word character
8348 cx.set_state("a ˇ");
8349 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8350 cx.assert_editor_state("a \"ˇ\"");
8351
8352 // Autoclose pair where the start and end characters are the same
8353 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8354 cx.assert_editor_state("a \"\"ˇ");
8355
8356 // Don't autoclose when immediately after a word character
8357 cx.set_state("aˇ");
8358 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8359 cx.assert_editor_state("a\"ˇ");
8360
8361 // Do autoclose when after a non-word character
8362 cx.set_state("{ˇ");
8363 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8364 cx.assert_editor_state("{\"ˇ\"");
8365
8366 // Non identical pairs autoclose regardless of preceding character
8367 cx.set_state("aˇ");
8368 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8369 cx.assert_editor_state("a{ˇ}");
8370
8371 // Don't autoclose pair if autoclose is disabled
8372 cx.set_state("ˇ");
8373 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8374 cx.assert_editor_state("<ˇ");
8375
8376 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
8377 cx.set_state("«aˇ» b");
8378 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8379 cx.assert_editor_state("<«aˇ»> b");
8380}
8381
8382#[gpui::test]
8383async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
8384 init_test(cx, |settings| {
8385 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8386 });
8387
8388 let mut cx = EditorTestContext::new(cx).await;
8389
8390 let language = Arc::new(Language::new(
8391 LanguageConfig {
8392 brackets: BracketPairConfig {
8393 pairs: vec![
8394 BracketPair {
8395 start: "{".to_string(),
8396 end: "}".to_string(),
8397 close: true,
8398 surround: true,
8399 newline: true,
8400 },
8401 BracketPair {
8402 start: "(".to_string(),
8403 end: ")".to_string(),
8404 close: true,
8405 surround: true,
8406 newline: true,
8407 },
8408 BracketPair {
8409 start: "[".to_string(),
8410 end: "]".to_string(),
8411 close: false,
8412 surround: false,
8413 newline: true,
8414 },
8415 ],
8416 ..Default::default()
8417 },
8418 autoclose_before: "})]".to_string(),
8419 ..Default::default()
8420 },
8421 Some(tree_sitter_rust::LANGUAGE.into()),
8422 ));
8423
8424 cx.language_registry().add(language.clone());
8425 cx.update_buffer(|buffer, cx| {
8426 buffer.set_language(Some(language), cx);
8427 });
8428
8429 cx.set_state(
8430 &"
8431 ˇ
8432 ˇ
8433 ˇ
8434 "
8435 .unindent(),
8436 );
8437
8438 // ensure only matching closing brackets are skipped over
8439 cx.update_editor(|editor, window, cx| {
8440 editor.handle_input("}", window, cx);
8441 editor.move_left(&MoveLeft, window, cx);
8442 editor.handle_input(")", window, cx);
8443 editor.move_left(&MoveLeft, window, cx);
8444 });
8445 cx.assert_editor_state(
8446 &"
8447 ˇ)}
8448 ˇ)}
8449 ˇ)}
8450 "
8451 .unindent(),
8452 );
8453
8454 // skip-over closing brackets at multiple cursors
8455 cx.update_editor(|editor, window, cx| {
8456 editor.handle_input(")", window, cx);
8457 editor.handle_input("}", window, cx);
8458 });
8459 cx.assert_editor_state(
8460 &"
8461 )}ˇ
8462 )}ˇ
8463 )}ˇ
8464 "
8465 .unindent(),
8466 );
8467
8468 // ignore non-close brackets
8469 cx.update_editor(|editor, window, cx| {
8470 editor.handle_input("]", window, cx);
8471 editor.move_left(&MoveLeft, window, cx);
8472 editor.handle_input("]", window, cx);
8473 });
8474 cx.assert_editor_state(
8475 &"
8476 )}]ˇ]
8477 )}]ˇ]
8478 )}]ˇ]
8479 "
8480 .unindent(),
8481 );
8482}
8483
8484#[gpui::test]
8485async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8486 init_test(cx, |_| {});
8487
8488 let mut cx = EditorTestContext::new(cx).await;
8489
8490 let html_language = Arc::new(
8491 Language::new(
8492 LanguageConfig {
8493 name: "HTML".into(),
8494 brackets: BracketPairConfig {
8495 pairs: vec![
8496 BracketPair {
8497 start: "<".into(),
8498 end: ">".into(),
8499 close: true,
8500 ..Default::default()
8501 },
8502 BracketPair {
8503 start: "{".into(),
8504 end: "}".into(),
8505 close: true,
8506 ..Default::default()
8507 },
8508 BracketPair {
8509 start: "(".into(),
8510 end: ")".into(),
8511 close: true,
8512 ..Default::default()
8513 },
8514 ],
8515 ..Default::default()
8516 },
8517 autoclose_before: "})]>".into(),
8518 ..Default::default()
8519 },
8520 Some(tree_sitter_html::LANGUAGE.into()),
8521 )
8522 .with_injection_query(
8523 r#"
8524 (script_element
8525 (raw_text) @injection.content
8526 (#set! injection.language "javascript"))
8527 "#,
8528 )
8529 .unwrap(),
8530 );
8531
8532 let javascript_language = Arc::new(Language::new(
8533 LanguageConfig {
8534 name: "JavaScript".into(),
8535 brackets: BracketPairConfig {
8536 pairs: vec![
8537 BracketPair {
8538 start: "/*".into(),
8539 end: " */".into(),
8540 close: true,
8541 ..Default::default()
8542 },
8543 BracketPair {
8544 start: "{".into(),
8545 end: "}".into(),
8546 close: true,
8547 ..Default::default()
8548 },
8549 BracketPair {
8550 start: "(".into(),
8551 end: ")".into(),
8552 close: true,
8553 ..Default::default()
8554 },
8555 ],
8556 ..Default::default()
8557 },
8558 autoclose_before: "})]>".into(),
8559 ..Default::default()
8560 },
8561 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8562 ));
8563
8564 cx.language_registry().add(html_language.clone());
8565 cx.language_registry().add(javascript_language.clone());
8566
8567 cx.update_buffer(|buffer, cx| {
8568 buffer.set_language(Some(html_language), cx);
8569 });
8570
8571 cx.set_state(
8572 &r#"
8573 <body>ˇ
8574 <script>
8575 var x = 1;ˇ
8576 </script>
8577 </body>ˇ
8578 "#
8579 .unindent(),
8580 );
8581
8582 // Precondition: different languages are active at different locations.
8583 cx.update_editor(|editor, window, cx| {
8584 let snapshot = editor.snapshot(window, cx);
8585 let cursors = editor.selections.ranges::<usize>(cx);
8586 let languages = cursors
8587 .iter()
8588 .map(|c| snapshot.language_at(c.start).unwrap().name())
8589 .collect::<Vec<_>>();
8590 assert_eq!(
8591 languages,
8592 &["HTML".into(), "JavaScript".into(), "HTML".into()]
8593 );
8594 });
8595
8596 // Angle brackets autoclose in HTML, but not JavaScript.
8597 cx.update_editor(|editor, window, cx| {
8598 editor.handle_input("<", window, cx);
8599 editor.handle_input("a", window, cx);
8600 });
8601 cx.assert_editor_state(
8602 &r#"
8603 <body><aˇ>
8604 <script>
8605 var x = 1;<aˇ
8606 </script>
8607 </body><aˇ>
8608 "#
8609 .unindent(),
8610 );
8611
8612 // Curly braces and parens autoclose in both HTML and JavaScript.
8613 cx.update_editor(|editor, window, cx| {
8614 editor.handle_input(" b=", window, cx);
8615 editor.handle_input("{", window, cx);
8616 editor.handle_input("c", window, cx);
8617 editor.handle_input("(", window, cx);
8618 });
8619 cx.assert_editor_state(
8620 &r#"
8621 <body><a b={c(ˇ)}>
8622 <script>
8623 var x = 1;<a b={c(ˇ)}
8624 </script>
8625 </body><a b={c(ˇ)}>
8626 "#
8627 .unindent(),
8628 );
8629
8630 // Brackets that were already autoclosed are skipped.
8631 cx.update_editor(|editor, window, cx| {
8632 editor.handle_input(")", window, cx);
8633 editor.handle_input("d", window, cx);
8634 editor.handle_input("}", window, cx);
8635 });
8636 cx.assert_editor_state(
8637 &r#"
8638 <body><a b={c()d}ˇ>
8639 <script>
8640 var x = 1;<a b={c()d}ˇ
8641 </script>
8642 </body><a b={c()d}ˇ>
8643 "#
8644 .unindent(),
8645 );
8646 cx.update_editor(|editor, window, cx| {
8647 editor.handle_input(">", window, cx);
8648 });
8649 cx.assert_editor_state(
8650 &r#"
8651 <body><a b={c()d}>ˇ
8652 <script>
8653 var x = 1;<a b={c()d}>ˇ
8654 </script>
8655 </body><a b={c()d}>ˇ
8656 "#
8657 .unindent(),
8658 );
8659
8660 // Reset
8661 cx.set_state(
8662 &r#"
8663 <body>ˇ
8664 <script>
8665 var x = 1;ˇ
8666 </script>
8667 </body>ˇ
8668 "#
8669 .unindent(),
8670 );
8671
8672 cx.update_editor(|editor, window, cx| {
8673 editor.handle_input("<", window, cx);
8674 });
8675 cx.assert_editor_state(
8676 &r#"
8677 <body><ˇ>
8678 <script>
8679 var x = 1;<ˇ
8680 </script>
8681 </body><ˇ>
8682 "#
8683 .unindent(),
8684 );
8685
8686 // When backspacing, the closing angle brackets are removed.
8687 cx.update_editor(|editor, window, cx| {
8688 editor.backspace(&Backspace, window, cx);
8689 });
8690 cx.assert_editor_state(
8691 &r#"
8692 <body>ˇ
8693 <script>
8694 var x = 1;ˇ
8695 </script>
8696 </body>ˇ
8697 "#
8698 .unindent(),
8699 );
8700
8701 // Block comments autoclose in JavaScript, but not HTML.
8702 cx.update_editor(|editor, window, cx| {
8703 editor.handle_input("/", window, cx);
8704 editor.handle_input("*", window, cx);
8705 });
8706 cx.assert_editor_state(
8707 &r#"
8708 <body>/*ˇ
8709 <script>
8710 var x = 1;/*ˇ */
8711 </script>
8712 </body>/*ˇ
8713 "#
8714 .unindent(),
8715 );
8716}
8717
8718#[gpui::test]
8719async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
8720 init_test(cx, |_| {});
8721
8722 let mut cx = EditorTestContext::new(cx).await;
8723
8724 let rust_language = Arc::new(
8725 Language::new(
8726 LanguageConfig {
8727 name: "Rust".into(),
8728 brackets: serde_json::from_value(json!([
8729 { "start": "{", "end": "}", "close": true, "newline": true },
8730 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
8731 ]))
8732 .unwrap(),
8733 autoclose_before: "})]>".into(),
8734 ..Default::default()
8735 },
8736 Some(tree_sitter_rust::LANGUAGE.into()),
8737 )
8738 .with_override_query("(string_literal) @string")
8739 .unwrap(),
8740 );
8741
8742 cx.language_registry().add(rust_language.clone());
8743 cx.update_buffer(|buffer, cx| {
8744 buffer.set_language(Some(rust_language), cx);
8745 });
8746
8747 cx.set_state(
8748 &r#"
8749 let x = ˇ
8750 "#
8751 .unindent(),
8752 );
8753
8754 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
8755 cx.update_editor(|editor, window, cx| {
8756 editor.handle_input("\"", window, cx);
8757 });
8758 cx.assert_editor_state(
8759 &r#"
8760 let x = "ˇ"
8761 "#
8762 .unindent(),
8763 );
8764
8765 // Inserting another quotation mark. The cursor moves across the existing
8766 // automatically-inserted quotation mark.
8767 cx.update_editor(|editor, window, cx| {
8768 editor.handle_input("\"", window, cx);
8769 });
8770 cx.assert_editor_state(
8771 &r#"
8772 let x = ""ˇ
8773 "#
8774 .unindent(),
8775 );
8776
8777 // Reset
8778 cx.set_state(
8779 &r#"
8780 let x = ˇ
8781 "#
8782 .unindent(),
8783 );
8784
8785 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
8786 cx.update_editor(|editor, window, cx| {
8787 editor.handle_input("\"", window, cx);
8788 editor.handle_input(" ", window, cx);
8789 editor.move_left(&Default::default(), window, cx);
8790 editor.handle_input("\\", window, cx);
8791 editor.handle_input("\"", window, cx);
8792 });
8793 cx.assert_editor_state(
8794 &r#"
8795 let x = "\"ˇ "
8796 "#
8797 .unindent(),
8798 );
8799
8800 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8801 // mark. Nothing is inserted.
8802 cx.update_editor(|editor, window, cx| {
8803 editor.move_right(&Default::default(), window, cx);
8804 editor.handle_input("\"", window, cx);
8805 });
8806 cx.assert_editor_state(
8807 &r#"
8808 let x = "\" "ˇ
8809 "#
8810 .unindent(),
8811 );
8812}
8813
8814#[gpui::test]
8815async fn test_surround_with_pair(cx: &mut TestAppContext) {
8816 init_test(cx, |_| {});
8817
8818 let language = Arc::new(Language::new(
8819 LanguageConfig {
8820 brackets: BracketPairConfig {
8821 pairs: vec![
8822 BracketPair {
8823 start: "{".to_string(),
8824 end: "}".to_string(),
8825 close: true,
8826 surround: true,
8827 newline: true,
8828 },
8829 BracketPair {
8830 start: "/* ".to_string(),
8831 end: "*/".to_string(),
8832 close: true,
8833 surround: true,
8834 ..Default::default()
8835 },
8836 ],
8837 ..Default::default()
8838 },
8839 ..Default::default()
8840 },
8841 Some(tree_sitter_rust::LANGUAGE.into()),
8842 ));
8843
8844 let text = r#"
8845 a
8846 b
8847 c
8848 "#
8849 .unindent();
8850
8851 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8852 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8853 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8854 editor
8855 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8856 .await;
8857
8858 editor.update_in(cx, |editor, window, cx| {
8859 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8860 s.select_display_ranges([
8861 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8862 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8863 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8864 ])
8865 });
8866
8867 editor.handle_input("{", window, cx);
8868 editor.handle_input("{", window, cx);
8869 editor.handle_input("{", window, cx);
8870 assert_eq!(
8871 editor.text(cx),
8872 "
8873 {{{a}}}
8874 {{{b}}}
8875 {{{c}}}
8876 "
8877 .unindent()
8878 );
8879 assert_eq!(
8880 editor.selections.display_ranges(cx),
8881 [
8882 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
8883 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
8884 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
8885 ]
8886 );
8887
8888 editor.undo(&Undo, window, cx);
8889 editor.undo(&Undo, window, cx);
8890 editor.undo(&Undo, window, cx);
8891 assert_eq!(
8892 editor.text(cx),
8893 "
8894 a
8895 b
8896 c
8897 "
8898 .unindent()
8899 );
8900 assert_eq!(
8901 editor.selections.display_ranges(cx),
8902 [
8903 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8904 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8905 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8906 ]
8907 );
8908
8909 // Ensure inserting the first character of a multi-byte bracket pair
8910 // doesn't surround the selections with the bracket.
8911 editor.handle_input("/", window, cx);
8912 assert_eq!(
8913 editor.text(cx),
8914 "
8915 /
8916 /
8917 /
8918 "
8919 .unindent()
8920 );
8921 assert_eq!(
8922 editor.selections.display_ranges(cx),
8923 [
8924 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8925 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8926 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8927 ]
8928 );
8929
8930 editor.undo(&Undo, window, cx);
8931 assert_eq!(
8932 editor.text(cx),
8933 "
8934 a
8935 b
8936 c
8937 "
8938 .unindent()
8939 );
8940 assert_eq!(
8941 editor.selections.display_ranges(cx),
8942 [
8943 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8944 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8945 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8946 ]
8947 );
8948
8949 // Ensure inserting the last character of a multi-byte bracket pair
8950 // doesn't surround the selections with the bracket.
8951 editor.handle_input("*", window, cx);
8952 assert_eq!(
8953 editor.text(cx),
8954 "
8955 *
8956 *
8957 *
8958 "
8959 .unindent()
8960 );
8961 assert_eq!(
8962 editor.selections.display_ranges(cx),
8963 [
8964 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8965 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8966 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8967 ]
8968 );
8969 });
8970}
8971
8972#[gpui::test]
8973async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
8974 init_test(cx, |_| {});
8975
8976 let language = Arc::new(Language::new(
8977 LanguageConfig {
8978 brackets: BracketPairConfig {
8979 pairs: vec![BracketPair {
8980 start: "{".to_string(),
8981 end: "}".to_string(),
8982 close: true,
8983 surround: true,
8984 newline: true,
8985 }],
8986 ..Default::default()
8987 },
8988 autoclose_before: "}".to_string(),
8989 ..Default::default()
8990 },
8991 Some(tree_sitter_rust::LANGUAGE.into()),
8992 ));
8993
8994 let text = r#"
8995 a
8996 b
8997 c
8998 "#
8999 .unindent();
9000
9001 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9002 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9003 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9004 editor
9005 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9006 .await;
9007
9008 editor.update_in(cx, |editor, window, cx| {
9009 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9010 s.select_ranges([
9011 Point::new(0, 1)..Point::new(0, 1),
9012 Point::new(1, 1)..Point::new(1, 1),
9013 Point::new(2, 1)..Point::new(2, 1),
9014 ])
9015 });
9016
9017 editor.handle_input("{", window, cx);
9018 editor.handle_input("{", window, cx);
9019 editor.handle_input("_", window, cx);
9020 assert_eq!(
9021 editor.text(cx),
9022 "
9023 a{{_}}
9024 b{{_}}
9025 c{{_}}
9026 "
9027 .unindent()
9028 );
9029 assert_eq!(
9030 editor.selections.ranges::<Point>(cx),
9031 [
9032 Point::new(0, 4)..Point::new(0, 4),
9033 Point::new(1, 4)..Point::new(1, 4),
9034 Point::new(2, 4)..Point::new(2, 4)
9035 ]
9036 );
9037
9038 editor.backspace(&Default::default(), window, cx);
9039 editor.backspace(&Default::default(), window, cx);
9040 assert_eq!(
9041 editor.text(cx),
9042 "
9043 a{}
9044 b{}
9045 c{}
9046 "
9047 .unindent()
9048 );
9049 assert_eq!(
9050 editor.selections.ranges::<Point>(cx),
9051 [
9052 Point::new(0, 2)..Point::new(0, 2),
9053 Point::new(1, 2)..Point::new(1, 2),
9054 Point::new(2, 2)..Point::new(2, 2)
9055 ]
9056 );
9057
9058 editor.delete_to_previous_word_start(&Default::default(), window, cx);
9059 assert_eq!(
9060 editor.text(cx),
9061 "
9062 a
9063 b
9064 c
9065 "
9066 .unindent()
9067 );
9068 assert_eq!(
9069 editor.selections.ranges::<Point>(cx),
9070 [
9071 Point::new(0, 1)..Point::new(0, 1),
9072 Point::new(1, 1)..Point::new(1, 1),
9073 Point::new(2, 1)..Point::new(2, 1)
9074 ]
9075 );
9076 });
9077}
9078
9079#[gpui::test]
9080async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
9081 init_test(cx, |settings| {
9082 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9083 });
9084
9085 let mut cx = EditorTestContext::new(cx).await;
9086
9087 let language = Arc::new(Language::new(
9088 LanguageConfig {
9089 brackets: BracketPairConfig {
9090 pairs: vec![
9091 BracketPair {
9092 start: "{".to_string(),
9093 end: "}".to_string(),
9094 close: true,
9095 surround: true,
9096 newline: true,
9097 },
9098 BracketPair {
9099 start: "(".to_string(),
9100 end: ")".to_string(),
9101 close: true,
9102 surround: true,
9103 newline: true,
9104 },
9105 BracketPair {
9106 start: "[".to_string(),
9107 end: "]".to_string(),
9108 close: false,
9109 surround: true,
9110 newline: true,
9111 },
9112 ],
9113 ..Default::default()
9114 },
9115 autoclose_before: "})]".to_string(),
9116 ..Default::default()
9117 },
9118 Some(tree_sitter_rust::LANGUAGE.into()),
9119 ));
9120
9121 cx.language_registry().add(language.clone());
9122 cx.update_buffer(|buffer, cx| {
9123 buffer.set_language(Some(language), cx);
9124 });
9125
9126 cx.set_state(
9127 &"
9128 {(ˇ)}
9129 [[ˇ]]
9130 {(ˇ)}
9131 "
9132 .unindent(),
9133 );
9134
9135 cx.update_editor(|editor, window, cx| {
9136 editor.backspace(&Default::default(), window, cx);
9137 editor.backspace(&Default::default(), window, cx);
9138 });
9139
9140 cx.assert_editor_state(
9141 &"
9142 ˇ
9143 ˇ]]
9144 ˇ
9145 "
9146 .unindent(),
9147 );
9148
9149 cx.update_editor(|editor, window, cx| {
9150 editor.handle_input("{", window, cx);
9151 editor.handle_input("{", window, cx);
9152 editor.move_right(&MoveRight, window, cx);
9153 editor.move_right(&MoveRight, window, cx);
9154 editor.move_left(&MoveLeft, window, cx);
9155 editor.move_left(&MoveLeft, window, cx);
9156 editor.backspace(&Default::default(), window, cx);
9157 });
9158
9159 cx.assert_editor_state(
9160 &"
9161 {ˇ}
9162 {ˇ}]]
9163 {ˇ}
9164 "
9165 .unindent(),
9166 );
9167
9168 cx.update_editor(|editor, window, cx| {
9169 editor.backspace(&Default::default(), window, cx);
9170 });
9171
9172 cx.assert_editor_state(
9173 &"
9174 ˇ
9175 ˇ]]
9176 ˇ
9177 "
9178 .unindent(),
9179 );
9180}
9181
9182#[gpui::test]
9183async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
9184 init_test(cx, |_| {});
9185
9186 let language = Arc::new(Language::new(
9187 LanguageConfig::default(),
9188 Some(tree_sitter_rust::LANGUAGE.into()),
9189 ));
9190
9191 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
9192 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9193 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9194 editor
9195 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9196 .await;
9197
9198 editor.update_in(cx, |editor, window, cx| {
9199 editor.set_auto_replace_emoji_shortcode(true);
9200
9201 editor.handle_input("Hello ", window, cx);
9202 editor.handle_input(":wave", window, cx);
9203 assert_eq!(editor.text(cx), "Hello :wave".unindent());
9204
9205 editor.handle_input(":", window, cx);
9206 assert_eq!(editor.text(cx), "Hello 👋".unindent());
9207
9208 editor.handle_input(" :smile", window, cx);
9209 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
9210
9211 editor.handle_input(":", window, cx);
9212 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
9213
9214 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
9215 editor.handle_input(":wave", window, cx);
9216 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
9217
9218 editor.handle_input(":", window, cx);
9219 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
9220
9221 editor.handle_input(":1", window, cx);
9222 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
9223
9224 editor.handle_input(":", window, cx);
9225 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
9226
9227 // Ensure shortcode does not get replaced when it is part of a word
9228 editor.handle_input(" Test:wave", window, cx);
9229 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
9230
9231 editor.handle_input(":", window, cx);
9232 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
9233
9234 editor.set_auto_replace_emoji_shortcode(false);
9235
9236 // Ensure shortcode does not get replaced when auto replace is off
9237 editor.handle_input(" :wave", window, cx);
9238 assert_eq!(
9239 editor.text(cx),
9240 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
9241 );
9242
9243 editor.handle_input(":", window, cx);
9244 assert_eq!(
9245 editor.text(cx),
9246 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
9247 );
9248 });
9249}
9250
9251#[gpui::test]
9252async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
9253 init_test(cx, |_| {});
9254
9255 let (text, insertion_ranges) = marked_text_ranges(
9256 indoc! {"
9257 ˇ
9258 "},
9259 false,
9260 );
9261
9262 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
9263 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9264
9265 _ = editor.update_in(cx, |editor, window, cx| {
9266 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
9267
9268 editor
9269 .insert_snippet(&insertion_ranges, snippet, window, cx)
9270 .unwrap();
9271
9272 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
9273 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
9274 assert_eq!(editor.text(cx), expected_text);
9275 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
9276 }
9277
9278 assert(
9279 editor,
9280 cx,
9281 indoc! {"
9282 type «» =•
9283 "},
9284 );
9285
9286 assert!(editor.context_menu_visible(), "There should be a matches");
9287 });
9288}
9289
9290#[gpui::test]
9291async fn test_snippets(cx: &mut TestAppContext) {
9292 init_test(cx, |_| {});
9293
9294 let mut cx = EditorTestContext::new(cx).await;
9295
9296 cx.set_state(indoc! {"
9297 a.ˇ b
9298 a.ˇ b
9299 a.ˇ b
9300 "});
9301
9302 cx.update_editor(|editor, window, cx| {
9303 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
9304 let insertion_ranges = editor
9305 .selections
9306 .all(cx)
9307 .iter()
9308 .map(|s| s.range().clone())
9309 .collect::<Vec<_>>();
9310 editor
9311 .insert_snippet(&insertion_ranges, snippet, window, cx)
9312 .unwrap();
9313 });
9314
9315 cx.assert_editor_state(indoc! {"
9316 a.f(«oneˇ», two, «threeˇ») b
9317 a.f(«oneˇ», two, «threeˇ») b
9318 a.f(«oneˇ», two, «threeˇ») b
9319 "});
9320
9321 // Can't move earlier than the first tab stop
9322 cx.update_editor(|editor, window, cx| {
9323 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9324 });
9325 cx.assert_editor_state(indoc! {"
9326 a.f(«oneˇ», two, «threeˇ») b
9327 a.f(«oneˇ», two, «threeˇ») b
9328 a.f(«oneˇ», two, «threeˇ») b
9329 "});
9330
9331 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9332 cx.assert_editor_state(indoc! {"
9333 a.f(one, «twoˇ», three) b
9334 a.f(one, «twoˇ», three) b
9335 a.f(one, «twoˇ», three) b
9336 "});
9337
9338 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
9339 cx.assert_editor_state(indoc! {"
9340 a.f(«oneˇ», two, «threeˇ») b
9341 a.f(«oneˇ», two, «threeˇ») b
9342 a.f(«oneˇ», two, «threeˇ») b
9343 "});
9344
9345 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9346 cx.assert_editor_state(indoc! {"
9347 a.f(one, «twoˇ», three) b
9348 a.f(one, «twoˇ», three) b
9349 a.f(one, «twoˇ», three) b
9350 "});
9351 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9352 cx.assert_editor_state(indoc! {"
9353 a.f(one, two, three)ˇ b
9354 a.f(one, two, three)ˇ b
9355 a.f(one, two, three)ˇ b
9356 "});
9357
9358 // As soon as the last tab stop is reached, snippet state is gone
9359 cx.update_editor(|editor, window, cx| {
9360 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9361 });
9362 cx.assert_editor_state(indoc! {"
9363 a.f(one, two, three)ˇ b
9364 a.f(one, two, three)ˇ b
9365 a.f(one, two, three)ˇ b
9366 "});
9367}
9368
9369#[gpui::test]
9370async fn test_snippet_indentation(cx: &mut TestAppContext) {
9371 init_test(cx, |_| {});
9372
9373 let mut cx = EditorTestContext::new(cx).await;
9374
9375 cx.update_editor(|editor, window, cx| {
9376 let snippet = Snippet::parse(indoc! {"
9377 /*
9378 * Multiline comment with leading indentation
9379 *
9380 * $1
9381 */
9382 $0"})
9383 .unwrap();
9384 let insertion_ranges = editor
9385 .selections
9386 .all(cx)
9387 .iter()
9388 .map(|s| s.range().clone())
9389 .collect::<Vec<_>>();
9390 editor
9391 .insert_snippet(&insertion_ranges, snippet, window, cx)
9392 .unwrap();
9393 });
9394
9395 cx.assert_editor_state(indoc! {"
9396 /*
9397 * Multiline comment with leading indentation
9398 *
9399 * ˇ
9400 */
9401 "});
9402
9403 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9404 cx.assert_editor_state(indoc! {"
9405 /*
9406 * Multiline comment with leading indentation
9407 *
9408 *•
9409 */
9410 ˇ"});
9411}
9412
9413#[gpui::test]
9414async fn test_document_format_during_save(cx: &mut TestAppContext) {
9415 init_test(cx, |_| {});
9416
9417 let fs = FakeFs::new(cx.executor());
9418 fs.insert_file(path!("/file.rs"), Default::default()).await;
9419
9420 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
9421
9422 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9423 language_registry.add(rust_lang());
9424 let mut fake_servers = language_registry.register_fake_lsp(
9425 "Rust",
9426 FakeLspAdapter {
9427 capabilities: lsp::ServerCapabilities {
9428 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9429 ..Default::default()
9430 },
9431 ..Default::default()
9432 },
9433 );
9434
9435 let buffer = project
9436 .update(cx, |project, cx| {
9437 project.open_local_buffer(path!("/file.rs"), cx)
9438 })
9439 .await
9440 .unwrap();
9441
9442 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9443 let (editor, cx) = cx.add_window_view(|window, cx| {
9444 build_editor_with_project(project.clone(), buffer, window, cx)
9445 });
9446 editor.update_in(cx, |editor, window, cx| {
9447 editor.set_text("one\ntwo\nthree\n", window, cx)
9448 });
9449 assert!(cx.read(|cx| editor.is_dirty(cx)));
9450
9451 cx.executor().start_waiting();
9452 let fake_server = fake_servers.next().await.unwrap();
9453
9454 {
9455 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9456 move |params, _| async move {
9457 assert_eq!(
9458 params.text_document.uri,
9459 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9460 );
9461 assert_eq!(params.options.tab_size, 4);
9462 Ok(Some(vec![lsp::TextEdit::new(
9463 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9464 ", ".to_string(),
9465 )]))
9466 },
9467 );
9468 let save = editor
9469 .update_in(cx, |editor, window, cx| {
9470 editor.save(
9471 SaveOptions {
9472 format: true,
9473 autosave: false,
9474 },
9475 project.clone(),
9476 window,
9477 cx,
9478 )
9479 })
9480 .unwrap();
9481 cx.executor().start_waiting();
9482 save.await;
9483
9484 assert_eq!(
9485 editor.update(cx, |editor, cx| editor.text(cx)),
9486 "one, two\nthree\n"
9487 );
9488 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9489 }
9490
9491 {
9492 editor.update_in(cx, |editor, window, cx| {
9493 editor.set_text("one\ntwo\nthree\n", window, cx)
9494 });
9495 assert!(cx.read(|cx| editor.is_dirty(cx)));
9496
9497 // Ensure we can still save even if formatting hangs.
9498 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9499 move |params, _| async move {
9500 assert_eq!(
9501 params.text_document.uri,
9502 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9503 );
9504 futures::future::pending::<()>().await;
9505 unreachable!()
9506 },
9507 );
9508 let save = editor
9509 .update_in(cx, |editor, window, cx| {
9510 editor.save(
9511 SaveOptions {
9512 format: true,
9513 autosave: false,
9514 },
9515 project.clone(),
9516 window,
9517 cx,
9518 )
9519 })
9520 .unwrap();
9521 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9522 cx.executor().start_waiting();
9523 save.await;
9524 assert_eq!(
9525 editor.update(cx, |editor, cx| editor.text(cx)),
9526 "one\ntwo\nthree\n"
9527 );
9528 }
9529
9530 // Set rust language override and assert overridden tabsize is sent to language server
9531 update_test_language_settings(cx, |settings| {
9532 settings.languages.0.insert(
9533 "Rust".into(),
9534 LanguageSettingsContent {
9535 tab_size: NonZeroU32::new(8),
9536 ..Default::default()
9537 },
9538 );
9539 });
9540
9541 {
9542 editor.update_in(cx, |editor, window, cx| {
9543 editor.set_text("somehting_new\n", window, cx)
9544 });
9545 assert!(cx.read(|cx| editor.is_dirty(cx)));
9546 let _formatting_request_signal = fake_server
9547 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9548 assert_eq!(
9549 params.text_document.uri,
9550 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9551 );
9552 assert_eq!(params.options.tab_size, 8);
9553 Ok(Some(vec![]))
9554 });
9555 let save = editor
9556 .update_in(cx, |editor, window, cx| {
9557 editor.save(
9558 SaveOptions {
9559 format: true,
9560 autosave: false,
9561 },
9562 project.clone(),
9563 window,
9564 cx,
9565 )
9566 })
9567 .unwrap();
9568 cx.executor().start_waiting();
9569 save.await;
9570 }
9571}
9572
9573#[gpui::test]
9574async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9575 init_test(cx, |_| {});
9576
9577 let cols = 4;
9578 let rows = 10;
9579 let sample_text_1 = sample_text(rows, cols, 'a');
9580 assert_eq!(
9581 sample_text_1,
9582 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9583 );
9584 let sample_text_2 = sample_text(rows, cols, 'l');
9585 assert_eq!(
9586 sample_text_2,
9587 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9588 );
9589 let sample_text_3 = sample_text(rows, cols, 'v');
9590 assert_eq!(
9591 sample_text_3,
9592 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9593 );
9594
9595 let fs = FakeFs::new(cx.executor());
9596 fs.insert_tree(
9597 path!("/a"),
9598 json!({
9599 "main.rs": sample_text_1,
9600 "other.rs": sample_text_2,
9601 "lib.rs": sample_text_3,
9602 }),
9603 )
9604 .await;
9605
9606 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9607 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9608 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9609
9610 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9611 language_registry.add(rust_lang());
9612 let mut fake_servers = language_registry.register_fake_lsp(
9613 "Rust",
9614 FakeLspAdapter {
9615 capabilities: lsp::ServerCapabilities {
9616 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9617 ..Default::default()
9618 },
9619 ..Default::default()
9620 },
9621 );
9622
9623 let worktree = project.update(cx, |project, cx| {
9624 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
9625 assert_eq!(worktrees.len(), 1);
9626 worktrees.pop().unwrap()
9627 });
9628 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9629
9630 let buffer_1 = project
9631 .update(cx, |project, cx| {
9632 project.open_buffer((worktree_id, "main.rs"), cx)
9633 })
9634 .await
9635 .unwrap();
9636 let buffer_2 = project
9637 .update(cx, |project, cx| {
9638 project.open_buffer((worktree_id, "other.rs"), cx)
9639 })
9640 .await
9641 .unwrap();
9642 let buffer_3 = project
9643 .update(cx, |project, cx| {
9644 project.open_buffer((worktree_id, "lib.rs"), cx)
9645 })
9646 .await
9647 .unwrap();
9648
9649 let multi_buffer = cx.new(|cx| {
9650 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9651 multi_buffer.push_excerpts(
9652 buffer_1.clone(),
9653 [
9654 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9655 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9656 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9657 ],
9658 cx,
9659 );
9660 multi_buffer.push_excerpts(
9661 buffer_2.clone(),
9662 [
9663 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9664 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9665 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9666 ],
9667 cx,
9668 );
9669 multi_buffer.push_excerpts(
9670 buffer_3.clone(),
9671 [
9672 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9673 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9674 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9675 ],
9676 cx,
9677 );
9678 multi_buffer
9679 });
9680 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
9681 Editor::new(
9682 EditorMode::full(),
9683 multi_buffer,
9684 Some(project.clone()),
9685 window,
9686 cx,
9687 )
9688 });
9689
9690 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9691 editor.change_selections(
9692 SelectionEffects::scroll(Autoscroll::Next),
9693 window,
9694 cx,
9695 |s| s.select_ranges(Some(1..2)),
9696 );
9697 editor.insert("|one|two|three|", window, cx);
9698 });
9699 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9700 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9701 editor.change_selections(
9702 SelectionEffects::scroll(Autoscroll::Next),
9703 window,
9704 cx,
9705 |s| s.select_ranges(Some(60..70)),
9706 );
9707 editor.insert("|four|five|six|", window, cx);
9708 });
9709 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9710
9711 // First two buffers should be edited, but not the third one.
9712 assert_eq!(
9713 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9714 "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}",
9715 );
9716 buffer_1.update(cx, |buffer, _| {
9717 assert!(buffer.is_dirty());
9718 assert_eq!(
9719 buffer.text(),
9720 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
9721 )
9722 });
9723 buffer_2.update(cx, |buffer, _| {
9724 assert!(buffer.is_dirty());
9725 assert_eq!(
9726 buffer.text(),
9727 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
9728 )
9729 });
9730 buffer_3.update(cx, |buffer, _| {
9731 assert!(!buffer.is_dirty());
9732 assert_eq!(buffer.text(), sample_text_3,)
9733 });
9734 cx.executor().run_until_parked();
9735
9736 cx.executor().start_waiting();
9737 let save = multi_buffer_editor
9738 .update_in(cx, |editor, window, cx| {
9739 editor.save(
9740 SaveOptions {
9741 format: true,
9742 autosave: false,
9743 },
9744 project.clone(),
9745 window,
9746 cx,
9747 )
9748 })
9749 .unwrap();
9750
9751 let fake_server = fake_servers.next().await.unwrap();
9752 fake_server
9753 .server
9754 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
9755 Ok(Some(vec![lsp::TextEdit::new(
9756 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9757 format!("[{} formatted]", params.text_document.uri),
9758 )]))
9759 })
9760 .detach();
9761 save.await;
9762
9763 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
9764 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
9765 assert_eq!(
9766 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9767 uri!(
9768 "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}"
9769 ),
9770 );
9771 buffer_1.update(cx, |buffer, _| {
9772 assert!(!buffer.is_dirty());
9773 assert_eq!(
9774 buffer.text(),
9775 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
9776 )
9777 });
9778 buffer_2.update(cx, |buffer, _| {
9779 assert!(!buffer.is_dirty());
9780 assert_eq!(
9781 buffer.text(),
9782 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
9783 )
9784 });
9785 buffer_3.update(cx, |buffer, _| {
9786 assert!(!buffer.is_dirty());
9787 assert_eq!(buffer.text(), sample_text_3,)
9788 });
9789}
9790
9791#[gpui::test]
9792async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
9793 init_test(cx, |_| {});
9794
9795 let fs = FakeFs::new(cx.executor());
9796 fs.insert_tree(
9797 path!("/dir"),
9798 json!({
9799 "file1.rs": "fn main() { println!(\"hello\"); }",
9800 "file2.rs": "fn test() { println!(\"test\"); }",
9801 "file3.rs": "fn other() { println!(\"other\"); }\n",
9802 }),
9803 )
9804 .await;
9805
9806 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
9807 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9808 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9809
9810 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9811 language_registry.add(rust_lang());
9812
9813 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
9814 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9815
9816 // Open three buffers
9817 let buffer_1 = project
9818 .update(cx, |project, cx| {
9819 project.open_buffer((worktree_id, "file1.rs"), cx)
9820 })
9821 .await
9822 .unwrap();
9823 let buffer_2 = project
9824 .update(cx, |project, cx| {
9825 project.open_buffer((worktree_id, "file2.rs"), cx)
9826 })
9827 .await
9828 .unwrap();
9829 let buffer_3 = project
9830 .update(cx, |project, cx| {
9831 project.open_buffer((worktree_id, "file3.rs"), cx)
9832 })
9833 .await
9834 .unwrap();
9835
9836 // Create a multi-buffer with all three buffers
9837 let multi_buffer = cx.new(|cx| {
9838 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9839 multi_buffer.push_excerpts(
9840 buffer_1.clone(),
9841 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9842 cx,
9843 );
9844 multi_buffer.push_excerpts(
9845 buffer_2.clone(),
9846 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9847 cx,
9848 );
9849 multi_buffer.push_excerpts(
9850 buffer_3.clone(),
9851 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9852 cx,
9853 );
9854 multi_buffer
9855 });
9856
9857 let editor = cx.new_window_entity(|window, cx| {
9858 Editor::new(
9859 EditorMode::full(),
9860 multi_buffer,
9861 Some(project.clone()),
9862 window,
9863 cx,
9864 )
9865 });
9866
9867 // Edit only the first buffer
9868 editor.update_in(cx, |editor, window, cx| {
9869 editor.change_selections(
9870 SelectionEffects::scroll(Autoscroll::Next),
9871 window,
9872 cx,
9873 |s| s.select_ranges(Some(10..10)),
9874 );
9875 editor.insert("// edited", window, cx);
9876 });
9877
9878 // Verify that only buffer 1 is dirty
9879 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
9880 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9881 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9882
9883 // Get write counts after file creation (files were created with initial content)
9884 // We expect each file to have been written once during creation
9885 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
9886 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
9887 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
9888
9889 // Perform autosave
9890 let save_task = editor.update_in(cx, |editor, window, cx| {
9891 editor.save(
9892 SaveOptions {
9893 format: true,
9894 autosave: true,
9895 },
9896 project.clone(),
9897 window,
9898 cx,
9899 )
9900 });
9901 save_task.await.unwrap();
9902
9903 // Only the dirty buffer should have been saved
9904 assert_eq!(
9905 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9906 1,
9907 "Buffer 1 was dirty, so it should have been written once during autosave"
9908 );
9909 assert_eq!(
9910 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9911 0,
9912 "Buffer 2 was clean, so it should not have been written during autosave"
9913 );
9914 assert_eq!(
9915 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9916 0,
9917 "Buffer 3 was clean, so it should not have been written during autosave"
9918 );
9919
9920 // Verify buffer states after autosave
9921 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9922 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9923 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9924
9925 // Now perform a manual save (format = true)
9926 let save_task = editor.update_in(cx, |editor, window, cx| {
9927 editor.save(
9928 SaveOptions {
9929 format: true,
9930 autosave: false,
9931 },
9932 project.clone(),
9933 window,
9934 cx,
9935 )
9936 });
9937 save_task.await.unwrap();
9938
9939 // During manual save, clean buffers don't get written to disk
9940 // They just get did_save called for language server notifications
9941 assert_eq!(
9942 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9943 1,
9944 "Buffer 1 should only have been written once total (during autosave, not manual save)"
9945 );
9946 assert_eq!(
9947 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9948 0,
9949 "Buffer 2 should not have been written at all"
9950 );
9951 assert_eq!(
9952 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9953 0,
9954 "Buffer 3 should not have been written at all"
9955 );
9956}
9957
9958#[gpui::test]
9959async fn test_range_format_during_save(cx: &mut TestAppContext) {
9960 init_test(cx, |_| {});
9961
9962 let fs = FakeFs::new(cx.executor());
9963 fs.insert_file(path!("/file.rs"), Default::default()).await;
9964
9965 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9966
9967 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9968 language_registry.add(rust_lang());
9969 let mut fake_servers = language_registry.register_fake_lsp(
9970 "Rust",
9971 FakeLspAdapter {
9972 capabilities: lsp::ServerCapabilities {
9973 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
9974 ..Default::default()
9975 },
9976 ..Default::default()
9977 },
9978 );
9979
9980 let buffer = project
9981 .update(cx, |project, cx| {
9982 project.open_local_buffer(path!("/file.rs"), cx)
9983 })
9984 .await
9985 .unwrap();
9986
9987 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9988 let (editor, cx) = cx.add_window_view(|window, cx| {
9989 build_editor_with_project(project.clone(), buffer, window, cx)
9990 });
9991 editor.update_in(cx, |editor, window, cx| {
9992 editor.set_text("one\ntwo\nthree\n", window, cx)
9993 });
9994 assert!(cx.read(|cx| editor.is_dirty(cx)));
9995
9996 cx.executor().start_waiting();
9997 let fake_server = fake_servers.next().await.unwrap();
9998
9999 let save = editor
10000 .update_in(cx, |editor, window, cx| {
10001 editor.save(
10002 SaveOptions {
10003 format: true,
10004 autosave: false,
10005 },
10006 project.clone(),
10007 window,
10008 cx,
10009 )
10010 })
10011 .unwrap();
10012 fake_server
10013 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10014 assert_eq!(
10015 params.text_document.uri,
10016 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10017 );
10018 assert_eq!(params.options.tab_size, 4);
10019 Ok(Some(vec![lsp::TextEdit::new(
10020 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10021 ", ".to_string(),
10022 )]))
10023 })
10024 .next()
10025 .await;
10026 cx.executor().start_waiting();
10027 save.await;
10028 assert_eq!(
10029 editor.update(cx, |editor, cx| editor.text(cx)),
10030 "one, two\nthree\n"
10031 );
10032 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10033
10034 editor.update_in(cx, |editor, window, cx| {
10035 editor.set_text("one\ntwo\nthree\n", window, cx)
10036 });
10037 assert!(cx.read(|cx| editor.is_dirty(cx)));
10038
10039 // Ensure we can still save even if formatting hangs.
10040 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
10041 move |params, _| async move {
10042 assert_eq!(
10043 params.text_document.uri,
10044 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10045 );
10046 futures::future::pending::<()>().await;
10047 unreachable!()
10048 },
10049 );
10050 let save = editor
10051 .update_in(cx, |editor, window, cx| {
10052 editor.save(
10053 SaveOptions {
10054 format: true,
10055 autosave: false,
10056 },
10057 project.clone(),
10058 window,
10059 cx,
10060 )
10061 })
10062 .unwrap();
10063 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10064 cx.executor().start_waiting();
10065 save.await;
10066 assert_eq!(
10067 editor.update(cx, |editor, cx| editor.text(cx)),
10068 "one\ntwo\nthree\n"
10069 );
10070 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10071
10072 // For non-dirty buffer, no formatting request should be sent
10073 let save = editor
10074 .update_in(cx, |editor, window, cx| {
10075 editor.save(
10076 SaveOptions {
10077 format: false,
10078 autosave: false,
10079 },
10080 project.clone(),
10081 window,
10082 cx,
10083 )
10084 })
10085 .unwrap();
10086 let _pending_format_request = fake_server
10087 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
10088 panic!("Should not be invoked");
10089 })
10090 .next();
10091 cx.executor().start_waiting();
10092 save.await;
10093
10094 // Set Rust language override and assert overridden tabsize is sent to language server
10095 update_test_language_settings(cx, |settings| {
10096 settings.languages.0.insert(
10097 "Rust".into(),
10098 LanguageSettingsContent {
10099 tab_size: NonZeroU32::new(8),
10100 ..Default::default()
10101 },
10102 );
10103 });
10104
10105 editor.update_in(cx, |editor, window, cx| {
10106 editor.set_text("somehting_new\n", window, cx)
10107 });
10108 assert!(cx.read(|cx| editor.is_dirty(cx)));
10109 let save = editor
10110 .update_in(cx, |editor, window, cx| {
10111 editor.save(
10112 SaveOptions {
10113 format: true,
10114 autosave: false,
10115 },
10116 project.clone(),
10117 window,
10118 cx,
10119 )
10120 })
10121 .unwrap();
10122 fake_server
10123 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10124 assert_eq!(
10125 params.text_document.uri,
10126 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10127 );
10128 assert_eq!(params.options.tab_size, 8);
10129 Ok(Some(Vec::new()))
10130 })
10131 .next()
10132 .await;
10133 save.await;
10134}
10135
10136#[gpui::test]
10137async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
10138 init_test(cx, |settings| {
10139 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
10140 Formatter::LanguageServer { name: None },
10141 )))
10142 });
10143
10144 let fs = FakeFs::new(cx.executor());
10145 fs.insert_file(path!("/file.rs"), Default::default()).await;
10146
10147 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10148
10149 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10150 language_registry.add(Arc::new(Language::new(
10151 LanguageConfig {
10152 name: "Rust".into(),
10153 matcher: LanguageMatcher {
10154 path_suffixes: vec!["rs".to_string()],
10155 ..Default::default()
10156 },
10157 ..LanguageConfig::default()
10158 },
10159 Some(tree_sitter_rust::LANGUAGE.into()),
10160 )));
10161 update_test_language_settings(cx, |settings| {
10162 // Enable Prettier formatting for the same buffer, and ensure
10163 // LSP is called instead of Prettier.
10164 settings.defaults.prettier = Some(PrettierSettings {
10165 allowed: true,
10166 ..PrettierSettings::default()
10167 });
10168 });
10169 let mut fake_servers = language_registry.register_fake_lsp(
10170 "Rust",
10171 FakeLspAdapter {
10172 capabilities: lsp::ServerCapabilities {
10173 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10174 ..Default::default()
10175 },
10176 ..Default::default()
10177 },
10178 );
10179
10180 let buffer = project
10181 .update(cx, |project, cx| {
10182 project.open_local_buffer(path!("/file.rs"), cx)
10183 })
10184 .await
10185 .unwrap();
10186
10187 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10188 let (editor, cx) = cx.add_window_view(|window, cx| {
10189 build_editor_with_project(project.clone(), buffer, window, cx)
10190 });
10191 editor.update_in(cx, |editor, window, cx| {
10192 editor.set_text("one\ntwo\nthree\n", window, cx)
10193 });
10194
10195 cx.executor().start_waiting();
10196 let fake_server = fake_servers.next().await.unwrap();
10197
10198 let format = editor
10199 .update_in(cx, |editor, window, cx| {
10200 editor.perform_format(
10201 project.clone(),
10202 FormatTrigger::Manual,
10203 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10204 window,
10205 cx,
10206 )
10207 })
10208 .unwrap();
10209 fake_server
10210 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10211 assert_eq!(
10212 params.text_document.uri,
10213 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10214 );
10215 assert_eq!(params.options.tab_size, 4);
10216 Ok(Some(vec![lsp::TextEdit::new(
10217 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10218 ", ".to_string(),
10219 )]))
10220 })
10221 .next()
10222 .await;
10223 cx.executor().start_waiting();
10224 format.await;
10225 assert_eq!(
10226 editor.update(cx, |editor, cx| editor.text(cx)),
10227 "one, two\nthree\n"
10228 );
10229
10230 editor.update_in(cx, |editor, window, cx| {
10231 editor.set_text("one\ntwo\nthree\n", window, cx)
10232 });
10233 // Ensure we don't lock if formatting hangs.
10234 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10235 move |params, _| async move {
10236 assert_eq!(
10237 params.text_document.uri,
10238 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10239 );
10240 futures::future::pending::<()>().await;
10241 unreachable!()
10242 },
10243 );
10244 let format = editor
10245 .update_in(cx, |editor, window, cx| {
10246 editor.perform_format(
10247 project,
10248 FormatTrigger::Manual,
10249 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10250 window,
10251 cx,
10252 )
10253 })
10254 .unwrap();
10255 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10256 cx.executor().start_waiting();
10257 format.await;
10258 assert_eq!(
10259 editor.update(cx, |editor, cx| editor.text(cx)),
10260 "one\ntwo\nthree\n"
10261 );
10262}
10263
10264#[gpui::test]
10265async fn test_multiple_formatters(cx: &mut TestAppContext) {
10266 init_test(cx, |settings| {
10267 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
10268 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10269 Formatter::LanguageServer { name: None },
10270 Formatter::CodeActions(
10271 [
10272 ("code-action-1".into(), true),
10273 ("code-action-2".into(), true),
10274 ]
10275 .into_iter()
10276 .collect(),
10277 ),
10278 ])))
10279 });
10280
10281 let fs = FakeFs::new(cx.executor());
10282 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
10283 .await;
10284
10285 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10286 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10287 language_registry.add(rust_lang());
10288
10289 let mut fake_servers = language_registry.register_fake_lsp(
10290 "Rust",
10291 FakeLspAdapter {
10292 capabilities: lsp::ServerCapabilities {
10293 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10294 execute_command_provider: Some(lsp::ExecuteCommandOptions {
10295 commands: vec!["the-command-for-code-action-1".into()],
10296 ..Default::default()
10297 }),
10298 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10299 ..Default::default()
10300 },
10301 ..Default::default()
10302 },
10303 );
10304
10305 let buffer = project
10306 .update(cx, |project, cx| {
10307 project.open_local_buffer(path!("/file.rs"), cx)
10308 })
10309 .await
10310 .unwrap();
10311
10312 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10313 let (editor, cx) = cx.add_window_view(|window, cx| {
10314 build_editor_with_project(project.clone(), buffer, window, cx)
10315 });
10316
10317 cx.executor().start_waiting();
10318
10319 let fake_server = fake_servers.next().await.unwrap();
10320 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10321 move |_params, _| async move {
10322 Ok(Some(vec![lsp::TextEdit::new(
10323 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10324 "applied-formatting\n".to_string(),
10325 )]))
10326 },
10327 );
10328 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10329 move |params, _| async move {
10330 assert_eq!(
10331 params.context.only,
10332 Some(vec!["code-action-1".into(), "code-action-2".into()])
10333 );
10334 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
10335 Ok(Some(vec![
10336 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10337 kind: Some("code-action-1".into()),
10338 edit: Some(lsp::WorkspaceEdit::new(
10339 [(
10340 uri.clone(),
10341 vec![lsp::TextEdit::new(
10342 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10343 "applied-code-action-1-edit\n".to_string(),
10344 )],
10345 )]
10346 .into_iter()
10347 .collect(),
10348 )),
10349 command: Some(lsp::Command {
10350 command: "the-command-for-code-action-1".into(),
10351 ..Default::default()
10352 }),
10353 ..Default::default()
10354 }),
10355 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10356 kind: Some("code-action-2".into()),
10357 edit: Some(lsp::WorkspaceEdit::new(
10358 [(
10359 uri.clone(),
10360 vec![lsp::TextEdit::new(
10361 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10362 "applied-code-action-2-edit\n".to_string(),
10363 )],
10364 )]
10365 .into_iter()
10366 .collect(),
10367 )),
10368 ..Default::default()
10369 }),
10370 ]))
10371 },
10372 );
10373
10374 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
10375 move |params, _| async move { Ok(params) }
10376 });
10377
10378 let command_lock = Arc::new(futures::lock::Mutex::new(()));
10379 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
10380 let fake = fake_server.clone();
10381 let lock = command_lock.clone();
10382 move |params, _| {
10383 assert_eq!(params.command, "the-command-for-code-action-1");
10384 let fake = fake.clone();
10385 let lock = lock.clone();
10386 async move {
10387 lock.lock().await;
10388 fake.server
10389 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
10390 label: None,
10391 edit: lsp::WorkspaceEdit {
10392 changes: Some(
10393 [(
10394 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
10395 vec![lsp::TextEdit {
10396 range: lsp::Range::new(
10397 lsp::Position::new(0, 0),
10398 lsp::Position::new(0, 0),
10399 ),
10400 new_text: "applied-code-action-1-command\n".into(),
10401 }],
10402 )]
10403 .into_iter()
10404 .collect(),
10405 ),
10406 ..Default::default()
10407 },
10408 })
10409 .await
10410 .into_response()
10411 .unwrap();
10412 Ok(Some(json!(null)))
10413 }
10414 }
10415 });
10416
10417 cx.executor().start_waiting();
10418 editor
10419 .update_in(cx, |editor, window, cx| {
10420 editor.perform_format(
10421 project.clone(),
10422 FormatTrigger::Manual,
10423 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10424 window,
10425 cx,
10426 )
10427 })
10428 .unwrap()
10429 .await;
10430 editor.update(cx, |editor, cx| {
10431 assert_eq!(
10432 editor.text(cx),
10433 r#"
10434 applied-code-action-2-edit
10435 applied-code-action-1-command
10436 applied-code-action-1-edit
10437 applied-formatting
10438 one
10439 two
10440 three
10441 "#
10442 .unindent()
10443 );
10444 });
10445
10446 editor.update_in(cx, |editor, window, cx| {
10447 editor.undo(&Default::default(), window, cx);
10448 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10449 });
10450
10451 // Perform a manual edit while waiting for an LSP command
10452 // that's being run as part of a formatting code action.
10453 let lock_guard = command_lock.lock().await;
10454 let format = editor
10455 .update_in(cx, |editor, window, cx| {
10456 editor.perform_format(
10457 project.clone(),
10458 FormatTrigger::Manual,
10459 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10460 window,
10461 cx,
10462 )
10463 })
10464 .unwrap();
10465 cx.run_until_parked();
10466 editor.update(cx, |editor, cx| {
10467 assert_eq!(
10468 editor.text(cx),
10469 r#"
10470 applied-code-action-1-edit
10471 applied-formatting
10472 one
10473 two
10474 three
10475 "#
10476 .unindent()
10477 );
10478
10479 editor.buffer.update(cx, |buffer, cx| {
10480 let ix = buffer.len(cx);
10481 buffer.edit([(ix..ix, "edited\n")], None, cx);
10482 });
10483 });
10484
10485 // Allow the LSP command to proceed. Because the buffer was edited,
10486 // the second code action will not be run.
10487 drop(lock_guard);
10488 format.await;
10489 editor.update_in(cx, |editor, window, cx| {
10490 assert_eq!(
10491 editor.text(cx),
10492 r#"
10493 applied-code-action-1-command
10494 applied-code-action-1-edit
10495 applied-formatting
10496 one
10497 two
10498 three
10499 edited
10500 "#
10501 .unindent()
10502 );
10503
10504 // The manual edit is undone first, because it is the last thing the user did
10505 // (even though the command completed afterwards).
10506 editor.undo(&Default::default(), window, cx);
10507 assert_eq!(
10508 editor.text(cx),
10509 r#"
10510 applied-code-action-1-command
10511 applied-code-action-1-edit
10512 applied-formatting
10513 one
10514 two
10515 three
10516 "#
10517 .unindent()
10518 );
10519
10520 // All the formatting (including the command, which completed after the manual edit)
10521 // is undone together.
10522 editor.undo(&Default::default(), window, cx);
10523 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10524 });
10525}
10526
10527#[gpui::test]
10528async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
10529 init_test(cx, |settings| {
10530 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10531 Formatter::LanguageServer { name: None },
10532 ])))
10533 });
10534
10535 let fs = FakeFs::new(cx.executor());
10536 fs.insert_file(path!("/file.ts"), Default::default()).await;
10537
10538 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10539
10540 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10541 language_registry.add(Arc::new(Language::new(
10542 LanguageConfig {
10543 name: "TypeScript".into(),
10544 matcher: LanguageMatcher {
10545 path_suffixes: vec!["ts".to_string()],
10546 ..Default::default()
10547 },
10548 ..LanguageConfig::default()
10549 },
10550 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10551 )));
10552 update_test_language_settings(cx, |settings| {
10553 settings.defaults.prettier = Some(PrettierSettings {
10554 allowed: true,
10555 ..PrettierSettings::default()
10556 });
10557 });
10558 let mut fake_servers = language_registry.register_fake_lsp(
10559 "TypeScript",
10560 FakeLspAdapter {
10561 capabilities: lsp::ServerCapabilities {
10562 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10563 ..Default::default()
10564 },
10565 ..Default::default()
10566 },
10567 );
10568
10569 let buffer = project
10570 .update(cx, |project, cx| {
10571 project.open_local_buffer(path!("/file.ts"), cx)
10572 })
10573 .await
10574 .unwrap();
10575
10576 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10577 let (editor, cx) = cx.add_window_view(|window, cx| {
10578 build_editor_with_project(project.clone(), buffer, window, cx)
10579 });
10580 editor.update_in(cx, |editor, window, cx| {
10581 editor.set_text(
10582 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10583 window,
10584 cx,
10585 )
10586 });
10587
10588 cx.executor().start_waiting();
10589 let fake_server = fake_servers.next().await.unwrap();
10590
10591 let format = editor
10592 .update_in(cx, |editor, window, cx| {
10593 editor.perform_code_action_kind(
10594 project.clone(),
10595 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10596 window,
10597 cx,
10598 )
10599 })
10600 .unwrap();
10601 fake_server
10602 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
10603 assert_eq!(
10604 params.text_document.uri,
10605 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10606 );
10607 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
10608 lsp::CodeAction {
10609 title: "Organize Imports".to_string(),
10610 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
10611 edit: Some(lsp::WorkspaceEdit {
10612 changes: Some(
10613 [(
10614 params.text_document.uri.clone(),
10615 vec![lsp::TextEdit::new(
10616 lsp::Range::new(
10617 lsp::Position::new(1, 0),
10618 lsp::Position::new(2, 0),
10619 ),
10620 "".to_string(),
10621 )],
10622 )]
10623 .into_iter()
10624 .collect(),
10625 ),
10626 ..Default::default()
10627 }),
10628 ..Default::default()
10629 },
10630 )]))
10631 })
10632 .next()
10633 .await;
10634 cx.executor().start_waiting();
10635 format.await;
10636 assert_eq!(
10637 editor.update(cx, |editor, cx| editor.text(cx)),
10638 "import { a } from 'module';\n\nconst x = a;\n"
10639 );
10640
10641 editor.update_in(cx, |editor, window, cx| {
10642 editor.set_text(
10643 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10644 window,
10645 cx,
10646 )
10647 });
10648 // Ensure we don't lock if code action hangs.
10649 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10650 move |params, _| async move {
10651 assert_eq!(
10652 params.text_document.uri,
10653 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10654 );
10655 futures::future::pending::<()>().await;
10656 unreachable!()
10657 },
10658 );
10659 let format = editor
10660 .update_in(cx, |editor, window, cx| {
10661 editor.perform_code_action_kind(
10662 project,
10663 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10664 window,
10665 cx,
10666 )
10667 })
10668 .unwrap();
10669 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
10670 cx.executor().start_waiting();
10671 format.await;
10672 assert_eq!(
10673 editor.update(cx, |editor, cx| editor.text(cx)),
10674 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
10675 );
10676}
10677
10678#[gpui::test]
10679async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
10680 init_test(cx, |_| {});
10681
10682 let mut cx = EditorLspTestContext::new_rust(
10683 lsp::ServerCapabilities {
10684 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10685 ..Default::default()
10686 },
10687 cx,
10688 )
10689 .await;
10690
10691 cx.set_state(indoc! {"
10692 one.twoˇ
10693 "});
10694
10695 // The format request takes a long time. When it completes, it inserts
10696 // a newline and an indent before the `.`
10697 cx.lsp
10698 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
10699 let executor = cx.background_executor().clone();
10700 async move {
10701 executor.timer(Duration::from_millis(100)).await;
10702 Ok(Some(vec![lsp::TextEdit {
10703 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
10704 new_text: "\n ".into(),
10705 }]))
10706 }
10707 });
10708
10709 // Submit a format request.
10710 let format_1 = cx
10711 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10712 .unwrap();
10713 cx.executor().run_until_parked();
10714
10715 // Submit a second format request.
10716 let format_2 = cx
10717 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10718 .unwrap();
10719 cx.executor().run_until_parked();
10720
10721 // Wait for both format requests to complete
10722 cx.executor().advance_clock(Duration::from_millis(200));
10723 cx.executor().start_waiting();
10724 format_1.await.unwrap();
10725 cx.executor().start_waiting();
10726 format_2.await.unwrap();
10727
10728 // The formatting edits only happens once.
10729 cx.assert_editor_state(indoc! {"
10730 one
10731 .twoˇ
10732 "});
10733}
10734
10735#[gpui::test]
10736async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
10737 init_test(cx, |settings| {
10738 settings.defaults.formatter = Some(SelectedFormatter::Auto)
10739 });
10740
10741 let mut cx = EditorLspTestContext::new_rust(
10742 lsp::ServerCapabilities {
10743 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10744 ..Default::default()
10745 },
10746 cx,
10747 )
10748 .await;
10749
10750 // Set up a buffer white some trailing whitespace and no trailing newline.
10751 cx.set_state(
10752 &[
10753 "one ", //
10754 "twoˇ", //
10755 "three ", //
10756 "four", //
10757 ]
10758 .join("\n"),
10759 );
10760
10761 // Submit a format request.
10762 let format = cx
10763 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10764 .unwrap();
10765
10766 // Record which buffer changes have been sent to the language server
10767 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
10768 cx.lsp
10769 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
10770 let buffer_changes = buffer_changes.clone();
10771 move |params, _| {
10772 buffer_changes.lock().extend(
10773 params
10774 .content_changes
10775 .into_iter()
10776 .map(|e| (e.range.unwrap(), e.text)),
10777 );
10778 }
10779 });
10780
10781 // Handle formatting requests to the language server.
10782 cx.lsp
10783 .set_request_handler::<lsp::request::Formatting, _, _>({
10784 let buffer_changes = buffer_changes.clone();
10785 move |_, _| {
10786 // When formatting is requested, trailing whitespace has already been stripped,
10787 // and the trailing newline has already been added.
10788 assert_eq!(
10789 &buffer_changes.lock()[1..],
10790 &[
10791 (
10792 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
10793 "".into()
10794 ),
10795 (
10796 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
10797 "".into()
10798 ),
10799 (
10800 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
10801 "\n".into()
10802 ),
10803 ]
10804 );
10805
10806 // Insert blank lines between each line of the buffer.
10807 async move {
10808 Ok(Some(vec![
10809 lsp::TextEdit {
10810 range: lsp::Range::new(
10811 lsp::Position::new(1, 0),
10812 lsp::Position::new(1, 0),
10813 ),
10814 new_text: "\n".into(),
10815 },
10816 lsp::TextEdit {
10817 range: lsp::Range::new(
10818 lsp::Position::new(2, 0),
10819 lsp::Position::new(2, 0),
10820 ),
10821 new_text: "\n".into(),
10822 },
10823 ]))
10824 }
10825 }
10826 });
10827
10828 // After formatting the buffer, the trailing whitespace is stripped,
10829 // a newline is appended, and the edits provided by the language server
10830 // have been applied.
10831 format.await.unwrap();
10832 cx.assert_editor_state(
10833 &[
10834 "one", //
10835 "", //
10836 "twoˇ", //
10837 "", //
10838 "three", //
10839 "four", //
10840 "", //
10841 ]
10842 .join("\n"),
10843 );
10844
10845 // Undoing the formatting undoes the trailing whitespace removal, the
10846 // trailing newline, and the LSP edits.
10847 cx.update_buffer(|buffer, cx| buffer.undo(cx));
10848 cx.assert_editor_state(
10849 &[
10850 "one ", //
10851 "twoˇ", //
10852 "three ", //
10853 "four", //
10854 ]
10855 .join("\n"),
10856 );
10857}
10858
10859#[gpui::test]
10860async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
10861 cx: &mut TestAppContext,
10862) {
10863 init_test(cx, |_| {});
10864
10865 cx.update(|cx| {
10866 cx.update_global::<SettingsStore, _>(|settings, cx| {
10867 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10868 settings.auto_signature_help = Some(true);
10869 });
10870 });
10871 });
10872
10873 let mut cx = EditorLspTestContext::new_rust(
10874 lsp::ServerCapabilities {
10875 signature_help_provider: Some(lsp::SignatureHelpOptions {
10876 ..Default::default()
10877 }),
10878 ..Default::default()
10879 },
10880 cx,
10881 )
10882 .await;
10883
10884 let language = Language::new(
10885 LanguageConfig {
10886 name: "Rust".into(),
10887 brackets: BracketPairConfig {
10888 pairs: vec![
10889 BracketPair {
10890 start: "{".to_string(),
10891 end: "}".to_string(),
10892 close: true,
10893 surround: true,
10894 newline: true,
10895 },
10896 BracketPair {
10897 start: "(".to_string(),
10898 end: ")".to_string(),
10899 close: true,
10900 surround: true,
10901 newline: true,
10902 },
10903 BracketPair {
10904 start: "/*".to_string(),
10905 end: " */".to_string(),
10906 close: true,
10907 surround: true,
10908 newline: true,
10909 },
10910 BracketPair {
10911 start: "[".to_string(),
10912 end: "]".to_string(),
10913 close: false,
10914 surround: false,
10915 newline: true,
10916 },
10917 BracketPair {
10918 start: "\"".to_string(),
10919 end: "\"".to_string(),
10920 close: true,
10921 surround: true,
10922 newline: false,
10923 },
10924 BracketPair {
10925 start: "<".to_string(),
10926 end: ">".to_string(),
10927 close: false,
10928 surround: true,
10929 newline: true,
10930 },
10931 ],
10932 ..Default::default()
10933 },
10934 autoclose_before: "})]".to_string(),
10935 ..Default::default()
10936 },
10937 Some(tree_sitter_rust::LANGUAGE.into()),
10938 );
10939 let language = Arc::new(language);
10940
10941 cx.language_registry().add(language.clone());
10942 cx.update_buffer(|buffer, cx| {
10943 buffer.set_language(Some(language), cx);
10944 });
10945
10946 cx.set_state(
10947 &r#"
10948 fn main() {
10949 sampleˇ
10950 }
10951 "#
10952 .unindent(),
10953 );
10954
10955 cx.update_editor(|editor, window, cx| {
10956 editor.handle_input("(", window, cx);
10957 });
10958 cx.assert_editor_state(
10959 &"
10960 fn main() {
10961 sample(ˇ)
10962 }
10963 "
10964 .unindent(),
10965 );
10966
10967 let mocked_response = lsp::SignatureHelp {
10968 signatures: vec![lsp::SignatureInformation {
10969 label: "fn sample(param1: u8, param2: u8)".to_string(),
10970 documentation: None,
10971 parameters: Some(vec![
10972 lsp::ParameterInformation {
10973 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10974 documentation: None,
10975 },
10976 lsp::ParameterInformation {
10977 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10978 documentation: None,
10979 },
10980 ]),
10981 active_parameter: None,
10982 }],
10983 active_signature: Some(0),
10984 active_parameter: Some(0),
10985 };
10986 handle_signature_help_request(&mut cx, mocked_response).await;
10987
10988 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10989 .await;
10990
10991 cx.editor(|editor, _, _| {
10992 let signature_help_state = editor.signature_help_state.popover().cloned();
10993 let signature = signature_help_state.unwrap();
10994 assert_eq!(
10995 signature.signatures[signature.current_signature].label,
10996 "fn sample(param1: u8, param2: u8)"
10997 );
10998 });
10999}
11000
11001#[gpui::test]
11002async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
11003 init_test(cx, |_| {});
11004
11005 cx.update(|cx| {
11006 cx.update_global::<SettingsStore, _>(|settings, cx| {
11007 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11008 settings.auto_signature_help = Some(false);
11009 settings.show_signature_help_after_edits = Some(false);
11010 });
11011 });
11012 });
11013
11014 let mut cx = EditorLspTestContext::new_rust(
11015 lsp::ServerCapabilities {
11016 signature_help_provider: Some(lsp::SignatureHelpOptions {
11017 ..Default::default()
11018 }),
11019 ..Default::default()
11020 },
11021 cx,
11022 )
11023 .await;
11024
11025 let language = Language::new(
11026 LanguageConfig {
11027 name: "Rust".into(),
11028 brackets: BracketPairConfig {
11029 pairs: vec![
11030 BracketPair {
11031 start: "{".to_string(),
11032 end: "}".to_string(),
11033 close: true,
11034 surround: true,
11035 newline: true,
11036 },
11037 BracketPair {
11038 start: "(".to_string(),
11039 end: ")".to_string(),
11040 close: true,
11041 surround: true,
11042 newline: true,
11043 },
11044 BracketPair {
11045 start: "/*".to_string(),
11046 end: " */".to_string(),
11047 close: true,
11048 surround: true,
11049 newline: true,
11050 },
11051 BracketPair {
11052 start: "[".to_string(),
11053 end: "]".to_string(),
11054 close: false,
11055 surround: false,
11056 newline: true,
11057 },
11058 BracketPair {
11059 start: "\"".to_string(),
11060 end: "\"".to_string(),
11061 close: true,
11062 surround: true,
11063 newline: false,
11064 },
11065 BracketPair {
11066 start: "<".to_string(),
11067 end: ">".to_string(),
11068 close: false,
11069 surround: true,
11070 newline: true,
11071 },
11072 ],
11073 ..Default::default()
11074 },
11075 autoclose_before: "})]".to_string(),
11076 ..Default::default()
11077 },
11078 Some(tree_sitter_rust::LANGUAGE.into()),
11079 );
11080 let language = Arc::new(language);
11081
11082 cx.language_registry().add(language.clone());
11083 cx.update_buffer(|buffer, cx| {
11084 buffer.set_language(Some(language), cx);
11085 });
11086
11087 // Ensure that signature_help is not called when no signature help is enabled.
11088 cx.set_state(
11089 &r#"
11090 fn main() {
11091 sampleˇ
11092 }
11093 "#
11094 .unindent(),
11095 );
11096 cx.update_editor(|editor, window, cx| {
11097 editor.handle_input("(", window, cx);
11098 });
11099 cx.assert_editor_state(
11100 &"
11101 fn main() {
11102 sample(ˇ)
11103 }
11104 "
11105 .unindent(),
11106 );
11107 cx.editor(|editor, _, _| {
11108 assert!(editor.signature_help_state.task().is_none());
11109 });
11110
11111 let mocked_response = lsp::SignatureHelp {
11112 signatures: vec![lsp::SignatureInformation {
11113 label: "fn sample(param1: u8, param2: u8)".to_string(),
11114 documentation: None,
11115 parameters: Some(vec![
11116 lsp::ParameterInformation {
11117 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11118 documentation: None,
11119 },
11120 lsp::ParameterInformation {
11121 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11122 documentation: None,
11123 },
11124 ]),
11125 active_parameter: None,
11126 }],
11127 active_signature: Some(0),
11128 active_parameter: Some(0),
11129 };
11130
11131 // Ensure that signature_help is called when enabled afte edits
11132 cx.update(|_, cx| {
11133 cx.update_global::<SettingsStore, _>(|settings, cx| {
11134 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11135 settings.auto_signature_help = Some(false);
11136 settings.show_signature_help_after_edits = Some(true);
11137 });
11138 });
11139 });
11140 cx.set_state(
11141 &r#"
11142 fn main() {
11143 sampleˇ
11144 }
11145 "#
11146 .unindent(),
11147 );
11148 cx.update_editor(|editor, window, cx| {
11149 editor.handle_input("(", window, cx);
11150 });
11151 cx.assert_editor_state(
11152 &"
11153 fn main() {
11154 sample(ˇ)
11155 }
11156 "
11157 .unindent(),
11158 );
11159 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11160 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11161 .await;
11162 cx.update_editor(|editor, _, _| {
11163 let signature_help_state = editor.signature_help_state.popover().cloned();
11164 assert!(signature_help_state.is_some());
11165 let signature = signature_help_state.unwrap();
11166 assert_eq!(
11167 signature.signatures[signature.current_signature].label,
11168 "fn sample(param1: u8, param2: u8)"
11169 );
11170 editor.signature_help_state = SignatureHelpState::default();
11171 });
11172
11173 // Ensure that signature_help is called when auto signature help override is enabled
11174 cx.update(|_, cx| {
11175 cx.update_global::<SettingsStore, _>(|settings, cx| {
11176 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11177 settings.auto_signature_help = Some(true);
11178 settings.show_signature_help_after_edits = Some(false);
11179 });
11180 });
11181 });
11182 cx.set_state(
11183 &r#"
11184 fn main() {
11185 sampleˇ
11186 }
11187 "#
11188 .unindent(),
11189 );
11190 cx.update_editor(|editor, window, cx| {
11191 editor.handle_input("(", window, cx);
11192 });
11193 cx.assert_editor_state(
11194 &"
11195 fn main() {
11196 sample(ˇ)
11197 }
11198 "
11199 .unindent(),
11200 );
11201 handle_signature_help_request(&mut cx, mocked_response).await;
11202 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11203 .await;
11204 cx.editor(|editor, _, _| {
11205 let signature_help_state = editor.signature_help_state.popover().cloned();
11206 assert!(signature_help_state.is_some());
11207 let signature = signature_help_state.unwrap();
11208 assert_eq!(
11209 signature.signatures[signature.current_signature].label,
11210 "fn sample(param1: u8, param2: u8)"
11211 );
11212 });
11213}
11214
11215#[gpui::test]
11216async fn test_signature_help(cx: &mut TestAppContext) {
11217 init_test(cx, |_| {});
11218 cx.update(|cx| {
11219 cx.update_global::<SettingsStore, _>(|settings, cx| {
11220 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11221 settings.auto_signature_help = Some(true);
11222 });
11223 });
11224 });
11225
11226 let mut cx = EditorLspTestContext::new_rust(
11227 lsp::ServerCapabilities {
11228 signature_help_provider: Some(lsp::SignatureHelpOptions {
11229 ..Default::default()
11230 }),
11231 ..Default::default()
11232 },
11233 cx,
11234 )
11235 .await;
11236
11237 // A test that directly calls `show_signature_help`
11238 cx.update_editor(|editor, window, cx| {
11239 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11240 });
11241
11242 let mocked_response = lsp::SignatureHelp {
11243 signatures: vec![lsp::SignatureInformation {
11244 label: "fn sample(param1: u8, param2: u8)".to_string(),
11245 documentation: None,
11246 parameters: Some(vec![
11247 lsp::ParameterInformation {
11248 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11249 documentation: None,
11250 },
11251 lsp::ParameterInformation {
11252 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11253 documentation: None,
11254 },
11255 ]),
11256 active_parameter: None,
11257 }],
11258 active_signature: Some(0),
11259 active_parameter: Some(0),
11260 };
11261 handle_signature_help_request(&mut cx, mocked_response).await;
11262
11263 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11264 .await;
11265
11266 cx.editor(|editor, _, _| {
11267 let signature_help_state = editor.signature_help_state.popover().cloned();
11268 assert!(signature_help_state.is_some());
11269 let signature = signature_help_state.unwrap();
11270 assert_eq!(
11271 signature.signatures[signature.current_signature].label,
11272 "fn sample(param1: u8, param2: u8)"
11273 );
11274 });
11275
11276 // When exiting outside from inside the brackets, `signature_help` is closed.
11277 cx.set_state(indoc! {"
11278 fn main() {
11279 sample(ˇ);
11280 }
11281
11282 fn sample(param1: u8, param2: u8) {}
11283 "});
11284
11285 cx.update_editor(|editor, window, cx| {
11286 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11287 s.select_ranges([0..0])
11288 });
11289 });
11290
11291 let mocked_response = lsp::SignatureHelp {
11292 signatures: Vec::new(),
11293 active_signature: None,
11294 active_parameter: None,
11295 };
11296 handle_signature_help_request(&mut cx, mocked_response).await;
11297
11298 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11299 .await;
11300
11301 cx.editor(|editor, _, _| {
11302 assert!(!editor.signature_help_state.is_shown());
11303 });
11304
11305 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
11306 cx.set_state(indoc! {"
11307 fn main() {
11308 sample(ˇ);
11309 }
11310
11311 fn sample(param1: u8, param2: u8) {}
11312 "});
11313
11314 let mocked_response = lsp::SignatureHelp {
11315 signatures: vec![lsp::SignatureInformation {
11316 label: "fn sample(param1: u8, param2: u8)".to_string(),
11317 documentation: None,
11318 parameters: Some(vec![
11319 lsp::ParameterInformation {
11320 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11321 documentation: None,
11322 },
11323 lsp::ParameterInformation {
11324 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11325 documentation: None,
11326 },
11327 ]),
11328 active_parameter: None,
11329 }],
11330 active_signature: Some(0),
11331 active_parameter: Some(0),
11332 };
11333 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11334 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11335 .await;
11336 cx.editor(|editor, _, _| {
11337 assert!(editor.signature_help_state.is_shown());
11338 });
11339
11340 // Restore the popover with more parameter input
11341 cx.set_state(indoc! {"
11342 fn main() {
11343 sample(param1, param2ˇ);
11344 }
11345
11346 fn sample(param1: u8, param2: u8) {}
11347 "});
11348
11349 let mocked_response = lsp::SignatureHelp {
11350 signatures: vec![lsp::SignatureInformation {
11351 label: "fn sample(param1: u8, param2: u8)".to_string(),
11352 documentation: None,
11353 parameters: Some(vec![
11354 lsp::ParameterInformation {
11355 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11356 documentation: None,
11357 },
11358 lsp::ParameterInformation {
11359 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11360 documentation: None,
11361 },
11362 ]),
11363 active_parameter: None,
11364 }],
11365 active_signature: Some(0),
11366 active_parameter: Some(1),
11367 };
11368 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11369 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11370 .await;
11371
11372 // When selecting a range, the popover is gone.
11373 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
11374 cx.update_editor(|editor, window, cx| {
11375 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11376 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11377 })
11378 });
11379 cx.assert_editor_state(indoc! {"
11380 fn main() {
11381 sample(param1, «ˇparam2»);
11382 }
11383
11384 fn sample(param1: u8, param2: u8) {}
11385 "});
11386 cx.editor(|editor, _, _| {
11387 assert!(!editor.signature_help_state.is_shown());
11388 });
11389
11390 // When unselecting again, the popover is back if within the brackets.
11391 cx.update_editor(|editor, window, cx| {
11392 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11393 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11394 })
11395 });
11396 cx.assert_editor_state(indoc! {"
11397 fn main() {
11398 sample(param1, ˇparam2);
11399 }
11400
11401 fn sample(param1: u8, param2: u8) {}
11402 "});
11403 handle_signature_help_request(&mut cx, mocked_response).await;
11404 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11405 .await;
11406 cx.editor(|editor, _, _| {
11407 assert!(editor.signature_help_state.is_shown());
11408 });
11409
11410 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
11411 cx.update_editor(|editor, window, cx| {
11412 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11413 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
11414 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11415 })
11416 });
11417 cx.assert_editor_state(indoc! {"
11418 fn main() {
11419 sample(param1, ˇparam2);
11420 }
11421
11422 fn sample(param1: u8, param2: u8) {}
11423 "});
11424
11425 let mocked_response = lsp::SignatureHelp {
11426 signatures: vec![lsp::SignatureInformation {
11427 label: "fn sample(param1: u8, param2: u8)".to_string(),
11428 documentation: None,
11429 parameters: Some(vec![
11430 lsp::ParameterInformation {
11431 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11432 documentation: None,
11433 },
11434 lsp::ParameterInformation {
11435 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11436 documentation: None,
11437 },
11438 ]),
11439 active_parameter: None,
11440 }],
11441 active_signature: Some(0),
11442 active_parameter: Some(1),
11443 };
11444 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11445 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11446 .await;
11447 cx.update_editor(|editor, _, cx| {
11448 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
11449 });
11450 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11451 .await;
11452 cx.update_editor(|editor, window, cx| {
11453 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11454 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11455 })
11456 });
11457 cx.assert_editor_state(indoc! {"
11458 fn main() {
11459 sample(param1, «ˇparam2»);
11460 }
11461
11462 fn sample(param1: u8, param2: u8) {}
11463 "});
11464 cx.update_editor(|editor, window, cx| {
11465 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11466 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11467 })
11468 });
11469 cx.assert_editor_state(indoc! {"
11470 fn main() {
11471 sample(param1, ˇparam2);
11472 }
11473
11474 fn sample(param1: u8, param2: u8) {}
11475 "});
11476 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
11477 .await;
11478}
11479
11480#[gpui::test]
11481async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
11482 init_test(cx, |_| {});
11483
11484 let mut cx = EditorLspTestContext::new_rust(
11485 lsp::ServerCapabilities {
11486 signature_help_provider: Some(lsp::SignatureHelpOptions {
11487 ..Default::default()
11488 }),
11489 ..Default::default()
11490 },
11491 cx,
11492 )
11493 .await;
11494
11495 cx.set_state(indoc! {"
11496 fn main() {
11497 overloadedˇ
11498 }
11499 "});
11500
11501 cx.update_editor(|editor, window, cx| {
11502 editor.handle_input("(", window, cx);
11503 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11504 });
11505
11506 // Mock response with 3 signatures
11507 let mocked_response = lsp::SignatureHelp {
11508 signatures: vec![
11509 lsp::SignatureInformation {
11510 label: "fn overloaded(x: i32)".to_string(),
11511 documentation: None,
11512 parameters: Some(vec![lsp::ParameterInformation {
11513 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11514 documentation: None,
11515 }]),
11516 active_parameter: None,
11517 },
11518 lsp::SignatureInformation {
11519 label: "fn overloaded(x: i32, y: i32)".to_string(),
11520 documentation: None,
11521 parameters: Some(vec![
11522 lsp::ParameterInformation {
11523 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11524 documentation: None,
11525 },
11526 lsp::ParameterInformation {
11527 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11528 documentation: None,
11529 },
11530 ]),
11531 active_parameter: None,
11532 },
11533 lsp::SignatureInformation {
11534 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
11535 documentation: None,
11536 parameters: Some(vec![
11537 lsp::ParameterInformation {
11538 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11539 documentation: None,
11540 },
11541 lsp::ParameterInformation {
11542 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11543 documentation: None,
11544 },
11545 lsp::ParameterInformation {
11546 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
11547 documentation: None,
11548 },
11549 ]),
11550 active_parameter: None,
11551 },
11552 ],
11553 active_signature: Some(1),
11554 active_parameter: Some(0),
11555 };
11556 handle_signature_help_request(&mut cx, mocked_response).await;
11557
11558 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11559 .await;
11560
11561 // Verify we have multiple signatures and the right one is selected
11562 cx.editor(|editor, _, _| {
11563 let popover = editor.signature_help_state.popover().cloned().unwrap();
11564 assert_eq!(popover.signatures.len(), 3);
11565 // active_signature was 1, so that should be the current
11566 assert_eq!(popover.current_signature, 1);
11567 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
11568 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
11569 assert_eq!(
11570 popover.signatures[2].label,
11571 "fn overloaded(x: i32, y: i32, z: i32)"
11572 );
11573 });
11574
11575 // Test navigation functionality
11576 cx.update_editor(|editor, window, cx| {
11577 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11578 });
11579
11580 cx.editor(|editor, _, _| {
11581 let popover = editor.signature_help_state.popover().cloned().unwrap();
11582 assert_eq!(popover.current_signature, 2);
11583 });
11584
11585 // Test wrap around
11586 cx.update_editor(|editor, window, cx| {
11587 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11588 });
11589
11590 cx.editor(|editor, _, _| {
11591 let popover = editor.signature_help_state.popover().cloned().unwrap();
11592 assert_eq!(popover.current_signature, 0);
11593 });
11594
11595 // Test previous navigation
11596 cx.update_editor(|editor, window, cx| {
11597 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
11598 });
11599
11600 cx.editor(|editor, _, _| {
11601 let popover = editor.signature_help_state.popover().cloned().unwrap();
11602 assert_eq!(popover.current_signature, 2);
11603 });
11604}
11605
11606#[gpui::test]
11607async fn test_completion_mode(cx: &mut TestAppContext) {
11608 init_test(cx, |_| {});
11609 let mut cx = EditorLspTestContext::new_rust(
11610 lsp::ServerCapabilities {
11611 completion_provider: Some(lsp::CompletionOptions {
11612 resolve_provider: Some(true),
11613 ..Default::default()
11614 }),
11615 ..Default::default()
11616 },
11617 cx,
11618 )
11619 .await;
11620
11621 struct Run {
11622 run_description: &'static str,
11623 initial_state: String,
11624 buffer_marked_text: String,
11625 completion_label: &'static str,
11626 completion_text: &'static str,
11627 expected_with_insert_mode: String,
11628 expected_with_replace_mode: String,
11629 expected_with_replace_subsequence_mode: String,
11630 expected_with_replace_suffix_mode: String,
11631 }
11632
11633 let runs = [
11634 Run {
11635 run_description: "Start of word matches completion text",
11636 initial_state: "before ediˇ after".into(),
11637 buffer_marked_text: "before <edi|> after".into(),
11638 completion_label: "editor",
11639 completion_text: "editor",
11640 expected_with_insert_mode: "before editorˇ after".into(),
11641 expected_with_replace_mode: "before editorˇ after".into(),
11642 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11643 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11644 },
11645 Run {
11646 run_description: "Accept same text at the middle of the word",
11647 initial_state: "before ediˇtor after".into(),
11648 buffer_marked_text: "before <edi|tor> after".into(),
11649 completion_label: "editor",
11650 completion_text: "editor",
11651 expected_with_insert_mode: "before editorˇtor after".into(),
11652 expected_with_replace_mode: "before editorˇ after".into(),
11653 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11654 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11655 },
11656 Run {
11657 run_description: "End of word matches completion text -- cursor at end",
11658 initial_state: "before torˇ after".into(),
11659 buffer_marked_text: "before <tor|> after".into(),
11660 completion_label: "editor",
11661 completion_text: "editor",
11662 expected_with_insert_mode: "before editorˇ after".into(),
11663 expected_with_replace_mode: "before editorˇ after".into(),
11664 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11665 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11666 },
11667 Run {
11668 run_description: "End of word matches completion text -- cursor at start",
11669 initial_state: "before ˇtor after".into(),
11670 buffer_marked_text: "before <|tor> after".into(),
11671 completion_label: "editor",
11672 completion_text: "editor",
11673 expected_with_insert_mode: "before editorˇtor after".into(),
11674 expected_with_replace_mode: "before editorˇ after".into(),
11675 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11676 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11677 },
11678 Run {
11679 run_description: "Prepend text containing whitespace",
11680 initial_state: "pˇfield: bool".into(),
11681 buffer_marked_text: "<p|field>: bool".into(),
11682 completion_label: "pub ",
11683 completion_text: "pub ",
11684 expected_with_insert_mode: "pub ˇfield: bool".into(),
11685 expected_with_replace_mode: "pub ˇ: bool".into(),
11686 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
11687 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
11688 },
11689 Run {
11690 run_description: "Add element to start of list",
11691 initial_state: "[element_ˇelement_2]".into(),
11692 buffer_marked_text: "[<element_|element_2>]".into(),
11693 completion_label: "element_1",
11694 completion_text: "element_1",
11695 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
11696 expected_with_replace_mode: "[element_1ˇ]".into(),
11697 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
11698 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
11699 },
11700 Run {
11701 run_description: "Add element to start of list -- first and second elements are equal",
11702 initial_state: "[elˇelement]".into(),
11703 buffer_marked_text: "[<el|element>]".into(),
11704 completion_label: "element",
11705 completion_text: "element",
11706 expected_with_insert_mode: "[elementˇelement]".into(),
11707 expected_with_replace_mode: "[elementˇ]".into(),
11708 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
11709 expected_with_replace_suffix_mode: "[elementˇ]".into(),
11710 },
11711 Run {
11712 run_description: "Ends with matching suffix",
11713 initial_state: "SubˇError".into(),
11714 buffer_marked_text: "<Sub|Error>".into(),
11715 completion_label: "SubscriptionError",
11716 completion_text: "SubscriptionError",
11717 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
11718 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11719 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11720 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
11721 },
11722 Run {
11723 run_description: "Suffix is a subsequence -- contiguous",
11724 initial_state: "SubˇErr".into(),
11725 buffer_marked_text: "<Sub|Err>".into(),
11726 completion_label: "SubscriptionError",
11727 completion_text: "SubscriptionError",
11728 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
11729 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11730 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11731 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
11732 },
11733 Run {
11734 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
11735 initial_state: "Suˇscrirr".into(),
11736 buffer_marked_text: "<Su|scrirr>".into(),
11737 completion_label: "SubscriptionError",
11738 completion_text: "SubscriptionError",
11739 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
11740 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11741 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11742 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
11743 },
11744 Run {
11745 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
11746 initial_state: "foo(indˇix)".into(),
11747 buffer_marked_text: "foo(<ind|ix>)".into(),
11748 completion_label: "node_index",
11749 completion_text: "node_index",
11750 expected_with_insert_mode: "foo(node_indexˇix)".into(),
11751 expected_with_replace_mode: "foo(node_indexˇ)".into(),
11752 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
11753 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
11754 },
11755 Run {
11756 run_description: "Replace range ends before cursor - should extend to cursor",
11757 initial_state: "before editˇo after".into(),
11758 buffer_marked_text: "before <{ed}>it|o after".into(),
11759 completion_label: "editor",
11760 completion_text: "editor",
11761 expected_with_insert_mode: "before editorˇo after".into(),
11762 expected_with_replace_mode: "before editorˇo after".into(),
11763 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
11764 expected_with_replace_suffix_mode: "before editorˇo after".into(),
11765 },
11766 Run {
11767 run_description: "Uses label for suffix matching",
11768 initial_state: "before ediˇtor after".into(),
11769 buffer_marked_text: "before <edi|tor> after".into(),
11770 completion_label: "editor",
11771 completion_text: "editor()",
11772 expected_with_insert_mode: "before editor()ˇtor after".into(),
11773 expected_with_replace_mode: "before editor()ˇ after".into(),
11774 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
11775 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
11776 },
11777 Run {
11778 run_description: "Case insensitive subsequence and suffix matching",
11779 initial_state: "before EDiˇtoR after".into(),
11780 buffer_marked_text: "before <EDi|toR> after".into(),
11781 completion_label: "editor",
11782 completion_text: "editor",
11783 expected_with_insert_mode: "before editorˇtoR after".into(),
11784 expected_with_replace_mode: "before editorˇ after".into(),
11785 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11786 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11787 },
11788 ];
11789
11790 for run in runs {
11791 let run_variations = [
11792 (LspInsertMode::Insert, run.expected_with_insert_mode),
11793 (LspInsertMode::Replace, run.expected_with_replace_mode),
11794 (
11795 LspInsertMode::ReplaceSubsequence,
11796 run.expected_with_replace_subsequence_mode,
11797 ),
11798 (
11799 LspInsertMode::ReplaceSuffix,
11800 run.expected_with_replace_suffix_mode,
11801 ),
11802 ];
11803
11804 for (lsp_insert_mode, expected_text) in run_variations {
11805 eprintln!(
11806 "run = {:?}, mode = {lsp_insert_mode:.?}",
11807 run.run_description,
11808 );
11809
11810 update_test_language_settings(&mut cx, |settings| {
11811 settings.defaults.completions = Some(CompletionSettings {
11812 lsp_insert_mode,
11813 words: WordsCompletionMode::Disabled,
11814 lsp: true,
11815 lsp_fetch_timeout_ms: 0,
11816 });
11817 });
11818
11819 cx.set_state(&run.initial_state);
11820 cx.update_editor(|editor, window, cx| {
11821 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11822 });
11823
11824 let counter = Arc::new(AtomicUsize::new(0));
11825 handle_completion_request_with_insert_and_replace(
11826 &mut cx,
11827 &run.buffer_marked_text,
11828 vec![(run.completion_label, run.completion_text)],
11829 counter.clone(),
11830 )
11831 .await;
11832 cx.condition(|editor, _| editor.context_menu_visible())
11833 .await;
11834 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11835
11836 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11837 editor
11838 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11839 .unwrap()
11840 });
11841 cx.assert_editor_state(&expected_text);
11842 handle_resolve_completion_request(&mut cx, None).await;
11843 apply_additional_edits.await.unwrap();
11844 }
11845 }
11846}
11847
11848#[gpui::test]
11849async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
11850 init_test(cx, |_| {});
11851 let mut cx = EditorLspTestContext::new_rust(
11852 lsp::ServerCapabilities {
11853 completion_provider: Some(lsp::CompletionOptions {
11854 resolve_provider: Some(true),
11855 ..Default::default()
11856 }),
11857 ..Default::default()
11858 },
11859 cx,
11860 )
11861 .await;
11862
11863 let initial_state = "SubˇError";
11864 let buffer_marked_text = "<Sub|Error>";
11865 let completion_text = "SubscriptionError";
11866 let expected_with_insert_mode = "SubscriptionErrorˇError";
11867 let expected_with_replace_mode = "SubscriptionErrorˇ";
11868
11869 update_test_language_settings(&mut cx, |settings| {
11870 settings.defaults.completions = Some(CompletionSettings {
11871 words: WordsCompletionMode::Disabled,
11872 // set the opposite here to ensure that the action is overriding the default behavior
11873 lsp_insert_mode: LspInsertMode::Insert,
11874 lsp: true,
11875 lsp_fetch_timeout_ms: 0,
11876 });
11877 });
11878
11879 cx.set_state(initial_state);
11880 cx.update_editor(|editor, window, cx| {
11881 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11882 });
11883
11884 let counter = Arc::new(AtomicUsize::new(0));
11885 handle_completion_request_with_insert_and_replace(
11886 &mut cx,
11887 &buffer_marked_text,
11888 vec![(completion_text, completion_text)],
11889 counter.clone(),
11890 )
11891 .await;
11892 cx.condition(|editor, _| editor.context_menu_visible())
11893 .await;
11894 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11895
11896 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11897 editor
11898 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11899 .unwrap()
11900 });
11901 cx.assert_editor_state(&expected_with_replace_mode);
11902 handle_resolve_completion_request(&mut cx, None).await;
11903 apply_additional_edits.await.unwrap();
11904
11905 update_test_language_settings(&mut cx, |settings| {
11906 settings.defaults.completions = Some(CompletionSettings {
11907 words: WordsCompletionMode::Disabled,
11908 // set the opposite here to ensure that the action is overriding the default behavior
11909 lsp_insert_mode: LspInsertMode::Replace,
11910 lsp: true,
11911 lsp_fetch_timeout_ms: 0,
11912 });
11913 });
11914
11915 cx.set_state(initial_state);
11916 cx.update_editor(|editor, window, cx| {
11917 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11918 });
11919 handle_completion_request_with_insert_and_replace(
11920 &mut cx,
11921 &buffer_marked_text,
11922 vec![(completion_text, completion_text)],
11923 counter.clone(),
11924 )
11925 .await;
11926 cx.condition(|editor, _| editor.context_menu_visible())
11927 .await;
11928 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11929
11930 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11931 editor
11932 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
11933 .unwrap()
11934 });
11935 cx.assert_editor_state(&expected_with_insert_mode);
11936 handle_resolve_completion_request(&mut cx, None).await;
11937 apply_additional_edits.await.unwrap();
11938}
11939
11940#[gpui::test]
11941async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
11942 init_test(cx, |_| {});
11943 let mut cx = EditorLspTestContext::new_rust(
11944 lsp::ServerCapabilities {
11945 completion_provider: Some(lsp::CompletionOptions {
11946 resolve_provider: Some(true),
11947 ..Default::default()
11948 }),
11949 ..Default::default()
11950 },
11951 cx,
11952 )
11953 .await;
11954
11955 // scenario: surrounding text matches completion text
11956 let completion_text = "to_offset";
11957 let initial_state = indoc! {"
11958 1. buf.to_offˇsuffix
11959 2. buf.to_offˇsuf
11960 3. buf.to_offˇfix
11961 4. buf.to_offˇ
11962 5. into_offˇensive
11963 6. ˇsuffix
11964 7. let ˇ //
11965 8. aaˇzz
11966 9. buf.to_off«zzzzzˇ»suffix
11967 10. buf.«ˇzzzzz»suffix
11968 11. to_off«ˇzzzzz»
11969
11970 buf.to_offˇsuffix // newest cursor
11971 "};
11972 let completion_marked_buffer = indoc! {"
11973 1. buf.to_offsuffix
11974 2. buf.to_offsuf
11975 3. buf.to_offfix
11976 4. buf.to_off
11977 5. into_offensive
11978 6. suffix
11979 7. let //
11980 8. aazz
11981 9. buf.to_offzzzzzsuffix
11982 10. buf.zzzzzsuffix
11983 11. to_offzzzzz
11984
11985 buf.<to_off|suffix> // newest cursor
11986 "};
11987 let expected = indoc! {"
11988 1. buf.to_offsetˇ
11989 2. buf.to_offsetˇsuf
11990 3. buf.to_offsetˇfix
11991 4. buf.to_offsetˇ
11992 5. into_offsetˇensive
11993 6. to_offsetˇsuffix
11994 7. let to_offsetˇ //
11995 8. aato_offsetˇzz
11996 9. buf.to_offsetˇ
11997 10. buf.to_offsetˇsuffix
11998 11. to_offsetˇ
11999
12000 buf.to_offsetˇ // newest cursor
12001 "};
12002 cx.set_state(initial_state);
12003 cx.update_editor(|editor, window, cx| {
12004 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12005 });
12006 handle_completion_request_with_insert_and_replace(
12007 &mut cx,
12008 completion_marked_buffer,
12009 vec![(completion_text, completion_text)],
12010 Arc::new(AtomicUsize::new(0)),
12011 )
12012 .await;
12013 cx.condition(|editor, _| editor.context_menu_visible())
12014 .await;
12015 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12016 editor
12017 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12018 .unwrap()
12019 });
12020 cx.assert_editor_state(expected);
12021 handle_resolve_completion_request(&mut cx, None).await;
12022 apply_additional_edits.await.unwrap();
12023
12024 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
12025 let completion_text = "foo_and_bar";
12026 let initial_state = indoc! {"
12027 1. ooanbˇ
12028 2. zooanbˇ
12029 3. ooanbˇz
12030 4. zooanbˇz
12031 5. ooanˇ
12032 6. oanbˇ
12033
12034 ooanbˇ
12035 "};
12036 let completion_marked_buffer = indoc! {"
12037 1. ooanb
12038 2. zooanb
12039 3. ooanbz
12040 4. zooanbz
12041 5. ooan
12042 6. oanb
12043
12044 <ooanb|>
12045 "};
12046 let expected = indoc! {"
12047 1. foo_and_barˇ
12048 2. zfoo_and_barˇ
12049 3. foo_and_barˇz
12050 4. zfoo_and_barˇz
12051 5. ooanfoo_and_barˇ
12052 6. oanbfoo_and_barˇ
12053
12054 foo_and_barˇ
12055 "};
12056 cx.set_state(initial_state);
12057 cx.update_editor(|editor, window, cx| {
12058 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12059 });
12060 handle_completion_request_with_insert_and_replace(
12061 &mut cx,
12062 completion_marked_buffer,
12063 vec![(completion_text, completion_text)],
12064 Arc::new(AtomicUsize::new(0)),
12065 )
12066 .await;
12067 cx.condition(|editor, _| editor.context_menu_visible())
12068 .await;
12069 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12070 editor
12071 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12072 .unwrap()
12073 });
12074 cx.assert_editor_state(expected);
12075 handle_resolve_completion_request(&mut cx, None).await;
12076 apply_additional_edits.await.unwrap();
12077
12078 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
12079 // (expects the same as if it was inserted at the end)
12080 let completion_text = "foo_and_bar";
12081 let initial_state = indoc! {"
12082 1. ooˇanb
12083 2. zooˇanb
12084 3. ooˇanbz
12085 4. zooˇanbz
12086
12087 ooˇanb
12088 "};
12089 let completion_marked_buffer = indoc! {"
12090 1. ooanb
12091 2. zooanb
12092 3. ooanbz
12093 4. zooanbz
12094
12095 <oo|anb>
12096 "};
12097 let expected = indoc! {"
12098 1. foo_and_barˇ
12099 2. zfoo_and_barˇ
12100 3. foo_and_barˇz
12101 4. zfoo_and_barˇz
12102
12103 foo_and_barˇ
12104 "};
12105 cx.set_state(initial_state);
12106 cx.update_editor(|editor, window, cx| {
12107 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12108 });
12109 handle_completion_request_with_insert_and_replace(
12110 &mut cx,
12111 completion_marked_buffer,
12112 vec![(completion_text, completion_text)],
12113 Arc::new(AtomicUsize::new(0)),
12114 )
12115 .await;
12116 cx.condition(|editor, _| editor.context_menu_visible())
12117 .await;
12118 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12119 editor
12120 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12121 .unwrap()
12122 });
12123 cx.assert_editor_state(expected);
12124 handle_resolve_completion_request(&mut cx, None).await;
12125 apply_additional_edits.await.unwrap();
12126}
12127
12128// This used to crash
12129#[gpui::test]
12130async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
12131 init_test(cx, |_| {});
12132
12133 let buffer_text = indoc! {"
12134 fn main() {
12135 10.satu;
12136
12137 //
12138 // separate cursors so they open in different excerpts (manually reproducible)
12139 //
12140
12141 10.satu20;
12142 }
12143 "};
12144 let multibuffer_text_with_selections = indoc! {"
12145 fn main() {
12146 10.satuˇ;
12147
12148 //
12149
12150 //
12151
12152 10.satuˇ20;
12153 }
12154 "};
12155 let expected_multibuffer = indoc! {"
12156 fn main() {
12157 10.saturating_sub()ˇ;
12158
12159 //
12160
12161 //
12162
12163 10.saturating_sub()ˇ;
12164 }
12165 "};
12166
12167 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
12168 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
12169
12170 let fs = FakeFs::new(cx.executor());
12171 fs.insert_tree(
12172 path!("/a"),
12173 json!({
12174 "main.rs": buffer_text,
12175 }),
12176 )
12177 .await;
12178
12179 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12180 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12181 language_registry.add(rust_lang());
12182 let mut fake_servers = language_registry.register_fake_lsp(
12183 "Rust",
12184 FakeLspAdapter {
12185 capabilities: lsp::ServerCapabilities {
12186 completion_provider: Some(lsp::CompletionOptions {
12187 resolve_provider: None,
12188 ..lsp::CompletionOptions::default()
12189 }),
12190 ..lsp::ServerCapabilities::default()
12191 },
12192 ..FakeLspAdapter::default()
12193 },
12194 );
12195 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12196 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12197 let buffer = project
12198 .update(cx, |project, cx| {
12199 project.open_local_buffer(path!("/a/main.rs"), cx)
12200 })
12201 .await
12202 .unwrap();
12203
12204 let multi_buffer = cx.new(|cx| {
12205 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
12206 multi_buffer.push_excerpts(
12207 buffer.clone(),
12208 [ExcerptRange::new(0..first_excerpt_end)],
12209 cx,
12210 );
12211 multi_buffer.push_excerpts(
12212 buffer.clone(),
12213 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
12214 cx,
12215 );
12216 multi_buffer
12217 });
12218
12219 let editor = workspace
12220 .update(cx, |_, window, cx| {
12221 cx.new(|cx| {
12222 Editor::new(
12223 EditorMode::Full {
12224 scale_ui_elements_with_buffer_font_size: false,
12225 show_active_line_background: false,
12226 sized_by_content: false,
12227 },
12228 multi_buffer.clone(),
12229 Some(project.clone()),
12230 window,
12231 cx,
12232 )
12233 })
12234 })
12235 .unwrap();
12236
12237 let pane = workspace
12238 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12239 .unwrap();
12240 pane.update_in(cx, |pane, window, cx| {
12241 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
12242 });
12243
12244 let fake_server = fake_servers.next().await.unwrap();
12245
12246 editor.update_in(cx, |editor, window, cx| {
12247 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12248 s.select_ranges([
12249 Point::new(1, 11)..Point::new(1, 11),
12250 Point::new(7, 11)..Point::new(7, 11),
12251 ])
12252 });
12253
12254 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
12255 });
12256
12257 editor.update_in(cx, |editor, window, cx| {
12258 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12259 });
12260
12261 fake_server
12262 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12263 let completion_item = lsp::CompletionItem {
12264 label: "saturating_sub()".into(),
12265 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
12266 lsp::InsertReplaceEdit {
12267 new_text: "saturating_sub()".to_owned(),
12268 insert: lsp::Range::new(
12269 lsp::Position::new(7, 7),
12270 lsp::Position::new(7, 11),
12271 ),
12272 replace: lsp::Range::new(
12273 lsp::Position::new(7, 7),
12274 lsp::Position::new(7, 13),
12275 ),
12276 },
12277 )),
12278 ..lsp::CompletionItem::default()
12279 };
12280
12281 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
12282 })
12283 .next()
12284 .await
12285 .unwrap();
12286
12287 cx.condition(&editor, |editor, _| editor.context_menu_visible())
12288 .await;
12289
12290 editor
12291 .update_in(cx, |editor, window, cx| {
12292 editor
12293 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12294 .unwrap()
12295 })
12296 .await
12297 .unwrap();
12298
12299 editor.update(cx, |editor, cx| {
12300 assert_text_with_selections(editor, expected_multibuffer, cx);
12301 })
12302}
12303
12304#[gpui::test]
12305async fn test_completion(cx: &mut TestAppContext) {
12306 init_test(cx, |_| {});
12307
12308 let mut cx = EditorLspTestContext::new_rust(
12309 lsp::ServerCapabilities {
12310 completion_provider: Some(lsp::CompletionOptions {
12311 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12312 resolve_provider: Some(true),
12313 ..Default::default()
12314 }),
12315 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12316 ..Default::default()
12317 },
12318 cx,
12319 )
12320 .await;
12321 let counter = Arc::new(AtomicUsize::new(0));
12322
12323 cx.set_state(indoc! {"
12324 oneˇ
12325 two
12326 three
12327 "});
12328 cx.simulate_keystroke(".");
12329 handle_completion_request(
12330 indoc! {"
12331 one.|<>
12332 two
12333 three
12334 "},
12335 vec!["first_completion", "second_completion"],
12336 true,
12337 counter.clone(),
12338 &mut cx,
12339 )
12340 .await;
12341 cx.condition(|editor, _| editor.context_menu_visible())
12342 .await;
12343 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12344
12345 let _handler = handle_signature_help_request(
12346 &mut cx,
12347 lsp::SignatureHelp {
12348 signatures: vec![lsp::SignatureInformation {
12349 label: "test signature".to_string(),
12350 documentation: None,
12351 parameters: Some(vec![lsp::ParameterInformation {
12352 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
12353 documentation: None,
12354 }]),
12355 active_parameter: None,
12356 }],
12357 active_signature: None,
12358 active_parameter: None,
12359 },
12360 );
12361 cx.update_editor(|editor, window, cx| {
12362 assert!(
12363 !editor.signature_help_state.is_shown(),
12364 "No signature help was called for"
12365 );
12366 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12367 });
12368 cx.run_until_parked();
12369 cx.update_editor(|editor, _, _| {
12370 assert!(
12371 !editor.signature_help_state.is_shown(),
12372 "No signature help should be shown when completions menu is open"
12373 );
12374 });
12375
12376 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12377 editor.context_menu_next(&Default::default(), window, cx);
12378 editor
12379 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12380 .unwrap()
12381 });
12382 cx.assert_editor_state(indoc! {"
12383 one.second_completionˇ
12384 two
12385 three
12386 "});
12387
12388 handle_resolve_completion_request(
12389 &mut cx,
12390 Some(vec![
12391 (
12392 //This overlaps with the primary completion edit which is
12393 //misbehavior from the LSP spec, test that we filter it out
12394 indoc! {"
12395 one.second_ˇcompletion
12396 two
12397 threeˇ
12398 "},
12399 "overlapping additional edit",
12400 ),
12401 (
12402 indoc! {"
12403 one.second_completion
12404 two
12405 threeˇ
12406 "},
12407 "\nadditional edit",
12408 ),
12409 ]),
12410 )
12411 .await;
12412 apply_additional_edits.await.unwrap();
12413 cx.assert_editor_state(indoc! {"
12414 one.second_completionˇ
12415 two
12416 three
12417 additional edit
12418 "});
12419
12420 cx.set_state(indoc! {"
12421 one.second_completion
12422 twoˇ
12423 threeˇ
12424 additional edit
12425 "});
12426 cx.simulate_keystroke(" ");
12427 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12428 cx.simulate_keystroke("s");
12429 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12430
12431 cx.assert_editor_state(indoc! {"
12432 one.second_completion
12433 two sˇ
12434 three sˇ
12435 additional edit
12436 "});
12437 handle_completion_request(
12438 indoc! {"
12439 one.second_completion
12440 two s
12441 three <s|>
12442 additional edit
12443 "},
12444 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12445 true,
12446 counter.clone(),
12447 &mut cx,
12448 )
12449 .await;
12450 cx.condition(|editor, _| editor.context_menu_visible())
12451 .await;
12452 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12453
12454 cx.simulate_keystroke("i");
12455
12456 handle_completion_request(
12457 indoc! {"
12458 one.second_completion
12459 two si
12460 three <si|>
12461 additional edit
12462 "},
12463 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12464 true,
12465 counter.clone(),
12466 &mut cx,
12467 )
12468 .await;
12469 cx.condition(|editor, _| editor.context_menu_visible())
12470 .await;
12471 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12472
12473 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12474 editor
12475 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12476 .unwrap()
12477 });
12478 cx.assert_editor_state(indoc! {"
12479 one.second_completion
12480 two sixth_completionˇ
12481 three sixth_completionˇ
12482 additional edit
12483 "});
12484
12485 apply_additional_edits.await.unwrap();
12486
12487 update_test_language_settings(&mut cx, |settings| {
12488 settings.defaults.show_completions_on_input = Some(false);
12489 });
12490 cx.set_state("editorˇ");
12491 cx.simulate_keystroke(".");
12492 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12493 cx.simulate_keystrokes("c l o");
12494 cx.assert_editor_state("editor.cloˇ");
12495 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12496 cx.update_editor(|editor, window, cx| {
12497 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12498 });
12499 handle_completion_request(
12500 "editor.<clo|>",
12501 vec!["close", "clobber"],
12502 true,
12503 counter.clone(),
12504 &mut cx,
12505 )
12506 .await;
12507 cx.condition(|editor, _| editor.context_menu_visible())
12508 .await;
12509 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
12510
12511 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12512 editor
12513 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12514 .unwrap()
12515 });
12516 cx.assert_editor_state("editor.clobberˇ");
12517 handle_resolve_completion_request(&mut cx, None).await;
12518 apply_additional_edits.await.unwrap();
12519}
12520
12521#[gpui::test]
12522async fn test_completion_reuse(cx: &mut TestAppContext) {
12523 init_test(cx, |_| {});
12524
12525 let mut cx = EditorLspTestContext::new_rust(
12526 lsp::ServerCapabilities {
12527 completion_provider: Some(lsp::CompletionOptions {
12528 trigger_characters: Some(vec![".".to_string()]),
12529 ..Default::default()
12530 }),
12531 ..Default::default()
12532 },
12533 cx,
12534 )
12535 .await;
12536
12537 let counter = Arc::new(AtomicUsize::new(0));
12538 cx.set_state("objˇ");
12539 cx.simulate_keystroke(".");
12540
12541 // Initial completion request returns complete results
12542 let is_incomplete = false;
12543 handle_completion_request(
12544 "obj.|<>",
12545 vec!["a", "ab", "abc"],
12546 is_incomplete,
12547 counter.clone(),
12548 &mut cx,
12549 )
12550 .await;
12551 cx.run_until_parked();
12552 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12553 cx.assert_editor_state("obj.ˇ");
12554 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12555
12556 // Type "a" - filters existing completions
12557 cx.simulate_keystroke("a");
12558 cx.run_until_parked();
12559 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12560 cx.assert_editor_state("obj.aˇ");
12561 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12562
12563 // Type "b" - filters existing completions
12564 cx.simulate_keystroke("b");
12565 cx.run_until_parked();
12566 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12567 cx.assert_editor_state("obj.abˇ");
12568 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12569
12570 // Type "c" - filters existing completions
12571 cx.simulate_keystroke("c");
12572 cx.run_until_parked();
12573 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12574 cx.assert_editor_state("obj.abcˇ");
12575 check_displayed_completions(vec!["abc"], &mut cx);
12576
12577 // Backspace to delete "c" - filters existing completions
12578 cx.update_editor(|editor, window, cx| {
12579 editor.backspace(&Backspace, window, cx);
12580 });
12581 cx.run_until_parked();
12582 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12583 cx.assert_editor_state("obj.abˇ");
12584 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12585
12586 // Moving cursor to the left dismisses menu.
12587 cx.update_editor(|editor, window, cx| {
12588 editor.move_left(&MoveLeft, window, cx);
12589 });
12590 cx.run_until_parked();
12591 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12592 cx.assert_editor_state("obj.aˇb");
12593 cx.update_editor(|editor, _, _| {
12594 assert_eq!(editor.context_menu_visible(), false);
12595 });
12596
12597 // Type "b" - new request
12598 cx.simulate_keystroke("b");
12599 let is_incomplete = false;
12600 handle_completion_request(
12601 "obj.<ab|>a",
12602 vec!["ab", "abc"],
12603 is_incomplete,
12604 counter.clone(),
12605 &mut cx,
12606 )
12607 .await;
12608 cx.run_until_parked();
12609 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12610 cx.assert_editor_state("obj.abˇb");
12611 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12612
12613 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
12614 cx.update_editor(|editor, window, cx| {
12615 editor.backspace(&Backspace, window, cx);
12616 });
12617 let is_incomplete = false;
12618 handle_completion_request(
12619 "obj.<a|>b",
12620 vec!["a", "ab", "abc"],
12621 is_incomplete,
12622 counter.clone(),
12623 &mut cx,
12624 )
12625 .await;
12626 cx.run_until_parked();
12627 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12628 cx.assert_editor_state("obj.aˇb");
12629 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12630
12631 // Backspace to delete "a" - dismisses menu.
12632 cx.update_editor(|editor, window, cx| {
12633 editor.backspace(&Backspace, window, cx);
12634 });
12635 cx.run_until_parked();
12636 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12637 cx.assert_editor_state("obj.ˇb");
12638 cx.update_editor(|editor, _, _| {
12639 assert_eq!(editor.context_menu_visible(), false);
12640 });
12641}
12642
12643#[gpui::test]
12644async fn test_word_completion(cx: &mut TestAppContext) {
12645 let lsp_fetch_timeout_ms = 10;
12646 init_test(cx, |language_settings| {
12647 language_settings.defaults.completions = Some(CompletionSettings {
12648 words: WordsCompletionMode::Fallback,
12649 lsp: true,
12650 lsp_fetch_timeout_ms: 10,
12651 lsp_insert_mode: LspInsertMode::Insert,
12652 });
12653 });
12654
12655 let mut cx = EditorLspTestContext::new_rust(
12656 lsp::ServerCapabilities {
12657 completion_provider: Some(lsp::CompletionOptions {
12658 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12659 ..lsp::CompletionOptions::default()
12660 }),
12661 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12662 ..lsp::ServerCapabilities::default()
12663 },
12664 cx,
12665 )
12666 .await;
12667
12668 let throttle_completions = Arc::new(AtomicBool::new(false));
12669
12670 let lsp_throttle_completions = throttle_completions.clone();
12671 let _completion_requests_handler =
12672 cx.lsp
12673 .server
12674 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
12675 let lsp_throttle_completions = lsp_throttle_completions.clone();
12676 let cx = cx.clone();
12677 async move {
12678 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
12679 cx.background_executor()
12680 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
12681 .await;
12682 }
12683 Ok(Some(lsp::CompletionResponse::Array(vec![
12684 lsp::CompletionItem {
12685 label: "first".into(),
12686 ..lsp::CompletionItem::default()
12687 },
12688 lsp::CompletionItem {
12689 label: "last".into(),
12690 ..lsp::CompletionItem::default()
12691 },
12692 ])))
12693 }
12694 });
12695
12696 cx.set_state(indoc! {"
12697 oneˇ
12698 two
12699 three
12700 "});
12701 cx.simulate_keystroke(".");
12702 cx.executor().run_until_parked();
12703 cx.condition(|editor, _| editor.context_menu_visible())
12704 .await;
12705 cx.update_editor(|editor, window, cx| {
12706 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12707 {
12708 assert_eq!(
12709 completion_menu_entries(&menu),
12710 &["first", "last"],
12711 "When LSP server is fast to reply, no fallback word completions are used"
12712 );
12713 } else {
12714 panic!("expected completion menu to be open");
12715 }
12716 editor.cancel(&Cancel, window, cx);
12717 });
12718 cx.executor().run_until_parked();
12719 cx.condition(|editor, _| !editor.context_menu_visible())
12720 .await;
12721
12722 throttle_completions.store(true, atomic::Ordering::Release);
12723 cx.simulate_keystroke(".");
12724 cx.executor()
12725 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
12726 cx.executor().run_until_parked();
12727 cx.condition(|editor, _| editor.context_menu_visible())
12728 .await;
12729 cx.update_editor(|editor, _, _| {
12730 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12731 {
12732 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
12733 "When LSP server is slow, document words can be shown instead, if configured accordingly");
12734 } else {
12735 panic!("expected completion menu to be open");
12736 }
12737 });
12738}
12739
12740#[gpui::test]
12741async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
12742 init_test(cx, |language_settings| {
12743 language_settings.defaults.completions = Some(CompletionSettings {
12744 words: WordsCompletionMode::Enabled,
12745 lsp: true,
12746 lsp_fetch_timeout_ms: 0,
12747 lsp_insert_mode: LspInsertMode::Insert,
12748 });
12749 });
12750
12751 let mut cx = EditorLspTestContext::new_rust(
12752 lsp::ServerCapabilities {
12753 completion_provider: Some(lsp::CompletionOptions {
12754 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12755 ..lsp::CompletionOptions::default()
12756 }),
12757 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12758 ..lsp::ServerCapabilities::default()
12759 },
12760 cx,
12761 )
12762 .await;
12763
12764 let _completion_requests_handler =
12765 cx.lsp
12766 .server
12767 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12768 Ok(Some(lsp::CompletionResponse::Array(vec![
12769 lsp::CompletionItem {
12770 label: "first".into(),
12771 ..lsp::CompletionItem::default()
12772 },
12773 lsp::CompletionItem {
12774 label: "last".into(),
12775 ..lsp::CompletionItem::default()
12776 },
12777 ])))
12778 });
12779
12780 cx.set_state(indoc! {"ˇ
12781 first
12782 last
12783 second
12784 "});
12785 cx.simulate_keystroke(".");
12786 cx.executor().run_until_parked();
12787 cx.condition(|editor, _| editor.context_menu_visible())
12788 .await;
12789 cx.update_editor(|editor, _, _| {
12790 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12791 {
12792 assert_eq!(
12793 completion_menu_entries(&menu),
12794 &["first", "last", "second"],
12795 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
12796 );
12797 } else {
12798 panic!("expected completion menu to be open");
12799 }
12800 });
12801}
12802
12803#[gpui::test]
12804async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
12805 init_test(cx, |language_settings| {
12806 language_settings.defaults.completions = Some(CompletionSettings {
12807 words: WordsCompletionMode::Disabled,
12808 lsp: true,
12809 lsp_fetch_timeout_ms: 0,
12810 lsp_insert_mode: LspInsertMode::Insert,
12811 });
12812 });
12813
12814 let mut cx = EditorLspTestContext::new_rust(
12815 lsp::ServerCapabilities {
12816 completion_provider: Some(lsp::CompletionOptions {
12817 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12818 ..lsp::CompletionOptions::default()
12819 }),
12820 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12821 ..lsp::ServerCapabilities::default()
12822 },
12823 cx,
12824 )
12825 .await;
12826
12827 let _completion_requests_handler =
12828 cx.lsp
12829 .server
12830 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12831 panic!("LSP completions should not be queried when dealing with word completions")
12832 });
12833
12834 cx.set_state(indoc! {"ˇ
12835 first
12836 last
12837 second
12838 "});
12839 cx.update_editor(|editor, window, cx| {
12840 editor.show_word_completions(&ShowWordCompletions, window, cx);
12841 });
12842 cx.executor().run_until_parked();
12843 cx.condition(|editor, _| editor.context_menu_visible())
12844 .await;
12845 cx.update_editor(|editor, _, _| {
12846 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12847 {
12848 assert_eq!(
12849 completion_menu_entries(&menu),
12850 &["first", "last", "second"],
12851 "`ShowWordCompletions` action should show word completions"
12852 );
12853 } else {
12854 panic!("expected completion menu to be open");
12855 }
12856 });
12857
12858 cx.simulate_keystroke("l");
12859 cx.executor().run_until_parked();
12860 cx.condition(|editor, _| editor.context_menu_visible())
12861 .await;
12862 cx.update_editor(|editor, _, _| {
12863 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12864 {
12865 assert_eq!(
12866 completion_menu_entries(&menu),
12867 &["last"],
12868 "After showing word completions, further editing should filter them and not query the LSP"
12869 );
12870 } else {
12871 panic!("expected completion menu to be open");
12872 }
12873 });
12874}
12875
12876#[gpui::test]
12877async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
12878 init_test(cx, |language_settings| {
12879 language_settings.defaults.completions = Some(CompletionSettings {
12880 words: WordsCompletionMode::Fallback,
12881 lsp: false,
12882 lsp_fetch_timeout_ms: 0,
12883 lsp_insert_mode: LspInsertMode::Insert,
12884 });
12885 });
12886
12887 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12888
12889 cx.set_state(indoc! {"ˇ
12890 0_usize
12891 let
12892 33
12893 4.5f32
12894 "});
12895 cx.update_editor(|editor, window, cx| {
12896 editor.show_completions(&ShowCompletions::default(), window, cx);
12897 });
12898 cx.executor().run_until_parked();
12899 cx.condition(|editor, _| editor.context_menu_visible())
12900 .await;
12901 cx.update_editor(|editor, window, cx| {
12902 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12903 {
12904 assert_eq!(
12905 completion_menu_entries(&menu),
12906 &["let"],
12907 "With no digits in the completion query, no digits should be in the word completions"
12908 );
12909 } else {
12910 panic!("expected completion menu to be open");
12911 }
12912 editor.cancel(&Cancel, window, cx);
12913 });
12914
12915 cx.set_state(indoc! {"3ˇ
12916 0_usize
12917 let
12918 3
12919 33.35f32
12920 "});
12921 cx.update_editor(|editor, window, cx| {
12922 editor.show_completions(&ShowCompletions::default(), window, cx);
12923 });
12924 cx.executor().run_until_parked();
12925 cx.condition(|editor, _| editor.context_menu_visible())
12926 .await;
12927 cx.update_editor(|editor, _, _| {
12928 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12929 {
12930 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
12931 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
12932 } else {
12933 panic!("expected completion menu to be open");
12934 }
12935 });
12936}
12937
12938fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
12939 let position = || lsp::Position {
12940 line: params.text_document_position.position.line,
12941 character: params.text_document_position.position.character,
12942 };
12943 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12944 range: lsp::Range {
12945 start: position(),
12946 end: position(),
12947 },
12948 new_text: text.to_string(),
12949 }))
12950}
12951
12952#[gpui::test]
12953async fn test_multiline_completion(cx: &mut TestAppContext) {
12954 init_test(cx, |_| {});
12955
12956 let fs = FakeFs::new(cx.executor());
12957 fs.insert_tree(
12958 path!("/a"),
12959 json!({
12960 "main.ts": "a",
12961 }),
12962 )
12963 .await;
12964
12965 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12966 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12967 let typescript_language = Arc::new(Language::new(
12968 LanguageConfig {
12969 name: "TypeScript".into(),
12970 matcher: LanguageMatcher {
12971 path_suffixes: vec!["ts".to_string()],
12972 ..LanguageMatcher::default()
12973 },
12974 line_comments: vec!["// ".into()],
12975 ..LanguageConfig::default()
12976 },
12977 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12978 ));
12979 language_registry.add(typescript_language.clone());
12980 let mut fake_servers = language_registry.register_fake_lsp(
12981 "TypeScript",
12982 FakeLspAdapter {
12983 capabilities: lsp::ServerCapabilities {
12984 completion_provider: Some(lsp::CompletionOptions {
12985 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12986 ..lsp::CompletionOptions::default()
12987 }),
12988 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12989 ..lsp::ServerCapabilities::default()
12990 },
12991 // Emulate vtsls label generation
12992 label_for_completion: Some(Box::new(|item, _| {
12993 let text = if let Some(description) = item
12994 .label_details
12995 .as_ref()
12996 .and_then(|label_details| label_details.description.as_ref())
12997 {
12998 format!("{} {}", item.label, description)
12999 } else if let Some(detail) = &item.detail {
13000 format!("{} {}", item.label, detail)
13001 } else {
13002 item.label.clone()
13003 };
13004 let len = text.len();
13005 Some(language::CodeLabel {
13006 text,
13007 runs: Vec::new(),
13008 filter_range: 0..len,
13009 })
13010 })),
13011 ..FakeLspAdapter::default()
13012 },
13013 );
13014 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13015 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13016 let worktree_id = workspace
13017 .update(cx, |workspace, _window, cx| {
13018 workspace.project().update(cx, |project, cx| {
13019 project.worktrees(cx).next().unwrap().read(cx).id()
13020 })
13021 })
13022 .unwrap();
13023 let _buffer = project
13024 .update(cx, |project, cx| {
13025 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
13026 })
13027 .await
13028 .unwrap();
13029 let editor = workspace
13030 .update(cx, |workspace, window, cx| {
13031 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
13032 })
13033 .unwrap()
13034 .await
13035 .unwrap()
13036 .downcast::<Editor>()
13037 .unwrap();
13038 let fake_server = fake_servers.next().await.unwrap();
13039
13040 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
13041 let multiline_label_2 = "a\nb\nc\n";
13042 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
13043 let multiline_description = "d\ne\nf\n";
13044 let multiline_detail_2 = "g\nh\ni\n";
13045
13046 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
13047 move |params, _| async move {
13048 Ok(Some(lsp::CompletionResponse::Array(vec![
13049 lsp::CompletionItem {
13050 label: multiline_label.to_string(),
13051 text_edit: gen_text_edit(¶ms, "new_text_1"),
13052 ..lsp::CompletionItem::default()
13053 },
13054 lsp::CompletionItem {
13055 label: "single line label 1".to_string(),
13056 detail: Some(multiline_detail.to_string()),
13057 text_edit: gen_text_edit(¶ms, "new_text_2"),
13058 ..lsp::CompletionItem::default()
13059 },
13060 lsp::CompletionItem {
13061 label: "single line label 2".to_string(),
13062 label_details: Some(lsp::CompletionItemLabelDetails {
13063 description: Some(multiline_description.to_string()),
13064 detail: None,
13065 }),
13066 text_edit: gen_text_edit(¶ms, "new_text_2"),
13067 ..lsp::CompletionItem::default()
13068 },
13069 lsp::CompletionItem {
13070 label: multiline_label_2.to_string(),
13071 detail: Some(multiline_detail_2.to_string()),
13072 text_edit: gen_text_edit(¶ms, "new_text_3"),
13073 ..lsp::CompletionItem::default()
13074 },
13075 lsp::CompletionItem {
13076 label: "Label with many spaces and \t but without newlines".to_string(),
13077 detail: Some(
13078 "Details with many spaces and \t but without newlines".to_string(),
13079 ),
13080 text_edit: gen_text_edit(¶ms, "new_text_4"),
13081 ..lsp::CompletionItem::default()
13082 },
13083 ])))
13084 },
13085 );
13086
13087 editor.update_in(cx, |editor, window, cx| {
13088 cx.focus_self(window);
13089 editor.move_to_end(&MoveToEnd, window, cx);
13090 editor.handle_input(".", window, cx);
13091 });
13092 cx.run_until_parked();
13093 completion_handle.next().await.unwrap();
13094
13095 editor.update(cx, |editor, _| {
13096 assert!(editor.context_menu_visible());
13097 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13098 {
13099 let completion_labels = menu
13100 .completions
13101 .borrow()
13102 .iter()
13103 .map(|c| c.label.text.clone())
13104 .collect::<Vec<_>>();
13105 assert_eq!(
13106 completion_labels,
13107 &[
13108 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
13109 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
13110 "single line label 2 d e f ",
13111 "a b c g h i ",
13112 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
13113 ],
13114 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
13115 );
13116
13117 for completion in menu
13118 .completions
13119 .borrow()
13120 .iter() {
13121 assert_eq!(
13122 completion.label.filter_range,
13123 0..completion.label.text.len(),
13124 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
13125 );
13126 }
13127 } else {
13128 panic!("expected completion menu to be open");
13129 }
13130 });
13131}
13132
13133#[gpui::test]
13134async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
13135 init_test(cx, |_| {});
13136 let mut cx = EditorLspTestContext::new_rust(
13137 lsp::ServerCapabilities {
13138 completion_provider: Some(lsp::CompletionOptions {
13139 trigger_characters: Some(vec![".".to_string()]),
13140 ..Default::default()
13141 }),
13142 ..Default::default()
13143 },
13144 cx,
13145 )
13146 .await;
13147 cx.lsp
13148 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13149 Ok(Some(lsp::CompletionResponse::Array(vec![
13150 lsp::CompletionItem {
13151 label: "first".into(),
13152 ..Default::default()
13153 },
13154 lsp::CompletionItem {
13155 label: "last".into(),
13156 ..Default::default()
13157 },
13158 ])))
13159 });
13160 cx.set_state("variableˇ");
13161 cx.simulate_keystroke(".");
13162 cx.executor().run_until_parked();
13163
13164 cx.update_editor(|editor, _, _| {
13165 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13166 {
13167 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
13168 } else {
13169 panic!("expected completion menu to be open");
13170 }
13171 });
13172
13173 cx.update_editor(|editor, window, cx| {
13174 editor.move_page_down(&MovePageDown::default(), window, cx);
13175 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13176 {
13177 assert!(
13178 menu.selected_item == 1,
13179 "expected PageDown to select the last item from the context menu"
13180 );
13181 } else {
13182 panic!("expected completion menu to stay open after PageDown");
13183 }
13184 });
13185
13186 cx.update_editor(|editor, window, cx| {
13187 editor.move_page_up(&MovePageUp::default(), window, cx);
13188 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13189 {
13190 assert!(
13191 menu.selected_item == 0,
13192 "expected PageUp to select the first item from the context menu"
13193 );
13194 } else {
13195 panic!("expected completion menu to stay open after PageUp");
13196 }
13197 });
13198}
13199
13200#[gpui::test]
13201async fn test_as_is_completions(cx: &mut TestAppContext) {
13202 init_test(cx, |_| {});
13203 let mut cx = EditorLspTestContext::new_rust(
13204 lsp::ServerCapabilities {
13205 completion_provider: Some(lsp::CompletionOptions {
13206 ..Default::default()
13207 }),
13208 ..Default::default()
13209 },
13210 cx,
13211 )
13212 .await;
13213 cx.lsp
13214 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13215 Ok(Some(lsp::CompletionResponse::Array(vec![
13216 lsp::CompletionItem {
13217 label: "unsafe".into(),
13218 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13219 range: lsp::Range {
13220 start: lsp::Position {
13221 line: 1,
13222 character: 2,
13223 },
13224 end: lsp::Position {
13225 line: 1,
13226 character: 3,
13227 },
13228 },
13229 new_text: "unsafe".to_string(),
13230 })),
13231 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
13232 ..Default::default()
13233 },
13234 ])))
13235 });
13236 cx.set_state("fn a() {}\n nˇ");
13237 cx.executor().run_until_parked();
13238 cx.update_editor(|editor, window, cx| {
13239 editor.show_completions(
13240 &ShowCompletions {
13241 trigger: Some("\n".into()),
13242 },
13243 window,
13244 cx,
13245 );
13246 });
13247 cx.executor().run_until_parked();
13248
13249 cx.update_editor(|editor, window, cx| {
13250 editor.confirm_completion(&Default::default(), window, cx)
13251 });
13252 cx.executor().run_until_parked();
13253 cx.assert_editor_state("fn a() {}\n unsafeˇ");
13254}
13255
13256#[gpui::test]
13257async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
13258 init_test(cx, |_| {});
13259
13260 let mut cx = EditorLspTestContext::new_rust(
13261 lsp::ServerCapabilities {
13262 completion_provider: Some(lsp::CompletionOptions {
13263 trigger_characters: Some(vec![".".to_string()]),
13264 resolve_provider: Some(true),
13265 ..Default::default()
13266 }),
13267 ..Default::default()
13268 },
13269 cx,
13270 )
13271 .await;
13272
13273 cx.set_state("fn main() { let a = 2ˇ; }");
13274 cx.simulate_keystroke(".");
13275 let completion_item = lsp::CompletionItem {
13276 label: "Some".into(),
13277 kind: Some(lsp::CompletionItemKind::SNIPPET),
13278 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13279 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13280 kind: lsp::MarkupKind::Markdown,
13281 value: "```rust\nSome(2)\n```".to_string(),
13282 })),
13283 deprecated: Some(false),
13284 sort_text: Some("Some".to_string()),
13285 filter_text: Some("Some".to_string()),
13286 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13287 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13288 range: lsp::Range {
13289 start: lsp::Position {
13290 line: 0,
13291 character: 22,
13292 },
13293 end: lsp::Position {
13294 line: 0,
13295 character: 22,
13296 },
13297 },
13298 new_text: "Some(2)".to_string(),
13299 })),
13300 additional_text_edits: Some(vec![lsp::TextEdit {
13301 range: lsp::Range {
13302 start: lsp::Position {
13303 line: 0,
13304 character: 20,
13305 },
13306 end: lsp::Position {
13307 line: 0,
13308 character: 22,
13309 },
13310 },
13311 new_text: "".to_string(),
13312 }]),
13313 ..Default::default()
13314 };
13315
13316 let closure_completion_item = completion_item.clone();
13317 let counter = Arc::new(AtomicUsize::new(0));
13318 let counter_clone = counter.clone();
13319 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13320 let task_completion_item = closure_completion_item.clone();
13321 counter_clone.fetch_add(1, atomic::Ordering::Release);
13322 async move {
13323 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13324 is_incomplete: true,
13325 item_defaults: None,
13326 items: vec![task_completion_item],
13327 })))
13328 }
13329 });
13330
13331 cx.condition(|editor, _| editor.context_menu_visible())
13332 .await;
13333 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
13334 assert!(request.next().await.is_some());
13335 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13336
13337 cx.simulate_keystrokes("S o m");
13338 cx.condition(|editor, _| editor.context_menu_visible())
13339 .await;
13340 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
13341 assert!(request.next().await.is_some());
13342 assert!(request.next().await.is_some());
13343 assert!(request.next().await.is_some());
13344 request.close();
13345 assert!(request.next().await.is_none());
13346 assert_eq!(
13347 counter.load(atomic::Ordering::Acquire),
13348 4,
13349 "With the completions menu open, only one LSP request should happen per input"
13350 );
13351}
13352
13353#[gpui::test]
13354async fn test_toggle_comment(cx: &mut TestAppContext) {
13355 init_test(cx, |_| {});
13356 let mut cx = EditorTestContext::new(cx).await;
13357 let language = Arc::new(Language::new(
13358 LanguageConfig {
13359 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13360 ..Default::default()
13361 },
13362 Some(tree_sitter_rust::LANGUAGE.into()),
13363 ));
13364 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13365
13366 // If multiple selections intersect a line, the line is only toggled once.
13367 cx.set_state(indoc! {"
13368 fn a() {
13369 «//b();
13370 ˇ»// «c();
13371 //ˇ» d();
13372 }
13373 "});
13374
13375 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13376
13377 cx.assert_editor_state(indoc! {"
13378 fn a() {
13379 «b();
13380 c();
13381 ˇ» d();
13382 }
13383 "});
13384
13385 // The comment prefix is inserted at the same column for every line in a
13386 // selection.
13387 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13388
13389 cx.assert_editor_state(indoc! {"
13390 fn a() {
13391 // «b();
13392 // c();
13393 ˇ»// d();
13394 }
13395 "});
13396
13397 // If a selection ends at the beginning of a line, that line is not toggled.
13398 cx.set_selections_state(indoc! {"
13399 fn a() {
13400 // b();
13401 «// c();
13402 ˇ» // d();
13403 }
13404 "});
13405
13406 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13407
13408 cx.assert_editor_state(indoc! {"
13409 fn a() {
13410 // b();
13411 «c();
13412 ˇ» // d();
13413 }
13414 "});
13415
13416 // If a selection span a single line and is empty, the line is toggled.
13417 cx.set_state(indoc! {"
13418 fn a() {
13419 a();
13420 b();
13421 ˇ
13422 }
13423 "});
13424
13425 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13426
13427 cx.assert_editor_state(indoc! {"
13428 fn a() {
13429 a();
13430 b();
13431 //•ˇ
13432 }
13433 "});
13434
13435 // If a selection span multiple lines, empty lines are not toggled.
13436 cx.set_state(indoc! {"
13437 fn a() {
13438 «a();
13439
13440 c();ˇ»
13441 }
13442 "});
13443
13444 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13445
13446 cx.assert_editor_state(indoc! {"
13447 fn a() {
13448 // «a();
13449
13450 // c();ˇ»
13451 }
13452 "});
13453
13454 // If a selection includes multiple comment prefixes, all lines are uncommented.
13455 cx.set_state(indoc! {"
13456 fn a() {
13457 «// a();
13458 /// b();
13459 //! c();ˇ»
13460 }
13461 "});
13462
13463 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13464
13465 cx.assert_editor_state(indoc! {"
13466 fn a() {
13467 «a();
13468 b();
13469 c();ˇ»
13470 }
13471 "});
13472}
13473
13474#[gpui::test]
13475async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
13476 init_test(cx, |_| {});
13477 let mut cx = EditorTestContext::new(cx).await;
13478 let language = Arc::new(Language::new(
13479 LanguageConfig {
13480 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13481 ..Default::default()
13482 },
13483 Some(tree_sitter_rust::LANGUAGE.into()),
13484 ));
13485 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13486
13487 let toggle_comments = &ToggleComments {
13488 advance_downwards: false,
13489 ignore_indent: true,
13490 };
13491
13492 // If multiple selections intersect a line, the line is only toggled once.
13493 cx.set_state(indoc! {"
13494 fn a() {
13495 // «b();
13496 // c();
13497 // ˇ» d();
13498 }
13499 "});
13500
13501 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13502
13503 cx.assert_editor_state(indoc! {"
13504 fn a() {
13505 «b();
13506 c();
13507 ˇ» d();
13508 }
13509 "});
13510
13511 // The comment prefix is inserted at the beginning of each line
13512 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13513
13514 cx.assert_editor_state(indoc! {"
13515 fn a() {
13516 // «b();
13517 // c();
13518 // ˇ» d();
13519 }
13520 "});
13521
13522 // If a selection ends at the beginning of a line, that line is not toggled.
13523 cx.set_selections_state(indoc! {"
13524 fn a() {
13525 // b();
13526 // «c();
13527 ˇ»// d();
13528 }
13529 "});
13530
13531 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13532
13533 cx.assert_editor_state(indoc! {"
13534 fn a() {
13535 // b();
13536 «c();
13537 ˇ»// d();
13538 }
13539 "});
13540
13541 // If a selection span a single line and is empty, the line is toggled.
13542 cx.set_state(indoc! {"
13543 fn a() {
13544 a();
13545 b();
13546 ˇ
13547 }
13548 "});
13549
13550 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13551
13552 cx.assert_editor_state(indoc! {"
13553 fn a() {
13554 a();
13555 b();
13556 //ˇ
13557 }
13558 "});
13559
13560 // If a selection span multiple lines, empty lines are not toggled.
13561 cx.set_state(indoc! {"
13562 fn a() {
13563 «a();
13564
13565 c();ˇ»
13566 }
13567 "});
13568
13569 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13570
13571 cx.assert_editor_state(indoc! {"
13572 fn a() {
13573 // «a();
13574
13575 // c();ˇ»
13576 }
13577 "});
13578
13579 // If a selection includes multiple comment prefixes, all lines are uncommented.
13580 cx.set_state(indoc! {"
13581 fn a() {
13582 // «a();
13583 /// b();
13584 //! c();ˇ»
13585 }
13586 "});
13587
13588 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13589
13590 cx.assert_editor_state(indoc! {"
13591 fn a() {
13592 «a();
13593 b();
13594 c();ˇ»
13595 }
13596 "});
13597}
13598
13599#[gpui::test]
13600async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
13601 init_test(cx, |_| {});
13602
13603 let language = Arc::new(Language::new(
13604 LanguageConfig {
13605 line_comments: vec!["// ".into()],
13606 ..Default::default()
13607 },
13608 Some(tree_sitter_rust::LANGUAGE.into()),
13609 ));
13610
13611 let mut cx = EditorTestContext::new(cx).await;
13612
13613 cx.language_registry().add(language.clone());
13614 cx.update_buffer(|buffer, cx| {
13615 buffer.set_language(Some(language), cx);
13616 });
13617
13618 let toggle_comments = &ToggleComments {
13619 advance_downwards: true,
13620 ignore_indent: false,
13621 };
13622
13623 // Single cursor on one line -> advance
13624 // Cursor moves horizontally 3 characters as well on non-blank line
13625 cx.set_state(indoc!(
13626 "fn a() {
13627 ˇdog();
13628 cat();
13629 }"
13630 ));
13631 cx.update_editor(|editor, window, cx| {
13632 editor.toggle_comments(toggle_comments, window, cx);
13633 });
13634 cx.assert_editor_state(indoc!(
13635 "fn a() {
13636 // dog();
13637 catˇ();
13638 }"
13639 ));
13640
13641 // Single selection on one line -> don't advance
13642 cx.set_state(indoc!(
13643 "fn a() {
13644 «dog()ˇ»;
13645 cat();
13646 }"
13647 ));
13648 cx.update_editor(|editor, window, cx| {
13649 editor.toggle_comments(toggle_comments, window, cx);
13650 });
13651 cx.assert_editor_state(indoc!(
13652 "fn a() {
13653 // «dog()ˇ»;
13654 cat();
13655 }"
13656 ));
13657
13658 // Multiple cursors on one line -> advance
13659 cx.set_state(indoc!(
13660 "fn a() {
13661 ˇdˇog();
13662 cat();
13663 }"
13664 ));
13665 cx.update_editor(|editor, window, cx| {
13666 editor.toggle_comments(toggle_comments, window, cx);
13667 });
13668 cx.assert_editor_state(indoc!(
13669 "fn a() {
13670 // dog();
13671 catˇ(ˇ);
13672 }"
13673 ));
13674
13675 // Multiple cursors on one line, with selection -> don't advance
13676 cx.set_state(indoc!(
13677 "fn a() {
13678 ˇdˇog«()ˇ»;
13679 cat();
13680 }"
13681 ));
13682 cx.update_editor(|editor, window, cx| {
13683 editor.toggle_comments(toggle_comments, window, cx);
13684 });
13685 cx.assert_editor_state(indoc!(
13686 "fn a() {
13687 // ˇdˇog«()ˇ»;
13688 cat();
13689 }"
13690 ));
13691
13692 // Single cursor on one line -> advance
13693 // Cursor moves to column 0 on blank line
13694 cx.set_state(indoc!(
13695 "fn a() {
13696 ˇdog();
13697
13698 cat();
13699 }"
13700 ));
13701 cx.update_editor(|editor, window, cx| {
13702 editor.toggle_comments(toggle_comments, window, cx);
13703 });
13704 cx.assert_editor_state(indoc!(
13705 "fn a() {
13706 // dog();
13707 ˇ
13708 cat();
13709 }"
13710 ));
13711
13712 // Single cursor on one line -> advance
13713 // Cursor starts and ends at column 0
13714 cx.set_state(indoc!(
13715 "fn a() {
13716 ˇ dog();
13717 cat();
13718 }"
13719 ));
13720 cx.update_editor(|editor, window, cx| {
13721 editor.toggle_comments(toggle_comments, window, cx);
13722 });
13723 cx.assert_editor_state(indoc!(
13724 "fn a() {
13725 // dog();
13726 ˇ cat();
13727 }"
13728 ));
13729}
13730
13731#[gpui::test]
13732async fn test_toggle_block_comment(cx: &mut TestAppContext) {
13733 init_test(cx, |_| {});
13734
13735 let mut cx = EditorTestContext::new(cx).await;
13736
13737 let html_language = Arc::new(
13738 Language::new(
13739 LanguageConfig {
13740 name: "HTML".into(),
13741 block_comment: Some(("<!-- ".into(), " -->".into())),
13742 ..Default::default()
13743 },
13744 Some(tree_sitter_html::LANGUAGE.into()),
13745 )
13746 .with_injection_query(
13747 r#"
13748 (script_element
13749 (raw_text) @injection.content
13750 (#set! injection.language "javascript"))
13751 "#,
13752 )
13753 .unwrap(),
13754 );
13755
13756 let javascript_language = Arc::new(Language::new(
13757 LanguageConfig {
13758 name: "JavaScript".into(),
13759 line_comments: vec!["// ".into()],
13760 ..Default::default()
13761 },
13762 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13763 ));
13764
13765 cx.language_registry().add(html_language.clone());
13766 cx.language_registry().add(javascript_language.clone());
13767 cx.update_buffer(|buffer, cx| {
13768 buffer.set_language(Some(html_language), cx);
13769 });
13770
13771 // Toggle comments for empty selections
13772 cx.set_state(
13773 &r#"
13774 <p>A</p>ˇ
13775 <p>B</p>ˇ
13776 <p>C</p>ˇ
13777 "#
13778 .unindent(),
13779 );
13780 cx.update_editor(|editor, window, cx| {
13781 editor.toggle_comments(&ToggleComments::default(), window, cx)
13782 });
13783 cx.assert_editor_state(
13784 &r#"
13785 <!-- <p>A</p>ˇ -->
13786 <!-- <p>B</p>ˇ -->
13787 <!-- <p>C</p>ˇ -->
13788 "#
13789 .unindent(),
13790 );
13791 cx.update_editor(|editor, window, cx| {
13792 editor.toggle_comments(&ToggleComments::default(), window, cx)
13793 });
13794 cx.assert_editor_state(
13795 &r#"
13796 <p>A</p>ˇ
13797 <p>B</p>ˇ
13798 <p>C</p>ˇ
13799 "#
13800 .unindent(),
13801 );
13802
13803 // Toggle comments for mixture of empty and non-empty selections, where
13804 // multiple selections occupy a given line.
13805 cx.set_state(
13806 &r#"
13807 <p>A«</p>
13808 <p>ˇ»B</p>ˇ
13809 <p>C«</p>
13810 <p>ˇ»D</p>ˇ
13811 "#
13812 .unindent(),
13813 );
13814
13815 cx.update_editor(|editor, window, cx| {
13816 editor.toggle_comments(&ToggleComments::default(), window, cx)
13817 });
13818 cx.assert_editor_state(
13819 &r#"
13820 <!-- <p>A«</p>
13821 <p>ˇ»B</p>ˇ -->
13822 <!-- <p>C«</p>
13823 <p>ˇ»D</p>ˇ -->
13824 "#
13825 .unindent(),
13826 );
13827 cx.update_editor(|editor, window, cx| {
13828 editor.toggle_comments(&ToggleComments::default(), window, cx)
13829 });
13830 cx.assert_editor_state(
13831 &r#"
13832 <p>A«</p>
13833 <p>ˇ»B</p>ˇ
13834 <p>C«</p>
13835 <p>ˇ»D</p>ˇ
13836 "#
13837 .unindent(),
13838 );
13839
13840 // Toggle comments when different languages are active for different
13841 // selections.
13842 cx.set_state(
13843 &r#"
13844 ˇ<script>
13845 ˇvar x = new Y();
13846 ˇ</script>
13847 "#
13848 .unindent(),
13849 );
13850 cx.executor().run_until_parked();
13851 cx.update_editor(|editor, window, cx| {
13852 editor.toggle_comments(&ToggleComments::default(), window, cx)
13853 });
13854 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
13855 // Uncommenting and commenting from this position brings in even more wrong artifacts.
13856 cx.assert_editor_state(
13857 &r#"
13858 <!-- ˇ<script> -->
13859 // ˇvar x = new Y();
13860 <!-- ˇ</script> -->
13861 "#
13862 .unindent(),
13863 );
13864}
13865
13866#[gpui::test]
13867fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
13868 init_test(cx, |_| {});
13869
13870 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13871 let multibuffer = cx.new(|cx| {
13872 let mut multibuffer = MultiBuffer::new(ReadWrite);
13873 multibuffer.push_excerpts(
13874 buffer.clone(),
13875 [
13876 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
13877 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
13878 ],
13879 cx,
13880 );
13881 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
13882 multibuffer
13883 });
13884
13885 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13886 editor.update_in(cx, |editor, window, cx| {
13887 assert_eq!(editor.text(cx), "aaaa\nbbbb");
13888 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13889 s.select_ranges([
13890 Point::new(0, 0)..Point::new(0, 0),
13891 Point::new(1, 0)..Point::new(1, 0),
13892 ])
13893 });
13894
13895 editor.handle_input("X", window, cx);
13896 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
13897 assert_eq!(
13898 editor.selections.ranges(cx),
13899 [
13900 Point::new(0, 1)..Point::new(0, 1),
13901 Point::new(1, 1)..Point::new(1, 1),
13902 ]
13903 );
13904
13905 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
13906 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13907 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
13908 });
13909 editor.backspace(&Default::default(), window, cx);
13910 assert_eq!(editor.text(cx), "Xa\nbbb");
13911 assert_eq!(
13912 editor.selections.ranges(cx),
13913 [Point::new(1, 0)..Point::new(1, 0)]
13914 );
13915
13916 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13917 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
13918 });
13919 editor.backspace(&Default::default(), window, cx);
13920 assert_eq!(editor.text(cx), "X\nbb");
13921 assert_eq!(
13922 editor.selections.ranges(cx),
13923 [Point::new(0, 1)..Point::new(0, 1)]
13924 );
13925 });
13926}
13927
13928#[gpui::test]
13929fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
13930 init_test(cx, |_| {});
13931
13932 let markers = vec![('[', ']').into(), ('(', ')').into()];
13933 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
13934 indoc! {"
13935 [aaaa
13936 (bbbb]
13937 cccc)",
13938 },
13939 markers.clone(),
13940 );
13941 let excerpt_ranges = markers.into_iter().map(|marker| {
13942 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
13943 ExcerptRange::new(context.clone())
13944 });
13945 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
13946 let multibuffer = cx.new(|cx| {
13947 let mut multibuffer = MultiBuffer::new(ReadWrite);
13948 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
13949 multibuffer
13950 });
13951
13952 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13953 editor.update_in(cx, |editor, window, cx| {
13954 let (expected_text, selection_ranges) = marked_text_ranges(
13955 indoc! {"
13956 aaaa
13957 bˇbbb
13958 bˇbbˇb
13959 cccc"
13960 },
13961 true,
13962 );
13963 assert_eq!(editor.text(cx), expected_text);
13964 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13965 s.select_ranges(selection_ranges)
13966 });
13967
13968 editor.handle_input("X", window, cx);
13969
13970 let (expected_text, expected_selections) = marked_text_ranges(
13971 indoc! {"
13972 aaaa
13973 bXˇbbXb
13974 bXˇbbXˇb
13975 cccc"
13976 },
13977 false,
13978 );
13979 assert_eq!(editor.text(cx), expected_text);
13980 assert_eq!(editor.selections.ranges(cx), expected_selections);
13981
13982 editor.newline(&Newline, window, cx);
13983 let (expected_text, expected_selections) = marked_text_ranges(
13984 indoc! {"
13985 aaaa
13986 bX
13987 ˇbbX
13988 b
13989 bX
13990 ˇbbX
13991 ˇb
13992 cccc"
13993 },
13994 false,
13995 );
13996 assert_eq!(editor.text(cx), expected_text);
13997 assert_eq!(editor.selections.ranges(cx), expected_selections);
13998 });
13999}
14000
14001#[gpui::test]
14002fn test_refresh_selections(cx: &mut TestAppContext) {
14003 init_test(cx, |_| {});
14004
14005 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14006 let mut excerpt1_id = None;
14007 let multibuffer = cx.new(|cx| {
14008 let mut multibuffer = MultiBuffer::new(ReadWrite);
14009 excerpt1_id = multibuffer
14010 .push_excerpts(
14011 buffer.clone(),
14012 [
14013 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14014 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14015 ],
14016 cx,
14017 )
14018 .into_iter()
14019 .next();
14020 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14021 multibuffer
14022 });
14023
14024 let editor = cx.add_window(|window, cx| {
14025 let mut editor = build_editor(multibuffer.clone(), window, cx);
14026 let snapshot = editor.snapshot(window, cx);
14027 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14028 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
14029 });
14030 editor.begin_selection(
14031 Point::new(2, 1).to_display_point(&snapshot),
14032 true,
14033 1,
14034 window,
14035 cx,
14036 );
14037 assert_eq!(
14038 editor.selections.ranges(cx),
14039 [
14040 Point::new(1, 3)..Point::new(1, 3),
14041 Point::new(2, 1)..Point::new(2, 1),
14042 ]
14043 );
14044 editor
14045 });
14046
14047 // Refreshing selections is a no-op when excerpts haven't changed.
14048 _ = editor.update(cx, |editor, window, cx| {
14049 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14050 assert_eq!(
14051 editor.selections.ranges(cx),
14052 [
14053 Point::new(1, 3)..Point::new(1, 3),
14054 Point::new(2, 1)..Point::new(2, 1),
14055 ]
14056 );
14057 });
14058
14059 multibuffer.update(cx, |multibuffer, cx| {
14060 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14061 });
14062 _ = editor.update(cx, |editor, window, cx| {
14063 // Removing an excerpt causes the first selection to become degenerate.
14064 assert_eq!(
14065 editor.selections.ranges(cx),
14066 [
14067 Point::new(0, 0)..Point::new(0, 0),
14068 Point::new(0, 1)..Point::new(0, 1)
14069 ]
14070 );
14071
14072 // Refreshing selections will relocate the first selection to the original buffer
14073 // location.
14074 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14075 assert_eq!(
14076 editor.selections.ranges(cx),
14077 [
14078 Point::new(0, 1)..Point::new(0, 1),
14079 Point::new(0, 3)..Point::new(0, 3)
14080 ]
14081 );
14082 assert!(editor.selections.pending_anchor().is_some());
14083 });
14084}
14085
14086#[gpui::test]
14087fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
14088 init_test(cx, |_| {});
14089
14090 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14091 let mut excerpt1_id = None;
14092 let multibuffer = cx.new(|cx| {
14093 let mut multibuffer = MultiBuffer::new(ReadWrite);
14094 excerpt1_id = multibuffer
14095 .push_excerpts(
14096 buffer.clone(),
14097 [
14098 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14099 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14100 ],
14101 cx,
14102 )
14103 .into_iter()
14104 .next();
14105 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14106 multibuffer
14107 });
14108
14109 let editor = cx.add_window(|window, cx| {
14110 let mut editor = build_editor(multibuffer.clone(), window, cx);
14111 let snapshot = editor.snapshot(window, cx);
14112 editor.begin_selection(
14113 Point::new(1, 3).to_display_point(&snapshot),
14114 false,
14115 1,
14116 window,
14117 cx,
14118 );
14119 assert_eq!(
14120 editor.selections.ranges(cx),
14121 [Point::new(1, 3)..Point::new(1, 3)]
14122 );
14123 editor
14124 });
14125
14126 multibuffer.update(cx, |multibuffer, cx| {
14127 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14128 });
14129 _ = editor.update(cx, |editor, window, cx| {
14130 assert_eq!(
14131 editor.selections.ranges(cx),
14132 [Point::new(0, 0)..Point::new(0, 0)]
14133 );
14134
14135 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
14136 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14137 assert_eq!(
14138 editor.selections.ranges(cx),
14139 [Point::new(0, 3)..Point::new(0, 3)]
14140 );
14141 assert!(editor.selections.pending_anchor().is_some());
14142 });
14143}
14144
14145#[gpui::test]
14146async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
14147 init_test(cx, |_| {});
14148
14149 let language = Arc::new(
14150 Language::new(
14151 LanguageConfig {
14152 brackets: BracketPairConfig {
14153 pairs: vec![
14154 BracketPair {
14155 start: "{".to_string(),
14156 end: "}".to_string(),
14157 close: true,
14158 surround: true,
14159 newline: true,
14160 },
14161 BracketPair {
14162 start: "/* ".to_string(),
14163 end: " */".to_string(),
14164 close: true,
14165 surround: true,
14166 newline: true,
14167 },
14168 ],
14169 ..Default::default()
14170 },
14171 ..Default::default()
14172 },
14173 Some(tree_sitter_rust::LANGUAGE.into()),
14174 )
14175 .with_indents_query("")
14176 .unwrap(),
14177 );
14178
14179 let text = concat!(
14180 "{ }\n", //
14181 " x\n", //
14182 " /* */\n", //
14183 "x\n", //
14184 "{{} }\n", //
14185 );
14186
14187 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
14188 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14189 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14190 editor
14191 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
14192 .await;
14193
14194 editor.update_in(cx, |editor, window, cx| {
14195 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14196 s.select_display_ranges([
14197 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
14198 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
14199 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
14200 ])
14201 });
14202 editor.newline(&Newline, window, cx);
14203
14204 assert_eq!(
14205 editor.buffer().read(cx).read(cx).text(),
14206 concat!(
14207 "{ \n", // Suppress rustfmt
14208 "\n", //
14209 "}\n", //
14210 " x\n", //
14211 " /* \n", //
14212 " \n", //
14213 " */\n", //
14214 "x\n", //
14215 "{{} \n", //
14216 "}\n", //
14217 )
14218 );
14219 });
14220}
14221
14222#[gpui::test]
14223fn test_highlighted_ranges(cx: &mut TestAppContext) {
14224 init_test(cx, |_| {});
14225
14226 let editor = cx.add_window(|window, cx| {
14227 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
14228 build_editor(buffer.clone(), window, cx)
14229 });
14230
14231 _ = editor.update(cx, |editor, window, cx| {
14232 struct Type1;
14233 struct Type2;
14234
14235 let buffer = editor.buffer.read(cx).snapshot(cx);
14236
14237 let anchor_range =
14238 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
14239
14240 editor.highlight_background::<Type1>(
14241 &[
14242 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
14243 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
14244 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
14245 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
14246 ],
14247 |_| Hsla::red(),
14248 cx,
14249 );
14250 editor.highlight_background::<Type2>(
14251 &[
14252 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
14253 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
14254 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
14255 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
14256 ],
14257 |_| Hsla::green(),
14258 cx,
14259 );
14260
14261 let snapshot = editor.snapshot(window, cx);
14262 let mut highlighted_ranges = editor.background_highlights_in_range(
14263 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
14264 &snapshot,
14265 cx.theme(),
14266 );
14267 // Enforce a consistent ordering based on color without relying on the ordering of the
14268 // highlight's `TypeId` which is non-executor.
14269 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
14270 assert_eq!(
14271 highlighted_ranges,
14272 &[
14273 (
14274 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
14275 Hsla::red(),
14276 ),
14277 (
14278 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14279 Hsla::red(),
14280 ),
14281 (
14282 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
14283 Hsla::green(),
14284 ),
14285 (
14286 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
14287 Hsla::green(),
14288 ),
14289 ]
14290 );
14291 assert_eq!(
14292 editor.background_highlights_in_range(
14293 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
14294 &snapshot,
14295 cx.theme(),
14296 ),
14297 &[(
14298 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14299 Hsla::red(),
14300 )]
14301 );
14302 });
14303}
14304
14305#[gpui::test]
14306async fn test_following(cx: &mut TestAppContext) {
14307 init_test(cx, |_| {});
14308
14309 let fs = FakeFs::new(cx.executor());
14310 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14311
14312 let buffer = project.update(cx, |project, cx| {
14313 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
14314 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
14315 });
14316 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
14317 let follower = cx.update(|cx| {
14318 cx.open_window(
14319 WindowOptions {
14320 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
14321 gpui::Point::new(px(0.), px(0.)),
14322 gpui::Point::new(px(10.), px(80.)),
14323 ))),
14324 ..Default::default()
14325 },
14326 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
14327 )
14328 .unwrap()
14329 });
14330
14331 let is_still_following = Rc::new(RefCell::new(true));
14332 let follower_edit_event_count = Rc::new(RefCell::new(0));
14333 let pending_update = Rc::new(RefCell::new(None));
14334 let leader_entity = leader.root(cx).unwrap();
14335 let follower_entity = follower.root(cx).unwrap();
14336 _ = follower.update(cx, {
14337 let update = pending_update.clone();
14338 let is_still_following = is_still_following.clone();
14339 let follower_edit_event_count = follower_edit_event_count.clone();
14340 |_, window, cx| {
14341 cx.subscribe_in(
14342 &leader_entity,
14343 window,
14344 move |_, leader, event, window, cx| {
14345 leader.read(cx).add_event_to_update_proto(
14346 event,
14347 &mut update.borrow_mut(),
14348 window,
14349 cx,
14350 );
14351 },
14352 )
14353 .detach();
14354
14355 cx.subscribe_in(
14356 &follower_entity,
14357 window,
14358 move |_, _, event: &EditorEvent, _window, _cx| {
14359 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
14360 *is_still_following.borrow_mut() = false;
14361 }
14362
14363 if let EditorEvent::BufferEdited = event {
14364 *follower_edit_event_count.borrow_mut() += 1;
14365 }
14366 },
14367 )
14368 .detach();
14369 }
14370 });
14371
14372 // Update the selections only
14373 _ = leader.update(cx, |leader, window, cx| {
14374 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14375 s.select_ranges([1..1])
14376 });
14377 });
14378 follower
14379 .update(cx, |follower, window, cx| {
14380 follower.apply_update_proto(
14381 &project,
14382 pending_update.borrow_mut().take().unwrap(),
14383 window,
14384 cx,
14385 )
14386 })
14387 .unwrap()
14388 .await
14389 .unwrap();
14390 _ = follower.update(cx, |follower, _, cx| {
14391 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
14392 });
14393 assert!(*is_still_following.borrow());
14394 assert_eq!(*follower_edit_event_count.borrow(), 0);
14395
14396 // Update the scroll position only
14397 _ = leader.update(cx, |leader, window, cx| {
14398 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14399 });
14400 follower
14401 .update(cx, |follower, window, cx| {
14402 follower.apply_update_proto(
14403 &project,
14404 pending_update.borrow_mut().take().unwrap(),
14405 window,
14406 cx,
14407 )
14408 })
14409 .unwrap()
14410 .await
14411 .unwrap();
14412 assert_eq!(
14413 follower
14414 .update(cx, |follower, _, cx| follower.scroll_position(cx))
14415 .unwrap(),
14416 gpui::Point::new(1.5, 3.5)
14417 );
14418 assert!(*is_still_following.borrow());
14419 assert_eq!(*follower_edit_event_count.borrow(), 0);
14420
14421 // Update the selections and scroll position. The follower's scroll position is updated
14422 // via autoscroll, not via the leader's exact scroll position.
14423 _ = leader.update(cx, |leader, window, cx| {
14424 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14425 s.select_ranges([0..0])
14426 });
14427 leader.request_autoscroll(Autoscroll::newest(), cx);
14428 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14429 });
14430 follower
14431 .update(cx, |follower, window, cx| {
14432 follower.apply_update_proto(
14433 &project,
14434 pending_update.borrow_mut().take().unwrap(),
14435 window,
14436 cx,
14437 )
14438 })
14439 .unwrap()
14440 .await
14441 .unwrap();
14442 _ = follower.update(cx, |follower, _, cx| {
14443 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
14444 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
14445 });
14446 assert!(*is_still_following.borrow());
14447
14448 // Creating a pending selection that precedes another selection
14449 _ = leader.update(cx, |leader, window, cx| {
14450 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14451 s.select_ranges([1..1])
14452 });
14453 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
14454 });
14455 follower
14456 .update(cx, |follower, window, cx| {
14457 follower.apply_update_proto(
14458 &project,
14459 pending_update.borrow_mut().take().unwrap(),
14460 window,
14461 cx,
14462 )
14463 })
14464 .unwrap()
14465 .await
14466 .unwrap();
14467 _ = follower.update(cx, |follower, _, cx| {
14468 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
14469 });
14470 assert!(*is_still_following.borrow());
14471
14472 // Extend the pending selection so that it surrounds another selection
14473 _ = leader.update(cx, |leader, window, cx| {
14474 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
14475 });
14476 follower
14477 .update(cx, |follower, window, cx| {
14478 follower.apply_update_proto(
14479 &project,
14480 pending_update.borrow_mut().take().unwrap(),
14481 window,
14482 cx,
14483 )
14484 })
14485 .unwrap()
14486 .await
14487 .unwrap();
14488 _ = follower.update(cx, |follower, _, cx| {
14489 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
14490 });
14491
14492 // Scrolling locally breaks the follow
14493 _ = follower.update(cx, |follower, window, cx| {
14494 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
14495 follower.set_scroll_anchor(
14496 ScrollAnchor {
14497 anchor: top_anchor,
14498 offset: gpui::Point::new(0.0, 0.5),
14499 },
14500 window,
14501 cx,
14502 );
14503 });
14504 assert!(!(*is_still_following.borrow()));
14505}
14506
14507#[gpui::test]
14508async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
14509 init_test(cx, |_| {});
14510
14511 let fs = FakeFs::new(cx.executor());
14512 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14513 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14514 let pane = workspace
14515 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14516 .unwrap();
14517
14518 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14519
14520 let leader = pane.update_in(cx, |_, window, cx| {
14521 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
14522 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
14523 });
14524
14525 // Start following the editor when it has no excerpts.
14526 let mut state_message =
14527 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14528 let workspace_entity = workspace.root(cx).unwrap();
14529 let follower_1 = cx
14530 .update_window(*workspace.deref(), |_, window, cx| {
14531 Editor::from_state_proto(
14532 workspace_entity,
14533 ViewId {
14534 creator: CollaboratorId::PeerId(PeerId::default()),
14535 id: 0,
14536 },
14537 &mut state_message,
14538 window,
14539 cx,
14540 )
14541 })
14542 .unwrap()
14543 .unwrap()
14544 .await
14545 .unwrap();
14546
14547 let update_message = Rc::new(RefCell::new(None));
14548 follower_1.update_in(cx, {
14549 let update = update_message.clone();
14550 |_, window, cx| {
14551 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
14552 leader.read(cx).add_event_to_update_proto(
14553 event,
14554 &mut update.borrow_mut(),
14555 window,
14556 cx,
14557 );
14558 })
14559 .detach();
14560 }
14561 });
14562
14563 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
14564 (
14565 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
14566 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
14567 )
14568 });
14569
14570 // Insert some excerpts.
14571 leader.update(cx, |leader, cx| {
14572 leader.buffer.update(cx, |multibuffer, cx| {
14573 multibuffer.set_excerpts_for_path(
14574 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
14575 buffer_1.clone(),
14576 vec![
14577 Point::row_range(0..3),
14578 Point::row_range(1..6),
14579 Point::row_range(12..15),
14580 ],
14581 0,
14582 cx,
14583 );
14584 multibuffer.set_excerpts_for_path(
14585 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
14586 buffer_2.clone(),
14587 vec![Point::row_range(0..6), Point::row_range(8..12)],
14588 0,
14589 cx,
14590 );
14591 });
14592 });
14593
14594 // Apply the update of adding the excerpts.
14595 follower_1
14596 .update_in(cx, |follower, window, cx| {
14597 follower.apply_update_proto(
14598 &project,
14599 update_message.borrow().clone().unwrap(),
14600 window,
14601 cx,
14602 )
14603 })
14604 .await
14605 .unwrap();
14606 assert_eq!(
14607 follower_1.update(cx, |editor, cx| editor.text(cx)),
14608 leader.update(cx, |editor, cx| editor.text(cx))
14609 );
14610 update_message.borrow_mut().take();
14611
14612 // Start following separately after it already has excerpts.
14613 let mut state_message =
14614 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14615 let workspace_entity = workspace.root(cx).unwrap();
14616 let follower_2 = cx
14617 .update_window(*workspace.deref(), |_, window, cx| {
14618 Editor::from_state_proto(
14619 workspace_entity,
14620 ViewId {
14621 creator: CollaboratorId::PeerId(PeerId::default()),
14622 id: 0,
14623 },
14624 &mut state_message,
14625 window,
14626 cx,
14627 )
14628 })
14629 .unwrap()
14630 .unwrap()
14631 .await
14632 .unwrap();
14633 assert_eq!(
14634 follower_2.update(cx, |editor, cx| editor.text(cx)),
14635 leader.update(cx, |editor, cx| editor.text(cx))
14636 );
14637
14638 // Remove some excerpts.
14639 leader.update(cx, |leader, cx| {
14640 leader.buffer.update(cx, |multibuffer, cx| {
14641 let excerpt_ids = multibuffer.excerpt_ids();
14642 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
14643 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
14644 });
14645 });
14646
14647 // Apply the update of removing the excerpts.
14648 follower_1
14649 .update_in(cx, |follower, window, cx| {
14650 follower.apply_update_proto(
14651 &project,
14652 update_message.borrow().clone().unwrap(),
14653 window,
14654 cx,
14655 )
14656 })
14657 .await
14658 .unwrap();
14659 follower_2
14660 .update_in(cx, |follower, window, cx| {
14661 follower.apply_update_proto(
14662 &project,
14663 update_message.borrow().clone().unwrap(),
14664 window,
14665 cx,
14666 )
14667 })
14668 .await
14669 .unwrap();
14670 update_message.borrow_mut().take();
14671 assert_eq!(
14672 follower_1.update(cx, |editor, cx| editor.text(cx)),
14673 leader.update(cx, |editor, cx| editor.text(cx))
14674 );
14675}
14676
14677#[gpui::test]
14678async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14679 init_test(cx, |_| {});
14680
14681 let mut cx = EditorTestContext::new(cx).await;
14682 let lsp_store =
14683 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
14684
14685 cx.set_state(indoc! {"
14686 ˇfn func(abc def: i32) -> u32 {
14687 }
14688 "});
14689
14690 cx.update(|_, cx| {
14691 lsp_store.update(cx, |lsp_store, cx| {
14692 lsp_store
14693 .update_diagnostics(
14694 LanguageServerId(0),
14695 lsp::PublishDiagnosticsParams {
14696 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
14697 version: None,
14698 diagnostics: vec![
14699 lsp::Diagnostic {
14700 range: lsp::Range::new(
14701 lsp::Position::new(0, 11),
14702 lsp::Position::new(0, 12),
14703 ),
14704 severity: Some(lsp::DiagnosticSeverity::ERROR),
14705 ..Default::default()
14706 },
14707 lsp::Diagnostic {
14708 range: lsp::Range::new(
14709 lsp::Position::new(0, 12),
14710 lsp::Position::new(0, 15),
14711 ),
14712 severity: Some(lsp::DiagnosticSeverity::ERROR),
14713 ..Default::default()
14714 },
14715 lsp::Diagnostic {
14716 range: lsp::Range::new(
14717 lsp::Position::new(0, 25),
14718 lsp::Position::new(0, 28),
14719 ),
14720 severity: Some(lsp::DiagnosticSeverity::ERROR),
14721 ..Default::default()
14722 },
14723 ],
14724 },
14725 None,
14726 DiagnosticSourceKind::Pushed,
14727 &[],
14728 cx,
14729 )
14730 .unwrap()
14731 });
14732 });
14733
14734 executor.run_until_parked();
14735
14736 cx.update_editor(|editor, window, cx| {
14737 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14738 });
14739
14740 cx.assert_editor_state(indoc! {"
14741 fn func(abc def: i32) -> ˇu32 {
14742 }
14743 "});
14744
14745 cx.update_editor(|editor, window, cx| {
14746 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14747 });
14748
14749 cx.assert_editor_state(indoc! {"
14750 fn func(abc ˇdef: i32) -> u32 {
14751 }
14752 "});
14753
14754 cx.update_editor(|editor, window, cx| {
14755 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14756 });
14757
14758 cx.assert_editor_state(indoc! {"
14759 fn func(abcˇ def: i32) -> u32 {
14760 }
14761 "});
14762
14763 cx.update_editor(|editor, window, cx| {
14764 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14765 });
14766
14767 cx.assert_editor_state(indoc! {"
14768 fn func(abc def: i32) -> ˇu32 {
14769 }
14770 "});
14771}
14772
14773#[gpui::test]
14774async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14775 init_test(cx, |_| {});
14776
14777 let mut cx = EditorTestContext::new(cx).await;
14778
14779 let diff_base = r#"
14780 use some::mod;
14781
14782 const A: u32 = 42;
14783
14784 fn main() {
14785 println!("hello");
14786
14787 println!("world");
14788 }
14789 "#
14790 .unindent();
14791
14792 // Edits are modified, removed, modified, added
14793 cx.set_state(
14794 &r#"
14795 use some::modified;
14796
14797 ˇ
14798 fn main() {
14799 println!("hello there");
14800
14801 println!("around the");
14802 println!("world");
14803 }
14804 "#
14805 .unindent(),
14806 );
14807
14808 cx.set_head_text(&diff_base);
14809 executor.run_until_parked();
14810
14811 cx.update_editor(|editor, window, cx| {
14812 //Wrap around the bottom of the buffer
14813 for _ in 0..3 {
14814 editor.go_to_next_hunk(&GoToHunk, window, cx);
14815 }
14816 });
14817
14818 cx.assert_editor_state(
14819 &r#"
14820 ˇuse some::modified;
14821
14822
14823 fn main() {
14824 println!("hello there");
14825
14826 println!("around the");
14827 println!("world");
14828 }
14829 "#
14830 .unindent(),
14831 );
14832
14833 cx.update_editor(|editor, window, cx| {
14834 //Wrap around the top of the buffer
14835 for _ in 0..2 {
14836 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14837 }
14838 });
14839
14840 cx.assert_editor_state(
14841 &r#"
14842 use some::modified;
14843
14844
14845 fn main() {
14846 ˇ println!("hello there");
14847
14848 println!("around the");
14849 println!("world");
14850 }
14851 "#
14852 .unindent(),
14853 );
14854
14855 cx.update_editor(|editor, window, cx| {
14856 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14857 });
14858
14859 cx.assert_editor_state(
14860 &r#"
14861 use some::modified;
14862
14863 ˇ
14864 fn main() {
14865 println!("hello there");
14866
14867 println!("around the");
14868 println!("world");
14869 }
14870 "#
14871 .unindent(),
14872 );
14873
14874 cx.update_editor(|editor, window, cx| {
14875 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14876 });
14877
14878 cx.assert_editor_state(
14879 &r#"
14880 ˇuse some::modified;
14881
14882
14883 fn main() {
14884 println!("hello there");
14885
14886 println!("around the");
14887 println!("world");
14888 }
14889 "#
14890 .unindent(),
14891 );
14892
14893 cx.update_editor(|editor, window, cx| {
14894 for _ in 0..2 {
14895 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14896 }
14897 });
14898
14899 cx.assert_editor_state(
14900 &r#"
14901 use some::modified;
14902
14903
14904 fn main() {
14905 ˇ println!("hello there");
14906
14907 println!("around the");
14908 println!("world");
14909 }
14910 "#
14911 .unindent(),
14912 );
14913
14914 cx.update_editor(|editor, window, cx| {
14915 editor.fold(&Fold, window, cx);
14916 });
14917
14918 cx.update_editor(|editor, window, cx| {
14919 editor.go_to_next_hunk(&GoToHunk, window, cx);
14920 });
14921
14922 cx.assert_editor_state(
14923 &r#"
14924 ˇuse some::modified;
14925
14926
14927 fn main() {
14928 println!("hello there");
14929
14930 println!("around the");
14931 println!("world");
14932 }
14933 "#
14934 .unindent(),
14935 );
14936}
14937
14938#[test]
14939fn test_split_words() {
14940 fn split(text: &str) -> Vec<&str> {
14941 split_words(text).collect()
14942 }
14943
14944 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
14945 assert_eq!(split("hello_world"), &["hello_", "world"]);
14946 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
14947 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
14948 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
14949 assert_eq!(split("helloworld"), &["helloworld"]);
14950
14951 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
14952}
14953
14954#[gpui::test]
14955async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
14956 init_test(cx, |_| {});
14957
14958 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
14959 let mut assert = |before, after| {
14960 let _state_context = cx.set_state(before);
14961 cx.run_until_parked();
14962 cx.update_editor(|editor, window, cx| {
14963 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
14964 });
14965 cx.run_until_parked();
14966 cx.assert_editor_state(after);
14967 };
14968
14969 // Outside bracket jumps to outside of matching bracket
14970 assert("console.logˇ(var);", "console.log(var)ˇ;");
14971 assert("console.log(var)ˇ;", "console.logˇ(var);");
14972
14973 // Inside bracket jumps to inside of matching bracket
14974 assert("console.log(ˇvar);", "console.log(varˇ);");
14975 assert("console.log(varˇ);", "console.log(ˇvar);");
14976
14977 // When outside a bracket and inside, favor jumping to the inside bracket
14978 assert(
14979 "console.log('foo', [1, 2, 3]ˇ);",
14980 "console.log(ˇ'foo', [1, 2, 3]);",
14981 );
14982 assert(
14983 "console.log(ˇ'foo', [1, 2, 3]);",
14984 "console.log('foo', [1, 2, 3]ˇ);",
14985 );
14986
14987 // Bias forward if two options are equally likely
14988 assert(
14989 "let result = curried_fun()ˇ();",
14990 "let result = curried_fun()()ˇ;",
14991 );
14992
14993 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
14994 assert(
14995 indoc! {"
14996 function test() {
14997 console.log('test')ˇ
14998 }"},
14999 indoc! {"
15000 function test() {
15001 console.logˇ('test')
15002 }"},
15003 );
15004}
15005
15006#[gpui::test]
15007async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
15008 init_test(cx, |_| {});
15009
15010 let fs = FakeFs::new(cx.executor());
15011 fs.insert_tree(
15012 path!("/a"),
15013 json!({
15014 "main.rs": "fn main() { let a = 5; }",
15015 "other.rs": "// Test file",
15016 }),
15017 )
15018 .await;
15019 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15020
15021 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15022 language_registry.add(Arc::new(Language::new(
15023 LanguageConfig {
15024 name: "Rust".into(),
15025 matcher: LanguageMatcher {
15026 path_suffixes: vec!["rs".to_string()],
15027 ..Default::default()
15028 },
15029 brackets: BracketPairConfig {
15030 pairs: vec![BracketPair {
15031 start: "{".to_string(),
15032 end: "}".to_string(),
15033 close: true,
15034 surround: true,
15035 newline: true,
15036 }],
15037 disabled_scopes_by_bracket_ix: Vec::new(),
15038 },
15039 ..Default::default()
15040 },
15041 Some(tree_sitter_rust::LANGUAGE.into()),
15042 )));
15043 let mut fake_servers = language_registry.register_fake_lsp(
15044 "Rust",
15045 FakeLspAdapter {
15046 capabilities: lsp::ServerCapabilities {
15047 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15048 first_trigger_character: "{".to_string(),
15049 more_trigger_character: None,
15050 }),
15051 ..Default::default()
15052 },
15053 ..Default::default()
15054 },
15055 );
15056
15057 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15058
15059 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15060
15061 let worktree_id = workspace
15062 .update(cx, |workspace, _, cx| {
15063 workspace.project().update(cx, |project, cx| {
15064 project.worktrees(cx).next().unwrap().read(cx).id()
15065 })
15066 })
15067 .unwrap();
15068
15069 let buffer = project
15070 .update(cx, |project, cx| {
15071 project.open_local_buffer(path!("/a/main.rs"), cx)
15072 })
15073 .await
15074 .unwrap();
15075 let editor_handle = workspace
15076 .update(cx, |workspace, window, cx| {
15077 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
15078 })
15079 .unwrap()
15080 .await
15081 .unwrap()
15082 .downcast::<Editor>()
15083 .unwrap();
15084
15085 cx.executor().start_waiting();
15086 let fake_server = fake_servers.next().await.unwrap();
15087
15088 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
15089 |params, _| async move {
15090 assert_eq!(
15091 params.text_document_position.text_document.uri,
15092 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
15093 );
15094 assert_eq!(
15095 params.text_document_position.position,
15096 lsp::Position::new(0, 21),
15097 );
15098
15099 Ok(Some(vec![lsp::TextEdit {
15100 new_text: "]".to_string(),
15101 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15102 }]))
15103 },
15104 );
15105
15106 editor_handle.update_in(cx, |editor, window, cx| {
15107 window.focus(&editor.focus_handle(cx));
15108 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15109 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
15110 });
15111 editor.handle_input("{", window, cx);
15112 });
15113
15114 cx.executor().run_until_parked();
15115
15116 buffer.update(cx, |buffer, _| {
15117 assert_eq!(
15118 buffer.text(),
15119 "fn main() { let a = {5}; }",
15120 "No extra braces from on type formatting should appear in the buffer"
15121 )
15122 });
15123}
15124
15125#[gpui::test(iterations = 20, seeds(31))]
15126async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
15127 init_test(cx, |_| {});
15128
15129 let mut cx = EditorLspTestContext::new_rust(
15130 lsp::ServerCapabilities {
15131 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15132 first_trigger_character: ".".to_string(),
15133 more_trigger_character: None,
15134 }),
15135 ..Default::default()
15136 },
15137 cx,
15138 )
15139 .await;
15140
15141 cx.update_buffer(|buffer, _| {
15142 // This causes autoindent to be async.
15143 buffer.set_sync_parse_timeout(Duration::ZERO)
15144 });
15145
15146 cx.set_state("fn c() {\n d()ˇ\n}\n");
15147 cx.simulate_keystroke("\n");
15148 cx.run_until_parked();
15149
15150 let buffer_cloned =
15151 cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap().clone());
15152 let mut request =
15153 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
15154 let buffer_cloned = buffer_cloned.clone();
15155 async move {
15156 buffer_cloned.update(&mut cx, |buffer, _| {
15157 assert_eq!(
15158 buffer.text(),
15159 "fn c() {\n d()\n .\n}\n",
15160 "OnTypeFormatting should triggered after autoindent applied"
15161 )
15162 })?;
15163
15164 Ok(Some(vec![]))
15165 }
15166 });
15167
15168 cx.simulate_keystroke(".");
15169 cx.run_until_parked();
15170
15171 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
15172 assert!(request.next().await.is_some());
15173 request.close();
15174 assert!(request.next().await.is_none());
15175}
15176
15177#[gpui::test]
15178async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
15179 init_test(cx, |_| {});
15180
15181 let fs = FakeFs::new(cx.executor());
15182 fs.insert_tree(
15183 path!("/a"),
15184 json!({
15185 "main.rs": "fn main() { let a = 5; }",
15186 "other.rs": "// Test file",
15187 }),
15188 )
15189 .await;
15190
15191 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15192
15193 let server_restarts = Arc::new(AtomicUsize::new(0));
15194 let closure_restarts = Arc::clone(&server_restarts);
15195 let language_server_name = "test language server";
15196 let language_name: LanguageName = "Rust".into();
15197
15198 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15199 language_registry.add(Arc::new(Language::new(
15200 LanguageConfig {
15201 name: language_name.clone(),
15202 matcher: LanguageMatcher {
15203 path_suffixes: vec!["rs".to_string()],
15204 ..Default::default()
15205 },
15206 ..Default::default()
15207 },
15208 Some(tree_sitter_rust::LANGUAGE.into()),
15209 )));
15210 let mut fake_servers = language_registry.register_fake_lsp(
15211 "Rust",
15212 FakeLspAdapter {
15213 name: language_server_name,
15214 initialization_options: Some(json!({
15215 "testOptionValue": true
15216 })),
15217 initializer: Some(Box::new(move |fake_server| {
15218 let task_restarts = Arc::clone(&closure_restarts);
15219 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
15220 task_restarts.fetch_add(1, atomic::Ordering::Release);
15221 futures::future::ready(Ok(()))
15222 });
15223 })),
15224 ..Default::default()
15225 },
15226 );
15227
15228 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15229 let _buffer = project
15230 .update(cx, |project, cx| {
15231 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
15232 })
15233 .await
15234 .unwrap();
15235 let _fake_server = fake_servers.next().await.unwrap();
15236 update_test_language_settings(cx, |language_settings| {
15237 language_settings.languages.0.insert(
15238 language_name.clone(),
15239 LanguageSettingsContent {
15240 tab_size: NonZeroU32::new(8),
15241 ..Default::default()
15242 },
15243 );
15244 });
15245 cx.executor().run_until_parked();
15246 assert_eq!(
15247 server_restarts.load(atomic::Ordering::Acquire),
15248 0,
15249 "Should not restart LSP server on an unrelated change"
15250 );
15251
15252 update_test_project_settings(cx, |project_settings| {
15253 project_settings.lsp.insert(
15254 "Some other server name".into(),
15255 LspSettings {
15256 binary: None,
15257 settings: None,
15258 initialization_options: Some(json!({
15259 "some other init value": false
15260 })),
15261 enable_lsp_tasks: false,
15262 },
15263 );
15264 });
15265 cx.executor().run_until_parked();
15266 assert_eq!(
15267 server_restarts.load(atomic::Ordering::Acquire),
15268 0,
15269 "Should not restart LSP server on an unrelated LSP settings change"
15270 );
15271
15272 update_test_project_settings(cx, |project_settings| {
15273 project_settings.lsp.insert(
15274 language_server_name.into(),
15275 LspSettings {
15276 binary: None,
15277 settings: None,
15278 initialization_options: Some(json!({
15279 "anotherInitValue": false
15280 })),
15281 enable_lsp_tasks: false,
15282 },
15283 );
15284 });
15285 cx.executor().run_until_parked();
15286 assert_eq!(
15287 server_restarts.load(atomic::Ordering::Acquire),
15288 1,
15289 "Should restart LSP server on a related LSP settings change"
15290 );
15291
15292 update_test_project_settings(cx, |project_settings| {
15293 project_settings.lsp.insert(
15294 language_server_name.into(),
15295 LspSettings {
15296 binary: None,
15297 settings: None,
15298 initialization_options: Some(json!({
15299 "anotherInitValue": false
15300 })),
15301 enable_lsp_tasks: false,
15302 },
15303 );
15304 });
15305 cx.executor().run_until_parked();
15306 assert_eq!(
15307 server_restarts.load(atomic::Ordering::Acquire),
15308 1,
15309 "Should not restart LSP server on a related LSP settings change that is the same"
15310 );
15311
15312 update_test_project_settings(cx, |project_settings| {
15313 project_settings.lsp.insert(
15314 language_server_name.into(),
15315 LspSettings {
15316 binary: None,
15317 settings: None,
15318 initialization_options: None,
15319 enable_lsp_tasks: false,
15320 },
15321 );
15322 });
15323 cx.executor().run_until_parked();
15324 assert_eq!(
15325 server_restarts.load(atomic::Ordering::Acquire),
15326 2,
15327 "Should restart LSP server on another related LSP settings change"
15328 );
15329}
15330
15331#[gpui::test]
15332async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
15333 init_test(cx, |_| {});
15334
15335 let mut cx = EditorLspTestContext::new_rust(
15336 lsp::ServerCapabilities {
15337 completion_provider: Some(lsp::CompletionOptions {
15338 trigger_characters: Some(vec![".".to_string()]),
15339 resolve_provider: Some(true),
15340 ..Default::default()
15341 }),
15342 ..Default::default()
15343 },
15344 cx,
15345 )
15346 .await;
15347
15348 cx.set_state("fn main() { let a = 2ˇ; }");
15349 cx.simulate_keystroke(".");
15350 let completion_item = lsp::CompletionItem {
15351 label: "some".into(),
15352 kind: Some(lsp::CompletionItemKind::SNIPPET),
15353 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15354 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15355 kind: lsp::MarkupKind::Markdown,
15356 value: "```rust\nSome(2)\n```".to_string(),
15357 })),
15358 deprecated: Some(false),
15359 sort_text: Some("fffffff2".to_string()),
15360 filter_text: Some("some".to_string()),
15361 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15362 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15363 range: lsp::Range {
15364 start: lsp::Position {
15365 line: 0,
15366 character: 22,
15367 },
15368 end: lsp::Position {
15369 line: 0,
15370 character: 22,
15371 },
15372 },
15373 new_text: "Some(2)".to_string(),
15374 })),
15375 additional_text_edits: Some(vec![lsp::TextEdit {
15376 range: lsp::Range {
15377 start: lsp::Position {
15378 line: 0,
15379 character: 20,
15380 },
15381 end: lsp::Position {
15382 line: 0,
15383 character: 22,
15384 },
15385 },
15386 new_text: "".to_string(),
15387 }]),
15388 ..Default::default()
15389 };
15390
15391 let closure_completion_item = completion_item.clone();
15392 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15393 let task_completion_item = closure_completion_item.clone();
15394 async move {
15395 Ok(Some(lsp::CompletionResponse::Array(vec![
15396 task_completion_item,
15397 ])))
15398 }
15399 });
15400
15401 request.next().await;
15402
15403 cx.condition(|editor, _| editor.context_menu_visible())
15404 .await;
15405 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15406 editor
15407 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15408 .unwrap()
15409 });
15410 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
15411
15412 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
15413 let task_completion_item = completion_item.clone();
15414 async move { Ok(task_completion_item) }
15415 })
15416 .next()
15417 .await
15418 .unwrap();
15419 apply_additional_edits.await.unwrap();
15420 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
15421}
15422
15423#[gpui::test]
15424async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
15425 init_test(cx, |_| {});
15426
15427 let mut cx = EditorLspTestContext::new_rust(
15428 lsp::ServerCapabilities {
15429 completion_provider: Some(lsp::CompletionOptions {
15430 trigger_characters: Some(vec![".".to_string()]),
15431 resolve_provider: Some(true),
15432 ..Default::default()
15433 }),
15434 ..Default::default()
15435 },
15436 cx,
15437 )
15438 .await;
15439
15440 cx.set_state("fn main() { let a = 2ˇ; }");
15441 cx.simulate_keystroke(".");
15442
15443 let item1 = lsp::CompletionItem {
15444 label: "method id()".to_string(),
15445 filter_text: Some("id".to_string()),
15446 detail: None,
15447 documentation: None,
15448 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15449 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15450 new_text: ".id".to_string(),
15451 })),
15452 ..lsp::CompletionItem::default()
15453 };
15454
15455 let item2 = lsp::CompletionItem {
15456 label: "other".to_string(),
15457 filter_text: Some("other".to_string()),
15458 detail: None,
15459 documentation: None,
15460 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15461 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15462 new_text: ".other".to_string(),
15463 })),
15464 ..lsp::CompletionItem::default()
15465 };
15466
15467 let item1 = item1.clone();
15468 cx.set_request_handler::<lsp::request::Completion, _, _>({
15469 let item1 = item1.clone();
15470 move |_, _, _| {
15471 let item1 = item1.clone();
15472 let item2 = item2.clone();
15473 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
15474 }
15475 })
15476 .next()
15477 .await;
15478
15479 cx.condition(|editor, _| editor.context_menu_visible())
15480 .await;
15481 cx.update_editor(|editor, _, _| {
15482 let context_menu = editor.context_menu.borrow_mut();
15483 let context_menu = context_menu
15484 .as_ref()
15485 .expect("Should have the context menu deployed");
15486 match context_menu {
15487 CodeContextMenu::Completions(completions_menu) => {
15488 let completions = completions_menu.completions.borrow_mut();
15489 assert_eq!(
15490 completions
15491 .iter()
15492 .map(|completion| &completion.label.text)
15493 .collect::<Vec<_>>(),
15494 vec!["method id()", "other"]
15495 )
15496 }
15497 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15498 }
15499 });
15500
15501 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
15502 let item1 = item1.clone();
15503 move |_, item_to_resolve, _| {
15504 let item1 = item1.clone();
15505 async move {
15506 if item1 == item_to_resolve {
15507 Ok(lsp::CompletionItem {
15508 label: "method id()".to_string(),
15509 filter_text: Some("id".to_string()),
15510 detail: Some("Now resolved!".to_string()),
15511 documentation: Some(lsp::Documentation::String("Docs".to_string())),
15512 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15513 range: lsp::Range::new(
15514 lsp::Position::new(0, 22),
15515 lsp::Position::new(0, 22),
15516 ),
15517 new_text: ".id".to_string(),
15518 })),
15519 ..lsp::CompletionItem::default()
15520 })
15521 } else {
15522 Ok(item_to_resolve)
15523 }
15524 }
15525 }
15526 })
15527 .next()
15528 .await
15529 .unwrap();
15530 cx.run_until_parked();
15531
15532 cx.update_editor(|editor, window, cx| {
15533 editor.context_menu_next(&Default::default(), window, cx);
15534 });
15535
15536 cx.update_editor(|editor, _, _| {
15537 let context_menu = editor.context_menu.borrow_mut();
15538 let context_menu = context_menu
15539 .as_ref()
15540 .expect("Should have the context menu deployed");
15541 match context_menu {
15542 CodeContextMenu::Completions(completions_menu) => {
15543 let completions = completions_menu.completions.borrow_mut();
15544 assert_eq!(
15545 completions
15546 .iter()
15547 .map(|completion| &completion.label.text)
15548 .collect::<Vec<_>>(),
15549 vec!["method id() Now resolved!", "other"],
15550 "Should update first completion label, but not second as the filter text did not match."
15551 );
15552 }
15553 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15554 }
15555 });
15556}
15557
15558#[gpui::test]
15559async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
15560 init_test(cx, |_| {});
15561 let mut cx = EditorLspTestContext::new_rust(
15562 lsp::ServerCapabilities {
15563 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
15564 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
15565 completion_provider: Some(lsp::CompletionOptions {
15566 resolve_provider: Some(true),
15567 ..Default::default()
15568 }),
15569 ..Default::default()
15570 },
15571 cx,
15572 )
15573 .await;
15574 cx.set_state(indoc! {"
15575 struct TestStruct {
15576 field: i32
15577 }
15578
15579 fn mainˇ() {
15580 let unused_var = 42;
15581 let test_struct = TestStruct { field: 42 };
15582 }
15583 "});
15584 let symbol_range = cx.lsp_range(indoc! {"
15585 struct TestStruct {
15586 field: i32
15587 }
15588
15589 «fn main»() {
15590 let unused_var = 42;
15591 let test_struct = TestStruct { field: 42 };
15592 }
15593 "});
15594 let mut hover_requests =
15595 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
15596 Ok(Some(lsp::Hover {
15597 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
15598 kind: lsp::MarkupKind::Markdown,
15599 value: "Function documentation".to_string(),
15600 }),
15601 range: Some(symbol_range),
15602 }))
15603 });
15604
15605 // Case 1: Test that code action menu hide hover popover
15606 cx.dispatch_action(Hover);
15607 hover_requests.next().await;
15608 cx.condition(|editor, _| editor.hover_state.visible()).await;
15609 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
15610 move |_, _, _| async move {
15611 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
15612 lsp::CodeAction {
15613 title: "Remove unused variable".to_string(),
15614 kind: Some(CodeActionKind::QUICKFIX),
15615 edit: Some(lsp::WorkspaceEdit {
15616 changes: Some(
15617 [(
15618 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
15619 vec![lsp::TextEdit {
15620 range: lsp::Range::new(
15621 lsp::Position::new(5, 4),
15622 lsp::Position::new(5, 27),
15623 ),
15624 new_text: "".to_string(),
15625 }],
15626 )]
15627 .into_iter()
15628 .collect(),
15629 ),
15630 ..Default::default()
15631 }),
15632 ..Default::default()
15633 },
15634 )]))
15635 },
15636 );
15637 cx.update_editor(|editor, window, cx| {
15638 editor.toggle_code_actions(
15639 &ToggleCodeActions {
15640 deployed_from: None,
15641 quick_launch: false,
15642 },
15643 window,
15644 cx,
15645 );
15646 });
15647 code_action_requests.next().await;
15648 cx.run_until_parked();
15649 cx.condition(|editor, _| editor.context_menu_visible())
15650 .await;
15651 cx.update_editor(|editor, _, _| {
15652 assert!(
15653 !editor.hover_state.visible(),
15654 "Hover popover should be hidden when code action menu is shown"
15655 );
15656 // Hide code actions
15657 editor.context_menu.take();
15658 });
15659
15660 // Case 2: Test that code completions hide hover popover
15661 cx.dispatch_action(Hover);
15662 hover_requests.next().await;
15663 cx.condition(|editor, _| editor.hover_state.visible()).await;
15664 let counter = Arc::new(AtomicUsize::new(0));
15665 let mut completion_requests =
15666 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15667 let counter = counter.clone();
15668 async move {
15669 counter.fetch_add(1, atomic::Ordering::Release);
15670 Ok(Some(lsp::CompletionResponse::Array(vec![
15671 lsp::CompletionItem {
15672 label: "main".into(),
15673 kind: Some(lsp::CompletionItemKind::FUNCTION),
15674 detail: Some("() -> ()".to_string()),
15675 ..Default::default()
15676 },
15677 lsp::CompletionItem {
15678 label: "TestStruct".into(),
15679 kind: Some(lsp::CompletionItemKind::STRUCT),
15680 detail: Some("struct TestStruct".to_string()),
15681 ..Default::default()
15682 },
15683 ])))
15684 }
15685 });
15686 cx.update_editor(|editor, window, cx| {
15687 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15688 });
15689 completion_requests.next().await;
15690 cx.condition(|editor, _| editor.context_menu_visible())
15691 .await;
15692 cx.update_editor(|editor, _, _| {
15693 assert!(
15694 !editor.hover_state.visible(),
15695 "Hover popover should be hidden when completion menu is shown"
15696 );
15697 });
15698}
15699
15700#[gpui::test]
15701async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
15702 init_test(cx, |_| {});
15703
15704 let mut cx = EditorLspTestContext::new_rust(
15705 lsp::ServerCapabilities {
15706 completion_provider: Some(lsp::CompletionOptions {
15707 trigger_characters: Some(vec![".".to_string()]),
15708 resolve_provider: Some(true),
15709 ..Default::default()
15710 }),
15711 ..Default::default()
15712 },
15713 cx,
15714 )
15715 .await;
15716
15717 cx.set_state("fn main() { let a = 2ˇ; }");
15718 cx.simulate_keystroke(".");
15719
15720 let unresolved_item_1 = lsp::CompletionItem {
15721 label: "id".to_string(),
15722 filter_text: Some("id".to_string()),
15723 detail: None,
15724 documentation: None,
15725 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15726 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15727 new_text: ".id".to_string(),
15728 })),
15729 ..lsp::CompletionItem::default()
15730 };
15731 let resolved_item_1 = lsp::CompletionItem {
15732 additional_text_edits: Some(vec![lsp::TextEdit {
15733 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15734 new_text: "!!".to_string(),
15735 }]),
15736 ..unresolved_item_1.clone()
15737 };
15738 let unresolved_item_2 = lsp::CompletionItem {
15739 label: "other".to_string(),
15740 filter_text: Some("other".to_string()),
15741 detail: None,
15742 documentation: None,
15743 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15744 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15745 new_text: ".other".to_string(),
15746 })),
15747 ..lsp::CompletionItem::default()
15748 };
15749 let resolved_item_2 = lsp::CompletionItem {
15750 additional_text_edits: Some(vec![lsp::TextEdit {
15751 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15752 new_text: "??".to_string(),
15753 }]),
15754 ..unresolved_item_2.clone()
15755 };
15756
15757 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
15758 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
15759 cx.lsp
15760 .server
15761 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15762 let unresolved_item_1 = unresolved_item_1.clone();
15763 let resolved_item_1 = resolved_item_1.clone();
15764 let unresolved_item_2 = unresolved_item_2.clone();
15765 let resolved_item_2 = resolved_item_2.clone();
15766 let resolve_requests_1 = resolve_requests_1.clone();
15767 let resolve_requests_2 = resolve_requests_2.clone();
15768 move |unresolved_request, _| {
15769 let unresolved_item_1 = unresolved_item_1.clone();
15770 let resolved_item_1 = resolved_item_1.clone();
15771 let unresolved_item_2 = unresolved_item_2.clone();
15772 let resolved_item_2 = resolved_item_2.clone();
15773 let resolve_requests_1 = resolve_requests_1.clone();
15774 let resolve_requests_2 = resolve_requests_2.clone();
15775 async move {
15776 if unresolved_request == unresolved_item_1 {
15777 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
15778 Ok(resolved_item_1.clone())
15779 } else if unresolved_request == unresolved_item_2 {
15780 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
15781 Ok(resolved_item_2.clone())
15782 } else {
15783 panic!("Unexpected completion item {unresolved_request:?}")
15784 }
15785 }
15786 }
15787 })
15788 .detach();
15789
15790 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15791 let unresolved_item_1 = unresolved_item_1.clone();
15792 let unresolved_item_2 = unresolved_item_2.clone();
15793 async move {
15794 Ok(Some(lsp::CompletionResponse::Array(vec![
15795 unresolved_item_1,
15796 unresolved_item_2,
15797 ])))
15798 }
15799 })
15800 .next()
15801 .await;
15802
15803 cx.condition(|editor, _| editor.context_menu_visible())
15804 .await;
15805 cx.update_editor(|editor, _, _| {
15806 let context_menu = editor.context_menu.borrow_mut();
15807 let context_menu = context_menu
15808 .as_ref()
15809 .expect("Should have the context menu deployed");
15810 match context_menu {
15811 CodeContextMenu::Completions(completions_menu) => {
15812 let completions = completions_menu.completions.borrow_mut();
15813 assert_eq!(
15814 completions
15815 .iter()
15816 .map(|completion| &completion.label.text)
15817 .collect::<Vec<_>>(),
15818 vec!["id", "other"]
15819 )
15820 }
15821 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15822 }
15823 });
15824 cx.run_until_parked();
15825
15826 cx.update_editor(|editor, window, cx| {
15827 editor.context_menu_next(&ContextMenuNext, window, cx);
15828 });
15829 cx.run_until_parked();
15830 cx.update_editor(|editor, window, cx| {
15831 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15832 });
15833 cx.run_until_parked();
15834 cx.update_editor(|editor, window, cx| {
15835 editor.context_menu_next(&ContextMenuNext, window, cx);
15836 });
15837 cx.run_until_parked();
15838 cx.update_editor(|editor, window, cx| {
15839 editor
15840 .compose_completion(&ComposeCompletion::default(), window, cx)
15841 .expect("No task returned")
15842 })
15843 .await
15844 .expect("Completion failed");
15845 cx.run_until_parked();
15846
15847 cx.update_editor(|editor, _, cx| {
15848 assert_eq!(
15849 resolve_requests_1.load(atomic::Ordering::Acquire),
15850 1,
15851 "Should always resolve once despite multiple selections"
15852 );
15853 assert_eq!(
15854 resolve_requests_2.load(atomic::Ordering::Acquire),
15855 1,
15856 "Should always resolve once after multiple selections and applying the completion"
15857 );
15858 assert_eq!(
15859 editor.text(cx),
15860 "fn main() { let a = ??.other; }",
15861 "Should use resolved data when applying the completion"
15862 );
15863 });
15864}
15865
15866#[gpui::test]
15867async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
15868 init_test(cx, |_| {});
15869
15870 let item_0 = lsp::CompletionItem {
15871 label: "abs".into(),
15872 insert_text: Some("abs".into()),
15873 data: Some(json!({ "very": "special"})),
15874 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
15875 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
15876 lsp::InsertReplaceEdit {
15877 new_text: "abs".to_string(),
15878 insert: lsp::Range::default(),
15879 replace: lsp::Range::default(),
15880 },
15881 )),
15882 ..lsp::CompletionItem::default()
15883 };
15884 let items = iter::once(item_0.clone())
15885 .chain((11..51).map(|i| lsp::CompletionItem {
15886 label: format!("item_{}", i),
15887 insert_text: Some(format!("item_{}", i)),
15888 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15889 ..lsp::CompletionItem::default()
15890 }))
15891 .collect::<Vec<_>>();
15892
15893 let default_commit_characters = vec!["?".to_string()];
15894 let default_data = json!({ "default": "data"});
15895 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
15896 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
15897 let default_edit_range = lsp::Range {
15898 start: lsp::Position {
15899 line: 0,
15900 character: 5,
15901 },
15902 end: lsp::Position {
15903 line: 0,
15904 character: 5,
15905 },
15906 };
15907
15908 let mut cx = EditorLspTestContext::new_rust(
15909 lsp::ServerCapabilities {
15910 completion_provider: Some(lsp::CompletionOptions {
15911 trigger_characters: Some(vec![".".to_string()]),
15912 resolve_provider: Some(true),
15913 ..Default::default()
15914 }),
15915 ..Default::default()
15916 },
15917 cx,
15918 )
15919 .await;
15920
15921 cx.set_state("fn main() { let a = 2ˇ; }");
15922 cx.simulate_keystroke(".");
15923
15924 let completion_data = default_data.clone();
15925 let completion_characters = default_commit_characters.clone();
15926 let completion_items = items.clone();
15927 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15928 let default_data = completion_data.clone();
15929 let default_commit_characters = completion_characters.clone();
15930 let items = completion_items.clone();
15931 async move {
15932 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15933 items,
15934 item_defaults: Some(lsp::CompletionListItemDefaults {
15935 data: Some(default_data.clone()),
15936 commit_characters: Some(default_commit_characters.clone()),
15937 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
15938 default_edit_range,
15939 )),
15940 insert_text_format: Some(default_insert_text_format),
15941 insert_text_mode: Some(default_insert_text_mode),
15942 }),
15943 ..lsp::CompletionList::default()
15944 })))
15945 }
15946 })
15947 .next()
15948 .await;
15949
15950 let resolved_items = Arc::new(Mutex::new(Vec::new()));
15951 cx.lsp
15952 .server
15953 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15954 let closure_resolved_items = resolved_items.clone();
15955 move |item_to_resolve, _| {
15956 let closure_resolved_items = closure_resolved_items.clone();
15957 async move {
15958 closure_resolved_items.lock().push(item_to_resolve.clone());
15959 Ok(item_to_resolve)
15960 }
15961 }
15962 })
15963 .detach();
15964
15965 cx.condition(|editor, _| editor.context_menu_visible())
15966 .await;
15967 cx.run_until_parked();
15968 cx.update_editor(|editor, _, _| {
15969 let menu = editor.context_menu.borrow_mut();
15970 match menu.as_ref().expect("should have the completions menu") {
15971 CodeContextMenu::Completions(completions_menu) => {
15972 assert_eq!(
15973 completions_menu
15974 .entries
15975 .borrow()
15976 .iter()
15977 .map(|mat| mat.string.clone())
15978 .collect::<Vec<String>>(),
15979 items
15980 .iter()
15981 .map(|completion| completion.label.clone())
15982 .collect::<Vec<String>>()
15983 );
15984 }
15985 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
15986 }
15987 });
15988 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
15989 // with 4 from the end.
15990 assert_eq!(
15991 *resolved_items.lock(),
15992 [&items[0..16], &items[items.len() - 4..items.len()]]
15993 .concat()
15994 .iter()
15995 .cloned()
15996 .map(|mut item| {
15997 if item.data.is_none() {
15998 item.data = Some(default_data.clone());
15999 }
16000 item
16001 })
16002 .collect::<Vec<lsp::CompletionItem>>(),
16003 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
16004 );
16005 resolved_items.lock().clear();
16006
16007 cx.update_editor(|editor, window, cx| {
16008 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
16009 });
16010 cx.run_until_parked();
16011 // Completions that have already been resolved are skipped.
16012 assert_eq!(
16013 *resolved_items.lock(),
16014 items[items.len() - 17..items.len() - 4]
16015 .iter()
16016 .cloned()
16017 .map(|mut item| {
16018 if item.data.is_none() {
16019 item.data = Some(default_data.clone());
16020 }
16021 item
16022 })
16023 .collect::<Vec<lsp::CompletionItem>>()
16024 );
16025 resolved_items.lock().clear();
16026}
16027
16028#[gpui::test]
16029async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
16030 init_test(cx, |_| {});
16031
16032 let mut cx = EditorLspTestContext::new(
16033 Language::new(
16034 LanguageConfig {
16035 matcher: LanguageMatcher {
16036 path_suffixes: vec!["jsx".into()],
16037 ..Default::default()
16038 },
16039 overrides: [(
16040 "element".into(),
16041 LanguageConfigOverride {
16042 completion_query_characters: Override::Set(['-'].into_iter().collect()),
16043 ..Default::default()
16044 },
16045 )]
16046 .into_iter()
16047 .collect(),
16048 ..Default::default()
16049 },
16050 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16051 )
16052 .with_override_query("(jsx_self_closing_element) @element")
16053 .unwrap(),
16054 lsp::ServerCapabilities {
16055 completion_provider: Some(lsp::CompletionOptions {
16056 trigger_characters: Some(vec![":".to_string()]),
16057 ..Default::default()
16058 }),
16059 ..Default::default()
16060 },
16061 cx,
16062 )
16063 .await;
16064
16065 cx.lsp
16066 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16067 Ok(Some(lsp::CompletionResponse::Array(vec![
16068 lsp::CompletionItem {
16069 label: "bg-blue".into(),
16070 ..Default::default()
16071 },
16072 lsp::CompletionItem {
16073 label: "bg-red".into(),
16074 ..Default::default()
16075 },
16076 lsp::CompletionItem {
16077 label: "bg-yellow".into(),
16078 ..Default::default()
16079 },
16080 ])))
16081 });
16082
16083 cx.set_state(r#"<p class="bgˇ" />"#);
16084
16085 // Trigger completion when typing a dash, because the dash is an extra
16086 // word character in the 'element' scope, which contains the cursor.
16087 cx.simulate_keystroke("-");
16088 cx.executor().run_until_parked();
16089 cx.update_editor(|editor, _, _| {
16090 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16091 {
16092 assert_eq!(
16093 completion_menu_entries(&menu),
16094 &["bg-blue", "bg-red", "bg-yellow"]
16095 );
16096 } else {
16097 panic!("expected completion menu to be open");
16098 }
16099 });
16100
16101 cx.simulate_keystroke("l");
16102 cx.executor().run_until_parked();
16103 cx.update_editor(|editor, _, _| {
16104 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16105 {
16106 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
16107 } else {
16108 panic!("expected completion menu to be open");
16109 }
16110 });
16111
16112 // When filtering completions, consider the character after the '-' to
16113 // be the start of a subword.
16114 cx.set_state(r#"<p class="yelˇ" />"#);
16115 cx.simulate_keystroke("l");
16116 cx.executor().run_until_parked();
16117 cx.update_editor(|editor, _, _| {
16118 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16119 {
16120 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
16121 } else {
16122 panic!("expected completion menu to be open");
16123 }
16124 });
16125}
16126
16127fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
16128 let entries = menu.entries.borrow();
16129 entries.iter().map(|mat| mat.string.clone()).collect()
16130}
16131
16132#[gpui::test]
16133async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
16134 init_test(cx, |settings| {
16135 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
16136 Formatter::Prettier,
16137 )))
16138 });
16139
16140 let fs = FakeFs::new(cx.executor());
16141 fs.insert_file(path!("/file.ts"), Default::default()).await;
16142
16143 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
16144 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16145
16146 language_registry.add(Arc::new(Language::new(
16147 LanguageConfig {
16148 name: "TypeScript".into(),
16149 matcher: LanguageMatcher {
16150 path_suffixes: vec!["ts".to_string()],
16151 ..Default::default()
16152 },
16153 ..Default::default()
16154 },
16155 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
16156 )));
16157 update_test_language_settings(cx, |settings| {
16158 settings.defaults.prettier = Some(PrettierSettings {
16159 allowed: true,
16160 ..PrettierSettings::default()
16161 });
16162 });
16163
16164 let test_plugin = "test_plugin";
16165 let _ = language_registry.register_fake_lsp(
16166 "TypeScript",
16167 FakeLspAdapter {
16168 prettier_plugins: vec![test_plugin],
16169 ..Default::default()
16170 },
16171 );
16172
16173 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
16174 let buffer = project
16175 .update(cx, |project, cx| {
16176 project.open_local_buffer(path!("/file.ts"), cx)
16177 })
16178 .await
16179 .unwrap();
16180
16181 let buffer_text = "one\ntwo\nthree\n";
16182 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16183 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16184 editor.update_in(cx, |editor, window, cx| {
16185 editor.set_text(buffer_text, window, cx)
16186 });
16187
16188 editor
16189 .update_in(cx, |editor, window, cx| {
16190 editor.perform_format(
16191 project.clone(),
16192 FormatTrigger::Manual,
16193 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16194 window,
16195 cx,
16196 )
16197 })
16198 .unwrap()
16199 .await;
16200 assert_eq!(
16201 editor.update(cx, |editor, cx| editor.text(cx)),
16202 buffer_text.to_string() + prettier_format_suffix,
16203 "Test prettier formatting was not applied to the original buffer text",
16204 );
16205
16206 update_test_language_settings(cx, |settings| {
16207 settings.defaults.formatter = Some(SelectedFormatter::Auto)
16208 });
16209 let format = editor.update_in(cx, |editor, window, cx| {
16210 editor.perform_format(
16211 project.clone(),
16212 FormatTrigger::Manual,
16213 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16214 window,
16215 cx,
16216 )
16217 });
16218 format.await.unwrap();
16219 assert_eq!(
16220 editor.update(cx, |editor, cx| editor.text(cx)),
16221 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
16222 "Autoformatting (via test prettier) was not applied to the original buffer text",
16223 );
16224}
16225
16226#[gpui::test]
16227async fn test_addition_reverts(cx: &mut TestAppContext) {
16228 init_test(cx, |_| {});
16229 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16230 let base_text = indoc! {r#"
16231 struct Row;
16232 struct Row1;
16233 struct Row2;
16234
16235 struct Row4;
16236 struct Row5;
16237 struct Row6;
16238
16239 struct Row8;
16240 struct Row9;
16241 struct Row10;"#};
16242
16243 // When addition hunks are not adjacent to carets, no hunk revert is performed
16244 assert_hunk_revert(
16245 indoc! {r#"struct Row;
16246 struct Row1;
16247 struct Row1.1;
16248 struct Row1.2;
16249 struct Row2;ˇ
16250
16251 struct Row4;
16252 struct Row5;
16253 struct Row6;
16254
16255 struct Row8;
16256 ˇstruct Row9;
16257 struct Row9.1;
16258 struct Row9.2;
16259 struct Row9.3;
16260 struct Row10;"#},
16261 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16262 indoc! {r#"struct Row;
16263 struct Row1;
16264 struct Row1.1;
16265 struct Row1.2;
16266 struct Row2;ˇ
16267
16268 struct Row4;
16269 struct Row5;
16270 struct Row6;
16271
16272 struct Row8;
16273 ˇstruct Row9;
16274 struct Row9.1;
16275 struct Row9.2;
16276 struct Row9.3;
16277 struct Row10;"#},
16278 base_text,
16279 &mut cx,
16280 );
16281 // Same for selections
16282 assert_hunk_revert(
16283 indoc! {r#"struct Row;
16284 struct Row1;
16285 struct Row2;
16286 struct Row2.1;
16287 struct Row2.2;
16288 «ˇ
16289 struct Row4;
16290 struct» Row5;
16291 «struct Row6;
16292 ˇ»
16293 struct Row9.1;
16294 struct Row9.2;
16295 struct Row9.3;
16296 struct Row8;
16297 struct Row9;
16298 struct Row10;"#},
16299 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16300 indoc! {r#"struct Row;
16301 struct Row1;
16302 struct Row2;
16303 struct Row2.1;
16304 struct Row2.2;
16305 «ˇ
16306 struct Row4;
16307 struct» Row5;
16308 «struct Row6;
16309 ˇ»
16310 struct Row9.1;
16311 struct Row9.2;
16312 struct Row9.3;
16313 struct Row8;
16314 struct Row9;
16315 struct Row10;"#},
16316 base_text,
16317 &mut cx,
16318 );
16319
16320 // When carets and selections intersect the addition hunks, those are reverted.
16321 // Adjacent carets got merged.
16322 assert_hunk_revert(
16323 indoc! {r#"struct Row;
16324 ˇ// something on the top
16325 struct Row1;
16326 struct Row2;
16327 struct Roˇw3.1;
16328 struct Row2.2;
16329 struct Row2.3;ˇ
16330
16331 struct Row4;
16332 struct ˇRow5.1;
16333 struct Row5.2;
16334 struct «Rowˇ»5.3;
16335 struct Row5;
16336 struct Row6;
16337 ˇ
16338 struct Row9.1;
16339 struct «Rowˇ»9.2;
16340 struct «ˇRow»9.3;
16341 struct Row8;
16342 struct Row9;
16343 «ˇ// something on bottom»
16344 struct Row10;"#},
16345 vec![
16346 DiffHunkStatusKind::Added,
16347 DiffHunkStatusKind::Added,
16348 DiffHunkStatusKind::Added,
16349 DiffHunkStatusKind::Added,
16350 DiffHunkStatusKind::Added,
16351 ],
16352 indoc! {r#"struct Row;
16353 ˇstruct Row1;
16354 struct Row2;
16355 ˇ
16356 struct Row4;
16357 ˇstruct Row5;
16358 struct Row6;
16359 ˇ
16360 ˇstruct Row8;
16361 struct Row9;
16362 ˇstruct Row10;"#},
16363 base_text,
16364 &mut cx,
16365 );
16366}
16367
16368#[gpui::test]
16369async fn test_modification_reverts(cx: &mut TestAppContext) {
16370 init_test(cx, |_| {});
16371 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16372 let base_text = indoc! {r#"
16373 struct Row;
16374 struct Row1;
16375 struct Row2;
16376
16377 struct Row4;
16378 struct Row5;
16379 struct Row6;
16380
16381 struct Row8;
16382 struct Row9;
16383 struct Row10;"#};
16384
16385 // Modification hunks behave the same as the addition ones.
16386 assert_hunk_revert(
16387 indoc! {r#"struct Row;
16388 struct Row1;
16389 struct Row33;
16390 ˇ
16391 struct Row4;
16392 struct Row5;
16393 struct Row6;
16394 ˇ
16395 struct Row99;
16396 struct Row9;
16397 struct Row10;"#},
16398 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16399 indoc! {r#"struct Row;
16400 struct Row1;
16401 struct Row33;
16402 ˇ
16403 struct Row4;
16404 struct Row5;
16405 struct Row6;
16406 ˇ
16407 struct Row99;
16408 struct Row9;
16409 struct Row10;"#},
16410 base_text,
16411 &mut cx,
16412 );
16413 assert_hunk_revert(
16414 indoc! {r#"struct Row;
16415 struct Row1;
16416 struct Row33;
16417 «ˇ
16418 struct Row4;
16419 struct» Row5;
16420 «struct Row6;
16421 ˇ»
16422 struct Row99;
16423 struct Row9;
16424 struct Row10;"#},
16425 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16426 indoc! {r#"struct Row;
16427 struct Row1;
16428 struct Row33;
16429 «ˇ
16430 struct Row4;
16431 struct» Row5;
16432 «struct Row6;
16433 ˇ»
16434 struct Row99;
16435 struct Row9;
16436 struct Row10;"#},
16437 base_text,
16438 &mut cx,
16439 );
16440
16441 assert_hunk_revert(
16442 indoc! {r#"ˇstruct Row1.1;
16443 struct Row1;
16444 «ˇstr»uct Row22;
16445
16446 struct ˇRow44;
16447 struct Row5;
16448 struct «Rˇ»ow66;ˇ
16449
16450 «struˇ»ct Row88;
16451 struct Row9;
16452 struct Row1011;ˇ"#},
16453 vec![
16454 DiffHunkStatusKind::Modified,
16455 DiffHunkStatusKind::Modified,
16456 DiffHunkStatusKind::Modified,
16457 DiffHunkStatusKind::Modified,
16458 DiffHunkStatusKind::Modified,
16459 DiffHunkStatusKind::Modified,
16460 ],
16461 indoc! {r#"struct Row;
16462 ˇstruct Row1;
16463 struct Row2;
16464 ˇ
16465 struct Row4;
16466 ˇstruct Row5;
16467 struct Row6;
16468 ˇ
16469 struct Row8;
16470 ˇstruct Row9;
16471 struct Row10;ˇ"#},
16472 base_text,
16473 &mut cx,
16474 );
16475}
16476
16477#[gpui::test]
16478async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
16479 init_test(cx, |_| {});
16480 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16481 let base_text = indoc! {r#"
16482 one
16483
16484 two
16485 three
16486 "#};
16487
16488 cx.set_head_text(base_text);
16489 cx.set_state("\nˇ\n");
16490 cx.executor().run_until_parked();
16491 cx.update_editor(|editor, _window, cx| {
16492 editor.expand_selected_diff_hunks(cx);
16493 });
16494 cx.executor().run_until_parked();
16495 cx.update_editor(|editor, window, cx| {
16496 editor.backspace(&Default::default(), window, cx);
16497 });
16498 cx.run_until_parked();
16499 cx.assert_state_with_diff(
16500 indoc! {r#"
16501
16502 - two
16503 - threeˇ
16504 +
16505 "#}
16506 .to_string(),
16507 );
16508}
16509
16510#[gpui::test]
16511async fn test_deletion_reverts(cx: &mut TestAppContext) {
16512 init_test(cx, |_| {});
16513 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16514 let base_text = indoc! {r#"struct Row;
16515struct Row1;
16516struct Row2;
16517
16518struct Row4;
16519struct Row5;
16520struct Row6;
16521
16522struct Row8;
16523struct Row9;
16524struct Row10;"#};
16525
16526 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
16527 assert_hunk_revert(
16528 indoc! {r#"struct Row;
16529 struct Row2;
16530
16531 ˇstruct Row4;
16532 struct Row5;
16533 struct Row6;
16534 ˇ
16535 struct Row8;
16536 struct Row10;"#},
16537 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16538 indoc! {r#"struct Row;
16539 struct Row2;
16540
16541 ˇstruct Row4;
16542 struct Row5;
16543 struct Row6;
16544 ˇ
16545 struct Row8;
16546 struct Row10;"#},
16547 base_text,
16548 &mut cx,
16549 );
16550 assert_hunk_revert(
16551 indoc! {r#"struct Row;
16552 struct Row2;
16553
16554 «ˇstruct Row4;
16555 struct» Row5;
16556 «struct Row6;
16557 ˇ»
16558 struct Row8;
16559 struct Row10;"#},
16560 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16561 indoc! {r#"struct Row;
16562 struct Row2;
16563
16564 «ˇstruct Row4;
16565 struct» Row5;
16566 «struct Row6;
16567 ˇ»
16568 struct Row8;
16569 struct Row10;"#},
16570 base_text,
16571 &mut cx,
16572 );
16573
16574 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
16575 assert_hunk_revert(
16576 indoc! {r#"struct Row;
16577 ˇstruct Row2;
16578
16579 struct Row4;
16580 struct Row5;
16581 struct Row6;
16582
16583 struct Row8;ˇ
16584 struct Row10;"#},
16585 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16586 indoc! {r#"struct Row;
16587 struct Row1;
16588 ˇstruct Row2;
16589
16590 struct Row4;
16591 struct Row5;
16592 struct Row6;
16593
16594 struct Row8;ˇ
16595 struct Row9;
16596 struct Row10;"#},
16597 base_text,
16598 &mut cx,
16599 );
16600 assert_hunk_revert(
16601 indoc! {r#"struct Row;
16602 struct Row2«ˇ;
16603 struct Row4;
16604 struct» Row5;
16605 «struct Row6;
16606
16607 struct Row8;ˇ»
16608 struct Row10;"#},
16609 vec![
16610 DiffHunkStatusKind::Deleted,
16611 DiffHunkStatusKind::Deleted,
16612 DiffHunkStatusKind::Deleted,
16613 ],
16614 indoc! {r#"struct Row;
16615 struct Row1;
16616 struct Row2«ˇ;
16617
16618 struct Row4;
16619 struct» Row5;
16620 «struct Row6;
16621
16622 struct Row8;ˇ»
16623 struct Row9;
16624 struct Row10;"#},
16625 base_text,
16626 &mut cx,
16627 );
16628}
16629
16630#[gpui::test]
16631async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
16632 init_test(cx, |_| {});
16633
16634 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
16635 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
16636 let base_text_3 =
16637 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
16638
16639 let text_1 = edit_first_char_of_every_line(base_text_1);
16640 let text_2 = edit_first_char_of_every_line(base_text_2);
16641 let text_3 = edit_first_char_of_every_line(base_text_3);
16642
16643 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
16644 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
16645 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
16646
16647 let multibuffer = cx.new(|cx| {
16648 let mut multibuffer = MultiBuffer::new(ReadWrite);
16649 multibuffer.push_excerpts(
16650 buffer_1.clone(),
16651 [
16652 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16653 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16654 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16655 ],
16656 cx,
16657 );
16658 multibuffer.push_excerpts(
16659 buffer_2.clone(),
16660 [
16661 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16662 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16663 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16664 ],
16665 cx,
16666 );
16667 multibuffer.push_excerpts(
16668 buffer_3.clone(),
16669 [
16670 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16671 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16672 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16673 ],
16674 cx,
16675 );
16676 multibuffer
16677 });
16678
16679 let fs = FakeFs::new(cx.executor());
16680 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
16681 let (editor, cx) = cx
16682 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
16683 editor.update_in(cx, |editor, _window, cx| {
16684 for (buffer, diff_base) in [
16685 (buffer_1.clone(), base_text_1),
16686 (buffer_2.clone(), base_text_2),
16687 (buffer_3.clone(), base_text_3),
16688 ] {
16689 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16690 editor
16691 .buffer
16692 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16693 }
16694 });
16695 cx.executor().run_until_parked();
16696
16697 editor.update_in(cx, |editor, window, cx| {
16698 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}");
16699 editor.select_all(&SelectAll, window, cx);
16700 editor.git_restore(&Default::default(), window, cx);
16701 });
16702 cx.executor().run_until_parked();
16703
16704 // When all ranges are selected, all buffer hunks are reverted.
16705 editor.update(cx, |editor, cx| {
16706 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");
16707 });
16708 buffer_1.update(cx, |buffer, _| {
16709 assert_eq!(buffer.text(), base_text_1);
16710 });
16711 buffer_2.update(cx, |buffer, _| {
16712 assert_eq!(buffer.text(), base_text_2);
16713 });
16714 buffer_3.update(cx, |buffer, _| {
16715 assert_eq!(buffer.text(), base_text_3);
16716 });
16717
16718 editor.update_in(cx, |editor, window, cx| {
16719 editor.undo(&Default::default(), window, cx);
16720 });
16721
16722 editor.update_in(cx, |editor, window, cx| {
16723 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16724 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
16725 });
16726 editor.git_restore(&Default::default(), window, cx);
16727 });
16728
16729 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
16730 // but not affect buffer_2 and its related excerpts.
16731 editor.update(cx, |editor, cx| {
16732 assert_eq!(
16733 editor.text(cx),
16734 "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}"
16735 );
16736 });
16737 buffer_1.update(cx, |buffer, _| {
16738 assert_eq!(buffer.text(), base_text_1);
16739 });
16740 buffer_2.update(cx, |buffer, _| {
16741 assert_eq!(
16742 buffer.text(),
16743 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
16744 );
16745 });
16746 buffer_3.update(cx, |buffer, _| {
16747 assert_eq!(
16748 buffer.text(),
16749 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
16750 );
16751 });
16752
16753 fn edit_first_char_of_every_line(text: &str) -> String {
16754 text.split('\n')
16755 .map(|line| format!("X{}", &line[1..]))
16756 .collect::<Vec<_>>()
16757 .join("\n")
16758 }
16759}
16760
16761#[gpui::test]
16762async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
16763 init_test(cx, |_| {});
16764
16765 let cols = 4;
16766 let rows = 10;
16767 let sample_text_1 = sample_text(rows, cols, 'a');
16768 assert_eq!(
16769 sample_text_1,
16770 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
16771 );
16772 let sample_text_2 = sample_text(rows, cols, 'l');
16773 assert_eq!(
16774 sample_text_2,
16775 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
16776 );
16777 let sample_text_3 = sample_text(rows, cols, 'v');
16778 assert_eq!(
16779 sample_text_3,
16780 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
16781 );
16782
16783 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
16784 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
16785 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
16786
16787 let multi_buffer = cx.new(|cx| {
16788 let mut multibuffer = MultiBuffer::new(ReadWrite);
16789 multibuffer.push_excerpts(
16790 buffer_1.clone(),
16791 [
16792 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16793 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16794 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16795 ],
16796 cx,
16797 );
16798 multibuffer.push_excerpts(
16799 buffer_2.clone(),
16800 [
16801 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16802 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16803 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16804 ],
16805 cx,
16806 );
16807 multibuffer.push_excerpts(
16808 buffer_3.clone(),
16809 [
16810 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16811 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16812 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16813 ],
16814 cx,
16815 );
16816 multibuffer
16817 });
16818
16819 let fs = FakeFs::new(cx.executor());
16820 fs.insert_tree(
16821 "/a",
16822 json!({
16823 "main.rs": sample_text_1,
16824 "other.rs": sample_text_2,
16825 "lib.rs": sample_text_3,
16826 }),
16827 )
16828 .await;
16829 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16830 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16831 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16832 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16833 Editor::new(
16834 EditorMode::full(),
16835 multi_buffer,
16836 Some(project.clone()),
16837 window,
16838 cx,
16839 )
16840 });
16841 let multibuffer_item_id = workspace
16842 .update(cx, |workspace, window, cx| {
16843 assert!(
16844 workspace.active_item(cx).is_none(),
16845 "active item should be None before the first item is added"
16846 );
16847 workspace.add_item_to_active_pane(
16848 Box::new(multi_buffer_editor.clone()),
16849 None,
16850 true,
16851 window,
16852 cx,
16853 );
16854 let active_item = workspace
16855 .active_item(cx)
16856 .expect("should have an active item after adding the multi buffer");
16857 assert!(
16858 !active_item.is_singleton(cx),
16859 "A multi buffer was expected to active after adding"
16860 );
16861 active_item.item_id()
16862 })
16863 .unwrap();
16864 cx.executor().run_until_parked();
16865
16866 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16867 editor.change_selections(
16868 SelectionEffects::scroll(Autoscroll::Next),
16869 window,
16870 cx,
16871 |s| s.select_ranges(Some(1..2)),
16872 );
16873 editor.open_excerpts(&OpenExcerpts, window, cx);
16874 });
16875 cx.executor().run_until_parked();
16876 let first_item_id = workspace
16877 .update(cx, |workspace, window, cx| {
16878 let active_item = workspace
16879 .active_item(cx)
16880 .expect("should have an active item after navigating into the 1st buffer");
16881 let first_item_id = active_item.item_id();
16882 assert_ne!(
16883 first_item_id, multibuffer_item_id,
16884 "Should navigate into the 1st buffer and activate it"
16885 );
16886 assert!(
16887 active_item.is_singleton(cx),
16888 "New active item should be a singleton buffer"
16889 );
16890 assert_eq!(
16891 active_item
16892 .act_as::<Editor>(cx)
16893 .expect("should have navigated into an editor for the 1st buffer")
16894 .read(cx)
16895 .text(cx),
16896 sample_text_1
16897 );
16898
16899 workspace
16900 .go_back(workspace.active_pane().downgrade(), window, cx)
16901 .detach_and_log_err(cx);
16902
16903 first_item_id
16904 })
16905 .unwrap();
16906 cx.executor().run_until_parked();
16907 workspace
16908 .update(cx, |workspace, _, cx| {
16909 let active_item = workspace
16910 .active_item(cx)
16911 .expect("should have an active item after navigating back");
16912 assert_eq!(
16913 active_item.item_id(),
16914 multibuffer_item_id,
16915 "Should navigate back to the multi buffer"
16916 );
16917 assert!(!active_item.is_singleton(cx));
16918 })
16919 .unwrap();
16920
16921 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16922 editor.change_selections(
16923 SelectionEffects::scroll(Autoscroll::Next),
16924 window,
16925 cx,
16926 |s| s.select_ranges(Some(39..40)),
16927 );
16928 editor.open_excerpts(&OpenExcerpts, window, cx);
16929 });
16930 cx.executor().run_until_parked();
16931 let second_item_id = workspace
16932 .update(cx, |workspace, window, cx| {
16933 let active_item = workspace
16934 .active_item(cx)
16935 .expect("should have an active item after navigating into the 2nd buffer");
16936 let second_item_id = active_item.item_id();
16937 assert_ne!(
16938 second_item_id, multibuffer_item_id,
16939 "Should navigate away from the multibuffer"
16940 );
16941 assert_ne!(
16942 second_item_id, first_item_id,
16943 "Should navigate into the 2nd buffer and activate it"
16944 );
16945 assert!(
16946 active_item.is_singleton(cx),
16947 "New active item should be a singleton buffer"
16948 );
16949 assert_eq!(
16950 active_item
16951 .act_as::<Editor>(cx)
16952 .expect("should have navigated into an editor")
16953 .read(cx)
16954 .text(cx),
16955 sample_text_2
16956 );
16957
16958 workspace
16959 .go_back(workspace.active_pane().downgrade(), window, cx)
16960 .detach_and_log_err(cx);
16961
16962 second_item_id
16963 })
16964 .unwrap();
16965 cx.executor().run_until_parked();
16966 workspace
16967 .update(cx, |workspace, _, cx| {
16968 let active_item = workspace
16969 .active_item(cx)
16970 .expect("should have an active item after navigating back from the 2nd buffer");
16971 assert_eq!(
16972 active_item.item_id(),
16973 multibuffer_item_id,
16974 "Should navigate back from the 2nd buffer to the multi buffer"
16975 );
16976 assert!(!active_item.is_singleton(cx));
16977 })
16978 .unwrap();
16979
16980 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16981 editor.change_selections(
16982 SelectionEffects::scroll(Autoscroll::Next),
16983 window,
16984 cx,
16985 |s| s.select_ranges(Some(70..70)),
16986 );
16987 editor.open_excerpts(&OpenExcerpts, window, cx);
16988 });
16989 cx.executor().run_until_parked();
16990 workspace
16991 .update(cx, |workspace, window, cx| {
16992 let active_item = workspace
16993 .active_item(cx)
16994 .expect("should have an active item after navigating into the 3rd buffer");
16995 let third_item_id = active_item.item_id();
16996 assert_ne!(
16997 third_item_id, multibuffer_item_id,
16998 "Should navigate into the 3rd buffer and activate it"
16999 );
17000 assert_ne!(third_item_id, first_item_id);
17001 assert_ne!(third_item_id, second_item_id);
17002 assert!(
17003 active_item.is_singleton(cx),
17004 "New active item should be a singleton buffer"
17005 );
17006 assert_eq!(
17007 active_item
17008 .act_as::<Editor>(cx)
17009 .expect("should have navigated into an editor")
17010 .read(cx)
17011 .text(cx),
17012 sample_text_3
17013 );
17014
17015 workspace
17016 .go_back(workspace.active_pane().downgrade(), window, cx)
17017 .detach_and_log_err(cx);
17018 })
17019 .unwrap();
17020 cx.executor().run_until_parked();
17021 workspace
17022 .update(cx, |workspace, _, cx| {
17023 let active_item = workspace
17024 .active_item(cx)
17025 .expect("should have an active item after navigating back from the 3rd buffer");
17026 assert_eq!(
17027 active_item.item_id(),
17028 multibuffer_item_id,
17029 "Should navigate back from the 3rd buffer to the multi buffer"
17030 );
17031 assert!(!active_item.is_singleton(cx));
17032 })
17033 .unwrap();
17034}
17035
17036#[gpui::test]
17037async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17038 init_test(cx, |_| {});
17039
17040 let mut cx = EditorTestContext::new(cx).await;
17041
17042 let diff_base = r#"
17043 use some::mod;
17044
17045 const A: u32 = 42;
17046
17047 fn main() {
17048 println!("hello");
17049
17050 println!("world");
17051 }
17052 "#
17053 .unindent();
17054
17055 cx.set_state(
17056 &r#"
17057 use some::modified;
17058
17059 ˇ
17060 fn main() {
17061 println!("hello there");
17062
17063 println!("around the");
17064 println!("world");
17065 }
17066 "#
17067 .unindent(),
17068 );
17069
17070 cx.set_head_text(&diff_base);
17071 executor.run_until_parked();
17072
17073 cx.update_editor(|editor, window, cx| {
17074 editor.go_to_next_hunk(&GoToHunk, window, cx);
17075 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17076 });
17077 executor.run_until_parked();
17078 cx.assert_state_with_diff(
17079 r#"
17080 use some::modified;
17081
17082
17083 fn main() {
17084 - println!("hello");
17085 + ˇ println!("hello there");
17086
17087 println!("around the");
17088 println!("world");
17089 }
17090 "#
17091 .unindent(),
17092 );
17093
17094 cx.update_editor(|editor, window, cx| {
17095 for _ in 0..2 {
17096 editor.go_to_next_hunk(&GoToHunk, window, cx);
17097 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17098 }
17099 });
17100 executor.run_until_parked();
17101 cx.assert_state_with_diff(
17102 r#"
17103 - use some::mod;
17104 + ˇuse some::modified;
17105
17106
17107 fn main() {
17108 - println!("hello");
17109 + println!("hello there");
17110
17111 + println!("around the");
17112 println!("world");
17113 }
17114 "#
17115 .unindent(),
17116 );
17117
17118 cx.update_editor(|editor, window, cx| {
17119 editor.go_to_next_hunk(&GoToHunk, window, cx);
17120 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17121 });
17122 executor.run_until_parked();
17123 cx.assert_state_with_diff(
17124 r#"
17125 - use some::mod;
17126 + use some::modified;
17127
17128 - const A: u32 = 42;
17129 ˇ
17130 fn main() {
17131 - println!("hello");
17132 + println!("hello there");
17133
17134 + println!("around the");
17135 println!("world");
17136 }
17137 "#
17138 .unindent(),
17139 );
17140
17141 cx.update_editor(|editor, window, cx| {
17142 editor.cancel(&Cancel, window, cx);
17143 });
17144
17145 cx.assert_state_with_diff(
17146 r#"
17147 use some::modified;
17148
17149 ˇ
17150 fn main() {
17151 println!("hello there");
17152
17153 println!("around the");
17154 println!("world");
17155 }
17156 "#
17157 .unindent(),
17158 );
17159}
17160
17161#[gpui::test]
17162async fn test_diff_base_change_with_expanded_diff_hunks(
17163 executor: BackgroundExecutor,
17164 cx: &mut TestAppContext,
17165) {
17166 init_test(cx, |_| {});
17167
17168 let mut cx = EditorTestContext::new(cx).await;
17169
17170 let diff_base = r#"
17171 use some::mod1;
17172 use some::mod2;
17173
17174 const A: u32 = 42;
17175 const B: u32 = 42;
17176 const C: u32 = 42;
17177
17178 fn main() {
17179 println!("hello");
17180
17181 println!("world");
17182 }
17183 "#
17184 .unindent();
17185
17186 cx.set_state(
17187 &r#"
17188 use some::mod2;
17189
17190 const A: u32 = 42;
17191 const C: u32 = 42;
17192
17193 fn main(ˇ) {
17194 //println!("hello");
17195
17196 println!("world");
17197 //
17198 //
17199 }
17200 "#
17201 .unindent(),
17202 );
17203
17204 cx.set_head_text(&diff_base);
17205 executor.run_until_parked();
17206
17207 cx.update_editor(|editor, window, cx| {
17208 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17209 });
17210 executor.run_until_parked();
17211 cx.assert_state_with_diff(
17212 r#"
17213 - use some::mod1;
17214 use some::mod2;
17215
17216 const A: u32 = 42;
17217 - const B: u32 = 42;
17218 const C: u32 = 42;
17219
17220 fn main(ˇ) {
17221 - println!("hello");
17222 + //println!("hello");
17223
17224 println!("world");
17225 + //
17226 + //
17227 }
17228 "#
17229 .unindent(),
17230 );
17231
17232 cx.set_head_text("new diff base!");
17233 executor.run_until_parked();
17234 cx.assert_state_with_diff(
17235 r#"
17236 - new diff base!
17237 + use some::mod2;
17238 +
17239 + const A: u32 = 42;
17240 + const C: u32 = 42;
17241 +
17242 + fn main(ˇ) {
17243 + //println!("hello");
17244 +
17245 + println!("world");
17246 + //
17247 + //
17248 + }
17249 "#
17250 .unindent(),
17251 );
17252}
17253
17254#[gpui::test]
17255async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
17256 init_test(cx, |_| {});
17257
17258 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17259 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17260 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17261 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17262 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
17263 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
17264
17265 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
17266 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
17267 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
17268
17269 let multi_buffer = cx.new(|cx| {
17270 let mut multibuffer = MultiBuffer::new(ReadWrite);
17271 multibuffer.push_excerpts(
17272 buffer_1.clone(),
17273 [
17274 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17275 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17276 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17277 ],
17278 cx,
17279 );
17280 multibuffer.push_excerpts(
17281 buffer_2.clone(),
17282 [
17283 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17284 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17285 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17286 ],
17287 cx,
17288 );
17289 multibuffer.push_excerpts(
17290 buffer_3.clone(),
17291 [
17292 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17293 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17294 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17295 ],
17296 cx,
17297 );
17298 multibuffer
17299 });
17300
17301 let editor =
17302 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17303 editor
17304 .update(cx, |editor, _window, cx| {
17305 for (buffer, diff_base) in [
17306 (buffer_1.clone(), file_1_old),
17307 (buffer_2.clone(), file_2_old),
17308 (buffer_3.clone(), file_3_old),
17309 ] {
17310 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
17311 editor
17312 .buffer
17313 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
17314 }
17315 })
17316 .unwrap();
17317
17318 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17319 cx.run_until_parked();
17320
17321 cx.assert_editor_state(
17322 &"
17323 ˇaaa
17324 ccc
17325 ddd
17326
17327 ggg
17328 hhh
17329
17330
17331 lll
17332 mmm
17333 NNN
17334
17335 qqq
17336 rrr
17337
17338 uuu
17339 111
17340 222
17341 333
17342
17343 666
17344 777
17345
17346 000
17347 !!!"
17348 .unindent(),
17349 );
17350
17351 cx.update_editor(|editor, window, cx| {
17352 editor.select_all(&SelectAll, window, cx);
17353 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17354 });
17355 cx.executor().run_until_parked();
17356
17357 cx.assert_state_with_diff(
17358 "
17359 «aaa
17360 - bbb
17361 ccc
17362 ddd
17363
17364 ggg
17365 hhh
17366
17367
17368 lll
17369 mmm
17370 - nnn
17371 + NNN
17372
17373 qqq
17374 rrr
17375
17376 uuu
17377 111
17378 222
17379 333
17380
17381 + 666
17382 777
17383
17384 000
17385 !!!ˇ»"
17386 .unindent(),
17387 );
17388}
17389
17390#[gpui::test]
17391async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
17392 init_test(cx, |_| {});
17393
17394 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
17395 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
17396
17397 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
17398 let multi_buffer = cx.new(|cx| {
17399 let mut multibuffer = MultiBuffer::new(ReadWrite);
17400 multibuffer.push_excerpts(
17401 buffer.clone(),
17402 [
17403 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
17404 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
17405 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
17406 ],
17407 cx,
17408 );
17409 multibuffer
17410 });
17411
17412 let editor =
17413 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17414 editor
17415 .update(cx, |editor, _window, cx| {
17416 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
17417 editor
17418 .buffer
17419 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
17420 })
17421 .unwrap();
17422
17423 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17424 cx.run_until_parked();
17425
17426 cx.update_editor(|editor, window, cx| {
17427 editor.expand_all_diff_hunks(&Default::default(), window, cx)
17428 });
17429 cx.executor().run_until_parked();
17430
17431 // When the start of a hunk coincides with the start of its excerpt,
17432 // the hunk is expanded. When the start of a a hunk is earlier than
17433 // the start of its excerpt, the hunk is not expanded.
17434 cx.assert_state_with_diff(
17435 "
17436 ˇaaa
17437 - bbb
17438 + BBB
17439
17440 - ddd
17441 - eee
17442 + DDD
17443 + EEE
17444 fff
17445
17446 iii
17447 "
17448 .unindent(),
17449 );
17450}
17451
17452#[gpui::test]
17453async fn test_edits_around_expanded_insertion_hunks(
17454 executor: BackgroundExecutor,
17455 cx: &mut TestAppContext,
17456) {
17457 init_test(cx, |_| {});
17458
17459 let mut cx = EditorTestContext::new(cx).await;
17460
17461 let diff_base = r#"
17462 use some::mod1;
17463 use some::mod2;
17464
17465 const A: u32 = 42;
17466
17467 fn main() {
17468 println!("hello");
17469
17470 println!("world");
17471 }
17472 "#
17473 .unindent();
17474 executor.run_until_parked();
17475 cx.set_state(
17476 &r#"
17477 use some::mod1;
17478 use some::mod2;
17479
17480 const A: u32 = 42;
17481 const B: u32 = 42;
17482 const C: u32 = 42;
17483 ˇ
17484
17485 fn main() {
17486 println!("hello");
17487
17488 println!("world");
17489 }
17490 "#
17491 .unindent(),
17492 );
17493
17494 cx.set_head_text(&diff_base);
17495 executor.run_until_parked();
17496
17497 cx.update_editor(|editor, window, cx| {
17498 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17499 });
17500 executor.run_until_parked();
17501
17502 cx.assert_state_with_diff(
17503 r#"
17504 use some::mod1;
17505 use some::mod2;
17506
17507 const A: u32 = 42;
17508 + const B: u32 = 42;
17509 + const C: u32 = 42;
17510 + ˇ
17511
17512 fn main() {
17513 println!("hello");
17514
17515 println!("world");
17516 }
17517 "#
17518 .unindent(),
17519 );
17520
17521 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
17522 executor.run_until_parked();
17523
17524 cx.assert_state_with_diff(
17525 r#"
17526 use some::mod1;
17527 use some::mod2;
17528
17529 const A: u32 = 42;
17530 + const B: u32 = 42;
17531 + const C: u32 = 42;
17532 + const D: u32 = 42;
17533 + ˇ
17534
17535 fn main() {
17536 println!("hello");
17537
17538 println!("world");
17539 }
17540 "#
17541 .unindent(),
17542 );
17543
17544 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
17545 executor.run_until_parked();
17546
17547 cx.assert_state_with_diff(
17548 r#"
17549 use some::mod1;
17550 use some::mod2;
17551
17552 const A: u32 = 42;
17553 + const B: u32 = 42;
17554 + const C: u32 = 42;
17555 + const D: u32 = 42;
17556 + const E: u32 = 42;
17557 + ˇ
17558
17559 fn main() {
17560 println!("hello");
17561
17562 println!("world");
17563 }
17564 "#
17565 .unindent(),
17566 );
17567
17568 cx.update_editor(|editor, window, cx| {
17569 editor.delete_line(&DeleteLine, window, cx);
17570 });
17571 executor.run_until_parked();
17572
17573 cx.assert_state_with_diff(
17574 r#"
17575 use some::mod1;
17576 use some::mod2;
17577
17578 const A: u32 = 42;
17579 + const B: u32 = 42;
17580 + const C: u32 = 42;
17581 + const D: u32 = 42;
17582 + const E: u32 = 42;
17583 ˇ
17584 fn main() {
17585 println!("hello");
17586
17587 println!("world");
17588 }
17589 "#
17590 .unindent(),
17591 );
17592
17593 cx.update_editor(|editor, window, cx| {
17594 editor.move_up(&MoveUp, window, cx);
17595 editor.delete_line(&DeleteLine, window, cx);
17596 editor.move_up(&MoveUp, window, cx);
17597 editor.delete_line(&DeleteLine, window, cx);
17598 editor.move_up(&MoveUp, window, cx);
17599 editor.delete_line(&DeleteLine, window, cx);
17600 });
17601 executor.run_until_parked();
17602 cx.assert_state_with_diff(
17603 r#"
17604 use some::mod1;
17605 use some::mod2;
17606
17607 const A: u32 = 42;
17608 + const B: u32 = 42;
17609 ˇ
17610 fn main() {
17611 println!("hello");
17612
17613 println!("world");
17614 }
17615 "#
17616 .unindent(),
17617 );
17618
17619 cx.update_editor(|editor, window, cx| {
17620 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
17621 editor.delete_line(&DeleteLine, window, cx);
17622 });
17623 executor.run_until_parked();
17624 cx.assert_state_with_diff(
17625 r#"
17626 ˇ
17627 fn main() {
17628 println!("hello");
17629
17630 println!("world");
17631 }
17632 "#
17633 .unindent(),
17634 );
17635}
17636
17637#[gpui::test]
17638async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
17639 init_test(cx, |_| {});
17640
17641 let mut cx = EditorTestContext::new(cx).await;
17642 cx.set_head_text(indoc! { "
17643 one
17644 two
17645 three
17646 four
17647 five
17648 "
17649 });
17650 cx.set_state(indoc! { "
17651 one
17652 ˇthree
17653 five
17654 "});
17655 cx.run_until_parked();
17656 cx.update_editor(|editor, window, cx| {
17657 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17658 });
17659 cx.assert_state_with_diff(
17660 indoc! { "
17661 one
17662 - two
17663 ˇthree
17664 - four
17665 five
17666 "}
17667 .to_string(),
17668 );
17669 cx.update_editor(|editor, window, cx| {
17670 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17671 });
17672
17673 cx.assert_state_with_diff(
17674 indoc! { "
17675 one
17676 ˇthree
17677 five
17678 "}
17679 .to_string(),
17680 );
17681
17682 cx.set_state(indoc! { "
17683 one
17684 ˇTWO
17685 three
17686 four
17687 five
17688 "});
17689 cx.run_until_parked();
17690 cx.update_editor(|editor, window, cx| {
17691 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17692 });
17693
17694 cx.assert_state_with_diff(
17695 indoc! { "
17696 one
17697 - two
17698 + ˇTWO
17699 three
17700 four
17701 five
17702 "}
17703 .to_string(),
17704 );
17705 cx.update_editor(|editor, window, cx| {
17706 editor.move_up(&Default::default(), window, cx);
17707 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17708 });
17709 cx.assert_state_with_diff(
17710 indoc! { "
17711 one
17712 ˇTWO
17713 three
17714 four
17715 five
17716 "}
17717 .to_string(),
17718 );
17719}
17720
17721#[gpui::test]
17722async fn test_edits_around_expanded_deletion_hunks(
17723 executor: BackgroundExecutor,
17724 cx: &mut TestAppContext,
17725) {
17726 init_test(cx, |_| {});
17727
17728 let mut cx = EditorTestContext::new(cx).await;
17729
17730 let diff_base = r#"
17731 use some::mod1;
17732 use some::mod2;
17733
17734 const A: u32 = 42;
17735 const B: u32 = 42;
17736 const C: u32 = 42;
17737
17738
17739 fn main() {
17740 println!("hello");
17741
17742 println!("world");
17743 }
17744 "#
17745 .unindent();
17746 executor.run_until_parked();
17747 cx.set_state(
17748 &r#"
17749 use some::mod1;
17750 use some::mod2;
17751
17752 ˇconst B: u32 = 42;
17753 const C: u32 = 42;
17754
17755
17756 fn main() {
17757 println!("hello");
17758
17759 println!("world");
17760 }
17761 "#
17762 .unindent(),
17763 );
17764
17765 cx.set_head_text(&diff_base);
17766 executor.run_until_parked();
17767
17768 cx.update_editor(|editor, window, cx| {
17769 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17770 });
17771 executor.run_until_parked();
17772
17773 cx.assert_state_with_diff(
17774 r#"
17775 use some::mod1;
17776 use some::mod2;
17777
17778 - const A: u32 = 42;
17779 ˇconst B: u32 = 42;
17780 const C: u32 = 42;
17781
17782
17783 fn main() {
17784 println!("hello");
17785
17786 println!("world");
17787 }
17788 "#
17789 .unindent(),
17790 );
17791
17792 cx.update_editor(|editor, window, cx| {
17793 editor.delete_line(&DeleteLine, window, cx);
17794 });
17795 executor.run_until_parked();
17796 cx.assert_state_with_diff(
17797 r#"
17798 use some::mod1;
17799 use some::mod2;
17800
17801 - const A: u32 = 42;
17802 - const B: u32 = 42;
17803 ˇconst C: u32 = 42;
17804
17805
17806 fn main() {
17807 println!("hello");
17808
17809 println!("world");
17810 }
17811 "#
17812 .unindent(),
17813 );
17814
17815 cx.update_editor(|editor, window, cx| {
17816 editor.delete_line(&DeleteLine, window, cx);
17817 });
17818 executor.run_until_parked();
17819 cx.assert_state_with_diff(
17820 r#"
17821 use some::mod1;
17822 use some::mod2;
17823
17824 - const A: u32 = 42;
17825 - const B: u32 = 42;
17826 - const C: u32 = 42;
17827 ˇ
17828
17829 fn main() {
17830 println!("hello");
17831
17832 println!("world");
17833 }
17834 "#
17835 .unindent(),
17836 );
17837
17838 cx.update_editor(|editor, window, cx| {
17839 editor.handle_input("replacement", window, cx);
17840 });
17841 executor.run_until_parked();
17842 cx.assert_state_with_diff(
17843 r#"
17844 use some::mod1;
17845 use some::mod2;
17846
17847 - const A: u32 = 42;
17848 - const B: u32 = 42;
17849 - const C: u32 = 42;
17850 -
17851 + replacementˇ
17852
17853 fn main() {
17854 println!("hello");
17855
17856 println!("world");
17857 }
17858 "#
17859 .unindent(),
17860 );
17861}
17862
17863#[gpui::test]
17864async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17865 init_test(cx, |_| {});
17866
17867 let mut cx = EditorTestContext::new(cx).await;
17868
17869 let base_text = r#"
17870 one
17871 two
17872 three
17873 four
17874 five
17875 "#
17876 .unindent();
17877 executor.run_until_parked();
17878 cx.set_state(
17879 &r#"
17880 one
17881 two
17882 fˇour
17883 five
17884 "#
17885 .unindent(),
17886 );
17887
17888 cx.set_head_text(&base_text);
17889 executor.run_until_parked();
17890
17891 cx.update_editor(|editor, window, cx| {
17892 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17893 });
17894 executor.run_until_parked();
17895
17896 cx.assert_state_with_diff(
17897 r#"
17898 one
17899 two
17900 - three
17901 fˇour
17902 five
17903 "#
17904 .unindent(),
17905 );
17906
17907 cx.update_editor(|editor, window, cx| {
17908 editor.backspace(&Backspace, window, cx);
17909 editor.backspace(&Backspace, window, cx);
17910 });
17911 executor.run_until_parked();
17912 cx.assert_state_with_diff(
17913 r#"
17914 one
17915 two
17916 - threeˇ
17917 - four
17918 + our
17919 five
17920 "#
17921 .unindent(),
17922 );
17923}
17924
17925#[gpui::test]
17926async fn test_edit_after_expanded_modification_hunk(
17927 executor: BackgroundExecutor,
17928 cx: &mut TestAppContext,
17929) {
17930 init_test(cx, |_| {});
17931
17932 let mut cx = EditorTestContext::new(cx).await;
17933
17934 let diff_base = r#"
17935 use some::mod1;
17936 use some::mod2;
17937
17938 const A: u32 = 42;
17939 const B: u32 = 42;
17940 const C: u32 = 42;
17941 const D: u32 = 42;
17942
17943
17944 fn main() {
17945 println!("hello");
17946
17947 println!("world");
17948 }"#
17949 .unindent();
17950
17951 cx.set_state(
17952 &r#"
17953 use some::mod1;
17954 use some::mod2;
17955
17956 const A: u32 = 42;
17957 const B: u32 = 42;
17958 const C: u32 = 43ˇ
17959 const D: u32 = 42;
17960
17961
17962 fn main() {
17963 println!("hello");
17964
17965 println!("world");
17966 }"#
17967 .unindent(),
17968 );
17969
17970 cx.set_head_text(&diff_base);
17971 executor.run_until_parked();
17972 cx.update_editor(|editor, window, cx| {
17973 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17974 });
17975 executor.run_until_parked();
17976
17977 cx.assert_state_with_diff(
17978 r#"
17979 use some::mod1;
17980 use some::mod2;
17981
17982 const A: u32 = 42;
17983 const B: u32 = 42;
17984 - const C: u32 = 42;
17985 + const C: u32 = 43ˇ
17986 const D: u32 = 42;
17987
17988
17989 fn main() {
17990 println!("hello");
17991
17992 println!("world");
17993 }"#
17994 .unindent(),
17995 );
17996
17997 cx.update_editor(|editor, window, cx| {
17998 editor.handle_input("\nnew_line\n", window, cx);
17999 });
18000 executor.run_until_parked();
18001
18002 cx.assert_state_with_diff(
18003 r#"
18004 use some::mod1;
18005 use some::mod2;
18006
18007 const A: u32 = 42;
18008 const B: u32 = 42;
18009 - const C: u32 = 42;
18010 + const C: u32 = 43
18011 + new_line
18012 + ˇ
18013 const D: u32 = 42;
18014
18015
18016 fn main() {
18017 println!("hello");
18018
18019 println!("world");
18020 }"#
18021 .unindent(),
18022 );
18023}
18024
18025#[gpui::test]
18026async fn test_stage_and_unstage_added_file_hunk(
18027 executor: BackgroundExecutor,
18028 cx: &mut TestAppContext,
18029) {
18030 init_test(cx, |_| {});
18031
18032 let mut cx = EditorTestContext::new(cx).await;
18033 cx.update_editor(|editor, _, cx| {
18034 editor.set_expand_all_diff_hunks(cx);
18035 });
18036
18037 let working_copy = r#"
18038 ˇfn main() {
18039 println!("hello, world!");
18040 }
18041 "#
18042 .unindent();
18043
18044 cx.set_state(&working_copy);
18045 executor.run_until_parked();
18046
18047 cx.assert_state_with_diff(
18048 r#"
18049 + ˇfn main() {
18050 + println!("hello, world!");
18051 + }
18052 "#
18053 .unindent(),
18054 );
18055 cx.assert_index_text(None);
18056
18057 cx.update_editor(|editor, window, cx| {
18058 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18059 });
18060 executor.run_until_parked();
18061 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
18062 cx.assert_state_with_diff(
18063 r#"
18064 + ˇfn main() {
18065 + println!("hello, world!");
18066 + }
18067 "#
18068 .unindent(),
18069 );
18070
18071 cx.update_editor(|editor, window, cx| {
18072 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18073 });
18074 executor.run_until_parked();
18075 cx.assert_index_text(None);
18076}
18077
18078async fn setup_indent_guides_editor(
18079 text: &str,
18080 cx: &mut TestAppContext,
18081) -> (BufferId, EditorTestContext) {
18082 init_test(cx, |_| {});
18083
18084 let mut cx = EditorTestContext::new(cx).await;
18085
18086 let buffer_id = cx.update_editor(|editor, window, cx| {
18087 editor.set_text(text, window, cx);
18088 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
18089
18090 buffer_ids[0]
18091 });
18092
18093 (buffer_id, cx)
18094}
18095
18096fn assert_indent_guides(
18097 range: Range<u32>,
18098 expected: Vec<IndentGuide>,
18099 active_indices: Option<Vec<usize>>,
18100 cx: &mut EditorTestContext,
18101) {
18102 let indent_guides = cx.update_editor(|editor, window, cx| {
18103 let snapshot = editor.snapshot(window, cx).display_snapshot;
18104 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
18105 editor,
18106 MultiBufferRow(range.start)..MultiBufferRow(range.end),
18107 true,
18108 &snapshot,
18109 cx,
18110 );
18111
18112 indent_guides.sort_by(|a, b| {
18113 a.depth.cmp(&b.depth).then(
18114 a.start_row
18115 .cmp(&b.start_row)
18116 .then(a.end_row.cmp(&b.end_row)),
18117 )
18118 });
18119 indent_guides
18120 });
18121
18122 if let Some(expected) = active_indices {
18123 let active_indices = cx.update_editor(|editor, window, cx| {
18124 let snapshot = editor.snapshot(window, cx).display_snapshot;
18125 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
18126 });
18127
18128 assert_eq!(
18129 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
18130 expected,
18131 "Active indent guide indices do not match"
18132 );
18133 }
18134
18135 assert_eq!(indent_guides, expected, "Indent guides do not match");
18136}
18137
18138fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
18139 IndentGuide {
18140 buffer_id,
18141 start_row: MultiBufferRow(start_row),
18142 end_row: MultiBufferRow(end_row),
18143 depth,
18144 tab_size: 4,
18145 settings: IndentGuideSettings {
18146 enabled: true,
18147 line_width: 1,
18148 active_line_width: 1,
18149 ..Default::default()
18150 },
18151 }
18152}
18153
18154#[gpui::test]
18155async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
18156 let (buffer_id, mut cx) = setup_indent_guides_editor(
18157 &"
18158 fn main() {
18159 let a = 1;
18160 }"
18161 .unindent(),
18162 cx,
18163 )
18164 .await;
18165
18166 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18167}
18168
18169#[gpui::test]
18170async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
18171 let (buffer_id, mut cx) = setup_indent_guides_editor(
18172 &"
18173 fn main() {
18174 let a = 1;
18175 let b = 2;
18176 }"
18177 .unindent(),
18178 cx,
18179 )
18180 .await;
18181
18182 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
18183}
18184
18185#[gpui::test]
18186async fn test_indent_guide_nested(cx: &mut TestAppContext) {
18187 let (buffer_id, mut cx) = setup_indent_guides_editor(
18188 &"
18189 fn main() {
18190 let a = 1;
18191 if a == 3 {
18192 let b = 2;
18193 } else {
18194 let c = 3;
18195 }
18196 }"
18197 .unindent(),
18198 cx,
18199 )
18200 .await;
18201
18202 assert_indent_guides(
18203 0..8,
18204 vec![
18205 indent_guide(buffer_id, 1, 6, 0),
18206 indent_guide(buffer_id, 3, 3, 1),
18207 indent_guide(buffer_id, 5, 5, 1),
18208 ],
18209 None,
18210 &mut cx,
18211 );
18212}
18213
18214#[gpui::test]
18215async fn test_indent_guide_tab(cx: &mut TestAppContext) {
18216 let (buffer_id, mut cx) = setup_indent_guides_editor(
18217 &"
18218 fn main() {
18219 let a = 1;
18220 let b = 2;
18221 let c = 3;
18222 }"
18223 .unindent(),
18224 cx,
18225 )
18226 .await;
18227
18228 assert_indent_guides(
18229 0..5,
18230 vec![
18231 indent_guide(buffer_id, 1, 3, 0),
18232 indent_guide(buffer_id, 2, 2, 1),
18233 ],
18234 None,
18235 &mut cx,
18236 );
18237}
18238
18239#[gpui::test]
18240async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
18241 let (buffer_id, mut cx) = setup_indent_guides_editor(
18242 &"
18243 fn main() {
18244 let a = 1;
18245
18246 let c = 3;
18247 }"
18248 .unindent(),
18249 cx,
18250 )
18251 .await;
18252
18253 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
18254}
18255
18256#[gpui::test]
18257async fn test_indent_guide_complex(cx: &mut TestAppContext) {
18258 let (buffer_id, mut cx) = setup_indent_guides_editor(
18259 &"
18260 fn main() {
18261 let a = 1;
18262
18263 let c = 3;
18264
18265 if a == 3 {
18266 let b = 2;
18267 } else {
18268 let c = 3;
18269 }
18270 }"
18271 .unindent(),
18272 cx,
18273 )
18274 .await;
18275
18276 assert_indent_guides(
18277 0..11,
18278 vec![
18279 indent_guide(buffer_id, 1, 9, 0),
18280 indent_guide(buffer_id, 6, 6, 1),
18281 indent_guide(buffer_id, 8, 8, 1),
18282 ],
18283 None,
18284 &mut cx,
18285 );
18286}
18287
18288#[gpui::test]
18289async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
18290 let (buffer_id, mut cx) = setup_indent_guides_editor(
18291 &"
18292 fn main() {
18293 let a = 1;
18294
18295 let c = 3;
18296
18297 if a == 3 {
18298 let b = 2;
18299 } else {
18300 let c = 3;
18301 }
18302 }"
18303 .unindent(),
18304 cx,
18305 )
18306 .await;
18307
18308 assert_indent_guides(
18309 1..11,
18310 vec![
18311 indent_guide(buffer_id, 1, 9, 0),
18312 indent_guide(buffer_id, 6, 6, 1),
18313 indent_guide(buffer_id, 8, 8, 1),
18314 ],
18315 None,
18316 &mut cx,
18317 );
18318}
18319
18320#[gpui::test]
18321async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
18322 let (buffer_id, mut cx) = setup_indent_guides_editor(
18323 &"
18324 fn main() {
18325 let a = 1;
18326
18327 let c = 3;
18328
18329 if a == 3 {
18330 let b = 2;
18331 } else {
18332 let c = 3;
18333 }
18334 }"
18335 .unindent(),
18336 cx,
18337 )
18338 .await;
18339
18340 assert_indent_guides(
18341 1..10,
18342 vec![
18343 indent_guide(buffer_id, 1, 9, 0),
18344 indent_guide(buffer_id, 6, 6, 1),
18345 indent_guide(buffer_id, 8, 8, 1),
18346 ],
18347 None,
18348 &mut cx,
18349 );
18350}
18351
18352#[gpui::test]
18353async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
18354 let (buffer_id, mut cx) = setup_indent_guides_editor(
18355 &"
18356 fn main() {
18357 if a {
18358 b(
18359 c,
18360 d,
18361 )
18362 } else {
18363 e(
18364 f
18365 )
18366 }
18367 }"
18368 .unindent(),
18369 cx,
18370 )
18371 .await;
18372
18373 assert_indent_guides(
18374 0..11,
18375 vec![
18376 indent_guide(buffer_id, 1, 10, 0),
18377 indent_guide(buffer_id, 2, 5, 1),
18378 indent_guide(buffer_id, 7, 9, 1),
18379 indent_guide(buffer_id, 3, 4, 2),
18380 indent_guide(buffer_id, 8, 8, 2),
18381 ],
18382 None,
18383 &mut cx,
18384 );
18385
18386 cx.update_editor(|editor, window, cx| {
18387 editor.fold_at(MultiBufferRow(2), window, cx);
18388 assert_eq!(
18389 editor.display_text(cx),
18390 "
18391 fn main() {
18392 if a {
18393 b(⋯
18394 )
18395 } else {
18396 e(
18397 f
18398 )
18399 }
18400 }"
18401 .unindent()
18402 );
18403 });
18404
18405 assert_indent_guides(
18406 0..11,
18407 vec![
18408 indent_guide(buffer_id, 1, 10, 0),
18409 indent_guide(buffer_id, 2, 5, 1),
18410 indent_guide(buffer_id, 7, 9, 1),
18411 indent_guide(buffer_id, 8, 8, 2),
18412 ],
18413 None,
18414 &mut cx,
18415 );
18416}
18417
18418#[gpui::test]
18419async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
18420 let (buffer_id, mut cx) = setup_indent_guides_editor(
18421 &"
18422 block1
18423 block2
18424 block3
18425 block4
18426 block2
18427 block1
18428 block1"
18429 .unindent(),
18430 cx,
18431 )
18432 .await;
18433
18434 assert_indent_guides(
18435 1..10,
18436 vec![
18437 indent_guide(buffer_id, 1, 4, 0),
18438 indent_guide(buffer_id, 2, 3, 1),
18439 indent_guide(buffer_id, 3, 3, 2),
18440 ],
18441 None,
18442 &mut cx,
18443 );
18444}
18445
18446#[gpui::test]
18447async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
18448 let (buffer_id, mut cx) = setup_indent_guides_editor(
18449 &"
18450 block1
18451 block2
18452 block3
18453
18454 block1
18455 block1"
18456 .unindent(),
18457 cx,
18458 )
18459 .await;
18460
18461 assert_indent_guides(
18462 0..6,
18463 vec![
18464 indent_guide(buffer_id, 1, 2, 0),
18465 indent_guide(buffer_id, 2, 2, 1),
18466 ],
18467 None,
18468 &mut cx,
18469 );
18470}
18471
18472#[gpui::test]
18473async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
18474 let (buffer_id, mut cx) = setup_indent_guides_editor(
18475 &"
18476 function component() {
18477 \treturn (
18478 \t\t\t
18479 \t\t<div>
18480 \t\t\t<abc></abc>
18481 \t\t</div>
18482 \t)
18483 }"
18484 .unindent(),
18485 cx,
18486 )
18487 .await;
18488
18489 assert_indent_guides(
18490 0..8,
18491 vec![
18492 indent_guide(buffer_id, 1, 6, 0),
18493 indent_guide(buffer_id, 2, 5, 1),
18494 indent_guide(buffer_id, 4, 4, 2),
18495 ],
18496 None,
18497 &mut cx,
18498 );
18499}
18500
18501#[gpui::test]
18502async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
18503 let (buffer_id, mut cx) = setup_indent_guides_editor(
18504 &"
18505 function component() {
18506 \treturn (
18507 \t
18508 \t\t<div>
18509 \t\t\t<abc></abc>
18510 \t\t</div>
18511 \t)
18512 }"
18513 .unindent(),
18514 cx,
18515 )
18516 .await;
18517
18518 assert_indent_guides(
18519 0..8,
18520 vec![
18521 indent_guide(buffer_id, 1, 6, 0),
18522 indent_guide(buffer_id, 2, 5, 1),
18523 indent_guide(buffer_id, 4, 4, 2),
18524 ],
18525 None,
18526 &mut cx,
18527 );
18528}
18529
18530#[gpui::test]
18531async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
18532 let (buffer_id, mut cx) = setup_indent_guides_editor(
18533 &"
18534 block1
18535
18536
18537
18538 block2
18539 "
18540 .unindent(),
18541 cx,
18542 )
18543 .await;
18544
18545 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18546}
18547
18548#[gpui::test]
18549async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
18550 let (buffer_id, mut cx) = setup_indent_guides_editor(
18551 &"
18552 def a:
18553 \tb = 3
18554 \tif True:
18555 \t\tc = 4
18556 \t\td = 5
18557 \tprint(b)
18558 "
18559 .unindent(),
18560 cx,
18561 )
18562 .await;
18563
18564 assert_indent_guides(
18565 0..6,
18566 vec![
18567 indent_guide(buffer_id, 1, 5, 0),
18568 indent_guide(buffer_id, 3, 4, 1),
18569 ],
18570 None,
18571 &mut cx,
18572 );
18573}
18574
18575#[gpui::test]
18576async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
18577 let (buffer_id, mut cx) = setup_indent_guides_editor(
18578 &"
18579 fn main() {
18580 let a = 1;
18581 }"
18582 .unindent(),
18583 cx,
18584 )
18585 .await;
18586
18587 cx.update_editor(|editor, window, cx| {
18588 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18589 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18590 });
18591 });
18592
18593 assert_indent_guides(
18594 0..3,
18595 vec![indent_guide(buffer_id, 1, 1, 0)],
18596 Some(vec![0]),
18597 &mut cx,
18598 );
18599}
18600
18601#[gpui::test]
18602async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
18603 let (buffer_id, mut cx) = setup_indent_guides_editor(
18604 &"
18605 fn main() {
18606 if 1 == 2 {
18607 let a = 1;
18608 }
18609 }"
18610 .unindent(),
18611 cx,
18612 )
18613 .await;
18614
18615 cx.update_editor(|editor, window, cx| {
18616 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18617 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18618 });
18619 });
18620
18621 assert_indent_guides(
18622 0..4,
18623 vec![
18624 indent_guide(buffer_id, 1, 3, 0),
18625 indent_guide(buffer_id, 2, 2, 1),
18626 ],
18627 Some(vec![1]),
18628 &mut cx,
18629 );
18630
18631 cx.update_editor(|editor, window, cx| {
18632 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18633 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18634 });
18635 });
18636
18637 assert_indent_guides(
18638 0..4,
18639 vec![
18640 indent_guide(buffer_id, 1, 3, 0),
18641 indent_guide(buffer_id, 2, 2, 1),
18642 ],
18643 Some(vec![1]),
18644 &mut cx,
18645 );
18646
18647 cx.update_editor(|editor, window, cx| {
18648 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18649 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
18650 });
18651 });
18652
18653 assert_indent_guides(
18654 0..4,
18655 vec![
18656 indent_guide(buffer_id, 1, 3, 0),
18657 indent_guide(buffer_id, 2, 2, 1),
18658 ],
18659 Some(vec![0]),
18660 &mut cx,
18661 );
18662}
18663
18664#[gpui::test]
18665async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
18666 let (buffer_id, mut cx) = setup_indent_guides_editor(
18667 &"
18668 fn main() {
18669 let a = 1;
18670
18671 let b = 2;
18672 }"
18673 .unindent(),
18674 cx,
18675 )
18676 .await;
18677
18678 cx.update_editor(|editor, window, cx| {
18679 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18680 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18681 });
18682 });
18683
18684 assert_indent_guides(
18685 0..5,
18686 vec![indent_guide(buffer_id, 1, 3, 0)],
18687 Some(vec![0]),
18688 &mut cx,
18689 );
18690}
18691
18692#[gpui::test]
18693async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
18694 let (buffer_id, mut cx) = setup_indent_guides_editor(
18695 &"
18696 def m:
18697 a = 1
18698 pass"
18699 .unindent(),
18700 cx,
18701 )
18702 .await;
18703
18704 cx.update_editor(|editor, window, cx| {
18705 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18706 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18707 });
18708 });
18709
18710 assert_indent_guides(
18711 0..3,
18712 vec![indent_guide(buffer_id, 1, 2, 0)],
18713 Some(vec![0]),
18714 &mut cx,
18715 );
18716}
18717
18718#[gpui::test]
18719async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
18720 init_test(cx, |_| {});
18721 let mut cx = EditorTestContext::new(cx).await;
18722 let text = indoc! {
18723 "
18724 impl A {
18725 fn b() {
18726 0;
18727 3;
18728 5;
18729 6;
18730 7;
18731 }
18732 }
18733 "
18734 };
18735 let base_text = indoc! {
18736 "
18737 impl A {
18738 fn b() {
18739 0;
18740 1;
18741 2;
18742 3;
18743 4;
18744 }
18745 fn c() {
18746 5;
18747 6;
18748 7;
18749 }
18750 }
18751 "
18752 };
18753
18754 cx.update_editor(|editor, window, cx| {
18755 editor.set_text(text, window, cx);
18756
18757 editor.buffer().update(cx, |multibuffer, cx| {
18758 let buffer = multibuffer.as_singleton().unwrap();
18759 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
18760
18761 multibuffer.set_all_diff_hunks_expanded(cx);
18762 multibuffer.add_diff(diff, cx);
18763
18764 buffer.read(cx).remote_id()
18765 })
18766 });
18767 cx.run_until_parked();
18768
18769 cx.assert_state_with_diff(
18770 indoc! { "
18771 impl A {
18772 fn b() {
18773 0;
18774 - 1;
18775 - 2;
18776 3;
18777 - 4;
18778 - }
18779 - fn c() {
18780 5;
18781 6;
18782 7;
18783 }
18784 }
18785 ˇ"
18786 }
18787 .to_string(),
18788 );
18789
18790 let mut actual_guides = cx.update_editor(|editor, window, cx| {
18791 editor
18792 .snapshot(window, cx)
18793 .buffer_snapshot
18794 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
18795 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
18796 .collect::<Vec<_>>()
18797 });
18798 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
18799 assert_eq!(
18800 actual_guides,
18801 vec![
18802 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
18803 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
18804 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
18805 ]
18806 );
18807}
18808
18809#[gpui::test]
18810async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18811 init_test(cx, |_| {});
18812 let mut cx = EditorTestContext::new(cx).await;
18813
18814 let diff_base = r#"
18815 a
18816 b
18817 c
18818 "#
18819 .unindent();
18820
18821 cx.set_state(
18822 &r#"
18823 ˇA
18824 b
18825 C
18826 "#
18827 .unindent(),
18828 );
18829 cx.set_head_text(&diff_base);
18830 cx.update_editor(|editor, window, cx| {
18831 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18832 });
18833 executor.run_until_parked();
18834
18835 let both_hunks_expanded = r#"
18836 - a
18837 + ˇA
18838 b
18839 - c
18840 + C
18841 "#
18842 .unindent();
18843
18844 cx.assert_state_with_diff(both_hunks_expanded.clone());
18845
18846 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18847 let snapshot = editor.snapshot(window, cx);
18848 let hunks = editor
18849 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18850 .collect::<Vec<_>>();
18851 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18852 let buffer_id = hunks[0].buffer_id;
18853 hunks
18854 .into_iter()
18855 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18856 .collect::<Vec<_>>()
18857 });
18858 assert_eq!(hunk_ranges.len(), 2);
18859
18860 cx.update_editor(|editor, _, cx| {
18861 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18862 });
18863 executor.run_until_parked();
18864
18865 let second_hunk_expanded = r#"
18866 ˇA
18867 b
18868 - c
18869 + C
18870 "#
18871 .unindent();
18872
18873 cx.assert_state_with_diff(second_hunk_expanded);
18874
18875 cx.update_editor(|editor, _, cx| {
18876 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18877 });
18878 executor.run_until_parked();
18879
18880 cx.assert_state_with_diff(both_hunks_expanded.clone());
18881
18882 cx.update_editor(|editor, _, cx| {
18883 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18884 });
18885 executor.run_until_parked();
18886
18887 let first_hunk_expanded = r#"
18888 - a
18889 + ˇA
18890 b
18891 C
18892 "#
18893 .unindent();
18894
18895 cx.assert_state_with_diff(first_hunk_expanded);
18896
18897 cx.update_editor(|editor, _, cx| {
18898 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18899 });
18900 executor.run_until_parked();
18901
18902 cx.assert_state_with_diff(both_hunks_expanded);
18903
18904 cx.set_state(
18905 &r#"
18906 ˇA
18907 b
18908 "#
18909 .unindent(),
18910 );
18911 cx.run_until_parked();
18912
18913 // TODO this cursor position seems bad
18914 cx.assert_state_with_diff(
18915 r#"
18916 - ˇa
18917 + A
18918 b
18919 "#
18920 .unindent(),
18921 );
18922
18923 cx.update_editor(|editor, window, cx| {
18924 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18925 });
18926
18927 cx.assert_state_with_diff(
18928 r#"
18929 - ˇa
18930 + A
18931 b
18932 - c
18933 "#
18934 .unindent(),
18935 );
18936
18937 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18938 let snapshot = editor.snapshot(window, cx);
18939 let hunks = editor
18940 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18941 .collect::<Vec<_>>();
18942 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18943 let buffer_id = hunks[0].buffer_id;
18944 hunks
18945 .into_iter()
18946 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18947 .collect::<Vec<_>>()
18948 });
18949 assert_eq!(hunk_ranges.len(), 2);
18950
18951 cx.update_editor(|editor, _, cx| {
18952 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18953 });
18954 executor.run_until_parked();
18955
18956 cx.assert_state_with_diff(
18957 r#"
18958 - ˇa
18959 + A
18960 b
18961 "#
18962 .unindent(),
18963 );
18964}
18965
18966#[gpui::test]
18967async fn test_toggle_deletion_hunk_at_start_of_file(
18968 executor: BackgroundExecutor,
18969 cx: &mut TestAppContext,
18970) {
18971 init_test(cx, |_| {});
18972 let mut cx = EditorTestContext::new(cx).await;
18973
18974 let diff_base = r#"
18975 a
18976 b
18977 c
18978 "#
18979 .unindent();
18980
18981 cx.set_state(
18982 &r#"
18983 ˇb
18984 c
18985 "#
18986 .unindent(),
18987 );
18988 cx.set_head_text(&diff_base);
18989 cx.update_editor(|editor, window, cx| {
18990 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18991 });
18992 executor.run_until_parked();
18993
18994 let hunk_expanded = r#"
18995 - a
18996 ˇb
18997 c
18998 "#
18999 .unindent();
19000
19001 cx.assert_state_with_diff(hunk_expanded.clone());
19002
19003 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19004 let snapshot = editor.snapshot(window, cx);
19005 let hunks = editor
19006 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19007 .collect::<Vec<_>>();
19008 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19009 let buffer_id = hunks[0].buffer_id;
19010 hunks
19011 .into_iter()
19012 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19013 .collect::<Vec<_>>()
19014 });
19015 assert_eq!(hunk_ranges.len(), 1);
19016
19017 cx.update_editor(|editor, _, cx| {
19018 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19019 });
19020 executor.run_until_parked();
19021
19022 let hunk_collapsed = r#"
19023 ˇb
19024 c
19025 "#
19026 .unindent();
19027
19028 cx.assert_state_with_diff(hunk_collapsed);
19029
19030 cx.update_editor(|editor, _, cx| {
19031 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19032 });
19033 executor.run_until_parked();
19034
19035 cx.assert_state_with_diff(hunk_expanded.clone());
19036}
19037
19038#[gpui::test]
19039async fn test_display_diff_hunks(cx: &mut TestAppContext) {
19040 init_test(cx, |_| {});
19041
19042 let fs = FakeFs::new(cx.executor());
19043 fs.insert_tree(
19044 path!("/test"),
19045 json!({
19046 ".git": {},
19047 "file-1": "ONE\n",
19048 "file-2": "TWO\n",
19049 "file-3": "THREE\n",
19050 }),
19051 )
19052 .await;
19053
19054 fs.set_head_for_repo(
19055 path!("/test/.git").as_ref(),
19056 &[
19057 ("file-1".into(), "one\n".into()),
19058 ("file-2".into(), "two\n".into()),
19059 ("file-3".into(), "three\n".into()),
19060 ],
19061 "deadbeef",
19062 );
19063
19064 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
19065 let mut buffers = vec![];
19066 for i in 1..=3 {
19067 let buffer = project
19068 .update(cx, |project, cx| {
19069 let path = format!(path!("/test/file-{}"), i);
19070 project.open_local_buffer(path, cx)
19071 })
19072 .await
19073 .unwrap();
19074 buffers.push(buffer);
19075 }
19076
19077 let multibuffer = cx.new(|cx| {
19078 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
19079 multibuffer.set_all_diff_hunks_expanded(cx);
19080 for buffer in &buffers {
19081 let snapshot = buffer.read(cx).snapshot();
19082 multibuffer.set_excerpts_for_path(
19083 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
19084 buffer.clone(),
19085 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
19086 DEFAULT_MULTIBUFFER_CONTEXT,
19087 cx,
19088 );
19089 }
19090 multibuffer
19091 });
19092
19093 let editor = cx.add_window(|window, cx| {
19094 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
19095 });
19096 cx.run_until_parked();
19097
19098 let snapshot = editor
19099 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19100 .unwrap();
19101 let hunks = snapshot
19102 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
19103 .map(|hunk| match hunk {
19104 DisplayDiffHunk::Unfolded {
19105 display_row_range, ..
19106 } => display_row_range,
19107 DisplayDiffHunk::Folded { .. } => unreachable!(),
19108 })
19109 .collect::<Vec<_>>();
19110 assert_eq!(
19111 hunks,
19112 [
19113 DisplayRow(2)..DisplayRow(4),
19114 DisplayRow(7)..DisplayRow(9),
19115 DisplayRow(12)..DisplayRow(14),
19116 ]
19117 );
19118}
19119
19120#[gpui::test]
19121async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
19122 init_test(cx, |_| {});
19123
19124 let mut cx = EditorTestContext::new(cx).await;
19125 cx.set_head_text(indoc! { "
19126 one
19127 two
19128 three
19129 four
19130 five
19131 "
19132 });
19133 cx.set_index_text(indoc! { "
19134 one
19135 two
19136 three
19137 four
19138 five
19139 "
19140 });
19141 cx.set_state(indoc! {"
19142 one
19143 TWO
19144 ˇTHREE
19145 FOUR
19146 five
19147 "});
19148 cx.run_until_parked();
19149 cx.update_editor(|editor, window, cx| {
19150 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19151 });
19152 cx.run_until_parked();
19153 cx.assert_index_text(Some(indoc! {"
19154 one
19155 TWO
19156 THREE
19157 FOUR
19158 five
19159 "}));
19160 cx.set_state(indoc! { "
19161 one
19162 TWO
19163 ˇTHREE-HUNDRED
19164 FOUR
19165 five
19166 "});
19167 cx.run_until_parked();
19168 cx.update_editor(|editor, window, cx| {
19169 let snapshot = editor.snapshot(window, cx);
19170 let hunks = editor
19171 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19172 .collect::<Vec<_>>();
19173 assert_eq!(hunks.len(), 1);
19174 assert_eq!(
19175 hunks[0].status(),
19176 DiffHunkStatus {
19177 kind: DiffHunkStatusKind::Modified,
19178 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
19179 }
19180 );
19181
19182 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19183 });
19184 cx.run_until_parked();
19185 cx.assert_index_text(Some(indoc! {"
19186 one
19187 TWO
19188 THREE-HUNDRED
19189 FOUR
19190 five
19191 "}));
19192}
19193
19194#[gpui::test]
19195fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
19196 init_test(cx, |_| {});
19197
19198 let editor = cx.add_window(|window, cx| {
19199 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
19200 build_editor(buffer, window, cx)
19201 });
19202
19203 let render_args = Arc::new(Mutex::new(None));
19204 let snapshot = editor
19205 .update(cx, |editor, window, cx| {
19206 let snapshot = editor.buffer().read(cx).snapshot(cx);
19207 let range =
19208 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
19209
19210 struct RenderArgs {
19211 row: MultiBufferRow,
19212 folded: bool,
19213 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
19214 }
19215
19216 let crease = Crease::inline(
19217 range,
19218 FoldPlaceholder::test(),
19219 {
19220 let toggle_callback = render_args.clone();
19221 move |row, folded, callback, _window, _cx| {
19222 *toggle_callback.lock() = Some(RenderArgs {
19223 row,
19224 folded,
19225 callback,
19226 });
19227 div()
19228 }
19229 },
19230 |_row, _folded, _window, _cx| div(),
19231 );
19232
19233 editor.insert_creases(Some(crease), cx);
19234 let snapshot = editor.snapshot(window, cx);
19235 let _div = snapshot.render_crease_toggle(
19236 MultiBufferRow(1),
19237 false,
19238 cx.entity().clone(),
19239 window,
19240 cx,
19241 );
19242 snapshot
19243 })
19244 .unwrap();
19245
19246 let render_args = render_args.lock().take().unwrap();
19247 assert_eq!(render_args.row, MultiBufferRow(1));
19248 assert!(!render_args.folded);
19249 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19250
19251 cx.update_window(*editor, |_, window, cx| {
19252 (render_args.callback)(true, window, cx)
19253 })
19254 .unwrap();
19255 let snapshot = editor
19256 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19257 .unwrap();
19258 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
19259
19260 cx.update_window(*editor, |_, window, cx| {
19261 (render_args.callback)(false, window, cx)
19262 })
19263 .unwrap();
19264 let snapshot = editor
19265 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19266 .unwrap();
19267 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19268}
19269
19270#[gpui::test]
19271async fn test_input_text(cx: &mut TestAppContext) {
19272 init_test(cx, |_| {});
19273 let mut cx = EditorTestContext::new(cx).await;
19274
19275 cx.set_state(
19276 &r#"ˇone
19277 two
19278
19279 three
19280 fourˇ
19281 five
19282
19283 siˇx"#
19284 .unindent(),
19285 );
19286
19287 cx.dispatch_action(HandleInput(String::new()));
19288 cx.assert_editor_state(
19289 &r#"ˇone
19290 two
19291
19292 three
19293 fourˇ
19294 five
19295
19296 siˇx"#
19297 .unindent(),
19298 );
19299
19300 cx.dispatch_action(HandleInput("AAAA".to_string()));
19301 cx.assert_editor_state(
19302 &r#"AAAAˇone
19303 two
19304
19305 three
19306 fourAAAAˇ
19307 five
19308
19309 siAAAAˇx"#
19310 .unindent(),
19311 );
19312}
19313
19314#[gpui::test]
19315async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
19316 init_test(cx, |_| {});
19317
19318 let mut cx = EditorTestContext::new(cx).await;
19319 cx.set_state(
19320 r#"let foo = 1;
19321let foo = 2;
19322let foo = 3;
19323let fooˇ = 4;
19324let foo = 5;
19325let foo = 6;
19326let foo = 7;
19327let foo = 8;
19328let foo = 9;
19329let foo = 10;
19330let foo = 11;
19331let foo = 12;
19332let foo = 13;
19333let foo = 14;
19334let foo = 15;"#,
19335 );
19336
19337 cx.update_editor(|e, window, cx| {
19338 assert_eq!(
19339 e.next_scroll_position,
19340 NextScrollCursorCenterTopBottom::Center,
19341 "Default next scroll direction is center",
19342 );
19343
19344 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19345 assert_eq!(
19346 e.next_scroll_position,
19347 NextScrollCursorCenterTopBottom::Top,
19348 "After center, next scroll direction should be top",
19349 );
19350
19351 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19352 assert_eq!(
19353 e.next_scroll_position,
19354 NextScrollCursorCenterTopBottom::Bottom,
19355 "After top, next scroll direction should be bottom",
19356 );
19357
19358 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19359 assert_eq!(
19360 e.next_scroll_position,
19361 NextScrollCursorCenterTopBottom::Center,
19362 "After bottom, scrolling should start over",
19363 );
19364
19365 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19366 assert_eq!(
19367 e.next_scroll_position,
19368 NextScrollCursorCenterTopBottom::Top,
19369 "Scrolling continues if retriggered fast enough"
19370 );
19371 });
19372
19373 cx.executor()
19374 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
19375 cx.executor().run_until_parked();
19376 cx.update_editor(|e, _, _| {
19377 assert_eq!(
19378 e.next_scroll_position,
19379 NextScrollCursorCenterTopBottom::Center,
19380 "If scrolling is not triggered fast enough, it should reset"
19381 );
19382 });
19383}
19384
19385#[gpui::test]
19386async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
19387 init_test(cx, |_| {});
19388 let mut cx = EditorLspTestContext::new_rust(
19389 lsp::ServerCapabilities {
19390 definition_provider: Some(lsp::OneOf::Left(true)),
19391 references_provider: Some(lsp::OneOf::Left(true)),
19392 ..lsp::ServerCapabilities::default()
19393 },
19394 cx,
19395 )
19396 .await;
19397
19398 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
19399 let go_to_definition = cx
19400 .lsp
19401 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19402 move |params, _| async move {
19403 if empty_go_to_definition {
19404 Ok(None)
19405 } else {
19406 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
19407 uri: params.text_document_position_params.text_document.uri,
19408 range: lsp::Range::new(
19409 lsp::Position::new(4, 3),
19410 lsp::Position::new(4, 6),
19411 ),
19412 })))
19413 }
19414 },
19415 );
19416 let references = cx
19417 .lsp
19418 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
19419 Ok(Some(vec![lsp::Location {
19420 uri: params.text_document_position.text_document.uri,
19421 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
19422 }]))
19423 });
19424 (go_to_definition, references)
19425 };
19426
19427 cx.set_state(
19428 &r#"fn one() {
19429 let mut a = ˇtwo();
19430 }
19431
19432 fn two() {}"#
19433 .unindent(),
19434 );
19435 set_up_lsp_handlers(false, &mut cx);
19436 let navigated = cx
19437 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19438 .await
19439 .expect("Failed to navigate to definition");
19440 assert_eq!(
19441 navigated,
19442 Navigated::Yes,
19443 "Should have navigated to definition from the GetDefinition response"
19444 );
19445 cx.assert_editor_state(
19446 &r#"fn one() {
19447 let mut a = two();
19448 }
19449
19450 fn «twoˇ»() {}"#
19451 .unindent(),
19452 );
19453
19454 let editors = cx.update_workspace(|workspace, _, cx| {
19455 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19456 });
19457 cx.update_editor(|_, _, test_editor_cx| {
19458 assert_eq!(
19459 editors.len(),
19460 1,
19461 "Initially, only one, test, editor should be open in the workspace"
19462 );
19463 assert_eq!(
19464 test_editor_cx.entity(),
19465 editors.last().expect("Asserted len is 1").clone()
19466 );
19467 });
19468
19469 set_up_lsp_handlers(true, &mut cx);
19470 let navigated = cx
19471 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19472 .await
19473 .expect("Failed to navigate to lookup references");
19474 assert_eq!(
19475 navigated,
19476 Navigated::Yes,
19477 "Should have navigated to references as a fallback after empty GoToDefinition response"
19478 );
19479 // We should not change the selections in the existing file,
19480 // if opening another milti buffer with the references
19481 cx.assert_editor_state(
19482 &r#"fn one() {
19483 let mut a = two();
19484 }
19485
19486 fn «twoˇ»() {}"#
19487 .unindent(),
19488 );
19489 let editors = cx.update_workspace(|workspace, _, cx| {
19490 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19491 });
19492 cx.update_editor(|_, _, test_editor_cx| {
19493 assert_eq!(
19494 editors.len(),
19495 2,
19496 "After falling back to references search, we open a new editor with the results"
19497 );
19498 let references_fallback_text = editors
19499 .into_iter()
19500 .find(|new_editor| *new_editor != test_editor_cx.entity())
19501 .expect("Should have one non-test editor now")
19502 .read(test_editor_cx)
19503 .text(test_editor_cx);
19504 assert_eq!(
19505 references_fallback_text, "fn one() {\n let mut a = two();\n}",
19506 "Should use the range from the references response and not the GoToDefinition one"
19507 );
19508 });
19509}
19510
19511#[gpui::test]
19512async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
19513 init_test(cx, |_| {});
19514 cx.update(|cx| {
19515 let mut editor_settings = EditorSettings::get_global(cx).clone();
19516 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
19517 EditorSettings::override_global(editor_settings, cx);
19518 });
19519 let mut cx = EditorLspTestContext::new_rust(
19520 lsp::ServerCapabilities {
19521 definition_provider: Some(lsp::OneOf::Left(true)),
19522 references_provider: Some(lsp::OneOf::Left(true)),
19523 ..lsp::ServerCapabilities::default()
19524 },
19525 cx,
19526 )
19527 .await;
19528 let original_state = r#"fn one() {
19529 let mut a = ˇtwo();
19530 }
19531
19532 fn two() {}"#
19533 .unindent();
19534 cx.set_state(&original_state);
19535
19536 let mut go_to_definition = cx
19537 .lsp
19538 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19539 move |_, _| async move { Ok(None) },
19540 );
19541 let _references = cx
19542 .lsp
19543 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
19544 panic!("Should not call for references with no go to definition fallback")
19545 });
19546
19547 let navigated = cx
19548 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19549 .await
19550 .expect("Failed to navigate to lookup references");
19551 go_to_definition
19552 .next()
19553 .await
19554 .expect("Should have called the go_to_definition handler");
19555
19556 assert_eq!(
19557 navigated,
19558 Navigated::No,
19559 "Should have navigated to references as a fallback after empty GoToDefinition response"
19560 );
19561 cx.assert_editor_state(&original_state);
19562 let editors = cx.update_workspace(|workspace, _, cx| {
19563 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19564 });
19565 cx.update_editor(|_, _, _| {
19566 assert_eq!(
19567 editors.len(),
19568 1,
19569 "After unsuccessful fallback, no other editor should have been opened"
19570 );
19571 });
19572}
19573
19574#[gpui::test]
19575async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
19576 init_test(cx, |_| {});
19577
19578 let language = Arc::new(Language::new(
19579 LanguageConfig::default(),
19580 Some(tree_sitter_rust::LANGUAGE.into()),
19581 ));
19582
19583 let text = r#"
19584 #[cfg(test)]
19585 mod tests() {
19586 #[test]
19587 fn runnable_1() {
19588 let a = 1;
19589 }
19590
19591 #[test]
19592 fn runnable_2() {
19593 let a = 1;
19594 let b = 2;
19595 }
19596 }
19597 "#
19598 .unindent();
19599
19600 let fs = FakeFs::new(cx.executor());
19601 fs.insert_file("/file.rs", Default::default()).await;
19602
19603 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19604 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19605 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19606 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
19607 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
19608
19609 let editor = cx.new_window_entity(|window, cx| {
19610 Editor::new(
19611 EditorMode::full(),
19612 multi_buffer,
19613 Some(project.clone()),
19614 window,
19615 cx,
19616 )
19617 });
19618
19619 editor.update_in(cx, |editor, window, cx| {
19620 let snapshot = editor.buffer().read(cx).snapshot(cx);
19621 editor.tasks.insert(
19622 (buffer.read(cx).remote_id(), 3),
19623 RunnableTasks {
19624 templates: vec![],
19625 offset: snapshot.anchor_before(43),
19626 column: 0,
19627 extra_variables: HashMap::default(),
19628 context_range: BufferOffset(43)..BufferOffset(85),
19629 },
19630 );
19631 editor.tasks.insert(
19632 (buffer.read(cx).remote_id(), 8),
19633 RunnableTasks {
19634 templates: vec![],
19635 offset: snapshot.anchor_before(86),
19636 column: 0,
19637 extra_variables: HashMap::default(),
19638 context_range: BufferOffset(86)..BufferOffset(191),
19639 },
19640 );
19641
19642 // Test finding task when cursor is inside function body
19643 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19644 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
19645 });
19646 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19647 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
19648
19649 // Test finding task when cursor is on function name
19650 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19651 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
19652 });
19653 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19654 assert_eq!(row, 8, "Should find task when cursor is on function name");
19655 });
19656}
19657
19658#[gpui::test]
19659async fn test_folding_buffers(cx: &mut TestAppContext) {
19660 init_test(cx, |_| {});
19661
19662 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19663 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
19664 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
19665
19666 let fs = FakeFs::new(cx.executor());
19667 fs.insert_tree(
19668 path!("/a"),
19669 json!({
19670 "first.rs": sample_text_1,
19671 "second.rs": sample_text_2,
19672 "third.rs": sample_text_3,
19673 }),
19674 )
19675 .await;
19676 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19677 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19678 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19679 let worktree = project.update(cx, |project, cx| {
19680 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19681 assert_eq!(worktrees.len(), 1);
19682 worktrees.pop().unwrap()
19683 });
19684 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19685
19686 let buffer_1 = project
19687 .update(cx, |project, cx| {
19688 project.open_buffer((worktree_id, "first.rs"), cx)
19689 })
19690 .await
19691 .unwrap();
19692 let buffer_2 = project
19693 .update(cx, |project, cx| {
19694 project.open_buffer((worktree_id, "second.rs"), cx)
19695 })
19696 .await
19697 .unwrap();
19698 let buffer_3 = project
19699 .update(cx, |project, cx| {
19700 project.open_buffer((worktree_id, "third.rs"), cx)
19701 })
19702 .await
19703 .unwrap();
19704
19705 let multi_buffer = cx.new(|cx| {
19706 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19707 multi_buffer.push_excerpts(
19708 buffer_1.clone(),
19709 [
19710 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19711 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19712 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19713 ],
19714 cx,
19715 );
19716 multi_buffer.push_excerpts(
19717 buffer_2.clone(),
19718 [
19719 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19720 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19721 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19722 ],
19723 cx,
19724 );
19725 multi_buffer.push_excerpts(
19726 buffer_3.clone(),
19727 [
19728 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19729 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19730 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19731 ],
19732 cx,
19733 );
19734 multi_buffer
19735 });
19736 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19737 Editor::new(
19738 EditorMode::full(),
19739 multi_buffer.clone(),
19740 Some(project.clone()),
19741 window,
19742 cx,
19743 )
19744 });
19745
19746 assert_eq!(
19747 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19748 "\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",
19749 );
19750
19751 multi_buffer_editor.update(cx, |editor, cx| {
19752 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19753 });
19754 assert_eq!(
19755 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19756 "\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",
19757 "After folding the first buffer, its text should not be displayed"
19758 );
19759
19760 multi_buffer_editor.update(cx, |editor, cx| {
19761 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19762 });
19763 assert_eq!(
19764 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19765 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
19766 "After folding the second buffer, its text should not be displayed"
19767 );
19768
19769 multi_buffer_editor.update(cx, |editor, cx| {
19770 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19771 });
19772 assert_eq!(
19773 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19774 "\n\n\n\n\n",
19775 "After folding the third buffer, its text should not be displayed"
19776 );
19777
19778 // Emulate selection inside the fold logic, that should work
19779 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19780 editor
19781 .snapshot(window, cx)
19782 .next_line_boundary(Point::new(0, 4));
19783 });
19784
19785 multi_buffer_editor.update(cx, |editor, cx| {
19786 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19787 });
19788 assert_eq!(
19789 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19790 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19791 "After unfolding the second buffer, its text should be displayed"
19792 );
19793
19794 // Typing inside of buffer 1 causes that buffer to be unfolded.
19795 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19796 assert_eq!(
19797 multi_buffer
19798 .read(cx)
19799 .snapshot(cx)
19800 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
19801 .collect::<String>(),
19802 "bbbb"
19803 );
19804 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
19805 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
19806 });
19807 editor.handle_input("B", window, cx);
19808 });
19809
19810 assert_eq!(
19811 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19812 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19813 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
19814 );
19815
19816 multi_buffer_editor.update(cx, |editor, cx| {
19817 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19818 });
19819 assert_eq!(
19820 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19821 "\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",
19822 "After unfolding the all buffers, all original text should be displayed"
19823 );
19824}
19825
19826#[gpui::test]
19827async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
19828 init_test(cx, |_| {});
19829
19830 let sample_text_1 = "1111\n2222\n3333".to_string();
19831 let sample_text_2 = "4444\n5555\n6666".to_string();
19832 let sample_text_3 = "7777\n8888\n9999".to_string();
19833
19834 let fs = FakeFs::new(cx.executor());
19835 fs.insert_tree(
19836 path!("/a"),
19837 json!({
19838 "first.rs": sample_text_1,
19839 "second.rs": sample_text_2,
19840 "third.rs": sample_text_3,
19841 }),
19842 )
19843 .await;
19844 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19845 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19846 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19847 let worktree = project.update(cx, |project, cx| {
19848 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19849 assert_eq!(worktrees.len(), 1);
19850 worktrees.pop().unwrap()
19851 });
19852 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19853
19854 let buffer_1 = project
19855 .update(cx, |project, cx| {
19856 project.open_buffer((worktree_id, "first.rs"), cx)
19857 })
19858 .await
19859 .unwrap();
19860 let buffer_2 = project
19861 .update(cx, |project, cx| {
19862 project.open_buffer((worktree_id, "second.rs"), cx)
19863 })
19864 .await
19865 .unwrap();
19866 let buffer_3 = project
19867 .update(cx, |project, cx| {
19868 project.open_buffer((worktree_id, "third.rs"), cx)
19869 })
19870 .await
19871 .unwrap();
19872
19873 let multi_buffer = cx.new(|cx| {
19874 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19875 multi_buffer.push_excerpts(
19876 buffer_1.clone(),
19877 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19878 cx,
19879 );
19880 multi_buffer.push_excerpts(
19881 buffer_2.clone(),
19882 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19883 cx,
19884 );
19885 multi_buffer.push_excerpts(
19886 buffer_3.clone(),
19887 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19888 cx,
19889 );
19890 multi_buffer
19891 });
19892
19893 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19894 Editor::new(
19895 EditorMode::full(),
19896 multi_buffer,
19897 Some(project.clone()),
19898 window,
19899 cx,
19900 )
19901 });
19902
19903 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
19904 assert_eq!(
19905 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19906 full_text,
19907 );
19908
19909 multi_buffer_editor.update(cx, |editor, cx| {
19910 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19911 });
19912 assert_eq!(
19913 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19914 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
19915 "After folding the first buffer, its text should not be displayed"
19916 );
19917
19918 multi_buffer_editor.update(cx, |editor, cx| {
19919 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19920 });
19921
19922 assert_eq!(
19923 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19924 "\n\n\n\n\n\n7777\n8888\n9999",
19925 "After folding the second buffer, its text should not be displayed"
19926 );
19927
19928 multi_buffer_editor.update(cx, |editor, cx| {
19929 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19930 });
19931 assert_eq!(
19932 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19933 "\n\n\n\n\n",
19934 "After folding the third buffer, its text should not be displayed"
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\n4444\n5555\n6666\n\n",
19943 "After unfolding the second buffer, its text should be displayed"
19944 );
19945
19946 multi_buffer_editor.update(cx, |editor, cx| {
19947 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
19948 });
19949 assert_eq!(
19950 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19951 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
19952 "After unfolding the first buffer, its text should be displayed"
19953 );
19954
19955 multi_buffer_editor.update(cx, |editor, cx| {
19956 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19957 });
19958 assert_eq!(
19959 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19960 full_text,
19961 "After unfolding all buffers, all original text should be displayed"
19962 );
19963}
19964
19965#[gpui::test]
19966async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
19967 init_test(cx, |_| {});
19968
19969 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19970
19971 let fs = FakeFs::new(cx.executor());
19972 fs.insert_tree(
19973 path!("/a"),
19974 json!({
19975 "main.rs": sample_text,
19976 }),
19977 )
19978 .await;
19979 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19980 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19981 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19982 let worktree = project.update(cx, |project, cx| {
19983 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19984 assert_eq!(worktrees.len(), 1);
19985 worktrees.pop().unwrap()
19986 });
19987 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19988
19989 let buffer_1 = project
19990 .update(cx, |project, cx| {
19991 project.open_buffer((worktree_id, "main.rs"), cx)
19992 })
19993 .await
19994 .unwrap();
19995
19996 let multi_buffer = cx.new(|cx| {
19997 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19998 multi_buffer.push_excerpts(
19999 buffer_1.clone(),
20000 [ExcerptRange::new(
20001 Point::new(0, 0)
20002 ..Point::new(
20003 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
20004 0,
20005 ),
20006 )],
20007 cx,
20008 );
20009 multi_buffer
20010 });
20011 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20012 Editor::new(
20013 EditorMode::full(),
20014 multi_buffer,
20015 Some(project.clone()),
20016 window,
20017 cx,
20018 )
20019 });
20020
20021 let selection_range = Point::new(1, 0)..Point::new(2, 0);
20022 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20023 enum TestHighlight {}
20024 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
20025 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
20026 editor.highlight_text::<TestHighlight>(
20027 vec![highlight_range.clone()],
20028 HighlightStyle::color(Hsla::green()),
20029 cx,
20030 );
20031 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20032 s.select_ranges(Some(highlight_range))
20033 });
20034 });
20035
20036 let full_text = format!("\n\n{sample_text}");
20037 assert_eq!(
20038 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20039 full_text,
20040 );
20041}
20042
20043#[gpui::test]
20044async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
20045 init_test(cx, |_| {});
20046 cx.update(|cx| {
20047 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
20048 "keymaps/default-linux.json",
20049 cx,
20050 )
20051 .unwrap();
20052 cx.bind_keys(default_key_bindings);
20053 });
20054
20055 let (editor, cx) = cx.add_window_view(|window, cx| {
20056 let multi_buffer = MultiBuffer::build_multi(
20057 [
20058 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
20059 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
20060 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
20061 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
20062 ],
20063 cx,
20064 );
20065 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
20066
20067 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
20068 // fold all but the second buffer, so that we test navigating between two
20069 // adjacent folded buffers, as well as folded buffers at the start and
20070 // end the multibuffer
20071 editor.fold_buffer(buffer_ids[0], cx);
20072 editor.fold_buffer(buffer_ids[2], cx);
20073 editor.fold_buffer(buffer_ids[3], cx);
20074
20075 editor
20076 });
20077 cx.simulate_resize(size(px(1000.), px(1000.)));
20078
20079 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
20080 cx.assert_excerpts_with_selections(indoc! {"
20081 [EXCERPT]
20082 ˇ[FOLDED]
20083 [EXCERPT]
20084 a1
20085 b1
20086 [EXCERPT]
20087 [FOLDED]
20088 [EXCERPT]
20089 [FOLDED]
20090 "
20091 });
20092 cx.simulate_keystroke("down");
20093 cx.assert_excerpts_with_selections(indoc! {"
20094 [EXCERPT]
20095 [FOLDED]
20096 [EXCERPT]
20097 ˇa1
20098 b1
20099 [EXCERPT]
20100 [FOLDED]
20101 [EXCERPT]
20102 [FOLDED]
20103 "
20104 });
20105 cx.simulate_keystroke("down");
20106 cx.assert_excerpts_with_selections(indoc! {"
20107 [EXCERPT]
20108 [FOLDED]
20109 [EXCERPT]
20110 a1
20111 ˇb1
20112 [EXCERPT]
20113 [FOLDED]
20114 [EXCERPT]
20115 [FOLDED]
20116 "
20117 });
20118 cx.simulate_keystroke("down");
20119 cx.assert_excerpts_with_selections(indoc! {"
20120 [EXCERPT]
20121 [FOLDED]
20122 [EXCERPT]
20123 a1
20124 b1
20125 ˇ[EXCERPT]
20126 [FOLDED]
20127 [EXCERPT]
20128 [FOLDED]
20129 "
20130 });
20131 cx.simulate_keystroke("down");
20132 cx.assert_excerpts_with_selections(indoc! {"
20133 [EXCERPT]
20134 [FOLDED]
20135 [EXCERPT]
20136 a1
20137 b1
20138 [EXCERPT]
20139 ˇ[FOLDED]
20140 [EXCERPT]
20141 [FOLDED]
20142 "
20143 });
20144 for _ in 0..5 {
20145 cx.simulate_keystroke("down");
20146 cx.assert_excerpts_with_selections(indoc! {"
20147 [EXCERPT]
20148 [FOLDED]
20149 [EXCERPT]
20150 a1
20151 b1
20152 [EXCERPT]
20153 [FOLDED]
20154 [EXCERPT]
20155 ˇ[FOLDED]
20156 "
20157 });
20158 }
20159
20160 cx.simulate_keystroke("up");
20161 cx.assert_excerpts_with_selections(indoc! {"
20162 [EXCERPT]
20163 [FOLDED]
20164 [EXCERPT]
20165 a1
20166 b1
20167 [EXCERPT]
20168 ˇ[FOLDED]
20169 [EXCERPT]
20170 [FOLDED]
20171 "
20172 });
20173 cx.simulate_keystroke("up");
20174 cx.assert_excerpts_with_selections(indoc! {"
20175 [EXCERPT]
20176 [FOLDED]
20177 [EXCERPT]
20178 a1
20179 b1
20180 ˇ[EXCERPT]
20181 [FOLDED]
20182 [EXCERPT]
20183 [FOLDED]
20184 "
20185 });
20186 cx.simulate_keystroke("up");
20187 cx.assert_excerpts_with_selections(indoc! {"
20188 [EXCERPT]
20189 [FOLDED]
20190 [EXCERPT]
20191 a1
20192 ˇb1
20193 [EXCERPT]
20194 [FOLDED]
20195 [EXCERPT]
20196 [FOLDED]
20197 "
20198 });
20199 cx.simulate_keystroke("up");
20200 cx.assert_excerpts_with_selections(indoc! {"
20201 [EXCERPT]
20202 [FOLDED]
20203 [EXCERPT]
20204 ˇa1
20205 b1
20206 [EXCERPT]
20207 [FOLDED]
20208 [EXCERPT]
20209 [FOLDED]
20210 "
20211 });
20212 for _ in 0..5 {
20213 cx.simulate_keystroke("up");
20214 cx.assert_excerpts_with_selections(indoc! {"
20215 [EXCERPT]
20216 ˇ[FOLDED]
20217 [EXCERPT]
20218 a1
20219 b1
20220 [EXCERPT]
20221 [FOLDED]
20222 [EXCERPT]
20223 [FOLDED]
20224 "
20225 });
20226 }
20227}
20228
20229#[gpui::test]
20230async fn test_inline_completion_text(cx: &mut TestAppContext) {
20231 init_test(cx, |_| {});
20232
20233 // Simple insertion
20234 assert_highlighted_edits(
20235 "Hello, world!",
20236 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
20237 true,
20238 cx,
20239 |highlighted_edits, cx| {
20240 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
20241 assert_eq!(highlighted_edits.highlights.len(), 1);
20242 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
20243 assert_eq!(
20244 highlighted_edits.highlights[0].1.background_color,
20245 Some(cx.theme().status().created_background)
20246 );
20247 },
20248 )
20249 .await;
20250
20251 // Replacement
20252 assert_highlighted_edits(
20253 "This is a test.",
20254 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
20255 false,
20256 cx,
20257 |highlighted_edits, cx| {
20258 assert_eq!(highlighted_edits.text, "That is a test.");
20259 assert_eq!(highlighted_edits.highlights.len(), 1);
20260 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
20261 assert_eq!(
20262 highlighted_edits.highlights[0].1.background_color,
20263 Some(cx.theme().status().created_background)
20264 );
20265 },
20266 )
20267 .await;
20268
20269 // Multiple edits
20270 assert_highlighted_edits(
20271 "Hello, world!",
20272 vec![
20273 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
20274 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
20275 ],
20276 false,
20277 cx,
20278 |highlighted_edits, cx| {
20279 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
20280 assert_eq!(highlighted_edits.highlights.len(), 2);
20281 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
20282 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
20283 assert_eq!(
20284 highlighted_edits.highlights[0].1.background_color,
20285 Some(cx.theme().status().created_background)
20286 );
20287 assert_eq!(
20288 highlighted_edits.highlights[1].1.background_color,
20289 Some(cx.theme().status().created_background)
20290 );
20291 },
20292 )
20293 .await;
20294
20295 // Multiple lines with edits
20296 assert_highlighted_edits(
20297 "First line\nSecond line\nThird line\nFourth line",
20298 vec![
20299 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
20300 (
20301 Point::new(2, 0)..Point::new(2, 10),
20302 "New third line".to_string(),
20303 ),
20304 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
20305 ],
20306 false,
20307 cx,
20308 |highlighted_edits, cx| {
20309 assert_eq!(
20310 highlighted_edits.text,
20311 "Second modified\nNew third line\nFourth updated line"
20312 );
20313 assert_eq!(highlighted_edits.highlights.len(), 3);
20314 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
20315 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
20316 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
20317 for highlight in &highlighted_edits.highlights {
20318 assert_eq!(
20319 highlight.1.background_color,
20320 Some(cx.theme().status().created_background)
20321 );
20322 }
20323 },
20324 )
20325 .await;
20326}
20327
20328#[gpui::test]
20329async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
20330 init_test(cx, |_| {});
20331
20332 // Deletion
20333 assert_highlighted_edits(
20334 "Hello, world!",
20335 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
20336 true,
20337 cx,
20338 |highlighted_edits, cx| {
20339 assert_eq!(highlighted_edits.text, "Hello, world!");
20340 assert_eq!(highlighted_edits.highlights.len(), 1);
20341 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
20342 assert_eq!(
20343 highlighted_edits.highlights[0].1.background_color,
20344 Some(cx.theme().status().deleted_background)
20345 );
20346 },
20347 )
20348 .await;
20349
20350 // Insertion
20351 assert_highlighted_edits(
20352 "Hello, world!",
20353 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
20354 true,
20355 cx,
20356 |highlighted_edits, cx| {
20357 assert_eq!(highlighted_edits.highlights.len(), 1);
20358 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
20359 assert_eq!(
20360 highlighted_edits.highlights[0].1.background_color,
20361 Some(cx.theme().status().created_background)
20362 );
20363 },
20364 )
20365 .await;
20366}
20367
20368async fn assert_highlighted_edits(
20369 text: &str,
20370 edits: Vec<(Range<Point>, String)>,
20371 include_deletions: bool,
20372 cx: &mut TestAppContext,
20373 assertion_fn: impl Fn(HighlightedText, &App),
20374) {
20375 let window = cx.add_window(|window, cx| {
20376 let buffer = MultiBuffer::build_simple(text, cx);
20377 Editor::new(EditorMode::full(), buffer, None, window, cx)
20378 });
20379 let cx = &mut VisualTestContext::from_window(*window, cx);
20380
20381 let (buffer, snapshot) = window
20382 .update(cx, |editor, _window, cx| {
20383 (
20384 editor.buffer().clone(),
20385 editor.buffer().read(cx).snapshot(cx),
20386 )
20387 })
20388 .unwrap();
20389
20390 let edits = edits
20391 .into_iter()
20392 .map(|(range, edit)| {
20393 (
20394 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
20395 edit,
20396 )
20397 })
20398 .collect::<Vec<_>>();
20399
20400 let text_anchor_edits = edits
20401 .clone()
20402 .into_iter()
20403 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
20404 .collect::<Vec<_>>();
20405
20406 let edit_preview = window
20407 .update(cx, |_, _window, cx| {
20408 buffer
20409 .read(cx)
20410 .as_singleton()
20411 .unwrap()
20412 .read(cx)
20413 .preview_edits(text_anchor_edits.into(), cx)
20414 })
20415 .unwrap()
20416 .await;
20417
20418 cx.update(|_window, cx| {
20419 let highlighted_edits = inline_completion_edit_text(
20420 &snapshot.as_singleton().unwrap().2,
20421 &edits,
20422 &edit_preview,
20423 include_deletions,
20424 cx,
20425 );
20426 assertion_fn(highlighted_edits, cx)
20427 });
20428}
20429
20430#[track_caller]
20431fn assert_breakpoint(
20432 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
20433 path: &Arc<Path>,
20434 expected: Vec<(u32, Breakpoint)>,
20435) {
20436 if expected.len() == 0usize {
20437 assert!(!breakpoints.contains_key(path), "{}", path.display());
20438 } else {
20439 let mut breakpoint = breakpoints
20440 .get(path)
20441 .unwrap()
20442 .into_iter()
20443 .map(|breakpoint| {
20444 (
20445 breakpoint.row,
20446 Breakpoint {
20447 message: breakpoint.message.clone(),
20448 state: breakpoint.state,
20449 condition: breakpoint.condition.clone(),
20450 hit_condition: breakpoint.hit_condition.clone(),
20451 },
20452 )
20453 })
20454 .collect::<Vec<_>>();
20455
20456 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
20457
20458 assert_eq!(expected, breakpoint);
20459 }
20460}
20461
20462fn add_log_breakpoint_at_cursor(
20463 editor: &mut Editor,
20464 log_message: &str,
20465 window: &mut Window,
20466 cx: &mut Context<Editor>,
20467) {
20468 let (anchor, bp) = editor
20469 .breakpoints_at_cursors(window, cx)
20470 .first()
20471 .and_then(|(anchor, bp)| {
20472 if let Some(bp) = bp {
20473 Some((*anchor, bp.clone()))
20474 } else {
20475 None
20476 }
20477 })
20478 .unwrap_or_else(|| {
20479 let cursor_position: Point = editor.selections.newest(cx).head();
20480
20481 let breakpoint_position = editor
20482 .snapshot(window, cx)
20483 .display_snapshot
20484 .buffer_snapshot
20485 .anchor_before(Point::new(cursor_position.row, 0));
20486
20487 (breakpoint_position, Breakpoint::new_log(&log_message))
20488 });
20489
20490 editor.edit_breakpoint_at_anchor(
20491 anchor,
20492 bp,
20493 BreakpointEditAction::EditLogMessage(log_message.into()),
20494 cx,
20495 );
20496}
20497
20498#[gpui::test]
20499async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
20500 init_test(cx, |_| {});
20501
20502 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20503 let fs = FakeFs::new(cx.executor());
20504 fs.insert_tree(
20505 path!("/a"),
20506 json!({
20507 "main.rs": sample_text,
20508 }),
20509 )
20510 .await;
20511 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20512 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20513 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20514
20515 let fs = FakeFs::new(cx.executor());
20516 fs.insert_tree(
20517 path!("/a"),
20518 json!({
20519 "main.rs": sample_text,
20520 }),
20521 )
20522 .await;
20523 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20524 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20525 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20526 let worktree_id = workspace
20527 .update(cx, |workspace, _window, cx| {
20528 workspace.project().update(cx, |project, cx| {
20529 project.worktrees(cx).next().unwrap().read(cx).id()
20530 })
20531 })
20532 .unwrap();
20533
20534 let buffer = project
20535 .update(cx, |project, cx| {
20536 project.open_buffer((worktree_id, "main.rs"), cx)
20537 })
20538 .await
20539 .unwrap();
20540
20541 let (editor, cx) = cx.add_window_view(|window, cx| {
20542 Editor::new(
20543 EditorMode::full(),
20544 MultiBuffer::build_from_buffer(buffer, cx),
20545 Some(project.clone()),
20546 window,
20547 cx,
20548 )
20549 });
20550
20551 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20552 let abs_path = project.read_with(cx, |project, cx| {
20553 project
20554 .absolute_path(&project_path, cx)
20555 .map(|path_buf| Arc::from(path_buf.to_owned()))
20556 .unwrap()
20557 });
20558
20559 // assert we can add breakpoint on the first line
20560 editor.update_in(cx, |editor, window, cx| {
20561 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20562 editor.move_to_end(&MoveToEnd, window, cx);
20563 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20564 });
20565
20566 let breakpoints = editor.update(cx, |editor, cx| {
20567 editor
20568 .breakpoint_store()
20569 .as_ref()
20570 .unwrap()
20571 .read(cx)
20572 .all_source_breakpoints(cx)
20573 .clone()
20574 });
20575
20576 assert_eq!(1, breakpoints.len());
20577 assert_breakpoint(
20578 &breakpoints,
20579 &abs_path,
20580 vec![
20581 (0, Breakpoint::new_standard()),
20582 (3, Breakpoint::new_standard()),
20583 ],
20584 );
20585
20586 editor.update_in(cx, |editor, window, cx| {
20587 editor.move_to_beginning(&MoveToBeginning, window, cx);
20588 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20589 });
20590
20591 let breakpoints = editor.update(cx, |editor, cx| {
20592 editor
20593 .breakpoint_store()
20594 .as_ref()
20595 .unwrap()
20596 .read(cx)
20597 .all_source_breakpoints(cx)
20598 .clone()
20599 });
20600
20601 assert_eq!(1, breakpoints.len());
20602 assert_breakpoint(
20603 &breakpoints,
20604 &abs_path,
20605 vec![(3, Breakpoint::new_standard())],
20606 );
20607
20608 editor.update_in(cx, |editor, window, cx| {
20609 editor.move_to_end(&MoveToEnd, window, cx);
20610 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20611 });
20612
20613 let breakpoints = editor.update(cx, |editor, cx| {
20614 editor
20615 .breakpoint_store()
20616 .as_ref()
20617 .unwrap()
20618 .read(cx)
20619 .all_source_breakpoints(cx)
20620 .clone()
20621 });
20622
20623 assert_eq!(0, breakpoints.len());
20624 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20625}
20626
20627#[gpui::test]
20628async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
20629 init_test(cx, |_| {});
20630
20631 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20632
20633 let fs = FakeFs::new(cx.executor());
20634 fs.insert_tree(
20635 path!("/a"),
20636 json!({
20637 "main.rs": sample_text,
20638 }),
20639 )
20640 .await;
20641 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20642 let (workspace, cx) =
20643 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20644
20645 let worktree_id = workspace.update(cx, |workspace, cx| {
20646 workspace.project().update(cx, |project, cx| {
20647 project.worktrees(cx).next().unwrap().read(cx).id()
20648 })
20649 });
20650
20651 let buffer = project
20652 .update(cx, |project, cx| {
20653 project.open_buffer((worktree_id, "main.rs"), cx)
20654 })
20655 .await
20656 .unwrap();
20657
20658 let (editor, cx) = cx.add_window_view(|window, cx| {
20659 Editor::new(
20660 EditorMode::full(),
20661 MultiBuffer::build_from_buffer(buffer, cx),
20662 Some(project.clone()),
20663 window,
20664 cx,
20665 )
20666 });
20667
20668 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20669 let abs_path = project.read_with(cx, |project, cx| {
20670 project
20671 .absolute_path(&project_path, cx)
20672 .map(|path_buf| Arc::from(path_buf.to_owned()))
20673 .unwrap()
20674 });
20675
20676 editor.update_in(cx, |editor, window, cx| {
20677 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20678 });
20679
20680 let breakpoints = editor.update(cx, |editor, cx| {
20681 editor
20682 .breakpoint_store()
20683 .as_ref()
20684 .unwrap()
20685 .read(cx)
20686 .all_source_breakpoints(cx)
20687 .clone()
20688 });
20689
20690 assert_breakpoint(
20691 &breakpoints,
20692 &abs_path,
20693 vec![(0, Breakpoint::new_log("hello world"))],
20694 );
20695
20696 // Removing a log message from a log breakpoint should remove it
20697 editor.update_in(cx, |editor, window, cx| {
20698 add_log_breakpoint_at_cursor(editor, "", window, cx);
20699 });
20700
20701 let breakpoints = editor.update(cx, |editor, cx| {
20702 editor
20703 .breakpoint_store()
20704 .as_ref()
20705 .unwrap()
20706 .read(cx)
20707 .all_source_breakpoints(cx)
20708 .clone()
20709 });
20710
20711 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20712
20713 editor.update_in(cx, |editor, window, cx| {
20714 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20715 editor.move_to_end(&MoveToEnd, window, cx);
20716 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20717 // Not adding a log message to a standard breakpoint shouldn't remove it
20718 add_log_breakpoint_at_cursor(editor, "", window, cx);
20719 });
20720
20721 let breakpoints = editor.update(cx, |editor, cx| {
20722 editor
20723 .breakpoint_store()
20724 .as_ref()
20725 .unwrap()
20726 .read(cx)
20727 .all_source_breakpoints(cx)
20728 .clone()
20729 });
20730
20731 assert_breakpoint(
20732 &breakpoints,
20733 &abs_path,
20734 vec![
20735 (0, Breakpoint::new_standard()),
20736 (3, Breakpoint::new_standard()),
20737 ],
20738 );
20739
20740 editor.update_in(cx, |editor, window, cx| {
20741 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20742 });
20743
20744 let breakpoints = editor.update(cx, |editor, cx| {
20745 editor
20746 .breakpoint_store()
20747 .as_ref()
20748 .unwrap()
20749 .read(cx)
20750 .all_source_breakpoints(cx)
20751 .clone()
20752 });
20753
20754 assert_breakpoint(
20755 &breakpoints,
20756 &abs_path,
20757 vec![
20758 (0, Breakpoint::new_standard()),
20759 (3, Breakpoint::new_log("hello world")),
20760 ],
20761 );
20762
20763 editor.update_in(cx, |editor, window, cx| {
20764 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
20765 });
20766
20767 let breakpoints = editor.update(cx, |editor, cx| {
20768 editor
20769 .breakpoint_store()
20770 .as_ref()
20771 .unwrap()
20772 .read(cx)
20773 .all_source_breakpoints(cx)
20774 .clone()
20775 });
20776
20777 assert_breakpoint(
20778 &breakpoints,
20779 &abs_path,
20780 vec![
20781 (0, Breakpoint::new_standard()),
20782 (3, Breakpoint::new_log("hello Earth!!")),
20783 ],
20784 );
20785}
20786
20787/// This also tests that Editor::breakpoint_at_cursor_head is working properly
20788/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
20789/// or when breakpoints were placed out of order. This tests for a regression too
20790#[gpui::test]
20791async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
20792 init_test(cx, |_| {});
20793
20794 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20795 let fs = FakeFs::new(cx.executor());
20796 fs.insert_tree(
20797 path!("/a"),
20798 json!({
20799 "main.rs": sample_text,
20800 }),
20801 )
20802 .await;
20803 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20804 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20805 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20806
20807 let fs = FakeFs::new(cx.executor());
20808 fs.insert_tree(
20809 path!("/a"),
20810 json!({
20811 "main.rs": sample_text,
20812 }),
20813 )
20814 .await;
20815 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20816 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20817 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20818 let worktree_id = workspace
20819 .update(cx, |workspace, _window, cx| {
20820 workspace.project().update(cx, |project, cx| {
20821 project.worktrees(cx).next().unwrap().read(cx).id()
20822 })
20823 })
20824 .unwrap();
20825
20826 let buffer = project
20827 .update(cx, |project, cx| {
20828 project.open_buffer((worktree_id, "main.rs"), cx)
20829 })
20830 .await
20831 .unwrap();
20832
20833 let (editor, cx) = cx.add_window_view(|window, cx| {
20834 Editor::new(
20835 EditorMode::full(),
20836 MultiBuffer::build_from_buffer(buffer, cx),
20837 Some(project.clone()),
20838 window,
20839 cx,
20840 )
20841 });
20842
20843 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20844 let abs_path = project.read_with(cx, |project, cx| {
20845 project
20846 .absolute_path(&project_path, cx)
20847 .map(|path_buf| Arc::from(path_buf.to_owned()))
20848 .unwrap()
20849 });
20850
20851 // assert we can add breakpoint on the first line
20852 editor.update_in(cx, |editor, window, cx| {
20853 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20854 editor.move_to_end(&MoveToEnd, window, cx);
20855 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20856 editor.move_up(&MoveUp, window, cx);
20857 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20858 });
20859
20860 let breakpoints = editor.update(cx, |editor, cx| {
20861 editor
20862 .breakpoint_store()
20863 .as_ref()
20864 .unwrap()
20865 .read(cx)
20866 .all_source_breakpoints(cx)
20867 .clone()
20868 });
20869
20870 assert_eq!(1, breakpoints.len());
20871 assert_breakpoint(
20872 &breakpoints,
20873 &abs_path,
20874 vec![
20875 (0, Breakpoint::new_standard()),
20876 (2, Breakpoint::new_standard()),
20877 (3, Breakpoint::new_standard()),
20878 ],
20879 );
20880
20881 editor.update_in(cx, |editor, window, cx| {
20882 editor.move_to_beginning(&MoveToBeginning, window, cx);
20883 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20884 editor.move_to_end(&MoveToEnd, window, cx);
20885 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20886 // Disabling a breakpoint that doesn't exist should do nothing
20887 editor.move_up(&MoveUp, window, cx);
20888 editor.move_up(&MoveUp, window, cx);
20889 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20890 });
20891
20892 let breakpoints = editor.update(cx, |editor, cx| {
20893 editor
20894 .breakpoint_store()
20895 .as_ref()
20896 .unwrap()
20897 .read(cx)
20898 .all_source_breakpoints(cx)
20899 .clone()
20900 });
20901
20902 let disable_breakpoint = {
20903 let mut bp = Breakpoint::new_standard();
20904 bp.state = BreakpointState::Disabled;
20905 bp
20906 };
20907
20908 assert_eq!(1, breakpoints.len());
20909 assert_breakpoint(
20910 &breakpoints,
20911 &abs_path,
20912 vec![
20913 (0, disable_breakpoint.clone()),
20914 (2, Breakpoint::new_standard()),
20915 (3, disable_breakpoint.clone()),
20916 ],
20917 );
20918
20919 editor.update_in(cx, |editor, window, cx| {
20920 editor.move_to_beginning(&MoveToBeginning, window, cx);
20921 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20922 editor.move_to_end(&MoveToEnd, window, cx);
20923 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20924 editor.move_up(&MoveUp, window, cx);
20925 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20926 });
20927
20928 let breakpoints = editor.update(cx, |editor, cx| {
20929 editor
20930 .breakpoint_store()
20931 .as_ref()
20932 .unwrap()
20933 .read(cx)
20934 .all_source_breakpoints(cx)
20935 .clone()
20936 });
20937
20938 assert_eq!(1, breakpoints.len());
20939 assert_breakpoint(
20940 &breakpoints,
20941 &abs_path,
20942 vec![
20943 (0, Breakpoint::new_standard()),
20944 (2, disable_breakpoint),
20945 (3, Breakpoint::new_standard()),
20946 ],
20947 );
20948}
20949
20950#[gpui::test]
20951async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
20952 init_test(cx, |_| {});
20953 let capabilities = lsp::ServerCapabilities {
20954 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
20955 prepare_provider: Some(true),
20956 work_done_progress_options: Default::default(),
20957 })),
20958 ..Default::default()
20959 };
20960 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
20961
20962 cx.set_state(indoc! {"
20963 struct Fˇoo {}
20964 "});
20965
20966 cx.update_editor(|editor, _, cx| {
20967 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
20968 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
20969 editor.highlight_background::<DocumentHighlightRead>(
20970 &[highlight_range],
20971 |theme| theme.colors().editor_document_highlight_read_background,
20972 cx,
20973 );
20974 });
20975
20976 let mut prepare_rename_handler = cx
20977 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
20978 move |_, _, _| async move {
20979 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
20980 start: lsp::Position {
20981 line: 0,
20982 character: 7,
20983 },
20984 end: lsp::Position {
20985 line: 0,
20986 character: 10,
20987 },
20988 })))
20989 },
20990 );
20991 let prepare_rename_task = cx
20992 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
20993 .expect("Prepare rename was not started");
20994 prepare_rename_handler.next().await.unwrap();
20995 prepare_rename_task.await.expect("Prepare rename failed");
20996
20997 let mut rename_handler =
20998 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
20999 let edit = lsp::TextEdit {
21000 range: lsp::Range {
21001 start: lsp::Position {
21002 line: 0,
21003 character: 7,
21004 },
21005 end: lsp::Position {
21006 line: 0,
21007 character: 10,
21008 },
21009 },
21010 new_text: "FooRenamed".to_string(),
21011 };
21012 Ok(Some(lsp::WorkspaceEdit::new(
21013 // Specify the same edit twice
21014 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
21015 )))
21016 });
21017 let rename_task = cx
21018 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21019 .expect("Confirm rename was not started");
21020 rename_handler.next().await.unwrap();
21021 rename_task.await.expect("Confirm rename failed");
21022 cx.run_until_parked();
21023
21024 // Despite two edits, only one is actually applied as those are identical
21025 cx.assert_editor_state(indoc! {"
21026 struct FooRenamedˇ {}
21027 "});
21028}
21029
21030#[gpui::test]
21031async fn test_rename_without_prepare(cx: &mut TestAppContext) {
21032 init_test(cx, |_| {});
21033 // These capabilities indicate that the server does not support prepare rename.
21034 let capabilities = lsp::ServerCapabilities {
21035 rename_provider: Some(lsp::OneOf::Left(true)),
21036 ..Default::default()
21037 };
21038 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21039
21040 cx.set_state(indoc! {"
21041 struct Fˇoo {}
21042 "});
21043
21044 cx.update_editor(|editor, _window, cx| {
21045 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21046 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21047 editor.highlight_background::<DocumentHighlightRead>(
21048 &[highlight_range],
21049 |theme| theme.colors().editor_document_highlight_read_background,
21050 cx,
21051 );
21052 });
21053
21054 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21055 .expect("Prepare rename was not started")
21056 .await
21057 .expect("Prepare rename failed");
21058
21059 let mut rename_handler =
21060 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21061 let edit = lsp::TextEdit {
21062 range: lsp::Range {
21063 start: lsp::Position {
21064 line: 0,
21065 character: 7,
21066 },
21067 end: lsp::Position {
21068 line: 0,
21069 character: 10,
21070 },
21071 },
21072 new_text: "FooRenamed".to_string(),
21073 };
21074 Ok(Some(lsp::WorkspaceEdit::new(
21075 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
21076 )))
21077 });
21078 let rename_task = cx
21079 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21080 .expect("Confirm rename was not started");
21081 rename_handler.next().await.unwrap();
21082 rename_task.await.expect("Confirm rename failed");
21083 cx.run_until_parked();
21084
21085 // Correct range is renamed, as `surrounding_word` is used to find it.
21086 cx.assert_editor_state(indoc! {"
21087 struct FooRenamedˇ {}
21088 "});
21089}
21090
21091#[gpui::test]
21092async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
21093 init_test(cx, |_| {});
21094 let mut cx = EditorTestContext::new(cx).await;
21095
21096 let language = Arc::new(
21097 Language::new(
21098 LanguageConfig::default(),
21099 Some(tree_sitter_html::LANGUAGE.into()),
21100 )
21101 .with_brackets_query(
21102 r#"
21103 ("<" @open "/>" @close)
21104 ("</" @open ">" @close)
21105 ("<" @open ">" @close)
21106 ("\"" @open "\"" @close)
21107 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
21108 "#,
21109 )
21110 .unwrap(),
21111 );
21112 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21113
21114 cx.set_state(indoc! {"
21115 <span>ˇ</span>
21116 "});
21117 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21118 cx.assert_editor_state(indoc! {"
21119 <span>
21120 ˇ
21121 </span>
21122 "});
21123
21124 cx.set_state(indoc! {"
21125 <span><span></span>ˇ</span>
21126 "});
21127 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21128 cx.assert_editor_state(indoc! {"
21129 <span><span></span>
21130 ˇ</span>
21131 "});
21132
21133 cx.set_state(indoc! {"
21134 <span>ˇ
21135 </span>
21136 "});
21137 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21138 cx.assert_editor_state(indoc! {"
21139 <span>
21140 ˇ
21141 </span>
21142 "});
21143}
21144
21145#[gpui::test(iterations = 10)]
21146async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
21147 init_test(cx, |_| {});
21148
21149 let fs = FakeFs::new(cx.executor());
21150 fs.insert_tree(
21151 path!("/dir"),
21152 json!({
21153 "a.ts": "a",
21154 }),
21155 )
21156 .await;
21157
21158 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
21159 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21160 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21161
21162 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21163 language_registry.add(Arc::new(Language::new(
21164 LanguageConfig {
21165 name: "TypeScript".into(),
21166 matcher: LanguageMatcher {
21167 path_suffixes: vec!["ts".to_string()],
21168 ..Default::default()
21169 },
21170 ..Default::default()
21171 },
21172 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
21173 )));
21174 let mut fake_language_servers = language_registry.register_fake_lsp(
21175 "TypeScript",
21176 FakeLspAdapter {
21177 capabilities: lsp::ServerCapabilities {
21178 code_lens_provider: Some(lsp::CodeLensOptions {
21179 resolve_provider: Some(true),
21180 }),
21181 execute_command_provider: Some(lsp::ExecuteCommandOptions {
21182 commands: vec!["_the/command".to_string()],
21183 ..lsp::ExecuteCommandOptions::default()
21184 }),
21185 ..lsp::ServerCapabilities::default()
21186 },
21187 ..FakeLspAdapter::default()
21188 },
21189 );
21190
21191 let (buffer, _handle) = project
21192 .update(cx, |p, cx| {
21193 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
21194 })
21195 .await
21196 .unwrap();
21197 cx.executor().run_until_parked();
21198
21199 let fake_server = fake_language_servers.next().await.unwrap();
21200
21201 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
21202 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
21203 drop(buffer_snapshot);
21204 let actions = cx
21205 .update_window(*workspace, |_, window, cx| {
21206 project.code_actions(&buffer, anchor..anchor, window, cx)
21207 })
21208 .unwrap();
21209
21210 fake_server
21211 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21212 Ok(Some(vec![
21213 lsp::CodeLens {
21214 range: lsp::Range::default(),
21215 command: Some(lsp::Command {
21216 title: "Code lens command".to_owned(),
21217 command: "_the/command".to_owned(),
21218 arguments: None,
21219 }),
21220 data: None,
21221 },
21222 lsp::CodeLens {
21223 range: lsp::Range::default(),
21224 command: Some(lsp::Command {
21225 title: "Command not in capabilities".to_owned(),
21226 command: "not in capabilities".to_owned(),
21227 arguments: None,
21228 }),
21229 data: None,
21230 },
21231 lsp::CodeLens {
21232 range: lsp::Range {
21233 start: lsp::Position {
21234 line: 1,
21235 character: 1,
21236 },
21237 end: lsp::Position {
21238 line: 1,
21239 character: 1,
21240 },
21241 },
21242 command: Some(lsp::Command {
21243 title: "Command not in range".to_owned(),
21244 command: "_the/command".to_owned(),
21245 arguments: None,
21246 }),
21247 data: None,
21248 },
21249 ]))
21250 })
21251 .next()
21252 .await;
21253
21254 let actions = actions.await.unwrap();
21255 assert_eq!(
21256 actions.len(),
21257 1,
21258 "Should have only one valid action for the 0..0 range"
21259 );
21260 let action = actions[0].clone();
21261 let apply = project.update(cx, |project, cx| {
21262 project.apply_code_action(buffer.clone(), action, true, cx)
21263 });
21264
21265 // Resolving the code action does not populate its edits. In absence of
21266 // edits, we must execute the given command.
21267 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
21268 |mut lens, _| async move {
21269 let lens_command = lens.command.as_mut().expect("should have a command");
21270 assert_eq!(lens_command.title, "Code lens command");
21271 lens_command.arguments = Some(vec![json!("the-argument")]);
21272 Ok(lens)
21273 },
21274 );
21275
21276 // While executing the command, the language server sends the editor
21277 // a `workspaceEdit` request.
21278 fake_server
21279 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
21280 let fake = fake_server.clone();
21281 move |params, _| {
21282 assert_eq!(params.command, "_the/command");
21283 let fake = fake.clone();
21284 async move {
21285 fake.server
21286 .request::<lsp::request::ApplyWorkspaceEdit>(
21287 lsp::ApplyWorkspaceEditParams {
21288 label: None,
21289 edit: lsp::WorkspaceEdit {
21290 changes: Some(
21291 [(
21292 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
21293 vec![lsp::TextEdit {
21294 range: lsp::Range::new(
21295 lsp::Position::new(0, 0),
21296 lsp::Position::new(0, 0),
21297 ),
21298 new_text: "X".into(),
21299 }],
21300 )]
21301 .into_iter()
21302 .collect(),
21303 ),
21304 ..Default::default()
21305 },
21306 },
21307 )
21308 .await
21309 .into_response()
21310 .unwrap();
21311 Ok(Some(json!(null)))
21312 }
21313 }
21314 })
21315 .next()
21316 .await;
21317
21318 // Applying the code lens command returns a project transaction containing the edits
21319 // sent by the language server in its `workspaceEdit` request.
21320 let transaction = apply.await.unwrap();
21321 assert!(transaction.0.contains_key(&buffer));
21322 buffer.update(cx, |buffer, cx| {
21323 assert_eq!(buffer.text(), "Xa");
21324 buffer.undo(cx);
21325 assert_eq!(buffer.text(), "a");
21326 });
21327}
21328
21329#[gpui::test]
21330async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
21331 init_test(cx, |_| {});
21332
21333 let fs = FakeFs::new(cx.executor());
21334 let main_text = r#"fn main() {
21335println!("1");
21336println!("2");
21337println!("3");
21338println!("4");
21339println!("5");
21340}"#;
21341 let lib_text = "mod foo {}";
21342 fs.insert_tree(
21343 path!("/a"),
21344 json!({
21345 "lib.rs": lib_text,
21346 "main.rs": main_text,
21347 }),
21348 )
21349 .await;
21350
21351 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21352 let (workspace, cx) =
21353 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21354 let worktree_id = workspace.update(cx, |workspace, cx| {
21355 workspace.project().update(cx, |project, cx| {
21356 project.worktrees(cx).next().unwrap().read(cx).id()
21357 })
21358 });
21359
21360 let expected_ranges = vec![
21361 Point::new(0, 0)..Point::new(0, 0),
21362 Point::new(1, 0)..Point::new(1, 1),
21363 Point::new(2, 0)..Point::new(2, 2),
21364 Point::new(3, 0)..Point::new(3, 3),
21365 ];
21366
21367 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21368 let editor_1 = workspace
21369 .update_in(cx, |workspace, window, cx| {
21370 workspace.open_path(
21371 (worktree_id, "main.rs"),
21372 Some(pane_1.downgrade()),
21373 true,
21374 window,
21375 cx,
21376 )
21377 })
21378 .unwrap()
21379 .await
21380 .downcast::<Editor>()
21381 .unwrap();
21382 pane_1.update(cx, |pane, cx| {
21383 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21384 open_editor.update(cx, |editor, cx| {
21385 assert_eq!(
21386 editor.display_text(cx),
21387 main_text,
21388 "Original main.rs text on initial open",
21389 );
21390 assert_eq!(
21391 editor
21392 .selections
21393 .all::<Point>(cx)
21394 .into_iter()
21395 .map(|s| s.range())
21396 .collect::<Vec<_>>(),
21397 vec![Point::zero()..Point::zero()],
21398 "Default selections on initial open",
21399 );
21400 })
21401 });
21402 editor_1.update_in(cx, |editor, window, cx| {
21403 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21404 s.select_ranges(expected_ranges.clone());
21405 });
21406 });
21407
21408 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
21409 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
21410 });
21411 let editor_2 = workspace
21412 .update_in(cx, |workspace, window, cx| {
21413 workspace.open_path(
21414 (worktree_id, "main.rs"),
21415 Some(pane_2.downgrade()),
21416 true,
21417 window,
21418 cx,
21419 )
21420 })
21421 .unwrap()
21422 .await
21423 .downcast::<Editor>()
21424 .unwrap();
21425 pane_2.update(cx, |pane, cx| {
21426 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21427 open_editor.update(cx, |editor, cx| {
21428 assert_eq!(
21429 editor.display_text(cx),
21430 main_text,
21431 "Original main.rs text on initial open in another panel",
21432 );
21433 assert_eq!(
21434 editor
21435 .selections
21436 .all::<Point>(cx)
21437 .into_iter()
21438 .map(|s| s.range())
21439 .collect::<Vec<_>>(),
21440 vec![Point::zero()..Point::zero()],
21441 "Default selections on initial open in another panel",
21442 );
21443 })
21444 });
21445
21446 editor_2.update_in(cx, |editor, window, cx| {
21447 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
21448 });
21449
21450 let _other_editor_1 = workspace
21451 .update_in(cx, |workspace, window, cx| {
21452 workspace.open_path(
21453 (worktree_id, "lib.rs"),
21454 Some(pane_1.downgrade()),
21455 true,
21456 window,
21457 cx,
21458 )
21459 })
21460 .unwrap()
21461 .await
21462 .downcast::<Editor>()
21463 .unwrap();
21464 pane_1
21465 .update_in(cx, |pane, window, cx| {
21466 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
21467 })
21468 .await
21469 .unwrap();
21470 drop(editor_1);
21471 pane_1.update(cx, |pane, cx| {
21472 pane.active_item()
21473 .unwrap()
21474 .downcast::<Editor>()
21475 .unwrap()
21476 .update(cx, |editor, cx| {
21477 assert_eq!(
21478 editor.display_text(cx),
21479 lib_text,
21480 "Other file should be open and active",
21481 );
21482 });
21483 assert_eq!(pane.items().count(), 1, "No other editors should be open");
21484 });
21485
21486 let _other_editor_2 = workspace
21487 .update_in(cx, |workspace, window, cx| {
21488 workspace.open_path(
21489 (worktree_id, "lib.rs"),
21490 Some(pane_2.downgrade()),
21491 true,
21492 window,
21493 cx,
21494 )
21495 })
21496 .unwrap()
21497 .await
21498 .downcast::<Editor>()
21499 .unwrap();
21500 pane_2
21501 .update_in(cx, |pane, window, cx| {
21502 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
21503 })
21504 .await
21505 .unwrap();
21506 drop(editor_2);
21507 pane_2.update(cx, |pane, cx| {
21508 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21509 open_editor.update(cx, |editor, cx| {
21510 assert_eq!(
21511 editor.display_text(cx),
21512 lib_text,
21513 "Other file should be open and active in another panel too",
21514 );
21515 });
21516 assert_eq!(
21517 pane.items().count(),
21518 1,
21519 "No other editors should be open in another pane",
21520 );
21521 });
21522
21523 let _editor_1_reopened = workspace
21524 .update_in(cx, |workspace, window, cx| {
21525 workspace.open_path(
21526 (worktree_id, "main.rs"),
21527 Some(pane_1.downgrade()),
21528 true,
21529 window,
21530 cx,
21531 )
21532 })
21533 .unwrap()
21534 .await
21535 .downcast::<Editor>()
21536 .unwrap();
21537 let _editor_2_reopened = workspace
21538 .update_in(cx, |workspace, window, cx| {
21539 workspace.open_path(
21540 (worktree_id, "main.rs"),
21541 Some(pane_2.downgrade()),
21542 true,
21543 window,
21544 cx,
21545 )
21546 })
21547 .unwrap()
21548 .await
21549 .downcast::<Editor>()
21550 .unwrap();
21551 pane_1.update(cx, |pane, cx| {
21552 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21553 open_editor.update(cx, |editor, cx| {
21554 assert_eq!(
21555 editor.display_text(cx),
21556 main_text,
21557 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
21558 );
21559 assert_eq!(
21560 editor
21561 .selections
21562 .all::<Point>(cx)
21563 .into_iter()
21564 .map(|s| s.range())
21565 .collect::<Vec<_>>(),
21566 expected_ranges,
21567 "Previous editor in the 1st panel had selections and should get them restored on reopen",
21568 );
21569 })
21570 });
21571 pane_2.update(cx, |pane, cx| {
21572 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21573 open_editor.update(cx, |editor, cx| {
21574 assert_eq!(
21575 editor.display_text(cx),
21576 r#"fn main() {
21577⋯rintln!("1");
21578⋯intln!("2");
21579⋯ntln!("3");
21580println!("4");
21581println!("5");
21582}"#,
21583 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
21584 );
21585 assert_eq!(
21586 editor
21587 .selections
21588 .all::<Point>(cx)
21589 .into_iter()
21590 .map(|s| s.range())
21591 .collect::<Vec<_>>(),
21592 vec![Point::zero()..Point::zero()],
21593 "Previous editor in the 2nd pane had no selections changed hence should restore none",
21594 );
21595 })
21596 });
21597}
21598
21599#[gpui::test]
21600async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
21601 init_test(cx, |_| {});
21602
21603 let fs = FakeFs::new(cx.executor());
21604 let main_text = r#"fn main() {
21605println!("1");
21606println!("2");
21607println!("3");
21608println!("4");
21609println!("5");
21610}"#;
21611 let lib_text = "mod foo {}";
21612 fs.insert_tree(
21613 path!("/a"),
21614 json!({
21615 "lib.rs": lib_text,
21616 "main.rs": main_text,
21617 }),
21618 )
21619 .await;
21620
21621 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21622 let (workspace, cx) =
21623 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21624 let worktree_id = workspace.update(cx, |workspace, cx| {
21625 workspace.project().update(cx, |project, cx| {
21626 project.worktrees(cx).next().unwrap().read(cx).id()
21627 })
21628 });
21629
21630 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21631 let editor = workspace
21632 .update_in(cx, |workspace, window, cx| {
21633 workspace.open_path(
21634 (worktree_id, "main.rs"),
21635 Some(pane.downgrade()),
21636 true,
21637 window,
21638 cx,
21639 )
21640 })
21641 .unwrap()
21642 .await
21643 .downcast::<Editor>()
21644 .unwrap();
21645 pane.update(cx, |pane, cx| {
21646 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21647 open_editor.update(cx, |editor, cx| {
21648 assert_eq!(
21649 editor.display_text(cx),
21650 main_text,
21651 "Original main.rs text on initial open",
21652 );
21653 })
21654 });
21655 editor.update_in(cx, |editor, window, cx| {
21656 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
21657 });
21658
21659 cx.update_global(|store: &mut SettingsStore, cx| {
21660 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21661 s.restore_on_file_reopen = Some(false);
21662 });
21663 });
21664 editor.update_in(cx, |editor, window, cx| {
21665 editor.fold_ranges(
21666 vec![
21667 Point::new(1, 0)..Point::new(1, 1),
21668 Point::new(2, 0)..Point::new(2, 2),
21669 Point::new(3, 0)..Point::new(3, 3),
21670 ],
21671 false,
21672 window,
21673 cx,
21674 );
21675 });
21676 pane.update_in(cx, |pane, window, cx| {
21677 pane.close_all_items(&CloseAllItems::default(), window, cx)
21678 })
21679 .await
21680 .unwrap();
21681 pane.update(cx, |pane, _| {
21682 assert!(pane.active_item().is_none());
21683 });
21684 cx.update_global(|store: &mut SettingsStore, cx| {
21685 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21686 s.restore_on_file_reopen = Some(true);
21687 });
21688 });
21689
21690 let _editor_reopened = workspace
21691 .update_in(cx, |workspace, window, cx| {
21692 workspace.open_path(
21693 (worktree_id, "main.rs"),
21694 Some(pane.downgrade()),
21695 true,
21696 window,
21697 cx,
21698 )
21699 })
21700 .unwrap()
21701 .await
21702 .downcast::<Editor>()
21703 .unwrap();
21704 pane.update(cx, |pane, cx| {
21705 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21706 open_editor.update(cx, |editor, cx| {
21707 assert_eq!(
21708 editor.display_text(cx),
21709 main_text,
21710 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
21711 );
21712 })
21713 });
21714}
21715
21716#[gpui::test]
21717async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
21718 struct EmptyModalView {
21719 focus_handle: gpui::FocusHandle,
21720 }
21721 impl EventEmitter<DismissEvent> for EmptyModalView {}
21722 impl Render for EmptyModalView {
21723 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
21724 div()
21725 }
21726 }
21727 impl Focusable for EmptyModalView {
21728 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
21729 self.focus_handle.clone()
21730 }
21731 }
21732 impl workspace::ModalView for EmptyModalView {}
21733 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
21734 EmptyModalView {
21735 focus_handle: cx.focus_handle(),
21736 }
21737 }
21738
21739 init_test(cx, |_| {});
21740
21741 let fs = FakeFs::new(cx.executor());
21742 let project = Project::test(fs, [], cx).await;
21743 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21744 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
21745 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21746 let editor = cx.new_window_entity(|window, cx| {
21747 Editor::new(
21748 EditorMode::full(),
21749 buffer,
21750 Some(project.clone()),
21751 window,
21752 cx,
21753 )
21754 });
21755 workspace
21756 .update(cx, |workspace, window, cx| {
21757 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
21758 })
21759 .unwrap();
21760 editor.update_in(cx, |editor, window, cx| {
21761 editor.open_context_menu(&OpenContextMenu, window, cx);
21762 assert!(editor.mouse_context_menu.is_some());
21763 });
21764 workspace
21765 .update(cx, |workspace, window, cx| {
21766 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
21767 })
21768 .unwrap();
21769 cx.read(|cx| {
21770 assert!(editor.read(cx).mouse_context_menu.is_none());
21771 });
21772}
21773
21774#[gpui::test]
21775async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
21776 init_test(cx, |_| {});
21777
21778 let fs = FakeFs::new(cx.executor());
21779 fs.insert_file(path!("/file.html"), Default::default())
21780 .await;
21781
21782 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
21783
21784 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21785 let html_language = Arc::new(Language::new(
21786 LanguageConfig {
21787 name: "HTML".into(),
21788 matcher: LanguageMatcher {
21789 path_suffixes: vec!["html".to_string()],
21790 ..LanguageMatcher::default()
21791 },
21792 brackets: BracketPairConfig {
21793 pairs: vec![BracketPair {
21794 start: "<".into(),
21795 end: ">".into(),
21796 close: true,
21797 ..Default::default()
21798 }],
21799 ..Default::default()
21800 },
21801 ..Default::default()
21802 },
21803 Some(tree_sitter_html::LANGUAGE.into()),
21804 ));
21805 language_registry.add(html_language);
21806 let mut fake_servers = language_registry.register_fake_lsp(
21807 "HTML",
21808 FakeLspAdapter {
21809 capabilities: lsp::ServerCapabilities {
21810 completion_provider: Some(lsp::CompletionOptions {
21811 resolve_provider: Some(true),
21812 ..Default::default()
21813 }),
21814 ..Default::default()
21815 },
21816 ..Default::default()
21817 },
21818 );
21819
21820 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21821 let cx = &mut VisualTestContext::from_window(*workspace, cx);
21822
21823 let worktree_id = workspace
21824 .update(cx, |workspace, _window, cx| {
21825 workspace.project().update(cx, |project, cx| {
21826 project.worktrees(cx).next().unwrap().read(cx).id()
21827 })
21828 })
21829 .unwrap();
21830 project
21831 .update(cx, |project, cx| {
21832 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
21833 })
21834 .await
21835 .unwrap();
21836 let editor = workspace
21837 .update(cx, |workspace, window, cx| {
21838 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
21839 })
21840 .unwrap()
21841 .await
21842 .unwrap()
21843 .downcast::<Editor>()
21844 .unwrap();
21845
21846 let fake_server = fake_servers.next().await.unwrap();
21847 editor.update_in(cx, |editor, window, cx| {
21848 editor.set_text("<ad></ad>", window, cx);
21849 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21850 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
21851 });
21852 let Some((buffer, _)) = editor
21853 .buffer
21854 .read(cx)
21855 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
21856 else {
21857 panic!("Failed to get buffer for selection position");
21858 };
21859 let buffer = buffer.read(cx);
21860 let buffer_id = buffer.remote_id();
21861 let opening_range =
21862 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
21863 let closing_range =
21864 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
21865 let mut linked_ranges = HashMap::default();
21866 linked_ranges.insert(
21867 buffer_id,
21868 vec![(opening_range.clone(), vec![closing_range.clone()])],
21869 );
21870 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
21871 });
21872 let mut completion_handle =
21873 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
21874 Ok(Some(lsp::CompletionResponse::Array(vec![
21875 lsp::CompletionItem {
21876 label: "head".to_string(),
21877 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21878 lsp::InsertReplaceEdit {
21879 new_text: "head".to_string(),
21880 insert: lsp::Range::new(
21881 lsp::Position::new(0, 1),
21882 lsp::Position::new(0, 3),
21883 ),
21884 replace: lsp::Range::new(
21885 lsp::Position::new(0, 1),
21886 lsp::Position::new(0, 3),
21887 ),
21888 },
21889 )),
21890 ..Default::default()
21891 },
21892 ])))
21893 });
21894 editor.update_in(cx, |editor, window, cx| {
21895 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
21896 });
21897 cx.run_until_parked();
21898 completion_handle.next().await.unwrap();
21899 editor.update(cx, |editor, _| {
21900 assert!(
21901 editor.context_menu_visible(),
21902 "Completion menu should be visible"
21903 );
21904 });
21905 editor.update_in(cx, |editor, window, cx| {
21906 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
21907 });
21908 cx.executor().run_until_parked();
21909 editor.update(cx, |editor, cx| {
21910 assert_eq!(editor.text(cx), "<head></head>");
21911 });
21912}
21913
21914#[gpui::test]
21915async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
21916 init_test(cx, |_| {});
21917
21918 let fs = FakeFs::new(cx.executor());
21919 fs.insert_tree(
21920 path!("/root"),
21921 json!({
21922 "a": {
21923 "main.rs": "fn main() {}",
21924 },
21925 "foo": {
21926 "bar": {
21927 "external_file.rs": "pub mod external {}",
21928 }
21929 }
21930 }),
21931 )
21932 .await;
21933
21934 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
21935 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21936 language_registry.add(rust_lang());
21937 let _fake_servers = language_registry.register_fake_lsp(
21938 "Rust",
21939 FakeLspAdapter {
21940 ..FakeLspAdapter::default()
21941 },
21942 );
21943 let (workspace, cx) =
21944 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21945 let worktree_id = workspace.update(cx, |workspace, cx| {
21946 workspace.project().update(cx, |project, cx| {
21947 project.worktrees(cx).next().unwrap().read(cx).id()
21948 })
21949 });
21950
21951 let assert_language_servers_count =
21952 |expected: usize, context: &str, cx: &mut VisualTestContext| {
21953 project.update(cx, |project, cx| {
21954 let current = project
21955 .lsp_store()
21956 .read(cx)
21957 .as_local()
21958 .unwrap()
21959 .language_servers
21960 .len();
21961 assert_eq!(expected, current, "{context}");
21962 });
21963 };
21964
21965 assert_language_servers_count(
21966 0,
21967 "No servers should be running before any file is open",
21968 cx,
21969 );
21970 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21971 let main_editor = workspace
21972 .update_in(cx, |workspace, window, cx| {
21973 workspace.open_path(
21974 (worktree_id, "main.rs"),
21975 Some(pane.downgrade()),
21976 true,
21977 window,
21978 cx,
21979 )
21980 })
21981 .unwrap()
21982 .await
21983 .downcast::<Editor>()
21984 .unwrap();
21985 pane.update(cx, |pane, cx| {
21986 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21987 open_editor.update(cx, |editor, cx| {
21988 assert_eq!(
21989 editor.display_text(cx),
21990 "fn main() {}",
21991 "Original main.rs text on initial open",
21992 );
21993 });
21994 assert_eq!(open_editor, main_editor);
21995 });
21996 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
21997
21998 let external_editor = workspace
21999 .update_in(cx, |workspace, window, cx| {
22000 workspace.open_abs_path(
22001 PathBuf::from("/root/foo/bar/external_file.rs"),
22002 OpenOptions::default(),
22003 window,
22004 cx,
22005 )
22006 })
22007 .await
22008 .expect("opening external file")
22009 .downcast::<Editor>()
22010 .expect("downcasted external file's open element to editor");
22011 pane.update(cx, |pane, cx| {
22012 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22013 open_editor.update(cx, |editor, cx| {
22014 assert_eq!(
22015 editor.display_text(cx),
22016 "pub mod external {}",
22017 "External file is open now",
22018 );
22019 });
22020 assert_eq!(open_editor, external_editor);
22021 });
22022 assert_language_servers_count(
22023 1,
22024 "Second, external, *.rs file should join the existing server",
22025 cx,
22026 );
22027
22028 pane.update_in(cx, |pane, window, cx| {
22029 pane.close_active_item(&CloseActiveItem::default(), window, cx)
22030 })
22031 .await
22032 .unwrap();
22033 pane.update_in(cx, |pane, window, cx| {
22034 pane.navigate_backward(window, cx);
22035 });
22036 cx.run_until_parked();
22037 pane.update(cx, |pane, cx| {
22038 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22039 open_editor.update(cx, |editor, cx| {
22040 assert_eq!(
22041 editor.display_text(cx),
22042 "pub mod external {}",
22043 "External file is open now",
22044 );
22045 });
22046 });
22047 assert_language_servers_count(
22048 1,
22049 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
22050 cx,
22051 );
22052
22053 cx.update(|_, cx| {
22054 workspace::reload(&workspace::Reload::default(), cx);
22055 });
22056 assert_language_servers_count(
22057 1,
22058 "After reloading the worktree with local and external files opened, only one project should be started",
22059 cx,
22060 );
22061}
22062
22063#[gpui::test]
22064async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
22065 init_test(cx, |_| {});
22066
22067 let mut cx = EditorTestContext::new(cx).await;
22068 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22069 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22070
22071 // test cursor move to start of each line on tab
22072 // for `if`, `elif`, `else`, `while`, `with` and `for`
22073 cx.set_state(indoc! {"
22074 def main():
22075 ˇ for item in items:
22076 ˇ while item.active:
22077 ˇ if item.value > 10:
22078 ˇ continue
22079 ˇ elif item.value < 0:
22080 ˇ break
22081 ˇ else:
22082 ˇ with item.context() as ctx:
22083 ˇ yield count
22084 ˇ else:
22085 ˇ log('while else')
22086 ˇ else:
22087 ˇ log('for else')
22088 "});
22089 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22090 cx.assert_editor_state(indoc! {"
22091 def main():
22092 ˇfor item in items:
22093 ˇwhile item.active:
22094 ˇif item.value > 10:
22095 ˇcontinue
22096 ˇelif item.value < 0:
22097 ˇbreak
22098 ˇelse:
22099 ˇwith item.context() as ctx:
22100 ˇyield count
22101 ˇelse:
22102 ˇlog('while else')
22103 ˇelse:
22104 ˇlog('for else')
22105 "});
22106 // test relative indent is preserved when tab
22107 // for `if`, `elif`, `else`, `while`, `with` and `for`
22108 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22109 cx.assert_editor_state(indoc! {"
22110 def main():
22111 ˇfor item in items:
22112 ˇwhile item.active:
22113 ˇif item.value > 10:
22114 ˇcontinue
22115 ˇelif item.value < 0:
22116 ˇbreak
22117 ˇelse:
22118 ˇwith item.context() as ctx:
22119 ˇyield count
22120 ˇelse:
22121 ˇlog('while else')
22122 ˇelse:
22123 ˇlog('for else')
22124 "});
22125
22126 // test cursor move to start of each line on tab
22127 // for `try`, `except`, `else`, `finally`, `match` and `def`
22128 cx.set_state(indoc! {"
22129 def main():
22130 ˇ try:
22131 ˇ fetch()
22132 ˇ except ValueError:
22133 ˇ handle_error()
22134 ˇ else:
22135 ˇ match value:
22136 ˇ case _:
22137 ˇ finally:
22138 ˇ def status():
22139 ˇ return 0
22140 "});
22141 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22142 cx.assert_editor_state(indoc! {"
22143 def main():
22144 ˇtry:
22145 ˇfetch()
22146 ˇexcept ValueError:
22147 ˇhandle_error()
22148 ˇelse:
22149 ˇmatch value:
22150 ˇcase _:
22151 ˇfinally:
22152 ˇdef status():
22153 ˇreturn 0
22154 "});
22155 // test relative indent is preserved when tab
22156 // for `try`, `except`, `else`, `finally`, `match` and `def`
22157 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22158 cx.assert_editor_state(indoc! {"
22159 def main():
22160 ˇtry:
22161 ˇfetch()
22162 ˇexcept ValueError:
22163 ˇhandle_error()
22164 ˇelse:
22165 ˇmatch value:
22166 ˇcase _:
22167 ˇfinally:
22168 ˇdef status():
22169 ˇreturn 0
22170 "});
22171}
22172
22173#[gpui::test]
22174async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
22175 init_test(cx, |_| {});
22176
22177 let mut cx = EditorTestContext::new(cx).await;
22178 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22179 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22180
22181 // test `else` auto outdents when typed inside `if` block
22182 cx.set_state(indoc! {"
22183 def main():
22184 if i == 2:
22185 return
22186 ˇ
22187 "});
22188 cx.update_editor(|editor, window, cx| {
22189 editor.handle_input("else:", window, cx);
22190 });
22191 cx.assert_editor_state(indoc! {"
22192 def main():
22193 if i == 2:
22194 return
22195 else:ˇ
22196 "});
22197
22198 // test `except` auto outdents when typed inside `try` block
22199 cx.set_state(indoc! {"
22200 def main():
22201 try:
22202 i = 2
22203 ˇ
22204 "});
22205 cx.update_editor(|editor, window, cx| {
22206 editor.handle_input("except:", window, cx);
22207 });
22208 cx.assert_editor_state(indoc! {"
22209 def main():
22210 try:
22211 i = 2
22212 except:ˇ
22213 "});
22214
22215 // test `else` auto outdents when typed inside `except` block
22216 cx.set_state(indoc! {"
22217 def main():
22218 try:
22219 i = 2
22220 except:
22221 j = 2
22222 ˇ
22223 "});
22224 cx.update_editor(|editor, window, cx| {
22225 editor.handle_input("else:", window, cx);
22226 });
22227 cx.assert_editor_state(indoc! {"
22228 def main():
22229 try:
22230 i = 2
22231 except:
22232 j = 2
22233 else:ˇ
22234 "});
22235
22236 // test `finally` auto outdents when typed inside `else` block
22237 cx.set_state(indoc! {"
22238 def main():
22239 try:
22240 i = 2
22241 except:
22242 j = 2
22243 else:
22244 k = 2
22245 ˇ
22246 "});
22247 cx.update_editor(|editor, window, cx| {
22248 editor.handle_input("finally:", window, cx);
22249 });
22250 cx.assert_editor_state(indoc! {"
22251 def main():
22252 try:
22253 i = 2
22254 except:
22255 j = 2
22256 else:
22257 k = 2
22258 finally:ˇ
22259 "});
22260
22261 // test `else` does not outdents when typed inside `except` block right after for block
22262 cx.set_state(indoc! {"
22263 def main():
22264 try:
22265 i = 2
22266 except:
22267 for i in range(n):
22268 pass
22269 ˇ
22270 "});
22271 cx.update_editor(|editor, window, cx| {
22272 editor.handle_input("else:", window, cx);
22273 });
22274 cx.assert_editor_state(indoc! {"
22275 def main():
22276 try:
22277 i = 2
22278 except:
22279 for i in range(n):
22280 pass
22281 else:ˇ
22282 "});
22283
22284 // test `finally` auto outdents when typed inside `else` block right after for block
22285 cx.set_state(indoc! {"
22286 def main():
22287 try:
22288 i = 2
22289 except:
22290 j = 2
22291 else:
22292 for i in range(n):
22293 pass
22294 ˇ
22295 "});
22296 cx.update_editor(|editor, window, cx| {
22297 editor.handle_input("finally:", window, cx);
22298 });
22299 cx.assert_editor_state(indoc! {"
22300 def main():
22301 try:
22302 i = 2
22303 except:
22304 j = 2
22305 else:
22306 for i in range(n):
22307 pass
22308 finally:ˇ
22309 "});
22310
22311 // test `except` outdents to inner "try" block
22312 cx.set_state(indoc! {"
22313 def main():
22314 try:
22315 i = 2
22316 if i == 2:
22317 try:
22318 i = 3
22319 ˇ
22320 "});
22321 cx.update_editor(|editor, window, cx| {
22322 editor.handle_input("except:", window, cx);
22323 });
22324 cx.assert_editor_state(indoc! {"
22325 def main():
22326 try:
22327 i = 2
22328 if i == 2:
22329 try:
22330 i = 3
22331 except:ˇ
22332 "});
22333
22334 // test `except` outdents to outer "try" block
22335 cx.set_state(indoc! {"
22336 def main():
22337 try:
22338 i = 2
22339 if i == 2:
22340 try:
22341 i = 3
22342 ˇ
22343 "});
22344 cx.update_editor(|editor, window, cx| {
22345 editor.handle_input("except:", window, cx);
22346 });
22347 cx.assert_editor_state(indoc! {"
22348 def main():
22349 try:
22350 i = 2
22351 if i == 2:
22352 try:
22353 i = 3
22354 except:ˇ
22355 "});
22356
22357 // test `else` stays at correct indent when typed after `for` block
22358 cx.set_state(indoc! {"
22359 def main():
22360 for i in range(10):
22361 if i == 3:
22362 break
22363 ˇ
22364 "});
22365 cx.update_editor(|editor, window, cx| {
22366 editor.handle_input("else:", window, cx);
22367 });
22368 cx.assert_editor_state(indoc! {"
22369 def main():
22370 for i in range(10):
22371 if i == 3:
22372 break
22373 else:ˇ
22374 "});
22375
22376 // test does not outdent on typing after line with square brackets
22377 cx.set_state(indoc! {"
22378 def f() -> list[str]:
22379 ˇ
22380 "});
22381 cx.update_editor(|editor, window, cx| {
22382 editor.handle_input("a", window, cx);
22383 });
22384 cx.assert_editor_state(indoc! {"
22385 def f() -> list[str]:
22386 aˇ
22387 "});
22388
22389 // test does not outdent on typing : after case keyword
22390 cx.set_state(indoc! {"
22391 match 1:
22392 caseˇ
22393 "});
22394 cx.update_editor(|editor, window, cx| {
22395 editor.handle_input(":", window, cx);
22396 });
22397 cx.assert_editor_state(indoc! {"
22398 match 1:
22399 case:ˇ
22400 "});
22401}
22402
22403#[gpui::test]
22404async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
22405 init_test(cx, |_| {});
22406 update_test_language_settings(cx, |settings| {
22407 settings.defaults.extend_comment_on_newline = Some(false);
22408 });
22409 let mut cx = EditorTestContext::new(cx).await;
22410 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22411 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22412
22413 // test correct indent after newline on comment
22414 cx.set_state(indoc! {"
22415 # COMMENT:ˇ
22416 "});
22417 cx.update_editor(|editor, window, cx| {
22418 editor.newline(&Newline, window, cx);
22419 });
22420 cx.assert_editor_state(indoc! {"
22421 # COMMENT:
22422 ˇ
22423 "});
22424
22425 // test correct indent after newline in brackets
22426 cx.set_state(indoc! {"
22427 {ˇ}
22428 "});
22429 cx.update_editor(|editor, window, cx| {
22430 editor.newline(&Newline, window, cx);
22431 });
22432 cx.run_until_parked();
22433 cx.assert_editor_state(indoc! {"
22434 {
22435 ˇ
22436 }
22437 "});
22438
22439 cx.set_state(indoc! {"
22440 (ˇ)
22441 "});
22442 cx.update_editor(|editor, window, cx| {
22443 editor.newline(&Newline, window, cx);
22444 });
22445 cx.run_until_parked();
22446 cx.assert_editor_state(indoc! {"
22447 (
22448 ˇ
22449 )
22450 "});
22451
22452 // do not indent after empty lists or dictionaries
22453 cx.set_state(indoc! {"
22454 a = []ˇ
22455 "});
22456 cx.update_editor(|editor, window, cx| {
22457 editor.newline(&Newline, window, cx);
22458 });
22459 cx.run_until_parked();
22460 cx.assert_editor_state(indoc! {"
22461 a = []
22462 ˇ
22463 "});
22464}
22465
22466fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
22467 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
22468 point..point
22469}
22470
22471#[track_caller]
22472fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
22473 let (text, ranges) = marked_text_ranges(marked_text, true);
22474 assert_eq!(editor.text(cx), text);
22475 assert_eq!(
22476 editor.selections.ranges(cx),
22477 ranges,
22478 "Assert selections are {}",
22479 marked_text
22480 );
22481}
22482
22483pub fn handle_signature_help_request(
22484 cx: &mut EditorLspTestContext,
22485 mocked_response: lsp::SignatureHelp,
22486) -> impl Future<Output = ()> + use<> {
22487 let mut request =
22488 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
22489 let mocked_response = mocked_response.clone();
22490 async move { Ok(Some(mocked_response)) }
22491 });
22492
22493 async move {
22494 request.next().await;
22495 }
22496}
22497
22498#[track_caller]
22499pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
22500 cx.update_editor(|editor, _, _| {
22501 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
22502 let entries = menu.entries.borrow();
22503 let entries = entries
22504 .iter()
22505 .map(|entry| entry.string.as_str())
22506 .collect::<Vec<_>>();
22507 assert_eq!(entries, expected);
22508 } else {
22509 panic!("Expected completions menu");
22510 }
22511 });
22512}
22513
22514/// Handle completion request passing a marked string specifying where the completion
22515/// should be triggered from using '|' character, what range should be replaced, and what completions
22516/// should be returned using '<' and '>' to delimit the range.
22517///
22518/// Also see `handle_completion_request_with_insert_and_replace`.
22519#[track_caller]
22520pub fn handle_completion_request(
22521 marked_string: &str,
22522 completions: Vec<&'static str>,
22523 is_incomplete: bool,
22524 counter: Arc<AtomicUsize>,
22525 cx: &mut EditorLspTestContext,
22526) -> impl Future<Output = ()> {
22527 let complete_from_marker: TextRangeMarker = '|'.into();
22528 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22529 let (_, mut marked_ranges) = marked_text_ranges_by(
22530 marked_string,
22531 vec![complete_from_marker.clone(), replace_range_marker.clone()],
22532 );
22533
22534 let complete_from_position =
22535 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22536 let replace_range =
22537 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22538
22539 let mut request =
22540 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22541 let completions = completions.clone();
22542 counter.fetch_add(1, atomic::Ordering::Release);
22543 async move {
22544 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22545 assert_eq!(
22546 params.text_document_position.position,
22547 complete_from_position
22548 );
22549 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
22550 is_incomplete: is_incomplete,
22551 item_defaults: None,
22552 items: completions
22553 .iter()
22554 .map(|completion_text| lsp::CompletionItem {
22555 label: completion_text.to_string(),
22556 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
22557 range: replace_range,
22558 new_text: completion_text.to_string(),
22559 })),
22560 ..Default::default()
22561 })
22562 .collect(),
22563 })))
22564 }
22565 });
22566
22567 async move {
22568 request.next().await;
22569 }
22570}
22571
22572/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
22573/// given instead, which also contains an `insert` range.
22574///
22575/// This function uses markers to define ranges:
22576/// - `|` marks the cursor position
22577/// - `<>` marks the replace range
22578/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
22579pub fn handle_completion_request_with_insert_and_replace(
22580 cx: &mut EditorLspTestContext,
22581 marked_string: &str,
22582 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
22583 counter: Arc<AtomicUsize>,
22584) -> impl Future<Output = ()> {
22585 let complete_from_marker: TextRangeMarker = '|'.into();
22586 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22587 let insert_range_marker: TextRangeMarker = ('{', '}').into();
22588
22589 let (_, mut marked_ranges) = marked_text_ranges_by(
22590 marked_string,
22591 vec![
22592 complete_from_marker.clone(),
22593 replace_range_marker.clone(),
22594 insert_range_marker.clone(),
22595 ],
22596 );
22597
22598 let complete_from_position =
22599 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22600 let replace_range =
22601 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22602
22603 let insert_range = match marked_ranges.remove(&insert_range_marker) {
22604 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
22605 _ => lsp::Range {
22606 start: replace_range.start,
22607 end: complete_from_position,
22608 },
22609 };
22610
22611 let mut request =
22612 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22613 let completions = completions.clone();
22614 counter.fetch_add(1, atomic::Ordering::Release);
22615 async move {
22616 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22617 assert_eq!(
22618 params.text_document_position.position, complete_from_position,
22619 "marker `|` position doesn't match",
22620 );
22621 Ok(Some(lsp::CompletionResponse::Array(
22622 completions
22623 .iter()
22624 .map(|(label, new_text)| lsp::CompletionItem {
22625 label: label.to_string(),
22626 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22627 lsp::InsertReplaceEdit {
22628 insert: insert_range,
22629 replace: replace_range,
22630 new_text: new_text.to_string(),
22631 },
22632 )),
22633 ..Default::default()
22634 })
22635 .collect(),
22636 )))
22637 }
22638 });
22639
22640 async move {
22641 request.next().await;
22642 }
22643}
22644
22645fn handle_resolve_completion_request(
22646 cx: &mut EditorLspTestContext,
22647 edits: Option<Vec<(&'static str, &'static str)>>,
22648) -> impl Future<Output = ()> {
22649 let edits = edits.map(|edits| {
22650 edits
22651 .iter()
22652 .map(|(marked_string, new_text)| {
22653 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
22654 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
22655 lsp::TextEdit::new(replace_range, new_text.to_string())
22656 })
22657 .collect::<Vec<_>>()
22658 });
22659
22660 let mut request =
22661 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
22662 let edits = edits.clone();
22663 async move {
22664 Ok(lsp::CompletionItem {
22665 additional_text_edits: edits,
22666 ..Default::default()
22667 })
22668 }
22669 });
22670
22671 async move {
22672 request.next().await;
22673 }
22674}
22675
22676pub(crate) fn update_test_language_settings(
22677 cx: &mut TestAppContext,
22678 f: impl Fn(&mut AllLanguageSettingsContent),
22679) {
22680 cx.update(|cx| {
22681 SettingsStore::update_global(cx, |store, cx| {
22682 store.update_user_settings::<AllLanguageSettings>(cx, f);
22683 });
22684 });
22685}
22686
22687pub(crate) fn update_test_project_settings(
22688 cx: &mut TestAppContext,
22689 f: impl Fn(&mut ProjectSettings),
22690) {
22691 cx.update(|cx| {
22692 SettingsStore::update_global(cx, |store, cx| {
22693 store.update_user_settings::<ProjectSettings>(cx, f);
22694 });
22695 });
22696}
22697
22698pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
22699 cx.update(|cx| {
22700 assets::Assets.load_test_fonts(cx);
22701 let store = SettingsStore::test(cx);
22702 cx.set_global(store);
22703 theme::init(theme::LoadThemes::JustBase, cx);
22704 release_channel::init(SemanticVersion::default(), cx);
22705 client::init_settings(cx);
22706 language::init(cx);
22707 Project::init_settings(cx);
22708 workspace::init_settings(cx);
22709 crate::init(cx);
22710 });
22711
22712 update_test_language_settings(cx, f);
22713}
22714
22715#[track_caller]
22716fn assert_hunk_revert(
22717 not_reverted_text_with_selections: &str,
22718 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
22719 expected_reverted_text_with_selections: &str,
22720 base_text: &str,
22721 cx: &mut EditorLspTestContext,
22722) {
22723 cx.set_state(not_reverted_text_with_selections);
22724 cx.set_head_text(base_text);
22725 cx.executor().run_until_parked();
22726
22727 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
22728 let snapshot = editor.snapshot(window, cx);
22729 let reverted_hunk_statuses = snapshot
22730 .buffer_snapshot
22731 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
22732 .map(|hunk| hunk.status().kind)
22733 .collect::<Vec<_>>();
22734
22735 editor.git_restore(&Default::default(), window, cx);
22736 reverted_hunk_statuses
22737 });
22738 cx.executor().run_until_parked();
22739 cx.assert_editor_state(expected_reverted_text_with_selections);
22740 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
22741}
22742
22743#[gpui::test(iterations = 10)]
22744async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
22745 init_test(cx, |_| {});
22746
22747 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
22748 let counter = diagnostic_requests.clone();
22749
22750 let fs = FakeFs::new(cx.executor());
22751 fs.insert_tree(
22752 path!("/a"),
22753 json!({
22754 "first.rs": "fn main() { let a = 5; }",
22755 "second.rs": "// Test file",
22756 }),
22757 )
22758 .await;
22759
22760 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22761 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22762 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22763
22764 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22765 language_registry.add(rust_lang());
22766 let mut fake_servers = language_registry.register_fake_lsp(
22767 "Rust",
22768 FakeLspAdapter {
22769 capabilities: lsp::ServerCapabilities {
22770 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
22771 lsp::DiagnosticOptions {
22772 identifier: None,
22773 inter_file_dependencies: true,
22774 workspace_diagnostics: true,
22775 work_done_progress_options: Default::default(),
22776 },
22777 )),
22778 ..Default::default()
22779 },
22780 ..Default::default()
22781 },
22782 );
22783
22784 let editor = workspace
22785 .update(cx, |workspace, window, cx| {
22786 workspace.open_abs_path(
22787 PathBuf::from(path!("/a/first.rs")),
22788 OpenOptions::default(),
22789 window,
22790 cx,
22791 )
22792 })
22793 .unwrap()
22794 .await
22795 .unwrap()
22796 .downcast::<Editor>()
22797 .unwrap();
22798 let fake_server = fake_servers.next().await.unwrap();
22799 let server_id = fake_server.server.server_id();
22800 let mut first_request = fake_server
22801 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
22802 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
22803 let result_id = Some(new_result_id.to_string());
22804 assert_eq!(
22805 params.text_document.uri,
22806 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
22807 );
22808 async move {
22809 Ok(lsp::DocumentDiagnosticReportResult::Report(
22810 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
22811 related_documents: None,
22812 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
22813 items: Vec::new(),
22814 result_id,
22815 },
22816 }),
22817 ))
22818 }
22819 });
22820
22821 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
22822 project.update(cx, |project, cx| {
22823 let buffer_id = editor
22824 .read(cx)
22825 .buffer()
22826 .read(cx)
22827 .as_singleton()
22828 .expect("created a singleton buffer")
22829 .read(cx)
22830 .remote_id();
22831 let buffer_result_id = project
22832 .lsp_store()
22833 .read(cx)
22834 .result_id(server_id, buffer_id, cx);
22835 assert_eq!(expected, buffer_result_id);
22836 });
22837 };
22838
22839 ensure_result_id(None, cx);
22840 cx.executor().advance_clock(Duration::from_millis(60));
22841 cx.executor().run_until_parked();
22842 assert_eq!(
22843 diagnostic_requests.load(atomic::Ordering::Acquire),
22844 1,
22845 "Opening file should trigger diagnostic request"
22846 );
22847 first_request
22848 .next()
22849 .await
22850 .expect("should have sent the first diagnostics pull request");
22851 ensure_result_id(Some("1".to_string()), cx);
22852
22853 // Editing should trigger diagnostics
22854 editor.update_in(cx, |editor, window, cx| {
22855 editor.handle_input("2", window, cx)
22856 });
22857 cx.executor().advance_clock(Duration::from_millis(60));
22858 cx.executor().run_until_parked();
22859 assert_eq!(
22860 diagnostic_requests.load(atomic::Ordering::Acquire),
22861 2,
22862 "Editing should trigger diagnostic request"
22863 );
22864 ensure_result_id(Some("2".to_string()), cx);
22865
22866 // Moving cursor should not trigger diagnostic request
22867 editor.update_in(cx, |editor, window, cx| {
22868 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22869 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
22870 });
22871 });
22872 cx.executor().advance_clock(Duration::from_millis(60));
22873 cx.executor().run_until_parked();
22874 assert_eq!(
22875 diagnostic_requests.load(atomic::Ordering::Acquire),
22876 2,
22877 "Cursor movement should not trigger diagnostic request"
22878 );
22879 ensure_result_id(Some("2".to_string()), cx);
22880 // Multiple rapid edits should be debounced
22881 for _ in 0..5 {
22882 editor.update_in(cx, |editor, window, cx| {
22883 editor.handle_input("x", window, cx)
22884 });
22885 }
22886 cx.executor().advance_clock(Duration::from_millis(60));
22887 cx.executor().run_until_parked();
22888
22889 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
22890 assert!(
22891 final_requests <= 4,
22892 "Multiple rapid edits should be debounced (got {final_requests} requests)",
22893 );
22894 ensure_result_id(Some(final_requests.to_string()), cx);
22895}
22896
22897#[gpui::test]
22898async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
22899 // Regression test for issue #11671
22900 // Previously, adding a cursor after moving multiple cursors would reset
22901 // the cursor count instead of adding to the existing cursors.
22902 init_test(cx, |_| {});
22903 let mut cx = EditorTestContext::new(cx).await;
22904
22905 // Create a simple buffer with cursor at start
22906 cx.set_state(indoc! {"
22907 ˇaaaa
22908 bbbb
22909 cccc
22910 dddd
22911 eeee
22912 ffff
22913 gggg
22914 hhhh"});
22915
22916 // Add 2 cursors below (so we have 3 total)
22917 cx.update_editor(|editor, window, cx| {
22918 editor.add_selection_below(&Default::default(), window, cx);
22919 editor.add_selection_below(&Default::default(), window, cx);
22920 });
22921
22922 // Verify we have 3 cursors
22923 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
22924 assert_eq!(
22925 initial_count, 3,
22926 "Should have 3 cursors after adding 2 below"
22927 );
22928
22929 // Move down one line
22930 cx.update_editor(|editor, window, cx| {
22931 editor.move_down(&MoveDown, window, cx);
22932 });
22933
22934 // Add another cursor below
22935 cx.update_editor(|editor, window, cx| {
22936 editor.add_selection_below(&Default::default(), window, cx);
22937 });
22938
22939 // Should now have 4 cursors (3 original + 1 new)
22940 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
22941 assert_eq!(
22942 final_count, 4,
22943 "Should have 4 cursors after moving and adding another"
22944 );
22945}
22946
22947#[gpui::test(iterations = 10)]
22948async fn test_document_colors(cx: &mut TestAppContext) {
22949 let expected_color = Rgba {
22950 r: 0.33,
22951 g: 0.33,
22952 b: 0.33,
22953 a: 0.33,
22954 };
22955
22956 init_test(cx, |_| {});
22957
22958 let fs = FakeFs::new(cx.executor());
22959 fs.insert_tree(
22960 path!("/a"),
22961 json!({
22962 "first.rs": "fn main() { let a = 5; }",
22963 }),
22964 )
22965 .await;
22966
22967 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22968 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22969 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22970
22971 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22972 language_registry.add(rust_lang());
22973 let mut fake_servers = language_registry.register_fake_lsp(
22974 "Rust",
22975 FakeLspAdapter {
22976 capabilities: lsp::ServerCapabilities {
22977 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
22978 ..lsp::ServerCapabilities::default()
22979 },
22980 name: "rust-analyzer",
22981 ..FakeLspAdapter::default()
22982 },
22983 );
22984 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
22985 "Rust",
22986 FakeLspAdapter {
22987 capabilities: lsp::ServerCapabilities {
22988 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
22989 ..lsp::ServerCapabilities::default()
22990 },
22991 name: "not-rust-analyzer",
22992 ..FakeLspAdapter::default()
22993 },
22994 );
22995
22996 let editor = workspace
22997 .update(cx, |workspace, window, cx| {
22998 workspace.open_abs_path(
22999 PathBuf::from(path!("/a/first.rs")),
23000 OpenOptions::default(),
23001 window,
23002 cx,
23003 )
23004 })
23005 .unwrap()
23006 .await
23007 .unwrap()
23008 .downcast::<Editor>()
23009 .unwrap();
23010 let fake_language_server = fake_servers.next().await.unwrap();
23011 let fake_language_server_without_capabilities =
23012 fake_servers_without_capabilities.next().await.unwrap();
23013 let requests_made = Arc::new(AtomicUsize::new(0));
23014 let closure_requests_made = Arc::clone(&requests_made);
23015 let mut color_request_handle = fake_language_server
23016 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
23017 let requests_made = Arc::clone(&closure_requests_made);
23018 async move {
23019 assert_eq!(
23020 params.text_document.uri,
23021 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
23022 );
23023 requests_made.fetch_add(1, atomic::Ordering::Release);
23024 Ok(vec![
23025 lsp::ColorInformation {
23026 range: lsp::Range {
23027 start: lsp::Position {
23028 line: 0,
23029 character: 0,
23030 },
23031 end: lsp::Position {
23032 line: 0,
23033 character: 1,
23034 },
23035 },
23036 color: lsp::Color {
23037 red: 0.33,
23038 green: 0.33,
23039 blue: 0.33,
23040 alpha: 0.33,
23041 },
23042 },
23043 lsp::ColorInformation {
23044 range: lsp::Range {
23045 start: lsp::Position {
23046 line: 0,
23047 character: 0,
23048 },
23049 end: lsp::Position {
23050 line: 0,
23051 character: 1,
23052 },
23053 },
23054 color: lsp::Color {
23055 red: 0.33,
23056 green: 0.33,
23057 blue: 0.33,
23058 alpha: 0.33,
23059 },
23060 },
23061 ])
23062 }
23063 });
23064
23065 let _handle = fake_language_server_without_capabilities
23066 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
23067 panic!("Should not be called");
23068 });
23069 cx.executor().advance_clock(Duration::from_millis(100));
23070 color_request_handle.next().await.unwrap();
23071 cx.run_until_parked();
23072 assert_eq!(
23073 1,
23074 requests_made.load(atomic::Ordering::Acquire),
23075 "Should query for colors once per editor open"
23076 );
23077 editor.update_in(cx, |editor, _, cx| {
23078 assert_eq!(
23079 vec![expected_color],
23080 extract_color_inlays(editor, cx),
23081 "Should have an initial inlay"
23082 );
23083 });
23084
23085 // opening another file in a split should not influence the LSP query counter
23086 workspace
23087 .update(cx, |workspace, window, cx| {
23088 assert_eq!(
23089 workspace.panes().len(),
23090 1,
23091 "Should have one pane with one editor"
23092 );
23093 workspace.move_item_to_pane_in_direction(
23094 &MoveItemToPaneInDirection {
23095 direction: SplitDirection::Right,
23096 focus: false,
23097 clone: true,
23098 },
23099 window,
23100 cx,
23101 );
23102 })
23103 .unwrap();
23104 cx.run_until_parked();
23105 workspace
23106 .update(cx, |workspace, _, cx| {
23107 let panes = workspace.panes();
23108 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
23109 for pane in panes {
23110 let editor = pane
23111 .read(cx)
23112 .active_item()
23113 .and_then(|item| item.downcast::<Editor>())
23114 .expect("Should have opened an editor in each split");
23115 let editor_file = editor
23116 .read(cx)
23117 .buffer()
23118 .read(cx)
23119 .as_singleton()
23120 .expect("test deals with singleton buffers")
23121 .read(cx)
23122 .file()
23123 .expect("test buffese should have a file")
23124 .path();
23125 assert_eq!(
23126 editor_file.as_ref(),
23127 Path::new("first.rs"),
23128 "Both editors should be opened for the same file"
23129 )
23130 }
23131 })
23132 .unwrap();
23133
23134 cx.executor().advance_clock(Duration::from_millis(500));
23135 let save = editor.update_in(cx, |editor, window, cx| {
23136 editor.move_to_end(&MoveToEnd, window, cx);
23137 editor.handle_input("dirty", window, cx);
23138 editor.save(
23139 SaveOptions {
23140 format: true,
23141 autosave: true,
23142 },
23143 project.clone(),
23144 window,
23145 cx,
23146 )
23147 });
23148 save.await.unwrap();
23149
23150 color_request_handle.next().await.unwrap();
23151 cx.run_until_parked();
23152 assert_eq!(
23153 3,
23154 requests_made.load(atomic::Ordering::Acquire),
23155 "Should query for colors once per save and once per formatting after save"
23156 );
23157
23158 drop(editor);
23159 let close = workspace
23160 .update(cx, |workspace, window, cx| {
23161 workspace.active_pane().update(cx, |pane, cx| {
23162 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23163 })
23164 })
23165 .unwrap();
23166 close.await.unwrap();
23167 let close = workspace
23168 .update(cx, |workspace, window, cx| {
23169 workspace.active_pane().update(cx, |pane, cx| {
23170 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23171 })
23172 })
23173 .unwrap();
23174 close.await.unwrap();
23175 assert_eq!(
23176 3,
23177 requests_made.load(atomic::Ordering::Acquire),
23178 "After saving and closing all editors, no extra requests should be made"
23179 );
23180 workspace
23181 .update(cx, |workspace, _, cx| {
23182 assert!(
23183 workspace.active_item(cx).is_none(),
23184 "Should close all editors"
23185 )
23186 })
23187 .unwrap();
23188
23189 workspace
23190 .update(cx, |workspace, window, cx| {
23191 workspace.active_pane().update(cx, |pane, cx| {
23192 pane.navigate_backward(window, cx);
23193 })
23194 })
23195 .unwrap();
23196 cx.executor().advance_clock(Duration::from_millis(100));
23197 cx.run_until_parked();
23198 let editor = workspace
23199 .update(cx, |workspace, _, cx| {
23200 workspace
23201 .active_item(cx)
23202 .expect("Should have reopened the editor again after navigating back")
23203 .downcast::<Editor>()
23204 .expect("Should be an editor")
23205 })
23206 .unwrap();
23207 color_request_handle.next().await.unwrap();
23208 assert_eq!(
23209 3,
23210 requests_made.load(atomic::Ordering::Acquire),
23211 "Cache should be reused on buffer close and reopen"
23212 );
23213 editor.update(cx, |editor, cx| {
23214 assert_eq!(
23215 vec![expected_color],
23216 extract_color_inlays(editor, cx),
23217 "Should have an initial inlay"
23218 );
23219 });
23220}
23221
23222#[gpui::test]
23223async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
23224 init_test(cx, |_| {});
23225 let (editor, cx) = cx.add_window_view(Editor::single_line);
23226 editor.update_in(cx, |editor, window, cx| {
23227 editor.set_text("oops\n\nwow\n", window, cx)
23228 });
23229 cx.run_until_parked();
23230 editor.update(cx, |editor, cx| {
23231 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
23232 });
23233 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
23234 cx.run_until_parked();
23235 editor.update(cx, |editor, cx| {
23236 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
23237 });
23238}
23239
23240#[track_caller]
23241fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
23242 editor
23243 .all_inlays(cx)
23244 .into_iter()
23245 .filter_map(|inlay| inlay.get_color())
23246 .map(Rgba::from)
23247 .collect()
23248}