1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 inline_completion_tests::FakeInlineCompletionProvider,
6 linked_editing_ranges::LinkedEditingRanges,
7 scroll::scroll_amount::ScrollAmount,
8 test::{
9 assert_text_with_selections, build_editor,
10 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
11 editor_test_context::EditorTestContext,
12 select_ranges,
13 },
14};
15use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
16use futures::StreamExt;
17use gpui::{
18 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
19 VisualTestContext, WindowBounds, WindowOptions, div,
20};
21use indoc::indoc;
22use language::{
23 BracketPairConfig,
24 Capability::ReadWrite,
25 DiagnosticSourceKind, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher,
26 LanguageName, Override, Point,
27 language_settings::{
28 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings, FormatterList,
29 LanguageSettingsContent, LspInsertMode, PrettierSettings, SelectedFormatter,
30 },
31 tree_sitter_python,
32};
33use language_settings::{Formatter, IndentGuideSettings};
34use lsp::CompletionParams;
35use multi_buffer::{IndentGuide, PathKey};
36use parking_lot::Mutex;
37use pretty_assertions::{assert_eq, assert_ne};
38use project::{
39 FakeFs,
40 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
41 project_settings::{LspSettings, ProjectSettings},
42};
43use serde_json::{self, json};
44use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
45use std::{
46 iter,
47 sync::atomic::{self, AtomicUsize},
48};
49use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
50use text::ToPoint as _;
51use unindent::Unindent;
52use util::{
53 assert_set_eq, path,
54 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
55 uri,
56};
57use workspace::{
58 CloseActiveItem, CloseAllItems, CloseInactiveItems, MoveItemToPaneInDirection, NavigationEntry,
59 OpenOptions, ViewId,
60 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
61};
62
63#[gpui::test]
64fn test_edit_events(cx: &mut TestAppContext) {
65 init_test(cx, |_| {});
66
67 let buffer = cx.new(|cx| {
68 let mut buffer = language::Buffer::local("123456", cx);
69 buffer.set_group_interval(Duration::from_secs(1));
70 buffer
71 });
72
73 let events = Rc::new(RefCell::new(Vec::new()));
74 let editor1 = cx.add_window({
75 let events = events.clone();
76 |window, cx| {
77 let entity = cx.entity().clone();
78 cx.subscribe_in(
79 &entity,
80 window,
81 move |_, _, event: &EditorEvent, _, _| match event {
82 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
83 EditorEvent::BufferEdited => {
84 events.borrow_mut().push(("editor1", "buffer edited"))
85 }
86 _ => {}
87 },
88 )
89 .detach();
90 Editor::for_buffer(buffer.clone(), None, window, cx)
91 }
92 });
93
94 let editor2 = cx.add_window({
95 let events = events.clone();
96 |window, cx| {
97 cx.subscribe_in(
98 &cx.entity().clone(),
99 window,
100 move |_, _, event: &EditorEvent, _, _| match event {
101 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
102 EditorEvent::BufferEdited => {
103 events.borrow_mut().push(("editor2", "buffer edited"))
104 }
105 _ => {}
106 },
107 )
108 .detach();
109 Editor::for_buffer(buffer.clone(), None, window, cx)
110 }
111 });
112
113 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
114
115 // Mutating editor 1 will emit an `Edited` event only for that editor.
116 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
117 assert_eq!(
118 mem::take(&mut *events.borrow_mut()),
119 [
120 ("editor1", "edited"),
121 ("editor1", "buffer edited"),
122 ("editor2", "buffer edited"),
123 ]
124 );
125
126 // Mutating editor 2 will emit an `Edited` event only for that editor.
127 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
128 assert_eq!(
129 mem::take(&mut *events.borrow_mut()),
130 [
131 ("editor2", "edited"),
132 ("editor1", "buffer edited"),
133 ("editor2", "buffer edited"),
134 ]
135 );
136
137 // Undoing on editor 1 will emit an `Edited` event only for that editor.
138 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
139 assert_eq!(
140 mem::take(&mut *events.borrow_mut()),
141 [
142 ("editor1", "edited"),
143 ("editor1", "buffer edited"),
144 ("editor2", "buffer edited"),
145 ]
146 );
147
148 // Redoing on editor 1 will emit an `Edited` event only for that editor.
149 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
150 assert_eq!(
151 mem::take(&mut *events.borrow_mut()),
152 [
153 ("editor1", "edited"),
154 ("editor1", "buffer edited"),
155 ("editor2", "buffer edited"),
156 ]
157 );
158
159 // Undoing on editor 2 will emit an `Edited` event only for that editor.
160 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
161 assert_eq!(
162 mem::take(&mut *events.borrow_mut()),
163 [
164 ("editor2", "edited"),
165 ("editor1", "buffer edited"),
166 ("editor2", "buffer edited"),
167 ]
168 );
169
170 // Redoing on editor 2 will emit an `Edited` event only for that editor.
171 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
172 assert_eq!(
173 mem::take(&mut *events.borrow_mut()),
174 [
175 ("editor2", "edited"),
176 ("editor1", "buffer edited"),
177 ("editor2", "buffer edited"),
178 ]
179 );
180
181 // No event is emitted when the mutation is a no-op.
182 _ = editor2.update(cx, |editor, window, cx| {
183 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
184 s.select_ranges([0..0])
185 });
186
187 editor.backspace(&Backspace, window, cx);
188 });
189 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
190}
191
192#[gpui::test]
193fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
194 init_test(cx, |_| {});
195
196 let mut now = Instant::now();
197 let group_interval = Duration::from_millis(1);
198 let buffer = cx.new(|cx| {
199 let mut buf = language::Buffer::local("123456", cx);
200 buf.set_group_interval(group_interval);
201 buf
202 });
203 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
204 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
205
206 _ = editor.update(cx, |editor, window, cx| {
207 editor.start_transaction_at(now, window, cx);
208 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
209 s.select_ranges([2..4])
210 });
211
212 editor.insert("cd", window, cx);
213 editor.end_transaction_at(now, cx);
214 assert_eq!(editor.text(cx), "12cd56");
215 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
216
217 editor.start_transaction_at(now, window, cx);
218 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
219 s.select_ranges([4..5])
220 });
221 editor.insert("e", window, cx);
222 editor.end_transaction_at(now, cx);
223 assert_eq!(editor.text(cx), "12cde6");
224 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
225
226 now += group_interval + Duration::from_millis(1);
227 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
228 s.select_ranges([2..2])
229 });
230
231 // Simulate an edit in another editor
232 buffer.update(cx, |buffer, cx| {
233 buffer.start_transaction_at(now, cx);
234 buffer.edit([(0..1, "a")], None, cx);
235 buffer.edit([(1..1, "b")], None, cx);
236 buffer.end_transaction_at(now, cx);
237 });
238
239 assert_eq!(editor.text(cx), "ab2cde6");
240 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
241
242 // Last transaction happened past the group interval in a different editor.
243 // Undo it individually and don't restore selections.
244 editor.undo(&Undo, window, cx);
245 assert_eq!(editor.text(cx), "12cde6");
246 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
247
248 // First two transactions happened within the group interval in this editor.
249 // Undo them together and restore selections.
250 editor.undo(&Undo, window, cx);
251 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
252 assert_eq!(editor.text(cx), "123456");
253 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
254
255 // Redo the first two transactions together.
256 editor.redo(&Redo, window, cx);
257 assert_eq!(editor.text(cx), "12cde6");
258 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
259
260 // Redo the last transaction on its own.
261 editor.redo(&Redo, window, cx);
262 assert_eq!(editor.text(cx), "ab2cde6");
263 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
264
265 // Test empty transactions.
266 editor.start_transaction_at(now, window, cx);
267 editor.end_transaction_at(now, cx);
268 editor.undo(&Undo, window, cx);
269 assert_eq!(editor.text(cx), "12cde6");
270 });
271}
272
273#[gpui::test]
274fn test_ime_composition(cx: &mut TestAppContext) {
275 init_test(cx, |_| {});
276
277 let buffer = cx.new(|cx| {
278 let mut buffer = language::Buffer::local("abcde", cx);
279 // Ensure automatic grouping doesn't occur.
280 buffer.set_group_interval(Duration::ZERO);
281 buffer
282 });
283
284 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
285 cx.add_window(|window, cx| {
286 let mut editor = build_editor(buffer.clone(), window, cx);
287
288 // Start a new IME composition.
289 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
290 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
291 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
292 assert_eq!(editor.text(cx), "äbcde");
293 assert_eq!(
294 editor.marked_text_ranges(cx),
295 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
296 );
297
298 // Finalize IME composition.
299 editor.replace_text_in_range(None, "ā", window, cx);
300 assert_eq!(editor.text(cx), "ābcde");
301 assert_eq!(editor.marked_text_ranges(cx), None);
302
303 // IME composition edits are grouped and are undone/redone at once.
304 editor.undo(&Default::default(), window, cx);
305 assert_eq!(editor.text(cx), "abcde");
306 assert_eq!(editor.marked_text_ranges(cx), None);
307 editor.redo(&Default::default(), window, cx);
308 assert_eq!(editor.text(cx), "ābcde");
309 assert_eq!(editor.marked_text_ranges(cx), None);
310
311 // Start a new IME composition.
312 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
313 assert_eq!(
314 editor.marked_text_ranges(cx),
315 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
316 );
317
318 // Undoing during an IME composition cancels it.
319 editor.undo(&Default::default(), window, cx);
320 assert_eq!(editor.text(cx), "ābcde");
321 assert_eq!(editor.marked_text_ranges(cx), None);
322
323 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
324 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
325 assert_eq!(editor.text(cx), "ābcdè");
326 assert_eq!(
327 editor.marked_text_ranges(cx),
328 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
329 );
330
331 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
332 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
333 assert_eq!(editor.text(cx), "ābcdę");
334 assert_eq!(editor.marked_text_ranges(cx), None);
335
336 // Start a new IME composition with multiple cursors.
337 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
338 s.select_ranges([
339 OffsetUtf16(1)..OffsetUtf16(1),
340 OffsetUtf16(3)..OffsetUtf16(3),
341 OffsetUtf16(5)..OffsetUtf16(5),
342 ])
343 });
344 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
345 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
346 assert_eq!(
347 editor.marked_text_ranges(cx),
348 Some(vec![
349 OffsetUtf16(0)..OffsetUtf16(3),
350 OffsetUtf16(4)..OffsetUtf16(7),
351 OffsetUtf16(8)..OffsetUtf16(11)
352 ])
353 );
354
355 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
356 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
357 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
358 assert_eq!(
359 editor.marked_text_ranges(cx),
360 Some(vec![
361 OffsetUtf16(1)..OffsetUtf16(2),
362 OffsetUtf16(5)..OffsetUtf16(6),
363 OffsetUtf16(9)..OffsetUtf16(10)
364 ])
365 );
366
367 // Finalize IME composition with multiple cursors.
368 editor.replace_text_in_range(Some(9..10), "2", window, cx);
369 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
370 assert_eq!(editor.marked_text_ranges(cx), None);
371
372 editor
373 });
374}
375
376#[gpui::test]
377fn test_selection_with_mouse(cx: &mut TestAppContext) {
378 init_test(cx, |_| {});
379
380 let editor = cx.add_window(|window, cx| {
381 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
382 build_editor(buffer, window, cx)
383 });
384
385 _ = editor.update(cx, |editor, window, cx| {
386 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
387 });
388 assert_eq!(
389 editor
390 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
391 .unwrap(),
392 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
393 );
394
395 _ = editor.update(cx, |editor, window, cx| {
396 editor.update_selection(
397 DisplayPoint::new(DisplayRow(3), 3),
398 0,
399 gpui::Point::<f32>::default(),
400 window,
401 cx,
402 );
403 });
404
405 assert_eq!(
406 editor
407 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
408 .unwrap(),
409 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
410 );
411
412 _ = editor.update(cx, |editor, window, cx| {
413 editor.update_selection(
414 DisplayPoint::new(DisplayRow(1), 1),
415 0,
416 gpui::Point::<f32>::default(),
417 window,
418 cx,
419 );
420 });
421
422 assert_eq!(
423 editor
424 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
425 .unwrap(),
426 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
427 );
428
429 _ = editor.update(cx, |editor, window, cx| {
430 editor.end_selection(window, cx);
431 editor.update_selection(
432 DisplayPoint::new(DisplayRow(3), 3),
433 0,
434 gpui::Point::<f32>::default(),
435 window,
436 cx,
437 );
438 });
439
440 assert_eq!(
441 editor
442 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
443 .unwrap(),
444 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
445 );
446
447 _ = editor.update(cx, |editor, window, cx| {
448 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
449 editor.update_selection(
450 DisplayPoint::new(DisplayRow(0), 0),
451 0,
452 gpui::Point::<f32>::default(),
453 window,
454 cx,
455 );
456 });
457
458 assert_eq!(
459 editor
460 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
461 .unwrap(),
462 [
463 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
464 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
465 ]
466 );
467
468 _ = editor.update(cx, |editor, window, cx| {
469 editor.end_selection(window, cx);
470 });
471
472 assert_eq!(
473 editor
474 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
475 .unwrap(),
476 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
477 );
478}
479
480#[gpui::test]
481fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
482 init_test(cx, |_| {});
483
484 let editor = cx.add_window(|window, cx| {
485 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
486 build_editor(buffer, window, cx)
487 });
488
489 _ = editor.update(cx, |editor, window, cx| {
490 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
491 });
492
493 _ = editor.update(cx, |editor, window, cx| {
494 editor.end_selection(window, cx);
495 });
496
497 _ = editor.update(cx, |editor, window, cx| {
498 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
499 });
500
501 _ = editor.update(cx, |editor, window, cx| {
502 editor.end_selection(window, cx);
503 });
504
505 assert_eq!(
506 editor
507 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
508 .unwrap(),
509 [
510 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
511 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
512 ]
513 );
514
515 _ = editor.update(cx, |editor, window, cx| {
516 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
517 });
518
519 _ = editor.update(cx, |editor, window, cx| {
520 editor.end_selection(window, cx);
521 });
522
523 assert_eq!(
524 editor
525 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
526 .unwrap(),
527 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
528 );
529}
530
531#[gpui::test]
532fn test_canceling_pending_selection(cx: &mut TestAppContext) {
533 init_test(cx, |_| {});
534
535 let editor = cx.add_window(|window, cx| {
536 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
537 build_editor(buffer, window, cx)
538 });
539
540 _ = editor.update(cx, |editor, window, cx| {
541 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
542 assert_eq!(
543 editor.selections.display_ranges(cx),
544 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
545 );
546 });
547
548 _ = editor.update(cx, |editor, window, cx| {
549 editor.update_selection(
550 DisplayPoint::new(DisplayRow(3), 3),
551 0,
552 gpui::Point::<f32>::default(),
553 window,
554 cx,
555 );
556 assert_eq!(
557 editor.selections.display_ranges(cx),
558 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
559 );
560 });
561
562 _ = editor.update(cx, |editor, window, cx| {
563 editor.cancel(&Cancel, window, cx);
564 editor.update_selection(
565 DisplayPoint::new(DisplayRow(1), 1),
566 0,
567 gpui::Point::<f32>::default(),
568 window,
569 cx,
570 );
571 assert_eq!(
572 editor.selections.display_ranges(cx),
573 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
574 );
575 });
576}
577
578#[gpui::test]
579fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
580 init_test(cx, |_| {});
581
582 let editor = cx.add_window(|window, cx| {
583 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
584 build_editor(buffer, window, cx)
585 });
586
587 _ = editor.update(cx, |editor, window, cx| {
588 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
589 assert_eq!(
590 editor.selections.display_ranges(cx),
591 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
592 );
593
594 editor.move_down(&Default::default(), window, cx);
595 assert_eq!(
596 editor.selections.display_ranges(cx),
597 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
598 );
599
600 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
601 assert_eq!(
602 editor.selections.display_ranges(cx),
603 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
604 );
605
606 editor.move_up(&Default::default(), window, cx);
607 assert_eq!(
608 editor.selections.display_ranges(cx),
609 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
610 );
611 });
612}
613
614#[gpui::test]
615fn test_clone(cx: &mut TestAppContext) {
616 init_test(cx, |_| {});
617
618 let (text, selection_ranges) = marked_text_ranges(
619 indoc! {"
620 one
621 two
622 threeˇ
623 four
624 fiveˇ
625 "},
626 true,
627 );
628
629 let editor = cx.add_window(|window, cx| {
630 let buffer = MultiBuffer::build_simple(&text, cx);
631 build_editor(buffer, window, cx)
632 });
633
634 _ = editor.update(cx, |editor, window, cx| {
635 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
636 s.select_ranges(selection_ranges.clone())
637 });
638 editor.fold_creases(
639 vec![
640 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
641 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
642 ],
643 true,
644 window,
645 cx,
646 );
647 });
648
649 let cloned_editor = editor
650 .update(cx, |editor, _, cx| {
651 cx.open_window(Default::default(), |window, cx| {
652 cx.new(|cx| editor.clone(window, cx))
653 })
654 })
655 .unwrap()
656 .unwrap();
657
658 let snapshot = editor
659 .update(cx, |e, window, cx| e.snapshot(window, cx))
660 .unwrap();
661 let cloned_snapshot = cloned_editor
662 .update(cx, |e, window, cx| e.snapshot(window, cx))
663 .unwrap();
664
665 assert_eq!(
666 cloned_editor
667 .update(cx, |e, _, cx| e.display_text(cx))
668 .unwrap(),
669 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
670 );
671 assert_eq!(
672 cloned_snapshot
673 .folds_in_range(0..text.len())
674 .collect::<Vec<_>>(),
675 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
676 );
677 assert_set_eq!(
678 cloned_editor
679 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
680 .unwrap(),
681 editor
682 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
683 .unwrap()
684 );
685 assert_set_eq!(
686 cloned_editor
687 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
688 .unwrap(),
689 editor
690 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
691 .unwrap()
692 );
693}
694
695#[gpui::test]
696async fn test_navigation_history(cx: &mut TestAppContext) {
697 init_test(cx, |_| {});
698
699 use workspace::item::Item;
700
701 let fs = FakeFs::new(cx.executor());
702 let project = Project::test(fs, [], cx).await;
703 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
704 let pane = workspace
705 .update(cx, |workspace, _, _| workspace.active_pane().clone())
706 .unwrap();
707
708 _ = workspace.update(cx, |_v, window, cx| {
709 cx.new(|cx| {
710 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
711 let mut editor = build_editor(buffer.clone(), window, cx);
712 let handle = cx.entity();
713 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
714
715 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
716 editor.nav_history.as_mut().unwrap().pop_backward(cx)
717 }
718
719 // Move the cursor a small distance.
720 // Nothing is added to the navigation history.
721 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
722 s.select_display_ranges([
723 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
724 ])
725 });
726 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
727 s.select_display_ranges([
728 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
729 ])
730 });
731 assert!(pop_history(&mut editor, cx).is_none());
732
733 // Move the cursor a large distance.
734 // The history can jump back to the previous position.
735 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
736 s.select_display_ranges([
737 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
738 ])
739 });
740 let nav_entry = pop_history(&mut editor, cx).unwrap();
741 editor.navigate(nav_entry.data.unwrap(), window, cx);
742 assert_eq!(nav_entry.item.id(), cx.entity_id());
743 assert_eq!(
744 editor.selections.display_ranges(cx),
745 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
746 );
747 assert!(pop_history(&mut editor, cx).is_none());
748
749 // Move the cursor a small distance via the mouse.
750 // Nothing is added to the navigation history.
751 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
752 editor.end_selection(window, cx);
753 assert_eq!(
754 editor.selections.display_ranges(cx),
755 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
756 );
757 assert!(pop_history(&mut editor, cx).is_none());
758
759 // Move the cursor a large distance via the mouse.
760 // The history can jump back to the previous position.
761 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
762 editor.end_selection(window, cx);
763 assert_eq!(
764 editor.selections.display_ranges(cx),
765 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
766 );
767 let nav_entry = pop_history(&mut editor, cx).unwrap();
768 editor.navigate(nav_entry.data.unwrap(), window, cx);
769 assert_eq!(nav_entry.item.id(), cx.entity_id());
770 assert_eq!(
771 editor.selections.display_ranges(cx),
772 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
773 );
774 assert!(pop_history(&mut editor, cx).is_none());
775
776 // Set scroll position to check later
777 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
778 let original_scroll_position = editor.scroll_manager.anchor();
779
780 // Jump to the end of the document and adjust scroll
781 editor.move_to_end(&MoveToEnd, window, cx);
782 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
783 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
784
785 let nav_entry = pop_history(&mut editor, cx).unwrap();
786 editor.navigate(nav_entry.data.unwrap(), window, cx);
787 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
788
789 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
790 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
791 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
792 let invalid_point = Point::new(9999, 0);
793 editor.navigate(
794 Box::new(NavigationData {
795 cursor_anchor: invalid_anchor,
796 cursor_position: invalid_point,
797 scroll_anchor: ScrollAnchor {
798 anchor: invalid_anchor,
799 offset: Default::default(),
800 },
801 scroll_top_row: invalid_point.row,
802 }),
803 window,
804 cx,
805 );
806 assert_eq!(
807 editor.selections.display_ranges(cx),
808 &[editor.max_point(cx)..editor.max_point(cx)]
809 );
810 assert_eq!(
811 editor.scroll_position(cx),
812 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
813 );
814
815 editor
816 })
817 });
818}
819
820#[gpui::test]
821fn test_cancel(cx: &mut TestAppContext) {
822 init_test(cx, |_| {});
823
824 let editor = cx.add_window(|window, cx| {
825 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
826 build_editor(buffer, window, cx)
827 });
828
829 _ = editor.update(cx, |editor, window, cx| {
830 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
831 editor.update_selection(
832 DisplayPoint::new(DisplayRow(1), 1),
833 0,
834 gpui::Point::<f32>::default(),
835 window,
836 cx,
837 );
838 editor.end_selection(window, cx);
839
840 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
841 editor.update_selection(
842 DisplayPoint::new(DisplayRow(0), 3),
843 0,
844 gpui::Point::<f32>::default(),
845 window,
846 cx,
847 );
848 editor.end_selection(window, cx);
849 assert_eq!(
850 editor.selections.display_ranges(cx),
851 [
852 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
853 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
854 ]
855 );
856 });
857
858 _ = editor.update(cx, |editor, window, cx| {
859 editor.cancel(&Cancel, window, cx);
860 assert_eq!(
861 editor.selections.display_ranges(cx),
862 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
863 );
864 });
865
866 _ = editor.update(cx, |editor, window, cx| {
867 editor.cancel(&Cancel, window, cx);
868 assert_eq!(
869 editor.selections.display_ranges(cx),
870 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
871 );
872 });
873}
874
875#[gpui::test]
876fn test_fold_action(cx: &mut TestAppContext) {
877 init_test(cx, |_| {});
878
879 let editor = cx.add_window(|window, cx| {
880 let buffer = MultiBuffer::build_simple(
881 &"
882 impl Foo {
883 // Hello!
884
885 fn a() {
886 1
887 }
888
889 fn b() {
890 2
891 }
892
893 fn c() {
894 3
895 }
896 }
897 "
898 .unindent(),
899 cx,
900 );
901 build_editor(buffer.clone(), window, cx)
902 });
903
904 _ = editor.update(cx, |editor, window, cx| {
905 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
906 s.select_display_ranges([
907 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
908 ]);
909 });
910 editor.fold(&Fold, window, cx);
911 assert_eq!(
912 editor.display_text(cx),
913 "
914 impl Foo {
915 // Hello!
916
917 fn a() {
918 1
919 }
920
921 fn b() {⋯
922 }
923
924 fn c() {⋯
925 }
926 }
927 "
928 .unindent(),
929 );
930
931 editor.fold(&Fold, window, cx);
932 assert_eq!(
933 editor.display_text(cx),
934 "
935 impl Foo {⋯
936 }
937 "
938 .unindent(),
939 );
940
941 editor.unfold_lines(&UnfoldLines, window, cx);
942 assert_eq!(
943 editor.display_text(cx),
944 "
945 impl Foo {
946 // Hello!
947
948 fn a() {
949 1
950 }
951
952 fn b() {⋯
953 }
954
955 fn c() {⋯
956 }
957 }
958 "
959 .unindent(),
960 );
961
962 editor.unfold_lines(&UnfoldLines, window, cx);
963 assert_eq!(
964 editor.display_text(cx),
965 editor.buffer.read(cx).read(cx).text()
966 );
967 });
968}
969
970#[gpui::test]
971fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
972 init_test(cx, |_| {});
973
974 let editor = cx.add_window(|window, cx| {
975 let buffer = MultiBuffer::build_simple(
976 &"
977 class Foo:
978 # Hello!
979
980 def a():
981 print(1)
982
983 def b():
984 print(2)
985
986 def c():
987 print(3)
988 "
989 .unindent(),
990 cx,
991 );
992 build_editor(buffer.clone(), window, cx)
993 });
994
995 _ = editor.update(cx, |editor, window, cx| {
996 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
997 s.select_display_ranges([
998 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
999 ]);
1000 });
1001 editor.fold(&Fold, window, cx);
1002 assert_eq!(
1003 editor.display_text(cx),
1004 "
1005 class Foo:
1006 # Hello!
1007
1008 def a():
1009 print(1)
1010
1011 def b():⋯
1012
1013 def c():⋯
1014 "
1015 .unindent(),
1016 );
1017
1018 editor.fold(&Fold, window, cx);
1019 assert_eq!(
1020 editor.display_text(cx),
1021 "
1022 class Foo:⋯
1023 "
1024 .unindent(),
1025 );
1026
1027 editor.unfold_lines(&UnfoldLines, window, cx);
1028 assert_eq!(
1029 editor.display_text(cx),
1030 "
1031 class Foo:
1032 # Hello!
1033
1034 def a():
1035 print(1)
1036
1037 def b():⋯
1038
1039 def c():⋯
1040 "
1041 .unindent(),
1042 );
1043
1044 editor.unfold_lines(&UnfoldLines, window, cx);
1045 assert_eq!(
1046 editor.display_text(cx),
1047 editor.buffer.read(cx).read(cx).text()
1048 );
1049 });
1050}
1051
1052#[gpui::test]
1053fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1054 init_test(cx, |_| {});
1055
1056 let editor = cx.add_window(|window, cx| {
1057 let buffer = MultiBuffer::build_simple(
1058 &"
1059 class Foo:
1060 # Hello!
1061
1062 def a():
1063 print(1)
1064
1065 def b():
1066 print(2)
1067
1068
1069 def c():
1070 print(3)
1071
1072
1073 "
1074 .unindent(),
1075 cx,
1076 );
1077 build_editor(buffer.clone(), window, cx)
1078 });
1079
1080 _ = editor.update(cx, |editor, window, cx| {
1081 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1082 s.select_display_ranges([
1083 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1084 ]);
1085 });
1086 editor.fold(&Fold, window, cx);
1087 assert_eq!(
1088 editor.display_text(cx),
1089 "
1090 class Foo:
1091 # Hello!
1092
1093 def a():
1094 print(1)
1095
1096 def b():⋯
1097
1098
1099 def c():⋯
1100
1101
1102 "
1103 .unindent(),
1104 );
1105
1106 editor.fold(&Fold, window, cx);
1107 assert_eq!(
1108 editor.display_text(cx),
1109 "
1110 class Foo:⋯
1111
1112
1113 "
1114 .unindent(),
1115 );
1116
1117 editor.unfold_lines(&UnfoldLines, window, cx);
1118 assert_eq!(
1119 editor.display_text(cx),
1120 "
1121 class Foo:
1122 # Hello!
1123
1124 def a():
1125 print(1)
1126
1127 def b():⋯
1128
1129
1130 def c():⋯
1131
1132
1133 "
1134 .unindent(),
1135 );
1136
1137 editor.unfold_lines(&UnfoldLines, window, cx);
1138 assert_eq!(
1139 editor.display_text(cx),
1140 editor.buffer.read(cx).read(cx).text()
1141 );
1142 });
1143}
1144
1145#[gpui::test]
1146fn test_fold_at_level(cx: &mut TestAppContext) {
1147 init_test(cx, |_| {});
1148
1149 let editor = cx.add_window(|window, cx| {
1150 let buffer = MultiBuffer::build_simple(
1151 &"
1152 class Foo:
1153 # Hello!
1154
1155 def a():
1156 print(1)
1157
1158 def b():
1159 print(2)
1160
1161
1162 class Bar:
1163 # World!
1164
1165 def a():
1166 print(1)
1167
1168 def b():
1169 print(2)
1170
1171
1172 "
1173 .unindent(),
1174 cx,
1175 );
1176 build_editor(buffer.clone(), window, cx)
1177 });
1178
1179 _ = editor.update(cx, |editor, window, cx| {
1180 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1181 assert_eq!(
1182 editor.display_text(cx),
1183 "
1184 class Foo:
1185 # Hello!
1186
1187 def a():⋯
1188
1189 def b():⋯
1190
1191
1192 class Bar:
1193 # World!
1194
1195 def a():⋯
1196
1197 def b():⋯
1198
1199
1200 "
1201 .unindent(),
1202 );
1203
1204 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1205 assert_eq!(
1206 editor.display_text(cx),
1207 "
1208 class Foo:⋯
1209
1210
1211 class Bar:⋯
1212
1213
1214 "
1215 .unindent(),
1216 );
1217
1218 editor.unfold_all(&UnfoldAll, window, cx);
1219 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1220 assert_eq!(
1221 editor.display_text(cx),
1222 "
1223 class Foo:
1224 # Hello!
1225
1226 def a():
1227 print(1)
1228
1229 def b():
1230 print(2)
1231
1232
1233 class Bar:
1234 # World!
1235
1236 def a():
1237 print(1)
1238
1239 def b():
1240 print(2)
1241
1242
1243 "
1244 .unindent(),
1245 );
1246
1247 assert_eq!(
1248 editor.display_text(cx),
1249 editor.buffer.read(cx).read(cx).text()
1250 );
1251 });
1252}
1253
1254#[gpui::test]
1255fn test_move_cursor(cx: &mut TestAppContext) {
1256 init_test(cx, |_| {});
1257
1258 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1259 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1260
1261 buffer.update(cx, |buffer, cx| {
1262 buffer.edit(
1263 vec![
1264 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1265 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1266 ],
1267 None,
1268 cx,
1269 );
1270 });
1271 _ = editor.update(cx, |editor, window, cx| {
1272 assert_eq!(
1273 editor.selections.display_ranges(cx),
1274 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1275 );
1276
1277 editor.move_down(&MoveDown, window, cx);
1278 assert_eq!(
1279 editor.selections.display_ranges(cx),
1280 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1281 );
1282
1283 editor.move_right(&MoveRight, window, cx);
1284 assert_eq!(
1285 editor.selections.display_ranges(cx),
1286 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1287 );
1288
1289 editor.move_left(&MoveLeft, window, cx);
1290 assert_eq!(
1291 editor.selections.display_ranges(cx),
1292 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1293 );
1294
1295 editor.move_up(&MoveUp, window, cx);
1296 assert_eq!(
1297 editor.selections.display_ranges(cx),
1298 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1299 );
1300
1301 editor.move_to_end(&MoveToEnd, window, cx);
1302 assert_eq!(
1303 editor.selections.display_ranges(cx),
1304 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1305 );
1306
1307 editor.move_to_beginning(&MoveToBeginning, window, cx);
1308 assert_eq!(
1309 editor.selections.display_ranges(cx),
1310 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1311 );
1312
1313 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1314 s.select_display_ranges([
1315 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1316 ]);
1317 });
1318 editor.select_to_beginning(&SelectToBeginning, window, cx);
1319 assert_eq!(
1320 editor.selections.display_ranges(cx),
1321 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1322 );
1323
1324 editor.select_to_end(&SelectToEnd, window, cx);
1325 assert_eq!(
1326 editor.selections.display_ranges(cx),
1327 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1328 );
1329 });
1330}
1331
1332#[gpui::test]
1333fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1334 init_test(cx, |_| {});
1335
1336 let editor = cx.add_window(|window, cx| {
1337 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1338 build_editor(buffer.clone(), window, cx)
1339 });
1340
1341 assert_eq!('🟥'.len_utf8(), 4);
1342 assert_eq!('α'.len_utf8(), 2);
1343
1344 _ = editor.update(cx, |editor, window, cx| {
1345 editor.fold_creases(
1346 vec![
1347 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1348 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1349 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1350 ],
1351 true,
1352 window,
1353 cx,
1354 );
1355 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1356
1357 editor.move_right(&MoveRight, window, cx);
1358 assert_eq!(
1359 editor.selections.display_ranges(cx),
1360 &[empty_range(0, "🟥".len())]
1361 );
1362 editor.move_right(&MoveRight, window, cx);
1363 assert_eq!(
1364 editor.selections.display_ranges(cx),
1365 &[empty_range(0, "🟥🟧".len())]
1366 );
1367 editor.move_right(&MoveRight, window, cx);
1368 assert_eq!(
1369 editor.selections.display_ranges(cx),
1370 &[empty_range(0, "🟥🟧⋯".len())]
1371 );
1372
1373 editor.move_down(&MoveDown, window, cx);
1374 assert_eq!(
1375 editor.selections.display_ranges(cx),
1376 &[empty_range(1, "ab⋯e".len())]
1377 );
1378 editor.move_left(&MoveLeft, window, cx);
1379 assert_eq!(
1380 editor.selections.display_ranges(cx),
1381 &[empty_range(1, "ab⋯".len())]
1382 );
1383 editor.move_left(&MoveLeft, window, cx);
1384 assert_eq!(
1385 editor.selections.display_ranges(cx),
1386 &[empty_range(1, "ab".len())]
1387 );
1388 editor.move_left(&MoveLeft, window, cx);
1389 assert_eq!(
1390 editor.selections.display_ranges(cx),
1391 &[empty_range(1, "a".len())]
1392 );
1393
1394 editor.move_down(&MoveDown, window, cx);
1395 assert_eq!(
1396 editor.selections.display_ranges(cx),
1397 &[empty_range(2, "α".len())]
1398 );
1399 editor.move_right(&MoveRight, window, cx);
1400 assert_eq!(
1401 editor.selections.display_ranges(cx),
1402 &[empty_range(2, "αβ".len())]
1403 );
1404 editor.move_right(&MoveRight, window, cx);
1405 assert_eq!(
1406 editor.selections.display_ranges(cx),
1407 &[empty_range(2, "αβ⋯".len())]
1408 );
1409 editor.move_right(&MoveRight, window, cx);
1410 assert_eq!(
1411 editor.selections.display_ranges(cx),
1412 &[empty_range(2, "αβ⋯ε".len())]
1413 );
1414
1415 editor.move_up(&MoveUp, window, cx);
1416 assert_eq!(
1417 editor.selections.display_ranges(cx),
1418 &[empty_range(1, "ab⋯e".len())]
1419 );
1420 editor.move_down(&MoveDown, window, cx);
1421 assert_eq!(
1422 editor.selections.display_ranges(cx),
1423 &[empty_range(2, "αβ⋯ε".len())]
1424 );
1425 editor.move_up(&MoveUp, window, cx);
1426 assert_eq!(
1427 editor.selections.display_ranges(cx),
1428 &[empty_range(1, "ab⋯e".len())]
1429 );
1430
1431 editor.move_up(&MoveUp, window, cx);
1432 assert_eq!(
1433 editor.selections.display_ranges(cx),
1434 &[empty_range(0, "🟥🟧".len())]
1435 );
1436 editor.move_left(&MoveLeft, window, cx);
1437 assert_eq!(
1438 editor.selections.display_ranges(cx),
1439 &[empty_range(0, "🟥".len())]
1440 );
1441 editor.move_left(&MoveLeft, window, cx);
1442 assert_eq!(
1443 editor.selections.display_ranges(cx),
1444 &[empty_range(0, "".len())]
1445 );
1446 });
1447}
1448
1449#[gpui::test]
1450fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1451 init_test(cx, |_| {});
1452
1453 let editor = cx.add_window(|window, cx| {
1454 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1455 build_editor(buffer.clone(), window, cx)
1456 });
1457 _ = editor.update(cx, |editor, window, cx| {
1458 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1459 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1460 });
1461
1462 // moving above start of document should move selection to start of document,
1463 // but the next move down should still be at the original goal_x
1464 editor.move_up(&MoveUp, window, cx);
1465 assert_eq!(
1466 editor.selections.display_ranges(cx),
1467 &[empty_range(0, "".len())]
1468 );
1469
1470 editor.move_down(&MoveDown, window, cx);
1471 assert_eq!(
1472 editor.selections.display_ranges(cx),
1473 &[empty_range(1, "abcd".len())]
1474 );
1475
1476 editor.move_down(&MoveDown, window, cx);
1477 assert_eq!(
1478 editor.selections.display_ranges(cx),
1479 &[empty_range(2, "αβγ".len())]
1480 );
1481
1482 editor.move_down(&MoveDown, window, cx);
1483 assert_eq!(
1484 editor.selections.display_ranges(cx),
1485 &[empty_range(3, "abcd".len())]
1486 );
1487
1488 editor.move_down(&MoveDown, window, cx);
1489 assert_eq!(
1490 editor.selections.display_ranges(cx),
1491 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1492 );
1493
1494 // moving past end of document should not change goal_x
1495 editor.move_down(&MoveDown, window, cx);
1496 assert_eq!(
1497 editor.selections.display_ranges(cx),
1498 &[empty_range(5, "".len())]
1499 );
1500
1501 editor.move_down(&MoveDown, window, cx);
1502 assert_eq!(
1503 editor.selections.display_ranges(cx),
1504 &[empty_range(5, "".len())]
1505 );
1506
1507 editor.move_up(&MoveUp, window, cx);
1508 assert_eq!(
1509 editor.selections.display_ranges(cx),
1510 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1511 );
1512
1513 editor.move_up(&MoveUp, window, cx);
1514 assert_eq!(
1515 editor.selections.display_ranges(cx),
1516 &[empty_range(3, "abcd".len())]
1517 );
1518
1519 editor.move_up(&MoveUp, window, cx);
1520 assert_eq!(
1521 editor.selections.display_ranges(cx),
1522 &[empty_range(2, "αβγ".len())]
1523 );
1524 });
1525}
1526
1527#[gpui::test]
1528fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1529 init_test(cx, |_| {});
1530 let move_to_beg = MoveToBeginningOfLine {
1531 stop_at_soft_wraps: true,
1532 stop_at_indent: true,
1533 };
1534
1535 let delete_to_beg = DeleteToBeginningOfLine {
1536 stop_at_indent: false,
1537 };
1538
1539 let move_to_end = MoveToEndOfLine {
1540 stop_at_soft_wraps: true,
1541 };
1542
1543 let editor = cx.add_window(|window, cx| {
1544 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1545 build_editor(buffer, window, cx)
1546 });
1547 _ = editor.update(cx, |editor, window, cx| {
1548 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1549 s.select_display_ranges([
1550 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1551 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1552 ]);
1553 });
1554 });
1555
1556 _ = editor.update(cx, |editor, window, cx| {
1557 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1558 assert_eq!(
1559 editor.selections.display_ranges(cx),
1560 &[
1561 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1562 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1563 ]
1564 );
1565 });
1566
1567 _ = editor.update(cx, |editor, window, cx| {
1568 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1569 assert_eq!(
1570 editor.selections.display_ranges(cx),
1571 &[
1572 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1573 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1574 ]
1575 );
1576 });
1577
1578 _ = editor.update(cx, |editor, window, cx| {
1579 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1580 assert_eq!(
1581 editor.selections.display_ranges(cx),
1582 &[
1583 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1584 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1585 ]
1586 );
1587 });
1588
1589 _ = editor.update(cx, |editor, window, cx| {
1590 editor.move_to_end_of_line(&move_to_end, window, cx);
1591 assert_eq!(
1592 editor.selections.display_ranges(cx),
1593 &[
1594 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1595 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1596 ]
1597 );
1598 });
1599
1600 // Moving to the end of line again is a no-op.
1601 _ = editor.update(cx, |editor, window, cx| {
1602 editor.move_to_end_of_line(&move_to_end, window, cx);
1603 assert_eq!(
1604 editor.selections.display_ranges(cx),
1605 &[
1606 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1607 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1608 ]
1609 );
1610 });
1611
1612 _ = editor.update(cx, |editor, window, cx| {
1613 editor.move_left(&MoveLeft, window, cx);
1614 editor.select_to_beginning_of_line(
1615 &SelectToBeginningOfLine {
1616 stop_at_soft_wraps: true,
1617 stop_at_indent: true,
1618 },
1619 window,
1620 cx,
1621 );
1622 assert_eq!(
1623 editor.selections.display_ranges(cx),
1624 &[
1625 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1626 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1627 ]
1628 );
1629 });
1630
1631 _ = editor.update(cx, |editor, window, cx| {
1632 editor.select_to_beginning_of_line(
1633 &SelectToBeginningOfLine {
1634 stop_at_soft_wraps: true,
1635 stop_at_indent: true,
1636 },
1637 window,
1638 cx,
1639 );
1640 assert_eq!(
1641 editor.selections.display_ranges(cx),
1642 &[
1643 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1644 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1645 ]
1646 );
1647 });
1648
1649 _ = editor.update(cx, |editor, window, cx| {
1650 editor.select_to_beginning_of_line(
1651 &SelectToBeginningOfLine {
1652 stop_at_soft_wraps: true,
1653 stop_at_indent: true,
1654 },
1655 window,
1656 cx,
1657 );
1658 assert_eq!(
1659 editor.selections.display_ranges(cx),
1660 &[
1661 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1662 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1663 ]
1664 );
1665 });
1666
1667 _ = editor.update(cx, |editor, window, cx| {
1668 editor.select_to_end_of_line(
1669 &SelectToEndOfLine {
1670 stop_at_soft_wraps: true,
1671 },
1672 window,
1673 cx,
1674 );
1675 assert_eq!(
1676 editor.selections.display_ranges(cx),
1677 &[
1678 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1679 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1680 ]
1681 );
1682 });
1683
1684 _ = editor.update(cx, |editor, window, cx| {
1685 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1686 assert_eq!(editor.display_text(cx), "ab\n de");
1687 assert_eq!(
1688 editor.selections.display_ranges(cx),
1689 &[
1690 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1691 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1692 ]
1693 );
1694 });
1695
1696 _ = editor.update(cx, |editor, window, cx| {
1697 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1698 assert_eq!(editor.display_text(cx), "\n");
1699 assert_eq!(
1700 editor.selections.display_ranges(cx),
1701 &[
1702 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1703 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1704 ]
1705 );
1706 });
1707}
1708
1709#[gpui::test]
1710fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1711 init_test(cx, |_| {});
1712 let move_to_beg = MoveToBeginningOfLine {
1713 stop_at_soft_wraps: false,
1714 stop_at_indent: false,
1715 };
1716
1717 let move_to_end = MoveToEndOfLine {
1718 stop_at_soft_wraps: false,
1719 };
1720
1721 let editor = cx.add_window(|window, cx| {
1722 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1723 build_editor(buffer, window, cx)
1724 });
1725
1726 _ = editor.update(cx, |editor, window, cx| {
1727 editor.set_wrap_width(Some(140.0.into()), cx);
1728
1729 // We expect the following lines after wrapping
1730 // ```
1731 // thequickbrownfox
1732 // jumpedoverthelazydo
1733 // gs
1734 // ```
1735 // The final `gs` was soft-wrapped onto a new line.
1736 assert_eq!(
1737 "thequickbrownfox\njumpedoverthelaz\nydogs",
1738 editor.display_text(cx),
1739 );
1740
1741 // First, let's assert behavior on the first line, that was not soft-wrapped.
1742 // Start the cursor at the `k` on the first line
1743 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1744 s.select_display_ranges([
1745 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1746 ]);
1747 });
1748
1749 // Moving to the beginning of the line should put us at the beginning of the line.
1750 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1751 assert_eq!(
1752 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1753 editor.selections.display_ranges(cx)
1754 );
1755
1756 // Moving to the end of the line should put us at the end of the line.
1757 editor.move_to_end_of_line(&move_to_end, window, cx);
1758 assert_eq!(
1759 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1760 editor.selections.display_ranges(cx)
1761 );
1762
1763 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1764 // Start the cursor at the last line (`y` that was wrapped to a new line)
1765 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1766 s.select_display_ranges([
1767 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1768 ]);
1769 });
1770
1771 // Moving to the beginning of the line should put us at the start of the second line of
1772 // display text, i.e., the `j`.
1773 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1774 assert_eq!(
1775 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1776 editor.selections.display_ranges(cx)
1777 );
1778
1779 // Moving to the beginning of the line again should be a no-op.
1780 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1781 assert_eq!(
1782 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1783 editor.selections.display_ranges(cx)
1784 );
1785
1786 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1787 // next display line.
1788 editor.move_to_end_of_line(&move_to_end, window, cx);
1789 assert_eq!(
1790 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1791 editor.selections.display_ranges(cx)
1792 );
1793
1794 // Moving to the end of the line again should be a no-op.
1795 editor.move_to_end_of_line(&move_to_end, window, cx);
1796 assert_eq!(
1797 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1798 editor.selections.display_ranges(cx)
1799 );
1800 });
1801}
1802
1803#[gpui::test]
1804fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1805 init_test(cx, |_| {});
1806
1807 let move_to_beg = MoveToBeginningOfLine {
1808 stop_at_soft_wraps: true,
1809 stop_at_indent: true,
1810 };
1811
1812 let select_to_beg = SelectToBeginningOfLine {
1813 stop_at_soft_wraps: true,
1814 stop_at_indent: true,
1815 };
1816
1817 let delete_to_beg = DeleteToBeginningOfLine {
1818 stop_at_indent: true,
1819 };
1820
1821 let move_to_end = MoveToEndOfLine {
1822 stop_at_soft_wraps: false,
1823 };
1824
1825 let editor = cx.add_window(|window, cx| {
1826 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1827 build_editor(buffer, window, cx)
1828 });
1829
1830 _ = editor.update(cx, |editor, window, cx| {
1831 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1832 s.select_display_ranges([
1833 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1834 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1835 ]);
1836 });
1837
1838 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1839 // and the second cursor at the first non-whitespace character in the line.
1840 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1841 assert_eq!(
1842 editor.selections.display_ranges(cx),
1843 &[
1844 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1845 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1846 ]
1847 );
1848
1849 // Moving to the beginning of the line again should be a no-op for the first cursor,
1850 // and should move the second cursor to the beginning of the line.
1851 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1852 assert_eq!(
1853 editor.selections.display_ranges(cx),
1854 &[
1855 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1856 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1857 ]
1858 );
1859
1860 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1861 // and should move the second cursor back to the first non-whitespace character in the line.
1862 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1863 assert_eq!(
1864 editor.selections.display_ranges(cx),
1865 &[
1866 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1867 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1868 ]
1869 );
1870
1871 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1872 // and to the first non-whitespace character in the line for the second cursor.
1873 editor.move_to_end_of_line(&move_to_end, window, cx);
1874 editor.move_left(&MoveLeft, window, cx);
1875 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1876 assert_eq!(
1877 editor.selections.display_ranges(cx),
1878 &[
1879 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1880 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1881 ]
1882 );
1883
1884 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1885 // and should select to the beginning of the line for the second cursor.
1886 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1887 assert_eq!(
1888 editor.selections.display_ranges(cx),
1889 &[
1890 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1891 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1892 ]
1893 );
1894
1895 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1896 // and should delete to the first non-whitespace character in the line for the second cursor.
1897 editor.move_to_end_of_line(&move_to_end, window, cx);
1898 editor.move_left(&MoveLeft, window, cx);
1899 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1900 assert_eq!(editor.text(cx), "c\n f");
1901 });
1902}
1903
1904#[gpui::test]
1905fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1906 init_test(cx, |_| {});
1907
1908 let editor = cx.add_window(|window, cx| {
1909 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1910 build_editor(buffer, window, cx)
1911 });
1912 _ = editor.update(cx, |editor, window, cx| {
1913 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1914 s.select_display_ranges([
1915 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1916 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1917 ])
1918 });
1919 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1920 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1921
1922 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1923 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1924
1925 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1926 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1927
1928 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1929 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1930
1931 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1932 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
1933
1934 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1935 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1936
1937 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1938 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1939
1940 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1941 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1942
1943 editor.move_right(&MoveRight, window, cx);
1944 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1945 assert_selection_ranges(
1946 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
1947 editor,
1948 cx,
1949 );
1950
1951 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1952 assert_selection_ranges(
1953 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
1954 editor,
1955 cx,
1956 );
1957
1958 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1959 assert_selection_ranges(
1960 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
1961 editor,
1962 cx,
1963 );
1964 });
1965}
1966
1967#[gpui::test]
1968fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1969 init_test(cx, |_| {});
1970
1971 let editor = cx.add_window(|window, cx| {
1972 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1973 build_editor(buffer, window, cx)
1974 });
1975
1976 _ = editor.update(cx, |editor, window, cx| {
1977 editor.set_wrap_width(Some(140.0.into()), cx);
1978 assert_eq!(
1979 editor.display_text(cx),
1980 "use one::{\n two::three::\n four::five\n};"
1981 );
1982
1983 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1984 s.select_display_ranges([
1985 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1986 ]);
1987 });
1988
1989 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1990 assert_eq!(
1991 editor.selections.display_ranges(cx),
1992 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1993 );
1994
1995 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1996 assert_eq!(
1997 editor.selections.display_ranges(cx),
1998 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1999 );
2000
2001 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2002 assert_eq!(
2003 editor.selections.display_ranges(cx),
2004 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2005 );
2006
2007 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2008 assert_eq!(
2009 editor.selections.display_ranges(cx),
2010 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2011 );
2012
2013 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2014 assert_eq!(
2015 editor.selections.display_ranges(cx),
2016 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2017 );
2018
2019 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2020 assert_eq!(
2021 editor.selections.display_ranges(cx),
2022 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2023 );
2024 });
2025}
2026
2027#[gpui::test]
2028async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2029 init_test(cx, |_| {});
2030 let mut cx = EditorTestContext::new(cx).await;
2031
2032 let line_height = cx.editor(|editor, window, _| {
2033 editor
2034 .style()
2035 .unwrap()
2036 .text
2037 .line_height_in_pixels(window.rem_size())
2038 });
2039 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2040
2041 cx.set_state(
2042 &r#"ˇone
2043 two
2044
2045 three
2046 fourˇ
2047 five
2048
2049 six"#
2050 .unindent(),
2051 );
2052
2053 cx.update_editor(|editor, window, cx| {
2054 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2055 });
2056 cx.assert_editor_state(
2057 &r#"one
2058 two
2059 ˇ
2060 three
2061 four
2062 five
2063 ˇ
2064 six"#
2065 .unindent(),
2066 );
2067
2068 cx.update_editor(|editor, window, cx| {
2069 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2070 });
2071 cx.assert_editor_state(
2072 &r#"one
2073 two
2074
2075 three
2076 four
2077 five
2078 ˇ
2079 sixˇ"#
2080 .unindent(),
2081 );
2082
2083 cx.update_editor(|editor, window, cx| {
2084 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2085 });
2086 cx.assert_editor_state(
2087 &r#"one
2088 two
2089
2090 three
2091 four
2092 five
2093
2094 sixˇ"#
2095 .unindent(),
2096 );
2097
2098 cx.update_editor(|editor, window, cx| {
2099 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2100 });
2101 cx.assert_editor_state(
2102 &r#"one
2103 two
2104
2105 three
2106 four
2107 five
2108 ˇ
2109 six"#
2110 .unindent(),
2111 );
2112
2113 cx.update_editor(|editor, window, cx| {
2114 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2115 });
2116 cx.assert_editor_state(
2117 &r#"one
2118 two
2119 ˇ
2120 three
2121 four
2122 five
2123
2124 six"#
2125 .unindent(),
2126 );
2127
2128 cx.update_editor(|editor, window, cx| {
2129 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2130 });
2131 cx.assert_editor_state(
2132 &r#"ˇone
2133 two
2134
2135 three
2136 four
2137 five
2138
2139 six"#
2140 .unindent(),
2141 );
2142}
2143
2144#[gpui::test]
2145async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2146 init_test(cx, |_| {});
2147 let mut cx = EditorTestContext::new(cx).await;
2148 let line_height = cx.editor(|editor, window, _| {
2149 editor
2150 .style()
2151 .unwrap()
2152 .text
2153 .line_height_in_pixels(window.rem_size())
2154 });
2155 let window = cx.window;
2156 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2157
2158 cx.set_state(
2159 r#"ˇone
2160 two
2161 three
2162 four
2163 five
2164 six
2165 seven
2166 eight
2167 nine
2168 ten
2169 "#,
2170 );
2171
2172 cx.update_editor(|editor, window, cx| {
2173 assert_eq!(
2174 editor.snapshot(window, cx).scroll_position(),
2175 gpui::Point::new(0., 0.)
2176 );
2177 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2178 assert_eq!(
2179 editor.snapshot(window, cx).scroll_position(),
2180 gpui::Point::new(0., 3.)
2181 );
2182 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2183 assert_eq!(
2184 editor.snapshot(window, cx).scroll_position(),
2185 gpui::Point::new(0., 6.)
2186 );
2187 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2188 assert_eq!(
2189 editor.snapshot(window, cx).scroll_position(),
2190 gpui::Point::new(0., 3.)
2191 );
2192
2193 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2194 assert_eq!(
2195 editor.snapshot(window, cx).scroll_position(),
2196 gpui::Point::new(0., 1.)
2197 );
2198 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2199 assert_eq!(
2200 editor.snapshot(window, cx).scroll_position(),
2201 gpui::Point::new(0., 3.)
2202 );
2203 });
2204}
2205
2206#[gpui::test]
2207async fn test_autoscroll(cx: &mut TestAppContext) {
2208 init_test(cx, |_| {});
2209 let mut cx = EditorTestContext::new(cx).await;
2210
2211 let line_height = cx.update_editor(|editor, window, cx| {
2212 editor.set_vertical_scroll_margin(2, cx);
2213 editor
2214 .style()
2215 .unwrap()
2216 .text
2217 .line_height_in_pixels(window.rem_size())
2218 });
2219 let window = cx.window;
2220 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2221
2222 cx.set_state(
2223 r#"ˇone
2224 two
2225 three
2226 four
2227 five
2228 six
2229 seven
2230 eight
2231 nine
2232 ten
2233 "#,
2234 );
2235 cx.update_editor(|editor, window, cx| {
2236 assert_eq!(
2237 editor.snapshot(window, cx).scroll_position(),
2238 gpui::Point::new(0., 0.0)
2239 );
2240 });
2241
2242 // Add a cursor below the visible area. Since both cursors cannot fit
2243 // on screen, the editor autoscrolls to reveal the newest cursor, and
2244 // allows the vertical scroll margin below that cursor.
2245 cx.update_editor(|editor, window, cx| {
2246 editor.change_selections(Default::default(), window, cx, |selections| {
2247 selections.select_ranges([
2248 Point::new(0, 0)..Point::new(0, 0),
2249 Point::new(6, 0)..Point::new(6, 0),
2250 ]);
2251 })
2252 });
2253 cx.update_editor(|editor, window, cx| {
2254 assert_eq!(
2255 editor.snapshot(window, cx).scroll_position(),
2256 gpui::Point::new(0., 3.0)
2257 );
2258 });
2259
2260 // Move down. The editor cursor scrolls down to track the newest cursor.
2261 cx.update_editor(|editor, window, cx| {
2262 editor.move_down(&Default::default(), window, cx);
2263 });
2264 cx.update_editor(|editor, window, cx| {
2265 assert_eq!(
2266 editor.snapshot(window, cx).scroll_position(),
2267 gpui::Point::new(0., 4.0)
2268 );
2269 });
2270
2271 // Add a cursor above the visible area. Since both cursors fit on screen,
2272 // the editor scrolls to show both.
2273 cx.update_editor(|editor, window, cx| {
2274 editor.change_selections(Default::default(), window, cx, |selections| {
2275 selections.select_ranges([
2276 Point::new(1, 0)..Point::new(1, 0),
2277 Point::new(6, 0)..Point::new(6, 0),
2278 ]);
2279 })
2280 });
2281 cx.update_editor(|editor, window, cx| {
2282 assert_eq!(
2283 editor.snapshot(window, cx).scroll_position(),
2284 gpui::Point::new(0., 1.0)
2285 );
2286 });
2287}
2288
2289#[gpui::test]
2290async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2291 init_test(cx, |_| {});
2292 let mut cx = EditorTestContext::new(cx).await;
2293
2294 let line_height = cx.editor(|editor, window, _cx| {
2295 editor
2296 .style()
2297 .unwrap()
2298 .text
2299 .line_height_in_pixels(window.rem_size())
2300 });
2301 let window = cx.window;
2302 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2303 cx.set_state(
2304 &r#"
2305 ˇone
2306 two
2307 threeˇ
2308 four
2309 five
2310 six
2311 seven
2312 eight
2313 nine
2314 ten
2315 "#
2316 .unindent(),
2317 );
2318
2319 cx.update_editor(|editor, window, cx| {
2320 editor.move_page_down(&MovePageDown::default(), window, cx)
2321 });
2322 cx.assert_editor_state(
2323 &r#"
2324 one
2325 two
2326 three
2327 ˇfour
2328 five
2329 sixˇ
2330 seven
2331 eight
2332 nine
2333 ten
2334 "#
2335 .unindent(),
2336 );
2337
2338 cx.update_editor(|editor, window, cx| {
2339 editor.move_page_down(&MovePageDown::default(), window, cx)
2340 });
2341 cx.assert_editor_state(
2342 &r#"
2343 one
2344 two
2345 three
2346 four
2347 five
2348 six
2349 ˇseven
2350 eight
2351 nineˇ
2352 ten
2353 "#
2354 .unindent(),
2355 );
2356
2357 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2358 cx.assert_editor_state(
2359 &r#"
2360 one
2361 two
2362 three
2363 ˇfour
2364 five
2365 sixˇ
2366 seven
2367 eight
2368 nine
2369 ten
2370 "#
2371 .unindent(),
2372 );
2373
2374 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2375 cx.assert_editor_state(
2376 &r#"
2377 ˇone
2378 two
2379 threeˇ
2380 four
2381 five
2382 six
2383 seven
2384 eight
2385 nine
2386 ten
2387 "#
2388 .unindent(),
2389 );
2390
2391 // Test select collapsing
2392 cx.update_editor(|editor, window, cx| {
2393 editor.move_page_down(&MovePageDown::default(), window, cx);
2394 editor.move_page_down(&MovePageDown::default(), window, cx);
2395 editor.move_page_down(&MovePageDown::default(), window, cx);
2396 });
2397 cx.assert_editor_state(
2398 &r#"
2399 one
2400 two
2401 three
2402 four
2403 five
2404 six
2405 seven
2406 eight
2407 nine
2408 ˇten
2409 ˇ"#
2410 .unindent(),
2411 );
2412}
2413
2414#[gpui::test]
2415async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2416 init_test(cx, |_| {});
2417 let mut cx = EditorTestContext::new(cx).await;
2418 cx.set_state("one «two threeˇ» four");
2419 cx.update_editor(|editor, window, cx| {
2420 editor.delete_to_beginning_of_line(
2421 &DeleteToBeginningOfLine {
2422 stop_at_indent: false,
2423 },
2424 window,
2425 cx,
2426 );
2427 assert_eq!(editor.text(cx), " four");
2428 });
2429}
2430
2431#[gpui::test]
2432fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2433 init_test(cx, |_| {});
2434
2435 let editor = cx.add_window(|window, cx| {
2436 let buffer = MultiBuffer::build_simple("one two three four", cx);
2437 build_editor(buffer.clone(), window, cx)
2438 });
2439
2440 _ = editor.update(cx, |editor, window, cx| {
2441 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2442 s.select_display_ranges([
2443 // an empty selection - the preceding word fragment is deleted
2444 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2445 // characters selected - they are deleted
2446 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2447 ])
2448 });
2449 editor.delete_to_previous_word_start(
2450 &DeleteToPreviousWordStart {
2451 ignore_newlines: false,
2452 },
2453 window,
2454 cx,
2455 );
2456 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2457 });
2458
2459 _ = editor.update(cx, |editor, window, cx| {
2460 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2461 s.select_display_ranges([
2462 // an empty selection - the following word fragment is deleted
2463 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2464 // characters selected - they are deleted
2465 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2466 ])
2467 });
2468 editor.delete_to_next_word_end(
2469 &DeleteToNextWordEnd {
2470 ignore_newlines: false,
2471 },
2472 window,
2473 cx,
2474 );
2475 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2476 });
2477}
2478
2479#[gpui::test]
2480fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2481 init_test(cx, |_| {});
2482
2483 let editor = cx.add_window(|window, cx| {
2484 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2485 build_editor(buffer.clone(), window, cx)
2486 });
2487 let del_to_prev_word_start = DeleteToPreviousWordStart {
2488 ignore_newlines: false,
2489 };
2490 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2491 ignore_newlines: true,
2492 };
2493
2494 _ = editor.update(cx, |editor, window, cx| {
2495 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2496 s.select_display_ranges([
2497 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2498 ])
2499 });
2500 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2501 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2502 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2503 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2504 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2505 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2506 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2507 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2508 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2509 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2510 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2511 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2512 });
2513}
2514
2515#[gpui::test]
2516fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2517 init_test(cx, |_| {});
2518
2519 let editor = cx.add_window(|window, cx| {
2520 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2521 build_editor(buffer.clone(), window, cx)
2522 });
2523 let del_to_next_word_end = DeleteToNextWordEnd {
2524 ignore_newlines: false,
2525 };
2526 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2527 ignore_newlines: true,
2528 };
2529
2530 _ = editor.update(cx, |editor, window, cx| {
2531 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2532 s.select_display_ranges([
2533 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2534 ])
2535 });
2536 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2537 assert_eq!(
2538 editor.buffer.read(cx).read(cx).text(),
2539 "one\n two\nthree\n four"
2540 );
2541 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2542 assert_eq!(
2543 editor.buffer.read(cx).read(cx).text(),
2544 "\n two\nthree\n four"
2545 );
2546 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2547 assert_eq!(
2548 editor.buffer.read(cx).read(cx).text(),
2549 "two\nthree\n four"
2550 );
2551 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2552 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2553 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2554 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2555 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2556 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2557 });
2558}
2559
2560#[gpui::test]
2561fn test_newline(cx: &mut TestAppContext) {
2562 init_test(cx, |_| {});
2563
2564 let editor = cx.add_window(|window, cx| {
2565 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2566 build_editor(buffer.clone(), window, cx)
2567 });
2568
2569 _ = editor.update(cx, |editor, window, cx| {
2570 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2571 s.select_display_ranges([
2572 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2573 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2574 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2575 ])
2576 });
2577
2578 editor.newline(&Newline, window, cx);
2579 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2580 });
2581}
2582
2583#[gpui::test]
2584fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2585 init_test(cx, |_| {});
2586
2587 let editor = cx.add_window(|window, cx| {
2588 let buffer = MultiBuffer::build_simple(
2589 "
2590 a
2591 b(
2592 X
2593 )
2594 c(
2595 X
2596 )
2597 "
2598 .unindent()
2599 .as_str(),
2600 cx,
2601 );
2602 let mut editor = build_editor(buffer.clone(), window, cx);
2603 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2604 s.select_ranges([
2605 Point::new(2, 4)..Point::new(2, 5),
2606 Point::new(5, 4)..Point::new(5, 5),
2607 ])
2608 });
2609 editor
2610 });
2611
2612 _ = editor.update(cx, |editor, window, cx| {
2613 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2614 editor.buffer.update(cx, |buffer, cx| {
2615 buffer.edit(
2616 [
2617 (Point::new(1, 2)..Point::new(3, 0), ""),
2618 (Point::new(4, 2)..Point::new(6, 0), ""),
2619 ],
2620 None,
2621 cx,
2622 );
2623 assert_eq!(
2624 buffer.read(cx).text(),
2625 "
2626 a
2627 b()
2628 c()
2629 "
2630 .unindent()
2631 );
2632 });
2633 assert_eq!(
2634 editor.selections.ranges(cx),
2635 &[
2636 Point::new(1, 2)..Point::new(1, 2),
2637 Point::new(2, 2)..Point::new(2, 2),
2638 ],
2639 );
2640
2641 editor.newline(&Newline, window, cx);
2642 assert_eq!(
2643 editor.text(cx),
2644 "
2645 a
2646 b(
2647 )
2648 c(
2649 )
2650 "
2651 .unindent()
2652 );
2653
2654 // The selections are moved after the inserted newlines
2655 assert_eq!(
2656 editor.selections.ranges(cx),
2657 &[
2658 Point::new(2, 0)..Point::new(2, 0),
2659 Point::new(4, 0)..Point::new(4, 0),
2660 ],
2661 );
2662 });
2663}
2664
2665#[gpui::test]
2666async fn test_newline_above(cx: &mut TestAppContext) {
2667 init_test(cx, |settings| {
2668 settings.defaults.tab_size = NonZeroU32::new(4)
2669 });
2670
2671 let language = Arc::new(
2672 Language::new(
2673 LanguageConfig::default(),
2674 Some(tree_sitter_rust::LANGUAGE.into()),
2675 )
2676 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2677 .unwrap(),
2678 );
2679
2680 let mut cx = EditorTestContext::new(cx).await;
2681 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2682 cx.set_state(indoc! {"
2683 const a: ˇA = (
2684 (ˇ
2685 «const_functionˇ»(ˇ),
2686 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2687 )ˇ
2688 ˇ);ˇ
2689 "});
2690
2691 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2692 cx.assert_editor_state(indoc! {"
2693 ˇ
2694 const a: A = (
2695 ˇ
2696 (
2697 ˇ
2698 ˇ
2699 const_function(),
2700 ˇ
2701 ˇ
2702 ˇ
2703 ˇ
2704 something_else,
2705 ˇ
2706 )
2707 ˇ
2708 ˇ
2709 );
2710 "});
2711}
2712
2713#[gpui::test]
2714async fn test_newline_below(cx: &mut TestAppContext) {
2715 init_test(cx, |settings| {
2716 settings.defaults.tab_size = NonZeroU32::new(4)
2717 });
2718
2719 let language = Arc::new(
2720 Language::new(
2721 LanguageConfig::default(),
2722 Some(tree_sitter_rust::LANGUAGE.into()),
2723 )
2724 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2725 .unwrap(),
2726 );
2727
2728 let mut cx = EditorTestContext::new(cx).await;
2729 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2730 cx.set_state(indoc! {"
2731 const a: ˇA = (
2732 (ˇ
2733 «const_functionˇ»(ˇ),
2734 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2735 )ˇ
2736 ˇ);ˇ
2737 "});
2738
2739 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2740 cx.assert_editor_state(indoc! {"
2741 const a: A = (
2742 ˇ
2743 (
2744 ˇ
2745 const_function(),
2746 ˇ
2747 ˇ
2748 something_else,
2749 ˇ
2750 ˇ
2751 ˇ
2752 ˇ
2753 )
2754 ˇ
2755 );
2756 ˇ
2757 ˇ
2758 "});
2759}
2760
2761#[gpui::test]
2762async fn test_newline_comments(cx: &mut TestAppContext) {
2763 init_test(cx, |settings| {
2764 settings.defaults.tab_size = NonZeroU32::new(4)
2765 });
2766
2767 let language = Arc::new(Language::new(
2768 LanguageConfig {
2769 line_comments: vec!["// ".into()],
2770 ..LanguageConfig::default()
2771 },
2772 None,
2773 ));
2774 {
2775 let mut cx = EditorTestContext::new(cx).await;
2776 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2777 cx.set_state(indoc! {"
2778 // Fooˇ
2779 "});
2780
2781 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2782 cx.assert_editor_state(indoc! {"
2783 // Foo
2784 // ˇ
2785 "});
2786 // Ensure that we add comment prefix when existing line contains space
2787 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2788 cx.assert_editor_state(
2789 indoc! {"
2790 // Foo
2791 //s
2792 // ˇ
2793 "}
2794 .replace("s", " ") // s is used as space placeholder to prevent format on save
2795 .as_str(),
2796 );
2797 // Ensure that we add comment prefix when existing line does not contain space
2798 cx.set_state(indoc! {"
2799 // Foo
2800 //ˇ
2801 "});
2802 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2803 cx.assert_editor_state(indoc! {"
2804 // Foo
2805 //
2806 // ˇ
2807 "});
2808 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2809 cx.set_state(indoc! {"
2810 ˇ// Foo
2811 "});
2812 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2813 cx.assert_editor_state(indoc! {"
2814
2815 ˇ// Foo
2816 "});
2817 }
2818 // Ensure that comment continuations can be disabled.
2819 update_test_language_settings(cx, |settings| {
2820 settings.defaults.extend_comment_on_newline = Some(false);
2821 });
2822 let mut cx = EditorTestContext::new(cx).await;
2823 cx.set_state(indoc! {"
2824 // Fooˇ
2825 "});
2826 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2827 cx.assert_editor_state(indoc! {"
2828 // Foo
2829 ˇ
2830 "});
2831}
2832
2833#[gpui::test]
2834async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
2835 init_test(cx, |settings| {
2836 settings.defaults.tab_size = NonZeroU32::new(4)
2837 });
2838
2839 let language = Arc::new(Language::new(
2840 LanguageConfig {
2841 line_comments: vec!["// ".into(), "/// ".into()],
2842 ..LanguageConfig::default()
2843 },
2844 None,
2845 ));
2846 {
2847 let mut cx = EditorTestContext::new(cx).await;
2848 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2849 cx.set_state(indoc! {"
2850 //ˇ
2851 "});
2852 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2853 cx.assert_editor_state(indoc! {"
2854 //
2855 // ˇ
2856 "});
2857
2858 cx.set_state(indoc! {"
2859 ///ˇ
2860 "});
2861 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2862 cx.assert_editor_state(indoc! {"
2863 ///
2864 /// ˇ
2865 "});
2866 }
2867}
2868
2869#[gpui::test]
2870async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
2871 init_test(cx, |settings| {
2872 settings.defaults.tab_size = NonZeroU32::new(4)
2873 });
2874
2875 let language = Arc::new(
2876 Language::new(
2877 LanguageConfig {
2878 documentation: Some(language::DocumentationConfig {
2879 start: "/**".into(),
2880 end: "*/".into(),
2881 prefix: "* ".into(),
2882 tab_size: NonZeroU32::new(1).unwrap(),
2883 }),
2884
2885 ..LanguageConfig::default()
2886 },
2887 Some(tree_sitter_rust::LANGUAGE.into()),
2888 )
2889 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
2890 .unwrap(),
2891 );
2892
2893 {
2894 let mut cx = EditorTestContext::new(cx).await;
2895 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2896 cx.set_state(indoc! {"
2897 /**ˇ
2898 "});
2899
2900 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2901 cx.assert_editor_state(indoc! {"
2902 /**
2903 * ˇ
2904 "});
2905 // Ensure that if cursor is before the comment start,
2906 // we do not actually insert a comment prefix.
2907 cx.set_state(indoc! {"
2908 ˇ/**
2909 "});
2910 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2911 cx.assert_editor_state(indoc! {"
2912
2913 ˇ/**
2914 "});
2915 // Ensure that if cursor is between it doesn't add comment prefix.
2916 cx.set_state(indoc! {"
2917 /*ˇ*
2918 "});
2919 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2920 cx.assert_editor_state(indoc! {"
2921 /*
2922 ˇ*
2923 "});
2924 // Ensure that if suffix exists on same line after cursor it adds new line.
2925 cx.set_state(indoc! {"
2926 /**ˇ*/
2927 "});
2928 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2929 cx.assert_editor_state(indoc! {"
2930 /**
2931 * ˇ
2932 */
2933 "});
2934 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2935 cx.set_state(indoc! {"
2936 /**ˇ */
2937 "});
2938 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2939 cx.assert_editor_state(indoc! {"
2940 /**
2941 * ˇ
2942 */
2943 "});
2944 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2945 cx.set_state(indoc! {"
2946 /** ˇ*/
2947 "});
2948 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2949 cx.assert_editor_state(
2950 indoc! {"
2951 /**s
2952 * ˇ
2953 */
2954 "}
2955 .replace("s", " ") // s is used as space placeholder to prevent format on save
2956 .as_str(),
2957 );
2958 // Ensure that delimiter space is preserved when newline on already
2959 // spaced delimiter.
2960 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2961 cx.assert_editor_state(
2962 indoc! {"
2963 /**s
2964 *s
2965 * ˇ
2966 */
2967 "}
2968 .replace("s", " ") // s is used as space placeholder to prevent format on save
2969 .as_str(),
2970 );
2971 // Ensure that delimiter space is preserved when space is not
2972 // on existing delimiter.
2973 cx.set_state(indoc! {"
2974 /**
2975 *ˇ
2976 */
2977 "});
2978 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2979 cx.assert_editor_state(indoc! {"
2980 /**
2981 *
2982 * ˇ
2983 */
2984 "});
2985 // Ensure that if suffix exists on same line after cursor it
2986 // doesn't add extra new line if prefix is not on same line.
2987 cx.set_state(indoc! {"
2988 /**
2989 ˇ*/
2990 "});
2991 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2992 cx.assert_editor_state(indoc! {"
2993 /**
2994
2995 ˇ*/
2996 "});
2997 // Ensure that it detects suffix after existing prefix.
2998 cx.set_state(indoc! {"
2999 /**ˇ/
3000 "});
3001 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3002 cx.assert_editor_state(indoc! {"
3003 /**
3004 ˇ/
3005 "});
3006 // Ensure that if suffix exists on same line before
3007 // cursor it does not add comment prefix.
3008 cx.set_state(indoc! {"
3009 /** */ˇ
3010 "});
3011 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3012 cx.assert_editor_state(indoc! {"
3013 /** */
3014 ˇ
3015 "});
3016 // Ensure that if suffix exists on same line before
3017 // cursor it does not add comment prefix.
3018 cx.set_state(indoc! {"
3019 /**
3020 *
3021 */ˇ
3022 "});
3023 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3024 cx.assert_editor_state(indoc! {"
3025 /**
3026 *
3027 */
3028 ˇ
3029 "});
3030
3031 // Ensure that inline comment followed by code
3032 // doesn't add comment prefix on newline
3033 cx.set_state(indoc! {"
3034 /** */ textˇ
3035 "});
3036 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3037 cx.assert_editor_state(indoc! {"
3038 /** */ text
3039 ˇ
3040 "});
3041
3042 // Ensure that text after comment end tag
3043 // doesn't add comment prefix on newline
3044 cx.set_state(indoc! {"
3045 /**
3046 *
3047 */ˇtext
3048 "});
3049 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3050 cx.assert_editor_state(indoc! {"
3051 /**
3052 *
3053 */
3054 ˇtext
3055 "});
3056
3057 // Ensure if not comment block it doesn't
3058 // add comment prefix on newline
3059 cx.set_state(indoc! {"
3060 * textˇ
3061 "});
3062 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3063 cx.assert_editor_state(indoc! {"
3064 * text
3065 ˇ
3066 "});
3067 }
3068 // Ensure that comment continuations can be disabled.
3069 update_test_language_settings(cx, |settings| {
3070 settings.defaults.extend_comment_on_newline = Some(false);
3071 });
3072 let mut cx = EditorTestContext::new(cx).await;
3073 cx.set_state(indoc! {"
3074 /**ˇ
3075 "});
3076 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3077 cx.assert_editor_state(indoc! {"
3078 /**
3079 ˇ
3080 "});
3081}
3082
3083#[gpui::test]
3084fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3085 init_test(cx, |_| {});
3086
3087 let editor = cx.add_window(|window, cx| {
3088 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3089 let mut editor = build_editor(buffer.clone(), window, cx);
3090 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3091 s.select_ranges([3..4, 11..12, 19..20])
3092 });
3093 editor
3094 });
3095
3096 _ = editor.update(cx, |editor, window, cx| {
3097 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3098 editor.buffer.update(cx, |buffer, cx| {
3099 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3100 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3101 });
3102 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3103
3104 editor.insert("Z", window, cx);
3105 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3106
3107 // The selections are moved after the inserted characters
3108 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3109 });
3110}
3111
3112#[gpui::test]
3113async fn test_tab(cx: &mut TestAppContext) {
3114 init_test(cx, |settings| {
3115 settings.defaults.tab_size = NonZeroU32::new(3)
3116 });
3117
3118 let mut cx = EditorTestContext::new(cx).await;
3119 cx.set_state(indoc! {"
3120 ˇabˇc
3121 ˇ🏀ˇ🏀ˇefg
3122 dˇ
3123 "});
3124 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3125 cx.assert_editor_state(indoc! {"
3126 ˇab ˇc
3127 ˇ🏀 ˇ🏀 ˇefg
3128 d ˇ
3129 "});
3130
3131 cx.set_state(indoc! {"
3132 a
3133 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3134 "});
3135 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3136 cx.assert_editor_state(indoc! {"
3137 a
3138 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3139 "});
3140}
3141
3142#[gpui::test]
3143async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3144 init_test(cx, |_| {});
3145
3146 let mut cx = EditorTestContext::new(cx).await;
3147 let language = Arc::new(
3148 Language::new(
3149 LanguageConfig::default(),
3150 Some(tree_sitter_rust::LANGUAGE.into()),
3151 )
3152 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3153 .unwrap(),
3154 );
3155 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3156
3157 // test when all cursors are not at suggested indent
3158 // then simply move to their suggested indent location
3159 cx.set_state(indoc! {"
3160 const a: B = (
3161 c(
3162 ˇ
3163 ˇ )
3164 );
3165 "});
3166 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3167 cx.assert_editor_state(indoc! {"
3168 const a: B = (
3169 c(
3170 ˇ
3171 ˇ)
3172 );
3173 "});
3174
3175 // test cursor already at suggested indent not moving when
3176 // other cursors are yet to reach their suggested indents
3177 cx.set_state(indoc! {"
3178 ˇ
3179 const a: B = (
3180 c(
3181 d(
3182 ˇ
3183 )
3184 ˇ
3185 ˇ )
3186 );
3187 "});
3188 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3189 cx.assert_editor_state(indoc! {"
3190 ˇ
3191 const a: B = (
3192 c(
3193 d(
3194 ˇ
3195 )
3196 ˇ
3197 ˇ)
3198 );
3199 "});
3200 // test when all cursors are at suggested indent then tab is inserted
3201 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3202 cx.assert_editor_state(indoc! {"
3203 ˇ
3204 const a: B = (
3205 c(
3206 d(
3207 ˇ
3208 )
3209 ˇ
3210 ˇ)
3211 );
3212 "});
3213
3214 // test when current indent is less than suggested indent,
3215 // we adjust line to match suggested indent and move cursor to it
3216 //
3217 // when no other cursor is at word boundary, all of them should move
3218 cx.set_state(indoc! {"
3219 const a: B = (
3220 c(
3221 d(
3222 ˇ
3223 ˇ )
3224 ˇ )
3225 );
3226 "});
3227 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3228 cx.assert_editor_state(indoc! {"
3229 const a: B = (
3230 c(
3231 d(
3232 ˇ
3233 ˇ)
3234 ˇ)
3235 );
3236 "});
3237
3238 // test when current indent is less than suggested indent,
3239 // we adjust line to match suggested indent and move cursor to it
3240 //
3241 // when some other cursor is at word boundary, it should not move
3242 cx.set_state(indoc! {"
3243 const a: B = (
3244 c(
3245 d(
3246 ˇ
3247 ˇ )
3248 ˇ)
3249 );
3250 "});
3251 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3252 cx.assert_editor_state(indoc! {"
3253 const a: B = (
3254 c(
3255 d(
3256 ˇ
3257 ˇ)
3258 ˇ)
3259 );
3260 "});
3261
3262 // test when current indent is more than suggested indent,
3263 // we just move cursor to current indent instead of suggested indent
3264 //
3265 // when no other cursor is at word boundary, all of them should move
3266 cx.set_state(indoc! {"
3267 const a: B = (
3268 c(
3269 d(
3270 ˇ
3271 ˇ )
3272 ˇ )
3273 );
3274 "});
3275 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3276 cx.assert_editor_state(indoc! {"
3277 const a: B = (
3278 c(
3279 d(
3280 ˇ
3281 ˇ)
3282 ˇ)
3283 );
3284 "});
3285 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3286 cx.assert_editor_state(indoc! {"
3287 const a: B = (
3288 c(
3289 d(
3290 ˇ
3291 ˇ)
3292 ˇ)
3293 );
3294 "});
3295
3296 // test when current indent is more than suggested indent,
3297 // we just move cursor to current indent instead of suggested indent
3298 //
3299 // when some other cursor is at word boundary, it doesn't move
3300 cx.set_state(indoc! {"
3301 const a: B = (
3302 c(
3303 d(
3304 ˇ
3305 ˇ )
3306 ˇ)
3307 );
3308 "});
3309 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3310 cx.assert_editor_state(indoc! {"
3311 const a: B = (
3312 c(
3313 d(
3314 ˇ
3315 ˇ)
3316 ˇ)
3317 );
3318 "});
3319
3320 // handle auto-indent when there are multiple cursors on the same line
3321 cx.set_state(indoc! {"
3322 const a: B = (
3323 c(
3324 ˇ ˇ
3325 ˇ )
3326 );
3327 "});
3328 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3329 cx.assert_editor_state(indoc! {"
3330 const a: B = (
3331 c(
3332 ˇ
3333 ˇ)
3334 );
3335 "});
3336}
3337
3338#[gpui::test]
3339async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3340 init_test(cx, |settings| {
3341 settings.defaults.tab_size = NonZeroU32::new(3)
3342 });
3343
3344 let mut cx = EditorTestContext::new(cx).await;
3345 cx.set_state(indoc! {"
3346 ˇ
3347 \t ˇ
3348 \t ˇ
3349 \t ˇ
3350 \t \t\t \t \t\t \t\t \t \t ˇ
3351 "});
3352
3353 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3354 cx.assert_editor_state(indoc! {"
3355 ˇ
3356 \t ˇ
3357 \t ˇ
3358 \t ˇ
3359 \t \t\t \t \t\t \t\t \t \t ˇ
3360 "});
3361}
3362
3363#[gpui::test]
3364async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3365 init_test(cx, |settings| {
3366 settings.defaults.tab_size = NonZeroU32::new(4)
3367 });
3368
3369 let language = Arc::new(
3370 Language::new(
3371 LanguageConfig::default(),
3372 Some(tree_sitter_rust::LANGUAGE.into()),
3373 )
3374 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3375 .unwrap(),
3376 );
3377
3378 let mut cx = EditorTestContext::new(cx).await;
3379 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3380 cx.set_state(indoc! {"
3381 fn a() {
3382 if b {
3383 \t ˇc
3384 }
3385 }
3386 "});
3387
3388 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3389 cx.assert_editor_state(indoc! {"
3390 fn a() {
3391 if b {
3392 ˇc
3393 }
3394 }
3395 "});
3396}
3397
3398#[gpui::test]
3399async fn test_indent_outdent(cx: &mut TestAppContext) {
3400 init_test(cx, |settings| {
3401 settings.defaults.tab_size = NonZeroU32::new(4);
3402 });
3403
3404 let mut cx = EditorTestContext::new(cx).await;
3405
3406 cx.set_state(indoc! {"
3407 «oneˇ» «twoˇ»
3408 three
3409 four
3410 "});
3411 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3412 cx.assert_editor_state(indoc! {"
3413 «oneˇ» «twoˇ»
3414 three
3415 four
3416 "});
3417
3418 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3419 cx.assert_editor_state(indoc! {"
3420 «oneˇ» «twoˇ»
3421 three
3422 four
3423 "});
3424
3425 // select across line ending
3426 cx.set_state(indoc! {"
3427 one two
3428 t«hree
3429 ˇ» four
3430 "});
3431 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3432 cx.assert_editor_state(indoc! {"
3433 one two
3434 t«hree
3435 ˇ» four
3436 "});
3437
3438 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3439 cx.assert_editor_state(indoc! {"
3440 one two
3441 t«hree
3442 ˇ» four
3443 "});
3444
3445 // Ensure that indenting/outdenting works when the cursor is at column 0.
3446 cx.set_state(indoc! {"
3447 one two
3448 ˇthree
3449 four
3450 "});
3451 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3452 cx.assert_editor_state(indoc! {"
3453 one two
3454 ˇthree
3455 four
3456 "});
3457
3458 cx.set_state(indoc! {"
3459 one two
3460 ˇ three
3461 four
3462 "});
3463 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3464 cx.assert_editor_state(indoc! {"
3465 one two
3466 ˇthree
3467 four
3468 "});
3469}
3470
3471#[gpui::test]
3472async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3473 // This is a regression test for issue #33761
3474 init_test(cx, |_| {});
3475
3476 let mut cx = EditorTestContext::new(cx).await;
3477 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3478 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3479
3480 cx.set_state(
3481 r#"ˇ# ingress:
3482ˇ# api:
3483ˇ# enabled: false
3484ˇ# pathType: Prefix
3485ˇ# console:
3486ˇ# enabled: false
3487ˇ# pathType: Prefix
3488"#,
3489 );
3490
3491 // Press tab to indent all lines
3492 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3493
3494 cx.assert_editor_state(
3495 r#" ˇ# ingress:
3496 ˇ# api:
3497 ˇ# enabled: false
3498 ˇ# pathType: Prefix
3499 ˇ# console:
3500 ˇ# enabled: false
3501 ˇ# pathType: Prefix
3502"#,
3503 );
3504}
3505
3506#[gpui::test]
3507async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3508 // This is a test to make sure our fix for issue #33761 didn't break anything
3509 init_test(cx, |_| {});
3510
3511 let mut cx = EditorTestContext::new(cx).await;
3512 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3513 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3514
3515 cx.set_state(
3516 r#"ˇingress:
3517ˇ api:
3518ˇ enabled: false
3519ˇ pathType: Prefix
3520"#,
3521 );
3522
3523 // Press tab to indent all lines
3524 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3525
3526 cx.assert_editor_state(
3527 r#"ˇingress:
3528 ˇapi:
3529 ˇenabled: false
3530 ˇpathType: Prefix
3531"#,
3532 );
3533}
3534
3535#[gpui::test]
3536async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3537 init_test(cx, |settings| {
3538 settings.defaults.hard_tabs = Some(true);
3539 });
3540
3541 let mut cx = EditorTestContext::new(cx).await;
3542
3543 // select two ranges on one line
3544 cx.set_state(indoc! {"
3545 «oneˇ» «twoˇ»
3546 three
3547 four
3548 "});
3549 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3550 cx.assert_editor_state(indoc! {"
3551 \t«oneˇ» «twoˇ»
3552 three
3553 four
3554 "});
3555 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3556 cx.assert_editor_state(indoc! {"
3557 \t\t«oneˇ» «twoˇ»
3558 three
3559 four
3560 "});
3561 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3562 cx.assert_editor_state(indoc! {"
3563 \t«oneˇ» «twoˇ»
3564 three
3565 four
3566 "});
3567 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3568 cx.assert_editor_state(indoc! {"
3569 «oneˇ» «twoˇ»
3570 three
3571 four
3572 "});
3573
3574 // select across a line ending
3575 cx.set_state(indoc! {"
3576 one two
3577 t«hree
3578 ˇ»four
3579 "});
3580 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3581 cx.assert_editor_state(indoc! {"
3582 one two
3583 \tt«hree
3584 ˇ»four
3585 "});
3586 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3587 cx.assert_editor_state(indoc! {"
3588 one two
3589 \t\tt«hree
3590 ˇ»four
3591 "});
3592 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3593 cx.assert_editor_state(indoc! {"
3594 one two
3595 \tt«hree
3596 ˇ»four
3597 "});
3598 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3599 cx.assert_editor_state(indoc! {"
3600 one two
3601 t«hree
3602 ˇ»four
3603 "});
3604
3605 // Ensure that indenting/outdenting works when the cursor is at column 0.
3606 cx.set_state(indoc! {"
3607 one two
3608 ˇthree
3609 four
3610 "});
3611 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3612 cx.assert_editor_state(indoc! {"
3613 one two
3614 ˇthree
3615 four
3616 "});
3617 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3618 cx.assert_editor_state(indoc! {"
3619 one two
3620 \tˇthree
3621 four
3622 "});
3623 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3624 cx.assert_editor_state(indoc! {"
3625 one two
3626 ˇthree
3627 four
3628 "});
3629}
3630
3631#[gpui::test]
3632fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3633 init_test(cx, |settings| {
3634 settings.languages.0.extend([
3635 (
3636 "TOML".into(),
3637 LanguageSettingsContent {
3638 tab_size: NonZeroU32::new(2),
3639 ..Default::default()
3640 },
3641 ),
3642 (
3643 "Rust".into(),
3644 LanguageSettingsContent {
3645 tab_size: NonZeroU32::new(4),
3646 ..Default::default()
3647 },
3648 ),
3649 ]);
3650 });
3651
3652 let toml_language = Arc::new(Language::new(
3653 LanguageConfig {
3654 name: "TOML".into(),
3655 ..Default::default()
3656 },
3657 None,
3658 ));
3659 let rust_language = Arc::new(Language::new(
3660 LanguageConfig {
3661 name: "Rust".into(),
3662 ..Default::default()
3663 },
3664 None,
3665 ));
3666
3667 let toml_buffer =
3668 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3669 let rust_buffer =
3670 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3671 let multibuffer = cx.new(|cx| {
3672 let mut multibuffer = MultiBuffer::new(ReadWrite);
3673 multibuffer.push_excerpts(
3674 toml_buffer.clone(),
3675 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3676 cx,
3677 );
3678 multibuffer.push_excerpts(
3679 rust_buffer.clone(),
3680 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3681 cx,
3682 );
3683 multibuffer
3684 });
3685
3686 cx.add_window(|window, cx| {
3687 let mut editor = build_editor(multibuffer, window, cx);
3688
3689 assert_eq!(
3690 editor.text(cx),
3691 indoc! {"
3692 a = 1
3693 b = 2
3694
3695 const c: usize = 3;
3696 "}
3697 );
3698
3699 select_ranges(
3700 &mut editor,
3701 indoc! {"
3702 «aˇ» = 1
3703 b = 2
3704
3705 «const c:ˇ» usize = 3;
3706 "},
3707 window,
3708 cx,
3709 );
3710
3711 editor.tab(&Tab, window, cx);
3712 assert_text_with_selections(
3713 &mut editor,
3714 indoc! {"
3715 «aˇ» = 1
3716 b = 2
3717
3718 «const c:ˇ» usize = 3;
3719 "},
3720 cx,
3721 );
3722 editor.backtab(&Backtab, window, cx);
3723 assert_text_with_selections(
3724 &mut editor,
3725 indoc! {"
3726 «aˇ» = 1
3727 b = 2
3728
3729 «const c:ˇ» usize = 3;
3730 "},
3731 cx,
3732 );
3733
3734 editor
3735 });
3736}
3737
3738#[gpui::test]
3739async fn test_backspace(cx: &mut TestAppContext) {
3740 init_test(cx, |_| {});
3741
3742 let mut cx = EditorTestContext::new(cx).await;
3743
3744 // Basic backspace
3745 cx.set_state(indoc! {"
3746 onˇe two three
3747 fou«rˇ» five six
3748 seven «ˇeight nine
3749 »ten
3750 "});
3751 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3752 cx.assert_editor_state(indoc! {"
3753 oˇe two three
3754 fouˇ five six
3755 seven ˇten
3756 "});
3757
3758 // Test backspace inside and around indents
3759 cx.set_state(indoc! {"
3760 zero
3761 ˇone
3762 ˇtwo
3763 ˇ ˇ ˇ three
3764 ˇ ˇ four
3765 "});
3766 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3767 cx.assert_editor_state(indoc! {"
3768 zero
3769 ˇone
3770 ˇtwo
3771 ˇ threeˇ four
3772 "});
3773}
3774
3775#[gpui::test]
3776async fn test_delete(cx: &mut TestAppContext) {
3777 init_test(cx, |_| {});
3778
3779 let mut cx = EditorTestContext::new(cx).await;
3780 cx.set_state(indoc! {"
3781 onˇe two three
3782 fou«rˇ» five six
3783 seven «ˇeight nine
3784 »ten
3785 "});
3786 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3787 cx.assert_editor_state(indoc! {"
3788 onˇ two three
3789 fouˇ five six
3790 seven ˇten
3791 "});
3792}
3793
3794#[gpui::test]
3795fn test_delete_line(cx: &mut TestAppContext) {
3796 init_test(cx, |_| {});
3797
3798 let editor = cx.add_window(|window, cx| {
3799 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3800 build_editor(buffer, window, cx)
3801 });
3802 _ = editor.update(cx, |editor, window, cx| {
3803 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3804 s.select_display_ranges([
3805 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3806 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3807 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3808 ])
3809 });
3810 editor.delete_line(&DeleteLine, window, cx);
3811 assert_eq!(editor.display_text(cx), "ghi");
3812 assert_eq!(
3813 editor.selections.display_ranges(cx),
3814 vec![
3815 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3816 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3817 ]
3818 );
3819 });
3820
3821 let editor = cx.add_window(|window, cx| {
3822 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3823 build_editor(buffer, window, cx)
3824 });
3825 _ = editor.update(cx, |editor, window, cx| {
3826 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3827 s.select_display_ranges([
3828 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3829 ])
3830 });
3831 editor.delete_line(&DeleteLine, window, cx);
3832 assert_eq!(editor.display_text(cx), "ghi\n");
3833 assert_eq!(
3834 editor.selections.display_ranges(cx),
3835 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3836 );
3837 });
3838}
3839
3840#[gpui::test]
3841fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3842 init_test(cx, |_| {});
3843
3844 cx.add_window(|window, cx| {
3845 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3846 let mut editor = build_editor(buffer.clone(), window, cx);
3847 let buffer = buffer.read(cx).as_singleton().unwrap();
3848
3849 assert_eq!(
3850 editor.selections.ranges::<Point>(cx),
3851 &[Point::new(0, 0)..Point::new(0, 0)]
3852 );
3853
3854 // When on single line, replace newline at end by space
3855 editor.join_lines(&JoinLines, window, cx);
3856 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3857 assert_eq!(
3858 editor.selections.ranges::<Point>(cx),
3859 &[Point::new(0, 3)..Point::new(0, 3)]
3860 );
3861
3862 // When multiple lines are selected, remove newlines that are spanned by the selection
3863 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3864 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3865 });
3866 editor.join_lines(&JoinLines, window, cx);
3867 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3868 assert_eq!(
3869 editor.selections.ranges::<Point>(cx),
3870 &[Point::new(0, 11)..Point::new(0, 11)]
3871 );
3872
3873 // Undo should be transactional
3874 editor.undo(&Undo, window, cx);
3875 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3876 assert_eq!(
3877 editor.selections.ranges::<Point>(cx),
3878 &[Point::new(0, 5)..Point::new(2, 2)]
3879 );
3880
3881 // When joining an empty line don't insert a space
3882 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3883 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3884 });
3885 editor.join_lines(&JoinLines, window, cx);
3886 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3887 assert_eq!(
3888 editor.selections.ranges::<Point>(cx),
3889 [Point::new(2, 3)..Point::new(2, 3)]
3890 );
3891
3892 // We can remove trailing newlines
3893 editor.join_lines(&JoinLines, window, cx);
3894 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3895 assert_eq!(
3896 editor.selections.ranges::<Point>(cx),
3897 [Point::new(2, 3)..Point::new(2, 3)]
3898 );
3899
3900 // We don't blow up on the last line
3901 editor.join_lines(&JoinLines, window, cx);
3902 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3903 assert_eq!(
3904 editor.selections.ranges::<Point>(cx),
3905 [Point::new(2, 3)..Point::new(2, 3)]
3906 );
3907
3908 // reset to test indentation
3909 editor.buffer.update(cx, |buffer, cx| {
3910 buffer.edit(
3911 [
3912 (Point::new(1, 0)..Point::new(1, 2), " "),
3913 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3914 ],
3915 None,
3916 cx,
3917 )
3918 });
3919
3920 // We remove any leading spaces
3921 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3922 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3923 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3924 });
3925 editor.join_lines(&JoinLines, window, cx);
3926 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3927
3928 // We don't insert a space for a line containing only spaces
3929 editor.join_lines(&JoinLines, window, cx);
3930 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3931
3932 // We ignore any leading tabs
3933 editor.join_lines(&JoinLines, window, cx);
3934 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3935
3936 editor
3937 });
3938}
3939
3940#[gpui::test]
3941fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3942 init_test(cx, |_| {});
3943
3944 cx.add_window(|window, cx| {
3945 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3946 let mut editor = build_editor(buffer.clone(), window, cx);
3947 let buffer = buffer.read(cx).as_singleton().unwrap();
3948
3949 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3950 s.select_ranges([
3951 Point::new(0, 2)..Point::new(1, 1),
3952 Point::new(1, 2)..Point::new(1, 2),
3953 Point::new(3, 1)..Point::new(3, 2),
3954 ])
3955 });
3956
3957 editor.join_lines(&JoinLines, window, cx);
3958 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3959
3960 assert_eq!(
3961 editor.selections.ranges::<Point>(cx),
3962 [
3963 Point::new(0, 7)..Point::new(0, 7),
3964 Point::new(1, 3)..Point::new(1, 3)
3965 ]
3966 );
3967 editor
3968 });
3969}
3970
3971#[gpui::test]
3972async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3973 init_test(cx, |_| {});
3974
3975 let mut cx = EditorTestContext::new(cx).await;
3976
3977 let diff_base = r#"
3978 Line 0
3979 Line 1
3980 Line 2
3981 Line 3
3982 "#
3983 .unindent();
3984
3985 cx.set_state(
3986 &r#"
3987 ˇLine 0
3988 Line 1
3989 Line 2
3990 Line 3
3991 "#
3992 .unindent(),
3993 );
3994
3995 cx.set_head_text(&diff_base);
3996 executor.run_until_parked();
3997
3998 // Join lines
3999 cx.update_editor(|editor, window, cx| {
4000 editor.join_lines(&JoinLines, window, cx);
4001 });
4002 executor.run_until_parked();
4003
4004 cx.assert_editor_state(
4005 &r#"
4006 Line 0ˇ Line 1
4007 Line 2
4008 Line 3
4009 "#
4010 .unindent(),
4011 );
4012 // Join again
4013 cx.update_editor(|editor, window, cx| {
4014 editor.join_lines(&JoinLines, window, cx);
4015 });
4016 executor.run_until_parked();
4017
4018 cx.assert_editor_state(
4019 &r#"
4020 Line 0 Line 1ˇ Line 2
4021 Line 3
4022 "#
4023 .unindent(),
4024 );
4025}
4026
4027#[gpui::test]
4028async fn test_custom_newlines_cause_no_false_positive_diffs(
4029 executor: BackgroundExecutor,
4030 cx: &mut TestAppContext,
4031) {
4032 init_test(cx, |_| {});
4033 let mut cx = EditorTestContext::new(cx).await;
4034 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4035 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4036 executor.run_until_parked();
4037
4038 cx.update_editor(|editor, window, cx| {
4039 let snapshot = editor.snapshot(window, cx);
4040 assert_eq!(
4041 snapshot
4042 .buffer_snapshot
4043 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
4044 .collect::<Vec<_>>(),
4045 Vec::new(),
4046 "Should not have any diffs for files with custom newlines"
4047 );
4048 });
4049}
4050
4051#[gpui::test]
4052async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4053 init_test(cx, |_| {});
4054
4055 let mut cx = EditorTestContext::new(cx).await;
4056
4057 // Test sort_lines_case_insensitive()
4058 cx.set_state(indoc! {"
4059 «z
4060 y
4061 x
4062 Z
4063 Y
4064 Xˇ»
4065 "});
4066 cx.update_editor(|e, window, cx| {
4067 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4068 });
4069 cx.assert_editor_state(indoc! {"
4070 «x
4071 X
4072 y
4073 Y
4074 z
4075 Zˇ»
4076 "});
4077
4078 // Test sort_lines_by_length()
4079 //
4080 // Demonstrates:
4081 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4082 // - sort is stable
4083 cx.set_state(indoc! {"
4084 «123
4085 æ
4086 12
4087 ∞
4088 1
4089 æˇ»
4090 "});
4091 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4092 cx.assert_editor_state(indoc! {"
4093 «æ
4094 ∞
4095 1
4096 æ
4097 12
4098 123ˇ»
4099 "});
4100
4101 // Test reverse_lines()
4102 cx.set_state(indoc! {"
4103 «5
4104 4
4105 3
4106 2
4107 1ˇ»
4108 "});
4109 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4110 cx.assert_editor_state(indoc! {"
4111 «1
4112 2
4113 3
4114 4
4115 5ˇ»
4116 "});
4117
4118 // Skip testing shuffle_line()
4119
4120 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4121 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4122
4123 // Don't manipulate when cursor is on single line, but expand the selection
4124 cx.set_state(indoc! {"
4125 ddˇdd
4126 ccc
4127 bb
4128 a
4129 "});
4130 cx.update_editor(|e, window, cx| {
4131 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4132 });
4133 cx.assert_editor_state(indoc! {"
4134 «ddddˇ»
4135 ccc
4136 bb
4137 a
4138 "});
4139
4140 // Basic manipulate case
4141 // Start selection moves to column 0
4142 // End of selection shrinks to fit shorter line
4143 cx.set_state(indoc! {"
4144 dd«d
4145 ccc
4146 bb
4147 aaaaaˇ»
4148 "});
4149 cx.update_editor(|e, window, cx| {
4150 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4151 });
4152 cx.assert_editor_state(indoc! {"
4153 «aaaaa
4154 bb
4155 ccc
4156 dddˇ»
4157 "});
4158
4159 // Manipulate case with newlines
4160 cx.set_state(indoc! {"
4161 dd«d
4162 ccc
4163
4164 bb
4165 aaaaa
4166
4167 ˇ»
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 «
4174
4175 aaaaa
4176 bb
4177 ccc
4178 dddˇ»
4179
4180 "});
4181
4182 // Adding new line
4183 cx.set_state(indoc! {"
4184 aa«a
4185 bbˇ»b
4186 "});
4187 cx.update_editor(|e, window, cx| {
4188 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4189 });
4190 cx.assert_editor_state(indoc! {"
4191 «aaa
4192 bbb
4193 added_lineˇ»
4194 "});
4195
4196 // Removing line
4197 cx.set_state(indoc! {"
4198 aa«a
4199 bbbˇ»
4200 "});
4201 cx.update_editor(|e, window, cx| {
4202 e.manipulate_immutable_lines(window, cx, |lines| {
4203 lines.pop();
4204 })
4205 });
4206 cx.assert_editor_state(indoc! {"
4207 «aaaˇ»
4208 "});
4209
4210 // Removing all lines
4211 cx.set_state(indoc! {"
4212 aa«a
4213 bbbˇ»
4214 "});
4215 cx.update_editor(|e, window, cx| {
4216 e.manipulate_immutable_lines(window, cx, |lines| {
4217 lines.drain(..);
4218 })
4219 });
4220 cx.assert_editor_state(indoc! {"
4221 ˇ
4222 "});
4223}
4224
4225#[gpui::test]
4226async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4227 init_test(cx, |_| {});
4228
4229 let mut cx = EditorTestContext::new(cx).await;
4230
4231 // Consider continuous selection as single selection
4232 cx.set_state(indoc! {"
4233 Aaa«aa
4234 cˇ»c«c
4235 bb
4236 aaaˇ»aa
4237 "});
4238 cx.update_editor(|e, window, cx| {
4239 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4240 });
4241 cx.assert_editor_state(indoc! {"
4242 «Aaaaa
4243 ccc
4244 bb
4245 aaaaaˇ»
4246 "});
4247
4248 cx.set_state(indoc! {"
4249 Aaa«aa
4250 cˇ»c«c
4251 bb
4252 aaaˇ»aa
4253 "});
4254 cx.update_editor(|e, window, cx| {
4255 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4256 });
4257 cx.assert_editor_state(indoc! {"
4258 «Aaaaa
4259 ccc
4260 bbˇ»
4261 "});
4262
4263 // Consider non continuous selection as distinct dedup operations
4264 cx.set_state(indoc! {"
4265 «aaaaa
4266 bb
4267 aaaaa
4268 aaaaaˇ»
4269
4270 aaa«aaˇ»
4271 "});
4272 cx.update_editor(|e, window, cx| {
4273 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4274 });
4275 cx.assert_editor_state(indoc! {"
4276 «aaaaa
4277 bbˇ»
4278
4279 «aaaaaˇ»
4280 "});
4281}
4282
4283#[gpui::test]
4284async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4285 init_test(cx, |_| {});
4286
4287 let mut cx = EditorTestContext::new(cx).await;
4288
4289 cx.set_state(indoc! {"
4290 «Aaa
4291 aAa
4292 Aaaˇ»
4293 "});
4294 cx.update_editor(|e, window, cx| {
4295 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4296 });
4297 cx.assert_editor_state(indoc! {"
4298 «Aaa
4299 aAaˇ»
4300 "});
4301
4302 cx.set_state(indoc! {"
4303 «Aaa
4304 aAa
4305 aaAˇ»
4306 "});
4307 cx.update_editor(|e, window, cx| {
4308 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4309 });
4310 cx.assert_editor_state(indoc! {"
4311 «Aaaˇ»
4312 "});
4313}
4314
4315#[gpui::test]
4316async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4317 init_test(cx, |_| {});
4318
4319 let mut cx = EditorTestContext::new(cx).await;
4320
4321 // Manipulate with multiple selections on a single line
4322 cx.set_state(indoc! {"
4323 dd«dd
4324 cˇ»c«c
4325 bb
4326 aaaˇ»aa
4327 "});
4328 cx.update_editor(|e, window, cx| {
4329 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4330 });
4331 cx.assert_editor_state(indoc! {"
4332 «aaaaa
4333 bb
4334 ccc
4335 ddddˇ»
4336 "});
4337
4338 // Manipulate with multiple disjoin selections
4339 cx.set_state(indoc! {"
4340 5«
4341 4
4342 3
4343 2
4344 1ˇ»
4345
4346 dd«dd
4347 ccc
4348 bb
4349 aaaˇ»aa
4350 "});
4351 cx.update_editor(|e, window, cx| {
4352 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4353 });
4354 cx.assert_editor_state(indoc! {"
4355 «1
4356 2
4357 3
4358 4
4359 5ˇ»
4360
4361 «aaaaa
4362 bb
4363 ccc
4364 ddddˇ»
4365 "});
4366
4367 // Adding lines on each selection
4368 cx.set_state(indoc! {"
4369 2«
4370 1ˇ»
4371
4372 bb«bb
4373 aaaˇ»aa
4374 "});
4375 cx.update_editor(|e, window, cx| {
4376 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4377 });
4378 cx.assert_editor_state(indoc! {"
4379 «2
4380 1
4381 added lineˇ»
4382
4383 «bbbb
4384 aaaaa
4385 added lineˇ»
4386 "});
4387
4388 // Removing lines on each selection
4389 cx.set_state(indoc! {"
4390 2«
4391 1ˇ»
4392
4393 bb«bb
4394 aaaˇ»aa
4395 "});
4396 cx.update_editor(|e, window, cx| {
4397 e.manipulate_immutable_lines(window, cx, |lines| {
4398 lines.pop();
4399 })
4400 });
4401 cx.assert_editor_state(indoc! {"
4402 «2ˇ»
4403
4404 «bbbbˇ»
4405 "});
4406}
4407
4408#[gpui::test]
4409async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4410 init_test(cx, |settings| {
4411 settings.defaults.tab_size = NonZeroU32::new(3)
4412 });
4413
4414 let mut cx = EditorTestContext::new(cx).await;
4415
4416 // MULTI SELECTION
4417 // Ln.1 "«" tests empty lines
4418 // Ln.9 tests just leading whitespace
4419 cx.set_state(indoc! {"
4420 «
4421 abc // No indentationˇ»
4422 «\tabc // 1 tabˇ»
4423 \t\tabc « ˇ» // 2 tabs
4424 \t ab«c // Tab followed by space
4425 \tabc // Space followed by tab (3 spaces should be the result)
4426 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4427 abˇ»ˇc ˇ ˇ // Already space indented«
4428 \t
4429 \tabc\tdef // Only the leading tab is manipulatedˇ»
4430 "});
4431 cx.update_editor(|e, window, cx| {
4432 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4433 });
4434 cx.assert_editor_state(
4435 indoc! {"
4436 «
4437 abc // No indentation
4438 abc // 1 tab
4439 abc // 2 tabs
4440 abc // Tab followed by space
4441 abc // Space followed by tab (3 spaces should be the result)
4442 abc // Mixed indentation (tab conversion depends on the column)
4443 abc // Already space indented
4444 ·
4445 abc\tdef // Only the leading tab is manipulatedˇ»
4446 "}
4447 .replace("·", "")
4448 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4449 );
4450
4451 // Test on just a few lines, the others should remain unchanged
4452 // Only lines (3, 5, 10, 11) should change
4453 cx.set_state(
4454 indoc! {"
4455 ·
4456 abc // No indentation
4457 \tabcˇ // 1 tab
4458 \t\tabc // 2 tabs
4459 \t abcˇ // Tab followed by space
4460 \tabc // Space followed by tab (3 spaces should be the result)
4461 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4462 abc // Already space indented
4463 «\t
4464 \tabc\tdef // Only the leading tab is manipulatedˇ»
4465 "}
4466 .replace("·", "")
4467 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4468 );
4469 cx.update_editor(|e, window, cx| {
4470 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4471 });
4472 cx.assert_editor_state(
4473 indoc! {"
4474 ·
4475 abc // No indentation
4476 « abc // 1 tabˇ»
4477 \t\tabc // 2 tabs
4478 « abc // Tab followed by spaceˇ»
4479 \tabc // Space followed by tab (3 spaces should be the result)
4480 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4481 abc // Already space indented
4482 « ·
4483 abc\tdef // Only the leading tab is manipulatedˇ»
4484 "}
4485 .replace("·", "")
4486 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4487 );
4488
4489 // SINGLE SELECTION
4490 // Ln.1 "«" tests empty lines
4491 // Ln.9 tests just leading whitespace
4492 cx.set_state(indoc! {"
4493 «
4494 abc // No indentation
4495 \tabc // 1 tab
4496 \t\tabc // 2 tabs
4497 \t abc // Tab followed by space
4498 \tabc // Space followed by tab (3 spaces should be the result)
4499 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4500 abc // Already space indented
4501 \t
4502 \tabc\tdef // Only the leading tab is manipulatedˇ»
4503 "});
4504 cx.update_editor(|e, window, cx| {
4505 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4506 });
4507 cx.assert_editor_state(
4508 indoc! {"
4509 «
4510 abc // No indentation
4511 abc // 1 tab
4512 abc // 2 tabs
4513 abc // Tab followed by space
4514 abc // Space followed by tab (3 spaces should be the result)
4515 abc // Mixed indentation (tab conversion depends on the column)
4516 abc // Already space indented
4517 ·
4518 abc\tdef // Only the leading tab is manipulatedˇ»
4519 "}
4520 .replace("·", "")
4521 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4522 );
4523}
4524
4525#[gpui::test]
4526async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
4527 init_test(cx, |settings| {
4528 settings.defaults.tab_size = NonZeroU32::new(3)
4529 });
4530
4531 let mut cx = EditorTestContext::new(cx).await;
4532
4533 // MULTI SELECTION
4534 // Ln.1 "«" tests empty lines
4535 // Ln.11 tests just leading whitespace
4536 cx.set_state(indoc! {"
4537 «
4538 abˇ»ˇc // No indentation
4539 abc ˇ ˇ // 1 space (< 3 so dont convert)
4540 abc « // 2 spaces (< 3 so dont convert)
4541 abc // 3 spaces (convert)
4542 abc ˇ» // 5 spaces (1 tab + 2 spaces)
4543 «\tˇ»\t«\tˇ»abc // Already tab indented
4544 «\t abc // Tab followed by space
4545 \tabc // Space followed by tab (should be consumed due to tab)
4546 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4547 \tˇ» «\t
4548 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
4549 "});
4550 cx.update_editor(|e, window, cx| {
4551 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4552 });
4553 cx.assert_editor_state(indoc! {"
4554 «
4555 abc // No indentation
4556 abc // 1 space (< 3 so dont convert)
4557 abc // 2 spaces (< 3 so dont convert)
4558 \tabc // 3 spaces (convert)
4559 \t abc // 5 spaces (1 tab + 2 spaces)
4560 \t\t\tabc // Already tab indented
4561 \t abc // Tab followed by space
4562 \tabc // Space followed by tab (should be consumed due to tab)
4563 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4564 \t\t\t
4565 \tabc \t // Only the leading spaces should be convertedˇ»
4566 "});
4567
4568 // Test on just a few lines, the other should remain unchanged
4569 // Only lines (4, 8, 11, 12) should change
4570 cx.set_state(
4571 indoc! {"
4572 ·
4573 abc // No indentation
4574 abc // 1 space (< 3 so dont convert)
4575 abc // 2 spaces (< 3 so dont convert)
4576 « abc // 3 spaces (convert)ˇ»
4577 abc // 5 spaces (1 tab + 2 spaces)
4578 \t\t\tabc // Already tab indented
4579 \t abc // Tab followed by space
4580 \tabc ˇ // Space followed by tab (should be consumed due to tab)
4581 \t\t \tabc // Mixed indentation
4582 \t \t \t \tabc // Mixed indentation
4583 \t \tˇ
4584 « abc \t // Only the leading spaces should be convertedˇ»
4585 "}
4586 .replace("·", "")
4587 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4588 );
4589 cx.update_editor(|e, window, cx| {
4590 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4591 });
4592 cx.assert_editor_state(
4593 indoc! {"
4594 ·
4595 abc // No indentation
4596 abc // 1 space (< 3 so dont convert)
4597 abc // 2 spaces (< 3 so dont convert)
4598 «\tabc // 3 spaces (convert)ˇ»
4599 abc // 5 spaces (1 tab + 2 spaces)
4600 \t\t\tabc // Already tab indented
4601 \t abc // Tab followed by space
4602 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
4603 \t\t \tabc // Mixed indentation
4604 \t \t \t \tabc // Mixed indentation
4605 «\t\t\t
4606 \tabc \t // Only the leading spaces should be convertedˇ»
4607 "}
4608 .replace("·", "")
4609 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4610 );
4611
4612 // SINGLE SELECTION
4613 // Ln.1 "«" tests empty lines
4614 // Ln.11 tests just leading whitespace
4615 cx.set_state(indoc! {"
4616 «
4617 abc // No indentation
4618 abc // 1 space (< 3 so dont convert)
4619 abc // 2 spaces (< 3 so dont convert)
4620 abc // 3 spaces (convert)
4621 abc // 5 spaces (1 tab + 2 spaces)
4622 \t\t\tabc // Already tab indented
4623 \t abc // Tab followed by space
4624 \tabc // Space followed by tab (should be consumed due to tab)
4625 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4626 \t \t
4627 abc \t // Only the leading spaces should be convertedˇ»
4628 "});
4629 cx.update_editor(|e, window, cx| {
4630 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4631 });
4632 cx.assert_editor_state(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 \t 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\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4643 \t\t\t
4644 \tabc \t // Only the leading spaces should be convertedˇ»
4645 "});
4646}
4647
4648#[gpui::test]
4649async fn test_toggle_case(cx: &mut TestAppContext) {
4650 init_test(cx, |_| {});
4651
4652 let mut cx = EditorTestContext::new(cx).await;
4653
4654 // If all lower case -> upper case
4655 cx.set_state(indoc! {"
4656 «hello worldˇ»
4657 "});
4658 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4659 cx.assert_editor_state(indoc! {"
4660 «HELLO WORLDˇ»
4661 "});
4662
4663 // If all upper case -> lower case
4664 cx.set_state(indoc! {"
4665 «HELLO WORLDˇ»
4666 "});
4667 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4668 cx.assert_editor_state(indoc! {"
4669 «hello worldˇ»
4670 "});
4671
4672 // If any upper case characters are identified -> lower case
4673 // This matches JetBrains IDEs
4674 cx.set_state(indoc! {"
4675 «hEllo worldˇ»
4676 "});
4677 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4678 cx.assert_editor_state(indoc! {"
4679 «hello worldˇ»
4680 "});
4681}
4682
4683#[gpui::test]
4684async fn test_manipulate_text(cx: &mut TestAppContext) {
4685 init_test(cx, |_| {});
4686
4687 let mut cx = EditorTestContext::new(cx).await;
4688
4689 // Test convert_to_upper_case()
4690 cx.set_state(indoc! {"
4691 «hello worldˇ»
4692 "});
4693 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4694 cx.assert_editor_state(indoc! {"
4695 «HELLO WORLDˇ»
4696 "});
4697
4698 // Test convert_to_lower_case()
4699 cx.set_state(indoc! {"
4700 «HELLO WORLDˇ»
4701 "});
4702 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4703 cx.assert_editor_state(indoc! {"
4704 «hello worldˇ»
4705 "});
4706
4707 // Test multiple line, single selection case
4708 cx.set_state(indoc! {"
4709 «The quick brown
4710 fox jumps over
4711 the lazy dogˇ»
4712 "});
4713 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4714 cx.assert_editor_state(indoc! {"
4715 «The Quick Brown
4716 Fox Jumps Over
4717 The Lazy Dogˇ»
4718 "});
4719
4720 // Test multiple line, single selection case
4721 cx.set_state(indoc! {"
4722 «The quick brown
4723 fox jumps over
4724 the lazy dogˇ»
4725 "});
4726 cx.update_editor(|e, window, cx| {
4727 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4728 });
4729 cx.assert_editor_state(indoc! {"
4730 «TheQuickBrown
4731 FoxJumpsOver
4732 TheLazyDogˇ»
4733 "});
4734
4735 // From here on out, test more complex cases of manipulate_text()
4736
4737 // Test no selection case - should affect words cursors are in
4738 // Cursor at beginning, middle, and end of word
4739 cx.set_state(indoc! {"
4740 ˇhello big beauˇtiful worldˇ
4741 "});
4742 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4743 cx.assert_editor_state(indoc! {"
4744 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4745 "});
4746
4747 // Test multiple selections on a single line and across multiple lines
4748 cx.set_state(indoc! {"
4749 «Theˇ» quick «brown
4750 foxˇ» jumps «overˇ»
4751 the «lazyˇ» dog
4752 "});
4753 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4754 cx.assert_editor_state(indoc! {"
4755 «THEˇ» quick «BROWN
4756 FOXˇ» jumps «OVERˇ»
4757 the «LAZYˇ» dog
4758 "});
4759
4760 // Test case where text length grows
4761 cx.set_state(indoc! {"
4762 «tschüߡ»
4763 "});
4764 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4765 cx.assert_editor_state(indoc! {"
4766 «TSCHÜSSˇ»
4767 "});
4768
4769 // Test to make sure we don't crash when text shrinks
4770 cx.set_state(indoc! {"
4771 aaa_bbbˇ
4772 "});
4773 cx.update_editor(|e, window, cx| {
4774 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4775 });
4776 cx.assert_editor_state(indoc! {"
4777 «aaaBbbˇ»
4778 "});
4779
4780 // Test to make sure we all aware of the fact that each word can grow and shrink
4781 // Final selections should be aware of this fact
4782 cx.set_state(indoc! {"
4783 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4784 "});
4785 cx.update_editor(|e, window, cx| {
4786 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4787 });
4788 cx.assert_editor_state(indoc! {"
4789 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4790 "});
4791
4792 cx.set_state(indoc! {"
4793 «hElLo, WoRld!ˇ»
4794 "});
4795 cx.update_editor(|e, window, cx| {
4796 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4797 });
4798 cx.assert_editor_state(indoc! {"
4799 «HeLlO, wOrLD!ˇ»
4800 "});
4801}
4802
4803#[gpui::test]
4804fn test_duplicate_line(cx: &mut TestAppContext) {
4805 init_test(cx, |_| {});
4806
4807 let editor = cx.add_window(|window, cx| {
4808 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4809 build_editor(buffer, window, cx)
4810 });
4811 _ = editor.update(cx, |editor, window, cx| {
4812 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4813 s.select_display_ranges([
4814 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4815 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4816 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4817 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4818 ])
4819 });
4820 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4821 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4822 assert_eq!(
4823 editor.selections.display_ranges(cx),
4824 vec![
4825 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4826 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4827 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4828 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4829 ]
4830 );
4831 });
4832
4833 let editor = cx.add_window(|window, cx| {
4834 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4835 build_editor(buffer, window, cx)
4836 });
4837 _ = editor.update(cx, |editor, window, cx| {
4838 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4839 s.select_display_ranges([
4840 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4841 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4842 ])
4843 });
4844 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4845 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4846 assert_eq!(
4847 editor.selections.display_ranges(cx),
4848 vec![
4849 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4850 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4851 ]
4852 );
4853 });
4854
4855 // With `move_upwards` the selections stay in place, except for
4856 // the lines inserted above them
4857 let editor = cx.add_window(|window, cx| {
4858 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4859 build_editor(buffer, window, cx)
4860 });
4861 _ = editor.update(cx, |editor, window, cx| {
4862 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4863 s.select_display_ranges([
4864 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4865 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4866 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4867 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4868 ])
4869 });
4870 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4871 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4872 assert_eq!(
4873 editor.selections.display_ranges(cx),
4874 vec![
4875 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4876 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4877 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4878 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4879 ]
4880 );
4881 });
4882
4883 let editor = cx.add_window(|window, cx| {
4884 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4885 build_editor(buffer, window, cx)
4886 });
4887 _ = editor.update(cx, |editor, window, cx| {
4888 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4889 s.select_display_ranges([
4890 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4891 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4892 ])
4893 });
4894 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4895 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4896 assert_eq!(
4897 editor.selections.display_ranges(cx),
4898 vec![
4899 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4900 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4901 ]
4902 );
4903 });
4904
4905 let editor = cx.add_window(|window, cx| {
4906 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4907 build_editor(buffer, window, cx)
4908 });
4909 _ = editor.update(cx, |editor, window, cx| {
4910 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4911 s.select_display_ranges([
4912 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4913 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4914 ])
4915 });
4916 editor.duplicate_selection(&DuplicateSelection, window, cx);
4917 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4918 assert_eq!(
4919 editor.selections.display_ranges(cx),
4920 vec![
4921 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4922 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4923 ]
4924 );
4925 });
4926}
4927
4928#[gpui::test]
4929fn test_move_line_up_down(cx: &mut TestAppContext) {
4930 init_test(cx, |_| {});
4931
4932 let editor = cx.add_window(|window, cx| {
4933 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4934 build_editor(buffer, window, cx)
4935 });
4936 _ = editor.update(cx, |editor, window, cx| {
4937 editor.fold_creases(
4938 vec![
4939 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4940 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4941 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4942 ],
4943 true,
4944 window,
4945 cx,
4946 );
4947 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4948 s.select_display_ranges([
4949 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4950 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4951 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4952 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4953 ])
4954 });
4955 assert_eq!(
4956 editor.display_text(cx),
4957 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4958 );
4959
4960 editor.move_line_up(&MoveLineUp, window, cx);
4961 assert_eq!(
4962 editor.display_text(cx),
4963 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4964 );
4965 assert_eq!(
4966 editor.selections.display_ranges(cx),
4967 vec![
4968 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4969 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4970 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4971 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4972 ]
4973 );
4974 });
4975
4976 _ = editor.update(cx, |editor, window, cx| {
4977 editor.move_line_down(&MoveLineDown, window, cx);
4978 assert_eq!(
4979 editor.display_text(cx),
4980 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4981 );
4982 assert_eq!(
4983 editor.selections.display_ranges(cx),
4984 vec![
4985 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4986 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4987 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4988 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4989 ]
4990 );
4991 });
4992
4993 _ = editor.update(cx, |editor, window, cx| {
4994 editor.move_line_down(&MoveLineDown, window, cx);
4995 assert_eq!(
4996 editor.display_text(cx),
4997 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4998 );
4999 assert_eq!(
5000 editor.selections.display_ranges(cx),
5001 vec![
5002 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5003 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5004 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5005 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5006 ]
5007 );
5008 });
5009
5010 _ = editor.update(cx, |editor, window, cx| {
5011 editor.move_line_up(&MoveLineUp, window, cx);
5012 assert_eq!(
5013 editor.display_text(cx),
5014 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5015 );
5016 assert_eq!(
5017 editor.selections.display_ranges(cx),
5018 vec![
5019 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5020 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5021 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5022 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5023 ]
5024 );
5025 });
5026}
5027
5028#[gpui::test]
5029fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5030 init_test(cx, |_| {});
5031
5032 let editor = cx.add_window(|window, cx| {
5033 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5034 build_editor(buffer, window, cx)
5035 });
5036 _ = editor.update(cx, |editor, window, cx| {
5037 let snapshot = editor.buffer.read(cx).snapshot(cx);
5038 editor.insert_blocks(
5039 [BlockProperties {
5040 style: BlockStyle::Fixed,
5041 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5042 height: Some(1),
5043 render: Arc::new(|_| div().into_any()),
5044 priority: 0,
5045 render_in_minimap: true,
5046 }],
5047 Some(Autoscroll::fit()),
5048 cx,
5049 );
5050 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5051 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5052 });
5053 editor.move_line_down(&MoveLineDown, window, cx);
5054 });
5055}
5056
5057#[gpui::test]
5058async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5059 init_test(cx, |_| {});
5060
5061 let mut cx = EditorTestContext::new(cx).await;
5062 cx.set_state(
5063 &"
5064 ˇzero
5065 one
5066 two
5067 three
5068 four
5069 five
5070 "
5071 .unindent(),
5072 );
5073
5074 // Create a four-line block that replaces three lines of text.
5075 cx.update_editor(|editor, window, cx| {
5076 let snapshot = editor.snapshot(window, cx);
5077 let snapshot = &snapshot.buffer_snapshot;
5078 let placement = BlockPlacement::Replace(
5079 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5080 );
5081 editor.insert_blocks(
5082 [BlockProperties {
5083 placement,
5084 height: Some(4),
5085 style: BlockStyle::Sticky,
5086 render: Arc::new(|_| gpui::div().into_any_element()),
5087 priority: 0,
5088 render_in_minimap: true,
5089 }],
5090 None,
5091 cx,
5092 );
5093 });
5094
5095 // Move down so that the cursor touches the block.
5096 cx.update_editor(|editor, window, cx| {
5097 editor.move_down(&Default::default(), window, cx);
5098 });
5099 cx.assert_editor_state(
5100 &"
5101 zero
5102 «one
5103 two
5104 threeˇ»
5105 four
5106 five
5107 "
5108 .unindent(),
5109 );
5110
5111 // Move down past the block.
5112 cx.update_editor(|editor, window, cx| {
5113 editor.move_down(&Default::default(), window, cx);
5114 });
5115 cx.assert_editor_state(
5116 &"
5117 zero
5118 one
5119 two
5120 three
5121 ˇfour
5122 five
5123 "
5124 .unindent(),
5125 );
5126}
5127
5128#[gpui::test]
5129fn test_transpose(cx: &mut TestAppContext) {
5130 init_test(cx, |_| {});
5131
5132 _ = cx.add_window(|window, cx| {
5133 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5134 editor.set_style(EditorStyle::default(), window, cx);
5135 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5136 s.select_ranges([1..1])
5137 });
5138 editor.transpose(&Default::default(), window, cx);
5139 assert_eq!(editor.text(cx), "bac");
5140 assert_eq!(editor.selections.ranges(cx), [2..2]);
5141
5142 editor.transpose(&Default::default(), window, cx);
5143 assert_eq!(editor.text(cx), "bca");
5144 assert_eq!(editor.selections.ranges(cx), [3..3]);
5145
5146 editor.transpose(&Default::default(), window, cx);
5147 assert_eq!(editor.text(cx), "bac");
5148 assert_eq!(editor.selections.ranges(cx), [3..3]);
5149
5150 editor
5151 });
5152
5153 _ = cx.add_window(|window, cx| {
5154 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5155 editor.set_style(EditorStyle::default(), window, cx);
5156 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5157 s.select_ranges([3..3])
5158 });
5159 editor.transpose(&Default::default(), window, cx);
5160 assert_eq!(editor.text(cx), "acb\nde");
5161 assert_eq!(editor.selections.ranges(cx), [3..3]);
5162
5163 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5164 s.select_ranges([4..4])
5165 });
5166 editor.transpose(&Default::default(), window, cx);
5167 assert_eq!(editor.text(cx), "acbd\ne");
5168 assert_eq!(editor.selections.ranges(cx), [5..5]);
5169
5170 editor.transpose(&Default::default(), window, cx);
5171 assert_eq!(editor.text(cx), "acbde\n");
5172 assert_eq!(editor.selections.ranges(cx), [6..6]);
5173
5174 editor.transpose(&Default::default(), window, cx);
5175 assert_eq!(editor.text(cx), "acbd\ne");
5176 assert_eq!(editor.selections.ranges(cx), [6..6]);
5177
5178 editor
5179 });
5180
5181 _ = cx.add_window(|window, cx| {
5182 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5183 editor.set_style(EditorStyle::default(), window, cx);
5184 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5185 s.select_ranges([1..1, 2..2, 4..4])
5186 });
5187 editor.transpose(&Default::default(), window, cx);
5188 assert_eq!(editor.text(cx), "bacd\ne");
5189 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5190
5191 editor.transpose(&Default::default(), window, cx);
5192 assert_eq!(editor.text(cx), "bcade\n");
5193 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5194
5195 editor.transpose(&Default::default(), window, cx);
5196 assert_eq!(editor.text(cx), "bcda\ne");
5197 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5198
5199 editor.transpose(&Default::default(), window, cx);
5200 assert_eq!(editor.text(cx), "bcade\n");
5201 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5202
5203 editor.transpose(&Default::default(), window, cx);
5204 assert_eq!(editor.text(cx), "bcaed\n");
5205 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5206
5207 editor
5208 });
5209
5210 _ = cx.add_window(|window, cx| {
5211 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5212 editor.set_style(EditorStyle::default(), window, cx);
5213 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5214 s.select_ranges([4..4])
5215 });
5216 editor.transpose(&Default::default(), window, cx);
5217 assert_eq!(editor.text(cx), "🏀🍐✋");
5218 assert_eq!(editor.selections.ranges(cx), [8..8]);
5219
5220 editor.transpose(&Default::default(), window, cx);
5221 assert_eq!(editor.text(cx), "🏀✋🍐");
5222 assert_eq!(editor.selections.ranges(cx), [11..11]);
5223
5224 editor.transpose(&Default::default(), window, cx);
5225 assert_eq!(editor.text(cx), "🏀🍐✋");
5226 assert_eq!(editor.selections.ranges(cx), [11..11]);
5227
5228 editor
5229 });
5230}
5231
5232#[gpui::test]
5233async fn test_rewrap(cx: &mut TestAppContext) {
5234 init_test(cx, |settings| {
5235 settings.languages.0.extend([
5236 (
5237 "Markdown".into(),
5238 LanguageSettingsContent {
5239 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5240 preferred_line_length: Some(40),
5241 ..Default::default()
5242 },
5243 ),
5244 (
5245 "Plain Text".into(),
5246 LanguageSettingsContent {
5247 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5248 preferred_line_length: Some(40),
5249 ..Default::default()
5250 },
5251 ),
5252 (
5253 "C++".into(),
5254 LanguageSettingsContent {
5255 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5256 preferred_line_length: Some(40),
5257 ..Default::default()
5258 },
5259 ),
5260 (
5261 "Python".into(),
5262 LanguageSettingsContent {
5263 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5264 preferred_line_length: Some(40),
5265 ..Default::default()
5266 },
5267 ),
5268 (
5269 "Rust".into(),
5270 LanguageSettingsContent {
5271 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5272 preferred_line_length: Some(40),
5273 ..Default::default()
5274 },
5275 ),
5276 ])
5277 });
5278
5279 let mut cx = EditorTestContext::new(cx).await;
5280
5281 let cpp_language = Arc::new(Language::new(
5282 LanguageConfig {
5283 name: "C++".into(),
5284 line_comments: vec!["// ".into()],
5285 ..LanguageConfig::default()
5286 },
5287 None,
5288 ));
5289 let python_language = Arc::new(Language::new(
5290 LanguageConfig {
5291 name: "Python".into(),
5292 line_comments: vec!["# ".into()],
5293 ..LanguageConfig::default()
5294 },
5295 None,
5296 ));
5297 let markdown_language = Arc::new(Language::new(
5298 LanguageConfig {
5299 name: "Markdown".into(),
5300 rewrap_prefixes: vec![
5301 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5302 regex::Regex::new("[-*+]\\s+").unwrap(),
5303 ],
5304 ..LanguageConfig::default()
5305 },
5306 None,
5307 ));
5308 let rust_language = Arc::new(Language::new(
5309 LanguageConfig {
5310 name: "Rust".into(),
5311 line_comments: vec!["// ".into(), "/// ".into()],
5312 ..LanguageConfig::default()
5313 },
5314 Some(tree_sitter_rust::LANGUAGE.into()),
5315 ));
5316
5317 let plaintext_language = Arc::new(Language::new(
5318 LanguageConfig {
5319 name: "Plain Text".into(),
5320 ..LanguageConfig::default()
5321 },
5322 None,
5323 ));
5324
5325 // Test basic rewrapping of a long line with a cursor
5326 assert_rewrap(
5327 indoc! {"
5328 // ˇThis is a long comment that needs to be wrapped.
5329 "},
5330 indoc! {"
5331 // ˇThis is a long comment that needs to
5332 // be wrapped.
5333 "},
5334 cpp_language.clone(),
5335 &mut cx,
5336 );
5337
5338 // Test rewrapping a full selection
5339 assert_rewrap(
5340 indoc! {"
5341 «// This selected long comment needs to be wrapped.ˇ»"
5342 },
5343 indoc! {"
5344 «// This selected long comment needs to
5345 // be wrapped.ˇ»"
5346 },
5347 cpp_language.clone(),
5348 &mut cx,
5349 );
5350
5351 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5352 assert_rewrap(
5353 indoc! {"
5354 // ˇThis is the first line.
5355 // Thisˇ is the second line.
5356 // This is the thirdˇ line, all part of one paragraph.
5357 "},
5358 indoc! {"
5359 // ˇThis is the first line. Thisˇ is the
5360 // second line. This is the thirdˇ line,
5361 // all part of one paragraph.
5362 "},
5363 cpp_language.clone(),
5364 &mut cx,
5365 );
5366
5367 // Test multiple cursors in different paragraphs trigger separate rewraps
5368 assert_rewrap(
5369 indoc! {"
5370 // ˇThis is the first paragraph, first line.
5371 // ˇThis is the first paragraph, second line.
5372
5373 // ˇThis is the second paragraph, first line.
5374 // ˇThis is the second paragraph, second line.
5375 "},
5376 indoc! {"
5377 // ˇThis is the first paragraph, first
5378 // line. ˇThis is the first paragraph,
5379 // second line.
5380
5381 // ˇThis is the second paragraph, first
5382 // line. ˇThis is the second paragraph,
5383 // second line.
5384 "},
5385 cpp_language.clone(),
5386 &mut cx,
5387 );
5388
5389 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
5390 assert_rewrap(
5391 indoc! {"
5392 «// A regular long long comment to be wrapped.
5393 /// A documentation long comment to be wrapped.ˇ»
5394 "},
5395 indoc! {"
5396 «// A regular long long comment to be
5397 // wrapped.
5398 /// A documentation long comment to be
5399 /// wrapped.ˇ»
5400 "},
5401 rust_language.clone(),
5402 &mut cx,
5403 );
5404
5405 // Test that change in indentation level trigger seperate rewraps
5406 assert_rewrap(
5407 indoc! {"
5408 fn foo() {
5409 «// This is a long comment at the base indent.
5410 // This is a long comment at the next indent.ˇ»
5411 }
5412 "},
5413 indoc! {"
5414 fn foo() {
5415 «// This is a long comment at the
5416 // base indent.
5417 // This is a long comment at the
5418 // next indent.ˇ»
5419 }
5420 "},
5421 rust_language.clone(),
5422 &mut cx,
5423 );
5424
5425 // Test that different comment prefix characters (e.g., '#') are handled correctly
5426 assert_rewrap(
5427 indoc! {"
5428 # ˇThis is a long comment using a pound sign.
5429 "},
5430 indoc! {"
5431 # ˇThis is a long comment using a pound
5432 # sign.
5433 "},
5434 python_language.clone(),
5435 &mut cx,
5436 );
5437
5438 // Test rewrapping only affects comments, not code even when selected
5439 assert_rewrap(
5440 indoc! {"
5441 «/// This doc comment is long and should be wrapped.
5442 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5443 "},
5444 indoc! {"
5445 «/// This doc comment is long and should
5446 /// be wrapped.
5447 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5448 "},
5449 rust_language.clone(),
5450 &mut cx,
5451 );
5452
5453 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
5454 assert_rewrap(
5455 indoc! {"
5456 # Header
5457
5458 A long long long line of markdown text to wrap.ˇ
5459 "},
5460 indoc! {"
5461 # Header
5462
5463 A long long long line of markdown text
5464 to wrap.ˇ
5465 "},
5466 markdown_language.clone(),
5467 &mut cx,
5468 );
5469
5470 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
5471 assert_rewrap(
5472 indoc! {"
5473 «1. This is a numbered list item that is very long and needs to be wrapped properly.
5474 2. This is a numbered list item that is very long and needs to be wrapped properly.
5475 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
5476 "},
5477 indoc! {"
5478 «1. This is a numbered list item that is
5479 very long and needs to be wrapped
5480 properly.
5481 2. This is a numbered list item that is
5482 very long and needs to be wrapped
5483 properly.
5484 - This is an unordered list item that is
5485 also very long and should not merge
5486 with the numbered item.ˇ»
5487 "},
5488 markdown_language.clone(),
5489 &mut cx,
5490 );
5491
5492 // Test that rewrapping add indents for rewrapping boundary if not exists already.
5493 assert_rewrap(
5494 indoc! {"
5495 «1. This is a numbered list item that is
5496 very long and needs to be wrapped
5497 properly.
5498 2. This is a numbered list item that is
5499 very long and needs to be wrapped
5500 properly.
5501 - This is an unordered list item that is
5502 also very long and should not merge with
5503 the numbered item.ˇ»
5504 "},
5505 indoc! {"
5506 «1. This is a numbered list item that is
5507 very long and needs to be wrapped
5508 properly.
5509 2. This is a numbered list item that is
5510 very long and needs to be wrapped
5511 properly.
5512 - This is an unordered list item that is
5513 also very long and should not merge
5514 with the numbered item.ˇ»
5515 "},
5516 markdown_language.clone(),
5517 &mut cx,
5518 );
5519
5520 // Test that rewrapping maintain indents even when they already exists.
5521 assert_rewrap(
5522 indoc! {"
5523 «1. This is a numbered list
5524 item that is very long and needs to be wrapped properly.
5525 2. This is a numbered list
5526 item that is very long and needs to be wrapped properly.
5527 - This is an unordered list item that is also very long and
5528 should not merge with the numbered item.ˇ»
5529 "},
5530 indoc! {"
5531 «1. This is a numbered list item that is
5532 very long and needs to be wrapped
5533 properly.
5534 2. This is a numbered list item that is
5535 very long and needs to be wrapped
5536 properly.
5537 - This is an unordered list item that is
5538 also very long and should not merge
5539 with the numbered item.ˇ»
5540 "},
5541 markdown_language.clone(),
5542 &mut cx,
5543 );
5544
5545 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
5546 assert_rewrap(
5547 indoc! {"
5548 ˇThis is a very long line of plain text that will be wrapped.
5549 "},
5550 indoc! {"
5551 ˇThis is a very long line of plain text
5552 that will be wrapped.
5553 "},
5554 plaintext_language.clone(),
5555 &mut cx,
5556 );
5557
5558 // Test that non-commented code acts as a paragraph boundary within a selection
5559 assert_rewrap(
5560 indoc! {"
5561 «// This is the first long comment block to be wrapped.
5562 fn my_func(a: u32);
5563 // This is the second long comment block to be wrapped.ˇ»
5564 "},
5565 indoc! {"
5566 «// This is the first long comment block
5567 // to be wrapped.
5568 fn my_func(a: u32);
5569 // This is the second long comment block
5570 // to be wrapped.ˇ»
5571 "},
5572 rust_language.clone(),
5573 &mut cx,
5574 );
5575
5576 // Test rewrapping multiple selections, including ones with blank lines or tabs
5577 assert_rewrap(
5578 indoc! {"
5579 «ˇThis is a very long line that will be wrapped.
5580
5581 This is another paragraph in the same selection.»
5582
5583 «\tThis is a very long indented line that will be wrapped.ˇ»
5584 "},
5585 indoc! {"
5586 «ˇThis is a very long line that will be
5587 wrapped.
5588
5589 This is another paragraph in the same
5590 selection.»
5591
5592 «\tThis is a very long indented line
5593 \tthat will be wrapped.ˇ»
5594 "},
5595 plaintext_language.clone(),
5596 &mut cx,
5597 );
5598
5599 // Test that an empty comment line acts as a paragraph boundary
5600 assert_rewrap(
5601 indoc! {"
5602 // ˇThis is a long comment that will be wrapped.
5603 //
5604 // And this is another long comment that will also be wrapped.ˇ
5605 "},
5606 indoc! {"
5607 // ˇThis is a long comment that will be
5608 // wrapped.
5609 //
5610 // And this is another long comment that
5611 // will also be wrapped.ˇ
5612 "},
5613 cpp_language,
5614 &mut cx,
5615 );
5616
5617 #[track_caller]
5618 fn assert_rewrap(
5619 unwrapped_text: &str,
5620 wrapped_text: &str,
5621 language: Arc<Language>,
5622 cx: &mut EditorTestContext,
5623 ) {
5624 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5625 cx.set_state(unwrapped_text);
5626 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5627 cx.assert_editor_state(wrapped_text);
5628 }
5629}
5630
5631#[gpui::test]
5632async fn test_hard_wrap(cx: &mut TestAppContext) {
5633 init_test(cx, |_| {});
5634 let mut cx = EditorTestContext::new(cx).await;
5635
5636 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5637 cx.update_editor(|editor, _, cx| {
5638 editor.set_hard_wrap(Some(14), cx);
5639 });
5640
5641 cx.set_state(indoc!(
5642 "
5643 one two three ˇ
5644 "
5645 ));
5646 cx.simulate_input("four");
5647 cx.run_until_parked();
5648
5649 cx.assert_editor_state(indoc!(
5650 "
5651 one two three
5652 fourˇ
5653 "
5654 ));
5655
5656 cx.update_editor(|editor, window, cx| {
5657 editor.newline(&Default::default(), window, cx);
5658 });
5659 cx.run_until_parked();
5660 cx.assert_editor_state(indoc!(
5661 "
5662 one two three
5663 four
5664 ˇ
5665 "
5666 ));
5667
5668 cx.simulate_input("five");
5669 cx.run_until_parked();
5670 cx.assert_editor_state(indoc!(
5671 "
5672 one two three
5673 four
5674 fiveˇ
5675 "
5676 ));
5677
5678 cx.update_editor(|editor, window, cx| {
5679 editor.newline(&Default::default(), window, cx);
5680 });
5681 cx.run_until_parked();
5682 cx.simulate_input("# ");
5683 cx.run_until_parked();
5684 cx.assert_editor_state(indoc!(
5685 "
5686 one two three
5687 four
5688 five
5689 # ˇ
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 five
5702 #\x20
5703 #ˇ
5704 "
5705 ));
5706
5707 cx.simulate_input(" 6");
5708 cx.run_until_parked();
5709 cx.assert_editor_state(indoc!(
5710 "
5711 one two three
5712 four
5713 five
5714 #
5715 # 6ˇ
5716 "
5717 ));
5718}
5719
5720#[gpui::test]
5721async fn test_clipboard(cx: &mut TestAppContext) {
5722 init_test(cx, |_| {});
5723
5724 let mut cx = EditorTestContext::new(cx).await;
5725
5726 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5727 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5728 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5729
5730 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5731 cx.set_state("two ˇfour ˇsix ˇ");
5732 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5733 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5734
5735 // Paste again but with only two cursors. Since the number of cursors doesn't
5736 // match the number of slices in the clipboard, the entire clipboard text
5737 // is pasted at each cursor.
5738 cx.set_state("ˇtwo one✅ four three six five ˇ");
5739 cx.update_editor(|e, window, cx| {
5740 e.handle_input("( ", window, cx);
5741 e.paste(&Paste, window, cx);
5742 e.handle_input(") ", window, cx);
5743 });
5744 cx.assert_editor_state(
5745 &([
5746 "( one✅ ",
5747 "three ",
5748 "five ) ˇtwo one✅ four three six five ( one✅ ",
5749 "three ",
5750 "five ) ˇ",
5751 ]
5752 .join("\n")),
5753 );
5754
5755 // Cut with three selections, one of which is full-line.
5756 cx.set_state(indoc! {"
5757 1«2ˇ»3
5758 4ˇ567
5759 «8ˇ»9"});
5760 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5761 cx.assert_editor_state(indoc! {"
5762 1ˇ3
5763 ˇ9"});
5764
5765 // Paste with three selections, noticing how the copied selection that was full-line
5766 // gets inserted before the second cursor.
5767 cx.set_state(indoc! {"
5768 1ˇ3
5769 9ˇ
5770 «oˇ»ne"});
5771 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5772 cx.assert_editor_state(indoc! {"
5773 12ˇ3
5774 4567
5775 9ˇ
5776 8ˇne"});
5777
5778 // Copy with a single cursor only, which writes the whole line into the clipboard.
5779 cx.set_state(indoc! {"
5780 The quick brown
5781 fox juˇmps over
5782 the lazy dog"});
5783 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5784 assert_eq!(
5785 cx.read_from_clipboard()
5786 .and_then(|item| item.text().as_deref().map(str::to_string)),
5787 Some("fox jumps over\n".to_string())
5788 );
5789
5790 // Paste with three selections, noticing how the copied full-line selection is inserted
5791 // before the empty selections but replaces the selection that is non-empty.
5792 cx.set_state(indoc! {"
5793 Tˇhe quick brown
5794 «foˇ»x jumps over
5795 tˇhe lazy dog"});
5796 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5797 cx.assert_editor_state(indoc! {"
5798 fox jumps over
5799 Tˇhe quick brown
5800 fox jumps over
5801 ˇx jumps over
5802 fox jumps over
5803 tˇhe lazy dog"});
5804}
5805
5806#[gpui::test]
5807async fn test_copy_trim(cx: &mut TestAppContext) {
5808 init_test(cx, |_| {});
5809
5810 let mut cx = EditorTestContext::new(cx).await;
5811 cx.set_state(
5812 r#" «for selection in selections.iter() {
5813 let mut start = selection.start;
5814 let mut end = selection.end;
5815 let is_entire_line = selection.is_empty();
5816 if is_entire_line {
5817 start = Point::new(start.row, 0);ˇ»
5818 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5819 }
5820 "#,
5821 );
5822 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5823 assert_eq!(
5824 cx.read_from_clipboard()
5825 .and_then(|item| item.text().as_deref().map(str::to_string)),
5826 Some(
5827 "for selection in selections.iter() {
5828 let mut start = selection.start;
5829 let mut end = selection.end;
5830 let is_entire_line = selection.is_empty();
5831 if is_entire_line {
5832 start = Point::new(start.row, 0);"
5833 .to_string()
5834 ),
5835 "Regular copying preserves all indentation selected",
5836 );
5837 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5838 assert_eq!(
5839 cx.read_from_clipboard()
5840 .and_then(|item| item.text().as_deref().map(str::to_string)),
5841 Some(
5842 "for selection in selections.iter() {
5843let mut start = selection.start;
5844let mut end = selection.end;
5845let is_entire_line = selection.is_empty();
5846if is_entire_line {
5847 start = Point::new(start.row, 0);"
5848 .to_string()
5849 ),
5850 "Copying with stripping should strip all leading whitespaces"
5851 );
5852
5853 cx.set_state(
5854 r#" « for selection in selections.iter() {
5855 let mut start = selection.start;
5856 let mut end = selection.end;
5857 let is_entire_line = selection.is_empty();
5858 if is_entire_line {
5859 start = Point::new(start.row, 0);ˇ»
5860 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5861 }
5862 "#,
5863 );
5864 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5865 assert_eq!(
5866 cx.read_from_clipboard()
5867 .and_then(|item| item.text().as_deref().map(str::to_string)),
5868 Some(
5869 " for selection in selections.iter() {
5870 let mut start = selection.start;
5871 let mut end = selection.end;
5872 let is_entire_line = selection.is_empty();
5873 if is_entire_line {
5874 start = Point::new(start.row, 0);"
5875 .to_string()
5876 ),
5877 "Regular copying preserves all indentation selected",
5878 );
5879 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5880 assert_eq!(
5881 cx.read_from_clipboard()
5882 .and_then(|item| item.text().as_deref().map(str::to_string)),
5883 Some(
5884 "for selection in selections.iter() {
5885let mut start = selection.start;
5886let mut end = selection.end;
5887let is_entire_line = selection.is_empty();
5888if is_entire_line {
5889 start = Point::new(start.row, 0);"
5890 .to_string()
5891 ),
5892 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5893 );
5894
5895 cx.set_state(
5896 r#" «ˇ for selection in selections.iter() {
5897 let mut start = selection.start;
5898 let mut end = selection.end;
5899 let is_entire_line = selection.is_empty();
5900 if is_entire_line {
5901 start = Point::new(start.row, 0);»
5902 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5903 }
5904 "#,
5905 );
5906 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5907 assert_eq!(
5908 cx.read_from_clipboard()
5909 .and_then(|item| item.text().as_deref().map(str::to_string)),
5910 Some(
5911 " for selection in selections.iter() {
5912 let mut start = selection.start;
5913 let mut end = selection.end;
5914 let is_entire_line = selection.is_empty();
5915 if is_entire_line {
5916 start = Point::new(start.row, 0);"
5917 .to_string()
5918 ),
5919 "Regular copying for reverse selection works the same",
5920 );
5921 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5922 assert_eq!(
5923 cx.read_from_clipboard()
5924 .and_then(|item| item.text().as_deref().map(str::to_string)),
5925 Some(
5926 "for selection in selections.iter() {
5927let mut start = selection.start;
5928let mut end = selection.end;
5929let is_entire_line = selection.is_empty();
5930if is_entire_line {
5931 start = Point::new(start.row, 0);"
5932 .to_string()
5933 ),
5934 "Copying with stripping for reverse selection works the same"
5935 );
5936
5937 cx.set_state(
5938 r#" for selection «in selections.iter() {
5939 let mut start = selection.start;
5940 let mut end = selection.end;
5941 let is_entire_line = selection.is_empty();
5942 if is_entire_line {
5943 start = Point::new(start.row, 0);ˇ»
5944 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5945 }
5946 "#,
5947 );
5948 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5949 assert_eq!(
5950 cx.read_from_clipboard()
5951 .and_then(|item| item.text().as_deref().map(str::to_string)),
5952 Some(
5953 "in selections.iter() {
5954 let mut start = selection.start;
5955 let mut end = selection.end;
5956 let is_entire_line = selection.is_empty();
5957 if is_entire_line {
5958 start = Point::new(start.row, 0);"
5959 .to_string()
5960 ),
5961 "When selecting past the indent, the copying works as usual",
5962 );
5963 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5964 assert_eq!(
5965 cx.read_from_clipboard()
5966 .and_then(|item| item.text().as_deref().map(str::to_string)),
5967 Some(
5968 "in selections.iter() {
5969 let mut start = selection.start;
5970 let mut end = selection.end;
5971 let is_entire_line = selection.is_empty();
5972 if is_entire_line {
5973 start = Point::new(start.row, 0);"
5974 .to_string()
5975 ),
5976 "When selecting past the indent, nothing is trimmed"
5977 );
5978
5979 cx.set_state(
5980 r#" «for selection in selections.iter() {
5981 let mut start = selection.start;
5982
5983 let mut end = selection.end;
5984 let is_entire_line = selection.is_empty();
5985 if is_entire_line {
5986 start = Point::new(start.row, 0);
5987ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5988 }
5989 "#,
5990 );
5991 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5992 assert_eq!(
5993 cx.read_from_clipboard()
5994 .and_then(|item| item.text().as_deref().map(str::to_string)),
5995 Some(
5996 "for selection in selections.iter() {
5997let mut start = selection.start;
5998
5999let mut end = selection.end;
6000let is_entire_line = selection.is_empty();
6001if is_entire_line {
6002 start = Point::new(start.row, 0);
6003"
6004 .to_string()
6005 ),
6006 "Copying with stripping should ignore empty lines"
6007 );
6008}
6009
6010#[gpui::test]
6011async fn test_paste_multiline(cx: &mut TestAppContext) {
6012 init_test(cx, |_| {});
6013
6014 let mut cx = EditorTestContext::new(cx).await;
6015 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6016
6017 // Cut an indented block, without the leading whitespace.
6018 cx.set_state(indoc! {"
6019 const a: B = (
6020 c(),
6021 «d(
6022 e,
6023 f
6024 )ˇ»
6025 );
6026 "});
6027 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6028 cx.assert_editor_state(indoc! {"
6029 const a: B = (
6030 c(),
6031 ˇ
6032 );
6033 "});
6034
6035 // Paste it at the same position.
6036 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6037 cx.assert_editor_state(indoc! {"
6038 const a: B = (
6039 c(),
6040 d(
6041 e,
6042 f
6043 )ˇ
6044 );
6045 "});
6046
6047 // Paste it at a line with a lower indent level.
6048 cx.set_state(indoc! {"
6049 ˇ
6050 const a: B = (
6051 c(),
6052 );
6053 "});
6054 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6055 cx.assert_editor_state(indoc! {"
6056 d(
6057 e,
6058 f
6059 )ˇ
6060 const a: B = (
6061 c(),
6062 );
6063 "});
6064
6065 // Cut an indented block, with the leading whitespace.
6066 cx.set_state(indoc! {"
6067 const a: B = (
6068 c(),
6069 « d(
6070 e,
6071 f
6072 )
6073 ˇ»);
6074 "});
6075 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6076 cx.assert_editor_state(indoc! {"
6077 const a: B = (
6078 c(),
6079 ˇ);
6080 "});
6081
6082 // Paste it at the same position.
6083 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6084 cx.assert_editor_state(indoc! {"
6085 const a: B = (
6086 c(),
6087 d(
6088 e,
6089 f
6090 )
6091 ˇ);
6092 "});
6093
6094 // Paste it at a line with a higher indent level.
6095 cx.set_state(indoc! {"
6096 const a: B = (
6097 c(),
6098 d(
6099 e,
6100 fˇ
6101 )
6102 );
6103 "});
6104 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6105 cx.assert_editor_state(indoc! {"
6106 const a: B = (
6107 c(),
6108 d(
6109 e,
6110 f d(
6111 e,
6112 f
6113 )
6114 ˇ
6115 )
6116 );
6117 "});
6118
6119 // Copy an indented block, starting mid-line
6120 cx.set_state(indoc! {"
6121 const a: B = (
6122 c(),
6123 somethin«g(
6124 e,
6125 f
6126 )ˇ»
6127 );
6128 "});
6129 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6130
6131 // Paste it on a line with a lower indent level
6132 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
6133 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6134 cx.assert_editor_state(indoc! {"
6135 const a: B = (
6136 c(),
6137 something(
6138 e,
6139 f
6140 )
6141 );
6142 g(
6143 e,
6144 f
6145 )ˇ"});
6146}
6147
6148#[gpui::test]
6149async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
6150 init_test(cx, |_| {});
6151
6152 cx.write_to_clipboard(ClipboardItem::new_string(
6153 " d(\n e\n );\n".into(),
6154 ));
6155
6156 let mut cx = EditorTestContext::new(cx).await;
6157 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6158
6159 cx.set_state(indoc! {"
6160 fn a() {
6161 b();
6162 if c() {
6163 ˇ
6164 }
6165 }
6166 "});
6167
6168 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6169 cx.assert_editor_state(indoc! {"
6170 fn a() {
6171 b();
6172 if c() {
6173 d(
6174 e
6175 );
6176 ˇ
6177 }
6178 }
6179 "});
6180
6181 cx.set_state(indoc! {"
6182 fn a() {
6183 b();
6184 ˇ
6185 }
6186 "});
6187
6188 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6189 cx.assert_editor_state(indoc! {"
6190 fn a() {
6191 b();
6192 d(
6193 e
6194 );
6195 ˇ
6196 }
6197 "});
6198}
6199
6200#[gpui::test]
6201fn test_select_all(cx: &mut TestAppContext) {
6202 init_test(cx, |_| {});
6203
6204 let editor = cx.add_window(|window, cx| {
6205 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
6206 build_editor(buffer, window, cx)
6207 });
6208 _ = editor.update(cx, |editor, window, cx| {
6209 editor.select_all(&SelectAll, window, cx);
6210 assert_eq!(
6211 editor.selections.display_ranges(cx),
6212 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
6213 );
6214 });
6215}
6216
6217#[gpui::test]
6218fn test_select_line(cx: &mut TestAppContext) {
6219 init_test(cx, |_| {});
6220
6221 let editor = cx.add_window(|window, cx| {
6222 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
6223 build_editor(buffer, window, cx)
6224 });
6225 _ = editor.update(cx, |editor, window, cx| {
6226 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6227 s.select_display_ranges([
6228 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6229 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6230 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6231 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
6232 ])
6233 });
6234 editor.select_line(&SelectLine, window, cx);
6235 assert_eq!(
6236 editor.selections.display_ranges(cx),
6237 vec![
6238 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
6239 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
6240 ]
6241 );
6242 });
6243
6244 _ = editor.update(cx, |editor, window, cx| {
6245 editor.select_line(&SelectLine, window, cx);
6246 assert_eq!(
6247 editor.selections.display_ranges(cx),
6248 vec![
6249 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
6250 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
6251 ]
6252 );
6253 });
6254
6255 _ = editor.update(cx, |editor, window, cx| {
6256 editor.select_line(&SelectLine, window, cx);
6257 assert_eq!(
6258 editor.selections.display_ranges(cx),
6259 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
6260 );
6261 });
6262}
6263
6264#[gpui::test]
6265async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
6266 init_test(cx, |_| {});
6267 let mut cx = EditorTestContext::new(cx).await;
6268
6269 #[track_caller]
6270 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
6271 cx.set_state(initial_state);
6272 cx.update_editor(|e, window, cx| {
6273 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
6274 });
6275 cx.assert_editor_state(expected_state);
6276 }
6277
6278 // Selection starts and ends at the middle of lines, left-to-right
6279 test(
6280 &mut cx,
6281 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
6282 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6283 );
6284 // Same thing, right-to-left
6285 test(
6286 &mut cx,
6287 "aa\nb«b\ncc\ndd\neˇ»e\nff",
6288 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6289 );
6290
6291 // Whole buffer, left-to-right, last line *doesn't* end with newline
6292 test(
6293 &mut cx,
6294 "«ˇaa\nbb\ncc\ndd\nee\nff»",
6295 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6296 );
6297 // Same thing, right-to-left
6298 test(
6299 &mut cx,
6300 "«aa\nbb\ncc\ndd\nee\nffˇ»",
6301 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6302 );
6303
6304 // Whole buffer, left-to-right, last line ends with newline
6305 test(
6306 &mut cx,
6307 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
6308 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6309 );
6310 // Same thing, right-to-left
6311 test(
6312 &mut cx,
6313 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
6314 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6315 );
6316
6317 // Starts at the end of a line, ends at the start of another
6318 test(
6319 &mut cx,
6320 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
6321 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
6322 );
6323}
6324
6325#[gpui::test]
6326async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
6327 init_test(cx, |_| {});
6328
6329 let editor = cx.add_window(|window, cx| {
6330 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
6331 build_editor(buffer, window, cx)
6332 });
6333
6334 // setup
6335 _ = editor.update(cx, |editor, window, cx| {
6336 editor.fold_creases(
6337 vec![
6338 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
6339 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
6340 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
6341 ],
6342 true,
6343 window,
6344 cx,
6345 );
6346 assert_eq!(
6347 editor.display_text(cx),
6348 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6349 );
6350 });
6351
6352 _ = editor.update(cx, |editor, window, cx| {
6353 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6354 s.select_display_ranges([
6355 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6356 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6357 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6358 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
6359 ])
6360 });
6361 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6362 assert_eq!(
6363 editor.display_text(cx),
6364 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6365 );
6366 });
6367 EditorTestContext::for_editor(editor, cx)
6368 .await
6369 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
6370
6371 _ = editor.update(cx, |editor, window, cx| {
6372 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6373 s.select_display_ranges([
6374 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
6375 ])
6376 });
6377 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6378 assert_eq!(
6379 editor.display_text(cx),
6380 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
6381 );
6382 assert_eq!(
6383 editor.selections.display_ranges(cx),
6384 [
6385 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
6386 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
6387 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
6388 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
6389 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
6390 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
6391 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
6392 ]
6393 );
6394 });
6395 EditorTestContext::for_editor(editor, cx)
6396 .await
6397 .assert_editor_state(
6398 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
6399 );
6400}
6401
6402#[gpui::test]
6403async fn test_add_selection_above_below(cx: &mut TestAppContext) {
6404 init_test(cx, |_| {});
6405
6406 let mut cx = EditorTestContext::new(cx).await;
6407
6408 cx.set_state(indoc!(
6409 r#"abc
6410 defˇghi
6411
6412 jk
6413 nlmo
6414 "#
6415 ));
6416
6417 cx.update_editor(|editor, window, cx| {
6418 editor.add_selection_above(&Default::default(), window, cx);
6419 });
6420
6421 cx.assert_editor_state(indoc!(
6422 r#"abcˇ
6423 defˇghi
6424
6425 jk
6426 nlmo
6427 "#
6428 ));
6429
6430 cx.update_editor(|editor, window, cx| {
6431 editor.add_selection_above(&Default::default(), window, cx);
6432 });
6433
6434 cx.assert_editor_state(indoc!(
6435 r#"abcˇ
6436 defˇghi
6437
6438 jk
6439 nlmo
6440 "#
6441 ));
6442
6443 cx.update_editor(|editor, window, cx| {
6444 editor.add_selection_below(&Default::default(), window, cx);
6445 });
6446
6447 cx.assert_editor_state(indoc!(
6448 r#"abc
6449 defˇghi
6450
6451 jk
6452 nlmo
6453 "#
6454 ));
6455
6456 cx.update_editor(|editor, window, cx| {
6457 editor.undo_selection(&Default::default(), window, cx);
6458 });
6459
6460 cx.assert_editor_state(indoc!(
6461 r#"abcˇ
6462 defˇghi
6463
6464 jk
6465 nlmo
6466 "#
6467 ));
6468
6469 cx.update_editor(|editor, window, cx| {
6470 editor.redo_selection(&Default::default(), window, cx);
6471 });
6472
6473 cx.assert_editor_state(indoc!(
6474 r#"abc
6475 defˇghi
6476
6477 jk
6478 nlmo
6479 "#
6480 ));
6481
6482 cx.update_editor(|editor, window, cx| {
6483 editor.add_selection_below(&Default::default(), window, cx);
6484 });
6485
6486 cx.assert_editor_state(indoc!(
6487 r#"abc
6488 defˇghi
6489 ˇ
6490 jk
6491 nlmo
6492 "#
6493 ));
6494
6495 cx.update_editor(|editor, window, cx| {
6496 editor.add_selection_below(&Default::default(), window, cx);
6497 });
6498
6499 cx.assert_editor_state(indoc!(
6500 r#"abc
6501 defˇghi
6502 ˇ
6503 jkˇ
6504 nlmo
6505 "#
6506 ));
6507
6508 cx.update_editor(|editor, window, cx| {
6509 editor.add_selection_below(&Default::default(), window, cx);
6510 });
6511
6512 cx.assert_editor_state(indoc!(
6513 r#"abc
6514 defˇghi
6515 ˇ
6516 jkˇ
6517 nlmˇo
6518 "#
6519 ));
6520
6521 cx.update_editor(|editor, window, cx| {
6522 editor.add_selection_below(&Default::default(), window, cx);
6523 });
6524
6525 cx.assert_editor_state(indoc!(
6526 r#"abc
6527 defˇghi
6528 ˇ
6529 jkˇ
6530 nlmˇo
6531 ˇ"#
6532 ));
6533
6534 // change selections
6535 cx.set_state(indoc!(
6536 r#"abc
6537 def«ˇg»hi
6538
6539 jk
6540 nlmo
6541 "#
6542 ));
6543
6544 cx.update_editor(|editor, window, cx| {
6545 editor.add_selection_below(&Default::default(), window, cx);
6546 });
6547
6548 cx.assert_editor_state(indoc!(
6549 r#"abc
6550 def«ˇg»hi
6551
6552 jk
6553 nlm«ˇo»
6554 "#
6555 ));
6556
6557 cx.update_editor(|editor, window, cx| {
6558 editor.add_selection_below(&Default::default(), window, cx);
6559 });
6560
6561 cx.assert_editor_state(indoc!(
6562 r#"abc
6563 def«ˇg»hi
6564
6565 jk
6566 nlm«ˇo»
6567 "#
6568 ));
6569
6570 cx.update_editor(|editor, window, cx| {
6571 editor.add_selection_above(&Default::default(), window, cx);
6572 });
6573
6574 cx.assert_editor_state(indoc!(
6575 r#"abc
6576 def«ˇg»hi
6577
6578 jk
6579 nlmo
6580 "#
6581 ));
6582
6583 cx.update_editor(|editor, window, cx| {
6584 editor.add_selection_above(&Default::default(), window, cx);
6585 });
6586
6587 cx.assert_editor_state(indoc!(
6588 r#"abc
6589 def«ˇg»hi
6590
6591 jk
6592 nlmo
6593 "#
6594 ));
6595
6596 // Change selections again
6597 cx.set_state(indoc!(
6598 r#"a«bc
6599 defgˇ»hi
6600
6601 jk
6602 nlmo
6603 "#
6604 ));
6605
6606 cx.update_editor(|editor, window, cx| {
6607 editor.add_selection_below(&Default::default(), window, cx);
6608 });
6609
6610 cx.assert_editor_state(indoc!(
6611 r#"a«bcˇ»
6612 d«efgˇ»hi
6613
6614 j«kˇ»
6615 nlmo
6616 "#
6617 ));
6618
6619 cx.update_editor(|editor, window, cx| {
6620 editor.add_selection_below(&Default::default(), window, cx);
6621 });
6622 cx.assert_editor_state(indoc!(
6623 r#"a«bcˇ»
6624 d«efgˇ»hi
6625
6626 j«kˇ»
6627 n«lmoˇ»
6628 "#
6629 ));
6630 cx.update_editor(|editor, window, cx| {
6631 editor.add_selection_above(&Default::default(), window, cx);
6632 });
6633
6634 cx.assert_editor_state(indoc!(
6635 r#"a«bcˇ»
6636 d«efgˇ»hi
6637
6638 j«kˇ»
6639 nlmo
6640 "#
6641 ));
6642
6643 // Change selections again
6644 cx.set_state(indoc!(
6645 r#"abc
6646 d«ˇefghi
6647
6648 jk
6649 nlm»o
6650 "#
6651 ));
6652
6653 cx.update_editor(|editor, window, cx| {
6654 editor.add_selection_above(&Default::default(), window, cx);
6655 });
6656
6657 cx.assert_editor_state(indoc!(
6658 r#"a«ˇbc»
6659 d«ˇef»ghi
6660
6661 j«ˇk»
6662 n«ˇlm»o
6663 "#
6664 ));
6665
6666 cx.update_editor(|editor, window, cx| {
6667 editor.add_selection_below(&Default::default(), window, cx);
6668 });
6669
6670 cx.assert_editor_state(indoc!(
6671 r#"abc
6672 d«ˇef»ghi
6673
6674 j«ˇk»
6675 n«ˇlm»o
6676 "#
6677 ));
6678}
6679
6680#[gpui::test]
6681async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
6682 init_test(cx, |_| {});
6683 let mut cx = EditorTestContext::new(cx).await;
6684
6685 cx.set_state(indoc!(
6686 r#"line onˇe
6687 liˇne two
6688 line three
6689 line four"#
6690 ));
6691
6692 cx.update_editor(|editor, window, cx| {
6693 editor.add_selection_below(&Default::default(), window, cx);
6694 });
6695
6696 // test multiple cursors expand in the same direction
6697 cx.assert_editor_state(indoc!(
6698 r#"line onˇe
6699 liˇne twˇo
6700 liˇne three
6701 line four"#
6702 ));
6703
6704 cx.update_editor(|editor, window, cx| {
6705 editor.add_selection_below(&Default::default(), window, cx);
6706 });
6707
6708 cx.update_editor(|editor, window, cx| {
6709 editor.add_selection_below(&Default::default(), window, cx);
6710 });
6711
6712 // test multiple cursors expand below overflow
6713 cx.assert_editor_state(indoc!(
6714 r#"line onˇe
6715 liˇne twˇo
6716 liˇne thˇree
6717 liˇne foˇur"#
6718 ));
6719
6720 cx.update_editor(|editor, window, cx| {
6721 editor.add_selection_above(&Default::default(), window, cx);
6722 });
6723
6724 // test multiple cursors retrieves back correctly
6725 cx.assert_editor_state(indoc!(
6726 r#"line onˇe
6727 liˇne twˇo
6728 liˇne thˇree
6729 line four"#
6730 ));
6731
6732 cx.update_editor(|editor, window, cx| {
6733 editor.add_selection_above(&Default::default(), window, cx);
6734 });
6735
6736 cx.update_editor(|editor, window, cx| {
6737 editor.add_selection_above(&Default::default(), window, cx);
6738 });
6739
6740 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
6741 cx.assert_editor_state(indoc!(
6742 r#"liˇne onˇe
6743 liˇne two
6744 line three
6745 line four"#
6746 ));
6747
6748 cx.update_editor(|editor, window, cx| {
6749 editor.undo_selection(&Default::default(), window, cx);
6750 });
6751
6752 // test undo
6753 cx.assert_editor_state(indoc!(
6754 r#"line onˇe
6755 liˇne twˇo
6756 line three
6757 line four"#
6758 ));
6759
6760 cx.update_editor(|editor, window, cx| {
6761 editor.redo_selection(&Default::default(), window, cx);
6762 });
6763
6764 // test redo
6765 cx.assert_editor_state(indoc!(
6766 r#"liˇne onˇe
6767 liˇne two
6768 line three
6769 line four"#
6770 ));
6771
6772 cx.set_state(indoc!(
6773 r#"abcd
6774 ef«ghˇ»
6775 ijkl
6776 «mˇ»nop"#
6777 ));
6778
6779 cx.update_editor(|editor, window, cx| {
6780 editor.add_selection_above(&Default::default(), window, cx);
6781 });
6782
6783 // test multiple selections expand in the same direction
6784 cx.assert_editor_state(indoc!(
6785 r#"ab«cdˇ»
6786 ef«ghˇ»
6787 «iˇ»jkl
6788 «mˇ»nop"#
6789 ));
6790
6791 cx.update_editor(|editor, window, cx| {
6792 editor.add_selection_above(&Default::default(), window, cx);
6793 });
6794
6795 // test multiple selection upward overflow
6796 cx.assert_editor_state(indoc!(
6797 r#"ab«cdˇ»
6798 «eˇ»f«ghˇ»
6799 «iˇ»jkl
6800 «mˇ»nop"#
6801 ));
6802
6803 cx.update_editor(|editor, window, cx| {
6804 editor.add_selection_below(&Default::default(), window, cx);
6805 });
6806
6807 // test multiple selection retrieves back correctly
6808 cx.assert_editor_state(indoc!(
6809 r#"abcd
6810 ef«ghˇ»
6811 «iˇ»jkl
6812 «mˇ»nop"#
6813 ));
6814
6815 cx.update_editor(|editor, window, cx| {
6816 editor.add_selection_below(&Default::default(), window, cx);
6817 });
6818
6819 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
6820 cx.assert_editor_state(indoc!(
6821 r#"abcd
6822 ef«ghˇ»
6823 ij«klˇ»
6824 «mˇ»nop"#
6825 ));
6826
6827 cx.update_editor(|editor, window, cx| {
6828 editor.undo_selection(&Default::default(), window, cx);
6829 });
6830
6831 // test undo
6832 cx.assert_editor_state(indoc!(
6833 r#"abcd
6834 ef«ghˇ»
6835 «iˇ»jkl
6836 «mˇ»nop"#
6837 ));
6838
6839 cx.update_editor(|editor, window, cx| {
6840 editor.redo_selection(&Default::default(), window, cx);
6841 });
6842
6843 // test redo
6844 cx.assert_editor_state(indoc!(
6845 r#"abcd
6846 ef«ghˇ»
6847 ij«klˇ»
6848 «mˇ»nop"#
6849 ));
6850}
6851
6852#[gpui::test]
6853async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
6854 init_test(cx, |_| {});
6855 let mut cx = EditorTestContext::new(cx).await;
6856
6857 cx.set_state(indoc!(
6858 r#"line onˇe
6859 liˇne two
6860 line three
6861 line four"#
6862 ));
6863
6864 cx.update_editor(|editor, window, cx| {
6865 editor.add_selection_below(&Default::default(), window, cx);
6866 editor.add_selection_below(&Default::default(), window, cx);
6867 editor.add_selection_below(&Default::default(), window, cx);
6868 });
6869
6870 // initial state with two multi cursor groups
6871 cx.assert_editor_state(indoc!(
6872 r#"line onˇe
6873 liˇne twˇo
6874 liˇne thˇree
6875 liˇne foˇur"#
6876 ));
6877
6878 // add single cursor in middle - simulate opt click
6879 cx.update_editor(|editor, window, cx| {
6880 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
6881 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6882 editor.end_selection(window, cx);
6883 });
6884
6885 cx.assert_editor_state(indoc!(
6886 r#"line onˇe
6887 liˇne twˇo
6888 liˇneˇ thˇree
6889 liˇne foˇur"#
6890 ));
6891
6892 cx.update_editor(|editor, window, cx| {
6893 editor.add_selection_above(&Default::default(), window, cx);
6894 });
6895
6896 // test new added selection expands above and existing selection shrinks
6897 cx.assert_editor_state(indoc!(
6898 r#"line onˇe
6899 liˇneˇ twˇo
6900 liˇneˇ thˇree
6901 line four"#
6902 ));
6903
6904 cx.update_editor(|editor, window, cx| {
6905 editor.add_selection_above(&Default::default(), window, cx);
6906 });
6907
6908 // test new added selection expands above and existing selection shrinks
6909 cx.assert_editor_state(indoc!(
6910 r#"lineˇ onˇe
6911 liˇneˇ twˇo
6912 lineˇ three
6913 line four"#
6914 ));
6915
6916 // intial state with two selection groups
6917 cx.set_state(indoc!(
6918 r#"abcd
6919 ef«ghˇ»
6920 ijkl
6921 «mˇ»nop"#
6922 ));
6923
6924 cx.update_editor(|editor, window, cx| {
6925 editor.add_selection_above(&Default::default(), window, cx);
6926 editor.add_selection_above(&Default::default(), window, cx);
6927 });
6928
6929 cx.assert_editor_state(indoc!(
6930 r#"ab«cdˇ»
6931 «eˇ»f«ghˇ»
6932 «iˇ»jkl
6933 «mˇ»nop"#
6934 ));
6935
6936 // add single selection in middle - simulate opt drag
6937 cx.update_editor(|editor, window, cx| {
6938 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
6939 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6940 editor.update_selection(
6941 DisplayPoint::new(DisplayRow(2), 4),
6942 0,
6943 gpui::Point::<f32>::default(),
6944 window,
6945 cx,
6946 );
6947 editor.end_selection(window, cx);
6948 });
6949
6950 cx.assert_editor_state(indoc!(
6951 r#"ab«cdˇ»
6952 «eˇ»f«ghˇ»
6953 «iˇ»jk«lˇ»
6954 «mˇ»nop"#
6955 ));
6956
6957 cx.update_editor(|editor, window, cx| {
6958 editor.add_selection_below(&Default::default(), window, cx);
6959 });
6960
6961 // test new added selection expands below, others shrinks from above
6962 cx.assert_editor_state(indoc!(
6963 r#"abcd
6964 ef«ghˇ»
6965 «iˇ»jk«lˇ»
6966 «mˇ»no«pˇ»"#
6967 ));
6968}
6969
6970#[gpui::test]
6971async fn test_select_next(cx: &mut TestAppContext) {
6972 init_test(cx, |_| {});
6973
6974 let mut cx = EditorTestContext::new(cx).await;
6975 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6976
6977 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6978 .unwrap();
6979 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6980
6981 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6982 .unwrap();
6983 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6984
6985 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6986 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6987
6988 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6989 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6990
6991 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6992 .unwrap();
6993 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6994
6995 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6996 .unwrap();
6997 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6998
6999 // Test selection direction should be preserved
7000 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7001
7002 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7003 .unwrap();
7004 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
7005}
7006
7007#[gpui::test]
7008async fn test_select_all_matches(cx: &mut TestAppContext) {
7009 init_test(cx, |_| {});
7010
7011 let mut cx = EditorTestContext::new(cx).await;
7012
7013 // Test caret-only selections
7014 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7015 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7016 .unwrap();
7017 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7018
7019 // Test left-to-right selections
7020 cx.set_state("abc\n«abcˇ»\nabc");
7021 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7022 .unwrap();
7023 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
7024
7025 // Test right-to-left selections
7026 cx.set_state("abc\n«ˇabc»\nabc");
7027 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7028 .unwrap();
7029 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
7030
7031 // Test selecting whitespace with caret selection
7032 cx.set_state("abc\nˇ abc\nabc");
7033 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7034 .unwrap();
7035 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7036
7037 // Test selecting whitespace with left-to-right selection
7038 cx.set_state("abc\n«ˇ »abc\nabc");
7039 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7040 .unwrap();
7041 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
7042
7043 // Test no matches with right-to-left selection
7044 cx.set_state("abc\n« ˇ»abc\nabc");
7045 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7046 .unwrap();
7047 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7048
7049 // Test with a single word and clip_at_line_ends=true (#29823)
7050 cx.set_state("aˇbc");
7051 cx.update_editor(|e, window, cx| {
7052 e.set_clip_at_line_ends(true, cx);
7053 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
7054 e.set_clip_at_line_ends(false, cx);
7055 });
7056 cx.assert_editor_state("«abcˇ»");
7057}
7058
7059#[gpui::test]
7060async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
7061 init_test(cx, |_| {});
7062
7063 let mut cx = EditorTestContext::new(cx).await;
7064
7065 let large_body_1 = "\nd".repeat(200);
7066 let large_body_2 = "\ne".repeat(200);
7067
7068 cx.set_state(&format!(
7069 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
7070 ));
7071 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
7072 let scroll_position = editor.scroll_position(cx);
7073 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
7074 scroll_position
7075 });
7076
7077 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7078 .unwrap();
7079 cx.assert_editor_state(&format!(
7080 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
7081 ));
7082 let scroll_position_after_selection =
7083 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
7084 assert_eq!(
7085 initial_scroll_position, scroll_position_after_selection,
7086 "Scroll position should not change after selecting all matches"
7087 );
7088}
7089
7090#[gpui::test]
7091async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
7092 init_test(cx, |_| {});
7093
7094 let mut cx = EditorLspTestContext::new_rust(
7095 lsp::ServerCapabilities {
7096 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7097 ..Default::default()
7098 },
7099 cx,
7100 )
7101 .await;
7102
7103 cx.set_state(indoc! {"
7104 line 1
7105 line 2
7106 linˇe 3
7107 line 4
7108 line 5
7109 "});
7110
7111 // Make an edit
7112 cx.update_editor(|editor, window, cx| {
7113 editor.handle_input("X", window, cx);
7114 });
7115
7116 // Move cursor to a different position
7117 cx.update_editor(|editor, window, cx| {
7118 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7119 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
7120 });
7121 });
7122
7123 cx.assert_editor_state(indoc! {"
7124 line 1
7125 line 2
7126 linXe 3
7127 line 4
7128 liˇne 5
7129 "});
7130
7131 cx.lsp
7132 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
7133 Ok(Some(vec![lsp::TextEdit::new(
7134 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
7135 "PREFIX ".to_string(),
7136 )]))
7137 });
7138
7139 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
7140 .unwrap()
7141 .await
7142 .unwrap();
7143
7144 cx.assert_editor_state(indoc! {"
7145 PREFIX line 1
7146 line 2
7147 linXe 3
7148 line 4
7149 liˇne 5
7150 "});
7151
7152 // Undo formatting
7153 cx.update_editor(|editor, window, cx| {
7154 editor.undo(&Default::default(), window, cx);
7155 });
7156
7157 // Verify cursor moved back to position after edit
7158 cx.assert_editor_state(indoc! {"
7159 line 1
7160 line 2
7161 linXˇe 3
7162 line 4
7163 line 5
7164 "});
7165}
7166
7167#[gpui::test]
7168async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) {
7169 init_test(cx, |_| {});
7170
7171 let mut cx = EditorTestContext::new(cx).await;
7172
7173 let provider = cx.new(|_| FakeInlineCompletionProvider::default());
7174 cx.update_editor(|editor, window, cx| {
7175 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
7176 });
7177
7178 cx.set_state(indoc! {"
7179 line 1
7180 line 2
7181 linˇe 3
7182 line 4
7183 line 5
7184 line 6
7185 line 7
7186 line 8
7187 line 9
7188 line 10
7189 "});
7190
7191 let snapshot = cx.buffer_snapshot();
7192 let edit_position = snapshot.anchor_after(Point::new(2, 4));
7193
7194 cx.update(|_, cx| {
7195 provider.update(cx, |provider, _| {
7196 provider.set_inline_completion(Some(inline_completion::InlineCompletion {
7197 id: None,
7198 edits: vec![(edit_position..edit_position, "X".into())],
7199 edit_preview: None,
7200 }))
7201 })
7202 });
7203
7204 cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
7205 cx.update_editor(|editor, window, cx| {
7206 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
7207 });
7208
7209 cx.assert_editor_state(indoc! {"
7210 line 1
7211 line 2
7212 lineXˇ 3
7213 line 4
7214 line 5
7215 line 6
7216 line 7
7217 line 8
7218 line 9
7219 line 10
7220 "});
7221
7222 cx.update_editor(|editor, window, cx| {
7223 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7224 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
7225 });
7226 });
7227
7228 cx.assert_editor_state(indoc! {"
7229 line 1
7230 line 2
7231 lineX 3
7232 line 4
7233 line 5
7234 line 6
7235 line 7
7236 line 8
7237 line 9
7238 liˇne 10
7239 "});
7240
7241 cx.update_editor(|editor, window, cx| {
7242 editor.undo(&Default::default(), window, cx);
7243 });
7244
7245 cx.assert_editor_state(indoc! {"
7246 line 1
7247 line 2
7248 lineˇ 3
7249 line 4
7250 line 5
7251 line 6
7252 line 7
7253 line 8
7254 line 9
7255 line 10
7256 "});
7257}
7258
7259#[gpui::test]
7260async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
7261 init_test(cx, |_| {});
7262
7263 let mut cx = EditorTestContext::new(cx).await;
7264 cx.set_state(
7265 r#"let foo = 2;
7266lˇet foo = 2;
7267let fooˇ = 2;
7268let foo = 2;
7269let foo = ˇ2;"#,
7270 );
7271
7272 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7273 .unwrap();
7274 cx.assert_editor_state(
7275 r#"let foo = 2;
7276«letˇ» foo = 2;
7277let «fooˇ» = 2;
7278let foo = 2;
7279let foo = «2ˇ»;"#,
7280 );
7281
7282 // noop for multiple selections with different contents
7283 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7284 .unwrap();
7285 cx.assert_editor_state(
7286 r#"let foo = 2;
7287«letˇ» foo = 2;
7288let «fooˇ» = 2;
7289let foo = 2;
7290let foo = «2ˇ»;"#,
7291 );
7292
7293 // Test last selection direction should be preserved
7294 cx.set_state(
7295 r#"let foo = 2;
7296let foo = 2;
7297let «fooˇ» = 2;
7298let «ˇfoo» = 2;
7299let foo = 2;"#,
7300 );
7301
7302 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7303 .unwrap();
7304 cx.assert_editor_state(
7305 r#"let foo = 2;
7306let foo = 2;
7307let «fooˇ» = 2;
7308let «ˇfoo» = 2;
7309let «ˇfoo» = 2;"#,
7310 );
7311}
7312
7313#[gpui::test]
7314async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
7315 init_test(cx, |_| {});
7316
7317 let mut cx =
7318 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
7319
7320 cx.assert_editor_state(indoc! {"
7321 ˇbbb
7322 ccc
7323
7324 bbb
7325 ccc
7326 "});
7327 cx.dispatch_action(SelectPrevious::default());
7328 cx.assert_editor_state(indoc! {"
7329 «bbbˇ»
7330 ccc
7331
7332 bbb
7333 ccc
7334 "});
7335 cx.dispatch_action(SelectPrevious::default());
7336 cx.assert_editor_state(indoc! {"
7337 «bbbˇ»
7338 ccc
7339
7340 «bbbˇ»
7341 ccc
7342 "});
7343}
7344
7345#[gpui::test]
7346async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
7347 init_test(cx, |_| {});
7348
7349 let mut cx = EditorTestContext::new(cx).await;
7350 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7351
7352 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7353 .unwrap();
7354 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7355
7356 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7357 .unwrap();
7358 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7359
7360 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7361 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7362
7363 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7364 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7365
7366 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7367 .unwrap();
7368 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
7369
7370 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7371 .unwrap();
7372 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7373}
7374
7375#[gpui::test]
7376async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
7377 init_test(cx, |_| {});
7378
7379 let mut cx = EditorTestContext::new(cx).await;
7380 cx.set_state("aˇ");
7381
7382 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7383 .unwrap();
7384 cx.assert_editor_state("«aˇ»");
7385 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7386 .unwrap();
7387 cx.assert_editor_state("«aˇ»");
7388}
7389
7390#[gpui::test]
7391async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
7392 init_test(cx, |_| {});
7393
7394 let mut cx = EditorTestContext::new(cx).await;
7395 cx.set_state(
7396 r#"let foo = 2;
7397lˇet foo = 2;
7398let fooˇ = 2;
7399let foo = 2;
7400let foo = ˇ2;"#,
7401 );
7402
7403 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7404 .unwrap();
7405 cx.assert_editor_state(
7406 r#"let foo = 2;
7407«letˇ» foo = 2;
7408let «fooˇ» = 2;
7409let foo = 2;
7410let foo = «2ˇ»;"#,
7411 );
7412
7413 // noop for multiple selections with different contents
7414 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7415 .unwrap();
7416 cx.assert_editor_state(
7417 r#"let foo = 2;
7418«letˇ» foo = 2;
7419let «fooˇ» = 2;
7420let foo = 2;
7421let foo = «2ˇ»;"#,
7422 );
7423}
7424
7425#[gpui::test]
7426async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
7427 init_test(cx, |_| {});
7428
7429 let mut cx = EditorTestContext::new(cx).await;
7430 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7431
7432 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7433 .unwrap();
7434 // selection direction is preserved
7435 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7436
7437 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7438 .unwrap();
7439 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7440
7441 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7442 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7443
7444 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7445 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7446
7447 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7448 .unwrap();
7449 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
7450
7451 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7452 .unwrap();
7453 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
7454}
7455
7456#[gpui::test]
7457async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
7458 init_test(cx, |_| {});
7459
7460 let language = Arc::new(Language::new(
7461 LanguageConfig::default(),
7462 Some(tree_sitter_rust::LANGUAGE.into()),
7463 ));
7464
7465 let text = r#"
7466 use mod1::mod2::{mod3, mod4};
7467
7468 fn fn_1(param1: bool, param2: &str) {
7469 let var1 = "text";
7470 }
7471 "#
7472 .unindent();
7473
7474 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7475 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7476 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7477
7478 editor
7479 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7480 .await;
7481
7482 editor.update_in(cx, |editor, window, cx| {
7483 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7484 s.select_display_ranges([
7485 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
7486 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
7487 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
7488 ]);
7489 });
7490 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7491 });
7492 editor.update(cx, |editor, cx| {
7493 assert_text_with_selections(
7494 editor,
7495 indoc! {r#"
7496 use mod1::mod2::{mod3, «mod4ˇ»};
7497
7498 fn fn_1«ˇ(param1: bool, param2: &str)» {
7499 let var1 = "«ˇtext»";
7500 }
7501 "#},
7502 cx,
7503 );
7504 });
7505
7506 editor.update_in(cx, |editor, window, cx| {
7507 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7508 });
7509 editor.update(cx, |editor, cx| {
7510 assert_text_with_selections(
7511 editor,
7512 indoc! {r#"
7513 use mod1::mod2::«{mod3, mod4}ˇ»;
7514
7515 «ˇfn fn_1(param1: bool, param2: &str) {
7516 let var1 = "text";
7517 }»
7518 "#},
7519 cx,
7520 );
7521 });
7522
7523 editor.update_in(cx, |editor, window, cx| {
7524 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7525 });
7526 assert_eq!(
7527 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7528 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7529 );
7530
7531 // Trying to expand the selected syntax node one more time has no effect.
7532 editor.update_in(cx, |editor, window, cx| {
7533 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7534 });
7535 assert_eq!(
7536 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7537 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7538 );
7539
7540 editor.update_in(cx, |editor, window, cx| {
7541 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7542 });
7543 editor.update(cx, |editor, cx| {
7544 assert_text_with_selections(
7545 editor,
7546 indoc! {r#"
7547 use mod1::mod2::«{mod3, mod4}ˇ»;
7548
7549 «ˇfn fn_1(param1: bool, param2: &str) {
7550 let var1 = "text";
7551 }»
7552 "#},
7553 cx,
7554 );
7555 });
7556
7557 editor.update_in(cx, |editor, window, cx| {
7558 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7559 });
7560 editor.update(cx, |editor, cx| {
7561 assert_text_with_selections(
7562 editor,
7563 indoc! {r#"
7564 use mod1::mod2::{mod3, «mod4ˇ»};
7565
7566 fn fn_1«ˇ(param1: bool, param2: &str)» {
7567 let var1 = "«ˇtext»";
7568 }
7569 "#},
7570 cx,
7571 );
7572 });
7573
7574 editor.update_in(cx, |editor, window, cx| {
7575 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7576 });
7577 editor.update(cx, |editor, cx| {
7578 assert_text_with_selections(
7579 editor,
7580 indoc! {r#"
7581 use mod1::mod2::{mod3, mo«ˇ»d4};
7582
7583 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7584 let var1 = "te«ˇ»xt";
7585 }
7586 "#},
7587 cx,
7588 );
7589 });
7590
7591 // Trying to shrink the selected syntax node one more time has no effect.
7592 editor.update_in(cx, |editor, window, cx| {
7593 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7594 });
7595 editor.update_in(cx, |editor, _, cx| {
7596 assert_text_with_selections(
7597 editor,
7598 indoc! {r#"
7599 use mod1::mod2::{mod3, mo«ˇ»d4};
7600
7601 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7602 let var1 = "te«ˇ»xt";
7603 }
7604 "#},
7605 cx,
7606 );
7607 });
7608
7609 // Ensure that we keep expanding the selection if the larger selection starts or ends within
7610 // a fold.
7611 editor.update_in(cx, |editor, window, cx| {
7612 editor.fold_creases(
7613 vec![
7614 Crease::simple(
7615 Point::new(0, 21)..Point::new(0, 24),
7616 FoldPlaceholder::test(),
7617 ),
7618 Crease::simple(
7619 Point::new(3, 20)..Point::new(3, 22),
7620 FoldPlaceholder::test(),
7621 ),
7622 ],
7623 true,
7624 window,
7625 cx,
7626 );
7627 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7628 });
7629 editor.update(cx, |editor, cx| {
7630 assert_text_with_selections(
7631 editor,
7632 indoc! {r#"
7633 use mod1::mod2::«{mod3, mod4}ˇ»;
7634
7635 fn fn_1«ˇ(param1: bool, param2: &str)» {
7636 let var1 = "«ˇtext»";
7637 }
7638 "#},
7639 cx,
7640 );
7641 });
7642}
7643
7644#[gpui::test]
7645async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
7646 init_test(cx, |_| {});
7647
7648 let language = Arc::new(Language::new(
7649 LanguageConfig::default(),
7650 Some(tree_sitter_rust::LANGUAGE.into()),
7651 ));
7652
7653 let text = "let a = 2;";
7654
7655 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7656 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7657 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7658
7659 editor
7660 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7661 .await;
7662
7663 // Test case 1: Cursor at end of word
7664 editor.update_in(cx, |editor, window, cx| {
7665 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7666 s.select_display_ranges([
7667 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
7668 ]);
7669 });
7670 });
7671 editor.update(cx, |editor, cx| {
7672 assert_text_with_selections(editor, "let aˇ = 2;", cx);
7673 });
7674 editor.update_in(cx, |editor, window, cx| {
7675 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7676 });
7677 editor.update(cx, |editor, cx| {
7678 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
7679 });
7680 editor.update_in(cx, |editor, window, cx| {
7681 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7682 });
7683 editor.update(cx, |editor, cx| {
7684 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7685 });
7686
7687 // Test case 2: Cursor at end of statement
7688 editor.update_in(cx, |editor, window, cx| {
7689 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7690 s.select_display_ranges([
7691 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
7692 ]);
7693 });
7694 });
7695 editor.update(cx, |editor, cx| {
7696 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
7697 });
7698 editor.update_in(cx, |editor, window, cx| {
7699 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7700 });
7701 editor.update(cx, |editor, cx| {
7702 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7703 });
7704}
7705
7706#[gpui::test]
7707async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
7708 init_test(cx, |_| {});
7709
7710 let language = Arc::new(Language::new(
7711 LanguageConfig::default(),
7712 Some(tree_sitter_rust::LANGUAGE.into()),
7713 ));
7714
7715 let text = r#"
7716 use mod1::mod2::{mod3, mod4};
7717
7718 fn fn_1(param1: bool, param2: &str) {
7719 let var1 = "hello world";
7720 }
7721 "#
7722 .unindent();
7723
7724 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7725 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7726 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7727
7728 editor
7729 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7730 .await;
7731
7732 // Test 1: Cursor on a letter of a string word
7733 editor.update_in(cx, |editor, window, cx| {
7734 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7735 s.select_display_ranges([
7736 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
7737 ]);
7738 });
7739 });
7740 editor.update_in(cx, |editor, window, cx| {
7741 assert_text_with_selections(
7742 editor,
7743 indoc! {r#"
7744 use mod1::mod2::{mod3, mod4};
7745
7746 fn fn_1(param1: bool, param2: &str) {
7747 let var1 = "hˇello world";
7748 }
7749 "#},
7750 cx,
7751 );
7752 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7753 assert_text_with_selections(
7754 editor,
7755 indoc! {r#"
7756 use mod1::mod2::{mod3, mod4};
7757
7758 fn fn_1(param1: bool, param2: &str) {
7759 let var1 = "«ˇhello» world";
7760 }
7761 "#},
7762 cx,
7763 );
7764 });
7765
7766 // Test 2: Partial selection within a word
7767 editor.update_in(cx, |editor, window, cx| {
7768 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7769 s.select_display_ranges([
7770 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7771 ]);
7772 });
7773 });
7774 editor.update_in(cx, |editor, window, cx| {
7775 assert_text_with_selections(
7776 editor,
7777 indoc! {r#"
7778 use mod1::mod2::{mod3, mod4};
7779
7780 fn fn_1(param1: bool, param2: &str) {
7781 let var1 = "h«elˇ»lo world";
7782 }
7783 "#},
7784 cx,
7785 );
7786 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7787 assert_text_with_selections(
7788 editor,
7789 indoc! {r#"
7790 use mod1::mod2::{mod3, mod4};
7791
7792 fn fn_1(param1: bool, param2: &str) {
7793 let var1 = "«ˇhello» world";
7794 }
7795 "#},
7796 cx,
7797 );
7798 });
7799
7800 // Test 3: Complete word already selected
7801 editor.update_in(cx, |editor, window, cx| {
7802 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7803 s.select_display_ranges([
7804 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7805 ]);
7806 });
7807 });
7808 editor.update_in(cx, |editor, window, cx| {
7809 assert_text_with_selections(
7810 editor,
7811 indoc! {r#"
7812 use mod1::mod2::{mod3, mod4};
7813
7814 fn fn_1(param1: bool, param2: &str) {
7815 let var1 = "«helloˇ» world";
7816 }
7817 "#},
7818 cx,
7819 );
7820 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7821 assert_text_with_selections(
7822 editor,
7823 indoc! {r#"
7824 use mod1::mod2::{mod3, mod4};
7825
7826 fn fn_1(param1: bool, param2: &str) {
7827 let var1 = "«hello worldˇ»";
7828 }
7829 "#},
7830 cx,
7831 );
7832 });
7833
7834 // Test 4: Selection spanning across words
7835 editor.update_in(cx, |editor, window, cx| {
7836 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7837 s.select_display_ranges([
7838 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7839 ]);
7840 });
7841 });
7842 editor.update_in(cx, |editor, window, cx| {
7843 assert_text_with_selections(
7844 editor,
7845 indoc! {r#"
7846 use mod1::mod2::{mod3, mod4};
7847
7848 fn fn_1(param1: bool, param2: &str) {
7849 let var1 = "hel«lo woˇ»rld";
7850 }
7851 "#},
7852 cx,
7853 );
7854 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7855 assert_text_with_selections(
7856 editor,
7857 indoc! {r#"
7858 use mod1::mod2::{mod3, mod4};
7859
7860 fn fn_1(param1: bool, param2: &str) {
7861 let var1 = "«ˇhello world»";
7862 }
7863 "#},
7864 cx,
7865 );
7866 });
7867
7868 // Test 5: Expansion beyond string
7869 editor.update_in(cx, |editor, window, cx| {
7870 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7871 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7872 assert_text_with_selections(
7873 editor,
7874 indoc! {r#"
7875 use mod1::mod2::{mod3, mod4};
7876
7877 fn fn_1(param1: bool, param2: &str) {
7878 «ˇlet var1 = "hello world";»
7879 }
7880 "#},
7881 cx,
7882 );
7883 });
7884}
7885
7886#[gpui::test]
7887async fn test_fold_function_bodies(cx: &mut TestAppContext) {
7888 init_test(cx, |_| {});
7889
7890 let base_text = r#"
7891 impl A {
7892 // this is an uncommitted comment
7893
7894 fn b() {
7895 c();
7896 }
7897
7898 // this is another uncommitted comment
7899
7900 fn d() {
7901 // e
7902 // f
7903 }
7904 }
7905
7906 fn g() {
7907 // h
7908 }
7909 "#
7910 .unindent();
7911
7912 let text = r#"
7913 ˇimpl A {
7914
7915 fn b() {
7916 c();
7917 }
7918
7919 fn d() {
7920 // e
7921 // f
7922 }
7923 }
7924
7925 fn g() {
7926 // h
7927 }
7928 "#
7929 .unindent();
7930
7931 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7932 cx.set_state(&text);
7933 cx.set_head_text(&base_text);
7934 cx.update_editor(|editor, window, cx| {
7935 editor.expand_all_diff_hunks(&Default::default(), window, cx);
7936 });
7937
7938 cx.assert_state_with_diff(
7939 "
7940 ˇimpl A {
7941 - // this is an uncommitted comment
7942
7943 fn b() {
7944 c();
7945 }
7946
7947 - // this is another uncommitted comment
7948 -
7949 fn d() {
7950 // e
7951 // f
7952 }
7953 }
7954
7955 fn g() {
7956 // h
7957 }
7958 "
7959 .unindent(),
7960 );
7961
7962 let expected_display_text = "
7963 impl A {
7964 // this is an uncommitted comment
7965
7966 fn b() {
7967 ⋯
7968 }
7969
7970 // this is another uncommitted comment
7971
7972 fn d() {
7973 ⋯
7974 }
7975 }
7976
7977 fn g() {
7978 ⋯
7979 }
7980 "
7981 .unindent();
7982
7983 cx.update_editor(|editor, window, cx| {
7984 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
7985 assert_eq!(editor.display_text(cx), expected_display_text);
7986 });
7987}
7988
7989#[gpui::test]
7990async fn test_autoindent(cx: &mut TestAppContext) {
7991 init_test(cx, |_| {});
7992
7993 let language = Arc::new(
7994 Language::new(
7995 LanguageConfig {
7996 brackets: BracketPairConfig {
7997 pairs: vec![
7998 BracketPair {
7999 start: "{".to_string(),
8000 end: "}".to_string(),
8001 close: false,
8002 surround: false,
8003 newline: true,
8004 },
8005 BracketPair {
8006 start: "(".to_string(),
8007 end: ")".to_string(),
8008 close: false,
8009 surround: false,
8010 newline: true,
8011 },
8012 ],
8013 ..Default::default()
8014 },
8015 ..Default::default()
8016 },
8017 Some(tree_sitter_rust::LANGUAGE.into()),
8018 )
8019 .with_indents_query(
8020 r#"
8021 (_ "(" ")" @end) @indent
8022 (_ "{" "}" @end) @indent
8023 "#,
8024 )
8025 .unwrap(),
8026 );
8027
8028 let text = "fn a() {}";
8029
8030 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8031 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8032 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8033 editor
8034 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8035 .await;
8036
8037 editor.update_in(cx, |editor, window, cx| {
8038 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8039 s.select_ranges([5..5, 8..8, 9..9])
8040 });
8041 editor.newline(&Newline, window, cx);
8042 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
8043 assert_eq!(
8044 editor.selections.ranges(cx),
8045 &[
8046 Point::new(1, 4)..Point::new(1, 4),
8047 Point::new(3, 4)..Point::new(3, 4),
8048 Point::new(5, 0)..Point::new(5, 0)
8049 ]
8050 );
8051 });
8052}
8053
8054#[gpui::test]
8055async fn test_autoindent_selections(cx: &mut TestAppContext) {
8056 init_test(cx, |_| {});
8057
8058 {
8059 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8060 cx.set_state(indoc! {"
8061 impl A {
8062
8063 fn b() {}
8064
8065 «fn c() {
8066
8067 }ˇ»
8068 }
8069 "});
8070
8071 cx.update_editor(|editor, window, cx| {
8072 editor.autoindent(&Default::default(), window, cx);
8073 });
8074
8075 cx.assert_editor_state(indoc! {"
8076 impl A {
8077
8078 fn b() {}
8079
8080 «fn c() {
8081
8082 }ˇ»
8083 }
8084 "});
8085 }
8086
8087 {
8088 let mut cx = EditorTestContext::new_multibuffer(
8089 cx,
8090 [indoc! { "
8091 impl A {
8092 «
8093 // a
8094 fn b(){}
8095 »
8096 «
8097 }
8098 fn c(){}
8099 »
8100 "}],
8101 );
8102
8103 let buffer = cx.update_editor(|editor, _, cx| {
8104 let buffer = editor.buffer().update(cx, |buffer, _| {
8105 buffer.all_buffers().iter().next().unwrap().clone()
8106 });
8107 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
8108 buffer
8109 });
8110
8111 cx.run_until_parked();
8112 cx.update_editor(|editor, window, cx| {
8113 editor.select_all(&Default::default(), window, cx);
8114 editor.autoindent(&Default::default(), window, cx)
8115 });
8116 cx.run_until_parked();
8117
8118 cx.update(|_, cx| {
8119 assert_eq!(
8120 buffer.read(cx).text(),
8121 indoc! { "
8122 impl A {
8123
8124 // a
8125 fn b(){}
8126
8127
8128 }
8129 fn c(){}
8130
8131 " }
8132 )
8133 });
8134 }
8135}
8136
8137#[gpui::test]
8138async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
8139 init_test(cx, |_| {});
8140
8141 let mut cx = EditorTestContext::new(cx).await;
8142
8143 let language = Arc::new(Language::new(
8144 LanguageConfig {
8145 brackets: BracketPairConfig {
8146 pairs: vec![
8147 BracketPair {
8148 start: "{".to_string(),
8149 end: "}".to_string(),
8150 close: true,
8151 surround: true,
8152 newline: true,
8153 },
8154 BracketPair {
8155 start: "(".to_string(),
8156 end: ")".to_string(),
8157 close: true,
8158 surround: true,
8159 newline: true,
8160 },
8161 BracketPair {
8162 start: "/*".to_string(),
8163 end: " */".to_string(),
8164 close: true,
8165 surround: true,
8166 newline: true,
8167 },
8168 BracketPair {
8169 start: "[".to_string(),
8170 end: "]".to_string(),
8171 close: false,
8172 surround: false,
8173 newline: true,
8174 },
8175 BracketPair {
8176 start: "\"".to_string(),
8177 end: "\"".to_string(),
8178 close: true,
8179 surround: true,
8180 newline: false,
8181 },
8182 BracketPair {
8183 start: "<".to_string(),
8184 end: ">".to_string(),
8185 close: false,
8186 surround: true,
8187 newline: true,
8188 },
8189 ],
8190 ..Default::default()
8191 },
8192 autoclose_before: "})]".to_string(),
8193 ..Default::default()
8194 },
8195 Some(tree_sitter_rust::LANGUAGE.into()),
8196 ));
8197
8198 cx.language_registry().add(language.clone());
8199 cx.update_buffer(|buffer, cx| {
8200 buffer.set_language(Some(language), cx);
8201 });
8202
8203 cx.set_state(
8204 &r#"
8205 🏀ˇ
8206 εˇ
8207 ❤️ˇ
8208 "#
8209 .unindent(),
8210 );
8211
8212 // autoclose multiple nested brackets at multiple cursors
8213 cx.update_editor(|editor, window, cx| {
8214 editor.handle_input("{", window, cx);
8215 editor.handle_input("{", window, cx);
8216 editor.handle_input("{", window, cx);
8217 });
8218 cx.assert_editor_state(
8219 &"
8220 🏀{{{ˇ}}}
8221 ε{{{ˇ}}}
8222 ❤️{{{ˇ}}}
8223 "
8224 .unindent(),
8225 );
8226
8227 // insert a different closing bracket
8228 cx.update_editor(|editor, window, cx| {
8229 editor.handle_input(")", window, cx);
8230 });
8231 cx.assert_editor_state(
8232 &"
8233 🏀{{{)ˇ}}}
8234 ε{{{)ˇ}}}
8235 ❤️{{{)ˇ}}}
8236 "
8237 .unindent(),
8238 );
8239
8240 // skip over the auto-closed brackets when typing a closing bracket
8241 cx.update_editor(|editor, window, cx| {
8242 editor.move_right(&MoveRight, window, cx);
8243 editor.handle_input("}", window, cx);
8244 editor.handle_input("}", window, cx);
8245 editor.handle_input("}", window, cx);
8246 });
8247 cx.assert_editor_state(
8248 &"
8249 🏀{{{)}}}}ˇ
8250 ε{{{)}}}}ˇ
8251 ❤️{{{)}}}}ˇ
8252 "
8253 .unindent(),
8254 );
8255
8256 // autoclose multi-character pairs
8257 cx.set_state(
8258 &"
8259 ˇ
8260 ˇ
8261 "
8262 .unindent(),
8263 );
8264 cx.update_editor(|editor, window, cx| {
8265 editor.handle_input("/", window, cx);
8266 editor.handle_input("*", window, cx);
8267 });
8268 cx.assert_editor_state(
8269 &"
8270 /*ˇ */
8271 /*ˇ */
8272 "
8273 .unindent(),
8274 );
8275
8276 // one cursor autocloses a multi-character pair, one cursor
8277 // does not autoclose.
8278 cx.set_state(
8279 &"
8280 /ˇ
8281 ˇ
8282 "
8283 .unindent(),
8284 );
8285 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
8286 cx.assert_editor_state(
8287 &"
8288 /*ˇ */
8289 *ˇ
8290 "
8291 .unindent(),
8292 );
8293
8294 // Don't autoclose if the next character isn't whitespace and isn't
8295 // listed in the language's "autoclose_before" section.
8296 cx.set_state("ˇa b");
8297 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8298 cx.assert_editor_state("{ˇa b");
8299
8300 // Don't autoclose if `close` is false for the bracket pair
8301 cx.set_state("ˇ");
8302 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
8303 cx.assert_editor_state("[ˇ");
8304
8305 // Surround with brackets if text is selected
8306 cx.set_state("«aˇ» b");
8307 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8308 cx.assert_editor_state("{«aˇ»} b");
8309
8310 // Autoclose when not immediately after a word character
8311 cx.set_state("a ˇ");
8312 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8313 cx.assert_editor_state("a \"ˇ\"");
8314
8315 // Autoclose pair where the start and end characters are the same
8316 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8317 cx.assert_editor_state("a \"\"ˇ");
8318
8319 // Don't autoclose when immediately after a word character
8320 cx.set_state("aˇ");
8321 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8322 cx.assert_editor_state("a\"ˇ");
8323
8324 // Do autoclose when after a non-word character
8325 cx.set_state("{ˇ");
8326 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8327 cx.assert_editor_state("{\"ˇ\"");
8328
8329 // Non identical pairs autoclose regardless of preceding character
8330 cx.set_state("aˇ");
8331 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8332 cx.assert_editor_state("a{ˇ}");
8333
8334 // Don't autoclose pair if autoclose is disabled
8335 cx.set_state("ˇ");
8336 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8337 cx.assert_editor_state("<ˇ");
8338
8339 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
8340 cx.set_state("«aˇ» b");
8341 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8342 cx.assert_editor_state("<«aˇ»> b");
8343}
8344
8345#[gpui::test]
8346async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
8347 init_test(cx, |settings| {
8348 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8349 });
8350
8351 let mut cx = EditorTestContext::new(cx).await;
8352
8353 let language = Arc::new(Language::new(
8354 LanguageConfig {
8355 brackets: BracketPairConfig {
8356 pairs: vec![
8357 BracketPair {
8358 start: "{".to_string(),
8359 end: "}".to_string(),
8360 close: true,
8361 surround: true,
8362 newline: true,
8363 },
8364 BracketPair {
8365 start: "(".to_string(),
8366 end: ")".to_string(),
8367 close: true,
8368 surround: true,
8369 newline: true,
8370 },
8371 BracketPair {
8372 start: "[".to_string(),
8373 end: "]".to_string(),
8374 close: false,
8375 surround: false,
8376 newline: true,
8377 },
8378 ],
8379 ..Default::default()
8380 },
8381 autoclose_before: "})]".to_string(),
8382 ..Default::default()
8383 },
8384 Some(tree_sitter_rust::LANGUAGE.into()),
8385 ));
8386
8387 cx.language_registry().add(language.clone());
8388 cx.update_buffer(|buffer, cx| {
8389 buffer.set_language(Some(language), cx);
8390 });
8391
8392 cx.set_state(
8393 &"
8394 ˇ
8395 ˇ
8396 ˇ
8397 "
8398 .unindent(),
8399 );
8400
8401 // ensure only matching closing brackets are skipped over
8402 cx.update_editor(|editor, window, cx| {
8403 editor.handle_input("}", window, cx);
8404 editor.move_left(&MoveLeft, window, cx);
8405 editor.handle_input(")", window, cx);
8406 editor.move_left(&MoveLeft, window, cx);
8407 });
8408 cx.assert_editor_state(
8409 &"
8410 ˇ)}
8411 ˇ)}
8412 ˇ)}
8413 "
8414 .unindent(),
8415 );
8416
8417 // skip-over closing brackets at multiple cursors
8418 cx.update_editor(|editor, window, cx| {
8419 editor.handle_input(")", window, cx);
8420 editor.handle_input("}", window, cx);
8421 });
8422 cx.assert_editor_state(
8423 &"
8424 )}ˇ
8425 )}ˇ
8426 )}ˇ
8427 "
8428 .unindent(),
8429 );
8430
8431 // ignore non-close brackets
8432 cx.update_editor(|editor, window, cx| {
8433 editor.handle_input("]", window, cx);
8434 editor.move_left(&MoveLeft, window, cx);
8435 editor.handle_input("]", window, cx);
8436 });
8437 cx.assert_editor_state(
8438 &"
8439 )}]ˇ]
8440 )}]ˇ]
8441 )}]ˇ]
8442 "
8443 .unindent(),
8444 );
8445}
8446
8447#[gpui::test]
8448async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8449 init_test(cx, |_| {});
8450
8451 let mut cx = EditorTestContext::new(cx).await;
8452
8453 let html_language = Arc::new(
8454 Language::new(
8455 LanguageConfig {
8456 name: "HTML".into(),
8457 brackets: BracketPairConfig {
8458 pairs: vec![
8459 BracketPair {
8460 start: "<".into(),
8461 end: ">".into(),
8462 close: true,
8463 ..Default::default()
8464 },
8465 BracketPair {
8466 start: "{".into(),
8467 end: "}".into(),
8468 close: true,
8469 ..Default::default()
8470 },
8471 BracketPair {
8472 start: "(".into(),
8473 end: ")".into(),
8474 close: true,
8475 ..Default::default()
8476 },
8477 ],
8478 ..Default::default()
8479 },
8480 autoclose_before: "})]>".into(),
8481 ..Default::default()
8482 },
8483 Some(tree_sitter_html::LANGUAGE.into()),
8484 )
8485 .with_injection_query(
8486 r#"
8487 (script_element
8488 (raw_text) @injection.content
8489 (#set! injection.language "javascript"))
8490 "#,
8491 )
8492 .unwrap(),
8493 );
8494
8495 let javascript_language = Arc::new(Language::new(
8496 LanguageConfig {
8497 name: "JavaScript".into(),
8498 brackets: BracketPairConfig {
8499 pairs: vec![
8500 BracketPair {
8501 start: "/*".into(),
8502 end: " */".into(),
8503 close: true,
8504 ..Default::default()
8505 },
8506 BracketPair {
8507 start: "{".into(),
8508 end: "}".into(),
8509 close: true,
8510 ..Default::default()
8511 },
8512 BracketPair {
8513 start: "(".into(),
8514 end: ")".into(),
8515 close: true,
8516 ..Default::default()
8517 },
8518 ],
8519 ..Default::default()
8520 },
8521 autoclose_before: "})]>".into(),
8522 ..Default::default()
8523 },
8524 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8525 ));
8526
8527 cx.language_registry().add(html_language.clone());
8528 cx.language_registry().add(javascript_language.clone());
8529
8530 cx.update_buffer(|buffer, cx| {
8531 buffer.set_language(Some(html_language), cx);
8532 });
8533
8534 cx.set_state(
8535 &r#"
8536 <body>ˇ
8537 <script>
8538 var x = 1;ˇ
8539 </script>
8540 </body>ˇ
8541 "#
8542 .unindent(),
8543 );
8544
8545 // Precondition: different languages are active at different locations.
8546 cx.update_editor(|editor, window, cx| {
8547 let snapshot = editor.snapshot(window, cx);
8548 let cursors = editor.selections.ranges::<usize>(cx);
8549 let languages = cursors
8550 .iter()
8551 .map(|c| snapshot.language_at(c.start).unwrap().name())
8552 .collect::<Vec<_>>();
8553 assert_eq!(
8554 languages,
8555 &["HTML".into(), "JavaScript".into(), "HTML".into()]
8556 );
8557 });
8558
8559 // Angle brackets autoclose in HTML, but not JavaScript.
8560 cx.update_editor(|editor, window, cx| {
8561 editor.handle_input("<", window, cx);
8562 editor.handle_input("a", window, cx);
8563 });
8564 cx.assert_editor_state(
8565 &r#"
8566 <body><aˇ>
8567 <script>
8568 var x = 1;<aˇ
8569 </script>
8570 </body><aˇ>
8571 "#
8572 .unindent(),
8573 );
8574
8575 // Curly braces and parens autoclose in both HTML and JavaScript.
8576 cx.update_editor(|editor, window, cx| {
8577 editor.handle_input(" b=", window, cx);
8578 editor.handle_input("{", window, cx);
8579 editor.handle_input("c", window, cx);
8580 editor.handle_input("(", window, cx);
8581 });
8582 cx.assert_editor_state(
8583 &r#"
8584 <body><a b={c(ˇ)}>
8585 <script>
8586 var x = 1;<a b={c(ˇ)}
8587 </script>
8588 </body><a b={c(ˇ)}>
8589 "#
8590 .unindent(),
8591 );
8592
8593 // Brackets that were already autoclosed are skipped.
8594 cx.update_editor(|editor, window, cx| {
8595 editor.handle_input(")", window, cx);
8596 editor.handle_input("d", window, cx);
8597 editor.handle_input("}", window, cx);
8598 });
8599 cx.assert_editor_state(
8600 &r#"
8601 <body><a b={c()d}ˇ>
8602 <script>
8603 var x = 1;<a b={c()d}ˇ
8604 </script>
8605 </body><a b={c()d}ˇ>
8606 "#
8607 .unindent(),
8608 );
8609 cx.update_editor(|editor, window, cx| {
8610 editor.handle_input(">", window, cx);
8611 });
8612 cx.assert_editor_state(
8613 &r#"
8614 <body><a b={c()d}>ˇ
8615 <script>
8616 var x = 1;<a b={c()d}>ˇ
8617 </script>
8618 </body><a b={c()d}>ˇ
8619 "#
8620 .unindent(),
8621 );
8622
8623 // Reset
8624 cx.set_state(
8625 &r#"
8626 <body>ˇ
8627 <script>
8628 var x = 1;ˇ
8629 </script>
8630 </body>ˇ
8631 "#
8632 .unindent(),
8633 );
8634
8635 cx.update_editor(|editor, window, cx| {
8636 editor.handle_input("<", window, cx);
8637 });
8638 cx.assert_editor_state(
8639 &r#"
8640 <body><ˇ>
8641 <script>
8642 var x = 1;<ˇ
8643 </script>
8644 </body><ˇ>
8645 "#
8646 .unindent(),
8647 );
8648
8649 // When backspacing, the closing angle brackets are removed.
8650 cx.update_editor(|editor, window, cx| {
8651 editor.backspace(&Backspace, window, cx);
8652 });
8653 cx.assert_editor_state(
8654 &r#"
8655 <body>ˇ
8656 <script>
8657 var x = 1;ˇ
8658 </script>
8659 </body>ˇ
8660 "#
8661 .unindent(),
8662 );
8663
8664 // Block comments autoclose in JavaScript, but not HTML.
8665 cx.update_editor(|editor, window, cx| {
8666 editor.handle_input("/", window, cx);
8667 editor.handle_input("*", window, cx);
8668 });
8669 cx.assert_editor_state(
8670 &r#"
8671 <body>/*ˇ
8672 <script>
8673 var x = 1;/*ˇ */
8674 </script>
8675 </body>/*ˇ
8676 "#
8677 .unindent(),
8678 );
8679}
8680
8681#[gpui::test]
8682async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
8683 init_test(cx, |_| {});
8684
8685 let mut cx = EditorTestContext::new(cx).await;
8686
8687 let rust_language = Arc::new(
8688 Language::new(
8689 LanguageConfig {
8690 name: "Rust".into(),
8691 brackets: serde_json::from_value(json!([
8692 { "start": "{", "end": "}", "close": true, "newline": true },
8693 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
8694 ]))
8695 .unwrap(),
8696 autoclose_before: "})]>".into(),
8697 ..Default::default()
8698 },
8699 Some(tree_sitter_rust::LANGUAGE.into()),
8700 )
8701 .with_override_query("(string_literal) @string")
8702 .unwrap(),
8703 );
8704
8705 cx.language_registry().add(rust_language.clone());
8706 cx.update_buffer(|buffer, cx| {
8707 buffer.set_language(Some(rust_language), cx);
8708 });
8709
8710 cx.set_state(
8711 &r#"
8712 let x = ˇ
8713 "#
8714 .unindent(),
8715 );
8716
8717 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
8718 cx.update_editor(|editor, window, cx| {
8719 editor.handle_input("\"", window, cx);
8720 });
8721 cx.assert_editor_state(
8722 &r#"
8723 let x = "ˇ"
8724 "#
8725 .unindent(),
8726 );
8727
8728 // Inserting another quotation mark. The cursor moves across the existing
8729 // automatically-inserted quotation mark.
8730 cx.update_editor(|editor, window, cx| {
8731 editor.handle_input("\"", window, cx);
8732 });
8733 cx.assert_editor_state(
8734 &r#"
8735 let x = ""ˇ
8736 "#
8737 .unindent(),
8738 );
8739
8740 // Reset
8741 cx.set_state(
8742 &r#"
8743 let x = ˇ
8744 "#
8745 .unindent(),
8746 );
8747
8748 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
8749 cx.update_editor(|editor, window, cx| {
8750 editor.handle_input("\"", window, cx);
8751 editor.handle_input(" ", window, cx);
8752 editor.move_left(&Default::default(), window, cx);
8753 editor.handle_input("\\", window, cx);
8754 editor.handle_input("\"", window, cx);
8755 });
8756 cx.assert_editor_state(
8757 &r#"
8758 let x = "\"ˇ "
8759 "#
8760 .unindent(),
8761 );
8762
8763 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8764 // mark. Nothing is inserted.
8765 cx.update_editor(|editor, window, cx| {
8766 editor.move_right(&Default::default(), window, cx);
8767 editor.handle_input("\"", window, cx);
8768 });
8769 cx.assert_editor_state(
8770 &r#"
8771 let x = "\" "ˇ
8772 "#
8773 .unindent(),
8774 );
8775}
8776
8777#[gpui::test]
8778async fn test_surround_with_pair(cx: &mut TestAppContext) {
8779 init_test(cx, |_| {});
8780
8781 let language = Arc::new(Language::new(
8782 LanguageConfig {
8783 brackets: BracketPairConfig {
8784 pairs: vec![
8785 BracketPair {
8786 start: "{".to_string(),
8787 end: "}".to_string(),
8788 close: true,
8789 surround: true,
8790 newline: true,
8791 },
8792 BracketPair {
8793 start: "/* ".to_string(),
8794 end: "*/".to_string(),
8795 close: true,
8796 surround: true,
8797 ..Default::default()
8798 },
8799 ],
8800 ..Default::default()
8801 },
8802 ..Default::default()
8803 },
8804 Some(tree_sitter_rust::LANGUAGE.into()),
8805 ));
8806
8807 let text = r#"
8808 a
8809 b
8810 c
8811 "#
8812 .unindent();
8813
8814 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8815 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8816 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8817 editor
8818 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8819 .await;
8820
8821 editor.update_in(cx, |editor, window, cx| {
8822 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8823 s.select_display_ranges([
8824 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8825 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8826 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8827 ])
8828 });
8829
8830 editor.handle_input("{", window, cx);
8831 editor.handle_input("{", window, cx);
8832 editor.handle_input("{", window, cx);
8833 assert_eq!(
8834 editor.text(cx),
8835 "
8836 {{{a}}}
8837 {{{b}}}
8838 {{{c}}}
8839 "
8840 .unindent()
8841 );
8842 assert_eq!(
8843 editor.selections.display_ranges(cx),
8844 [
8845 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
8846 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
8847 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
8848 ]
8849 );
8850
8851 editor.undo(&Undo, window, cx);
8852 editor.undo(&Undo, window, cx);
8853 editor.undo(&Undo, window, cx);
8854 assert_eq!(
8855 editor.text(cx),
8856 "
8857 a
8858 b
8859 c
8860 "
8861 .unindent()
8862 );
8863 assert_eq!(
8864 editor.selections.display_ranges(cx),
8865 [
8866 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8867 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8868 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8869 ]
8870 );
8871
8872 // Ensure inserting the first character of a multi-byte bracket pair
8873 // doesn't surround the selections with the bracket.
8874 editor.handle_input("/", window, cx);
8875 assert_eq!(
8876 editor.text(cx),
8877 "
8878 /
8879 /
8880 /
8881 "
8882 .unindent()
8883 );
8884 assert_eq!(
8885 editor.selections.display_ranges(cx),
8886 [
8887 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8888 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8889 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8890 ]
8891 );
8892
8893 editor.undo(&Undo, window, cx);
8894 assert_eq!(
8895 editor.text(cx),
8896 "
8897 a
8898 b
8899 c
8900 "
8901 .unindent()
8902 );
8903 assert_eq!(
8904 editor.selections.display_ranges(cx),
8905 [
8906 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8907 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8908 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8909 ]
8910 );
8911
8912 // Ensure inserting the last character of a multi-byte bracket pair
8913 // doesn't surround the selections with the bracket.
8914 editor.handle_input("*", window, cx);
8915 assert_eq!(
8916 editor.text(cx),
8917 "
8918 *
8919 *
8920 *
8921 "
8922 .unindent()
8923 );
8924 assert_eq!(
8925 editor.selections.display_ranges(cx),
8926 [
8927 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8928 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8929 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8930 ]
8931 );
8932 });
8933}
8934
8935#[gpui::test]
8936async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
8937 init_test(cx, |_| {});
8938
8939 let language = Arc::new(Language::new(
8940 LanguageConfig {
8941 brackets: BracketPairConfig {
8942 pairs: vec![BracketPair {
8943 start: "{".to_string(),
8944 end: "}".to_string(),
8945 close: true,
8946 surround: true,
8947 newline: true,
8948 }],
8949 ..Default::default()
8950 },
8951 autoclose_before: "}".to_string(),
8952 ..Default::default()
8953 },
8954 Some(tree_sitter_rust::LANGUAGE.into()),
8955 ));
8956
8957 let text = r#"
8958 a
8959 b
8960 c
8961 "#
8962 .unindent();
8963
8964 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8965 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8966 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8967 editor
8968 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8969 .await;
8970
8971 editor.update_in(cx, |editor, window, cx| {
8972 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8973 s.select_ranges([
8974 Point::new(0, 1)..Point::new(0, 1),
8975 Point::new(1, 1)..Point::new(1, 1),
8976 Point::new(2, 1)..Point::new(2, 1),
8977 ])
8978 });
8979
8980 editor.handle_input("{", window, cx);
8981 editor.handle_input("{", window, cx);
8982 editor.handle_input("_", window, cx);
8983 assert_eq!(
8984 editor.text(cx),
8985 "
8986 a{{_}}
8987 b{{_}}
8988 c{{_}}
8989 "
8990 .unindent()
8991 );
8992 assert_eq!(
8993 editor.selections.ranges::<Point>(cx),
8994 [
8995 Point::new(0, 4)..Point::new(0, 4),
8996 Point::new(1, 4)..Point::new(1, 4),
8997 Point::new(2, 4)..Point::new(2, 4)
8998 ]
8999 );
9000
9001 editor.backspace(&Default::default(), window, cx);
9002 editor.backspace(&Default::default(), window, cx);
9003 assert_eq!(
9004 editor.text(cx),
9005 "
9006 a{}
9007 b{}
9008 c{}
9009 "
9010 .unindent()
9011 );
9012 assert_eq!(
9013 editor.selections.ranges::<Point>(cx),
9014 [
9015 Point::new(0, 2)..Point::new(0, 2),
9016 Point::new(1, 2)..Point::new(1, 2),
9017 Point::new(2, 2)..Point::new(2, 2)
9018 ]
9019 );
9020
9021 editor.delete_to_previous_word_start(&Default::default(), window, cx);
9022 assert_eq!(
9023 editor.text(cx),
9024 "
9025 a
9026 b
9027 c
9028 "
9029 .unindent()
9030 );
9031 assert_eq!(
9032 editor.selections.ranges::<Point>(cx),
9033 [
9034 Point::new(0, 1)..Point::new(0, 1),
9035 Point::new(1, 1)..Point::new(1, 1),
9036 Point::new(2, 1)..Point::new(2, 1)
9037 ]
9038 );
9039 });
9040}
9041
9042#[gpui::test]
9043async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
9044 init_test(cx, |settings| {
9045 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9046 });
9047
9048 let mut cx = EditorTestContext::new(cx).await;
9049
9050 let language = Arc::new(Language::new(
9051 LanguageConfig {
9052 brackets: BracketPairConfig {
9053 pairs: vec![
9054 BracketPair {
9055 start: "{".to_string(),
9056 end: "}".to_string(),
9057 close: true,
9058 surround: true,
9059 newline: true,
9060 },
9061 BracketPair {
9062 start: "(".to_string(),
9063 end: ")".to_string(),
9064 close: true,
9065 surround: true,
9066 newline: true,
9067 },
9068 BracketPair {
9069 start: "[".to_string(),
9070 end: "]".to_string(),
9071 close: false,
9072 surround: true,
9073 newline: true,
9074 },
9075 ],
9076 ..Default::default()
9077 },
9078 autoclose_before: "})]".to_string(),
9079 ..Default::default()
9080 },
9081 Some(tree_sitter_rust::LANGUAGE.into()),
9082 ));
9083
9084 cx.language_registry().add(language.clone());
9085 cx.update_buffer(|buffer, cx| {
9086 buffer.set_language(Some(language), cx);
9087 });
9088
9089 cx.set_state(
9090 &"
9091 {(ˇ)}
9092 [[ˇ]]
9093 {(ˇ)}
9094 "
9095 .unindent(),
9096 );
9097
9098 cx.update_editor(|editor, window, cx| {
9099 editor.backspace(&Default::default(), window, cx);
9100 editor.backspace(&Default::default(), window, cx);
9101 });
9102
9103 cx.assert_editor_state(
9104 &"
9105 ˇ
9106 ˇ]]
9107 ˇ
9108 "
9109 .unindent(),
9110 );
9111
9112 cx.update_editor(|editor, window, cx| {
9113 editor.handle_input("{", window, cx);
9114 editor.handle_input("{", window, cx);
9115 editor.move_right(&MoveRight, window, cx);
9116 editor.move_right(&MoveRight, window, cx);
9117 editor.move_left(&MoveLeft, window, cx);
9118 editor.move_left(&MoveLeft, window, cx);
9119 editor.backspace(&Default::default(), window, cx);
9120 });
9121
9122 cx.assert_editor_state(
9123 &"
9124 {ˇ}
9125 {ˇ}]]
9126 {ˇ}
9127 "
9128 .unindent(),
9129 );
9130
9131 cx.update_editor(|editor, window, cx| {
9132 editor.backspace(&Default::default(), window, cx);
9133 });
9134
9135 cx.assert_editor_state(
9136 &"
9137 ˇ
9138 ˇ]]
9139 ˇ
9140 "
9141 .unindent(),
9142 );
9143}
9144
9145#[gpui::test]
9146async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
9147 init_test(cx, |_| {});
9148
9149 let language = Arc::new(Language::new(
9150 LanguageConfig::default(),
9151 Some(tree_sitter_rust::LANGUAGE.into()),
9152 ));
9153
9154 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
9155 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9156 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9157 editor
9158 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9159 .await;
9160
9161 editor.update_in(cx, |editor, window, cx| {
9162 editor.set_auto_replace_emoji_shortcode(true);
9163
9164 editor.handle_input("Hello ", window, cx);
9165 editor.handle_input(":wave", window, cx);
9166 assert_eq!(editor.text(cx), "Hello :wave".unindent());
9167
9168 editor.handle_input(":", window, cx);
9169 assert_eq!(editor.text(cx), "Hello 👋".unindent());
9170
9171 editor.handle_input(" :smile", window, cx);
9172 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
9173
9174 editor.handle_input(":", window, cx);
9175 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
9176
9177 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
9178 editor.handle_input(":wave", window, cx);
9179 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
9180
9181 editor.handle_input(":", window, cx);
9182 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
9183
9184 editor.handle_input(":1", window, cx);
9185 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
9186
9187 editor.handle_input(":", window, cx);
9188 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
9189
9190 // Ensure shortcode does not get replaced when it is part of a word
9191 editor.handle_input(" Test:wave", window, cx);
9192 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
9193
9194 editor.handle_input(":", window, cx);
9195 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
9196
9197 editor.set_auto_replace_emoji_shortcode(false);
9198
9199 // Ensure shortcode does not get replaced when auto replace is off
9200 editor.handle_input(" :wave", window, cx);
9201 assert_eq!(
9202 editor.text(cx),
9203 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
9204 );
9205
9206 editor.handle_input(":", window, cx);
9207 assert_eq!(
9208 editor.text(cx),
9209 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
9210 );
9211 });
9212}
9213
9214#[gpui::test]
9215async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
9216 init_test(cx, |_| {});
9217
9218 let (text, insertion_ranges) = marked_text_ranges(
9219 indoc! {"
9220 ˇ
9221 "},
9222 false,
9223 );
9224
9225 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
9226 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9227
9228 _ = editor.update_in(cx, |editor, window, cx| {
9229 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
9230
9231 editor
9232 .insert_snippet(&insertion_ranges, snippet, window, cx)
9233 .unwrap();
9234
9235 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
9236 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
9237 assert_eq!(editor.text(cx), expected_text);
9238 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
9239 }
9240
9241 assert(
9242 editor,
9243 cx,
9244 indoc! {"
9245 type «» =•
9246 "},
9247 );
9248
9249 assert!(editor.context_menu_visible(), "There should be a matches");
9250 });
9251}
9252
9253#[gpui::test]
9254async fn test_snippets(cx: &mut TestAppContext) {
9255 init_test(cx, |_| {});
9256
9257 let mut cx = EditorTestContext::new(cx).await;
9258
9259 cx.set_state(indoc! {"
9260 a.ˇ b
9261 a.ˇ b
9262 a.ˇ b
9263 "});
9264
9265 cx.update_editor(|editor, window, cx| {
9266 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
9267 let insertion_ranges = editor
9268 .selections
9269 .all(cx)
9270 .iter()
9271 .map(|s| s.range().clone())
9272 .collect::<Vec<_>>();
9273 editor
9274 .insert_snippet(&insertion_ranges, snippet, window, cx)
9275 .unwrap();
9276 });
9277
9278 cx.assert_editor_state(indoc! {"
9279 a.f(«oneˇ», two, «threeˇ») b
9280 a.f(«oneˇ», two, «threeˇ») b
9281 a.f(«oneˇ», two, «threeˇ») b
9282 "});
9283
9284 // Can't move earlier than the first tab stop
9285 cx.update_editor(|editor, window, cx| {
9286 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9287 });
9288 cx.assert_editor_state(indoc! {"
9289 a.f(«oneˇ», two, «threeˇ») b
9290 a.f(«oneˇ», two, «threeˇ») b
9291 a.f(«oneˇ», two, «threeˇ») b
9292 "});
9293
9294 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9295 cx.assert_editor_state(indoc! {"
9296 a.f(one, «twoˇ», three) b
9297 a.f(one, «twoˇ», three) b
9298 a.f(one, «twoˇ», three) b
9299 "});
9300
9301 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
9302 cx.assert_editor_state(indoc! {"
9303 a.f(«oneˇ», two, «threeˇ») b
9304 a.f(«oneˇ», two, «threeˇ») b
9305 a.f(«oneˇ», two, «threeˇ») b
9306 "});
9307
9308 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9309 cx.assert_editor_state(indoc! {"
9310 a.f(one, «twoˇ», three) b
9311 a.f(one, «twoˇ», three) b
9312 a.f(one, «twoˇ», three) b
9313 "});
9314 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
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 // As soon as the last tab stop is reached, snippet state is gone
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
9332#[gpui::test]
9333async fn test_snippet_indentation(cx: &mut TestAppContext) {
9334 init_test(cx, |_| {});
9335
9336 let mut cx = EditorTestContext::new(cx).await;
9337
9338 cx.update_editor(|editor, window, cx| {
9339 let snippet = Snippet::parse(indoc! {"
9340 /*
9341 * Multiline comment with leading indentation
9342 *
9343 * $1
9344 */
9345 $0"})
9346 .unwrap();
9347 let insertion_ranges = editor
9348 .selections
9349 .all(cx)
9350 .iter()
9351 .map(|s| s.range().clone())
9352 .collect::<Vec<_>>();
9353 editor
9354 .insert_snippet(&insertion_ranges, snippet, window, cx)
9355 .unwrap();
9356 });
9357
9358 cx.assert_editor_state(indoc! {"
9359 /*
9360 * Multiline comment with leading indentation
9361 *
9362 * ˇ
9363 */
9364 "});
9365
9366 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9367 cx.assert_editor_state(indoc! {"
9368 /*
9369 * Multiline comment with leading indentation
9370 *
9371 *•
9372 */
9373 ˇ"});
9374}
9375
9376#[gpui::test]
9377async fn test_document_format_during_save(cx: &mut TestAppContext) {
9378 init_test(cx, |_| {});
9379
9380 let fs = FakeFs::new(cx.executor());
9381 fs.insert_file(path!("/file.rs"), Default::default()).await;
9382
9383 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
9384
9385 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9386 language_registry.add(rust_lang());
9387 let mut fake_servers = language_registry.register_fake_lsp(
9388 "Rust",
9389 FakeLspAdapter {
9390 capabilities: lsp::ServerCapabilities {
9391 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9392 ..Default::default()
9393 },
9394 ..Default::default()
9395 },
9396 );
9397
9398 let buffer = project
9399 .update(cx, |project, cx| {
9400 project.open_local_buffer(path!("/file.rs"), cx)
9401 })
9402 .await
9403 .unwrap();
9404
9405 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9406 let (editor, cx) = cx.add_window_view(|window, cx| {
9407 build_editor_with_project(project.clone(), buffer, window, cx)
9408 });
9409 editor.update_in(cx, |editor, window, cx| {
9410 editor.set_text("one\ntwo\nthree\n", window, cx)
9411 });
9412 assert!(cx.read(|cx| editor.is_dirty(cx)));
9413
9414 cx.executor().start_waiting();
9415 let fake_server = fake_servers.next().await.unwrap();
9416
9417 {
9418 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9419 move |params, _| async move {
9420 assert_eq!(
9421 params.text_document.uri,
9422 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9423 );
9424 assert_eq!(params.options.tab_size, 4);
9425 Ok(Some(vec![lsp::TextEdit::new(
9426 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9427 ", ".to_string(),
9428 )]))
9429 },
9430 );
9431 let save = editor
9432 .update_in(cx, |editor, window, cx| {
9433 editor.save(
9434 SaveOptions {
9435 format: true,
9436 autosave: false,
9437 },
9438 project.clone(),
9439 window,
9440 cx,
9441 )
9442 })
9443 .unwrap();
9444 cx.executor().start_waiting();
9445 save.await;
9446
9447 assert_eq!(
9448 editor.update(cx, |editor, cx| editor.text(cx)),
9449 "one, two\nthree\n"
9450 );
9451 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9452 }
9453
9454 {
9455 editor.update_in(cx, |editor, window, cx| {
9456 editor.set_text("one\ntwo\nthree\n", window, cx)
9457 });
9458 assert!(cx.read(|cx| editor.is_dirty(cx)));
9459
9460 // Ensure we can still save even if formatting hangs.
9461 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9462 move |params, _| async move {
9463 assert_eq!(
9464 params.text_document.uri,
9465 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9466 );
9467 futures::future::pending::<()>().await;
9468 unreachable!()
9469 },
9470 );
9471 let save = editor
9472 .update_in(cx, |editor, window, cx| {
9473 editor.save(
9474 SaveOptions {
9475 format: true,
9476 autosave: false,
9477 },
9478 project.clone(),
9479 window,
9480 cx,
9481 )
9482 })
9483 .unwrap();
9484 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9485 cx.executor().start_waiting();
9486 save.await;
9487 assert_eq!(
9488 editor.update(cx, |editor, cx| editor.text(cx)),
9489 "one\ntwo\nthree\n"
9490 );
9491 }
9492
9493 // Set rust language override and assert overridden tabsize is sent to language server
9494 update_test_language_settings(cx, |settings| {
9495 settings.languages.0.insert(
9496 "Rust".into(),
9497 LanguageSettingsContent {
9498 tab_size: NonZeroU32::new(8),
9499 ..Default::default()
9500 },
9501 );
9502 });
9503
9504 {
9505 editor.update_in(cx, |editor, window, cx| {
9506 editor.set_text("somehting_new\n", window, cx)
9507 });
9508 assert!(cx.read(|cx| editor.is_dirty(cx)));
9509 let _formatting_request_signal = fake_server
9510 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9511 assert_eq!(
9512 params.text_document.uri,
9513 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9514 );
9515 assert_eq!(params.options.tab_size, 8);
9516 Ok(Some(vec![]))
9517 });
9518 let save = editor
9519 .update_in(cx, |editor, window, cx| {
9520 editor.save(
9521 SaveOptions {
9522 format: true,
9523 autosave: false,
9524 },
9525 project.clone(),
9526 window,
9527 cx,
9528 )
9529 })
9530 .unwrap();
9531 cx.executor().start_waiting();
9532 save.await;
9533 }
9534}
9535
9536#[gpui::test]
9537async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9538 init_test(cx, |_| {});
9539
9540 let cols = 4;
9541 let rows = 10;
9542 let sample_text_1 = sample_text(rows, cols, 'a');
9543 assert_eq!(
9544 sample_text_1,
9545 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9546 );
9547 let sample_text_2 = sample_text(rows, cols, 'l');
9548 assert_eq!(
9549 sample_text_2,
9550 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9551 );
9552 let sample_text_3 = sample_text(rows, cols, 'v');
9553 assert_eq!(
9554 sample_text_3,
9555 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9556 );
9557
9558 let fs = FakeFs::new(cx.executor());
9559 fs.insert_tree(
9560 path!("/a"),
9561 json!({
9562 "main.rs": sample_text_1,
9563 "other.rs": sample_text_2,
9564 "lib.rs": sample_text_3,
9565 }),
9566 )
9567 .await;
9568
9569 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9570 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9571 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9572
9573 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9574 language_registry.add(rust_lang());
9575 let mut fake_servers = language_registry.register_fake_lsp(
9576 "Rust",
9577 FakeLspAdapter {
9578 capabilities: lsp::ServerCapabilities {
9579 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9580 ..Default::default()
9581 },
9582 ..Default::default()
9583 },
9584 );
9585
9586 let worktree = project.update(cx, |project, cx| {
9587 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
9588 assert_eq!(worktrees.len(), 1);
9589 worktrees.pop().unwrap()
9590 });
9591 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9592
9593 let buffer_1 = project
9594 .update(cx, |project, cx| {
9595 project.open_buffer((worktree_id, "main.rs"), cx)
9596 })
9597 .await
9598 .unwrap();
9599 let buffer_2 = project
9600 .update(cx, |project, cx| {
9601 project.open_buffer((worktree_id, "other.rs"), cx)
9602 })
9603 .await
9604 .unwrap();
9605 let buffer_3 = project
9606 .update(cx, |project, cx| {
9607 project.open_buffer((worktree_id, "lib.rs"), cx)
9608 })
9609 .await
9610 .unwrap();
9611
9612 let multi_buffer = cx.new(|cx| {
9613 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9614 multi_buffer.push_excerpts(
9615 buffer_1.clone(),
9616 [
9617 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9618 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9619 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9620 ],
9621 cx,
9622 );
9623 multi_buffer.push_excerpts(
9624 buffer_2.clone(),
9625 [
9626 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9627 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9628 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9629 ],
9630 cx,
9631 );
9632 multi_buffer.push_excerpts(
9633 buffer_3.clone(),
9634 [
9635 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9636 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9637 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9638 ],
9639 cx,
9640 );
9641 multi_buffer
9642 });
9643 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
9644 Editor::new(
9645 EditorMode::full(),
9646 multi_buffer,
9647 Some(project.clone()),
9648 window,
9649 cx,
9650 )
9651 });
9652
9653 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9654 editor.change_selections(
9655 SelectionEffects::scroll(Autoscroll::Next),
9656 window,
9657 cx,
9658 |s| s.select_ranges(Some(1..2)),
9659 );
9660 editor.insert("|one|two|three|", window, cx);
9661 });
9662 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9663 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9664 editor.change_selections(
9665 SelectionEffects::scroll(Autoscroll::Next),
9666 window,
9667 cx,
9668 |s| s.select_ranges(Some(60..70)),
9669 );
9670 editor.insert("|four|five|six|", window, cx);
9671 });
9672 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9673
9674 // First two buffers should be edited, but not the third one.
9675 assert_eq!(
9676 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9677 "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}",
9678 );
9679 buffer_1.update(cx, |buffer, _| {
9680 assert!(buffer.is_dirty());
9681 assert_eq!(
9682 buffer.text(),
9683 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
9684 )
9685 });
9686 buffer_2.update(cx, |buffer, _| {
9687 assert!(buffer.is_dirty());
9688 assert_eq!(
9689 buffer.text(),
9690 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
9691 )
9692 });
9693 buffer_3.update(cx, |buffer, _| {
9694 assert!(!buffer.is_dirty());
9695 assert_eq!(buffer.text(), sample_text_3,)
9696 });
9697 cx.executor().run_until_parked();
9698
9699 cx.executor().start_waiting();
9700 let save = multi_buffer_editor
9701 .update_in(cx, |editor, window, cx| {
9702 editor.save(
9703 SaveOptions {
9704 format: true,
9705 autosave: false,
9706 },
9707 project.clone(),
9708 window,
9709 cx,
9710 )
9711 })
9712 .unwrap();
9713
9714 let fake_server = fake_servers.next().await.unwrap();
9715 fake_server
9716 .server
9717 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
9718 Ok(Some(vec![lsp::TextEdit::new(
9719 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9720 format!("[{} formatted]", params.text_document.uri),
9721 )]))
9722 })
9723 .detach();
9724 save.await;
9725
9726 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
9727 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
9728 assert_eq!(
9729 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9730 uri!(
9731 "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}"
9732 ),
9733 );
9734 buffer_1.update(cx, |buffer, _| {
9735 assert!(!buffer.is_dirty());
9736 assert_eq!(
9737 buffer.text(),
9738 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
9739 )
9740 });
9741 buffer_2.update(cx, |buffer, _| {
9742 assert!(!buffer.is_dirty());
9743 assert_eq!(
9744 buffer.text(),
9745 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
9746 )
9747 });
9748 buffer_3.update(cx, |buffer, _| {
9749 assert!(!buffer.is_dirty());
9750 assert_eq!(buffer.text(), sample_text_3,)
9751 });
9752}
9753
9754#[gpui::test]
9755async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
9756 init_test(cx, |_| {});
9757
9758 let fs = FakeFs::new(cx.executor());
9759 fs.insert_tree(
9760 path!("/dir"),
9761 json!({
9762 "file1.rs": "fn main() { println!(\"hello\"); }",
9763 "file2.rs": "fn test() { println!(\"test\"); }",
9764 "file3.rs": "fn other() { println!(\"other\"); }\n",
9765 }),
9766 )
9767 .await;
9768
9769 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
9770 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9771 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9772
9773 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9774 language_registry.add(rust_lang());
9775
9776 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
9777 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9778
9779 // Open three buffers
9780 let buffer_1 = project
9781 .update(cx, |project, cx| {
9782 project.open_buffer((worktree_id, "file1.rs"), cx)
9783 })
9784 .await
9785 .unwrap();
9786 let buffer_2 = project
9787 .update(cx, |project, cx| {
9788 project.open_buffer((worktree_id, "file2.rs"), cx)
9789 })
9790 .await
9791 .unwrap();
9792 let buffer_3 = project
9793 .update(cx, |project, cx| {
9794 project.open_buffer((worktree_id, "file3.rs"), cx)
9795 })
9796 .await
9797 .unwrap();
9798
9799 // Create a multi-buffer with all three buffers
9800 let multi_buffer = cx.new(|cx| {
9801 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9802 multi_buffer.push_excerpts(
9803 buffer_1.clone(),
9804 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9805 cx,
9806 );
9807 multi_buffer.push_excerpts(
9808 buffer_2.clone(),
9809 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9810 cx,
9811 );
9812 multi_buffer.push_excerpts(
9813 buffer_3.clone(),
9814 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9815 cx,
9816 );
9817 multi_buffer
9818 });
9819
9820 let editor = cx.new_window_entity(|window, cx| {
9821 Editor::new(
9822 EditorMode::full(),
9823 multi_buffer,
9824 Some(project.clone()),
9825 window,
9826 cx,
9827 )
9828 });
9829
9830 // Edit only the first buffer
9831 editor.update_in(cx, |editor, window, cx| {
9832 editor.change_selections(
9833 SelectionEffects::scroll(Autoscroll::Next),
9834 window,
9835 cx,
9836 |s| s.select_ranges(Some(10..10)),
9837 );
9838 editor.insert("// edited", window, cx);
9839 });
9840
9841 // Verify that only buffer 1 is dirty
9842 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
9843 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9844 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9845
9846 // Get write counts after file creation (files were created with initial content)
9847 // We expect each file to have been written once during creation
9848 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
9849 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
9850 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
9851
9852 // Perform autosave
9853 let save_task = editor.update_in(cx, |editor, window, cx| {
9854 editor.save(
9855 SaveOptions {
9856 format: true,
9857 autosave: true,
9858 },
9859 project.clone(),
9860 window,
9861 cx,
9862 )
9863 });
9864 save_task.await.unwrap();
9865
9866 // Only the dirty buffer should have been saved
9867 assert_eq!(
9868 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9869 1,
9870 "Buffer 1 was dirty, so it should have been written once during autosave"
9871 );
9872 assert_eq!(
9873 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9874 0,
9875 "Buffer 2 was clean, so it should not have been written during autosave"
9876 );
9877 assert_eq!(
9878 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9879 0,
9880 "Buffer 3 was clean, so it should not have been written during autosave"
9881 );
9882
9883 // Verify buffer states after autosave
9884 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9885 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9886 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9887
9888 // Now perform a manual save (format = true)
9889 let save_task = editor.update_in(cx, |editor, window, cx| {
9890 editor.save(
9891 SaveOptions {
9892 format: true,
9893 autosave: false,
9894 },
9895 project.clone(),
9896 window,
9897 cx,
9898 )
9899 });
9900 save_task.await.unwrap();
9901
9902 // During manual save, clean buffers don't get written to disk
9903 // They just get did_save called for language server notifications
9904 assert_eq!(
9905 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9906 1,
9907 "Buffer 1 should only have been written once total (during autosave, not manual save)"
9908 );
9909 assert_eq!(
9910 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9911 0,
9912 "Buffer 2 should not have been written at all"
9913 );
9914 assert_eq!(
9915 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9916 0,
9917 "Buffer 3 should not have been written at all"
9918 );
9919}
9920
9921#[gpui::test]
9922async fn test_range_format_during_save(cx: &mut TestAppContext) {
9923 init_test(cx, |_| {});
9924
9925 let fs = FakeFs::new(cx.executor());
9926 fs.insert_file(path!("/file.rs"), Default::default()).await;
9927
9928 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9929
9930 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9931 language_registry.add(rust_lang());
9932 let mut fake_servers = language_registry.register_fake_lsp(
9933 "Rust",
9934 FakeLspAdapter {
9935 capabilities: lsp::ServerCapabilities {
9936 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
9937 ..Default::default()
9938 },
9939 ..Default::default()
9940 },
9941 );
9942
9943 let buffer = project
9944 .update(cx, |project, cx| {
9945 project.open_local_buffer(path!("/file.rs"), cx)
9946 })
9947 .await
9948 .unwrap();
9949
9950 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9951 let (editor, cx) = cx.add_window_view(|window, cx| {
9952 build_editor_with_project(project.clone(), buffer, window, cx)
9953 });
9954 editor.update_in(cx, |editor, window, cx| {
9955 editor.set_text("one\ntwo\nthree\n", window, cx)
9956 });
9957 assert!(cx.read(|cx| editor.is_dirty(cx)));
9958
9959 cx.executor().start_waiting();
9960 let fake_server = fake_servers.next().await.unwrap();
9961
9962 let save = editor
9963 .update_in(cx, |editor, window, cx| {
9964 editor.save(
9965 SaveOptions {
9966 format: true,
9967 autosave: false,
9968 },
9969 project.clone(),
9970 window,
9971 cx,
9972 )
9973 })
9974 .unwrap();
9975 fake_server
9976 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9977 assert_eq!(
9978 params.text_document.uri,
9979 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9980 );
9981 assert_eq!(params.options.tab_size, 4);
9982 Ok(Some(vec![lsp::TextEdit::new(
9983 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9984 ", ".to_string(),
9985 )]))
9986 })
9987 .next()
9988 .await;
9989 cx.executor().start_waiting();
9990 save.await;
9991 assert_eq!(
9992 editor.update(cx, |editor, cx| editor.text(cx)),
9993 "one, two\nthree\n"
9994 );
9995 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9996
9997 editor.update_in(cx, |editor, window, cx| {
9998 editor.set_text("one\ntwo\nthree\n", window, cx)
9999 });
10000 assert!(cx.read(|cx| editor.is_dirty(cx)));
10001
10002 // Ensure we can still save even if formatting hangs.
10003 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
10004 move |params, _| async move {
10005 assert_eq!(
10006 params.text_document.uri,
10007 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10008 );
10009 futures::future::pending::<()>().await;
10010 unreachable!()
10011 },
10012 );
10013 let save = editor
10014 .update_in(cx, |editor, window, cx| {
10015 editor.save(
10016 SaveOptions {
10017 format: true,
10018 autosave: false,
10019 },
10020 project.clone(),
10021 window,
10022 cx,
10023 )
10024 })
10025 .unwrap();
10026 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10027 cx.executor().start_waiting();
10028 save.await;
10029 assert_eq!(
10030 editor.update(cx, |editor, cx| editor.text(cx)),
10031 "one\ntwo\nthree\n"
10032 );
10033 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10034
10035 // For non-dirty buffer, no formatting request should be sent
10036 let save = editor
10037 .update_in(cx, |editor, window, cx| {
10038 editor.save(
10039 SaveOptions {
10040 format: false,
10041 autosave: false,
10042 },
10043 project.clone(),
10044 window,
10045 cx,
10046 )
10047 })
10048 .unwrap();
10049 let _pending_format_request = fake_server
10050 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
10051 panic!("Should not be invoked");
10052 })
10053 .next();
10054 cx.executor().start_waiting();
10055 save.await;
10056
10057 // Set Rust language override and assert overridden tabsize is sent to language server
10058 update_test_language_settings(cx, |settings| {
10059 settings.languages.0.insert(
10060 "Rust".into(),
10061 LanguageSettingsContent {
10062 tab_size: NonZeroU32::new(8),
10063 ..Default::default()
10064 },
10065 );
10066 });
10067
10068 editor.update_in(cx, |editor, window, cx| {
10069 editor.set_text("somehting_new\n", window, cx)
10070 });
10071 assert!(cx.read(|cx| editor.is_dirty(cx)));
10072 let save = editor
10073 .update_in(cx, |editor, window, cx| {
10074 editor.save(
10075 SaveOptions {
10076 format: true,
10077 autosave: false,
10078 },
10079 project.clone(),
10080 window,
10081 cx,
10082 )
10083 })
10084 .unwrap();
10085 fake_server
10086 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10087 assert_eq!(
10088 params.text_document.uri,
10089 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10090 );
10091 assert_eq!(params.options.tab_size, 8);
10092 Ok(Some(Vec::new()))
10093 })
10094 .next()
10095 .await;
10096 save.await;
10097}
10098
10099#[gpui::test]
10100async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
10101 init_test(cx, |settings| {
10102 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
10103 Formatter::LanguageServer { name: None },
10104 )))
10105 });
10106
10107 let fs = FakeFs::new(cx.executor());
10108 fs.insert_file(path!("/file.rs"), Default::default()).await;
10109
10110 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10111
10112 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10113 language_registry.add(Arc::new(Language::new(
10114 LanguageConfig {
10115 name: "Rust".into(),
10116 matcher: LanguageMatcher {
10117 path_suffixes: vec!["rs".to_string()],
10118 ..Default::default()
10119 },
10120 ..LanguageConfig::default()
10121 },
10122 Some(tree_sitter_rust::LANGUAGE.into()),
10123 )));
10124 update_test_language_settings(cx, |settings| {
10125 // Enable Prettier formatting for the same buffer, and ensure
10126 // LSP is called instead of Prettier.
10127 settings.defaults.prettier = Some(PrettierSettings {
10128 allowed: true,
10129 ..PrettierSettings::default()
10130 });
10131 });
10132 let mut fake_servers = language_registry.register_fake_lsp(
10133 "Rust",
10134 FakeLspAdapter {
10135 capabilities: lsp::ServerCapabilities {
10136 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10137 ..Default::default()
10138 },
10139 ..Default::default()
10140 },
10141 );
10142
10143 let buffer = project
10144 .update(cx, |project, cx| {
10145 project.open_local_buffer(path!("/file.rs"), cx)
10146 })
10147 .await
10148 .unwrap();
10149
10150 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10151 let (editor, cx) = cx.add_window_view(|window, cx| {
10152 build_editor_with_project(project.clone(), buffer, window, cx)
10153 });
10154 editor.update_in(cx, |editor, window, cx| {
10155 editor.set_text("one\ntwo\nthree\n", window, cx)
10156 });
10157
10158 cx.executor().start_waiting();
10159 let fake_server = fake_servers.next().await.unwrap();
10160
10161 let format = editor
10162 .update_in(cx, |editor, window, cx| {
10163 editor.perform_format(
10164 project.clone(),
10165 FormatTrigger::Manual,
10166 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10167 window,
10168 cx,
10169 )
10170 })
10171 .unwrap();
10172 fake_server
10173 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10174 assert_eq!(
10175 params.text_document.uri,
10176 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10177 );
10178 assert_eq!(params.options.tab_size, 4);
10179 Ok(Some(vec![lsp::TextEdit::new(
10180 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10181 ", ".to_string(),
10182 )]))
10183 })
10184 .next()
10185 .await;
10186 cx.executor().start_waiting();
10187 format.await;
10188 assert_eq!(
10189 editor.update(cx, |editor, cx| editor.text(cx)),
10190 "one, two\nthree\n"
10191 );
10192
10193 editor.update_in(cx, |editor, window, cx| {
10194 editor.set_text("one\ntwo\nthree\n", window, cx)
10195 });
10196 // Ensure we don't lock if formatting hangs.
10197 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10198 move |params, _| async move {
10199 assert_eq!(
10200 params.text_document.uri,
10201 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10202 );
10203 futures::future::pending::<()>().await;
10204 unreachable!()
10205 },
10206 );
10207 let format = editor
10208 .update_in(cx, |editor, window, cx| {
10209 editor.perform_format(
10210 project,
10211 FormatTrigger::Manual,
10212 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10213 window,
10214 cx,
10215 )
10216 })
10217 .unwrap();
10218 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10219 cx.executor().start_waiting();
10220 format.await;
10221 assert_eq!(
10222 editor.update(cx, |editor, cx| editor.text(cx)),
10223 "one\ntwo\nthree\n"
10224 );
10225}
10226
10227#[gpui::test]
10228async fn test_multiple_formatters(cx: &mut TestAppContext) {
10229 init_test(cx, |settings| {
10230 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
10231 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10232 Formatter::LanguageServer { name: None },
10233 Formatter::CodeActions(
10234 [
10235 ("code-action-1".into(), true),
10236 ("code-action-2".into(), true),
10237 ]
10238 .into_iter()
10239 .collect(),
10240 ),
10241 ])))
10242 });
10243
10244 let fs = FakeFs::new(cx.executor());
10245 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
10246 .await;
10247
10248 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10249 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10250 language_registry.add(rust_lang());
10251
10252 let mut fake_servers = language_registry.register_fake_lsp(
10253 "Rust",
10254 FakeLspAdapter {
10255 capabilities: lsp::ServerCapabilities {
10256 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10257 execute_command_provider: Some(lsp::ExecuteCommandOptions {
10258 commands: vec!["the-command-for-code-action-1".into()],
10259 ..Default::default()
10260 }),
10261 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10262 ..Default::default()
10263 },
10264 ..Default::default()
10265 },
10266 );
10267
10268 let buffer = project
10269 .update(cx, |project, cx| {
10270 project.open_local_buffer(path!("/file.rs"), cx)
10271 })
10272 .await
10273 .unwrap();
10274
10275 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10276 let (editor, cx) = cx.add_window_view(|window, cx| {
10277 build_editor_with_project(project.clone(), buffer, window, cx)
10278 });
10279
10280 cx.executor().start_waiting();
10281
10282 let fake_server = fake_servers.next().await.unwrap();
10283 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10284 move |_params, _| async move {
10285 Ok(Some(vec![lsp::TextEdit::new(
10286 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10287 "applied-formatting\n".to_string(),
10288 )]))
10289 },
10290 );
10291 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10292 move |params, _| async move {
10293 assert_eq!(
10294 params.context.only,
10295 Some(vec!["code-action-1".into(), "code-action-2".into()])
10296 );
10297 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
10298 Ok(Some(vec![
10299 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10300 kind: Some("code-action-1".into()),
10301 edit: Some(lsp::WorkspaceEdit::new(
10302 [(
10303 uri.clone(),
10304 vec![lsp::TextEdit::new(
10305 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10306 "applied-code-action-1-edit\n".to_string(),
10307 )],
10308 )]
10309 .into_iter()
10310 .collect(),
10311 )),
10312 command: Some(lsp::Command {
10313 command: "the-command-for-code-action-1".into(),
10314 ..Default::default()
10315 }),
10316 ..Default::default()
10317 }),
10318 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10319 kind: Some("code-action-2".into()),
10320 edit: Some(lsp::WorkspaceEdit::new(
10321 [(
10322 uri.clone(),
10323 vec![lsp::TextEdit::new(
10324 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10325 "applied-code-action-2-edit\n".to_string(),
10326 )],
10327 )]
10328 .into_iter()
10329 .collect(),
10330 )),
10331 ..Default::default()
10332 }),
10333 ]))
10334 },
10335 );
10336
10337 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
10338 move |params, _| async move { Ok(params) }
10339 });
10340
10341 let command_lock = Arc::new(futures::lock::Mutex::new(()));
10342 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
10343 let fake = fake_server.clone();
10344 let lock = command_lock.clone();
10345 move |params, _| {
10346 assert_eq!(params.command, "the-command-for-code-action-1");
10347 let fake = fake.clone();
10348 let lock = lock.clone();
10349 async move {
10350 lock.lock().await;
10351 fake.server
10352 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
10353 label: None,
10354 edit: lsp::WorkspaceEdit {
10355 changes: Some(
10356 [(
10357 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
10358 vec![lsp::TextEdit {
10359 range: lsp::Range::new(
10360 lsp::Position::new(0, 0),
10361 lsp::Position::new(0, 0),
10362 ),
10363 new_text: "applied-code-action-1-command\n".into(),
10364 }],
10365 )]
10366 .into_iter()
10367 .collect(),
10368 ),
10369 ..Default::default()
10370 },
10371 })
10372 .await
10373 .into_response()
10374 .unwrap();
10375 Ok(Some(json!(null)))
10376 }
10377 }
10378 });
10379
10380 cx.executor().start_waiting();
10381 editor
10382 .update_in(cx, |editor, window, cx| {
10383 editor.perform_format(
10384 project.clone(),
10385 FormatTrigger::Manual,
10386 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10387 window,
10388 cx,
10389 )
10390 })
10391 .unwrap()
10392 .await;
10393 editor.update(cx, |editor, cx| {
10394 assert_eq!(
10395 editor.text(cx),
10396 r#"
10397 applied-code-action-2-edit
10398 applied-code-action-1-command
10399 applied-code-action-1-edit
10400 applied-formatting
10401 one
10402 two
10403 three
10404 "#
10405 .unindent()
10406 );
10407 });
10408
10409 editor.update_in(cx, |editor, window, cx| {
10410 editor.undo(&Default::default(), window, cx);
10411 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10412 });
10413
10414 // Perform a manual edit while waiting for an LSP command
10415 // that's being run as part of a formatting code action.
10416 let lock_guard = command_lock.lock().await;
10417 let format = editor
10418 .update_in(cx, |editor, window, cx| {
10419 editor.perform_format(
10420 project.clone(),
10421 FormatTrigger::Manual,
10422 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10423 window,
10424 cx,
10425 )
10426 })
10427 .unwrap();
10428 cx.run_until_parked();
10429 editor.update(cx, |editor, cx| {
10430 assert_eq!(
10431 editor.text(cx),
10432 r#"
10433 applied-code-action-1-edit
10434 applied-formatting
10435 one
10436 two
10437 three
10438 "#
10439 .unindent()
10440 );
10441
10442 editor.buffer.update(cx, |buffer, cx| {
10443 let ix = buffer.len(cx);
10444 buffer.edit([(ix..ix, "edited\n")], None, cx);
10445 });
10446 });
10447
10448 // Allow the LSP command to proceed. Because the buffer was edited,
10449 // the second code action will not be run.
10450 drop(lock_guard);
10451 format.await;
10452 editor.update_in(cx, |editor, window, cx| {
10453 assert_eq!(
10454 editor.text(cx),
10455 r#"
10456 applied-code-action-1-command
10457 applied-code-action-1-edit
10458 applied-formatting
10459 one
10460 two
10461 three
10462 edited
10463 "#
10464 .unindent()
10465 );
10466
10467 // The manual edit is undone first, because it is the last thing the user did
10468 // (even though the command completed afterwards).
10469 editor.undo(&Default::default(), window, cx);
10470 assert_eq!(
10471 editor.text(cx),
10472 r#"
10473 applied-code-action-1-command
10474 applied-code-action-1-edit
10475 applied-formatting
10476 one
10477 two
10478 three
10479 "#
10480 .unindent()
10481 );
10482
10483 // All the formatting (including the command, which completed after the manual edit)
10484 // is undone together.
10485 editor.undo(&Default::default(), window, cx);
10486 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10487 });
10488}
10489
10490#[gpui::test]
10491async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
10492 init_test(cx, |settings| {
10493 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10494 Formatter::LanguageServer { name: None },
10495 ])))
10496 });
10497
10498 let fs = FakeFs::new(cx.executor());
10499 fs.insert_file(path!("/file.ts"), Default::default()).await;
10500
10501 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10502
10503 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10504 language_registry.add(Arc::new(Language::new(
10505 LanguageConfig {
10506 name: "TypeScript".into(),
10507 matcher: LanguageMatcher {
10508 path_suffixes: vec!["ts".to_string()],
10509 ..Default::default()
10510 },
10511 ..LanguageConfig::default()
10512 },
10513 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10514 )));
10515 update_test_language_settings(cx, |settings| {
10516 settings.defaults.prettier = Some(PrettierSettings {
10517 allowed: true,
10518 ..PrettierSettings::default()
10519 });
10520 });
10521 let mut fake_servers = language_registry.register_fake_lsp(
10522 "TypeScript",
10523 FakeLspAdapter {
10524 capabilities: lsp::ServerCapabilities {
10525 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10526 ..Default::default()
10527 },
10528 ..Default::default()
10529 },
10530 );
10531
10532 let buffer = project
10533 .update(cx, |project, cx| {
10534 project.open_local_buffer(path!("/file.ts"), cx)
10535 })
10536 .await
10537 .unwrap();
10538
10539 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10540 let (editor, cx) = cx.add_window_view(|window, cx| {
10541 build_editor_with_project(project.clone(), buffer, window, cx)
10542 });
10543 editor.update_in(cx, |editor, window, cx| {
10544 editor.set_text(
10545 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10546 window,
10547 cx,
10548 )
10549 });
10550
10551 cx.executor().start_waiting();
10552 let fake_server = fake_servers.next().await.unwrap();
10553
10554 let format = editor
10555 .update_in(cx, |editor, window, cx| {
10556 editor.perform_code_action_kind(
10557 project.clone(),
10558 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10559 window,
10560 cx,
10561 )
10562 })
10563 .unwrap();
10564 fake_server
10565 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
10566 assert_eq!(
10567 params.text_document.uri,
10568 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10569 );
10570 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
10571 lsp::CodeAction {
10572 title: "Organize Imports".to_string(),
10573 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
10574 edit: Some(lsp::WorkspaceEdit {
10575 changes: Some(
10576 [(
10577 params.text_document.uri.clone(),
10578 vec![lsp::TextEdit::new(
10579 lsp::Range::new(
10580 lsp::Position::new(1, 0),
10581 lsp::Position::new(2, 0),
10582 ),
10583 "".to_string(),
10584 )],
10585 )]
10586 .into_iter()
10587 .collect(),
10588 ),
10589 ..Default::default()
10590 }),
10591 ..Default::default()
10592 },
10593 )]))
10594 })
10595 .next()
10596 .await;
10597 cx.executor().start_waiting();
10598 format.await;
10599 assert_eq!(
10600 editor.update(cx, |editor, cx| editor.text(cx)),
10601 "import { a } from 'module';\n\nconst x = a;\n"
10602 );
10603
10604 editor.update_in(cx, |editor, window, cx| {
10605 editor.set_text(
10606 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10607 window,
10608 cx,
10609 )
10610 });
10611 // Ensure we don't lock if code action hangs.
10612 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10613 move |params, _| async move {
10614 assert_eq!(
10615 params.text_document.uri,
10616 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10617 );
10618 futures::future::pending::<()>().await;
10619 unreachable!()
10620 },
10621 );
10622 let format = editor
10623 .update_in(cx, |editor, window, cx| {
10624 editor.perform_code_action_kind(
10625 project,
10626 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10627 window,
10628 cx,
10629 )
10630 })
10631 .unwrap();
10632 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
10633 cx.executor().start_waiting();
10634 format.await;
10635 assert_eq!(
10636 editor.update(cx, |editor, cx| editor.text(cx)),
10637 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
10638 );
10639}
10640
10641#[gpui::test]
10642async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
10643 init_test(cx, |_| {});
10644
10645 let mut cx = EditorLspTestContext::new_rust(
10646 lsp::ServerCapabilities {
10647 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10648 ..Default::default()
10649 },
10650 cx,
10651 )
10652 .await;
10653
10654 cx.set_state(indoc! {"
10655 one.twoˇ
10656 "});
10657
10658 // The format request takes a long time. When it completes, it inserts
10659 // a newline and an indent before the `.`
10660 cx.lsp
10661 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
10662 let executor = cx.background_executor().clone();
10663 async move {
10664 executor.timer(Duration::from_millis(100)).await;
10665 Ok(Some(vec![lsp::TextEdit {
10666 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
10667 new_text: "\n ".into(),
10668 }]))
10669 }
10670 });
10671
10672 // Submit a format request.
10673 let format_1 = cx
10674 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10675 .unwrap();
10676 cx.executor().run_until_parked();
10677
10678 // Submit a second format request.
10679 let format_2 = cx
10680 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10681 .unwrap();
10682 cx.executor().run_until_parked();
10683
10684 // Wait for both format requests to complete
10685 cx.executor().advance_clock(Duration::from_millis(200));
10686 cx.executor().start_waiting();
10687 format_1.await.unwrap();
10688 cx.executor().start_waiting();
10689 format_2.await.unwrap();
10690
10691 // The formatting edits only happens once.
10692 cx.assert_editor_state(indoc! {"
10693 one
10694 .twoˇ
10695 "});
10696}
10697
10698#[gpui::test]
10699async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
10700 init_test(cx, |settings| {
10701 settings.defaults.formatter = Some(SelectedFormatter::Auto)
10702 });
10703
10704 let mut cx = EditorLspTestContext::new_rust(
10705 lsp::ServerCapabilities {
10706 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10707 ..Default::default()
10708 },
10709 cx,
10710 )
10711 .await;
10712
10713 // Set up a buffer white some trailing whitespace and no trailing newline.
10714 cx.set_state(
10715 &[
10716 "one ", //
10717 "twoˇ", //
10718 "three ", //
10719 "four", //
10720 ]
10721 .join("\n"),
10722 );
10723
10724 // Submit a format request.
10725 let format = cx
10726 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10727 .unwrap();
10728
10729 // Record which buffer changes have been sent to the language server
10730 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
10731 cx.lsp
10732 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
10733 let buffer_changes = buffer_changes.clone();
10734 move |params, _| {
10735 buffer_changes.lock().extend(
10736 params
10737 .content_changes
10738 .into_iter()
10739 .map(|e| (e.range.unwrap(), e.text)),
10740 );
10741 }
10742 });
10743
10744 // Handle formatting requests to the language server.
10745 cx.lsp
10746 .set_request_handler::<lsp::request::Formatting, _, _>({
10747 let buffer_changes = buffer_changes.clone();
10748 move |_, _| {
10749 // When formatting is requested, trailing whitespace has already been stripped,
10750 // and the trailing newline has already been added.
10751 assert_eq!(
10752 &buffer_changes.lock()[1..],
10753 &[
10754 (
10755 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
10756 "".into()
10757 ),
10758 (
10759 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
10760 "".into()
10761 ),
10762 (
10763 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
10764 "\n".into()
10765 ),
10766 ]
10767 );
10768
10769 // Insert blank lines between each line of the buffer.
10770 async move {
10771 Ok(Some(vec![
10772 lsp::TextEdit {
10773 range: lsp::Range::new(
10774 lsp::Position::new(1, 0),
10775 lsp::Position::new(1, 0),
10776 ),
10777 new_text: "\n".into(),
10778 },
10779 lsp::TextEdit {
10780 range: lsp::Range::new(
10781 lsp::Position::new(2, 0),
10782 lsp::Position::new(2, 0),
10783 ),
10784 new_text: "\n".into(),
10785 },
10786 ]))
10787 }
10788 }
10789 });
10790
10791 // After formatting the buffer, the trailing whitespace is stripped,
10792 // a newline is appended, and the edits provided by the language server
10793 // have been applied.
10794 format.await.unwrap();
10795 cx.assert_editor_state(
10796 &[
10797 "one", //
10798 "", //
10799 "twoˇ", //
10800 "", //
10801 "three", //
10802 "four", //
10803 "", //
10804 ]
10805 .join("\n"),
10806 );
10807
10808 // Undoing the formatting undoes the trailing whitespace removal, the
10809 // trailing newline, and the LSP edits.
10810 cx.update_buffer(|buffer, cx| buffer.undo(cx));
10811 cx.assert_editor_state(
10812 &[
10813 "one ", //
10814 "twoˇ", //
10815 "three ", //
10816 "four", //
10817 ]
10818 .join("\n"),
10819 );
10820}
10821
10822#[gpui::test]
10823async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
10824 cx: &mut TestAppContext,
10825) {
10826 init_test(cx, |_| {});
10827
10828 cx.update(|cx| {
10829 cx.update_global::<SettingsStore, _>(|settings, cx| {
10830 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10831 settings.auto_signature_help = Some(true);
10832 });
10833 });
10834 });
10835
10836 let mut cx = EditorLspTestContext::new_rust(
10837 lsp::ServerCapabilities {
10838 signature_help_provider: Some(lsp::SignatureHelpOptions {
10839 ..Default::default()
10840 }),
10841 ..Default::default()
10842 },
10843 cx,
10844 )
10845 .await;
10846
10847 let language = Language::new(
10848 LanguageConfig {
10849 name: "Rust".into(),
10850 brackets: BracketPairConfig {
10851 pairs: vec![
10852 BracketPair {
10853 start: "{".to_string(),
10854 end: "}".to_string(),
10855 close: true,
10856 surround: true,
10857 newline: true,
10858 },
10859 BracketPair {
10860 start: "(".to_string(),
10861 end: ")".to_string(),
10862 close: true,
10863 surround: true,
10864 newline: true,
10865 },
10866 BracketPair {
10867 start: "/*".to_string(),
10868 end: " */".to_string(),
10869 close: true,
10870 surround: true,
10871 newline: true,
10872 },
10873 BracketPair {
10874 start: "[".to_string(),
10875 end: "]".to_string(),
10876 close: false,
10877 surround: false,
10878 newline: true,
10879 },
10880 BracketPair {
10881 start: "\"".to_string(),
10882 end: "\"".to_string(),
10883 close: true,
10884 surround: true,
10885 newline: false,
10886 },
10887 BracketPair {
10888 start: "<".to_string(),
10889 end: ">".to_string(),
10890 close: false,
10891 surround: true,
10892 newline: true,
10893 },
10894 ],
10895 ..Default::default()
10896 },
10897 autoclose_before: "})]".to_string(),
10898 ..Default::default()
10899 },
10900 Some(tree_sitter_rust::LANGUAGE.into()),
10901 );
10902 let language = Arc::new(language);
10903
10904 cx.language_registry().add(language.clone());
10905 cx.update_buffer(|buffer, cx| {
10906 buffer.set_language(Some(language), cx);
10907 });
10908
10909 cx.set_state(
10910 &r#"
10911 fn main() {
10912 sampleˇ
10913 }
10914 "#
10915 .unindent(),
10916 );
10917
10918 cx.update_editor(|editor, window, cx| {
10919 editor.handle_input("(", window, cx);
10920 });
10921 cx.assert_editor_state(
10922 &"
10923 fn main() {
10924 sample(ˇ)
10925 }
10926 "
10927 .unindent(),
10928 );
10929
10930 let mocked_response = lsp::SignatureHelp {
10931 signatures: vec![lsp::SignatureInformation {
10932 label: "fn sample(param1: u8, param2: u8)".to_string(),
10933 documentation: None,
10934 parameters: Some(vec![
10935 lsp::ParameterInformation {
10936 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10937 documentation: None,
10938 },
10939 lsp::ParameterInformation {
10940 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10941 documentation: None,
10942 },
10943 ]),
10944 active_parameter: None,
10945 }],
10946 active_signature: Some(0),
10947 active_parameter: Some(0),
10948 };
10949 handle_signature_help_request(&mut cx, mocked_response).await;
10950
10951 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10952 .await;
10953
10954 cx.editor(|editor, _, _| {
10955 let signature_help_state = editor.signature_help_state.popover().cloned();
10956 let signature = signature_help_state.unwrap();
10957 assert_eq!(
10958 signature.signatures[signature.current_signature].label,
10959 "fn sample(param1: u8, param2: u8)"
10960 );
10961 });
10962}
10963
10964#[gpui::test]
10965async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
10966 init_test(cx, |_| {});
10967
10968 cx.update(|cx| {
10969 cx.update_global::<SettingsStore, _>(|settings, cx| {
10970 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10971 settings.auto_signature_help = Some(false);
10972 settings.show_signature_help_after_edits = Some(false);
10973 });
10974 });
10975 });
10976
10977 let mut cx = EditorLspTestContext::new_rust(
10978 lsp::ServerCapabilities {
10979 signature_help_provider: Some(lsp::SignatureHelpOptions {
10980 ..Default::default()
10981 }),
10982 ..Default::default()
10983 },
10984 cx,
10985 )
10986 .await;
10987
10988 let language = Language::new(
10989 LanguageConfig {
10990 name: "Rust".into(),
10991 brackets: BracketPairConfig {
10992 pairs: vec![
10993 BracketPair {
10994 start: "{".to_string(),
10995 end: "}".to_string(),
10996 close: true,
10997 surround: true,
10998 newline: true,
10999 },
11000 BracketPair {
11001 start: "(".to_string(),
11002 end: ")".to_string(),
11003 close: true,
11004 surround: true,
11005 newline: true,
11006 },
11007 BracketPair {
11008 start: "/*".to_string(),
11009 end: " */".to_string(),
11010 close: true,
11011 surround: true,
11012 newline: true,
11013 },
11014 BracketPair {
11015 start: "[".to_string(),
11016 end: "]".to_string(),
11017 close: false,
11018 surround: false,
11019 newline: true,
11020 },
11021 BracketPair {
11022 start: "\"".to_string(),
11023 end: "\"".to_string(),
11024 close: true,
11025 surround: true,
11026 newline: false,
11027 },
11028 BracketPair {
11029 start: "<".to_string(),
11030 end: ">".to_string(),
11031 close: false,
11032 surround: true,
11033 newline: true,
11034 },
11035 ],
11036 ..Default::default()
11037 },
11038 autoclose_before: "})]".to_string(),
11039 ..Default::default()
11040 },
11041 Some(tree_sitter_rust::LANGUAGE.into()),
11042 );
11043 let language = Arc::new(language);
11044
11045 cx.language_registry().add(language.clone());
11046 cx.update_buffer(|buffer, cx| {
11047 buffer.set_language(Some(language), cx);
11048 });
11049
11050 // Ensure that signature_help is not called when no signature help is enabled.
11051 cx.set_state(
11052 &r#"
11053 fn main() {
11054 sampleˇ
11055 }
11056 "#
11057 .unindent(),
11058 );
11059 cx.update_editor(|editor, window, cx| {
11060 editor.handle_input("(", window, cx);
11061 });
11062 cx.assert_editor_state(
11063 &"
11064 fn main() {
11065 sample(ˇ)
11066 }
11067 "
11068 .unindent(),
11069 );
11070 cx.editor(|editor, _, _| {
11071 assert!(editor.signature_help_state.task().is_none());
11072 });
11073
11074 let mocked_response = lsp::SignatureHelp {
11075 signatures: vec![lsp::SignatureInformation {
11076 label: "fn sample(param1: u8, param2: u8)".to_string(),
11077 documentation: None,
11078 parameters: Some(vec![
11079 lsp::ParameterInformation {
11080 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11081 documentation: None,
11082 },
11083 lsp::ParameterInformation {
11084 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11085 documentation: None,
11086 },
11087 ]),
11088 active_parameter: None,
11089 }],
11090 active_signature: Some(0),
11091 active_parameter: Some(0),
11092 };
11093
11094 // Ensure that signature_help is called when enabled afte edits
11095 cx.update(|_, cx| {
11096 cx.update_global::<SettingsStore, _>(|settings, cx| {
11097 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11098 settings.auto_signature_help = Some(false);
11099 settings.show_signature_help_after_edits = Some(true);
11100 });
11101 });
11102 });
11103 cx.set_state(
11104 &r#"
11105 fn main() {
11106 sampleˇ
11107 }
11108 "#
11109 .unindent(),
11110 );
11111 cx.update_editor(|editor, window, cx| {
11112 editor.handle_input("(", window, cx);
11113 });
11114 cx.assert_editor_state(
11115 &"
11116 fn main() {
11117 sample(ˇ)
11118 }
11119 "
11120 .unindent(),
11121 );
11122 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11123 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11124 .await;
11125 cx.update_editor(|editor, _, _| {
11126 let signature_help_state = editor.signature_help_state.popover().cloned();
11127 assert!(signature_help_state.is_some());
11128 let signature = signature_help_state.unwrap();
11129 assert_eq!(
11130 signature.signatures[signature.current_signature].label,
11131 "fn sample(param1: u8, param2: u8)"
11132 );
11133 editor.signature_help_state = SignatureHelpState::default();
11134 });
11135
11136 // Ensure that signature_help is called when auto signature help override is enabled
11137 cx.update(|_, cx| {
11138 cx.update_global::<SettingsStore, _>(|settings, cx| {
11139 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11140 settings.auto_signature_help = Some(true);
11141 settings.show_signature_help_after_edits = Some(false);
11142 });
11143 });
11144 });
11145 cx.set_state(
11146 &r#"
11147 fn main() {
11148 sampleˇ
11149 }
11150 "#
11151 .unindent(),
11152 );
11153 cx.update_editor(|editor, window, cx| {
11154 editor.handle_input("(", window, cx);
11155 });
11156 cx.assert_editor_state(
11157 &"
11158 fn main() {
11159 sample(ˇ)
11160 }
11161 "
11162 .unindent(),
11163 );
11164 handle_signature_help_request(&mut cx, mocked_response).await;
11165 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11166 .await;
11167 cx.editor(|editor, _, _| {
11168 let signature_help_state = editor.signature_help_state.popover().cloned();
11169 assert!(signature_help_state.is_some());
11170 let signature = signature_help_state.unwrap();
11171 assert_eq!(
11172 signature.signatures[signature.current_signature].label,
11173 "fn sample(param1: u8, param2: u8)"
11174 );
11175 });
11176}
11177
11178#[gpui::test]
11179async fn test_signature_help(cx: &mut TestAppContext) {
11180 init_test(cx, |_| {});
11181 cx.update(|cx| {
11182 cx.update_global::<SettingsStore, _>(|settings, cx| {
11183 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11184 settings.auto_signature_help = Some(true);
11185 });
11186 });
11187 });
11188
11189 let mut cx = EditorLspTestContext::new_rust(
11190 lsp::ServerCapabilities {
11191 signature_help_provider: Some(lsp::SignatureHelpOptions {
11192 ..Default::default()
11193 }),
11194 ..Default::default()
11195 },
11196 cx,
11197 )
11198 .await;
11199
11200 // A test that directly calls `show_signature_help`
11201 cx.update_editor(|editor, window, cx| {
11202 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11203 });
11204
11205 let mocked_response = lsp::SignatureHelp {
11206 signatures: vec![lsp::SignatureInformation {
11207 label: "fn sample(param1: u8, param2: u8)".to_string(),
11208 documentation: None,
11209 parameters: Some(vec![
11210 lsp::ParameterInformation {
11211 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11212 documentation: None,
11213 },
11214 lsp::ParameterInformation {
11215 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11216 documentation: None,
11217 },
11218 ]),
11219 active_parameter: None,
11220 }],
11221 active_signature: Some(0),
11222 active_parameter: Some(0),
11223 };
11224 handle_signature_help_request(&mut cx, mocked_response).await;
11225
11226 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11227 .await;
11228
11229 cx.editor(|editor, _, _| {
11230 let signature_help_state = editor.signature_help_state.popover().cloned();
11231 assert!(signature_help_state.is_some());
11232 let signature = signature_help_state.unwrap();
11233 assert_eq!(
11234 signature.signatures[signature.current_signature].label,
11235 "fn sample(param1: u8, param2: u8)"
11236 );
11237 });
11238
11239 // When exiting outside from inside the brackets, `signature_help` is closed.
11240 cx.set_state(indoc! {"
11241 fn main() {
11242 sample(ˇ);
11243 }
11244
11245 fn sample(param1: u8, param2: u8) {}
11246 "});
11247
11248 cx.update_editor(|editor, window, cx| {
11249 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11250 s.select_ranges([0..0])
11251 });
11252 });
11253
11254 let mocked_response = lsp::SignatureHelp {
11255 signatures: Vec::new(),
11256 active_signature: None,
11257 active_parameter: None,
11258 };
11259 handle_signature_help_request(&mut cx, mocked_response).await;
11260
11261 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11262 .await;
11263
11264 cx.editor(|editor, _, _| {
11265 assert!(!editor.signature_help_state.is_shown());
11266 });
11267
11268 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
11269 cx.set_state(indoc! {"
11270 fn main() {
11271 sample(ˇ);
11272 }
11273
11274 fn sample(param1: u8, param2: u8) {}
11275 "});
11276
11277 let mocked_response = lsp::SignatureHelp {
11278 signatures: vec![lsp::SignatureInformation {
11279 label: "fn sample(param1: u8, param2: u8)".to_string(),
11280 documentation: None,
11281 parameters: Some(vec![
11282 lsp::ParameterInformation {
11283 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11284 documentation: None,
11285 },
11286 lsp::ParameterInformation {
11287 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11288 documentation: None,
11289 },
11290 ]),
11291 active_parameter: None,
11292 }],
11293 active_signature: Some(0),
11294 active_parameter: Some(0),
11295 };
11296 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11297 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11298 .await;
11299 cx.editor(|editor, _, _| {
11300 assert!(editor.signature_help_state.is_shown());
11301 });
11302
11303 // Restore the popover with more parameter input
11304 cx.set_state(indoc! {"
11305 fn main() {
11306 sample(param1, param2ˇ);
11307 }
11308
11309 fn sample(param1: u8, param2: u8) {}
11310 "});
11311
11312 let mocked_response = lsp::SignatureHelp {
11313 signatures: vec![lsp::SignatureInformation {
11314 label: "fn sample(param1: u8, param2: u8)".to_string(),
11315 documentation: None,
11316 parameters: Some(vec![
11317 lsp::ParameterInformation {
11318 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11319 documentation: None,
11320 },
11321 lsp::ParameterInformation {
11322 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11323 documentation: None,
11324 },
11325 ]),
11326 active_parameter: None,
11327 }],
11328 active_signature: Some(0),
11329 active_parameter: Some(1),
11330 };
11331 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11332 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11333 .await;
11334
11335 // When selecting a range, the popover is gone.
11336 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
11337 cx.update_editor(|editor, window, cx| {
11338 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11339 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11340 })
11341 });
11342 cx.assert_editor_state(indoc! {"
11343 fn main() {
11344 sample(param1, «ˇparam2»);
11345 }
11346
11347 fn sample(param1: u8, param2: u8) {}
11348 "});
11349 cx.editor(|editor, _, _| {
11350 assert!(!editor.signature_help_state.is_shown());
11351 });
11352
11353 // When unselecting again, the popover is back if within the brackets.
11354 cx.update_editor(|editor, window, cx| {
11355 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11356 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11357 })
11358 });
11359 cx.assert_editor_state(indoc! {"
11360 fn main() {
11361 sample(param1, ˇparam2);
11362 }
11363
11364 fn sample(param1: u8, param2: u8) {}
11365 "});
11366 handle_signature_help_request(&mut cx, mocked_response).await;
11367 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11368 .await;
11369 cx.editor(|editor, _, _| {
11370 assert!(editor.signature_help_state.is_shown());
11371 });
11372
11373 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
11374 cx.update_editor(|editor, window, cx| {
11375 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11376 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
11377 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11378 })
11379 });
11380 cx.assert_editor_state(indoc! {"
11381 fn main() {
11382 sample(param1, ˇparam2);
11383 }
11384
11385 fn sample(param1: u8, param2: u8) {}
11386 "});
11387
11388 let mocked_response = lsp::SignatureHelp {
11389 signatures: vec![lsp::SignatureInformation {
11390 label: "fn sample(param1: u8, param2: u8)".to_string(),
11391 documentation: None,
11392 parameters: Some(vec![
11393 lsp::ParameterInformation {
11394 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11395 documentation: None,
11396 },
11397 lsp::ParameterInformation {
11398 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11399 documentation: None,
11400 },
11401 ]),
11402 active_parameter: None,
11403 }],
11404 active_signature: Some(0),
11405 active_parameter: Some(1),
11406 };
11407 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11408 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11409 .await;
11410 cx.update_editor(|editor, _, cx| {
11411 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
11412 });
11413 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11414 .await;
11415 cx.update_editor(|editor, window, cx| {
11416 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11417 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11418 })
11419 });
11420 cx.assert_editor_state(indoc! {"
11421 fn main() {
11422 sample(param1, «ˇparam2»);
11423 }
11424
11425 fn sample(param1: u8, param2: u8) {}
11426 "});
11427 cx.update_editor(|editor, window, cx| {
11428 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11429 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11430 })
11431 });
11432 cx.assert_editor_state(indoc! {"
11433 fn main() {
11434 sample(param1, ˇparam2);
11435 }
11436
11437 fn sample(param1: u8, param2: u8) {}
11438 "});
11439 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
11440 .await;
11441}
11442
11443#[gpui::test]
11444async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
11445 init_test(cx, |_| {});
11446
11447 let mut cx = EditorLspTestContext::new_rust(
11448 lsp::ServerCapabilities {
11449 signature_help_provider: Some(lsp::SignatureHelpOptions {
11450 ..Default::default()
11451 }),
11452 ..Default::default()
11453 },
11454 cx,
11455 )
11456 .await;
11457
11458 cx.set_state(indoc! {"
11459 fn main() {
11460 overloadedˇ
11461 }
11462 "});
11463
11464 cx.update_editor(|editor, window, cx| {
11465 editor.handle_input("(", window, cx);
11466 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11467 });
11468
11469 // Mock response with 3 signatures
11470 let mocked_response = lsp::SignatureHelp {
11471 signatures: vec![
11472 lsp::SignatureInformation {
11473 label: "fn overloaded(x: i32)".to_string(),
11474 documentation: None,
11475 parameters: Some(vec![lsp::ParameterInformation {
11476 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11477 documentation: None,
11478 }]),
11479 active_parameter: None,
11480 },
11481 lsp::SignatureInformation {
11482 label: "fn overloaded(x: i32, y: i32)".to_string(),
11483 documentation: None,
11484 parameters: Some(vec![
11485 lsp::ParameterInformation {
11486 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11487 documentation: None,
11488 },
11489 lsp::ParameterInformation {
11490 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11491 documentation: None,
11492 },
11493 ]),
11494 active_parameter: None,
11495 },
11496 lsp::SignatureInformation {
11497 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
11498 documentation: None,
11499 parameters: Some(vec![
11500 lsp::ParameterInformation {
11501 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11502 documentation: None,
11503 },
11504 lsp::ParameterInformation {
11505 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11506 documentation: None,
11507 },
11508 lsp::ParameterInformation {
11509 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
11510 documentation: None,
11511 },
11512 ]),
11513 active_parameter: None,
11514 },
11515 ],
11516 active_signature: Some(1),
11517 active_parameter: Some(0),
11518 };
11519 handle_signature_help_request(&mut cx, mocked_response).await;
11520
11521 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11522 .await;
11523
11524 // Verify we have multiple signatures and the right one is selected
11525 cx.editor(|editor, _, _| {
11526 let popover = editor.signature_help_state.popover().cloned().unwrap();
11527 assert_eq!(popover.signatures.len(), 3);
11528 // active_signature was 1, so that should be the current
11529 assert_eq!(popover.current_signature, 1);
11530 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
11531 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
11532 assert_eq!(
11533 popover.signatures[2].label,
11534 "fn overloaded(x: i32, y: i32, z: i32)"
11535 );
11536 });
11537
11538 // Test navigation functionality
11539 cx.update_editor(|editor, window, cx| {
11540 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11541 });
11542
11543 cx.editor(|editor, _, _| {
11544 let popover = editor.signature_help_state.popover().cloned().unwrap();
11545 assert_eq!(popover.current_signature, 2);
11546 });
11547
11548 // Test wrap around
11549 cx.update_editor(|editor, window, cx| {
11550 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11551 });
11552
11553 cx.editor(|editor, _, _| {
11554 let popover = editor.signature_help_state.popover().cloned().unwrap();
11555 assert_eq!(popover.current_signature, 0);
11556 });
11557
11558 // Test previous navigation
11559 cx.update_editor(|editor, window, cx| {
11560 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
11561 });
11562
11563 cx.editor(|editor, _, _| {
11564 let popover = editor.signature_help_state.popover().cloned().unwrap();
11565 assert_eq!(popover.current_signature, 2);
11566 });
11567}
11568
11569#[gpui::test]
11570async fn test_completion_mode(cx: &mut TestAppContext) {
11571 init_test(cx, |_| {});
11572 let mut cx = EditorLspTestContext::new_rust(
11573 lsp::ServerCapabilities {
11574 completion_provider: Some(lsp::CompletionOptions {
11575 resolve_provider: Some(true),
11576 ..Default::default()
11577 }),
11578 ..Default::default()
11579 },
11580 cx,
11581 )
11582 .await;
11583
11584 struct Run {
11585 run_description: &'static str,
11586 initial_state: String,
11587 buffer_marked_text: String,
11588 completion_label: &'static str,
11589 completion_text: &'static str,
11590 expected_with_insert_mode: String,
11591 expected_with_replace_mode: String,
11592 expected_with_replace_subsequence_mode: String,
11593 expected_with_replace_suffix_mode: String,
11594 }
11595
11596 let runs = [
11597 Run {
11598 run_description: "Start of word matches completion text",
11599 initial_state: "before ediˇ after".into(),
11600 buffer_marked_text: "before <edi|> after".into(),
11601 completion_label: "editor",
11602 completion_text: "editor",
11603 expected_with_insert_mode: "before editorˇ after".into(),
11604 expected_with_replace_mode: "before editorˇ after".into(),
11605 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11606 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11607 },
11608 Run {
11609 run_description: "Accept same text at the middle of the word",
11610 initial_state: "before ediˇtor after".into(),
11611 buffer_marked_text: "before <edi|tor> after".into(),
11612 completion_label: "editor",
11613 completion_text: "editor",
11614 expected_with_insert_mode: "before editorˇtor after".into(),
11615 expected_with_replace_mode: "before editorˇ after".into(),
11616 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11617 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11618 },
11619 Run {
11620 run_description: "End of word matches completion text -- cursor at end",
11621 initial_state: "before torˇ after".into(),
11622 buffer_marked_text: "before <tor|> after".into(),
11623 completion_label: "editor",
11624 completion_text: "editor",
11625 expected_with_insert_mode: "before editorˇ after".into(),
11626 expected_with_replace_mode: "before editorˇ after".into(),
11627 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11628 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11629 },
11630 Run {
11631 run_description: "End of word matches completion text -- cursor at start",
11632 initial_state: "before ˇtor after".into(),
11633 buffer_marked_text: "before <|tor> after".into(),
11634 completion_label: "editor",
11635 completion_text: "editor",
11636 expected_with_insert_mode: "before editorˇtor after".into(),
11637 expected_with_replace_mode: "before editorˇ after".into(),
11638 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11639 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11640 },
11641 Run {
11642 run_description: "Prepend text containing whitespace",
11643 initial_state: "pˇfield: bool".into(),
11644 buffer_marked_text: "<p|field>: bool".into(),
11645 completion_label: "pub ",
11646 completion_text: "pub ",
11647 expected_with_insert_mode: "pub ˇfield: bool".into(),
11648 expected_with_replace_mode: "pub ˇ: bool".into(),
11649 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
11650 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
11651 },
11652 Run {
11653 run_description: "Add element to start of list",
11654 initial_state: "[element_ˇelement_2]".into(),
11655 buffer_marked_text: "[<element_|element_2>]".into(),
11656 completion_label: "element_1",
11657 completion_text: "element_1",
11658 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
11659 expected_with_replace_mode: "[element_1ˇ]".into(),
11660 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
11661 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
11662 },
11663 Run {
11664 run_description: "Add element to start of list -- first and second elements are equal",
11665 initial_state: "[elˇelement]".into(),
11666 buffer_marked_text: "[<el|element>]".into(),
11667 completion_label: "element",
11668 completion_text: "element",
11669 expected_with_insert_mode: "[elementˇelement]".into(),
11670 expected_with_replace_mode: "[elementˇ]".into(),
11671 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
11672 expected_with_replace_suffix_mode: "[elementˇ]".into(),
11673 },
11674 Run {
11675 run_description: "Ends with matching suffix",
11676 initial_state: "SubˇError".into(),
11677 buffer_marked_text: "<Sub|Error>".into(),
11678 completion_label: "SubscriptionError",
11679 completion_text: "SubscriptionError",
11680 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
11681 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11682 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11683 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
11684 },
11685 Run {
11686 run_description: "Suffix is a subsequence -- contiguous",
11687 initial_state: "SubˇErr".into(),
11688 buffer_marked_text: "<Sub|Err>".into(),
11689 completion_label: "SubscriptionError",
11690 completion_text: "SubscriptionError",
11691 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
11692 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11693 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11694 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
11695 },
11696 Run {
11697 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
11698 initial_state: "Suˇscrirr".into(),
11699 buffer_marked_text: "<Su|scrirr>".into(),
11700 completion_label: "SubscriptionError",
11701 completion_text: "SubscriptionError",
11702 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
11703 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11704 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11705 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
11706 },
11707 Run {
11708 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
11709 initial_state: "foo(indˇix)".into(),
11710 buffer_marked_text: "foo(<ind|ix>)".into(),
11711 completion_label: "node_index",
11712 completion_text: "node_index",
11713 expected_with_insert_mode: "foo(node_indexˇix)".into(),
11714 expected_with_replace_mode: "foo(node_indexˇ)".into(),
11715 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
11716 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
11717 },
11718 Run {
11719 run_description: "Replace range ends before cursor - should extend to cursor",
11720 initial_state: "before editˇo after".into(),
11721 buffer_marked_text: "before <{ed}>it|o after".into(),
11722 completion_label: "editor",
11723 completion_text: "editor",
11724 expected_with_insert_mode: "before editorˇo after".into(),
11725 expected_with_replace_mode: "before editorˇo after".into(),
11726 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
11727 expected_with_replace_suffix_mode: "before editorˇo after".into(),
11728 },
11729 Run {
11730 run_description: "Uses label for suffix matching",
11731 initial_state: "before ediˇtor after".into(),
11732 buffer_marked_text: "before <edi|tor> after".into(),
11733 completion_label: "editor",
11734 completion_text: "editor()",
11735 expected_with_insert_mode: "before editor()ˇtor after".into(),
11736 expected_with_replace_mode: "before editor()ˇ after".into(),
11737 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
11738 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
11739 },
11740 Run {
11741 run_description: "Case insensitive subsequence and suffix matching",
11742 initial_state: "before EDiˇtoR after".into(),
11743 buffer_marked_text: "before <EDi|toR> after".into(),
11744 completion_label: "editor",
11745 completion_text: "editor",
11746 expected_with_insert_mode: "before editorˇtoR after".into(),
11747 expected_with_replace_mode: "before editorˇ after".into(),
11748 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11749 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11750 },
11751 ];
11752
11753 for run in runs {
11754 let run_variations = [
11755 (LspInsertMode::Insert, run.expected_with_insert_mode),
11756 (LspInsertMode::Replace, run.expected_with_replace_mode),
11757 (
11758 LspInsertMode::ReplaceSubsequence,
11759 run.expected_with_replace_subsequence_mode,
11760 ),
11761 (
11762 LspInsertMode::ReplaceSuffix,
11763 run.expected_with_replace_suffix_mode,
11764 ),
11765 ];
11766
11767 for (lsp_insert_mode, expected_text) in run_variations {
11768 eprintln!(
11769 "run = {:?}, mode = {lsp_insert_mode:.?}",
11770 run.run_description,
11771 );
11772
11773 update_test_language_settings(&mut cx, |settings| {
11774 settings.defaults.completions = Some(CompletionSettings {
11775 lsp_insert_mode,
11776 words: WordsCompletionMode::Disabled,
11777 lsp: true,
11778 lsp_fetch_timeout_ms: 0,
11779 });
11780 });
11781
11782 cx.set_state(&run.initial_state);
11783 cx.update_editor(|editor, window, cx| {
11784 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11785 });
11786
11787 let counter = Arc::new(AtomicUsize::new(0));
11788 handle_completion_request_with_insert_and_replace(
11789 &mut cx,
11790 &run.buffer_marked_text,
11791 vec![(run.completion_label, run.completion_text)],
11792 counter.clone(),
11793 )
11794 .await;
11795 cx.condition(|editor, _| editor.context_menu_visible())
11796 .await;
11797 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11798
11799 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11800 editor
11801 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11802 .unwrap()
11803 });
11804 cx.assert_editor_state(&expected_text);
11805 handle_resolve_completion_request(&mut cx, None).await;
11806 apply_additional_edits.await.unwrap();
11807 }
11808 }
11809}
11810
11811#[gpui::test]
11812async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
11813 init_test(cx, |_| {});
11814 let mut cx = EditorLspTestContext::new_rust(
11815 lsp::ServerCapabilities {
11816 completion_provider: Some(lsp::CompletionOptions {
11817 resolve_provider: Some(true),
11818 ..Default::default()
11819 }),
11820 ..Default::default()
11821 },
11822 cx,
11823 )
11824 .await;
11825
11826 let initial_state = "SubˇError";
11827 let buffer_marked_text = "<Sub|Error>";
11828 let completion_text = "SubscriptionError";
11829 let expected_with_insert_mode = "SubscriptionErrorˇError";
11830 let expected_with_replace_mode = "SubscriptionErrorˇ";
11831
11832 update_test_language_settings(&mut cx, |settings| {
11833 settings.defaults.completions = Some(CompletionSettings {
11834 words: WordsCompletionMode::Disabled,
11835 // set the opposite here to ensure that the action is overriding the default behavior
11836 lsp_insert_mode: LspInsertMode::Insert,
11837 lsp: true,
11838 lsp_fetch_timeout_ms: 0,
11839 });
11840 });
11841
11842 cx.set_state(initial_state);
11843 cx.update_editor(|editor, window, cx| {
11844 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11845 });
11846
11847 let counter = Arc::new(AtomicUsize::new(0));
11848 handle_completion_request_with_insert_and_replace(
11849 &mut cx,
11850 &buffer_marked_text,
11851 vec![(completion_text, completion_text)],
11852 counter.clone(),
11853 )
11854 .await;
11855 cx.condition(|editor, _| editor.context_menu_visible())
11856 .await;
11857 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11858
11859 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11860 editor
11861 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11862 .unwrap()
11863 });
11864 cx.assert_editor_state(&expected_with_replace_mode);
11865 handle_resolve_completion_request(&mut cx, None).await;
11866 apply_additional_edits.await.unwrap();
11867
11868 update_test_language_settings(&mut cx, |settings| {
11869 settings.defaults.completions = Some(CompletionSettings {
11870 words: WordsCompletionMode::Disabled,
11871 // set the opposite here to ensure that the action is overriding the default behavior
11872 lsp_insert_mode: LspInsertMode::Replace,
11873 lsp: true,
11874 lsp_fetch_timeout_ms: 0,
11875 });
11876 });
11877
11878 cx.set_state(initial_state);
11879 cx.update_editor(|editor, window, cx| {
11880 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11881 });
11882 handle_completion_request_with_insert_and_replace(
11883 &mut cx,
11884 &buffer_marked_text,
11885 vec![(completion_text, completion_text)],
11886 counter.clone(),
11887 )
11888 .await;
11889 cx.condition(|editor, _| editor.context_menu_visible())
11890 .await;
11891 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11892
11893 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11894 editor
11895 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
11896 .unwrap()
11897 });
11898 cx.assert_editor_state(&expected_with_insert_mode);
11899 handle_resolve_completion_request(&mut cx, None).await;
11900 apply_additional_edits.await.unwrap();
11901}
11902
11903#[gpui::test]
11904async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
11905 init_test(cx, |_| {});
11906 let mut cx = EditorLspTestContext::new_rust(
11907 lsp::ServerCapabilities {
11908 completion_provider: Some(lsp::CompletionOptions {
11909 resolve_provider: Some(true),
11910 ..Default::default()
11911 }),
11912 ..Default::default()
11913 },
11914 cx,
11915 )
11916 .await;
11917
11918 // scenario: surrounding text matches completion text
11919 let completion_text = "to_offset";
11920 let initial_state = indoc! {"
11921 1. buf.to_offˇsuffix
11922 2. buf.to_offˇsuf
11923 3. buf.to_offˇfix
11924 4. buf.to_offˇ
11925 5. into_offˇensive
11926 6. ˇsuffix
11927 7. let ˇ //
11928 8. aaˇzz
11929 9. buf.to_off«zzzzzˇ»suffix
11930 10. buf.«ˇzzzzz»suffix
11931 11. to_off«ˇzzzzz»
11932
11933 buf.to_offˇsuffix // newest cursor
11934 "};
11935 let completion_marked_buffer = indoc! {"
11936 1. buf.to_offsuffix
11937 2. buf.to_offsuf
11938 3. buf.to_offfix
11939 4. buf.to_off
11940 5. into_offensive
11941 6. suffix
11942 7. let //
11943 8. aazz
11944 9. buf.to_offzzzzzsuffix
11945 10. buf.zzzzzsuffix
11946 11. to_offzzzzz
11947
11948 buf.<to_off|suffix> // newest cursor
11949 "};
11950 let expected = indoc! {"
11951 1. buf.to_offsetˇ
11952 2. buf.to_offsetˇsuf
11953 3. buf.to_offsetˇfix
11954 4. buf.to_offsetˇ
11955 5. into_offsetˇensive
11956 6. to_offsetˇsuffix
11957 7. let to_offsetˇ //
11958 8. aato_offsetˇzz
11959 9. buf.to_offsetˇ
11960 10. buf.to_offsetˇsuffix
11961 11. to_offsetˇ
11962
11963 buf.to_offsetˇ // newest cursor
11964 "};
11965 cx.set_state(initial_state);
11966 cx.update_editor(|editor, window, cx| {
11967 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11968 });
11969 handle_completion_request_with_insert_and_replace(
11970 &mut cx,
11971 completion_marked_buffer,
11972 vec![(completion_text, completion_text)],
11973 Arc::new(AtomicUsize::new(0)),
11974 )
11975 .await;
11976 cx.condition(|editor, _| editor.context_menu_visible())
11977 .await;
11978 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11979 editor
11980 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11981 .unwrap()
11982 });
11983 cx.assert_editor_state(expected);
11984 handle_resolve_completion_request(&mut cx, None).await;
11985 apply_additional_edits.await.unwrap();
11986
11987 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
11988 let completion_text = "foo_and_bar";
11989 let initial_state = indoc! {"
11990 1. ooanbˇ
11991 2. zooanbˇ
11992 3. ooanbˇz
11993 4. zooanbˇz
11994 5. ooanˇ
11995 6. oanbˇ
11996
11997 ooanbˇ
11998 "};
11999 let completion_marked_buffer = indoc! {"
12000 1. ooanb
12001 2. zooanb
12002 3. ooanbz
12003 4. zooanbz
12004 5. ooan
12005 6. oanb
12006
12007 <ooanb|>
12008 "};
12009 let expected = indoc! {"
12010 1. foo_and_barˇ
12011 2. zfoo_and_barˇ
12012 3. foo_and_barˇz
12013 4. zfoo_and_barˇz
12014 5. ooanfoo_and_barˇ
12015 6. oanbfoo_and_barˇ
12016
12017 foo_and_barˇ
12018 "};
12019 cx.set_state(initial_state);
12020 cx.update_editor(|editor, window, cx| {
12021 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12022 });
12023 handle_completion_request_with_insert_and_replace(
12024 &mut cx,
12025 completion_marked_buffer,
12026 vec![(completion_text, completion_text)],
12027 Arc::new(AtomicUsize::new(0)),
12028 )
12029 .await;
12030 cx.condition(|editor, _| editor.context_menu_visible())
12031 .await;
12032 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12033 editor
12034 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12035 .unwrap()
12036 });
12037 cx.assert_editor_state(expected);
12038 handle_resolve_completion_request(&mut cx, None).await;
12039 apply_additional_edits.await.unwrap();
12040
12041 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
12042 // (expects the same as if it was inserted at the end)
12043 let completion_text = "foo_and_bar";
12044 let initial_state = indoc! {"
12045 1. ooˇanb
12046 2. zooˇanb
12047 3. ooˇanbz
12048 4. zooˇanbz
12049
12050 ooˇanb
12051 "};
12052 let completion_marked_buffer = indoc! {"
12053 1. ooanb
12054 2. zooanb
12055 3. ooanbz
12056 4. zooanbz
12057
12058 <oo|anb>
12059 "};
12060 let expected = indoc! {"
12061 1. foo_and_barˇ
12062 2. zfoo_and_barˇ
12063 3. foo_and_barˇz
12064 4. zfoo_and_barˇz
12065
12066 foo_and_barˇ
12067 "};
12068 cx.set_state(initial_state);
12069 cx.update_editor(|editor, window, cx| {
12070 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12071 });
12072 handle_completion_request_with_insert_and_replace(
12073 &mut cx,
12074 completion_marked_buffer,
12075 vec![(completion_text, completion_text)],
12076 Arc::new(AtomicUsize::new(0)),
12077 )
12078 .await;
12079 cx.condition(|editor, _| editor.context_menu_visible())
12080 .await;
12081 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12082 editor
12083 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12084 .unwrap()
12085 });
12086 cx.assert_editor_state(expected);
12087 handle_resolve_completion_request(&mut cx, None).await;
12088 apply_additional_edits.await.unwrap();
12089}
12090
12091// This used to crash
12092#[gpui::test]
12093async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
12094 init_test(cx, |_| {});
12095
12096 let buffer_text = indoc! {"
12097 fn main() {
12098 10.satu;
12099
12100 //
12101 // separate cursors so they open in different excerpts (manually reproducible)
12102 //
12103
12104 10.satu20;
12105 }
12106 "};
12107 let multibuffer_text_with_selections = indoc! {"
12108 fn main() {
12109 10.satuˇ;
12110
12111 //
12112
12113 //
12114
12115 10.satuˇ20;
12116 }
12117 "};
12118 let expected_multibuffer = indoc! {"
12119 fn main() {
12120 10.saturating_sub()ˇ;
12121
12122 //
12123
12124 //
12125
12126 10.saturating_sub()ˇ;
12127 }
12128 "};
12129
12130 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
12131 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
12132
12133 let fs = FakeFs::new(cx.executor());
12134 fs.insert_tree(
12135 path!("/a"),
12136 json!({
12137 "main.rs": buffer_text,
12138 }),
12139 )
12140 .await;
12141
12142 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12143 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12144 language_registry.add(rust_lang());
12145 let mut fake_servers = language_registry.register_fake_lsp(
12146 "Rust",
12147 FakeLspAdapter {
12148 capabilities: lsp::ServerCapabilities {
12149 completion_provider: Some(lsp::CompletionOptions {
12150 resolve_provider: None,
12151 ..lsp::CompletionOptions::default()
12152 }),
12153 ..lsp::ServerCapabilities::default()
12154 },
12155 ..FakeLspAdapter::default()
12156 },
12157 );
12158 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12159 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12160 let buffer = project
12161 .update(cx, |project, cx| {
12162 project.open_local_buffer(path!("/a/main.rs"), cx)
12163 })
12164 .await
12165 .unwrap();
12166
12167 let multi_buffer = cx.new(|cx| {
12168 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
12169 multi_buffer.push_excerpts(
12170 buffer.clone(),
12171 [ExcerptRange::new(0..first_excerpt_end)],
12172 cx,
12173 );
12174 multi_buffer.push_excerpts(
12175 buffer.clone(),
12176 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
12177 cx,
12178 );
12179 multi_buffer
12180 });
12181
12182 let editor = workspace
12183 .update(cx, |_, window, cx| {
12184 cx.new(|cx| {
12185 Editor::new(
12186 EditorMode::Full {
12187 scale_ui_elements_with_buffer_font_size: false,
12188 show_active_line_background: false,
12189 sized_by_content: false,
12190 },
12191 multi_buffer.clone(),
12192 Some(project.clone()),
12193 window,
12194 cx,
12195 )
12196 })
12197 })
12198 .unwrap();
12199
12200 let pane = workspace
12201 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12202 .unwrap();
12203 pane.update_in(cx, |pane, window, cx| {
12204 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
12205 });
12206
12207 let fake_server = fake_servers.next().await.unwrap();
12208
12209 editor.update_in(cx, |editor, window, cx| {
12210 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12211 s.select_ranges([
12212 Point::new(1, 11)..Point::new(1, 11),
12213 Point::new(7, 11)..Point::new(7, 11),
12214 ])
12215 });
12216
12217 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
12218 });
12219
12220 editor.update_in(cx, |editor, window, cx| {
12221 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12222 });
12223
12224 fake_server
12225 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12226 let completion_item = lsp::CompletionItem {
12227 label: "saturating_sub()".into(),
12228 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
12229 lsp::InsertReplaceEdit {
12230 new_text: "saturating_sub()".to_owned(),
12231 insert: lsp::Range::new(
12232 lsp::Position::new(7, 7),
12233 lsp::Position::new(7, 11),
12234 ),
12235 replace: lsp::Range::new(
12236 lsp::Position::new(7, 7),
12237 lsp::Position::new(7, 13),
12238 ),
12239 },
12240 )),
12241 ..lsp::CompletionItem::default()
12242 };
12243
12244 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
12245 })
12246 .next()
12247 .await
12248 .unwrap();
12249
12250 cx.condition(&editor, |editor, _| editor.context_menu_visible())
12251 .await;
12252
12253 editor
12254 .update_in(cx, |editor, window, cx| {
12255 editor
12256 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12257 .unwrap()
12258 })
12259 .await
12260 .unwrap();
12261
12262 editor.update(cx, |editor, cx| {
12263 assert_text_with_selections(editor, expected_multibuffer, cx);
12264 })
12265}
12266
12267#[gpui::test]
12268async fn test_completion(cx: &mut TestAppContext) {
12269 init_test(cx, |_| {});
12270
12271 let mut cx = EditorLspTestContext::new_rust(
12272 lsp::ServerCapabilities {
12273 completion_provider: Some(lsp::CompletionOptions {
12274 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12275 resolve_provider: Some(true),
12276 ..Default::default()
12277 }),
12278 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12279 ..Default::default()
12280 },
12281 cx,
12282 )
12283 .await;
12284 let counter = Arc::new(AtomicUsize::new(0));
12285
12286 cx.set_state(indoc! {"
12287 oneˇ
12288 two
12289 three
12290 "});
12291 cx.simulate_keystroke(".");
12292 handle_completion_request(
12293 indoc! {"
12294 one.|<>
12295 two
12296 three
12297 "},
12298 vec!["first_completion", "second_completion"],
12299 true,
12300 counter.clone(),
12301 &mut cx,
12302 )
12303 .await;
12304 cx.condition(|editor, _| editor.context_menu_visible())
12305 .await;
12306 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12307
12308 let _handler = handle_signature_help_request(
12309 &mut cx,
12310 lsp::SignatureHelp {
12311 signatures: vec![lsp::SignatureInformation {
12312 label: "test signature".to_string(),
12313 documentation: None,
12314 parameters: Some(vec![lsp::ParameterInformation {
12315 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
12316 documentation: None,
12317 }]),
12318 active_parameter: None,
12319 }],
12320 active_signature: None,
12321 active_parameter: None,
12322 },
12323 );
12324 cx.update_editor(|editor, window, cx| {
12325 assert!(
12326 !editor.signature_help_state.is_shown(),
12327 "No signature help was called for"
12328 );
12329 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12330 });
12331 cx.run_until_parked();
12332 cx.update_editor(|editor, _, _| {
12333 assert!(
12334 !editor.signature_help_state.is_shown(),
12335 "No signature help should be shown when completions menu is open"
12336 );
12337 });
12338
12339 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12340 editor.context_menu_next(&Default::default(), window, cx);
12341 editor
12342 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12343 .unwrap()
12344 });
12345 cx.assert_editor_state(indoc! {"
12346 one.second_completionˇ
12347 two
12348 three
12349 "});
12350
12351 handle_resolve_completion_request(
12352 &mut cx,
12353 Some(vec![
12354 (
12355 //This overlaps with the primary completion edit which is
12356 //misbehavior from the LSP spec, test that we filter it out
12357 indoc! {"
12358 one.second_ˇcompletion
12359 two
12360 threeˇ
12361 "},
12362 "overlapping additional edit",
12363 ),
12364 (
12365 indoc! {"
12366 one.second_completion
12367 two
12368 threeˇ
12369 "},
12370 "\nadditional edit",
12371 ),
12372 ]),
12373 )
12374 .await;
12375 apply_additional_edits.await.unwrap();
12376 cx.assert_editor_state(indoc! {"
12377 one.second_completionˇ
12378 two
12379 three
12380 additional edit
12381 "});
12382
12383 cx.set_state(indoc! {"
12384 one.second_completion
12385 twoˇ
12386 threeˇ
12387 additional edit
12388 "});
12389 cx.simulate_keystroke(" ");
12390 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12391 cx.simulate_keystroke("s");
12392 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12393
12394 cx.assert_editor_state(indoc! {"
12395 one.second_completion
12396 two sˇ
12397 three sˇ
12398 additional edit
12399 "});
12400 handle_completion_request(
12401 indoc! {"
12402 one.second_completion
12403 two s
12404 three <s|>
12405 additional edit
12406 "},
12407 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12408 true,
12409 counter.clone(),
12410 &mut cx,
12411 )
12412 .await;
12413 cx.condition(|editor, _| editor.context_menu_visible())
12414 .await;
12415 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12416
12417 cx.simulate_keystroke("i");
12418
12419 handle_completion_request(
12420 indoc! {"
12421 one.second_completion
12422 two si
12423 three <si|>
12424 additional edit
12425 "},
12426 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12427 true,
12428 counter.clone(),
12429 &mut cx,
12430 )
12431 .await;
12432 cx.condition(|editor, _| editor.context_menu_visible())
12433 .await;
12434 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12435
12436 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12437 editor
12438 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12439 .unwrap()
12440 });
12441 cx.assert_editor_state(indoc! {"
12442 one.second_completion
12443 two sixth_completionˇ
12444 three sixth_completionˇ
12445 additional edit
12446 "});
12447
12448 apply_additional_edits.await.unwrap();
12449
12450 update_test_language_settings(&mut cx, |settings| {
12451 settings.defaults.show_completions_on_input = Some(false);
12452 });
12453 cx.set_state("editorˇ");
12454 cx.simulate_keystroke(".");
12455 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12456 cx.simulate_keystrokes("c l o");
12457 cx.assert_editor_state("editor.cloˇ");
12458 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12459 cx.update_editor(|editor, window, cx| {
12460 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12461 });
12462 handle_completion_request(
12463 "editor.<clo|>",
12464 vec!["close", "clobber"],
12465 true,
12466 counter.clone(),
12467 &mut cx,
12468 )
12469 .await;
12470 cx.condition(|editor, _| editor.context_menu_visible())
12471 .await;
12472 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
12473
12474 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12475 editor
12476 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12477 .unwrap()
12478 });
12479 cx.assert_editor_state("editor.clobberˇ");
12480 handle_resolve_completion_request(&mut cx, None).await;
12481 apply_additional_edits.await.unwrap();
12482}
12483
12484#[gpui::test]
12485async fn test_completion_reuse(cx: &mut TestAppContext) {
12486 init_test(cx, |_| {});
12487
12488 let mut cx = EditorLspTestContext::new_rust(
12489 lsp::ServerCapabilities {
12490 completion_provider: Some(lsp::CompletionOptions {
12491 trigger_characters: Some(vec![".".to_string()]),
12492 ..Default::default()
12493 }),
12494 ..Default::default()
12495 },
12496 cx,
12497 )
12498 .await;
12499
12500 let counter = Arc::new(AtomicUsize::new(0));
12501 cx.set_state("objˇ");
12502 cx.simulate_keystroke(".");
12503
12504 // Initial completion request returns complete results
12505 let is_incomplete = false;
12506 handle_completion_request(
12507 "obj.|<>",
12508 vec!["a", "ab", "abc"],
12509 is_incomplete,
12510 counter.clone(),
12511 &mut cx,
12512 )
12513 .await;
12514 cx.run_until_parked();
12515 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12516 cx.assert_editor_state("obj.ˇ");
12517 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12518
12519 // Type "a" - filters existing completions
12520 cx.simulate_keystroke("a");
12521 cx.run_until_parked();
12522 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12523 cx.assert_editor_state("obj.aˇ");
12524 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12525
12526 // Type "b" - filters existing completions
12527 cx.simulate_keystroke("b");
12528 cx.run_until_parked();
12529 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12530 cx.assert_editor_state("obj.abˇ");
12531 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12532
12533 // Type "c" - filters existing completions
12534 cx.simulate_keystroke("c");
12535 cx.run_until_parked();
12536 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12537 cx.assert_editor_state("obj.abcˇ");
12538 check_displayed_completions(vec!["abc"], &mut cx);
12539
12540 // Backspace to delete "c" - filters existing completions
12541 cx.update_editor(|editor, window, cx| {
12542 editor.backspace(&Backspace, window, cx);
12543 });
12544 cx.run_until_parked();
12545 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12546 cx.assert_editor_state("obj.abˇ");
12547 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12548
12549 // Moving cursor to the left dismisses menu.
12550 cx.update_editor(|editor, window, cx| {
12551 editor.move_left(&MoveLeft, window, cx);
12552 });
12553 cx.run_until_parked();
12554 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12555 cx.assert_editor_state("obj.aˇb");
12556 cx.update_editor(|editor, _, _| {
12557 assert_eq!(editor.context_menu_visible(), false);
12558 });
12559
12560 // Type "b" - new request
12561 cx.simulate_keystroke("b");
12562 let is_incomplete = false;
12563 handle_completion_request(
12564 "obj.<ab|>a",
12565 vec!["ab", "abc"],
12566 is_incomplete,
12567 counter.clone(),
12568 &mut cx,
12569 )
12570 .await;
12571 cx.run_until_parked();
12572 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12573 cx.assert_editor_state("obj.abˇb");
12574 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12575
12576 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
12577 cx.update_editor(|editor, window, cx| {
12578 editor.backspace(&Backspace, window, cx);
12579 });
12580 let is_incomplete = false;
12581 handle_completion_request(
12582 "obj.<a|>b",
12583 vec!["a", "ab", "abc"],
12584 is_incomplete,
12585 counter.clone(),
12586 &mut cx,
12587 )
12588 .await;
12589 cx.run_until_parked();
12590 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12591 cx.assert_editor_state("obj.aˇb");
12592 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12593
12594 // Backspace to delete "a" - dismisses menu.
12595 cx.update_editor(|editor, window, cx| {
12596 editor.backspace(&Backspace, window, cx);
12597 });
12598 cx.run_until_parked();
12599 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12600 cx.assert_editor_state("obj.ˇb");
12601 cx.update_editor(|editor, _, _| {
12602 assert_eq!(editor.context_menu_visible(), false);
12603 });
12604}
12605
12606#[gpui::test]
12607async fn test_word_completion(cx: &mut TestAppContext) {
12608 let lsp_fetch_timeout_ms = 10;
12609 init_test(cx, |language_settings| {
12610 language_settings.defaults.completions = Some(CompletionSettings {
12611 words: WordsCompletionMode::Fallback,
12612 lsp: true,
12613 lsp_fetch_timeout_ms: 10,
12614 lsp_insert_mode: LspInsertMode::Insert,
12615 });
12616 });
12617
12618 let mut cx = EditorLspTestContext::new_rust(
12619 lsp::ServerCapabilities {
12620 completion_provider: Some(lsp::CompletionOptions {
12621 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12622 ..lsp::CompletionOptions::default()
12623 }),
12624 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12625 ..lsp::ServerCapabilities::default()
12626 },
12627 cx,
12628 )
12629 .await;
12630
12631 let throttle_completions = Arc::new(AtomicBool::new(false));
12632
12633 let lsp_throttle_completions = throttle_completions.clone();
12634 let _completion_requests_handler =
12635 cx.lsp
12636 .server
12637 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
12638 let lsp_throttle_completions = lsp_throttle_completions.clone();
12639 let cx = cx.clone();
12640 async move {
12641 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
12642 cx.background_executor()
12643 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
12644 .await;
12645 }
12646 Ok(Some(lsp::CompletionResponse::Array(vec![
12647 lsp::CompletionItem {
12648 label: "first".into(),
12649 ..lsp::CompletionItem::default()
12650 },
12651 lsp::CompletionItem {
12652 label: "last".into(),
12653 ..lsp::CompletionItem::default()
12654 },
12655 ])))
12656 }
12657 });
12658
12659 cx.set_state(indoc! {"
12660 oneˇ
12661 two
12662 three
12663 "});
12664 cx.simulate_keystroke(".");
12665 cx.executor().run_until_parked();
12666 cx.condition(|editor, _| editor.context_menu_visible())
12667 .await;
12668 cx.update_editor(|editor, window, cx| {
12669 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12670 {
12671 assert_eq!(
12672 completion_menu_entries(&menu),
12673 &["first", "last"],
12674 "When LSP server is fast to reply, no fallback word completions are used"
12675 );
12676 } else {
12677 panic!("expected completion menu to be open");
12678 }
12679 editor.cancel(&Cancel, window, cx);
12680 });
12681 cx.executor().run_until_parked();
12682 cx.condition(|editor, _| !editor.context_menu_visible())
12683 .await;
12684
12685 throttle_completions.store(true, atomic::Ordering::Release);
12686 cx.simulate_keystroke(".");
12687 cx.executor()
12688 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
12689 cx.executor().run_until_parked();
12690 cx.condition(|editor, _| editor.context_menu_visible())
12691 .await;
12692 cx.update_editor(|editor, _, _| {
12693 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12694 {
12695 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
12696 "When LSP server is slow, document words can be shown instead, if configured accordingly");
12697 } else {
12698 panic!("expected completion menu to be open");
12699 }
12700 });
12701}
12702
12703#[gpui::test]
12704async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
12705 init_test(cx, |language_settings| {
12706 language_settings.defaults.completions = Some(CompletionSettings {
12707 words: WordsCompletionMode::Enabled,
12708 lsp: true,
12709 lsp_fetch_timeout_ms: 0,
12710 lsp_insert_mode: LspInsertMode::Insert,
12711 });
12712 });
12713
12714 let mut cx = EditorLspTestContext::new_rust(
12715 lsp::ServerCapabilities {
12716 completion_provider: Some(lsp::CompletionOptions {
12717 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12718 ..lsp::CompletionOptions::default()
12719 }),
12720 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12721 ..lsp::ServerCapabilities::default()
12722 },
12723 cx,
12724 )
12725 .await;
12726
12727 let _completion_requests_handler =
12728 cx.lsp
12729 .server
12730 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12731 Ok(Some(lsp::CompletionResponse::Array(vec![
12732 lsp::CompletionItem {
12733 label: "first".into(),
12734 ..lsp::CompletionItem::default()
12735 },
12736 lsp::CompletionItem {
12737 label: "last".into(),
12738 ..lsp::CompletionItem::default()
12739 },
12740 ])))
12741 });
12742
12743 cx.set_state(indoc! {"ˇ
12744 first
12745 last
12746 second
12747 "});
12748 cx.simulate_keystroke(".");
12749 cx.executor().run_until_parked();
12750 cx.condition(|editor, _| editor.context_menu_visible())
12751 .await;
12752 cx.update_editor(|editor, _, _| {
12753 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12754 {
12755 assert_eq!(
12756 completion_menu_entries(&menu),
12757 &["first", "last", "second"],
12758 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
12759 );
12760 } else {
12761 panic!("expected completion menu to be open");
12762 }
12763 });
12764}
12765
12766#[gpui::test]
12767async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
12768 init_test(cx, |language_settings| {
12769 language_settings.defaults.completions = Some(CompletionSettings {
12770 words: WordsCompletionMode::Disabled,
12771 lsp: true,
12772 lsp_fetch_timeout_ms: 0,
12773 lsp_insert_mode: LspInsertMode::Insert,
12774 });
12775 });
12776
12777 let mut cx = EditorLspTestContext::new_rust(
12778 lsp::ServerCapabilities {
12779 completion_provider: Some(lsp::CompletionOptions {
12780 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12781 ..lsp::CompletionOptions::default()
12782 }),
12783 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12784 ..lsp::ServerCapabilities::default()
12785 },
12786 cx,
12787 )
12788 .await;
12789
12790 let _completion_requests_handler =
12791 cx.lsp
12792 .server
12793 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12794 panic!("LSP completions should not be queried when dealing with word completions")
12795 });
12796
12797 cx.set_state(indoc! {"ˇ
12798 first
12799 last
12800 second
12801 "});
12802 cx.update_editor(|editor, window, cx| {
12803 editor.show_word_completions(&ShowWordCompletions, window, cx);
12804 });
12805 cx.executor().run_until_parked();
12806 cx.condition(|editor, _| editor.context_menu_visible())
12807 .await;
12808 cx.update_editor(|editor, _, _| {
12809 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12810 {
12811 assert_eq!(
12812 completion_menu_entries(&menu),
12813 &["first", "last", "second"],
12814 "`ShowWordCompletions` action should show word completions"
12815 );
12816 } else {
12817 panic!("expected completion menu to be open");
12818 }
12819 });
12820
12821 cx.simulate_keystroke("l");
12822 cx.executor().run_until_parked();
12823 cx.condition(|editor, _| editor.context_menu_visible())
12824 .await;
12825 cx.update_editor(|editor, _, _| {
12826 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12827 {
12828 assert_eq!(
12829 completion_menu_entries(&menu),
12830 &["last"],
12831 "After showing word completions, further editing should filter them and not query the LSP"
12832 );
12833 } else {
12834 panic!("expected completion menu to be open");
12835 }
12836 });
12837}
12838
12839#[gpui::test]
12840async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
12841 init_test(cx, |language_settings| {
12842 language_settings.defaults.completions = Some(CompletionSettings {
12843 words: WordsCompletionMode::Fallback,
12844 lsp: false,
12845 lsp_fetch_timeout_ms: 0,
12846 lsp_insert_mode: LspInsertMode::Insert,
12847 });
12848 });
12849
12850 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12851
12852 cx.set_state(indoc! {"ˇ
12853 0_usize
12854 let
12855 33
12856 4.5f32
12857 "});
12858 cx.update_editor(|editor, window, cx| {
12859 editor.show_completions(&ShowCompletions::default(), window, cx);
12860 });
12861 cx.executor().run_until_parked();
12862 cx.condition(|editor, _| editor.context_menu_visible())
12863 .await;
12864 cx.update_editor(|editor, window, cx| {
12865 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12866 {
12867 assert_eq!(
12868 completion_menu_entries(&menu),
12869 &["let"],
12870 "With no digits in the completion query, no digits should be in the word completions"
12871 );
12872 } else {
12873 panic!("expected completion menu to be open");
12874 }
12875 editor.cancel(&Cancel, window, cx);
12876 });
12877
12878 cx.set_state(indoc! {"3ˇ
12879 0_usize
12880 let
12881 3
12882 33.35f32
12883 "});
12884 cx.update_editor(|editor, window, cx| {
12885 editor.show_completions(&ShowCompletions::default(), window, cx);
12886 });
12887 cx.executor().run_until_parked();
12888 cx.condition(|editor, _| editor.context_menu_visible())
12889 .await;
12890 cx.update_editor(|editor, _, _| {
12891 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12892 {
12893 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
12894 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
12895 } else {
12896 panic!("expected completion menu to be open");
12897 }
12898 });
12899}
12900
12901fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
12902 let position = || lsp::Position {
12903 line: params.text_document_position.position.line,
12904 character: params.text_document_position.position.character,
12905 };
12906 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12907 range: lsp::Range {
12908 start: position(),
12909 end: position(),
12910 },
12911 new_text: text.to_string(),
12912 }))
12913}
12914
12915#[gpui::test]
12916async fn test_multiline_completion(cx: &mut TestAppContext) {
12917 init_test(cx, |_| {});
12918
12919 let fs = FakeFs::new(cx.executor());
12920 fs.insert_tree(
12921 path!("/a"),
12922 json!({
12923 "main.ts": "a",
12924 }),
12925 )
12926 .await;
12927
12928 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12929 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12930 let typescript_language = Arc::new(Language::new(
12931 LanguageConfig {
12932 name: "TypeScript".into(),
12933 matcher: LanguageMatcher {
12934 path_suffixes: vec!["ts".to_string()],
12935 ..LanguageMatcher::default()
12936 },
12937 line_comments: vec!["// ".into()],
12938 ..LanguageConfig::default()
12939 },
12940 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12941 ));
12942 language_registry.add(typescript_language.clone());
12943 let mut fake_servers = language_registry.register_fake_lsp(
12944 "TypeScript",
12945 FakeLspAdapter {
12946 capabilities: lsp::ServerCapabilities {
12947 completion_provider: Some(lsp::CompletionOptions {
12948 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12949 ..lsp::CompletionOptions::default()
12950 }),
12951 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12952 ..lsp::ServerCapabilities::default()
12953 },
12954 // Emulate vtsls label generation
12955 label_for_completion: Some(Box::new(|item, _| {
12956 let text = if let Some(description) = item
12957 .label_details
12958 .as_ref()
12959 .and_then(|label_details| label_details.description.as_ref())
12960 {
12961 format!("{} {}", item.label, description)
12962 } else if let Some(detail) = &item.detail {
12963 format!("{} {}", item.label, detail)
12964 } else {
12965 item.label.clone()
12966 };
12967 let len = text.len();
12968 Some(language::CodeLabel {
12969 text,
12970 runs: Vec::new(),
12971 filter_range: 0..len,
12972 })
12973 })),
12974 ..FakeLspAdapter::default()
12975 },
12976 );
12977 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12978 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12979 let worktree_id = workspace
12980 .update(cx, |workspace, _window, cx| {
12981 workspace.project().update(cx, |project, cx| {
12982 project.worktrees(cx).next().unwrap().read(cx).id()
12983 })
12984 })
12985 .unwrap();
12986 let _buffer = project
12987 .update(cx, |project, cx| {
12988 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
12989 })
12990 .await
12991 .unwrap();
12992 let editor = workspace
12993 .update(cx, |workspace, window, cx| {
12994 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
12995 })
12996 .unwrap()
12997 .await
12998 .unwrap()
12999 .downcast::<Editor>()
13000 .unwrap();
13001 let fake_server = fake_servers.next().await.unwrap();
13002
13003 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
13004 let multiline_label_2 = "a\nb\nc\n";
13005 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
13006 let multiline_description = "d\ne\nf\n";
13007 let multiline_detail_2 = "g\nh\ni\n";
13008
13009 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
13010 move |params, _| async move {
13011 Ok(Some(lsp::CompletionResponse::Array(vec![
13012 lsp::CompletionItem {
13013 label: multiline_label.to_string(),
13014 text_edit: gen_text_edit(¶ms, "new_text_1"),
13015 ..lsp::CompletionItem::default()
13016 },
13017 lsp::CompletionItem {
13018 label: "single line label 1".to_string(),
13019 detail: Some(multiline_detail.to_string()),
13020 text_edit: gen_text_edit(¶ms, "new_text_2"),
13021 ..lsp::CompletionItem::default()
13022 },
13023 lsp::CompletionItem {
13024 label: "single line label 2".to_string(),
13025 label_details: Some(lsp::CompletionItemLabelDetails {
13026 description: Some(multiline_description.to_string()),
13027 detail: None,
13028 }),
13029 text_edit: gen_text_edit(¶ms, "new_text_2"),
13030 ..lsp::CompletionItem::default()
13031 },
13032 lsp::CompletionItem {
13033 label: multiline_label_2.to_string(),
13034 detail: Some(multiline_detail_2.to_string()),
13035 text_edit: gen_text_edit(¶ms, "new_text_3"),
13036 ..lsp::CompletionItem::default()
13037 },
13038 lsp::CompletionItem {
13039 label: "Label with many spaces and \t but without newlines".to_string(),
13040 detail: Some(
13041 "Details with many spaces and \t but without newlines".to_string(),
13042 ),
13043 text_edit: gen_text_edit(¶ms, "new_text_4"),
13044 ..lsp::CompletionItem::default()
13045 },
13046 ])))
13047 },
13048 );
13049
13050 editor.update_in(cx, |editor, window, cx| {
13051 cx.focus_self(window);
13052 editor.move_to_end(&MoveToEnd, window, cx);
13053 editor.handle_input(".", window, cx);
13054 });
13055 cx.run_until_parked();
13056 completion_handle.next().await.unwrap();
13057
13058 editor.update(cx, |editor, _| {
13059 assert!(editor.context_menu_visible());
13060 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13061 {
13062 let completion_labels = menu
13063 .completions
13064 .borrow()
13065 .iter()
13066 .map(|c| c.label.text.clone())
13067 .collect::<Vec<_>>();
13068 assert_eq!(
13069 completion_labels,
13070 &[
13071 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
13072 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
13073 "single line label 2 d e f ",
13074 "a b c g h i ",
13075 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
13076 ],
13077 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
13078 );
13079
13080 for completion in menu
13081 .completions
13082 .borrow()
13083 .iter() {
13084 assert_eq!(
13085 completion.label.filter_range,
13086 0..completion.label.text.len(),
13087 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
13088 );
13089 }
13090 } else {
13091 panic!("expected completion menu to be open");
13092 }
13093 });
13094}
13095
13096#[gpui::test]
13097async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
13098 init_test(cx, |_| {});
13099 let mut cx = EditorLspTestContext::new_rust(
13100 lsp::ServerCapabilities {
13101 completion_provider: Some(lsp::CompletionOptions {
13102 trigger_characters: Some(vec![".".to_string()]),
13103 ..Default::default()
13104 }),
13105 ..Default::default()
13106 },
13107 cx,
13108 )
13109 .await;
13110 cx.lsp
13111 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13112 Ok(Some(lsp::CompletionResponse::Array(vec![
13113 lsp::CompletionItem {
13114 label: "first".into(),
13115 ..Default::default()
13116 },
13117 lsp::CompletionItem {
13118 label: "last".into(),
13119 ..Default::default()
13120 },
13121 ])))
13122 });
13123 cx.set_state("variableˇ");
13124 cx.simulate_keystroke(".");
13125 cx.executor().run_until_parked();
13126
13127 cx.update_editor(|editor, _, _| {
13128 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13129 {
13130 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
13131 } else {
13132 panic!("expected completion menu to be open");
13133 }
13134 });
13135
13136 cx.update_editor(|editor, window, cx| {
13137 editor.move_page_down(&MovePageDown::default(), window, cx);
13138 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13139 {
13140 assert!(
13141 menu.selected_item == 1,
13142 "expected PageDown to select the last item from the context menu"
13143 );
13144 } else {
13145 panic!("expected completion menu to stay open after PageDown");
13146 }
13147 });
13148
13149 cx.update_editor(|editor, window, cx| {
13150 editor.move_page_up(&MovePageUp::default(), window, cx);
13151 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13152 {
13153 assert!(
13154 menu.selected_item == 0,
13155 "expected PageUp to select the first item from the context menu"
13156 );
13157 } else {
13158 panic!("expected completion menu to stay open after PageUp");
13159 }
13160 });
13161}
13162
13163#[gpui::test]
13164async fn test_as_is_completions(cx: &mut TestAppContext) {
13165 init_test(cx, |_| {});
13166 let mut cx = EditorLspTestContext::new_rust(
13167 lsp::ServerCapabilities {
13168 completion_provider: Some(lsp::CompletionOptions {
13169 ..Default::default()
13170 }),
13171 ..Default::default()
13172 },
13173 cx,
13174 )
13175 .await;
13176 cx.lsp
13177 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13178 Ok(Some(lsp::CompletionResponse::Array(vec![
13179 lsp::CompletionItem {
13180 label: "unsafe".into(),
13181 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13182 range: lsp::Range {
13183 start: lsp::Position {
13184 line: 1,
13185 character: 2,
13186 },
13187 end: lsp::Position {
13188 line: 1,
13189 character: 3,
13190 },
13191 },
13192 new_text: "unsafe".to_string(),
13193 })),
13194 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
13195 ..Default::default()
13196 },
13197 ])))
13198 });
13199 cx.set_state("fn a() {}\n nˇ");
13200 cx.executor().run_until_parked();
13201 cx.update_editor(|editor, window, cx| {
13202 editor.show_completions(
13203 &ShowCompletions {
13204 trigger: Some("\n".into()),
13205 },
13206 window,
13207 cx,
13208 );
13209 });
13210 cx.executor().run_until_parked();
13211
13212 cx.update_editor(|editor, window, cx| {
13213 editor.confirm_completion(&Default::default(), window, cx)
13214 });
13215 cx.executor().run_until_parked();
13216 cx.assert_editor_state("fn a() {}\n unsafeˇ");
13217}
13218
13219#[gpui::test]
13220async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
13221 init_test(cx, |_| {});
13222
13223 let mut cx = EditorLspTestContext::new_rust(
13224 lsp::ServerCapabilities {
13225 completion_provider: Some(lsp::CompletionOptions {
13226 trigger_characters: Some(vec![".".to_string()]),
13227 resolve_provider: Some(true),
13228 ..Default::default()
13229 }),
13230 ..Default::default()
13231 },
13232 cx,
13233 )
13234 .await;
13235
13236 cx.set_state("fn main() { let a = 2ˇ; }");
13237 cx.simulate_keystroke(".");
13238 let completion_item = lsp::CompletionItem {
13239 label: "Some".into(),
13240 kind: Some(lsp::CompletionItemKind::SNIPPET),
13241 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13242 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13243 kind: lsp::MarkupKind::Markdown,
13244 value: "```rust\nSome(2)\n```".to_string(),
13245 })),
13246 deprecated: Some(false),
13247 sort_text: Some("Some".to_string()),
13248 filter_text: Some("Some".to_string()),
13249 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13250 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13251 range: lsp::Range {
13252 start: lsp::Position {
13253 line: 0,
13254 character: 22,
13255 },
13256 end: lsp::Position {
13257 line: 0,
13258 character: 22,
13259 },
13260 },
13261 new_text: "Some(2)".to_string(),
13262 })),
13263 additional_text_edits: Some(vec![lsp::TextEdit {
13264 range: lsp::Range {
13265 start: lsp::Position {
13266 line: 0,
13267 character: 20,
13268 },
13269 end: lsp::Position {
13270 line: 0,
13271 character: 22,
13272 },
13273 },
13274 new_text: "".to_string(),
13275 }]),
13276 ..Default::default()
13277 };
13278
13279 let closure_completion_item = completion_item.clone();
13280 let counter = Arc::new(AtomicUsize::new(0));
13281 let counter_clone = counter.clone();
13282 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13283 let task_completion_item = closure_completion_item.clone();
13284 counter_clone.fetch_add(1, atomic::Ordering::Release);
13285 async move {
13286 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13287 is_incomplete: true,
13288 item_defaults: None,
13289 items: vec![task_completion_item],
13290 })))
13291 }
13292 });
13293
13294 cx.condition(|editor, _| editor.context_menu_visible())
13295 .await;
13296 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
13297 assert!(request.next().await.is_some());
13298 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13299
13300 cx.simulate_keystrokes("S o m");
13301 cx.condition(|editor, _| editor.context_menu_visible())
13302 .await;
13303 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
13304 assert!(request.next().await.is_some());
13305 assert!(request.next().await.is_some());
13306 assert!(request.next().await.is_some());
13307 request.close();
13308 assert!(request.next().await.is_none());
13309 assert_eq!(
13310 counter.load(atomic::Ordering::Acquire),
13311 4,
13312 "With the completions menu open, only one LSP request should happen per input"
13313 );
13314}
13315
13316#[gpui::test]
13317async fn test_toggle_comment(cx: &mut TestAppContext) {
13318 init_test(cx, |_| {});
13319 let mut cx = EditorTestContext::new(cx).await;
13320 let language = Arc::new(Language::new(
13321 LanguageConfig {
13322 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13323 ..Default::default()
13324 },
13325 Some(tree_sitter_rust::LANGUAGE.into()),
13326 ));
13327 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13328
13329 // If multiple selections intersect a line, the line is only toggled once.
13330 cx.set_state(indoc! {"
13331 fn a() {
13332 «//b();
13333 ˇ»// «c();
13334 //ˇ» d();
13335 }
13336 "});
13337
13338 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13339
13340 cx.assert_editor_state(indoc! {"
13341 fn a() {
13342 «b();
13343 c();
13344 ˇ» d();
13345 }
13346 "});
13347
13348 // The comment prefix is inserted at the same column for every line in a
13349 // selection.
13350 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13351
13352 cx.assert_editor_state(indoc! {"
13353 fn a() {
13354 // «b();
13355 // c();
13356 ˇ»// d();
13357 }
13358 "});
13359
13360 // If a selection ends at the beginning of a line, that line is not toggled.
13361 cx.set_selections_state(indoc! {"
13362 fn a() {
13363 // b();
13364 «// c();
13365 ˇ» // d();
13366 }
13367 "});
13368
13369 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13370
13371 cx.assert_editor_state(indoc! {"
13372 fn a() {
13373 // b();
13374 «c();
13375 ˇ» // d();
13376 }
13377 "});
13378
13379 // If a selection span a single line and is empty, the line is toggled.
13380 cx.set_state(indoc! {"
13381 fn a() {
13382 a();
13383 b();
13384 ˇ
13385 }
13386 "});
13387
13388 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13389
13390 cx.assert_editor_state(indoc! {"
13391 fn a() {
13392 a();
13393 b();
13394 //•ˇ
13395 }
13396 "});
13397
13398 // If a selection span multiple lines, empty lines are not toggled.
13399 cx.set_state(indoc! {"
13400 fn a() {
13401 «a();
13402
13403 c();ˇ»
13404 }
13405 "});
13406
13407 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13408
13409 cx.assert_editor_state(indoc! {"
13410 fn a() {
13411 // «a();
13412
13413 // c();ˇ»
13414 }
13415 "});
13416
13417 // If a selection includes multiple comment prefixes, all lines are uncommented.
13418 cx.set_state(indoc! {"
13419 fn a() {
13420 «// a();
13421 /// b();
13422 //! c();ˇ»
13423 }
13424 "});
13425
13426 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13427
13428 cx.assert_editor_state(indoc! {"
13429 fn a() {
13430 «a();
13431 b();
13432 c();ˇ»
13433 }
13434 "});
13435}
13436
13437#[gpui::test]
13438async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
13439 init_test(cx, |_| {});
13440 let mut cx = EditorTestContext::new(cx).await;
13441 let language = Arc::new(Language::new(
13442 LanguageConfig {
13443 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13444 ..Default::default()
13445 },
13446 Some(tree_sitter_rust::LANGUAGE.into()),
13447 ));
13448 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13449
13450 let toggle_comments = &ToggleComments {
13451 advance_downwards: false,
13452 ignore_indent: true,
13453 };
13454
13455 // If multiple selections intersect a line, the line is only toggled once.
13456 cx.set_state(indoc! {"
13457 fn a() {
13458 // «b();
13459 // c();
13460 // ˇ» d();
13461 }
13462 "});
13463
13464 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13465
13466 cx.assert_editor_state(indoc! {"
13467 fn a() {
13468 «b();
13469 c();
13470 ˇ» d();
13471 }
13472 "});
13473
13474 // The comment prefix is inserted at the beginning of each line
13475 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13476
13477 cx.assert_editor_state(indoc! {"
13478 fn a() {
13479 // «b();
13480 // c();
13481 // ˇ» d();
13482 }
13483 "});
13484
13485 // If a selection ends at the beginning of a line, that line is not toggled.
13486 cx.set_selections_state(indoc! {"
13487 fn a() {
13488 // b();
13489 // «c();
13490 ˇ»// d();
13491 }
13492 "});
13493
13494 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13495
13496 cx.assert_editor_state(indoc! {"
13497 fn a() {
13498 // b();
13499 «c();
13500 ˇ»// d();
13501 }
13502 "});
13503
13504 // If a selection span a single line and is empty, the line is toggled.
13505 cx.set_state(indoc! {"
13506 fn a() {
13507 a();
13508 b();
13509 ˇ
13510 }
13511 "});
13512
13513 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13514
13515 cx.assert_editor_state(indoc! {"
13516 fn a() {
13517 a();
13518 b();
13519 //ˇ
13520 }
13521 "});
13522
13523 // If a selection span multiple lines, empty lines are not toggled.
13524 cx.set_state(indoc! {"
13525 fn a() {
13526 «a();
13527
13528 c();ˇ»
13529 }
13530 "});
13531
13532 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13533
13534 cx.assert_editor_state(indoc! {"
13535 fn a() {
13536 // «a();
13537
13538 // c();ˇ»
13539 }
13540 "});
13541
13542 // If a selection includes multiple comment prefixes, all lines are uncommented.
13543 cx.set_state(indoc! {"
13544 fn a() {
13545 // «a();
13546 /// b();
13547 //! c();ˇ»
13548 }
13549 "});
13550
13551 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13552
13553 cx.assert_editor_state(indoc! {"
13554 fn a() {
13555 «a();
13556 b();
13557 c();ˇ»
13558 }
13559 "});
13560}
13561
13562#[gpui::test]
13563async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
13564 init_test(cx, |_| {});
13565
13566 let language = Arc::new(Language::new(
13567 LanguageConfig {
13568 line_comments: vec!["// ".into()],
13569 ..Default::default()
13570 },
13571 Some(tree_sitter_rust::LANGUAGE.into()),
13572 ));
13573
13574 let mut cx = EditorTestContext::new(cx).await;
13575
13576 cx.language_registry().add(language.clone());
13577 cx.update_buffer(|buffer, cx| {
13578 buffer.set_language(Some(language), cx);
13579 });
13580
13581 let toggle_comments = &ToggleComments {
13582 advance_downwards: true,
13583 ignore_indent: false,
13584 };
13585
13586 // Single cursor on one line -> advance
13587 // Cursor moves horizontally 3 characters as well on non-blank line
13588 cx.set_state(indoc!(
13589 "fn a() {
13590 ˇdog();
13591 cat();
13592 }"
13593 ));
13594 cx.update_editor(|editor, window, cx| {
13595 editor.toggle_comments(toggle_comments, window, cx);
13596 });
13597 cx.assert_editor_state(indoc!(
13598 "fn a() {
13599 // dog();
13600 catˇ();
13601 }"
13602 ));
13603
13604 // Single selection on one line -> don't advance
13605 cx.set_state(indoc!(
13606 "fn a() {
13607 «dog()ˇ»;
13608 cat();
13609 }"
13610 ));
13611 cx.update_editor(|editor, window, cx| {
13612 editor.toggle_comments(toggle_comments, window, cx);
13613 });
13614 cx.assert_editor_state(indoc!(
13615 "fn a() {
13616 // «dog()ˇ»;
13617 cat();
13618 }"
13619 ));
13620
13621 // Multiple cursors on one line -> advance
13622 cx.set_state(indoc!(
13623 "fn a() {
13624 ˇdˇog();
13625 cat();
13626 }"
13627 ));
13628 cx.update_editor(|editor, window, cx| {
13629 editor.toggle_comments(toggle_comments, window, cx);
13630 });
13631 cx.assert_editor_state(indoc!(
13632 "fn a() {
13633 // dog();
13634 catˇ(ˇ);
13635 }"
13636 ));
13637
13638 // Multiple cursors on one line, with selection -> don't advance
13639 cx.set_state(indoc!(
13640 "fn a() {
13641 ˇdˇog«()ˇ»;
13642 cat();
13643 }"
13644 ));
13645 cx.update_editor(|editor, window, cx| {
13646 editor.toggle_comments(toggle_comments, window, cx);
13647 });
13648 cx.assert_editor_state(indoc!(
13649 "fn a() {
13650 // ˇdˇog«()ˇ»;
13651 cat();
13652 }"
13653 ));
13654
13655 // Single cursor on one line -> advance
13656 // Cursor moves to column 0 on blank line
13657 cx.set_state(indoc!(
13658 "fn a() {
13659 ˇdog();
13660
13661 cat();
13662 }"
13663 ));
13664 cx.update_editor(|editor, window, cx| {
13665 editor.toggle_comments(toggle_comments, window, cx);
13666 });
13667 cx.assert_editor_state(indoc!(
13668 "fn a() {
13669 // dog();
13670 ˇ
13671 cat();
13672 }"
13673 ));
13674
13675 // Single cursor on one line -> advance
13676 // Cursor starts and ends at column 0
13677 cx.set_state(indoc!(
13678 "fn a() {
13679 ˇ dog();
13680 cat();
13681 }"
13682 ));
13683 cx.update_editor(|editor, window, cx| {
13684 editor.toggle_comments(toggle_comments, window, cx);
13685 });
13686 cx.assert_editor_state(indoc!(
13687 "fn a() {
13688 // dog();
13689 ˇ cat();
13690 }"
13691 ));
13692}
13693
13694#[gpui::test]
13695async fn test_toggle_block_comment(cx: &mut TestAppContext) {
13696 init_test(cx, |_| {});
13697
13698 let mut cx = EditorTestContext::new(cx).await;
13699
13700 let html_language = Arc::new(
13701 Language::new(
13702 LanguageConfig {
13703 name: "HTML".into(),
13704 block_comment: Some(("<!-- ".into(), " -->".into())),
13705 ..Default::default()
13706 },
13707 Some(tree_sitter_html::LANGUAGE.into()),
13708 )
13709 .with_injection_query(
13710 r#"
13711 (script_element
13712 (raw_text) @injection.content
13713 (#set! injection.language "javascript"))
13714 "#,
13715 )
13716 .unwrap(),
13717 );
13718
13719 let javascript_language = Arc::new(Language::new(
13720 LanguageConfig {
13721 name: "JavaScript".into(),
13722 line_comments: vec!["// ".into()],
13723 ..Default::default()
13724 },
13725 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13726 ));
13727
13728 cx.language_registry().add(html_language.clone());
13729 cx.language_registry().add(javascript_language.clone());
13730 cx.update_buffer(|buffer, cx| {
13731 buffer.set_language(Some(html_language), cx);
13732 });
13733
13734 // Toggle comments for empty selections
13735 cx.set_state(
13736 &r#"
13737 <p>A</p>ˇ
13738 <p>B</p>ˇ
13739 <p>C</p>ˇ
13740 "#
13741 .unindent(),
13742 );
13743 cx.update_editor(|editor, window, cx| {
13744 editor.toggle_comments(&ToggleComments::default(), window, cx)
13745 });
13746 cx.assert_editor_state(
13747 &r#"
13748 <!-- <p>A</p>ˇ -->
13749 <!-- <p>B</p>ˇ -->
13750 <!-- <p>C</p>ˇ -->
13751 "#
13752 .unindent(),
13753 );
13754 cx.update_editor(|editor, window, cx| {
13755 editor.toggle_comments(&ToggleComments::default(), window, cx)
13756 });
13757 cx.assert_editor_state(
13758 &r#"
13759 <p>A</p>ˇ
13760 <p>B</p>ˇ
13761 <p>C</p>ˇ
13762 "#
13763 .unindent(),
13764 );
13765
13766 // Toggle comments for mixture of empty and non-empty selections, where
13767 // multiple selections occupy a given line.
13768 cx.set_state(
13769 &r#"
13770 <p>A«</p>
13771 <p>ˇ»B</p>ˇ
13772 <p>C«</p>
13773 <p>ˇ»D</p>ˇ
13774 "#
13775 .unindent(),
13776 );
13777
13778 cx.update_editor(|editor, window, cx| {
13779 editor.toggle_comments(&ToggleComments::default(), window, cx)
13780 });
13781 cx.assert_editor_state(
13782 &r#"
13783 <!-- <p>A«</p>
13784 <p>ˇ»B</p>ˇ -->
13785 <!-- <p>C«</p>
13786 <p>ˇ»D</p>ˇ -->
13787 "#
13788 .unindent(),
13789 );
13790 cx.update_editor(|editor, window, cx| {
13791 editor.toggle_comments(&ToggleComments::default(), window, cx)
13792 });
13793 cx.assert_editor_state(
13794 &r#"
13795 <p>A«</p>
13796 <p>ˇ»B</p>ˇ
13797 <p>C«</p>
13798 <p>ˇ»D</p>ˇ
13799 "#
13800 .unindent(),
13801 );
13802
13803 // Toggle comments when different languages are active for different
13804 // selections.
13805 cx.set_state(
13806 &r#"
13807 ˇ<script>
13808 ˇvar x = new Y();
13809 ˇ</script>
13810 "#
13811 .unindent(),
13812 );
13813 cx.executor().run_until_parked();
13814 cx.update_editor(|editor, window, cx| {
13815 editor.toggle_comments(&ToggleComments::default(), window, cx)
13816 });
13817 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
13818 // Uncommenting and commenting from this position brings in even more wrong artifacts.
13819 cx.assert_editor_state(
13820 &r#"
13821 <!-- ˇ<script> -->
13822 // ˇvar x = new Y();
13823 <!-- ˇ</script> -->
13824 "#
13825 .unindent(),
13826 );
13827}
13828
13829#[gpui::test]
13830fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
13831 init_test(cx, |_| {});
13832
13833 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13834 let multibuffer = cx.new(|cx| {
13835 let mut multibuffer = MultiBuffer::new(ReadWrite);
13836 multibuffer.push_excerpts(
13837 buffer.clone(),
13838 [
13839 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
13840 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
13841 ],
13842 cx,
13843 );
13844 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
13845 multibuffer
13846 });
13847
13848 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13849 editor.update_in(cx, |editor, window, cx| {
13850 assert_eq!(editor.text(cx), "aaaa\nbbbb");
13851 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13852 s.select_ranges([
13853 Point::new(0, 0)..Point::new(0, 0),
13854 Point::new(1, 0)..Point::new(1, 0),
13855 ])
13856 });
13857
13858 editor.handle_input("X", window, cx);
13859 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
13860 assert_eq!(
13861 editor.selections.ranges(cx),
13862 [
13863 Point::new(0, 1)..Point::new(0, 1),
13864 Point::new(1, 1)..Point::new(1, 1),
13865 ]
13866 );
13867
13868 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
13869 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13870 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
13871 });
13872 editor.backspace(&Default::default(), window, cx);
13873 assert_eq!(editor.text(cx), "Xa\nbbb");
13874 assert_eq!(
13875 editor.selections.ranges(cx),
13876 [Point::new(1, 0)..Point::new(1, 0)]
13877 );
13878
13879 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13880 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
13881 });
13882 editor.backspace(&Default::default(), window, cx);
13883 assert_eq!(editor.text(cx), "X\nbb");
13884 assert_eq!(
13885 editor.selections.ranges(cx),
13886 [Point::new(0, 1)..Point::new(0, 1)]
13887 );
13888 });
13889}
13890
13891#[gpui::test]
13892fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
13893 init_test(cx, |_| {});
13894
13895 let markers = vec![('[', ']').into(), ('(', ')').into()];
13896 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
13897 indoc! {"
13898 [aaaa
13899 (bbbb]
13900 cccc)",
13901 },
13902 markers.clone(),
13903 );
13904 let excerpt_ranges = markers.into_iter().map(|marker| {
13905 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
13906 ExcerptRange::new(context.clone())
13907 });
13908 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
13909 let multibuffer = cx.new(|cx| {
13910 let mut multibuffer = MultiBuffer::new(ReadWrite);
13911 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
13912 multibuffer
13913 });
13914
13915 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13916 editor.update_in(cx, |editor, window, cx| {
13917 let (expected_text, selection_ranges) = marked_text_ranges(
13918 indoc! {"
13919 aaaa
13920 bˇbbb
13921 bˇbbˇb
13922 cccc"
13923 },
13924 true,
13925 );
13926 assert_eq!(editor.text(cx), expected_text);
13927 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13928 s.select_ranges(selection_ranges)
13929 });
13930
13931 editor.handle_input("X", window, cx);
13932
13933 let (expected_text, expected_selections) = marked_text_ranges(
13934 indoc! {"
13935 aaaa
13936 bXˇbbXb
13937 bXˇbbXˇb
13938 cccc"
13939 },
13940 false,
13941 );
13942 assert_eq!(editor.text(cx), expected_text);
13943 assert_eq!(editor.selections.ranges(cx), expected_selections);
13944
13945 editor.newline(&Newline, window, cx);
13946 let (expected_text, expected_selections) = marked_text_ranges(
13947 indoc! {"
13948 aaaa
13949 bX
13950 ˇbbX
13951 b
13952 bX
13953 ˇbbX
13954 ˇb
13955 cccc"
13956 },
13957 false,
13958 );
13959 assert_eq!(editor.text(cx), expected_text);
13960 assert_eq!(editor.selections.ranges(cx), expected_selections);
13961 });
13962}
13963
13964#[gpui::test]
13965fn test_refresh_selections(cx: &mut TestAppContext) {
13966 init_test(cx, |_| {});
13967
13968 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13969 let mut excerpt1_id = None;
13970 let multibuffer = cx.new(|cx| {
13971 let mut multibuffer = MultiBuffer::new(ReadWrite);
13972 excerpt1_id = multibuffer
13973 .push_excerpts(
13974 buffer.clone(),
13975 [
13976 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
13977 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
13978 ],
13979 cx,
13980 )
13981 .into_iter()
13982 .next();
13983 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
13984 multibuffer
13985 });
13986
13987 let editor = cx.add_window(|window, cx| {
13988 let mut editor = build_editor(multibuffer.clone(), window, cx);
13989 let snapshot = editor.snapshot(window, cx);
13990 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13991 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
13992 });
13993 editor.begin_selection(
13994 Point::new(2, 1).to_display_point(&snapshot),
13995 true,
13996 1,
13997 window,
13998 cx,
13999 );
14000 assert_eq!(
14001 editor.selections.ranges(cx),
14002 [
14003 Point::new(1, 3)..Point::new(1, 3),
14004 Point::new(2, 1)..Point::new(2, 1),
14005 ]
14006 );
14007 editor
14008 });
14009
14010 // Refreshing selections is a no-op when excerpts haven't changed.
14011 _ = editor.update(cx, |editor, window, cx| {
14012 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14013 assert_eq!(
14014 editor.selections.ranges(cx),
14015 [
14016 Point::new(1, 3)..Point::new(1, 3),
14017 Point::new(2, 1)..Point::new(2, 1),
14018 ]
14019 );
14020 });
14021
14022 multibuffer.update(cx, |multibuffer, cx| {
14023 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14024 });
14025 _ = editor.update(cx, |editor, window, cx| {
14026 // Removing an excerpt causes the first selection to become degenerate.
14027 assert_eq!(
14028 editor.selections.ranges(cx),
14029 [
14030 Point::new(0, 0)..Point::new(0, 0),
14031 Point::new(0, 1)..Point::new(0, 1)
14032 ]
14033 );
14034
14035 // Refreshing selections will relocate the first selection to the original buffer
14036 // location.
14037 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14038 assert_eq!(
14039 editor.selections.ranges(cx),
14040 [
14041 Point::new(0, 1)..Point::new(0, 1),
14042 Point::new(0, 3)..Point::new(0, 3)
14043 ]
14044 );
14045 assert!(editor.selections.pending_anchor().is_some());
14046 });
14047}
14048
14049#[gpui::test]
14050fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
14051 init_test(cx, |_| {});
14052
14053 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14054 let mut excerpt1_id = None;
14055 let multibuffer = cx.new(|cx| {
14056 let mut multibuffer = MultiBuffer::new(ReadWrite);
14057 excerpt1_id = multibuffer
14058 .push_excerpts(
14059 buffer.clone(),
14060 [
14061 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14062 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14063 ],
14064 cx,
14065 )
14066 .into_iter()
14067 .next();
14068 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14069 multibuffer
14070 });
14071
14072 let editor = cx.add_window(|window, cx| {
14073 let mut editor = build_editor(multibuffer.clone(), window, cx);
14074 let snapshot = editor.snapshot(window, cx);
14075 editor.begin_selection(
14076 Point::new(1, 3).to_display_point(&snapshot),
14077 false,
14078 1,
14079 window,
14080 cx,
14081 );
14082 assert_eq!(
14083 editor.selections.ranges(cx),
14084 [Point::new(1, 3)..Point::new(1, 3)]
14085 );
14086 editor
14087 });
14088
14089 multibuffer.update(cx, |multibuffer, cx| {
14090 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14091 });
14092 _ = editor.update(cx, |editor, window, cx| {
14093 assert_eq!(
14094 editor.selections.ranges(cx),
14095 [Point::new(0, 0)..Point::new(0, 0)]
14096 );
14097
14098 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
14099 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14100 assert_eq!(
14101 editor.selections.ranges(cx),
14102 [Point::new(0, 3)..Point::new(0, 3)]
14103 );
14104 assert!(editor.selections.pending_anchor().is_some());
14105 });
14106}
14107
14108#[gpui::test]
14109async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
14110 init_test(cx, |_| {});
14111
14112 let language = Arc::new(
14113 Language::new(
14114 LanguageConfig {
14115 brackets: BracketPairConfig {
14116 pairs: vec![
14117 BracketPair {
14118 start: "{".to_string(),
14119 end: "}".to_string(),
14120 close: true,
14121 surround: true,
14122 newline: true,
14123 },
14124 BracketPair {
14125 start: "/* ".to_string(),
14126 end: " */".to_string(),
14127 close: true,
14128 surround: true,
14129 newline: true,
14130 },
14131 ],
14132 ..Default::default()
14133 },
14134 ..Default::default()
14135 },
14136 Some(tree_sitter_rust::LANGUAGE.into()),
14137 )
14138 .with_indents_query("")
14139 .unwrap(),
14140 );
14141
14142 let text = concat!(
14143 "{ }\n", //
14144 " x\n", //
14145 " /* */\n", //
14146 "x\n", //
14147 "{{} }\n", //
14148 );
14149
14150 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
14151 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14152 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14153 editor
14154 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
14155 .await;
14156
14157 editor.update_in(cx, |editor, window, cx| {
14158 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14159 s.select_display_ranges([
14160 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
14161 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
14162 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
14163 ])
14164 });
14165 editor.newline(&Newline, window, cx);
14166
14167 assert_eq!(
14168 editor.buffer().read(cx).read(cx).text(),
14169 concat!(
14170 "{ \n", // Suppress rustfmt
14171 "\n", //
14172 "}\n", //
14173 " x\n", //
14174 " /* \n", //
14175 " \n", //
14176 " */\n", //
14177 "x\n", //
14178 "{{} \n", //
14179 "}\n", //
14180 )
14181 );
14182 });
14183}
14184
14185#[gpui::test]
14186fn test_highlighted_ranges(cx: &mut TestAppContext) {
14187 init_test(cx, |_| {});
14188
14189 let editor = cx.add_window(|window, cx| {
14190 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
14191 build_editor(buffer.clone(), window, cx)
14192 });
14193
14194 _ = editor.update(cx, |editor, window, cx| {
14195 struct Type1;
14196 struct Type2;
14197
14198 let buffer = editor.buffer.read(cx).snapshot(cx);
14199
14200 let anchor_range =
14201 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
14202
14203 editor.highlight_background::<Type1>(
14204 &[
14205 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
14206 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
14207 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
14208 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
14209 ],
14210 |_| Hsla::red(),
14211 cx,
14212 );
14213 editor.highlight_background::<Type2>(
14214 &[
14215 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
14216 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
14217 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
14218 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
14219 ],
14220 |_| Hsla::green(),
14221 cx,
14222 );
14223
14224 let snapshot = editor.snapshot(window, cx);
14225 let mut highlighted_ranges = editor.background_highlights_in_range(
14226 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
14227 &snapshot,
14228 cx.theme(),
14229 );
14230 // Enforce a consistent ordering based on color without relying on the ordering of the
14231 // highlight's `TypeId` which is non-executor.
14232 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
14233 assert_eq!(
14234 highlighted_ranges,
14235 &[
14236 (
14237 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
14238 Hsla::red(),
14239 ),
14240 (
14241 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14242 Hsla::red(),
14243 ),
14244 (
14245 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
14246 Hsla::green(),
14247 ),
14248 (
14249 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
14250 Hsla::green(),
14251 ),
14252 ]
14253 );
14254 assert_eq!(
14255 editor.background_highlights_in_range(
14256 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
14257 &snapshot,
14258 cx.theme(),
14259 ),
14260 &[(
14261 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14262 Hsla::red(),
14263 )]
14264 );
14265 });
14266}
14267
14268#[gpui::test]
14269async fn test_following(cx: &mut TestAppContext) {
14270 init_test(cx, |_| {});
14271
14272 let fs = FakeFs::new(cx.executor());
14273 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14274
14275 let buffer = project.update(cx, |project, cx| {
14276 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
14277 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
14278 });
14279 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
14280 let follower = cx.update(|cx| {
14281 cx.open_window(
14282 WindowOptions {
14283 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
14284 gpui::Point::new(px(0.), px(0.)),
14285 gpui::Point::new(px(10.), px(80.)),
14286 ))),
14287 ..Default::default()
14288 },
14289 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
14290 )
14291 .unwrap()
14292 });
14293
14294 let is_still_following = Rc::new(RefCell::new(true));
14295 let follower_edit_event_count = Rc::new(RefCell::new(0));
14296 let pending_update = Rc::new(RefCell::new(None));
14297 let leader_entity = leader.root(cx).unwrap();
14298 let follower_entity = follower.root(cx).unwrap();
14299 _ = follower.update(cx, {
14300 let update = pending_update.clone();
14301 let is_still_following = is_still_following.clone();
14302 let follower_edit_event_count = follower_edit_event_count.clone();
14303 |_, window, cx| {
14304 cx.subscribe_in(
14305 &leader_entity,
14306 window,
14307 move |_, leader, event, window, cx| {
14308 leader.read(cx).add_event_to_update_proto(
14309 event,
14310 &mut update.borrow_mut(),
14311 window,
14312 cx,
14313 );
14314 },
14315 )
14316 .detach();
14317
14318 cx.subscribe_in(
14319 &follower_entity,
14320 window,
14321 move |_, _, event: &EditorEvent, _window, _cx| {
14322 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
14323 *is_still_following.borrow_mut() = false;
14324 }
14325
14326 if let EditorEvent::BufferEdited = event {
14327 *follower_edit_event_count.borrow_mut() += 1;
14328 }
14329 },
14330 )
14331 .detach();
14332 }
14333 });
14334
14335 // Update the selections only
14336 _ = leader.update(cx, |leader, window, cx| {
14337 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14338 s.select_ranges([1..1])
14339 });
14340 });
14341 follower
14342 .update(cx, |follower, window, cx| {
14343 follower.apply_update_proto(
14344 &project,
14345 pending_update.borrow_mut().take().unwrap(),
14346 window,
14347 cx,
14348 )
14349 })
14350 .unwrap()
14351 .await
14352 .unwrap();
14353 _ = follower.update(cx, |follower, _, cx| {
14354 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
14355 });
14356 assert!(*is_still_following.borrow());
14357 assert_eq!(*follower_edit_event_count.borrow(), 0);
14358
14359 // Update the scroll position only
14360 _ = leader.update(cx, |leader, window, cx| {
14361 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14362 });
14363 follower
14364 .update(cx, |follower, window, cx| {
14365 follower.apply_update_proto(
14366 &project,
14367 pending_update.borrow_mut().take().unwrap(),
14368 window,
14369 cx,
14370 )
14371 })
14372 .unwrap()
14373 .await
14374 .unwrap();
14375 assert_eq!(
14376 follower
14377 .update(cx, |follower, _, cx| follower.scroll_position(cx))
14378 .unwrap(),
14379 gpui::Point::new(1.5, 3.5)
14380 );
14381 assert!(*is_still_following.borrow());
14382 assert_eq!(*follower_edit_event_count.borrow(), 0);
14383
14384 // Update the selections and scroll position. The follower's scroll position is updated
14385 // via autoscroll, not via the leader's exact scroll position.
14386 _ = leader.update(cx, |leader, window, cx| {
14387 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14388 s.select_ranges([0..0])
14389 });
14390 leader.request_autoscroll(Autoscroll::newest(), cx);
14391 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14392 });
14393 follower
14394 .update(cx, |follower, window, cx| {
14395 follower.apply_update_proto(
14396 &project,
14397 pending_update.borrow_mut().take().unwrap(),
14398 window,
14399 cx,
14400 )
14401 })
14402 .unwrap()
14403 .await
14404 .unwrap();
14405 _ = follower.update(cx, |follower, _, cx| {
14406 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
14407 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
14408 });
14409 assert!(*is_still_following.borrow());
14410
14411 // Creating a pending selection that precedes another selection
14412 _ = leader.update(cx, |leader, window, cx| {
14413 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14414 s.select_ranges([1..1])
14415 });
14416 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
14417 });
14418 follower
14419 .update(cx, |follower, window, cx| {
14420 follower.apply_update_proto(
14421 &project,
14422 pending_update.borrow_mut().take().unwrap(),
14423 window,
14424 cx,
14425 )
14426 })
14427 .unwrap()
14428 .await
14429 .unwrap();
14430 _ = follower.update(cx, |follower, _, cx| {
14431 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
14432 });
14433 assert!(*is_still_following.borrow());
14434
14435 // Extend the pending selection so that it surrounds another selection
14436 _ = leader.update(cx, |leader, window, cx| {
14437 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
14438 });
14439 follower
14440 .update(cx, |follower, window, cx| {
14441 follower.apply_update_proto(
14442 &project,
14443 pending_update.borrow_mut().take().unwrap(),
14444 window,
14445 cx,
14446 )
14447 })
14448 .unwrap()
14449 .await
14450 .unwrap();
14451 _ = follower.update(cx, |follower, _, cx| {
14452 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
14453 });
14454
14455 // Scrolling locally breaks the follow
14456 _ = follower.update(cx, |follower, window, cx| {
14457 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
14458 follower.set_scroll_anchor(
14459 ScrollAnchor {
14460 anchor: top_anchor,
14461 offset: gpui::Point::new(0.0, 0.5),
14462 },
14463 window,
14464 cx,
14465 );
14466 });
14467 assert!(!(*is_still_following.borrow()));
14468}
14469
14470#[gpui::test]
14471async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
14472 init_test(cx, |_| {});
14473
14474 let fs = FakeFs::new(cx.executor());
14475 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14476 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14477 let pane = workspace
14478 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14479 .unwrap();
14480
14481 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14482
14483 let leader = pane.update_in(cx, |_, window, cx| {
14484 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
14485 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
14486 });
14487
14488 // Start following the editor when it has no excerpts.
14489 let mut state_message =
14490 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14491 let workspace_entity = workspace.root(cx).unwrap();
14492 let follower_1 = cx
14493 .update_window(*workspace.deref(), |_, window, cx| {
14494 Editor::from_state_proto(
14495 workspace_entity,
14496 ViewId {
14497 creator: CollaboratorId::PeerId(PeerId::default()),
14498 id: 0,
14499 },
14500 &mut state_message,
14501 window,
14502 cx,
14503 )
14504 })
14505 .unwrap()
14506 .unwrap()
14507 .await
14508 .unwrap();
14509
14510 let update_message = Rc::new(RefCell::new(None));
14511 follower_1.update_in(cx, {
14512 let update = update_message.clone();
14513 |_, window, cx| {
14514 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
14515 leader.read(cx).add_event_to_update_proto(
14516 event,
14517 &mut update.borrow_mut(),
14518 window,
14519 cx,
14520 );
14521 })
14522 .detach();
14523 }
14524 });
14525
14526 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
14527 (
14528 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
14529 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
14530 )
14531 });
14532
14533 // Insert some excerpts.
14534 leader.update(cx, |leader, cx| {
14535 leader.buffer.update(cx, |multibuffer, cx| {
14536 multibuffer.set_excerpts_for_path(
14537 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
14538 buffer_1.clone(),
14539 vec![
14540 Point::row_range(0..3),
14541 Point::row_range(1..6),
14542 Point::row_range(12..15),
14543 ],
14544 0,
14545 cx,
14546 );
14547 multibuffer.set_excerpts_for_path(
14548 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
14549 buffer_2.clone(),
14550 vec![Point::row_range(0..6), Point::row_range(8..12)],
14551 0,
14552 cx,
14553 );
14554 });
14555 });
14556
14557 // Apply the update of adding the excerpts.
14558 follower_1
14559 .update_in(cx, |follower, window, cx| {
14560 follower.apply_update_proto(
14561 &project,
14562 update_message.borrow().clone().unwrap(),
14563 window,
14564 cx,
14565 )
14566 })
14567 .await
14568 .unwrap();
14569 assert_eq!(
14570 follower_1.update(cx, |editor, cx| editor.text(cx)),
14571 leader.update(cx, |editor, cx| editor.text(cx))
14572 );
14573 update_message.borrow_mut().take();
14574
14575 // Start following separately after it already has excerpts.
14576 let mut state_message =
14577 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14578 let workspace_entity = workspace.root(cx).unwrap();
14579 let follower_2 = cx
14580 .update_window(*workspace.deref(), |_, window, cx| {
14581 Editor::from_state_proto(
14582 workspace_entity,
14583 ViewId {
14584 creator: CollaboratorId::PeerId(PeerId::default()),
14585 id: 0,
14586 },
14587 &mut state_message,
14588 window,
14589 cx,
14590 )
14591 })
14592 .unwrap()
14593 .unwrap()
14594 .await
14595 .unwrap();
14596 assert_eq!(
14597 follower_2.update(cx, |editor, cx| editor.text(cx)),
14598 leader.update(cx, |editor, cx| editor.text(cx))
14599 );
14600
14601 // Remove some excerpts.
14602 leader.update(cx, |leader, cx| {
14603 leader.buffer.update(cx, |multibuffer, cx| {
14604 let excerpt_ids = multibuffer.excerpt_ids();
14605 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
14606 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
14607 });
14608 });
14609
14610 // Apply the update of removing the excerpts.
14611 follower_1
14612 .update_in(cx, |follower, window, cx| {
14613 follower.apply_update_proto(
14614 &project,
14615 update_message.borrow().clone().unwrap(),
14616 window,
14617 cx,
14618 )
14619 })
14620 .await
14621 .unwrap();
14622 follower_2
14623 .update_in(cx, |follower, window, cx| {
14624 follower.apply_update_proto(
14625 &project,
14626 update_message.borrow().clone().unwrap(),
14627 window,
14628 cx,
14629 )
14630 })
14631 .await
14632 .unwrap();
14633 update_message.borrow_mut().take();
14634 assert_eq!(
14635 follower_1.update(cx, |editor, cx| editor.text(cx)),
14636 leader.update(cx, |editor, cx| editor.text(cx))
14637 );
14638}
14639
14640#[gpui::test]
14641async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14642 init_test(cx, |_| {});
14643
14644 let mut cx = EditorTestContext::new(cx).await;
14645 let lsp_store =
14646 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
14647
14648 cx.set_state(indoc! {"
14649 ˇfn func(abc def: i32) -> u32 {
14650 }
14651 "});
14652
14653 cx.update(|_, cx| {
14654 lsp_store.update(cx, |lsp_store, cx| {
14655 lsp_store
14656 .update_diagnostics(
14657 LanguageServerId(0),
14658 lsp::PublishDiagnosticsParams {
14659 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
14660 version: None,
14661 diagnostics: vec![
14662 lsp::Diagnostic {
14663 range: lsp::Range::new(
14664 lsp::Position::new(0, 11),
14665 lsp::Position::new(0, 12),
14666 ),
14667 severity: Some(lsp::DiagnosticSeverity::ERROR),
14668 ..Default::default()
14669 },
14670 lsp::Diagnostic {
14671 range: lsp::Range::new(
14672 lsp::Position::new(0, 12),
14673 lsp::Position::new(0, 15),
14674 ),
14675 severity: Some(lsp::DiagnosticSeverity::ERROR),
14676 ..Default::default()
14677 },
14678 lsp::Diagnostic {
14679 range: lsp::Range::new(
14680 lsp::Position::new(0, 25),
14681 lsp::Position::new(0, 28),
14682 ),
14683 severity: Some(lsp::DiagnosticSeverity::ERROR),
14684 ..Default::default()
14685 },
14686 ],
14687 },
14688 None,
14689 DiagnosticSourceKind::Pushed,
14690 &[],
14691 cx,
14692 )
14693 .unwrap()
14694 });
14695 });
14696
14697 executor.run_until_parked();
14698
14699 cx.update_editor(|editor, window, cx| {
14700 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14701 });
14702
14703 cx.assert_editor_state(indoc! {"
14704 fn func(abc def: i32) -> ˇu32 {
14705 }
14706 "});
14707
14708 cx.update_editor(|editor, window, cx| {
14709 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14710 });
14711
14712 cx.assert_editor_state(indoc! {"
14713 fn func(abc ˇdef: i32) -> u32 {
14714 }
14715 "});
14716
14717 cx.update_editor(|editor, window, cx| {
14718 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14719 });
14720
14721 cx.assert_editor_state(indoc! {"
14722 fn func(abcˇ def: i32) -> u32 {
14723 }
14724 "});
14725
14726 cx.update_editor(|editor, window, cx| {
14727 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
14728 });
14729
14730 cx.assert_editor_state(indoc! {"
14731 fn func(abc def: i32) -> ˇu32 {
14732 }
14733 "});
14734}
14735
14736#[gpui::test]
14737async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14738 init_test(cx, |_| {});
14739
14740 let mut cx = EditorTestContext::new(cx).await;
14741
14742 let diff_base = r#"
14743 use some::mod;
14744
14745 const A: u32 = 42;
14746
14747 fn main() {
14748 println!("hello");
14749
14750 println!("world");
14751 }
14752 "#
14753 .unindent();
14754
14755 // Edits are modified, removed, modified, added
14756 cx.set_state(
14757 &r#"
14758 use some::modified;
14759
14760 ˇ
14761 fn main() {
14762 println!("hello there");
14763
14764 println!("around the");
14765 println!("world");
14766 }
14767 "#
14768 .unindent(),
14769 );
14770
14771 cx.set_head_text(&diff_base);
14772 executor.run_until_parked();
14773
14774 cx.update_editor(|editor, window, cx| {
14775 //Wrap around the bottom of the buffer
14776 for _ in 0..3 {
14777 editor.go_to_next_hunk(&GoToHunk, window, cx);
14778 }
14779 });
14780
14781 cx.assert_editor_state(
14782 &r#"
14783 ˇuse some::modified;
14784
14785
14786 fn main() {
14787 println!("hello there");
14788
14789 println!("around the");
14790 println!("world");
14791 }
14792 "#
14793 .unindent(),
14794 );
14795
14796 cx.update_editor(|editor, window, cx| {
14797 //Wrap around the top of the buffer
14798 for _ in 0..2 {
14799 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14800 }
14801 });
14802
14803 cx.assert_editor_state(
14804 &r#"
14805 use some::modified;
14806
14807
14808 fn main() {
14809 ˇ println!("hello there");
14810
14811 println!("around the");
14812 println!("world");
14813 }
14814 "#
14815 .unindent(),
14816 );
14817
14818 cx.update_editor(|editor, window, cx| {
14819 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14820 });
14821
14822 cx.assert_editor_state(
14823 &r#"
14824 use some::modified;
14825
14826 ˇ
14827 fn main() {
14828 println!("hello there");
14829
14830 println!("around the");
14831 println!("world");
14832 }
14833 "#
14834 .unindent(),
14835 );
14836
14837 cx.update_editor(|editor, window, cx| {
14838 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14839 });
14840
14841 cx.assert_editor_state(
14842 &r#"
14843 ˇuse some::modified;
14844
14845
14846 fn main() {
14847 println!("hello there");
14848
14849 println!("around the");
14850 println!("world");
14851 }
14852 "#
14853 .unindent(),
14854 );
14855
14856 cx.update_editor(|editor, window, cx| {
14857 for _ in 0..2 {
14858 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14859 }
14860 });
14861
14862 cx.assert_editor_state(
14863 &r#"
14864 use some::modified;
14865
14866
14867 fn main() {
14868 ˇ println!("hello there");
14869
14870 println!("around the");
14871 println!("world");
14872 }
14873 "#
14874 .unindent(),
14875 );
14876
14877 cx.update_editor(|editor, window, cx| {
14878 editor.fold(&Fold, window, cx);
14879 });
14880
14881 cx.update_editor(|editor, window, cx| {
14882 editor.go_to_next_hunk(&GoToHunk, window, cx);
14883 });
14884
14885 cx.assert_editor_state(
14886 &r#"
14887 ˇuse some::modified;
14888
14889
14890 fn main() {
14891 println!("hello there");
14892
14893 println!("around the");
14894 println!("world");
14895 }
14896 "#
14897 .unindent(),
14898 );
14899}
14900
14901#[test]
14902fn test_split_words() {
14903 fn split(text: &str) -> Vec<&str> {
14904 split_words(text).collect()
14905 }
14906
14907 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
14908 assert_eq!(split("hello_world"), &["hello_", "world"]);
14909 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
14910 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
14911 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
14912 assert_eq!(split("helloworld"), &["helloworld"]);
14913
14914 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
14915}
14916
14917#[gpui::test]
14918async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
14919 init_test(cx, |_| {});
14920
14921 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
14922 let mut assert = |before, after| {
14923 let _state_context = cx.set_state(before);
14924 cx.run_until_parked();
14925 cx.update_editor(|editor, window, cx| {
14926 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
14927 });
14928 cx.run_until_parked();
14929 cx.assert_editor_state(after);
14930 };
14931
14932 // Outside bracket jumps to outside of matching bracket
14933 assert("console.logˇ(var);", "console.log(var)ˇ;");
14934 assert("console.log(var)ˇ;", "console.logˇ(var);");
14935
14936 // Inside bracket jumps to inside of matching bracket
14937 assert("console.log(ˇvar);", "console.log(varˇ);");
14938 assert("console.log(varˇ);", "console.log(ˇvar);");
14939
14940 // When outside a bracket and inside, favor jumping to the inside bracket
14941 assert(
14942 "console.log('foo', [1, 2, 3]ˇ);",
14943 "console.log(ˇ'foo', [1, 2, 3]);",
14944 );
14945 assert(
14946 "console.log(ˇ'foo', [1, 2, 3]);",
14947 "console.log('foo', [1, 2, 3]ˇ);",
14948 );
14949
14950 // Bias forward if two options are equally likely
14951 assert(
14952 "let result = curried_fun()ˇ();",
14953 "let result = curried_fun()()ˇ;",
14954 );
14955
14956 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
14957 assert(
14958 indoc! {"
14959 function test() {
14960 console.log('test')ˇ
14961 }"},
14962 indoc! {"
14963 function test() {
14964 console.logˇ('test')
14965 }"},
14966 );
14967}
14968
14969#[gpui::test]
14970async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
14971 init_test(cx, |_| {});
14972
14973 let fs = FakeFs::new(cx.executor());
14974 fs.insert_tree(
14975 path!("/a"),
14976 json!({
14977 "main.rs": "fn main() { let a = 5; }",
14978 "other.rs": "// Test file",
14979 }),
14980 )
14981 .await;
14982 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14983
14984 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14985 language_registry.add(Arc::new(Language::new(
14986 LanguageConfig {
14987 name: "Rust".into(),
14988 matcher: LanguageMatcher {
14989 path_suffixes: vec!["rs".to_string()],
14990 ..Default::default()
14991 },
14992 brackets: BracketPairConfig {
14993 pairs: vec![BracketPair {
14994 start: "{".to_string(),
14995 end: "}".to_string(),
14996 close: true,
14997 surround: true,
14998 newline: true,
14999 }],
15000 disabled_scopes_by_bracket_ix: Vec::new(),
15001 },
15002 ..Default::default()
15003 },
15004 Some(tree_sitter_rust::LANGUAGE.into()),
15005 )));
15006 let mut fake_servers = language_registry.register_fake_lsp(
15007 "Rust",
15008 FakeLspAdapter {
15009 capabilities: lsp::ServerCapabilities {
15010 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15011 first_trigger_character: "{".to_string(),
15012 more_trigger_character: None,
15013 }),
15014 ..Default::default()
15015 },
15016 ..Default::default()
15017 },
15018 );
15019
15020 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15021
15022 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15023
15024 let worktree_id = workspace
15025 .update(cx, |workspace, _, cx| {
15026 workspace.project().update(cx, |project, cx| {
15027 project.worktrees(cx).next().unwrap().read(cx).id()
15028 })
15029 })
15030 .unwrap();
15031
15032 let buffer = project
15033 .update(cx, |project, cx| {
15034 project.open_local_buffer(path!("/a/main.rs"), cx)
15035 })
15036 .await
15037 .unwrap();
15038 let editor_handle = workspace
15039 .update(cx, |workspace, window, cx| {
15040 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
15041 })
15042 .unwrap()
15043 .await
15044 .unwrap()
15045 .downcast::<Editor>()
15046 .unwrap();
15047
15048 cx.executor().start_waiting();
15049 let fake_server = fake_servers.next().await.unwrap();
15050
15051 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
15052 |params, _| async move {
15053 assert_eq!(
15054 params.text_document_position.text_document.uri,
15055 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
15056 );
15057 assert_eq!(
15058 params.text_document_position.position,
15059 lsp::Position::new(0, 21),
15060 );
15061
15062 Ok(Some(vec![lsp::TextEdit {
15063 new_text: "]".to_string(),
15064 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15065 }]))
15066 },
15067 );
15068
15069 editor_handle.update_in(cx, |editor, window, cx| {
15070 window.focus(&editor.focus_handle(cx));
15071 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15072 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
15073 });
15074 editor.handle_input("{", window, cx);
15075 });
15076
15077 cx.executor().run_until_parked();
15078
15079 buffer.update(cx, |buffer, _| {
15080 assert_eq!(
15081 buffer.text(),
15082 "fn main() { let a = {5}; }",
15083 "No extra braces from on type formatting should appear in the buffer"
15084 )
15085 });
15086}
15087
15088#[gpui::test(iterations = 20, seeds(31))]
15089async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
15090 init_test(cx, |_| {});
15091
15092 let mut cx = EditorLspTestContext::new_rust(
15093 lsp::ServerCapabilities {
15094 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15095 first_trigger_character: ".".to_string(),
15096 more_trigger_character: None,
15097 }),
15098 ..Default::default()
15099 },
15100 cx,
15101 )
15102 .await;
15103
15104 cx.update_buffer(|buffer, _| {
15105 // This causes autoindent to be async.
15106 buffer.set_sync_parse_timeout(Duration::ZERO)
15107 });
15108
15109 cx.set_state("fn c() {\n d()ˇ\n}\n");
15110 cx.simulate_keystroke("\n");
15111 cx.run_until_parked();
15112
15113 let buffer_cloned =
15114 cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap().clone());
15115 let mut request =
15116 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
15117 let buffer_cloned = buffer_cloned.clone();
15118 async move {
15119 buffer_cloned.update(&mut cx, |buffer, _| {
15120 assert_eq!(
15121 buffer.text(),
15122 "fn c() {\n d()\n .\n}\n",
15123 "OnTypeFormatting should triggered after autoindent applied"
15124 )
15125 })?;
15126
15127 Ok(Some(vec![]))
15128 }
15129 });
15130
15131 cx.simulate_keystroke(".");
15132 cx.run_until_parked();
15133
15134 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
15135 assert!(request.next().await.is_some());
15136 request.close();
15137 assert!(request.next().await.is_none());
15138}
15139
15140#[gpui::test]
15141async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
15142 init_test(cx, |_| {});
15143
15144 let fs = FakeFs::new(cx.executor());
15145 fs.insert_tree(
15146 path!("/a"),
15147 json!({
15148 "main.rs": "fn main() { let a = 5; }",
15149 "other.rs": "// Test file",
15150 }),
15151 )
15152 .await;
15153
15154 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15155
15156 let server_restarts = Arc::new(AtomicUsize::new(0));
15157 let closure_restarts = Arc::clone(&server_restarts);
15158 let language_server_name = "test language server";
15159 let language_name: LanguageName = "Rust".into();
15160
15161 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15162 language_registry.add(Arc::new(Language::new(
15163 LanguageConfig {
15164 name: language_name.clone(),
15165 matcher: LanguageMatcher {
15166 path_suffixes: vec!["rs".to_string()],
15167 ..Default::default()
15168 },
15169 ..Default::default()
15170 },
15171 Some(tree_sitter_rust::LANGUAGE.into()),
15172 )));
15173 let mut fake_servers = language_registry.register_fake_lsp(
15174 "Rust",
15175 FakeLspAdapter {
15176 name: language_server_name,
15177 initialization_options: Some(json!({
15178 "testOptionValue": true
15179 })),
15180 initializer: Some(Box::new(move |fake_server| {
15181 let task_restarts = Arc::clone(&closure_restarts);
15182 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
15183 task_restarts.fetch_add(1, atomic::Ordering::Release);
15184 futures::future::ready(Ok(()))
15185 });
15186 })),
15187 ..Default::default()
15188 },
15189 );
15190
15191 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15192 let _buffer = project
15193 .update(cx, |project, cx| {
15194 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
15195 })
15196 .await
15197 .unwrap();
15198 let _fake_server = fake_servers.next().await.unwrap();
15199 update_test_language_settings(cx, |language_settings| {
15200 language_settings.languages.0.insert(
15201 language_name.clone(),
15202 LanguageSettingsContent {
15203 tab_size: NonZeroU32::new(8),
15204 ..Default::default()
15205 },
15206 );
15207 });
15208 cx.executor().run_until_parked();
15209 assert_eq!(
15210 server_restarts.load(atomic::Ordering::Acquire),
15211 0,
15212 "Should not restart LSP server on an unrelated change"
15213 );
15214
15215 update_test_project_settings(cx, |project_settings| {
15216 project_settings.lsp.insert(
15217 "Some other server name".into(),
15218 LspSettings {
15219 binary: None,
15220 settings: None,
15221 initialization_options: Some(json!({
15222 "some other init value": false
15223 })),
15224 enable_lsp_tasks: false,
15225 },
15226 );
15227 });
15228 cx.executor().run_until_parked();
15229 assert_eq!(
15230 server_restarts.load(atomic::Ordering::Acquire),
15231 0,
15232 "Should not restart LSP server on an unrelated LSP settings change"
15233 );
15234
15235 update_test_project_settings(cx, |project_settings| {
15236 project_settings.lsp.insert(
15237 language_server_name.into(),
15238 LspSettings {
15239 binary: None,
15240 settings: None,
15241 initialization_options: Some(json!({
15242 "anotherInitValue": false
15243 })),
15244 enable_lsp_tasks: false,
15245 },
15246 );
15247 });
15248 cx.executor().run_until_parked();
15249 assert_eq!(
15250 server_restarts.load(atomic::Ordering::Acquire),
15251 1,
15252 "Should restart LSP server on a related LSP settings change"
15253 );
15254
15255 update_test_project_settings(cx, |project_settings| {
15256 project_settings.lsp.insert(
15257 language_server_name.into(),
15258 LspSettings {
15259 binary: None,
15260 settings: None,
15261 initialization_options: Some(json!({
15262 "anotherInitValue": false
15263 })),
15264 enable_lsp_tasks: false,
15265 },
15266 );
15267 });
15268 cx.executor().run_until_parked();
15269 assert_eq!(
15270 server_restarts.load(atomic::Ordering::Acquire),
15271 1,
15272 "Should not restart LSP server on a related LSP settings change that is the same"
15273 );
15274
15275 update_test_project_settings(cx, |project_settings| {
15276 project_settings.lsp.insert(
15277 language_server_name.into(),
15278 LspSettings {
15279 binary: None,
15280 settings: None,
15281 initialization_options: None,
15282 enable_lsp_tasks: false,
15283 },
15284 );
15285 });
15286 cx.executor().run_until_parked();
15287 assert_eq!(
15288 server_restarts.load(atomic::Ordering::Acquire),
15289 2,
15290 "Should restart LSP server on another related LSP settings change"
15291 );
15292}
15293
15294#[gpui::test]
15295async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
15296 init_test(cx, |_| {});
15297
15298 let mut cx = EditorLspTestContext::new_rust(
15299 lsp::ServerCapabilities {
15300 completion_provider: Some(lsp::CompletionOptions {
15301 trigger_characters: Some(vec![".".to_string()]),
15302 resolve_provider: Some(true),
15303 ..Default::default()
15304 }),
15305 ..Default::default()
15306 },
15307 cx,
15308 )
15309 .await;
15310
15311 cx.set_state("fn main() { let a = 2ˇ; }");
15312 cx.simulate_keystroke(".");
15313 let completion_item = lsp::CompletionItem {
15314 label: "some".into(),
15315 kind: Some(lsp::CompletionItemKind::SNIPPET),
15316 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15317 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15318 kind: lsp::MarkupKind::Markdown,
15319 value: "```rust\nSome(2)\n```".to_string(),
15320 })),
15321 deprecated: Some(false),
15322 sort_text: Some("fffffff2".to_string()),
15323 filter_text: Some("some".to_string()),
15324 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15325 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15326 range: lsp::Range {
15327 start: lsp::Position {
15328 line: 0,
15329 character: 22,
15330 },
15331 end: lsp::Position {
15332 line: 0,
15333 character: 22,
15334 },
15335 },
15336 new_text: "Some(2)".to_string(),
15337 })),
15338 additional_text_edits: Some(vec![lsp::TextEdit {
15339 range: lsp::Range {
15340 start: lsp::Position {
15341 line: 0,
15342 character: 20,
15343 },
15344 end: lsp::Position {
15345 line: 0,
15346 character: 22,
15347 },
15348 },
15349 new_text: "".to_string(),
15350 }]),
15351 ..Default::default()
15352 };
15353
15354 let closure_completion_item = completion_item.clone();
15355 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15356 let task_completion_item = closure_completion_item.clone();
15357 async move {
15358 Ok(Some(lsp::CompletionResponse::Array(vec![
15359 task_completion_item,
15360 ])))
15361 }
15362 });
15363
15364 request.next().await;
15365
15366 cx.condition(|editor, _| editor.context_menu_visible())
15367 .await;
15368 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15369 editor
15370 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15371 .unwrap()
15372 });
15373 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
15374
15375 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
15376 let task_completion_item = completion_item.clone();
15377 async move { Ok(task_completion_item) }
15378 })
15379 .next()
15380 .await
15381 .unwrap();
15382 apply_additional_edits.await.unwrap();
15383 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
15384}
15385
15386#[gpui::test]
15387async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
15388 init_test(cx, |_| {});
15389
15390 let mut cx = EditorLspTestContext::new_rust(
15391 lsp::ServerCapabilities {
15392 completion_provider: Some(lsp::CompletionOptions {
15393 trigger_characters: Some(vec![".".to_string()]),
15394 resolve_provider: Some(true),
15395 ..Default::default()
15396 }),
15397 ..Default::default()
15398 },
15399 cx,
15400 )
15401 .await;
15402
15403 cx.set_state("fn main() { let a = 2ˇ; }");
15404 cx.simulate_keystroke(".");
15405
15406 let item1 = lsp::CompletionItem {
15407 label: "method id()".to_string(),
15408 filter_text: Some("id".to_string()),
15409 detail: None,
15410 documentation: None,
15411 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15412 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15413 new_text: ".id".to_string(),
15414 })),
15415 ..lsp::CompletionItem::default()
15416 };
15417
15418 let item2 = lsp::CompletionItem {
15419 label: "other".to_string(),
15420 filter_text: Some("other".to_string()),
15421 detail: None,
15422 documentation: None,
15423 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15424 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15425 new_text: ".other".to_string(),
15426 })),
15427 ..lsp::CompletionItem::default()
15428 };
15429
15430 let item1 = item1.clone();
15431 cx.set_request_handler::<lsp::request::Completion, _, _>({
15432 let item1 = item1.clone();
15433 move |_, _, _| {
15434 let item1 = item1.clone();
15435 let item2 = item2.clone();
15436 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
15437 }
15438 })
15439 .next()
15440 .await;
15441
15442 cx.condition(|editor, _| editor.context_menu_visible())
15443 .await;
15444 cx.update_editor(|editor, _, _| {
15445 let context_menu = editor.context_menu.borrow_mut();
15446 let context_menu = context_menu
15447 .as_ref()
15448 .expect("Should have the context menu deployed");
15449 match context_menu {
15450 CodeContextMenu::Completions(completions_menu) => {
15451 let completions = completions_menu.completions.borrow_mut();
15452 assert_eq!(
15453 completions
15454 .iter()
15455 .map(|completion| &completion.label.text)
15456 .collect::<Vec<_>>(),
15457 vec!["method id()", "other"]
15458 )
15459 }
15460 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15461 }
15462 });
15463
15464 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
15465 let item1 = item1.clone();
15466 move |_, item_to_resolve, _| {
15467 let item1 = item1.clone();
15468 async move {
15469 if item1 == item_to_resolve {
15470 Ok(lsp::CompletionItem {
15471 label: "method id()".to_string(),
15472 filter_text: Some("id".to_string()),
15473 detail: Some("Now resolved!".to_string()),
15474 documentation: Some(lsp::Documentation::String("Docs".to_string())),
15475 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15476 range: lsp::Range::new(
15477 lsp::Position::new(0, 22),
15478 lsp::Position::new(0, 22),
15479 ),
15480 new_text: ".id".to_string(),
15481 })),
15482 ..lsp::CompletionItem::default()
15483 })
15484 } else {
15485 Ok(item_to_resolve)
15486 }
15487 }
15488 }
15489 })
15490 .next()
15491 .await
15492 .unwrap();
15493 cx.run_until_parked();
15494
15495 cx.update_editor(|editor, window, cx| {
15496 editor.context_menu_next(&Default::default(), window, cx);
15497 });
15498
15499 cx.update_editor(|editor, _, _| {
15500 let context_menu = editor.context_menu.borrow_mut();
15501 let context_menu = context_menu
15502 .as_ref()
15503 .expect("Should have the context menu deployed");
15504 match context_menu {
15505 CodeContextMenu::Completions(completions_menu) => {
15506 let completions = completions_menu.completions.borrow_mut();
15507 assert_eq!(
15508 completions
15509 .iter()
15510 .map(|completion| &completion.label.text)
15511 .collect::<Vec<_>>(),
15512 vec!["method id() Now resolved!", "other"],
15513 "Should update first completion label, but not second as the filter text did not match."
15514 );
15515 }
15516 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15517 }
15518 });
15519}
15520
15521#[gpui::test]
15522async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
15523 init_test(cx, |_| {});
15524 let mut cx = EditorLspTestContext::new_rust(
15525 lsp::ServerCapabilities {
15526 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
15527 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
15528 completion_provider: Some(lsp::CompletionOptions {
15529 resolve_provider: Some(true),
15530 ..Default::default()
15531 }),
15532 ..Default::default()
15533 },
15534 cx,
15535 )
15536 .await;
15537 cx.set_state(indoc! {"
15538 struct TestStruct {
15539 field: i32
15540 }
15541
15542 fn mainˇ() {
15543 let unused_var = 42;
15544 let test_struct = TestStruct { field: 42 };
15545 }
15546 "});
15547 let symbol_range = cx.lsp_range(indoc! {"
15548 struct TestStruct {
15549 field: i32
15550 }
15551
15552 «fn main»() {
15553 let unused_var = 42;
15554 let test_struct = TestStruct { field: 42 };
15555 }
15556 "});
15557 let mut hover_requests =
15558 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
15559 Ok(Some(lsp::Hover {
15560 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
15561 kind: lsp::MarkupKind::Markdown,
15562 value: "Function documentation".to_string(),
15563 }),
15564 range: Some(symbol_range),
15565 }))
15566 });
15567
15568 // Case 1: Test that code action menu hide hover popover
15569 cx.dispatch_action(Hover);
15570 hover_requests.next().await;
15571 cx.condition(|editor, _| editor.hover_state.visible()).await;
15572 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
15573 move |_, _, _| async move {
15574 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
15575 lsp::CodeAction {
15576 title: "Remove unused variable".to_string(),
15577 kind: Some(CodeActionKind::QUICKFIX),
15578 edit: Some(lsp::WorkspaceEdit {
15579 changes: Some(
15580 [(
15581 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
15582 vec![lsp::TextEdit {
15583 range: lsp::Range::new(
15584 lsp::Position::new(5, 4),
15585 lsp::Position::new(5, 27),
15586 ),
15587 new_text: "".to_string(),
15588 }],
15589 )]
15590 .into_iter()
15591 .collect(),
15592 ),
15593 ..Default::default()
15594 }),
15595 ..Default::default()
15596 },
15597 )]))
15598 },
15599 );
15600 cx.update_editor(|editor, window, cx| {
15601 editor.toggle_code_actions(
15602 &ToggleCodeActions {
15603 deployed_from: None,
15604 quick_launch: false,
15605 },
15606 window,
15607 cx,
15608 );
15609 });
15610 code_action_requests.next().await;
15611 cx.run_until_parked();
15612 cx.condition(|editor, _| editor.context_menu_visible())
15613 .await;
15614 cx.update_editor(|editor, _, _| {
15615 assert!(
15616 !editor.hover_state.visible(),
15617 "Hover popover should be hidden when code action menu is shown"
15618 );
15619 // Hide code actions
15620 editor.context_menu.take();
15621 });
15622
15623 // Case 2: Test that code completions hide hover popover
15624 cx.dispatch_action(Hover);
15625 hover_requests.next().await;
15626 cx.condition(|editor, _| editor.hover_state.visible()).await;
15627 let counter = Arc::new(AtomicUsize::new(0));
15628 let mut completion_requests =
15629 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15630 let counter = counter.clone();
15631 async move {
15632 counter.fetch_add(1, atomic::Ordering::Release);
15633 Ok(Some(lsp::CompletionResponse::Array(vec![
15634 lsp::CompletionItem {
15635 label: "main".into(),
15636 kind: Some(lsp::CompletionItemKind::FUNCTION),
15637 detail: Some("() -> ()".to_string()),
15638 ..Default::default()
15639 },
15640 lsp::CompletionItem {
15641 label: "TestStruct".into(),
15642 kind: Some(lsp::CompletionItemKind::STRUCT),
15643 detail: Some("struct TestStruct".to_string()),
15644 ..Default::default()
15645 },
15646 ])))
15647 }
15648 });
15649 cx.update_editor(|editor, window, cx| {
15650 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15651 });
15652 completion_requests.next().await;
15653 cx.condition(|editor, _| editor.context_menu_visible())
15654 .await;
15655 cx.update_editor(|editor, _, _| {
15656 assert!(
15657 !editor.hover_state.visible(),
15658 "Hover popover should be hidden when completion menu is shown"
15659 );
15660 });
15661}
15662
15663#[gpui::test]
15664async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
15665 init_test(cx, |_| {});
15666
15667 let mut cx = EditorLspTestContext::new_rust(
15668 lsp::ServerCapabilities {
15669 completion_provider: Some(lsp::CompletionOptions {
15670 trigger_characters: Some(vec![".".to_string()]),
15671 resolve_provider: Some(true),
15672 ..Default::default()
15673 }),
15674 ..Default::default()
15675 },
15676 cx,
15677 )
15678 .await;
15679
15680 cx.set_state("fn main() { let a = 2ˇ; }");
15681 cx.simulate_keystroke(".");
15682
15683 let unresolved_item_1 = lsp::CompletionItem {
15684 label: "id".to_string(),
15685 filter_text: Some("id".to_string()),
15686 detail: None,
15687 documentation: None,
15688 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15689 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15690 new_text: ".id".to_string(),
15691 })),
15692 ..lsp::CompletionItem::default()
15693 };
15694 let resolved_item_1 = lsp::CompletionItem {
15695 additional_text_edits: Some(vec![lsp::TextEdit {
15696 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15697 new_text: "!!".to_string(),
15698 }]),
15699 ..unresolved_item_1.clone()
15700 };
15701 let unresolved_item_2 = lsp::CompletionItem {
15702 label: "other".to_string(),
15703 filter_text: Some("other".to_string()),
15704 detail: None,
15705 documentation: None,
15706 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15707 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15708 new_text: ".other".to_string(),
15709 })),
15710 ..lsp::CompletionItem::default()
15711 };
15712 let resolved_item_2 = lsp::CompletionItem {
15713 additional_text_edits: Some(vec![lsp::TextEdit {
15714 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15715 new_text: "??".to_string(),
15716 }]),
15717 ..unresolved_item_2.clone()
15718 };
15719
15720 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
15721 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
15722 cx.lsp
15723 .server
15724 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15725 let unresolved_item_1 = unresolved_item_1.clone();
15726 let resolved_item_1 = resolved_item_1.clone();
15727 let unresolved_item_2 = unresolved_item_2.clone();
15728 let resolved_item_2 = resolved_item_2.clone();
15729 let resolve_requests_1 = resolve_requests_1.clone();
15730 let resolve_requests_2 = resolve_requests_2.clone();
15731 move |unresolved_request, _| {
15732 let unresolved_item_1 = unresolved_item_1.clone();
15733 let resolved_item_1 = resolved_item_1.clone();
15734 let unresolved_item_2 = unresolved_item_2.clone();
15735 let resolved_item_2 = resolved_item_2.clone();
15736 let resolve_requests_1 = resolve_requests_1.clone();
15737 let resolve_requests_2 = resolve_requests_2.clone();
15738 async move {
15739 if unresolved_request == unresolved_item_1 {
15740 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
15741 Ok(resolved_item_1.clone())
15742 } else if unresolved_request == unresolved_item_2 {
15743 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
15744 Ok(resolved_item_2.clone())
15745 } else {
15746 panic!("Unexpected completion item {unresolved_request:?}")
15747 }
15748 }
15749 }
15750 })
15751 .detach();
15752
15753 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15754 let unresolved_item_1 = unresolved_item_1.clone();
15755 let unresolved_item_2 = unresolved_item_2.clone();
15756 async move {
15757 Ok(Some(lsp::CompletionResponse::Array(vec![
15758 unresolved_item_1,
15759 unresolved_item_2,
15760 ])))
15761 }
15762 })
15763 .next()
15764 .await;
15765
15766 cx.condition(|editor, _| editor.context_menu_visible())
15767 .await;
15768 cx.update_editor(|editor, _, _| {
15769 let context_menu = editor.context_menu.borrow_mut();
15770 let context_menu = context_menu
15771 .as_ref()
15772 .expect("Should have the context menu deployed");
15773 match context_menu {
15774 CodeContextMenu::Completions(completions_menu) => {
15775 let completions = completions_menu.completions.borrow_mut();
15776 assert_eq!(
15777 completions
15778 .iter()
15779 .map(|completion| &completion.label.text)
15780 .collect::<Vec<_>>(),
15781 vec!["id", "other"]
15782 )
15783 }
15784 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15785 }
15786 });
15787 cx.run_until_parked();
15788
15789 cx.update_editor(|editor, window, cx| {
15790 editor.context_menu_next(&ContextMenuNext, window, cx);
15791 });
15792 cx.run_until_parked();
15793 cx.update_editor(|editor, window, cx| {
15794 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15795 });
15796 cx.run_until_parked();
15797 cx.update_editor(|editor, window, cx| {
15798 editor.context_menu_next(&ContextMenuNext, window, cx);
15799 });
15800 cx.run_until_parked();
15801 cx.update_editor(|editor, window, cx| {
15802 editor
15803 .compose_completion(&ComposeCompletion::default(), window, cx)
15804 .expect("No task returned")
15805 })
15806 .await
15807 .expect("Completion failed");
15808 cx.run_until_parked();
15809
15810 cx.update_editor(|editor, _, cx| {
15811 assert_eq!(
15812 resolve_requests_1.load(atomic::Ordering::Acquire),
15813 1,
15814 "Should always resolve once despite multiple selections"
15815 );
15816 assert_eq!(
15817 resolve_requests_2.load(atomic::Ordering::Acquire),
15818 1,
15819 "Should always resolve once after multiple selections and applying the completion"
15820 );
15821 assert_eq!(
15822 editor.text(cx),
15823 "fn main() { let a = ??.other; }",
15824 "Should use resolved data when applying the completion"
15825 );
15826 });
15827}
15828
15829#[gpui::test]
15830async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
15831 init_test(cx, |_| {});
15832
15833 let item_0 = lsp::CompletionItem {
15834 label: "abs".into(),
15835 insert_text: Some("abs".into()),
15836 data: Some(json!({ "very": "special"})),
15837 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
15838 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
15839 lsp::InsertReplaceEdit {
15840 new_text: "abs".to_string(),
15841 insert: lsp::Range::default(),
15842 replace: lsp::Range::default(),
15843 },
15844 )),
15845 ..lsp::CompletionItem::default()
15846 };
15847 let items = iter::once(item_0.clone())
15848 .chain((11..51).map(|i| lsp::CompletionItem {
15849 label: format!("item_{}", i),
15850 insert_text: Some(format!("item_{}", i)),
15851 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15852 ..lsp::CompletionItem::default()
15853 }))
15854 .collect::<Vec<_>>();
15855
15856 let default_commit_characters = vec!["?".to_string()];
15857 let default_data = json!({ "default": "data"});
15858 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
15859 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
15860 let default_edit_range = lsp::Range {
15861 start: lsp::Position {
15862 line: 0,
15863 character: 5,
15864 },
15865 end: lsp::Position {
15866 line: 0,
15867 character: 5,
15868 },
15869 };
15870
15871 let mut cx = EditorLspTestContext::new_rust(
15872 lsp::ServerCapabilities {
15873 completion_provider: Some(lsp::CompletionOptions {
15874 trigger_characters: Some(vec![".".to_string()]),
15875 resolve_provider: Some(true),
15876 ..Default::default()
15877 }),
15878 ..Default::default()
15879 },
15880 cx,
15881 )
15882 .await;
15883
15884 cx.set_state("fn main() { let a = 2ˇ; }");
15885 cx.simulate_keystroke(".");
15886
15887 let completion_data = default_data.clone();
15888 let completion_characters = default_commit_characters.clone();
15889 let completion_items = items.clone();
15890 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15891 let default_data = completion_data.clone();
15892 let default_commit_characters = completion_characters.clone();
15893 let items = completion_items.clone();
15894 async move {
15895 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15896 items,
15897 item_defaults: Some(lsp::CompletionListItemDefaults {
15898 data: Some(default_data.clone()),
15899 commit_characters: Some(default_commit_characters.clone()),
15900 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
15901 default_edit_range,
15902 )),
15903 insert_text_format: Some(default_insert_text_format),
15904 insert_text_mode: Some(default_insert_text_mode),
15905 }),
15906 ..lsp::CompletionList::default()
15907 })))
15908 }
15909 })
15910 .next()
15911 .await;
15912
15913 let resolved_items = Arc::new(Mutex::new(Vec::new()));
15914 cx.lsp
15915 .server
15916 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15917 let closure_resolved_items = resolved_items.clone();
15918 move |item_to_resolve, _| {
15919 let closure_resolved_items = closure_resolved_items.clone();
15920 async move {
15921 closure_resolved_items.lock().push(item_to_resolve.clone());
15922 Ok(item_to_resolve)
15923 }
15924 }
15925 })
15926 .detach();
15927
15928 cx.condition(|editor, _| editor.context_menu_visible())
15929 .await;
15930 cx.run_until_parked();
15931 cx.update_editor(|editor, _, _| {
15932 let menu = editor.context_menu.borrow_mut();
15933 match menu.as_ref().expect("should have the completions menu") {
15934 CodeContextMenu::Completions(completions_menu) => {
15935 assert_eq!(
15936 completions_menu
15937 .entries
15938 .borrow()
15939 .iter()
15940 .map(|mat| mat.string.clone())
15941 .collect::<Vec<String>>(),
15942 items
15943 .iter()
15944 .map(|completion| completion.label.clone())
15945 .collect::<Vec<String>>()
15946 );
15947 }
15948 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
15949 }
15950 });
15951 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
15952 // with 4 from the end.
15953 assert_eq!(
15954 *resolved_items.lock(),
15955 [&items[0..16], &items[items.len() - 4..items.len()]]
15956 .concat()
15957 .iter()
15958 .cloned()
15959 .map(|mut item| {
15960 if item.data.is_none() {
15961 item.data = Some(default_data.clone());
15962 }
15963 item
15964 })
15965 .collect::<Vec<lsp::CompletionItem>>(),
15966 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
15967 );
15968 resolved_items.lock().clear();
15969
15970 cx.update_editor(|editor, window, cx| {
15971 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15972 });
15973 cx.run_until_parked();
15974 // Completions that have already been resolved are skipped.
15975 assert_eq!(
15976 *resolved_items.lock(),
15977 items[items.len() - 17..items.len() - 4]
15978 .iter()
15979 .cloned()
15980 .map(|mut item| {
15981 if item.data.is_none() {
15982 item.data = Some(default_data.clone());
15983 }
15984 item
15985 })
15986 .collect::<Vec<lsp::CompletionItem>>()
15987 );
15988 resolved_items.lock().clear();
15989}
15990
15991#[gpui::test]
15992async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
15993 init_test(cx, |_| {});
15994
15995 let mut cx = EditorLspTestContext::new(
15996 Language::new(
15997 LanguageConfig {
15998 matcher: LanguageMatcher {
15999 path_suffixes: vec!["jsx".into()],
16000 ..Default::default()
16001 },
16002 overrides: [(
16003 "element".into(),
16004 LanguageConfigOverride {
16005 completion_query_characters: Override::Set(['-'].into_iter().collect()),
16006 ..Default::default()
16007 },
16008 )]
16009 .into_iter()
16010 .collect(),
16011 ..Default::default()
16012 },
16013 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16014 )
16015 .with_override_query("(jsx_self_closing_element) @element")
16016 .unwrap(),
16017 lsp::ServerCapabilities {
16018 completion_provider: Some(lsp::CompletionOptions {
16019 trigger_characters: Some(vec![":".to_string()]),
16020 ..Default::default()
16021 }),
16022 ..Default::default()
16023 },
16024 cx,
16025 )
16026 .await;
16027
16028 cx.lsp
16029 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16030 Ok(Some(lsp::CompletionResponse::Array(vec![
16031 lsp::CompletionItem {
16032 label: "bg-blue".into(),
16033 ..Default::default()
16034 },
16035 lsp::CompletionItem {
16036 label: "bg-red".into(),
16037 ..Default::default()
16038 },
16039 lsp::CompletionItem {
16040 label: "bg-yellow".into(),
16041 ..Default::default()
16042 },
16043 ])))
16044 });
16045
16046 cx.set_state(r#"<p class="bgˇ" />"#);
16047
16048 // Trigger completion when typing a dash, because the dash is an extra
16049 // word character in the 'element' scope, which contains the cursor.
16050 cx.simulate_keystroke("-");
16051 cx.executor().run_until_parked();
16052 cx.update_editor(|editor, _, _| {
16053 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16054 {
16055 assert_eq!(
16056 completion_menu_entries(&menu),
16057 &["bg-blue", "bg-red", "bg-yellow"]
16058 );
16059 } else {
16060 panic!("expected completion menu to be open");
16061 }
16062 });
16063
16064 cx.simulate_keystroke("l");
16065 cx.executor().run_until_parked();
16066 cx.update_editor(|editor, _, _| {
16067 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16068 {
16069 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
16070 } else {
16071 panic!("expected completion menu to be open");
16072 }
16073 });
16074
16075 // When filtering completions, consider the character after the '-' to
16076 // be the start of a subword.
16077 cx.set_state(r#"<p class="yelˇ" />"#);
16078 cx.simulate_keystroke("l");
16079 cx.executor().run_until_parked();
16080 cx.update_editor(|editor, _, _| {
16081 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16082 {
16083 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
16084 } else {
16085 panic!("expected completion menu to be open");
16086 }
16087 });
16088}
16089
16090fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
16091 let entries = menu.entries.borrow();
16092 entries.iter().map(|mat| mat.string.clone()).collect()
16093}
16094
16095#[gpui::test]
16096async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
16097 init_test(cx, |settings| {
16098 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
16099 Formatter::Prettier,
16100 )))
16101 });
16102
16103 let fs = FakeFs::new(cx.executor());
16104 fs.insert_file(path!("/file.ts"), Default::default()).await;
16105
16106 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
16107 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16108
16109 language_registry.add(Arc::new(Language::new(
16110 LanguageConfig {
16111 name: "TypeScript".into(),
16112 matcher: LanguageMatcher {
16113 path_suffixes: vec!["ts".to_string()],
16114 ..Default::default()
16115 },
16116 ..Default::default()
16117 },
16118 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
16119 )));
16120 update_test_language_settings(cx, |settings| {
16121 settings.defaults.prettier = Some(PrettierSettings {
16122 allowed: true,
16123 ..PrettierSettings::default()
16124 });
16125 });
16126
16127 let test_plugin = "test_plugin";
16128 let _ = language_registry.register_fake_lsp(
16129 "TypeScript",
16130 FakeLspAdapter {
16131 prettier_plugins: vec![test_plugin],
16132 ..Default::default()
16133 },
16134 );
16135
16136 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
16137 let buffer = project
16138 .update(cx, |project, cx| {
16139 project.open_local_buffer(path!("/file.ts"), cx)
16140 })
16141 .await
16142 .unwrap();
16143
16144 let buffer_text = "one\ntwo\nthree\n";
16145 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16146 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16147 editor.update_in(cx, |editor, window, cx| {
16148 editor.set_text(buffer_text, window, cx)
16149 });
16150
16151 editor
16152 .update_in(cx, |editor, window, cx| {
16153 editor.perform_format(
16154 project.clone(),
16155 FormatTrigger::Manual,
16156 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16157 window,
16158 cx,
16159 )
16160 })
16161 .unwrap()
16162 .await;
16163 assert_eq!(
16164 editor.update(cx, |editor, cx| editor.text(cx)),
16165 buffer_text.to_string() + prettier_format_suffix,
16166 "Test prettier formatting was not applied to the original buffer text",
16167 );
16168
16169 update_test_language_settings(cx, |settings| {
16170 settings.defaults.formatter = Some(SelectedFormatter::Auto)
16171 });
16172 let format = editor.update_in(cx, |editor, window, cx| {
16173 editor.perform_format(
16174 project.clone(),
16175 FormatTrigger::Manual,
16176 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16177 window,
16178 cx,
16179 )
16180 });
16181 format.await.unwrap();
16182 assert_eq!(
16183 editor.update(cx, |editor, cx| editor.text(cx)),
16184 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
16185 "Autoformatting (via test prettier) was not applied to the original buffer text",
16186 );
16187}
16188
16189#[gpui::test]
16190async fn test_addition_reverts(cx: &mut TestAppContext) {
16191 init_test(cx, |_| {});
16192 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16193 let base_text = indoc! {r#"
16194 struct Row;
16195 struct Row1;
16196 struct Row2;
16197
16198 struct Row4;
16199 struct Row5;
16200 struct Row6;
16201
16202 struct Row8;
16203 struct Row9;
16204 struct Row10;"#};
16205
16206 // When addition hunks are not adjacent to carets, no hunk revert is performed
16207 assert_hunk_revert(
16208 indoc! {r#"struct Row;
16209 struct Row1;
16210 struct Row1.1;
16211 struct Row1.2;
16212 struct Row2;ˇ
16213
16214 struct Row4;
16215 struct Row5;
16216 struct Row6;
16217
16218 struct Row8;
16219 ˇstruct Row9;
16220 struct Row9.1;
16221 struct Row9.2;
16222 struct Row9.3;
16223 struct Row10;"#},
16224 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16225 indoc! {r#"struct Row;
16226 struct Row1;
16227 struct Row1.1;
16228 struct Row1.2;
16229 struct Row2;ˇ
16230
16231 struct Row4;
16232 struct Row5;
16233 struct Row6;
16234
16235 struct Row8;
16236 ˇstruct Row9;
16237 struct Row9.1;
16238 struct Row9.2;
16239 struct Row9.3;
16240 struct Row10;"#},
16241 base_text,
16242 &mut cx,
16243 );
16244 // Same for selections
16245 assert_hunk_revert(
16246 indoc! {r#"struct Row;
16247 struct Row1;
16248 struct Row2;
16249 struct Row2.1;
16250 struct Row2.2;
16251 «ˇ
16252 struct Row4;
16253 struct» Row5;
16254 «struct Row6;
16255 ˇ»
16256 struct Row9.1;
16257 struct Row9.2;
16258 struct Row9.3;
16259 struct Row8;
16260 struct Row9;
16261 struct Row10;"#},
16262 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16263 indoc! {r#"struct Row;
16264 struct Row1;
16265 struct Row2;
16266 struct Row2.1;
16267 struct Row2.2;
16268 «ˇ
16269 struct Row4;
16270 struct» Row5;
16271 «struct Row6;
16272 ˇ»
16273 struct Row9.1;
16274 struct Row9.2;
16275 struct Row9.3;
16276 struct Row8;
16277 struct Row9;
16278 struct Row10;"#},
16279 base_text,
16280 &mut cx,
16281 );
16282
16283 // When carets and selections intersect the addition hunks, those are reverted.
16284 // Adjacent carets got merged.
16285 assert_hunk_revert(
16286 indoc! {r#"struct Row;
16287 ˇ// something on the top
16288 struct Row1;
16289 struct Row2;
16290 struct Roˇw3.1;
16291 struct Row2.2;
16292 struct Row2.3;ˇ
16293
16294 struct Row4;
16295 struct ˇRow5.1;
16296 struct Row5.2;
16297 struct «Rowˇ»5.3;
16298 struct Row5;
16299 struct Row6;
16300 ˇ
16301 struct Row9.1;
16302 struct «Rowˇ»9.2;
16303 struct «ˇRow»9.3;
16304 struct Row8;
16305 struct Row9;
16306 «ˇ// something on bottom»
16307 struct Row10;"#},
16308 vec![
16309 DiffHunkStatusKind::Added,
16310 DiffHunkStatusKind::Added,
16311 DiffHunkStatusKind::Added,
16312 DiffHunkStatusKind::Added,
16313 DiffHunkStatusKind::Added,
16314 ],
16315 indoc! {r#"struct Row;
16316 ˇstruct Row1;
16317 struct Row2;
16318 ˇ
16319 struct Row4;
16320 ˇstruct Row5;
16321 struct Row6;
16322 ˇ
16323 ˇstruct Row8;
16324 struct Row9;
16325 ˇstruct Row10;"#},
16326 base_text,
16327 &mut cx,
16328 );
16329}
16330
16331#[gpui::test]
16332async fn test_modification_reverts(cx: &mut TestAppContext) {
16333 init_test(cx, |_| {});
16334 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16335 let base_text = indoc! {r#"
16336 struct Row;
16337 struct Row1;
16338 struct Row2;
16339
16340 struct Row4;
16341 struct Row5;
16342 struct Row6;
16343
16344 struct Row8;
16345 struct Row9;
16346 struct Row10;"#};
16347
16348 // Modification hunks behave the same as the addition ones.
16349 assert_hunk_revert(
16350 indoc! {r#"struct Row;
16351 struct Row1;
16352 struct Row33;
16353 ˇ
16354 struct Row4;
16355 struct Row5;
16356 struct Row6;
16357 ˇ
16358 struct Row99;
16359 struct Row9;
16360 struct Row10;"#},
16361 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16362 indoc! {r#"struct Row;
16363 struct Row1;
16364 struct Row33;
16365 ˇ
16366 struct Row4;
16367 struct Row5;
16368 struct Row6;
16369 ˇ
16370 struct Row99;
16371 struct Row9;
16372 struct Row10;"#},
16373 base_text,
16374 &mut cx,
16375 );
16376 assert_hunk_revert(
16377 indoc! {r#"struct Row;
16378 struct Row1;
16379 struct Row33;
16380 «ˇ
16381 struct Row4;
16382 struct» Row5;
16383 «struct Row6;
16384 ˇ»
16385 struct Row99;
16386 struct Row9;
16387 struct Row10;"#},
16388 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16389 indoc! {r#"struct Row;
16390 struct Row1;
16391 struct Row33;
16392 «ˇ
16393 struct Row4;
16394 struct» Row5;
16395 «struct Row6;
16396 ˇ»
16397 struct Row99;
16398 struct Row9;
16399 struct Row10;"#},
16400 base_text,
16401 &mut cx,
16402 );
16403
16404 assert_hunk_revert(
16405 indoc! {r#"ˇstruct Row1.1;
16406 struct Row1;
16407 «ˇstr»uct Row22;
16408
16409 struct ˇRow44;
16410 struct Row5;
16411 struct «Rˇ»ow66;ˇ
16412
16413 «struˇ»ct Row88;
16414 struct Row9;
16415 struct Row1011;ˇ"#},
16416 vec![
16417 DiffHunkStatusKind::Modified,
16418 DiffHunkStatusKind::Modified,
16419 DiffHunkStatusKind::Modified,
16420 DiffHunkStatusKind::Modified,
16421 DiffHunkStatusKind::Modified,
16422 DiffHunkStatusKind::Modified,
16423 ],
16424 indoc! {r#"struct Row;
16425 ˇstruct Row1;
16426 struct Row2;
16427 ˇ
16428 struct Row4;
16429 ˇstruct Row5;
16430 struct Row6;
16431 ˇ
16432 struct Row8;
16433 ˇstruct Row9;
16434 struct Row10;ˇ"#},
16435 base_text,
16436 &mut cx,
16437 );
16438}
16439
16440#[gpui::test]
16441async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
16442 init_test(cx, |_| {});
16443 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16444 let base_text = indoc! {r#"
16445 one
16446
16447 two
16448 three
16449 "#};
16450
16451 cx.set_head_text(base_text);
16452 cx.set_state("\nˇ\n");
16453 cx.executor().run_until_parked();
16454 cx.update_editor(|editor, _window, cx| {
16455 editor.expand_selected_diff_hunks(cx);
16456 });
16457 cx.executor().run_until_parked();
16458 cx.update_editor(|editor, window, cx| {
16459 editor.backspace(&Default::default(), window, cx);
16460 });
16461 cx.run_until_parked();
16462 cx.assert_state_with_diff(
16463 indoc! {r#"
16464
16465 - two
16466 - threeˇ
16467 +
16468 "#}
16469 .to_string(),
16470 );
16471}
16472
16473#[gpui::test]
16474async fn test_deletion_reverts(cx: &mut TestAppContext) {
16475 init_test(cx, |_| {});
16476 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16477 let base_text = indoc! {r#"struct Row;
16478struct Row1;
16479struct Row2;
16480
16481struct Row4;
16482struct Row5;
16483struct Row6;
16484
16485struct Row8;
16486struct Row9;
16487struct Row10;"#};
16488
16489 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
16490 assert_hunk_revert(
16491 indoc! {r#"struct Row;
16492 struct Row2;
16493
16494 ˇstruct Row4;
16495 struct Row5;
16496 struct Row6;
16497 ˇ
16498 struct Row8;
16499 struct Row10;"#},
16500 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16501 indoc! {r#"struct Row;
16502 struct Row2;
16503
16504 ˇstruct Row4;
16505 struct Row5;
16506 struct Row6;
16507 ˇ
16508 struct Row8;
16509 struct Row10;"#},
16510 base_text,
16511 &mut cx,
16512 );
16513 assert_hunk_revert(
16514 indoc! {r#"struct Row;
16515 struct Row2;
16516
16517 «ˇstruct Row4;
16518 struct» Row5;
16519 «struct Row6;
16520 ˇ»
16521 struct Row8;
16522 struct Row10;"#},
16523 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16524 indoc! {r#"struct Row;
16525 struct Row2;
16526
16527 «ˇstruct Row4;
16528 struct» Row5;
16529 «struct Row6;
16530 ˇ»
16531 struct Row8;
16532 struct Row10;"#},
16533 base_text,
16534 &mut cx,
16535 );
16536
16537 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
16538 assert_hunk_revert(
16539 indoc! {r#"struct Row;
16540 ˇstruct Row2;
16541
16542 struct Row4;
16543 struct Row5;
16544 struct Row6;
16545
16546 struct Row8;ˇ
16547 struct Row10;"#},
16548 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16549 indoc! {r#"struct Row;
16550 struct Row1;
16551 ˇstruct Row2;
16552
16553 struct Row4;
16554 struct Row5;
16555 struct Row6;
16556
16557 struct Row8;ˇ
16558 struct Row9;
16559 struct Row10;"#},
16560 base_text,
16561 &mut cx,
16562 );
16563 assert_hunk_revert(
16564 indoc! {r#"struct Row;
16565 struct Row2«ˇ;
16566 struct Row4;
16567 struct» Row5;
16568 «struct Row6;
16569
16570 struct Row8;ˇ»
16571 struct Row10;"#},
16572 vec![
16573 DiffHunkStatusKind::Deleted,
16574 DiffHunkStatusKind::Deleted,
16575 DiffHunkStatusKind::Deleted,
16576 ],
16577 indoc! {r#"struct Row;
16578 struct Row1;
16579 struct Row2«ˇ;
16580
16581 struct Row4;
16582 struct» Row5;
16583 «struct Row6;
16584
16585 struct Row8;ˇ»
16586 struct Row9;
16587 struct Row10;"#},
16588 base_text,
16589 &mut cx,
16590 );
16591}
16592
16593#[gpui::test]
16594async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
16595 init_test(cx, |_| {});
16596
16597 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
16598 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
16599 let base_text_3 =
16600 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
16601
16602 let text_1 = edit_first_char_of_every_line(base_text_1);
16603 let text_2 = edit_first_char_of_every_line(base_text_2);
16604 let text_3 = edit_first_char_of_every_line(base_text_3);
16605
16606 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
16607 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
16608 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
16609
16610 let multibuffer = cx.new(|cx| {
16611 let mut multibuffer = MultiBuffer::new(ReadWrite);
16612 multibuffer.push_excerpts(
16613 buffer_1.clone(),
16614 [
16615 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16616 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16617 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16618 ],
16619 cx,
16620 );
16621 multibuffer.push_excerpts(
16622 buffer_2.clone(),
16623 [
16624 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16625 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16626 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16627 ],
16628 cx,
16629 );
16630 multibuffer.push_excerpts(
16631 buffer_3.clone(),
16632 [
16633 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16634 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16635 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16636 ],
16637 cx,
16638 );
16639 multibuffer
16640 });
16641
16642 let fs = FakeFs::new(cx.executor());
16643 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
16644 let (editor, cx) = cx
16645 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
16646 editor.update_in(cx, |editor, _window, cx| {
16647 for (buffer, diff_base) in [
16648 (buffer_1.clone(), base_text_1),
16649 (buffer_2.clone(), base_text_2),
16650 (buffer_3.clone(), base_text_3),
16651 ] {
16652 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16653 editor
16654 .buffer
16655 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16656 }
16657 });
16658 cx.executor().run_until_parked();
16659
16660 editor.update_in(cx, |editor, window, cx| {
16661 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}");
16662 editor.select_all(&SelectAll, window, cx);
16663 editor.git_restore(&Default::default(), window, cx);
16664 });
16665 cx.executor().run_until_parked();
16666
16667 // When all ranges are selected, all buffer hunks are reverted.
16668 editor.update(cx, |editor, cx| {
16669 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");
16670 });
16671 buffer_1.update(cx, |buffer, _| {
16672 assert_eq!(buffer.text(), base_text_1);
16673 });
16674 buffer_2.update(cx, |buffer, _| {
16675 assert_eq!(buffer.text(), base_text_2);
16676 });
16677 buffer_3.update(cx, |buffer, _| {
16678 assert_eq!(buffer.text(), base_text_3);
16679 });
16680
16681 editor.update_in(cx, |editor, window, cx| {
16682 editor.undo(&Default::default(), window, cx);
16683 });
16684
16685 editor.update_in(cx, |editor, window, cx| {
16686 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16687 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
16688 });
16689 editor.git_restore(&Default::default(), window, cx);
16690 });
16691
16692 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
16693 // but not affect buffer_2 and its related excerpts.
16694 editor.update(cx, |editor, cx| {
16695 assert_eq!(
16696 editor.text(cx),
16697 "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}"
16698 );
16699 });
16700 buffer_1.update(cx, |buffer, _| {
16701 assert_eq!(buffer.text(), base_text_1);
16702 });
16703 buffer_2.update(cx, |buffer, _| {
16704 assert_eq!(
16705 buffer.text(),
16706 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
16707 );
16708 });
16709 buffer_3.update(cx, |buffer, _| {
16710 assert_eq!(
16711 buffer.text(),
16712 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
16713 );
16714 });
16715
16716 fn edit_first_char_of_every_line(text: &str) -> String {
16717 text.split('\n')
16718 .map(|line| format!("X{}", &line[1..]))
16719 .collect::<Vec<_>>()
16720 .join("\n")
16721 }
16722}
16723
16724#[gpui::test]
16725async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
16726 init_test(cx, |_| {});
16727
16728 let cols = 4;
16729 let rows = 10;
16730 let sample_text_1 = sample_text(rows, cols, 'a');
16731 assert_eq!(
16732 sample_text_1,
16733 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
16734 );
16735 let sample_text_2 = sample_text(rows, cols, 'l');
16736 assert_eq!(
16737 sample_text_2,
16738 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
16739 );
16740 let sample_text_3 = sample_text(rows, cols, 'v');
16741 assert_eq!(
16742 sample_text_3,
16743 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
16744 );
16745
16746 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
16747 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
16748 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
16749
16750 let multi_buffer = cx.new(|cx| {
16751 let mut multibuffer = MultiBuffer::new(ReadWrite);
16752 multibuffer.push_excerpts(
16753 buffer_1.clone(),
16754 [
16755 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16756 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16757 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16758 ],
16759 cx,
16760 );
16761 multibuffer.push_excerpts(
16762 buffer_2.clone(),
16763 [
16764 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16765 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16766 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16767 ],
16768 cx,
16769 );
16770 multibuffer.push_excerpts(
16771 buffer_3.clone(),
16772 [
16773 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16774 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16775 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16776 ],
16777 cx,
16778 );
16779 multibuffer
16780 });
16781
16782 let fs = FakeFs::new(cx.executor());
16783 fs.insert_tree(
16784 "/a",
16785 json!({
16786 "main.rs": sample_text_1,
16787 "other.rs": sample_text_2,
16788 "lib.rs": sample_text_3,
16789 }),
16790 )
16791 .await;
16792 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16793 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16794 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16795 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16796 Editor::new(
16797 EditorMode::full(),
16798 multi_buffer,
16799 Some(project.clone()),
16800 window,
16801 cx,
16802 )
16803 });
16804 let multibuffer_item_id = workspace
16805 .update(cx, |workspace, window, cx| {
16806 assert!(
16807 workspace.active_item(cx).is_none(),
16808 "active item should be None before the first item is added"
16809 );
16810 workspace.add_item_to_active_pane(
16811 Box::new(multi_buffer_editor.clone()),
16812 None,
16813 true,
16814 window,
16815 cx,
16816 );
16817 let active_item = workspace
16818 .active_item(cx)
16819 .expect("should have an active item after adding the multi buffer");
16820 assert!(
16821 !active_item.is_singleton(cx),
16822 "A multi buffer was expected to active after adding"
16823 );
16824 active_item.item_id()
16825 })
16826 .unwrap();
16827 cx.executor().run_until_parked();
16828
16829 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16830 editor.change_selections(
16831 SelectionEffects::scroll(Autoscroll::Next),
16832 window,
16833 cx,
16834 |s| s.select_ranges(Some(1..2)),
16835 );
16836 editor.open_excerpts(&OpenExcerpts, window, cx);
16837 });
16838 cx.executor().run_until_parked();
16839 let first_item_id = workspace
16840 .update(cx, |workspace, window, cx| {
16841 let active_item = workspace
16842 .active_item(cx)
16843 .expect("should have an active item after navigating into the 1st buffer");
16844 let first_item_id = active_item.item_id();
16845 assert_ne!(
16846 first_item_id, multibuffer_item_id,
16847 "Should navigate into the 1st buffer and activate it"
16848 );
16849 assert!(
16850 active_item.is_singleton(cx),
16851 "New active item should be a singleton buffer"
16852 );
16853 assert_eq!(
16854 active_item
16855 .act_as::<Editor>(cx)
16856 .expect("should have navigated into an editor for the 1st buffer")
16857 .read(cx)
16858 .text(cx),
16859 sample_text_1
16860 );
16861
16862 workspace
16863 .go_back(workspace.active_pane().downgrade(), window, cx)
16864 .detach_and_log_err(cx);
16865
16866 first_item_id
16867 })
16868 .unwrap();
16869 cx.executor().run_until_parked();
16870 workspace
16871 .update(cx, |workspace, _, cx| {
16872 let active_item = workspace
16873 .active_item(cx)
16874 .expect("should have an active item after navigating back");
16875 assert_eq!(
16876 active_item.item_id(),
16877 multibuffer_item_id,
16878 "Should navigate back to the multi buffer"
16879 );
16880 assert!(!active_item.is_singleton(cx));
16881 })
16882 .unwrap();
16883
16884 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16885 editor.change_selections(
16886 SelectionEffects::scroll(Autoscroll::Next),
16887 window,
16888 cx,
16889 |s| s.select_ranges(Some(39..40)),
16890 );
16891 editor.open_excerpts(&OpenExcerpts, window, cx);
16892 });
16893 cx.executor().run_until_parked();
16894 let second_item_id = workspace
16895 .update(cx, |workspace, window, cx| {
16896 let active_item = workspace
16897 .active_item(cx)
16898 .expect("should have an active item after navigating into the 2nd buffer");
16899 let second_item_id = active_item.item_id();
16900 assert_ne!(
16901 second_item_id, multibuffer_item_id,
16902 "Should navigate away from the multibuffer"
16903 );
16904 assert_ne!(
16905 second_item_id, first_item_id,
16906 "Should navigate into the 2nd buffer and activate it"
16907 );
16908 assert!(
16909 active_item.is_singleton(cx),
16910 "New active item should be a singleton buffer"
16911 );
16912 assert_eq!(
16913 active_item
16914 .act_as::<Editor>(cx)
16915 .expect("should have navigated into an editor")
16916 .read(cx)
16917 .text(cx),
16918 sample_text_2
16919 );
16920
16921 workspace
16922 .go_back(workspace.active_pane().downgrade(), window, cx)
16923 .detach_and_log_err(cx);
16924
16925 second_item_id
16926 })
16927 .unwrap();
16928 cx.executor().run_until_parked();
16929 workspace
16930 .update(cx, |workspace, _, cx| {
16931 let active_item = workspace
16932 .active_item(cx)
16933 .expect("should have an active item after navigating back from the 2nd buffer");
16934 assert_eq!(
16935 active_item.item_id(),
16936 multibuffer_item_id,
16937 "Should navigate back from the 2nd buffer to the multi buffer"
16938 );
16939 assert!(!active_item.is_singleton(cx));
16940 })
16941 .unwrap();
16942
16943 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16944 editor.change_selections(
16945 SelectionEffects::scroll(Autoscroll::Next),
16946 window,
16947 cx,
16948 |s| s.select_ranges(Some(70..70)),
16949 );
16950 editor.open_excerpts(&OpenExcerpts, window, cx);
16951 });
16952 cx.executor().run_until_parked();
16953 workspace
16954 .update(cx, |workspace, window, cx| {
16955 let active_item = workspace
16956 .active_item(cx)
16957 .expect("should have an active item after navigating into the 3rd buffer");
16958 let third_item_id = active_item.item_id();
16959 assert_ne!(
16960 third_item_id, multibuffer_item_id,
16961 "Should navigate into the 3rd buffer and activate it"
16962 );
16963 assert_ne!(third_item_id, first_item_id);
16964 assert_ne!(third_item_id, second_item_id);
16965 assert!(
16966 active_item.is_singleton(cx),
16967 "New active item should be a singleton buffer"
16968 );
16969 assert_eq!(
16970 active_item
16971 .act_as::<Editor>(cx)
16972 .expect("should have navigated into an editor")
16973 .read(cx)
16974 .text(cx),
16975 sample_text_3
16976 );
16977
16978 workspace
16979 .go_back(workspace.active_pane().downgrade(), window, cx)
16980 .detach_and_log_err(cx);
16981 })
16982 .unwrap();
16983 cx.executor().run_until_parked();
16984 workspace
16985 .update(cx, |workspace, _, cx| {
16986 let active_item = workspace
16987 .active_item(cx)
16988 .expect("should have an active item after navigating back from the 3rd buffer");
16989 assert_eq!(
16990 active_item.item_id(),
16991 multibuffer_item_id,
16992 "Should navigate back from the 3rd buffer to the multi buffer"
16993 );
16994 assert!(!active_item.is_singleton(cx));
16995 })
16996 .unwrap();
16997}
16998
16999#[gpui::test]
17000async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17001 init_test(cx, |_| {});
17002
17003 let mut cx = EditorTestContext::new(cx).await;
17004
17005 let diff_base = r#"
17006 use some::mod;
17007
17008 const A: u32 = 42;
17009
17010 fn main() {
17011 println!("hello");
17012
17013 println!("world");
17014 }
17015 "#
17016 .unindent();
17017
17018 cx.set_state(
17019 &r#"
17020 use some::modified;
17021
17022 ˇ
17023 fn main() {
17024 println!("hello there");
17025
17026 println!("around the");
17027 println!("world");
17028 }
17029 "#
17030 .unindent(),
17031 );
17032
17033 cx.set_head_text(&diff_base);
17034 executor.run_until_parked();
17035
17036 cx.update_editor(|editor, window, cx| {
17037 editor.go_to_next_hunk(&GoToHunk, window, cx);
17038 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17039 });
17040 executor.run_until_parked();
17041 cx.assert_state_with_diff(
17042 r#"
17043 use some::modified;
17044
17045
17046 fn main() {
17047 - println!("hello");
17048 + ˇ println!("hello there");
17049
17050 println!("around the");
17051 println!("world");
17052 }
17053 "#
17054 .unindent(),
17055 );
17056
17057 cx.update_editor(|editor, window, cx| {
17058 for _ in 0..2 {
17059 editor.go_to_next_hunk(&GoToHunk, window, cx);
17060 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17061 }
17062 });
17063 executor.run_until_parked();
17064 cx.assert_state_with_diff(
17065 r#"
17066 - use some::mod;
17067 + ˇuse some::modified;
17068
17069
17070 fn main() {
17071 - println!("hello");
17072 + println!("hello there");
17073
17074 + println!("around the");
17075 println!("world");
17076 }
17077 "#
17078 .unindent(),
17079 );
17080
17081 cx.update_editor(|editor, window, cx| {
17082 editor.go_to_next_hunk(&GoToHunk, window, cx);
17083 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17084 });
17085 executor.run_until_parked();
17086 cx.assert_state_with_diff(
17087 r#"
17088 - use some::mod;
17089 + use some::modified;
17090
17091 - const A: u32 = 42;
17092 ˇ
17093 fn main() {
17094 - println!("hello");
17095 + println!("hello there");
17096
17097 + println!("around the");
17098 println!("world");
17099 }
17100 "#
17101 .unindent(),
17102 );
17103
17104 cx.update_editor(|editor, window, cx| {
17105 editor.cancel(&Cancel, window, cx);
17106 });
17107
17108 cx.assert_state_with_diff(
17109 r#"
17110 use some::modified;
17111
17112 ˇ
17113 fn main() {
17114 println!("hello there");
17115
17116 println!("around the");
17117 println!("world");
17118 }
17119 "#
17120 .unindent(),
17121 );
17122}
17123
17124#[gpui::test]
17125async fn test_diff_base_change_with_expanded_diff_hunks(
17126 executor: BackgroundExecutor,
17127 cx: &mut TestAppContext,
17128) {
17129 init_test(cx, |_| {});
17130
17131 let mut cx = EditorTestContext::new(cx).await;
17132
17133 let diff_base = r#"
17134 use some::mod1;
17135 use some::mod2;
17136
17137 const A: u32 = 42;
17138 const B: u32 = 42;
17139 const C: u32 = 42;
17140
17141 fn main() {
17142 println!("hello");
17143
17144 println!("world");
17145 }
17146 "#
17147 .unindent();
17148
17149 cx.set_state(
17150 &r#"
17151 use some::mod2;
17152
17153 const A: u32 = 42;
17154 const C: u32 = 42;
17155
17156 fn main(ˇ) {
17157 //println!("hello");
17158
17159 println!("world");
17160 //
17161 //
17162 }
17163 "#
17164 .unindent(),
17165 );
17166
17167 cx.set_head_text(&diff_base);
17168 executor.run_until_parked();
17169
17170 cx.update_editor(|editor, window, cx| {
17171 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17172 });
17173 executor.run_until_parked();
17174 cx.assert_state_with_diff(
17175 r#"
17176 - use some::mod1;
17177 use some::mod2;
17178
17179 const A: u32 = 42;
17180 - const B: u32 = 42;
17181 const C: u32 = 42;
17182
17183 fn main(ˇ) {
17184 - println!("hello");
17185 + //println!("hello");
17186
17187 println!("world");
17188 + //
17189 + //
17190 }
17191 "#
17192 .unindent(),
17193 );
17194
17195 cx.set_head_text("new diff base!");
17196 executor.run_until_parked();
17197 cx.assert_state_with_diff(
17198 r#"
17199 - new diff base!
17200 + use some::mod2;
17201 +
17202 + const A: u32 = 42;
17203 + const C: u32 = 42;
17204 +
17205 + fn main(ˇ) {
17206 + //println!("hello");
17207 +
17208 + println!("world");
17209 + //
17210 + //
17211 + }
17212 "#
17213 .unindent(),
17214 );
17215}
17216
17217#[gpui::test]
17218async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
17219 init_test(cx, |_| {});
17220
17221 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17222 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17223 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17224 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17225 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
17226 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
17227
17228 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
17229 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
17230 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
17231
17232 let multi_buffer = cx.new(|cx| {
17233 let mut multibuffer = MultiBuffer::new(ReadWrite);
17234 multibuffer.push_excerpts(
17235 buffer_1.clone(),
17236 [
17237 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17238 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17239 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17240 ],
17241 cx,
17242 );
17243 multibuffer.push_excerpts(
17244 buffer_2.clone(),
17245 [
17246 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17247 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17248 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17249 ],
17250 cx,
17251 );
17252 multibuffer.push_excerpts(
17253 buffer_3.clone(),
17254 [
17255 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17256 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17257 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17258 ],
17259 cx,
17260 );
17261 multibuffer
17262 });
17263
17264 let editor =
17265 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17266 editor
17267 .update(cx, |editor, _window, cx| {
17268 for (buffer, diff_base) in [
17269 (buffer_1.clone(), file_1_old),
17270 (buffer_2.clone(), file_2_old),
17271 (buffer_3.clone(), file_3_old),
17272 ] {
17273 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
17274 editor
17275 .buffer
17276 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
17277 }
17278 })
17279 .unwrap();
17280
17281 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17282 cx.run_until_parked();
17283
17284 cx.assert_editor_state(
17285 &"
17286 ˇaaa
17287 ccc
17288 ddd
17289
17290 ggg
17291 hhh
17292
17293
17294 lll
17295 mmm
17296 NNN
17297
17298 qqq
17299 rrr
17300
17301 uuu
17302 111
17303 222
17304 333
17305
17306 666
17307 777
17308
17309 000
17310 !!!"
17311 .unindent(),
17312 );
17313
17314 cx.update_editor(|editor, window, cx| {
17315 editor.select_all(&SelectAll, window, cx);
17316 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17317 });
17318 cx.executor().run_until_parked();
17319
17320 cx.assert_state_with_diff(
17321 "
17322 «aaa
17323 - bbb
17324 ccc
17325 ddd
17326
17327 ggg
17328 hhh
17329
17330
17331 lll
17332 mmm
17333 - nnn
17334 + NNN
17335
17336 qqq
17337 rrr
17338
17339 uuu
17340 111
17341 222
17342 333
17343
17344 + 666
17345 777
17346
17347 000
17348 !!!ˇ»"
17349 .unindent(),
17350 );
17351}
17352
17353#[gpui::test]
17354async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
17355 init_test(cx, |_| {});
17356
17357 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
17358 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
17359
17360 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
17361 let multi_buffer = cx.new(|cx| {
17362 let mut multibuffer = MultiBuffer::new(ReadWrite);
17363 multibuffer.push_excerpts(
17364 buffer.clone(),
17365 [
17366 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
17367 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
17368 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
17369 ],
17370 cx,
17371 );
17372 multibuffer
17373 });
17374
17375 let editor =
17376 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17377 editor
17378 .update(cx, |editor, _window, cx| {
17379 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
17380 editor
17381 .buffer
17382 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
17383 })
17384 .unwrap();
17385
17386 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17387 cx.run_until_parked();
17388
17389 cx.update_editor(|editor, window, cx| {
17390 editor.expand_all_diff_hunks(&Default::default(), window, cx)
17391 });
17392 cx.executor().run_until_parked();
17393
17394 // When the start of a hunk coincides with the start of its excerpt,
17395 // the hunk is expanded. When the start of a a hunk is earlier than
17396 // the start of its excerpt, the hunk is not expanded.
17397 cx.assert_state_with_diff(
17398 "
17399 ˇaaa
17400 - bbb
17401 + BBB
17402
17403 - ddd
17404 - eee
17405 + DDD
17406 + EEE
17407 fff
17408
17409 iii
17410 "
17411 .unindent(),
17412 );
17413}
17414
17415#[gpui::test]
17416async fn test_edits_around_expanded_insertion_hunks(
17417 executor: BackgroundExecutor,
17418 cx: &mut TestAppContext,
17419) {
17420 init_test(cx, |_| {});
17421
17422 let mut cx = EditorTestContext::new(cx).await;
17423
17424 let diff_base = r#"
17425 use some::mod1;
17426 use some::mod2;
17427
17428 const A: u32 = 42;
17429
17430 fn main() {
17431 println!("hello");
17432
17433 println!("world");
17434 }
17435 "#
17436 .unindent();
17437 executor.run_until_parked();
17438 cx.set_state(
17439 &r#"
17440 use some::mod1;
17441 use some::mod2;
17442
17443 const A: u32 = 42;
17444 const B: u32 = 42;
17445 const C: u32 = 42;
17446 ˇ
17447
17448 fn main() {
17449 println!("hello");
17450
17451 println!("world");
17452 }
17453 "#
17454 .unindent(),
17455 );
17456
17457 cx.set_head_text(&diff_base);
17458 executor.run_until_parked();
17459
17460 cx.update_editor(|editor, window, cx| {
17461 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17462 });
17463 executor.run_until_parked();
17464
17465 cx.assert_state_with_diff(
17466 r#"
17467 use some::mod1;
17468 use some::mod2;
17469
17470 const A: u32 = 42;
17471 + const B: u32 = 42;
17472 + const C: u32 = 42;
17473 + ˇ
17474
17475 fn main() {
17476 println!("hello");
17477
17478 println!("world");
17479 }
17480 "#
17481 .unindent(),
17482 );
17483
17484 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
17485 executor.run_until_parked();
17486
17487 cx.assert_state_with_diff(
17488 r#"
17489 use some::mod1;
17490 use some::mod2;
17491
17492 const A: u32 = 42;
17493 + const B: u32 = 42;
17494 + const C: u32 = 42;
17495 + const D: u32 = 42;
17496 + ˇ
17497
17498 fn main() {
17499 println!("hello");
17500
17501 println!("world");
17502 }
17503 "#
17504 .unindent(),
17505 );
17506
17507 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
17508 executor.run_until_parked();
17509
17510 cx.assert_state_with_diff(
17511 r#"
17512 use some::mod1;
17513 use some::mod2;
17514
17515 const A: u32 = 42;
17516 + const B: u32 = 42;
17517 + const C: u32 = 42;
17518 + const D: u32 = 42;
17519 + const E: u32 = 42;
17520 + ˇ
17521
17522 fn main() {
17523 println!("hello");
17524
17525 println!("world");
17526 }
17527 "#
17528 .unindent(),
17529 );
17530
17531 cx.update_editor(|editor, window, cx| {
17532 editor.delete_line(&DeleteLine, window, cx);
17533 });
17534 executor.run_until_parked();
17535
17536 cx.assert_state_with_diff(
17537 r#"
17538 use some::mod1;
17539 use some::mod2;
17540
17541 const A: u32 = 42;
17542 + const B: u32 = 42;
17543 + const C: u32 = 42;
17544 + const D: u32 = 42;
17545 + const E: u32 = 42;
17546 ˇ
17547 fn main() {
17548 println!("hello");
17549
17550 println!("world");
17551 }
17552 "#
17553 .unindent(),
17554 );
17555
17556 cx.update_editor(|editor, window, cx| {
17557 editor.move_up(&MoveUp, window, cx);
17558 editor.delete_line(&DeleteLine, window, cx);
17559 editor.move_up(&MoveUp, window, cx);
17560 editor.delete_line(&DeleteLine, window, cx);
17561 editor.move_up(&MoveUp, window, cx);
17562 editor.delete_line(&DeleteLine, window, cx);
17563 });
17564 executor.run_until_parked();
17565 cx.assert_state_with_diff(
17566 r#"
17567 use some::mod1;
17568 use some::mod2;
17569
17570 const A: u32 = 42;
17571 + const B: u32 = 42;
17572 ˇ
17573 fn main() {
17574 println!("hello");
17575
17576 println!("world");
17577 }
17578 "#
17579 .unindent(),
17580 );
17581
17582 cx.update_editor(|editor, window, cx| {
17583 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
17584 editor.delete_line(&DeleteLine, window, cx);
17585 });
17586 executor.run_until_parked();
17587 cx.assert_state_with_diff(
17588 r#"
17589 ˇ
17590 fn main() {
17591 println!("hello");
17592
17593 println!("world");
17594 }
17595 "#
17596 .unindent(),
17597 );
17598}
17599
17600#[gpui::test]
17601async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
17602 init_test(cx, |_| {});
17603
17604 let mut cx = EditorTestContext::new(cx).await;
17605 cx.set_head_text(indoc! { "
17606 one
17607 two
17608 three
17609 four
17610 five
17611 "
17612 });
17613 cx.set_state(indoc! { "
17614 one
17615 ˇthree
17616 five
17617 "});
17618 cx.run_until_parked();
17619 cx.update_editor(|editor, window, cx| {
17620 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17621 });
17622 cx.assert_state_with_diff(
17623 indoc! { "
17624 one
17625 - two
17626 ˇthree
17627 - four
17628 five
17629 "}
17630 .to_string(),
17631 );
17632 cx.update_editor(|editor, window, cx| {
17633 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17634 });
17635
17636 cx.assert_state_with_diff(
17637 indoc! { "
17638 one
17639 ˇthree
17640 five
17641 "}
17642 .to_string(),
17643 );
17644
17645 cx.set_state(indoc! { "
17646 one
17647 ˇTWO
17648 three
17649 four
17650 five
17651 "});
17652 cx.run_until_parked();
17653 cx.update_editor(|editor, window, cx| {
17654 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17655 });
17656
17657 cx.assert_state_with_diff(
17658 indoc! { "
17659 one
17660 - two
17661 + ˇTWO
17662 three
17663 four
17664 five
17665 "}
17666 .to_string(),
17667 );
17668 cx.update_editor(|editor, window, cx| {
17669 editor.move_up(&Default::default(), window, cx);
17670 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17671 });
17672 cx.assert_state_with_diff(
17673 indoc! { "
17674 one
17675 ˇTWO
17676 three
17677 four
17678 five
17679 "}
17680 .to_string(),
17681 );
17682}
17683
17684#[gpui::test]
17685async fn test_edits_around_expanded_deletion_hunks(
17686 executor: BackgroundExecutor,
17687 cx: &mut TestAppContext,
17688) {
17689 init_test(cx, |_| {});
17690
17691 let mut cx = EditorTestContext::new(cx).await;
17692
17693 let diff_base = r#"
17694 use some::mod1;
17695 use some::mod2;
17696
17697 const A: u32 = 42;
17698 const B: u32 = 42;
17699 const C: u32 = 42;
17700
17701
17702 fn main() {
17703 println!("hello");
17704
17705 println!("world");
17706 }
17707 "#
17708 .unindent();
17709 executor.run_until_parked();
17710 cx.set_state(
17711 &r#"
17712 use some::mod1;
17713 use some::mod2;
17714
17715 ˇconst B: u32 = 42;
17716 const C: u32 = 42;
17717
17718
17719 fn main() {
17720 println!("hello");
17721
17722 println!("world");
17723 }
17724 "#
17725 .unindent(),
17726 );
17727
17728 cx.set_head_text(&diff_base);
17729 executor.run_until_parked();
17730
17731 cx.update_editor(|editor, window, cx| {
17732 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17733 });
17734 executor.run_until_parked();
17735
17736 cx.assert_state_with_diff(
17737 r#"
17738 use some::mod1;
17739 use some::mod2;
17740
17741 - const A: u32 = 42;
17742 ˇconst B: u32 = 42;
17743 const C: u32 = 42;
17744
17745
17746 fn main() {
17747 println!("hello");
17748
17749 println!("world");
17750 }
17751 "#
17752 .unindent(),
17753 );
17754
17755 cx.update_editor(|editor, window, cx| {
17756 editor.delete_line(&DeleteLine, window, cx);
17757 });
17758 executor.run_until_parked();
17759 cx.assert_state_with_diff(
17760 r#"
17761 use some::mod1;
17762 use some::mod2;
17763
17764 - const A: u32 = 42;
17765 - const B: u32 = 42;
17766 ˇconst C: u32 = 42;
17767
17768
17769 fn main() {
17770 println!("hello");
17771
17772 println!("world");
17773 }
17774 "#
17775 .unindent(),
17776 );
17777
17778 cx.update_editor(|editor, window, cx| {
17779 editor.delete_line(&DeleteLine, window, cx);
17780 });
17781 executor.run_until_parked();
17782 cx.assert_state_with_diff(
17783 r#"
17784 use some::mod1;
17785 use some::mod2;
17786
17787 - const A: u32 = 42;
17788 - const B: u32 = 42;
17789 - const C: u32 = 42;
17790 ˇ
17791
17792 fn main() {
17793 println!("hello");
17794
17795 println!("world");
17796 }
17797 "#
17798 .unindent(),
17799 );
17800
17801 cx.update_editor(|editor, window, cx| {
17802 editor.handle_input("replacement", window, cx);
17803 });
17804 executor.run_until_parked();
17805 cx.assert_state_with_diff(
17806 r#"
17807 use some::mod1;
17808 use some::mod2;
17809
17810 - const A: u32 = 42;
17811 - const B: u32 = 42;
17812 - const C: u32 = 42;
17813 -
17814 + replacementˇ
17815
17816 fn main() {
17817 println!("hello");
17818
17819 println!("world");
17820 }
17821 "#
17822 .unindent(),
17823 );
17824}
17825
17826#[gpui::test]
17827async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17828 init_test(cx, |_| {});
17829
17830 let mut cx = EditorTestContext::new(cx).await;
17831
17832 let base_text = r#"
17833 one
17834 two
17835 three
17836 four
17837 five
17838 "#
17839 .unindent();
17840 executor.run_until_parked();
17841 cx.set_state(
17842 &r#"
17843 one
17844 two
17845 fˇour
17846 five
17847 "#
17848 .unindent(),
17849 );
17850
17851 cx.set_head_text(&base_text);
17852 executor.run_until_parked();
17853
17854 cx.update_editor(|editor, window, cx| {
17855 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17856 });
17857 executor.run_until_parked();
17858
17859 cx.assert_state_with_diff(
17860 r#"
17861 one
17862 two
17863 - three
17864 fˇour
17865 five
17866 "#
17867 .unindent(),
17868 );
17869
17870 cx.update_editor(|editor, window, cx| {
17871 editor.backspace(&Backspace, window, cx);
17872 editor.backspace(&Backspace, window, cx);
17873 });
17874 executor.run_until_parked();
17875 cx.assert_state_with_diff(
17876 r#"
17877 one
17878 two
17879 - threeˇ
17880 - four
17881 + our
17882 five
17883 "#
17884 .unindent(),
17885 );
17886}
17887
17888#[gpui::test]
17889async fn test_edit_after_expanded_modification_hunk(
17890 executor: BackgroundExecutor,
17891 cx: &mut TestAppContext,
17892) {
17893 init_test(cx, |_| {});
17894
17895 let mut cx = EditorTestContext::new(cx).await;
17896
17897 let diff_base = r#"
17898 use some::mod1;
17899 use some::mod2;
17900
17901 const A: u32 = 42;
17902 const B: u32 = 42;
17903 const C: u32 = 42;
17904 const D: u32 = 42;
17905
17906
17907 fn main() {
17908 println!("hello");
17909
17910 println!("world");
17911 }"#
17912 .unindent();
17913
17914 cx.set_state(
17915 &r#"
17916 use some::mod1;
17917 use some::mod2;
17918
17919 const A: u32 = 42;
17920 const B: u32 = 42;
17921 const C: u32 = 43ˇ
17922 const D: u32 = 42;
17923
17924
17925 fn main() {
17926 println!("hello");
17927
17928 println!("world");
17929 }"#
17930 .unindent(),
17931 );
17932
17933 cx.set_head_text(&diff_base);
17934 executor.run_until_parked();
17935 cx.update_editor(|editor, window, cx| {
17936 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17937 });
17938 executor.run_until_parked();
17939
17940 cx.assert_state_with_diff(
17941 r#"
17942 use some::mod1;
17943 use some::mod2;
17944
17945 const A: u32 = 42;
17946 const B: u32 = 42;
17947 - const C: u32 = 42;
17948 + const C: u32 = 43ˇ
17949 const D: u32 = 42;
17950
17951
17952 fn main() {
17953 println!("hello");
17954
17955 println!("world");
17956 }"#
17957 .unindent(),
17958 );
17959
17960 cx.update_editor(|editor, window, cx| {
17961 editor.handle_input("\nnew_line\n", window, cx);
17962 });
17963 executor.run_until_parked();
17964
17965 cx.assert_state_with_diff(
17966 r#"
17967 use some::mod1;
17968 use some::mod2;
17969
17970 const A: u32 = 42;
17971 const B: u32 = 42;
17972 - const C: u32 = 42;
17973 + const C: u32 = 43
17974 + new_line
17975 + ˇ
17976 const D: u32 = 42;
17977
17978
17979 fn main() {
17980 println!("hello");
17981
17982 println!("world");
17983 }"#
17984 .unindent(),
17985 );
17986}
17987
17988#[gpui::test]
17989async fn test_stage_and_unstage_added_file_hunk(
17990 executor: BackgroundExecutor,
17991 cx: &mut TestAppContext,
17992) {
17993 init_test(cx, |_| {});
17994
17995 let mut cx = EditorTestContext::new(cx).await;
17996 cx.update_editor(|editor, _, cx| {
17997 editor.set_expand_all_diff_hunks(cx);
17998 });
17999
18000 let working_copy = r#"
18001 ˇfn main() {
18002 println!("hello, world!");
18003 }
18004 "#
18005 .unindent();
18006
18007 cx.set_state(&working_copy);
18008 executor.run_until_parked();
18009
18010 cx.assert_state_with_diff(
18011 r#"
18012 + ˇfn main() {
18013 + println!("hello, world!");
18014 + }
18015 "#
18016 .unindent(),
18017 );
18018 cx.assert_index_text(None);
18019
18020 cx.update_editor(|editor, window, cx| {
18021 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18022 });
18023 executor.run_until_parked();
18024 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
18025 cx.assert_state_with_diff(
18026 r#"
18027 + ˇfn main() {
18028 + println!("hello, world!");
18029 + }
18030 "#
18031 .unindent(),
18032 );
18033
18034 cx.update_editor(|editor, window, cx| {
18035 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18036 });
18037 executor.run_until_parked();
18038 cx.assert_index_text(None);
18039}
18040
18041async fn setup_indent_guides_editor(
18042 text: &str,
18043 cx: &mut TestAppContext,
18044) -> (BufferId, EditorTestContext) {
18045 init_test(cx, |_| {});
18046
18047 let mut cx = EditorTestContext::new(cx).await;
18048
18049 let buffer_id = cx.update_editor(|editor, window, cx| {
18050 editor.set_text(text, window, cx);
18051 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
18052
18053 buffer_ids[0]
18054 });
18055
18056 (buffer_id, cx)
18057}
18058
18059fn assert_indent_guides(
18060 range: Range<u32>,
18061 expected: Vec<IndentGuide>,
18062 active_indices: Option<Vec<usize>>,
18063 cx: &mut EditorTestContext,
18064) {
18065 let indent_guides = cx.update_editor(|editor, window, cx| {
18066 let snapshot = editor.snapshot(window, cx).display_snapshot;
18067 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
18068 editor,
18069 MultiBufferRow(range.start)..MultiBufferRow(range.end),
18070 true,
18071 &snapshot,
18072 cx,
18073 );
18074
18075 indent_guides.sort_by(|a, b| {
18076 a.depth.cmp(&b.depth).then(
18077 a.start_row
18078 .cmp(&b.start_row)
18079 .then(a.end_row.cmp(&b.end_row)),
18080 )
18081 });
18082 indent_guides
18083 });
18084
18085 if let Some(expected) = active_indices {
18086 let active_indices = cx.update_editor(|editor, window, cx| {
18087 let snapshot = editor.snapshot(window, cx).display_snapshot;
18088 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
18089 });
18090
18091 assert_eq!(
18092 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
18093 expected,
18094 "Active indent guide indices do not match"
18095 );
18096 }
18097
18098 assert_eq!(indent_guides, expected, "Indent guides do not match");
18099}
18100
18101fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
18102 IndentGuide {
18103 buffer_id,
18104 start_row: MultiBufferRow(start_row),
18105 end_row: MultiBufferRow(end_row),
18106 depth,
18107 tab_size: 4,
18108 settings: IndentGuideSettings {
18109 enabled: true,
18110 line_width: 1,
18111 active_line_width: 1,
18112 ..Default::default()
18113 },
18114 }
18115}
18116
18117#[gpui::test]
18118async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
18119 let (buffer_id, mut cx) = setup_indent_guides_editor(
18120 &"
18121 fn main() {
18122 let a = 1;
18123 }"
18124 .unindent(),
18125 cx,
18126 )
18127 .await;
18128
18129 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18130}
18131
18132#[gpui::test]
18133async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
18134 let (buffer_id, mut cx) = setup_indent_guides_editor(
18135 &"
18136 fn main() {
18137 let a = 1;
18138 let b = 2;
18139 }"
18140 .unindent(),
18141 cx,
18142 )
18143 .await;
18144
18145 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
18146}
18147
18148#[gpui::test]
18149async fn test_indent_guide_nested(cx: &mut TestAppContext) {
18150 let (buffer_id, mut cx) = setup_indent_guides_editor(
18151 &"
18152 fn main() {
18153 let a = 1;
18154 if a == 3 {
18155 let b = 2;
18156 } else {
18157 let c = 3;
18158 }
18159 }"
18160 .unindent(),
18161 cx,
18162 )
18163 .await;
18164
18165 assert_indent_guides(
18166 0..8,
18167 vec![
18168 indent_guide(buffer_id, 1, 6, 0),
18169 indent_guide(buffer_id, 3, 3, 1),
18170 indent_guide(buffer_id, 5, 5, 1),
18171 ],
18172 None,
18173 &mut cx,
18174 );
18175}
18176
18177#[gpui::test]
18178async fn test_indent_guide_tab(cx: &mut TestAppContext) {
18179 let (buffer_id, mut cx) = setup_indent_guides_editor(
18180 &"
18181 fn main() {
18182 let a = 1;
18183 let b = 2;
18184 let c = 3;
18185 }"
18186 .unindent(),
18187 cx,
18188 )
18189 .await;
18190
18191 assert_indent_guides(
18192 0..5,
18193 vec![
18194 indent_guide(buffer_id, 1, 3, 0),
18195 indent_guide(buffer_id, 2, 2, 1),
18196 ],
18197 None,
18198 &mut cx,
18199 );
18200}
18201
18202#[gpui::test]
18203async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
18204 let (buffer_id, mut cx) = setup_indent_guides_editor(
18205 &"
18206 fn main() {
18207 let a = 1;
18208
18209 let c = 3;
18210 }"
18211 .unindent(),
18212 cx,
18213 )
18214 .await;
18215
18216 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
18217}
18218
18219#[gpui::test]
18220async fn test_indent_guide_complex(cx: &mut TestAppContext) {
18221 let (buffer_id, mut cx) = setup_indent_guides_editor(
18222 &"
18223 fn main() {
18224 let a = 1;
18225
18226 let c = 3;
18227
18228 if a == 3 {
18229 let b = 2;
18230 } else {
18231 let c = 3;
18232 }
18233 }"
18234 .unindent(),
18235 cx,
18236 )
18237 .await;
18238
18239 assert_indent_guides(
18240 0..11,
18241 vec![
18242 indent_guide(buffer_id, 1, 9, 0),
18243 indent_guide(buffer_id, 6, 6, 1),
18244 indent_guide(buffer_id, 8, 8, 1),
18245 ],
18246 None,
18247 &mut cx,
18248 );
18249}
18250
18251#[gpui::test]
18252async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
18253 let (buffer_id, mut cx) = setup_indent_guides_editor(
18254 &"
18255 fn main() {
18256 let a = 1;
18257
18258 let c = 3;
18259
18260 if a == 3 {
18261 let b = 2;
18262 } else {
18263 let c = 3;
18264 }
18265 }"
18266 .unindent(),
18267 cx,
18268 )
18269 .await;
18270
18271 assert_indent_guides(
18272 1..11,
18273 vec![
18274 indent_guide(buffer_id, 1, 9, 0),
18275 indent_guide(buffer_id, 6, 6, 1),
18276 indent_guide(buffer_id, 8, 8, 1),
18277 ],
18278 None,
18279 &mut cx,
18280 );
18281}
18282
18283#[gpui::test]
18284async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
18285 let (buffer_id, mut cx) = setup_indent_guides_editor(
18286 &"
18287 fn main() {
18288 let a = 1;
18289
18290 let c = 3;
18291
18292 if a == 3 {
18293 let b = 2;
18294 } else {
18295 let c = 3;
18296 }
18297 }"
18298 .unindent(),
18299 cx,
18300 )
18301 .await;
18302
18303 assert_indent_guides(
18304 1..10,
18305 vec![
18306 indent_guide(buffer_id, 1, 9, 0),
18307 indent_guide(buffer_id, 6, 6, 1),
18308 indent_guide(buffer_id, 8, 8, 1),
18309 ],
18310 None,
18311 &mut cx,
18312 );
18313}
18314
18315#[gpui::test]
18316async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
18317 let (buffer_id, mut cx) = setup_indent_guides_editor(
18318 &"
18319 fn main() {
18320 if a {
18321 b(
18322 c,
18323 d,
18324 )
18325 } else {
18326 e(
18327 f
18328 )
18329 }
18330 }"
18331 .unindent(),
18332 cx,
18333 )
18334 .await;
18335
18336 assert_indent_guides(
18337 0..11,
18338 vec![
18339 indent_guide(buffer_id, 1, 10, 0),
18340 indent_guide(buffer_id, 2, 5, 1),
18341 indent_guide(buffer_id, 7, 9, 1),
18342 indent_guide(buffer_id, 3, 4, 2),
18343 indent_guide(buffer_id, 8, 8, 2),
18344 ],
18345 None,
18346 &mut cx,
18347 );
18348
18349 cx.update_editor(|editor, window, cx| {
18350 editor.fold_at(MultiBufferRow(2), window, cx);
18351 assert_eq!(
18352 editor.display_text(cx),
18353 "
18354 fn main() {
18355 if a {
18356 b(⋯
18357 )
18358 } else {
18359 e(
18360 f
18361 )
18362 }
18363 }"
18364 .unindent()
18365 );
18366 });
18367
18368 assert_indent_guides(
18369 0..11,
18370 vec![
18371 indent_guide(buffer_id, 1, 10, 0),
18372 indent_guide(buffer_id, 2, 5, 1),
18373 indent_guide(buffer_id, 7, 9, 1),
18374 indent_guide(buffer_id, 8, 8, 2),
18375 ],
18376 None,
18377 &mut cx,
18378 );
18379}
18380
18381#[gpui::test]
18382async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
18383 let (buffer_id, mut cx) = setup_indent_guides_editor(
18384 &"
18385 block1
18386 block2
18387 block3
18388 block4
18389 block2
18390 block1
18391 block1"
18392 .unindent(),
18393 cx,
18394 )
18395 .await;
18396
18397 assert_indent_guides(
18398 1..10,
18399 vec![
18400 indent_guide(buffer_id, 1, 4, 0),
18401 indent_guide(buffer_id, 2, 3, 1),
18402 indent_guide(buffer_id, 3, 3, 2),
18403 ],
18404 None,
18405 &mut cx,
18406 );
18407}
18408
18409#[gpui::test]
18410async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
18411 let (buffer_id, mut cx) = setup_indent_guides_editor(
18412 &"
18413 block1
18414 block2
18415 block3
18416
18417 block1
18418 block1"
18419 .unindent(),
18420 cx,
18421 )
18422 .await;
18423
18424 assert_indent_guides(
18425 0..6,
18426 vec![
18427 indent_guide(buffer_id, 1, 2, 0),
18428 indent_guide(buffer_id, 2, 2, 1),
18429 ],
18430 None,
18431 &mut cx,
18432 );
18433}
18434
18435#[gpui::test]
18436async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
18437 let (buffer_id, mut cx) = setup_indent_guides_editor(
18438 &"
18439 function component() {
18440 \treturn (
18441 \t\t\t
18442 \t\t<div>
18443 \t\t\t<abc></abc>
18444 \t\t</div>
18445 \t)
18446 }"
18447 .unindent(),
18448 cx,
18449 )
18450 .await;
18451
18452 assert_indent_guides(
18453 0..8,
18454 vec![
18455 indent_guide(buffer_id, 1, 6, 0),
18456 indent_guide(buffer_id, 2, 5, 1),
18457 indent_guide(buffer_id, 4, 4, 2),
18458 ],
18459 None,
18460 &mut cx,
18461 );
18462}
18463
18464#[gpui::test]
18465async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
18466 let (buffer_id, mut cx) = setup_indent_guides_editor(
18467 &"
18468 function component() {
18469 \treturn (
18470 \t
18471 \t\t<div>
18472 \t\t\t<abc></abc>
18473 \t\t</div>
18474 \t)
18475 }"
18476 .unindent(),
18477 cx,
18478 )
18479 .await;
18480
18481 assert_indent_guides(
18482 0..8,
18483 vec![
18484 indent_guide(buffer_id, 1, 6, 0),
18485 indent_guide(buffer_id, 2, 5, 1),
18486 indent_guide(buffer_id, 4, 4, 2),
18487 ],
18488 None,
18489 &mut cx,
18490 );
18491}
18492
18493#[gpui::test]
18494async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
18495 let (buffer_id, mut cx) = setup_indent_guides_editor(
18496 &"
18497 block1
18498
18499
18500
18501 block2
18502 "
18503 .unindent(),
18504 cx,
18505 )
18506 .await;
18507
18508 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18509}
18510
18511#[gpui::test]
18512async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
18513 let (buffer_id, mut cx) = setup_indent_guides_editor(
18514 &"
18515 def a:
18516 \tb = 3
18517 \tif True:
18518 \t\tc = 4
18519 \t\td = 5
18520 \tprint(b)
18521 "
18522 .unindent(),
18523 cx,
18524 )
18525 .await;
18526
18527 assert_indent_guides(
18528 0..6,
18529 vec![
18530 indent_guide(buffer_id, 1, 5, 0),
18531 indent_guide(buffer_id, 3, 4, 1),
18532 ],
18533 None,
18534 &mut cx,
18535 );
18536}
18537
18538#[gpui::test]
18539async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
18540 let (buffer_id, mut cx) = setup_indent_guides_editor(
18541 &"
18542 fn main() {
18543 let a = 1;
18544 }"
18545 .unindent(),
18546 cx,
18547 )
18548 .await;
18549
18550 cx.update_editor(|editor, window, cx| {
18551 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18552 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18553 });
18554 });
18555
18556 assert_indent_guides(
18557 0..3,
18558 vec![indent_guide(buffer_id, 1, 1, 0)],
18559 Some(vec![0]),
18560 &mut cx,
18561 );
18562}
18563
18564#[gpui::test]
18565async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
18566 let (buffer_id, mut cx) = setup_indent_guides_editor(
18567 &"
18568 fn main() {
18569 if 1 == 2 {
18570 let a = 1;
18571 }
18572 }"
18573 .unindent(),
18574 cx,
18575 )
18576 .await;
18577
18578 cx.update_editor(|editor, window, cx| {
18579 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18580 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18581 });
18582 });
18583
18584 assert_indent_guides(
18585 0..4,
18586 vec![
18587 indent_guide(buffer_id, 1, 3, 0),
18588 indent_guide(buffer_id, 2, 2, 1),
18589 ],
18590 Some(vec![1]),
18591 &mut cx,
18592 );
18593
18594 cx.update_editor(|editor, window, cx| {
18595 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18596 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18597 });
18598 });
18599
18600 assert_indent_guides(
18601 0..4,
18602 vec![
18603 indent_guide(buffer_id, 1, 3, 0),
18604 indent_guide(buffer_id, 2, 2, 1),
18605 ],
18606 Some(vec![1]),
18607 &mut cx,
18608 );
18609
18610 cx.update_editor(|editor, window, cx| {
18611 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18612 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
18613 });
18614 });
18615
18616 assert_indent_guides(
18617 0..4,
18618 vec![
18619 indent_guide(buffer_id, 1, 3, 0),
18620 indent_guide(buffer_id, 2, 2, 1),
18621 ],
18622 Some(vec![0]),
18623 &mut cx,
18624 );
18625}
18626
18627#[gpui::test]
18628async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
18629 let (buffer_id, mut cx) = setup_indent_guides_editor(
18630 &"
18631 fn main() {
18632 let a = 1;
18633
18634 let b = 2;
18635 }"
18636 .unindent(),
18637 cx,
18638 )
18639 .await;
18640
18641 cx.update_editor(|editor, window, cx| {
18642 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18643 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18644 });
18645 });
18646
18647 assert_indent_guides(
18648 0..5,
18649 vec![indent_guide(buffer_id, 1, 3, 0)],
18650 Some(vec![0]),
18651 &mut cx,
18652 );
18653}
18654
18655#[gpui::test]
18656async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
18657 let (buffer_id, mut cx) = setup_indent_guides_editor(
18658 &"
18659 def m:
18660 a = 1
18661 pass"
18662 .unindent(),
18663 cx,
18664 )
18665 .await;
18666
18667 cx.update_editor(|editor, window, cx| {
18668 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18669 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18670 });
18671 });
18672
18673 assert_indent_guides(
18674 0..3,
18675 vec![indent_guide(buffer_id, 1, 2, 0)],
18676 Some(vec![0]),
18677 &mut cx,
18678 );
18679}
18680
18681#[gpui::test]
18682async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
18683 init_test(cx, |_| {});
18684 let mut cx = EditorTestContext::new(cx).await;
18685 let text = indoc! {
18686 "
18687 impl A {
18688 fn b() {
18689 0;
18690 3;
18691 5;
18692 6;
18693 7;
18694 }
18695 }
18696 "
18697 };
18698 let base_text = indoc! {
18699 "
18700 impl A {
18701 fn b() {
18702 0;
18703 1;
18704 2;
18705 3;
18706 4;
18707 }
18708 fn c() {
18709 5;
18710 6;
18711 7;
18712 }
18713 }
18714 "
18715 };
18716
18717 cx.update_editor(|editor, window, cx| {
18718 editor.set_text(text, window, cx);
18719
18720 editor.buffer().update(cx, |multibuffer, cx| {
18721 let buffer = multibuffer.as_singleton().unwrap();
18722 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
18723
18724 multibuffer.set_all_diff_hunks_expanded(cx);
18725 multibuffer.add_diff(diff, cx);
18726
18727 buffer.read(cx).remote_id()
18728 })
18729 });
18730 cx.run_until_parked();
18731
18732 cx.assert_state_with_diff(
18733 indoc! { "
18734 impl A {
18735 fn b() {
18736 0;
18737 - 1;
18738 - 2;
18739 3;
18740 - 4;
18741 - }
18742 - fn c() {
18743 5;
18744 6;
18745 7;
18746 }
18747 }
18748 ˇ"
18749 }
18750 .to_string(),
18751 );
18752
18753 let mut actual_guides = cx.update_editor(|editor, window, cx| {
18754 editor
18755 .snapshot(window, cx)
18756 .buffer_snapshot
18757 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
18758 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
18759 .collect::<Vec<_>>()
18760 });
18761 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
18762 assert_eq!(
18763 actual_guides,
18764 vec![
18765 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
18766 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
18767 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
18768 ]
18769 );
18770}
18771
18772#[gpui::test]
18773async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18774 init_test(cx, |_| {});
18775 let mut cx = EditorTestContext::new(cx).await;
18776
18777 let diff_base = r#"
18778 a
18779 b
18780 c
18781 "#
18782 .unindent();
18783
18784 cx.set_state(
18785 &r#"
18786 ˇA
18787 b
18788 C
18789 "#
18790 .unindent(),
18791 );
18792 cx.set_head_text(&diff_base);
18793 cx.update_editor(|editor, window, cx| {
18794 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18795 });
18796 executor.run_until_parked();
18797
18798 let both_hunks_expanded = r#"
18799 - a
18800 + ˇA
18801 b
18802 - c
18803 + C
18804 "#
18805 .unindent();
18806
18807 cx.assert_state_with_diff(both_hunks_expanded.clone());
18808
18809 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18810 let snapshot = editor.snapshot(window, cx);
18811 let hunks = editor
18812 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18813 .collect::<Vec<_>>();
18814 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18815 let buffer_id = hunks[0].buffer_id;
18816 hunks
18817 .into_iter()
18818 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18819 .collect::<Vec<_>>()
18820 });
18821 assert_eq!(hunk_ranges.len(), 2);
18822
18823 cx.update_editor(|editor, _, cx| {
18824 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18825 });
18826 executor.run_until_parked();
18827
18828 let second_hunk_expanded = r#"
18829 ˇA
18830 b
18831 - c
18832 + C
18833 "#
18834 .unindent();
18835
18836 cx.assert_state_with_diff(second_hunk_expanded);
18837
18838 cx.update_editor(|editor, _, cx| {
18839 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18840 });
18841 executor.run_until_parked();
18842
18843 cx.assert_state_with_diff(both_hunks_expanded.clone());
18844
18845 cx.update_editor(|editor, _, cx| {
18846 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18847 });
18848 executor.run_until_parked();
18849
18850 let first_hunk_expanded = r#"
18851 - a
18852 + ˇA
18853 b
18854 C
18855 "#
18856 .unindent();
18857
18858 cx.assert_state_with_diff(first_hunk_expanded);
18859
18860 cx.update_editor(|editor, _, cx| {
18861 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18862 });
18863 executor.run_until_parked();
18864
18865 cx.assert_state_with_diff(both_hunks_expanded);
18866
18867 cx.set_state(
18868 &r#"
18869 ˇA
18870 b
18871 "#
18872 .unindent(),
18873 );
18874 cx.run_until_parked();
18875
18876 // TODO this cursor position seems bad
18877 cx.assert_state_with_diff(
18878 r#"
18879 - ˇa
18880 + A
18881 b
18882 "#
18883 .unindent(),
18884 );
18885
18886 cx.update_editor(|editor, window, cx| {
18887 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18888 });
18889
18890 cx.assert_state_with_diff(
18891 r#"
18892 - ˇa
18893 + A
18894 b
18895 - c
18896 "#
18897 .unindent(),
18898 );
18899
18900 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18901 let snapshot = editor.snapshot(window, cx);
18902 let hunks = editor
18903 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18904 .collect::<Vec<_>>();
18905 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18906 let buffer_id = hunks[0].buffer_id;
18907 hunks
18908 .into_iter()
18909 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18910 .collect::<Vec<_>>()
18911 });
18912 assert_eq!(hunk_ranges.len(), 2);
18913
18914 cx.update_editor(|editor, _, cx| {
18915 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18916 });
18917 executor.run_until_parked();
18918
18919 cx.assert_state_with_diff(
18920 r#"
18921 - ˇa
18922 + A
18923 b
18924 "#
18925 .unindent(),
18926 );
18927}
18928
18929#[gpui::test]
18930async fn test_toggle_deletion_hunk_at_start_of_file(
18931 executor: BackgroundExecutor,
18932 cx: &mut TestAppContext,
18933) {
18934 init_test(cx, |_| {});
18935 let mut cx = EditorTestContext::new(cx).await;
18936
18937 let diff_base = r#"
18938 a
18939 b
18940 c
18941 "#
18942 .unindent();
18943
18944 cx.set_state(
18945 &r#"
18946 ˇb
18947 c
18948 "#
18949 .unindent(),
18950 );
18951 cx.set_head_text(&diff_base);
18952 cx.update_editor(|editor, window, cx| {
18953 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18954 });
18955 executor.run_until_parked();
18956
18957 let hunk_expanded = r#"
18958 - a
18959 ˇb
18960 c
18961 "#
18962 .unindent();
18963
18964 cx.assert_state_with_diff(hunk_expanded.clone());
18965
18966 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18967 let snapshot = editor.snapshot(window, cx);
18968 let hunks = editor
18969 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18970 .collect::<Vec<_>>();
18971 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18972 let buffer_id = hunks[0].buffer_id;
18973 hunks
18974 .into_iter()
18975 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18976 .collect::<Vec<_>>()
18977 });
18978 assert_eq!(hunk_ranges.len(), 1);
18979
18980 cx.update_editor(|editor, _, cx| {
18981 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18982 });
18983 executor.run_until_parked();
18984
18985 let hunk_collapsed = r#"
18986 ˇb
18987 c
18988 "#
18989 .unindent();
18990
18991 cx.assert_state_with_diff(hunk_collapsed);
18992
18993 cx.update_editor(|editor, _, cx| {
18994 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18995 });
18996 executor.run_until_parked();
18997
18998 cx.assert_state_with_diff(hunk_expanded.clone());
18999}
19000
19001#[gpui::test]
19002async fn test_display_diff_hunks(cx: &mut TestAppContext) {
19003 init_test(cx, |_| {});
19004
19005 let fs = FakeFs::new(cx.executor());
19006 fs.insert_tree(
19007 path!("/test"),
19008 json!({
19009 ".git": {},
19010 "file-1": "ONE\n",
19011 "file-2": "TWO\n",
19012 "file-3": "THREE\n",
19013 }),
19014 )
19015 .await;
19016
19017 fs.set_head_for_repo(
19018 path!("/test/.git").as_ref(),
19019 &[
19020 ("file-1".into(), "one\n".into()),
19021 ("file-2".into(), "two\n".into()),
19022 ("file-3".into(), "three\n".into()),
19023 ],
19024 "deadbeef",
19025 );
19026
19027 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
19028 let mut buffers = vec![];
19029 for i in 1..=3 {
19030 let buffer = project
19031 .update(cx, |project, cx| {
19032 let path = format!(path!("/test/file-{}"), i);
19033 project.open_local_buffer(path, cx)
19034 })
19035 .await
19036 .unwrap();
19037 buffers.push(buffer);
19038 }
19039
19040 let multibuffer = cx.new(|cx| {
19041 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
19042 multibuffer.set_all_diff_hunks_expanded(cx);
19043 for buffer in &buffers {
19044 let snapshot = buffer.read(cx).snapshot();
19045 multibuffer.set_excerpts_for_path(
19046 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
19047 buffer.clone(),
19048 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
19049 DEFAULT_MULTIBUFFER_CONTEXT,
19050 cx,
19051 );
19052 }
19053 multibuffer
19054 });
19055
19056 let editor = cx.add_window(|window, cx| {
19057 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
19058 });
19059 cx.run_until_parked();
19060
19061 let snapshot = editor
19062 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19063 .unwrap();
19064 let hunks = snapshot
19065 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
19066 .map(|hunk| match hunk {
19067 DisplayDiffHunk::Unfolded {
19068 display_row_range, ..
19069 } => display_row_range,
19070 DisplayDiffHunk::Folded { .. } => unreachable!(),
19071 })
19072 .collect::<Vec<_>>();
19073 assert_eq!(
19074 hunks,
19075 [
19076 DisplayRow(2)..DisplayRow(4),
19077 DisplayRow(7)..DisplayRow(9),
19078 DisplayRow(12)..DisplayRow(14),
19079 ]
19080 );
19081}
19082
19083#[gpui::test]
19084async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
19085 init_test(cx, |_| {});
19086
19087 let mut cx = EditorTestContext::new(cx).await;
19088 cx.set_head_text(indoc! { "
19089 one
19090 two
19091 three
19092 four
19093 five
19094 "
19095 });
19096 cx.set_index_text(indoc! { "
19097 one
19098 two
19099 three
19100 four
19101 five
19102 "
19103 });
19104 cx.set_state(indoc! {"
19105 one
19106 TWO
19107 ˇTHREE
19108 FOUR
19109 five
19110 "});
19111 cx.run_until_parked();
19112 cx.update_editor(|editor, window, cx| {
19113 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19114 });
19115 cx.run_until_parked();
19116 cx.assert_index_text(Some(indoc! {"
19117 one
19118 TWO
19119 THREE
19120 FOUR
19121 five
19122 "}));
19123 cx.set_state(indoc! { "
19124 one
19125 TWO
19126 ˇTHREE-HUNDRED
19127 FOUR
19128 five
19129 "});
19130 cx.run_until_parked();
19131 cx.update_editor(|editor, window, cx| {
19132 let snapshot = editor.snapshot(window, cx);
19133 let hunks = editor
19134 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19135 .collect::<Vec<_>>();
19136 assert_eq!(hunks.len(), 1);
19137 assert_eq!(
19138 hunks[0].status(),
19139 DiffHunkStatus {
19140 kind: DiffHunkStatusKind::Modified,
19141 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
19142 }
19143 );
19144
19145 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19146 });
19147 cx.run_until_parked();
19148 cx.assert_index_text(Some(indoc! {"
19149 one
19150 TWO
19151 THREE-HUNDRED
19152 FOUR
19153 five
19154 "}));
19155}
19156
19157#[gpui::test]
19158fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
19159 init_test(cx, |_| {});
19160
19161 let editor = cx.add_window(|window, cx| {
19162 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
19163 build_editor(buffer, window, cx)
19164 });
19165
19166 let render_args = Arc::new(Mutex::new(None));
19167 let snapshot = editor
19168 .update(cx, |editor, window, cx| {
19169 let snapshot = editor.buffer().read(cx).snapshot(cx);
19170 let range =
19171 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
19172
19173 struct RenderArgs {
19174 row: MultiBufferRow,
19175 folded: bool,
19176 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
19177 }
19178
19179 let crease = Crease::inline(
19180 range,
19181 FoldPlaceholder::test(),
19182 {
19183 let toggle_callback = render_args.clone();
19184 move |row, folded, callback, _window, _cx| {
19185 *toggle_callback.lock() = Some(RenderArgs {
19186 row,
19187 folded,
19188 callback,
19189 });
19190 div()
19191 }
19192 },
19193 |_row, _folded, _window, _cx| div(),
19194 );
19195
19196 editor.insert_creases(Some(crease), cx);
19197 let snapshot = editor.snapshot(window, cx);
19198 let _div = snapshot.render_crease_toggle(
19199 MultiBufferRow(1),
19200 false,
19201 cx.entity().clone(),
19202 window,
19203 cx,
19204 );
19205 snapshot
19206 })
19207 .unwrap();
19208
19209 let render_args = render_args.lock().take().unwrap();
19210 assert_eq!(render_args.row, MultiBufferRow(1));
19211 assert!(!render_args.folded);
19212 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19213
19214 cx.update_window(*editor, |_, window, cx| {
19215 (render_args.callback)(true, window, cx)
19216 })
19217 .unwrap();
19218 let snapshot = editor
19219 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19220 .unwrap();
19221 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
19222
19223 cx.update_window(*editor, |_, window, cx| {
19224 (render_args.callback)(false, window, cx)
19225 })
19226 .unwrap();
19227 let snapshot = editor
19228 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19229 .unwrap();
19230 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19231}
19232
19233#[gpui::test]
19234async fn test_input_text(cx: &mut TestAppContext) {
19235 init_test(cx, |_| {});
19236 let mut cx = EditorTestContext::new(cx).await;
19237
19238 cx.set_state(
19239 &r#"ˇone
19240 two
19241
19242 three
19243 fourˇ
19244 five
19245
19246 siˇx"#
19247 .unindent(),
19248 );
19249
19250 cx.dispatch_action(HandleInput(String::new()));
19251 cx.assert_editor_state(
19252 &r#"ˇone
19253 two
19254
19255 three
19256 fourˇ
19257 five
19258
19259 siˇx"#
19260 .unindent(),
19261 );
19262
19263 cx.dispatch_action(HandleInput("AAAA".to_string()));
19264 cx.assert_editor_state(
19265 &r#"AAAAˇone
19266 two
19267
19268 three
19269 fourAAAAˇ
19270 five
19271
19272 siAAAAˇx"#
19273 .unindent(),
19274 );
19275}
19276
19277#[gpui::test]
19278async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
19279 init_test(cx, |_| {});
19280
19281 let mut cx = EditorTestContext::new(cx).await;
19282 cx.set_state(
19283 r#"let foo = 1;
19284let foo = 2;
19285let foo = 3;
19286let fooˇ = 4;
19287let foo = 5;
19288let foo = 6;
19289let foo = 7;
19290let foo = 8;
19291let foo = 9;
19292let foo = 10;
19293let foo = 11;
19294let foo = 12;
19295let foo = 13;
19296let foo = 14;
19297let foo = 15;"#,
19298 );
19299
19300 cx.update_editor(|e, window, cx| {
19301 assert_eq!(
19302 e.next_scroll_position,
19303 NextScrollCursorCenterTopBottom::Center,
19304 "Default next scroll direction is center",
19305 );
19306
19307 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19308 assert_eq!(
19309 e.next_scroll_position,
19310 NextScrollCursorCenterTopBottom::Top,
19311 "After center, next scroll direction should be top",
19312 );
19313
19314 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19315 assert_eq!(
19316 e.next_scroll_position,
19317 NextScrollCursorCenterTopBottom::Bottom,
19318 "After top, next scroll direction should be bottom",
19319 );
19320
19321 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19322 assert_eq!(
19323 e.next_scroll_position,
19324 NextScrollCursorCenterTopBottom::Center,
19325 "After bottom, scrolling should start over",
19326 );
19327
19328 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19329 assert_eq!(
19330 e.next_scroll_position,
19331 NextScrollCursorCenterTopBottom::Top,
19332 "Scrolling continues if retriggered fast enough"
19333 );
19334 });
19335
19336 cx.executor()
19337 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
19338 cx.executor().run_until_parked();
19339 cx.update_editor(|e, _, _| {
19340 assert_eq!(
19341 e.next_scroll_position,
19342 NextScrollCursorCenterTopBottom::Center,
19343 "If scrolling is not triggered fast enough, it should reset"
19344 );
19345 });
19346}
19347
19348#[gpui::test]
19349async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
19350 init_test(cx, |_| {});
19351 let mut cx = EditorLspTestContext::new_rust(
19352 lsp::ServerCapabilities {
19353 definition_provider: Some(lsp::OneOf::Left(true)),
19354 references_provider: Some(lsp::OneOf::Left(true)),
19355 ..lsp::ServerCapabilities::default()
19356 },
19357 cx,
19358 )
19359 .await;
19360
19361 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
19362 let go_to_definition = cx
19363 .lsp
19364 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19365 move |params, _| async move {
19366 if empty_go_to_definition {
19367 Ok(None)
19368 } else {
19369 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
19370 uri: params.text_document_position_params.text_document.uri,
19371 range: lsp::Range::new(
19372 lsp::Position::new(4, 3),
19373 lsp::Position::new(4, 6),
19374 ),
19375 })))
19376 }
19377 },
19378 );
19379 let references = cx
19380 .lsp
19381 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
19382 Ok(Some(vec![lsp::Location {
19383 uri: params.text_document_position.text_document.uri,
19384 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
19385 }]))
19386 });
19387 (go_to_definition, references)
19388 };
19389
19390 cx.set_state(
19391 &r#"fn one() {
19392 let mut a = ˇtwo();
19393 }
19394
19395 fn two() {}"#
19396 .unindent(),
19397 );
19398 set_up_lsp_handlers(false, &mut cx);
19399 let navigated = cx
19400 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19401 .await
19402 .expect("Failed to navigate to definition");
19403 assert_eq!(
19404 navigated,
19405 Navigated::Yes,
19406 "Should have navigated to definition from the GetDefinition response"
19407 );
19408 cx.assert_editor_state(
19409 &r#"fn one() {
19410 let mut a = two();
19411 }
19412
19413 fn «twoˇ»() {}"#
19414 .unindent(),
19415 );
19416
19417 let editors = cx.update_workspace(|workspace, _, cx| {
19418 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19419 });
19420 cx.update_editor(|_, _, test_editor_cx| {
19421 assert_eq!(
19422 editors.len(),
19423 1,
19424 "Initially, only one, test, editor should be open in the workspace"
19425 );
19426 assert_eq!(
19427 test_editor_cx.entity(),
19428 editors.last().expect("Asserted len is 1").clone()
19429 );
19430 });
19431
19432 set_up_lsp_handlers(true, &mut cx);
19433 let navigated = cx
19434 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19435 .await
19436 .expect("Failed to navigate to lookup references");
19437 assert_eq!(
19438 navigated,
19439 Navigated::Yes,
19440 "Should have navigated to references as a fallback after empty GoToDefinition response"
19441 );
19442 // We should not change the selections in the existing file,
19443 // if opening another milti buffer with the references
19444 cx.assert_editor_state(
19445 &r#"fn one() {
19446 let mut a = two();
19447 }
19448
19449 fn «twoˇ»() {}"#
19450 .unindent(),
19451 );
19452 let editors = cx.update_workspace(|workspace, _, cx| {
19453 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19454 });
19455 cx.update_editor(|_, _, test_editor_cx| {
19456 assert_eq!(
19457 editors.len(),
19458 2,
19459 "After falling back to references search, we open a new editor with the results"
19460 );
19461 let references_fallback_text = editors
19462 .into_iter()
19463 .find(|new_editor| *new_editor != test_editor_cx.entity())
19464 .expect("Should have one non-test editor now")
19465 .read(test_editor_cx)
19466 .text(test_editor_cx);
19467 assert_eq!(
19468 references_fallback_text, "fn one() {\n let mut a = two();\n}",
19469 "Should use the range from the references response and not the GoToDefinition one"
19470 );
19471 });
19472}
19473
19474#[gpui::test]
19475async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
19476 init_test(cx, |_| {});
19477 cx.update(|cx| {
19478 let mut editor_settings = EditorSettings::get_global(cx).clone();
19479 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
19480 EditorSettings::override_global(editor_settings, cx);
19481 });
19482 let mut cx = EditorLspTestContext::new_rust(
19483 lsp::ServerCapabilities {
19484 definition_provider: Some(lsp::OneOf::Left(true)),
19485 references_provider: Some(lsp::OneOf::Left(true)),
19486 ..lsp::ServerCapabilities::default()
19487 },
19488 cx,
19489 )
19490 .await;
19491 let original_state = r#"fn one() {
19492 let mut a = ˇtwo();
19493 }
19494
19495 fn two() {}"#
19496 .unindent();
19497 cx.set_state(&original_state);
19498
19499 let mut go_to_definition = cx
19500 .lsp
19501 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19502 move |_, _| async move { Ok(None) },
19503 );
19504 let _references = cx
19505 .lsp
19506 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
19507 panic!("Should not call for references with no go to definition fallback")
19508 });
19509
19510 let navigated = cx
19511 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19512 .await
19513 .expect("Failed to navigate to lookup references");
19514 go_to_definition
19515 .next()
19516 .await
19517 .expect("Should have called the go_to_definition handler");
19518
19519 assert_eq!(
19520 navigated,
19521 Navigated::No,
19522 "Should have navigated to references as a fallback after empty GoToDefinition response"
19523 );
19524 cx.assert_editor_state(&original_state);
19525 let editors = cx.update_workspace(|workspace, _, cx| {
19526 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19527 });
19528 cx.update_editor(|_, _, _| {
19529 assert_eq!(
19530 editors.len(),
19531 1,
19532 "After unsuccessful fallback, no other editor should have been opened"
19533 );
19534 });
19535}
19536
19537#[gpui::test]
19538async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
19539 init_test(cx, |_| {});
19540
19541 let language = Arc::new(Language::new(
19542 LanguageConfig::default(),
19543 Some(tree_sitter_rust::LANGUAGE.into()),
19544 ));
19545
19546 let text = r#"
19547 #[cfg(test)]
19548 mod tests() {
19549 #[test]
19550 fn runnable_1() {
19551 let a = 1;
19552 }
19553
19554 #[test]
19555 fn runnable_2() {
19556 let a = 1;
19557 let b = 2;
19558 }
19559 }
19560 "#
19561 .unindent();
19562
19563 let fs = FakeFs::new(cx.executor());
19564 fs.insert_file("/file.rs", Default::default()).await;
19565
19566 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19567 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19568 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19569 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
19570 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
19571
19572 let editor = cx.new_window_entity(|window, cx| {
19573 Editor::new(
19574 EditorMode::full(),
19575 multi_buffer,
19576 Some(project.clone()),
19577 window,
19578 cx,
19579 )
19580 });
19581
19582 editor.update_in(cx, |editor, window, cx| {
19583 let snapshot = editor.buffer().read(cx).snapshot(cx);
19584 editor.tasks.insert(
19585 (buffer.read(cx).remote_id(), 3),
19586 RunnableTasks {
19587 templates: vec![],
19588 offset: snapshot.anchor_before(43),
19589 column: 0,
19590 extra_variables: HashMap::default(),
19591 context_range: BufferOffset(43)..BufferOffset(85),
19592 },
19593 );
19594 editor.tasks.insert(
19595 (buffer.read(cx).remote_id(), 8),
19596 RunnableTasks {
19597 templates: vec![],
19598 offset: snapshot.anchor_before(86),
19599 column: 0,
19600 extra_variables: HashMap::default(),
19601 context_range: BufferOffset(86)..BufferOffset(191),
19602 },
19603 );
19604
19605 // Test finding task when cursor is inside function body
19606 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19607 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
19608 });
19609 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19610 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
19611
19612 // Test finding task when cursor is on function name
19613 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19614 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
19615 });
19616 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19617 assert_eq!(row, 8, "Should find task when cursor is on function name");
19618 });
19619}
19620
19621#[gpui::test]
19622async fn test_folding_buffers(cx: &mut TestAppContext) {
19623 init_test(cx, |_| {});
19624
19625 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19626 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
19627 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
19628
19629 let fs = FakeFs::new(cx.executor());
19630 fs.insert_tree(
19631 path!("/a"),
19632 json!({
19633 "first.rs": sample_text_1,
19634 "second.rs": sample_text_2,
19635 "third.rs": sample_text_3,
19636 }),
19637 )
19638 .await;
19639 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19640 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19641 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19642 let worktree = project.update(cx, |project, cx| {
19643 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19644 assert_eq!(worktrees.len(), 1);
19645 worktrees.pop().unwrap()
19646 });
19647 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19648
19649 let buffer_1 = project
19650 .update(cx, |project, cx| {
19651 project.open_buffer((worktree_id, "first.rs"), cx)
19652 })
19653 .await
19654 .unwrap();
19655 let buffer_2 = project
19656 .update(cx, |project, cx| {
19657 project.open_buffer((worktree_id, "second.rs"), cx)
19658 })
19659 .await
19660 .unwrap();
19661 let buffer_3 = project
19662 .update(cx, |project, cx| {
19663 project.open_buffer((worktree_id, "third.rs"), cx)
19664 })
19665 .await
19666 .unwrap();
19667
19668 let multi_buffer = cx.new(|cx| {
19669 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19670 multi_buffer.push_excerpts(
19671 buffer_1.clone(),
19672 [
19673 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19674 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19675 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19676 ],
19677 cx,
19678 );
19679 multi_buffer.push_excerpts(
19680 buffer_2.clone(),
19681 [
19682 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19683 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19684 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19685 ],
19686 cx,
19687 );
19688 multi_buffer.push_excerpts(
19689 buffer_3.clone(),
19690 [
19691 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19692 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19693 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19694 ],
19695 cx,
19696 );
19697 multi_buffer
19698 });
19699 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19700 Editor::new(
19701 EditorMode::full(),
19702 multi_buffer.clone(),
19703 Some(project.clone()),
19704 window,
19705 cx,
19706 )
19707 });
19708
19709 assert_eq!(
19710 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19711 "\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",
19712 );
19713
19714 multi_buffer_editor.update(cx, |editor, cx| {
19715 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19716 });
19717 assert_eq!(
19718 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19719 "\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",
19720 "After folding the first buffer, its text should not be displayed"
19721 );
19722
19723 multi_buffer_editor.update(cx, |editor, cx| {
19724 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19725 });
19726 assert_eq!(
19727 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19728 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
19729 "After folding the second buffer, its text should not be displayed"
19730 );
19731
19732 multi_buffer_editor.update(cx, |editor, cx| {
19733 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19734 });
19735 assert_eq!(
19736 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19737 "\n\n\n\n\n",
19738 "After folding the third buffer, its text should not be displayed"
19739 );
19740
19741 // Emulate selection inside the fold logic, that should work
19742 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19743 editor
19744 .snapshot(window, cx)
19745 .next_line_boundary(Point::new(0, 4));
19746 });
19747
19748 multi_buffer_editor.update(cx, |editor, cx| {
19749 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19750 });
19751 assert_eq!(
19752 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19753 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19754 "After unfolding the second buffer, its text should be displayed"
19755 );
19756
19757 // Typing inside of buffer 1 causes that buffer to be unfolded.
19758 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19759 assert_eq!(
19760 multi_buffer
19761 .read(cx)
19762 .snapshot(cx)
19763 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
19764 .collect::<String>(),
19765 "bbbb"
19766 );
19767 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
19768 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
19769 });
19770 editor.handle_input("B", window, cx);
19771 });
19772
19773 assert_eq!(
19774 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19775 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19776 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
19777 );
19778
19779 multi_buffer_editor.update(cx, |editor, cx| {
19780 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19781 });
19782 assert_eq!(
19783 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19784 "\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",
19785 "After unfolding the all buffers, all original text should be displayed"
19786 );
19787}
19788
19789#[gpui::test]
19790async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
19791 init_test(cx, |_| {});
19792
19793 let sample_text_1 = "1111\n2222\n3333".to_string();
19794 let sample_text_2 = "4444\n5555\n6666".to_string();
19795 let sample_text_3 = "7777\n8888\n9999".to_string();
19796
19797 let fs = FakeFs::new(cx.executor());
19798 fs.insert_tree(
19799 path!("/a"),
19800 json!({
19801 "first.rs": sample_text_1,
19802 "second.rs": sample_text_2,
19803 "third.rs": sample_text_3,
19804 }),
19805 )
19806 .await;
19807 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19808 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19809 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19810 let worktree = project.update(cx, |project, cx| {
19811 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19812 assert_eq!(worktrees.len(), 1);
19813 worktrees.pop().unwrap()
19814 });
19815 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19816
19817 let buffer_1 = project
19818 .update(cx, |project, cx| {
19819 project.open_buffer((worktree_id, "first.rs"), cx)
19820 })
19821 .await
19822 .unwrap();
19823 let buffer_2 = project
19824 .update(cx, |project, cx| {
19825 project.open_buffer((worktree_id, "second.rs"), cx)
19826 })
19827 .await
19828 .unwrap();
19829 let buffer_3 = project
19830 .update(cx, |project, cx| {
19831 project.open_buffer((worktree_id, "third.rs"), cx)
19832 })
19833 .await
19834 .unwrap();
19835
19836 let multi_buffer = cx.new(|cx| {
19837 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19838 multi_buffer.push_excerpts(
19839 buffer_1.clone(),
19840 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19841 cx,
19842 );
19843 multi_buffer.push_excerpts(
19844 buffer_2.clone(),
19845 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19846 cx,
19847 );
19848 multi_buffer.push_excerpts(
19849 buffer_3.clone(),
19850 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19851 cx,
19852 );
19853 multi_buffer
19854 });
19855
19856 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19857 Editor::new(
19858 EditorMode::full(),
19859 multi_buffer,
19860 Some(project.clone()),
19861 window,
19862 cx,
19863 )
19864 });
19865
19866 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
19867 assert_eq!(
19868 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19869 full_text,
19870 );
19871
19872 multi_buffer_editor.update(cx, |editor, cx| {
19873 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19874 });
19875 assert_eq!(
19876 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19877 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
19878 "After folding the first buffer, its text should not be displayed"
19879 );
19880
19881 multi_buffer_editor.update(cx, |editor, cx| {
19882 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19883 });
19884
19885 assert_eq!(
19886 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19887 "\n\n\n\n\n\n7777\n8888\n9999",
19888 "After folding the second buffer, its text should not be displayed"
19889 );
19890
19891 multi_buffer_editor.update(cx, |editor, cx| {
19892 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19893 });
19894 assert_eq!(
19895 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19896 "\n\n\n\n\n",
19897 "After folding the third buffer, its text should not be displayed"
19898 );
19899
19900 multi_buffer_editor.update(cx, |editor, cx| {
19901 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19902 });
19903 assert_eq!(
19904 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19905 "\n\n\n\n4444\n5555\n6666\n\n",
19906 "After unfolding the second buffer, its text should be displayed"
19907 );
19908
19909 multi_buffer_editor.update(cx, |editor, cx| {
19910 editor.unfold_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\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
19915 "After unfolding the first buffer, its text should be displayed"
19916 );
19917
19918 multi_buffer_editor.update(cx, |editor, cx| {
19919 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19920 });
19921 assert_eq!(
19922 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19923 full_text,
19924 "After unfolding all buffers, all original text should be displayed"
19925 );
19926}
19927
19928#[gpui::test]
19929async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
19930 init_test(cx, |_| {});
19931
19932 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19933
19934 let fs = FakeFs::new(cx.executor());
19935 fs.insert_tree(
19936 path!("/a"),
19937 json!({
19938 "main.rs": sample_text,
19939 }),
19940 )
19941 .await;
19942 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19943 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19944 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19945 let worktree = project.update(cx, |project, cx| {
19946 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19947 assert_eq!(worktrees.len(), 1);
19948 worktrees.pop().unwrap()
19949 });
19950 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19951
19952 let buffer_1 = project
19953 .update(cx, |project, cx| {
19954 project.open_buffer((worktree_id, "main.rs"), cx)
19955 })
19956 .await
19957 .unwrap();
19958
19959 let multi_buffer = cx.new(|cx| {
19960 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19961 multi_buffer.push_excerpts(
19962 buffer_1.clone(),
19963 [ExcerptRange::new(
19964 Point::new(0, 0)
19965 ..Point::new(
19966 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
19967 0,
19968 ),
19969 )],
19970 cx,
19971 );
19972 multi_buffer
19973 });
19974 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19975 Editor::new(
19976 EditorMode::full(),
19977 multi_buffer,
19978 Some(project.clone()),
19979 window,
19980 cx,
19981 )
19982 });
19983
19984 let selection_range = Point::new(1, 0)..Point::new(2, 0);
19985 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19986 enum TestHighlight {}
19987 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
19988 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
19989 editor.highlight_text::<TestHighlight>(
19990 vec![highlight_range.clone()],
19991 HighlightStyle::color(Hsla::green()),
19992 cx,
19993 );
19994 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19995 s.select_ranges(Some(highlight_range))
19996 });
19997 });
19998
19999 let full_text = format!("\n\n{sample_text}");
20000 assert_eq!(
20001 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20002 full_text,
20003 );
20004}
20005
20006#[gpui::test]
20007async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
20008 init_test(cx, |_| {});
20009 cx.update(|cx| {
20010 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
20011 "keymaps/default-linux.json",
20012 cx,
20013 )
20014 .unwrap();
20015 cx.bind_keys(default_key_bindings);
20016 });
20017
20018 let (editor, cx) = cx.add_window_view(|window, cx| {
20019 let multi_buffer = MultiBuffer::build_multi(
20020 [
20021 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
20022 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
20023 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
20024 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
20025 ],
20026 cx,
20027 );
20028 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
20029
20030 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
20031 // fold all but the second buffer, so that we test navigating between two
20032 // adjacent folded buffers, as well as folded buffers at the start and
20033 // end the multibuffer
20034 editor.fold_buffer(buffer_ids[0], cx);
20035 editor.fold_buffer(buffer_ids[2], cx);
20036 editor.fold_buffer(buffer_ids[3], cx);
20037
20038 editor
20039 });
20040 cx.simulate_resize(size(px(1000.), px(1000.)));
20041
20042 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
20043 cx.assert_excerpts_with_selections(indoc! {"
20044 [EXCERPT]
20045 ˇ[FOLDED]
20046 [EXCERPT]
20047 a1
20048 b1
20049 [EXCERPT]
20050 [FOLDED]
20051 [EXCERPT]
20052 [FOLDED]
20053 "
20054 });
20055 cx.simulate_keystroke("down");
20056 cx.assert_excerpts_with_selections(indoc! {"
20057 [EXCERPT]
20058 [FOLDED]
20059 [EXCERPT]
20060 ˇa1
20061 b1
20062 [EXCERPT]
20063 [FOLDED]
20064 [EXCERPT]
20065 [FOLDED]
20066 "
20067 });
20068 cx.simulate_keystroke("down");
20069 cx.assert_excerpts_with_selections(indoc! {"
20070 [EXCERPT]
20071 [FOLDED]
20072 [EXCERPT]
20073 a1
20074 ˇb1
20075 [EXCERPT]
20076 [FOLDED]
20077 [EXCERPT]
20078 [FOLDED]
20079 "
20080 });
20081 cx.simulate_keystroke("down");
20082 cx.assert_excerpts_with_selections(indoc! {"
20083 [EXCERPT]
20084 [FOLDED]
20085 [EXCERPT]
20086 a1
20087 b1
20088 ˇ[EXCERPT]
20089 [FOLDED]
20090 [EXCERPT]
20091 [FOLDED]
20092 "
20093 });
20094 cx.simulate_keystroke("down");
20095 cx.assert_excerpts_with_selections(indoc! {"
20096 [EXCERPT]
20097 [FOLDED]
20098 [EXCERPT]
20099 a1
20100 b1
20101 [EXCERPT]
20102 ˇ[FOLDED]
20103 [EXCERPT]
20104 [FOLDED]
20105 "
20106 });
20107 for _ in 0..5 {
20108 cx.simulate_keystroke("down");
20109 cx.assert_excerpts_with_selections(indoc! {"
20110 [EXCERPT]
20111 [FOLDED]
20112 [EXCERPT]
20113 a1
20114 b1
20115 [EXCERPT]
20116 [FOLDED]
20117 [EXCERPT]
20118 ˇ[FOLDED]
20119 "
20120 });
20121 }
20122
20123 cx.simulate_keystroke("up");
20124 cx.assert_excerpts_with_selections(indoc! {"
20125 [EXCERPT]
20126 [FOLDED]
20127 [EXCERPT]
20128 a1
20129 b1
20130 [EXCERPT]
20131 ˇ[FOLDED]
20132 [EXCERPT]
20133 [FOLDED]
20134 "
20135 });
20136 cx.simulate_keystroke("up");
20137 cx.assert_excerpts_with_selections(indoc! {"
20138 [EXCERPT]
20139 [FOLDED]
20140 [EXCERPT]
20141 a1
20142 b1
20143 ˇ[EXCERPT]
20144 [FOLDED]
20145 [EXCERPT]
20146 [FOLDED]
20147 "
20148 });
20149 cx.simulate_keystroke("up");
20150 cx.assert_excerpts_with_selections(indoc! {"
20151 [EXCERPT]
20152 [FOLDED]
20153 [EXCERPT]
20154 a1
20155 ˇb1
20156 [EXCERPT]
20157 [FOLDED]
20158 [EXCERPT]
20159 [FOLDED]
20160 "
20161 });
20162 cx.simulate_keystroke("up");
20163 cx.assert_excerpts_with_selections(indoc! {"
20164 [EXCERPT]
20165 [FOLDED]
20166 [EXCERPT]
20167 ˇa1
20168 b1
20169 [EXCERPT]
20170 [FOLDED]
20171 [EXCERPT]
20172 [FOLDED]
20173 "
20174 });
20175 for _ in 0..5 {
20176 cx.simulate_keystroke("up");
20177 cx.assert_excerpts_with_selections(indoc! {"
20178 [EXCERPT]
20179 ˇ[FOLDED]
20180 [EXCERPT]
20181 a1
20182 b1
20183 [EXCERPT]
20184 [FOLDED]
20185 [EXCERPT]
20186 [FOLDED]
20187 "
20188 });
20189 }
20190}
20191
20192#[gpui::test]
20193async fn test_inline_completion_text(cx: &mut TestAppContext) {
20194 init_test(cx, |_| {});
20195
20196 // Simple insertion
20197 assert_highlighted_edits(
20198 "Hello, world!",
20199 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
20200 true,
20201 cx,
20202 |highlighted_edits, cx| {
20203 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
20204 assert_eq!(highlighted_edits.highlights.len(), 1);
20205 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
20206 assert_eq!(
20207 highlighted_edits.highlights[0].1.background_color,
20208 Some(cx.theme().status().created_background)
20209 );
20210 },
20211 )
20212 .await;
20213
20214 // Replacement
20215 assert_highlighted_edits(
20216 "This is a test.",
20217 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
20218 false,
20219 cx,
20220 |highlighted_edits, cx| {
20221 assert_eq!(highlighted_edits.text, "That is a test.");
20222 assert_eq!(highlighted_edits.highlights.len(), 1);
20223 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
20224 assert_eq!(
20225 highlighted_edits.highlights[0].1.background_color,
20226 Some(cx.theme().status().created_background)
20227 );
20228 },
20229 )
20230 .await;
20231
20232 // Multiple edits
20233 assert_highlighted_edits(
20234 "Hello, world!",
20235 vec![
20236 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
20237 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
20238 ],
20239 false,
20240 cx,
20241 |highlighted_edits, cx| {
20242 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
20243 assert_eq!(highlighted_edits.highlights.len(), 2);
20244 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
20245 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
20246 assert_eq!(
20247 highlighted_edits.highlights[0].1.background_color,
20248 Some(cx.theme().status().created_background)
20249 );
20250 assert_eq!(
20251 highlighted_edits.highlights[1].1.background_color,
20252 Some(cx.theme().status().created_background)
20253 );
20254 },
20255 )
20256 .await;
20257
20258 // Multiple lines with edits
20259 assert_highlighted_edits(
20260 "First line\nSecond line\nThird line\nFourth line",
20261 vec![
20262 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
20263 (
20264 Point::new(2, 0)..Point::new(2, 10),
20265 "New third line".to_string(),
20266 ),
20267 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
20268 ],
20269 false,
20270 cx,
20271 |highlighted_edits, cx| {
20272 assert_eq!(
20273 highlighted_edits.text,
20274 "Second modified\nNew third line\nFourth updated line"
20275 );
20276 assert_eq!(highlighted_edits.highlights.len(), 3);
20277 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
20278 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
20279 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
20280 for highlight in &highlighted_edits.highlights {
20281 assert_eq!(
20282 highlight.1.background_color,
20283 Some(cx.theme().status().created_background)
20284 );
20285 }
20286 },
20287 )
20288 .await;
20289}
20290
20291#[gpui::test]
20292async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
20293 init_test(cx, |_| {});
20294
20295 // Deletion
20296 assert_highlighted_edits(
20297 "Hello, world!",
20298 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
20299 true,
20300 cx,
20301 |highlighted_edits, cx| {
20302 assert_eq!(highlighted_edits.text, "Hello, world!");
20303 assert_eq!(highlighted_edits.highlights.len(), 1);
20304 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
20305 assert_eq!(
20306 highlighted_edits.highlights[0].1.background_color,
20307 Some(cx.theme().status().deleted_background)
20308 );
20309 },
20310 )
20311 .await;
20312
20313 // Insertion
20314 assert_highlighted_edits(
20315 "Hello, world!",
20316 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
20317 true,
20318 cx,
20319 |highlighted_edits, cx| {
20320 assert_eq!(highlighted_edits.highlights.len(), 1);
20321 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
20322 assert_eq!(
20323 highlighted_edits.highlights[0].1.background_color,
20324 Some(cx.theme().status().created_background)
20325 );
20326 },
20327 )
20328 .await;
20329}
20330
20331async fn assert_highlighted_edits(
20332 text: &str,
20333 edits: Vec<(Range<Point>, String)>,
20334 include_deletions: bool,
20335 cx: &mut TestAppContext,
20336 assertion_fn: impl Fn(HighlightedText, &App),
20337) {
20338 let window = cx.add_window(|window, cx| {
20339 let buffer = MultiBuffer::build_simple(text, cx);
20340 Editor::new(EditorMode::full(), buffer, None, window, cx)
20341 });
20342 let cx = &mut VisualTestContext::from_window(*window, cx);
20343
20344 let (buffer, snapshot) = window
20345 .update(cx, |editor, _window, cx| {
20346 (
20347 editor.buffer().clone(),
20348 editor.buffer().read(cx).snapshot(cx),
20349 )
20350 })
20351 .unwrap();
20352
20353 let edits = edits
20354 .into_iter()
20355 .map(|(range, edit)| {
20356 (
20357 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
20358 edit,
20359 )
20360 })
20361 .collect::<Vec<_>>();
20362
20363 let text_anchor_edits = edits
20364 .clone()
20365 .into_iter()
20366 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
20367 .collect::<Vec<_>>();
20368
20369 let edit_preview = window
20370 .update(cx, |_, _window, cx| {
20371 buffer
20372 .read(cx)
20373 .as_singleton()
20374 .unwrap()
20375 .read(cx)
20376 .preview_edits(text_anchor_edits.into(), cx)
20377 })
20378 .unwrap()
20379 .await;
20380
20381 cx.update(|_window, cx| {
20382 let highlighted_edits = inline_completion_edit_text(
20383 &snapshot.as_singleton().unwrap().2,
20384 &edits,
20385 &edit_preview,
20386 include_deletions,
20387 cx,
20388 );
20389 assertion_fn(highlighted_edits, cx)
20390 });
20391}
20392
20393#[track_caller]
20394fn assert_breakpoint(
20395 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
20396 path: &Arc<Path>,
20397 expected: Vec<(u32, Breakpoint)>,
20398) {
20399 if expected.len() == 0usize {
20400 assert!(!breakpoints.contains_key(path), "{}", path.display());
20401 } else {
20402 let mut breakpoint = breakpoints
20403 .get(path)
20404 .unwrap()
20405 .into_iter()
20406 .map(|breakpoint| {
20407 (
20408 breakpoint.row,
20409 Breakpoint {
20410 message: breakpoint.message.clone(),
20411 state: breakpoint.state,
20412 condition: breakpoint.condition.clone(),
20413 hit_condition: breakpoint.hit_condition.clone(),
20414 },
20415 )
20416 })
20417 .collect::<Vec<_>>();
20418
20419 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
20420
20421 assert_eq!(expected, breakpoint);
20422 }
20423}
20424
20425fn add_log_breakpoint_at_cursor(
20426 editor: &mut Editor,
20427 log_message: &str,
20428 window: &mut Window,
20429 cx: &mut Context<Editor>,
20430) {
20431 let (anchor, bp) = editor
20432 .breakpoints_at_cursors(window, cx)
20433 .first()
20434 .and_then(|(anchor, bp)| {
20435 if let Some(bp) = bp {
20436 Some((*anchor, bp.clone()))
20437 } else {
20438 None
20439 }
20440 })
20441 .unwrap_or_else(|| {
20442 let cursor_position: Point = editor.selections.newest(cx).head();
20443
20444 let breakpoint_position = editor
20445 .snapshot(window, cx)
20446 .display_snapshot
20447 .buffer_snapshot
20448 .anchor_before(Point::new(cursor_position.row, 0));
20449
20450 (breakpoint_position, Breakpoint::new_log(&log_message))
20451 });
20452
20453 editor.edit_breakpoint_at_anchor(
20454 anchor,
20455 bp,
20456 BreakpointEditAction::EditLogMessage(log_message.into()),
20457 cx,
20458 );
20459}
20460
20461#[gpui::test]
20462async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
20463 init_test(cx, |_| {});
20464
20465 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20466 let fs = FakeFs::new(cx.executor());
20467 fs.insert_tree(
20468 path!("/a"),
20469 json!({
20470 "main.rs": sample_text,
20471 }),
20472 )
20473 .await;
20474 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20475 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20476 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20477
20478 let fs = FakeFs::new(cx.executor());
20479 fs.insert_tree(
20480 path!("/a"),
20481 json!({
20482 "main.rs": sample_text,
20483 }),
20484 )
20485 .await;
20486 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20487 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20488 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20489 let worktree_id = workspace
20490 .update(cx, |workspace, _window, cx| {
20491 workspace.project().update(cx, |project, cx| {
20492 project.worktrees(cx).next().unwrap().read(cx).id()
20493 })
20494 })
20495 .unwrap();
20496
20497 let buffer = project
20498 .update(cx, |project, cx| {
20499 project.open_buffer((worktree_id, "main.rs"), cx)
20500 })
20501 .await
20502 .unwrap();
20503
20504 let (editor, cx) = cx.add_window_view(|window, cx| {
20505 Editor::new(
20506 EditorMode::full(),
20507 MultiBuffer::build_from_buffer(buffer, cx),
20508 Some(project.clone()),
20509 window,
20510 cx,
20511 )
20512 });
20513
20514 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20515 let abs_path = project.read_with(cx, |project, cx| {
20516 project
20517 .absolute_path(&project_path, cx)
20518 .map(|path_buf| Arc::from(path_buf.to_owned()))
20519 .unwrap()
20520 });
20521
20522 // assert we can add breakpoint on the first line
20523 editor.update_in(cx, |editor, window, cx| {
20524 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20525 editor.move_to_end(&MoveToEnd, window, cx);
20526 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20527 });
20528
20529 let breakpoints = editor.update(cx, |editor, cx| {
20530 editor
20531 .breakpoint_store()
20532 .as_ref()
20533 .unwrap()
20534 .read(cx)
20535 .all_source_breakpoints(cx)
20536 .clone()
20537 });
20538
20539 assert_eq!(1, breakpoints.len());
20540 assert_breakpoint(
20541 &breakpoints,
20542 &abs_path,
20543 vec![
20544 (0, Breakpoint::new_standard()),
20545 (3, Breakpoint::new_standard()),
20546 ],
20547 );
20548
20549 editor.update_in(cx, |editor, window, cx| {
20550 editor.move_to_beginning(&MoveToBeginning, window, cx);
20551 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20552 });
20553
20554 let breakpoints = editor.update(cx, |editor, cx| {
20555 editor
20556 .breakpoint_store()
20557 .as_ref()
20558 .unwrap()
20559 .read(cx)
20560 .all_source_breakpoints(cx)
20561 .clone()
20562 });
20563
20564 assert_eq!(1, breakpoints.len());
20565 assert_breakpoint(
20566 &breakpoints,
20567 &abs_path,
20568 vec![(3, Breakpoint::new_standard())],
20569 );
20570
20571 editor.update_in(cx, |editor, window, cx| {
20572 editor.move_to_end(&MoveToEnd, window, cx);
20573 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20574 });
20575
20576 let breakpoints = editor.update(cx, |editor, cx| {
20577 editor
20578 .breakpoint_store()
20579 .as_ref()
20580 .unwrap()
20581 .read(cx)
20582 .all_source_breakpoints(cx)
20583 .clone()
20584 });
20585
20586 assert_eq!(0, breakpoints.len());
20587 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20588}
20589
20590#[gpui::test]
20591async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
20592 init_test(cx, |_| {});
20593
20594 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20595
20596 let fs = FakeFs::new(cx.executor());
20597 fs.insert_tree(
20598 path!("/a"),
20599 json!({
20600 "main.rs": sample_text,
20601 }),
20602 )
20603 .await;
20604 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20605 let (workspace, cx) =
20606 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20607
20608 let worktree_id = workspace.update(cx, |workspace, cx| {
20609 workspace.project().update(cx, |project, cx| {
20610 project.worktrees(cx).next().unwrap().read(cx).id()
20611 })
20612 });
20613
20614 let buffer = project
20615 .update(cx, |project, cx| {
20616 project.open_buffer((worktree_id, "main.rs"), cx)
20617 })
20618 .await
20619 .unwrap();
20620
20621 let (editor, cx) = cx.add_window_view(|window, cx| {
20622 Editor::new(
20623 EditorMode::full(),
20624 MultiBuffer::build_from_buffer(buffer, cx),
20625 Some(project.clone()),
20626 window,
20627 cx,
20628 )
20629 });
20630
20631 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20632 let abs_path = project.read_with(cx, |project, cx| {
20633 project
20634 .absolute_path(&project_path, cx)
20635 .map(|path_buf| Arc::from(path_buf.to_owned()))
20636 .unwrap()
20637 });
20638
20639 editor.update_in(cx, |editor, window, cx| {
20640 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20641 });
20642
20643 let breakpoints = editor.update(cx, |editor, cx| {
20644 editor
20645 .breakpoint_store()
20646 .as_ref()
20647 .unwrap()
20648 .read(cx)
20649 .all_source_breakpoints(cx)
20650 .clone()
20651 });
20652
20653 assert_breakpoint(
20654 &breakpoints,
20655 &abs_path,
20656 vec![(0, Breakpoint::new_log("hello world"))],
20657 );
20658
20659 // Removing a log message from a log breakpoint should remove it
20660 editor.update_in(cx, |editor, window, cx| {
20661 add_log_breakpoint_at_cursor(editor, "", window, cx);
20662 });
20663
20664 let breakpoints = editor.update(cx, |editor, cx| {
20665 editor
20666 .breakpoint_store()
20667 .as_ref()
20668 .unwrap()
20669 .read(cx)
20670 .all_source_breakpoints(cx)
20671 .clone()
20672 });
20673
20674 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20675
20676 editor.update_in(cx, |editor, window, cx| {
20677 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20678 editor.move_to_end(&MoveToEnd, window, cx);
20679 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20680 // Not adding a log message to a standard breakpoint shouldn't remove it
20681 add_log_breakpoint_at_cursor(editor, "", window, cx);
20682 });
20683
20684 let breakpoints = editor.update(cx, |editor, cx| {
20685 editor
20686 .breakpoint_store()
20687 .as_ref()
20688 .unwrap()
20689 .read(cx)
20690 .all_source_breakpoints(cx)
20691 .clone()
20692 });
20693
20694 assert_breakpoint(
20695 &breakpoints,
20696 &abs_path,
20697 vec![
20698 (0, Breakpoint::new_standard()),
20699 (3, Breakpoint::new_standard()),
20700 ],
20701 );
20702
20703 editor.update_in(cx, |editor, window, cx| {
20704 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20705 });
20706
20707 let breakpoints = editor.update(cx, |editor, cx| {
20708 editor
20709 .breakpoint_store()
20710 .as_ref()
20711 .unwrap()
20712 .read(cx)
20713 .all_source_breakpoints(cx)
20714 .clone()
20715 });
20716
20717 assert_breakpoint(
20718 &breakpoints,
20719 &abs_path,
20720 vec![
20721 (0, Breakpoint::new_standard()),
20722 (3, Breakpoint::new_log("hello world")),
20723 ],
20724 );
20725
20726 editor.update_in(cx, |editor, window, cx| {
20727 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
20728 });
20729
20730 let breakpoints = editor.update(cx, |editor, cx| {
20731 editor
20732 .breakpoint_store()
20733 .as_ref()
20734 .unwrap()
20735 .read(cx)
20736 .all_source_breakpoints(cx)
20737 .clone()
20738 });
20739
20740 assert_breakpoint(
20741 &breakpoints,
20742 &abs_path,
20743 vec![
20744 (0, Breakpoint::new_standard()),
20745 (3, Breakpoint::new_log("hello Earth!!")),
20746 ],
20747 );
20748}
20749
20750/// This also tests that Editor::breakpoint_at_cursor_head is working properly
20751/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
20752/// or when breakpoints were placed out of order. This tests for a regression too
20753#[gpui::test]
20754async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
20755 init_test(cx, |_| {});
20756
20757 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20758 let fs = FakeFs::new(cx.executor());
20759 fs.insert_tree(
20760 path!("/a"),
20761 json!({
20762 "main.rs": sample_text,
20763 }),
20764 )
20765 .await;
20766 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20767 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20768 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20769
20770 let fs = FakeFs::new(cx.executor());
20771 fs.insert_tree(
20772 path!("/a"),
20773 json!({
20774 "main.rs": sample_text,
20775 }),
20776 )
20777 .await;
20778 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20779 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20780 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20781 let worktree_id = workspace
20782 .update(cx, |workspace, _window, cx| {
20783 workspace.project().update(cx, |project, cx| {
20784 project.worktrees(cx).next().unwrap().read(cx).id()
20785 })
20786 })
20787 .unwrap();
20788
20789 let buffer = project
20790 .update(cx, |project, cx| {
20791 project.open_buffer((worktree_id, "main.rs"), cx)
20792 })
20793 .await
20794 .unwrap();
20795
20796 let (editor, cx) = cx.add_window_view(|window, cx| {
20797 Editor::new(
20798 EditorMode::full(),
20799 MultiBuffer::build_from_buffer(buffer, cx),
20800 Some(project.clone()),
20801 window,
20802 cx,
20803 )
20804 });
20805
20806 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20807 let abs_path = project.read_with(cx, |project, cx| {
20808 project
20809 .absolute_path(&project_path, cx)
20810 .map(|path_buf| Arc::from(path_buf.to_owned()))
20811 .unwrap()
20812 });
20813
20814 // assert we can add breakpoint on the first line
20815 editor.update_in(cx, |editor, window, cx| {
20816 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20817 editor.move_to_end(&MoveToEnd, window, cx);
20818 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20819 editor.move_up(&MoveUp, window, cx);
20820 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20821 });
20822
20823 let breakpoints = editor.update(cx, |editor, cx| {
20824 editor
20825 .breakpoint_store()
20826 .as_ref()
20827 .unwrap()
20828 .read(cx)
20829 .all_source_breakpoints(cx)
20830 .clone()
20831 });
20832
20833 assert_eq!(1, breakpoints.len());
20834 assert_breakpoint(
20835 &breakpoints,
20836 &abs_path,
20837 vec![
20838 (0, Breakpoint::new_standard()),
20839 (2, Breakpoint::new_standard()),
20840 (3, Breakpoint::new_standard()),
20841 ],
20842 );
20843
20844 editor.update_in(cx, |editor, window, cx| {
20845 editor.move_to_beginning(&MoveToBeginning, window, cx);
20846 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20847 editor.move_to_end(&MoveToEnd, window, cx);
20848 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20849 // Disabling a breakpoint that doesn't exist should do nothing
20850 editor.move_up(&MoveUp, window, cx);
20851 editor.move_up(&MoveUp, window, cx);
20852 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20853 });
20854
20855 let breakpoints = editor.update(cx, |editor, cx| {
20856 editor
20857 .breakpoint_store()
20858 .as_ref()
20859 .unwrap()
20860 .read(cx)
20861 .all_source_breakpoints(cx)
20862 .clone()
20863 });
20864
20865 let disable_breakpoint = {
20866 let mut bp = Breakpoint::new_standard();
20867 bp.state = BreakpointState::Disabled;
20868 bp
20869 };
20870
20871 assert_eq!(1, breakpoints.len());
20872 assert_breakpoint(
20873 &breakpoints,
20874 &abs_path,
20875 vec![
20876 (0, disable_breakpoint.clone()),
20877 (2, Breakpoint::new_standard()),
20878 (3, disable_breakpoint.clone()),
20879 ],
20880 );
20881
20882 editor.update_in(cx, |editor, window, cx| {
20883 editor.move_to_beginning(&MoveToBeginning, window, cx);
20884 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20885 editor.move_to_end(&MoveToEnd, window, cx);
20886 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
20887 editor.move_up(&MoveUp, window, cx);
20888 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20889 });
20890
20891 let breakpoints = editor.update(cx, |editor, cx| {
20892 editor
20893 .breakpoint_store()
20894 .as_ref()
20895 .unwrap()
20896 .read(cx)
20897 .all_source_breakpoints(cx)
20898 .clone()
20899 });
20900
20901 assert_eq!(1, breakpoints.len());
20902 assert_breakpoint(
20903 &breakpoints,
20904 &abs_path,
20905 vec![
20906 (0, Breakpoint::new_standard()),
20907 (2, disable_breakpoint),
20908 (3, Breakpoint::new_standard()),
20909 ],
20910 );
20911}
20912
20913#[gpui::test]
20914async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
20915 init_test(cx, |_| {});
20916 let capabilities = lsp::ServerCapabilities {
20917 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
20918 prepare_provider: Some(true),
20919 work_done_progress_options: Default::default(),
20920 })),
20921 ..Default::default()
20922 };
20923 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
20924
20925 cx.set_state(indoc! {"
20926 struct Fˇoo {}
20927 "});
20928
20929 cx.update_editor(|editor, _, cx| {
20930 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
20931 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
20932 editor.highlight_background::<DocumentHighlightRead>(
20933 &[highlight_range],
20934 |theme| theme.colors().editor_document_highlight_read_background,
20935 cx,
20936 );
20937 });
20938
20939 let mut prepare_rename_handler = cx
20940 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
20941 move |_, _, _| async move {
20942 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
20943 start: lsp::Position {
20944 line: 0,
20945 character: 7,
20946 },
20947 end: lsp::Position {
20948 line: 0,
20949 character: 10,
20950 },
20951 })))
20952 },
20953 );
20954 let prepare_rename_task = cx
20955 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
20956 .expect("Prepare rename was not started");
20957 prepare_rename_handler.next().await.unwrap();
20958 prepare_rename_task.await.expect("Prepare rename failed");
20959
20960 let mut rename_handler =
20961 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
20962 let edit = lsp::TextEdit {
20963 range: lsp::Range {
20964 start: lsp::Position {
20965 line: 0,
20966 character: 7,
20967 },
20968 end: lsp::Position {
20969 line: 0,
20970 character: 10,
20971 },
20972 },
20973 new_text: "FooRenamed".to_string(),
20974 };
20975 Ok(Some(lsp::WorkspaceEdit::new(
20976 // Specify the same edit twice
20977 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
20978 )))
20979 });
20980 let rename_task = cx
20981 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
20982 .expect("Confirm rename was not started");
20983 rename_handler.next().await.unwrap();
20984 rename_task.await.expect("Confirm rename failed");
20985 cx.run_until_parked();
20986
20987 // Despite two edits, only one is actually applied as those are identical
20988 cx.assert_editor_state(indoc! {"
20989 struct FooRenamedˇ {}
20990 "});
20991}
20992
20993#[gpui::test]
20994async fn test_rename_without_prepare(cx: &mut TestAppContext) {
20995 init_test(cx, |_| {});
20996 // These capabilities indicate that the server does not support prepare rename.
20997 let capabilities = lsp::ServerCapabilities {
20998 rename_provider: Some(lsp::OneOf::Left(true)),
20999 ..Default::default()
21000 };
21001 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21002
21003 cx.set_state(indoc! {"
21004 struct Fˇoo {}
21005 "});
21006
21007 cx.update_editor(|editor, _window, cx| {
21008 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21009 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21010 editor.highlight_background::<DocumentHighlightRead>(
21011 &[highlight_range],
21012 |theme| theme.colors().editor_document_highlight_read_background,
21013 cx,
21014 );
21015 });
21016
21017 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21018 .expect("Prepare rename was not started")
21019 .await
21020 .expect("Prepare rename failed");
21021
21022 let mut rename_handler =
21023 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21024 let edit = lsp::TextEdit {
21025 range: lsp::Range {
21026 start: lsp::Position {
21027 line: 0,
21028 character: 7,
21029 },
21030 end: lsp::Position {
21031 line: 0,
21032 character: 10,
21033 },
21034 },
21035 new_text: "FooRenamed".to_string(),
21036 };
21037 Ok(Some(lsp::WorkspaceEdit::new(
21038 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
21039 )))
21040 });
21041 let rename_task = cx
21042 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21043 .expect("Confirm rename was not started");
21044 rename_handler.next().await.unwrap();
21045 rename_task.await.expect("Confirm rename failed");
21046 cx.run_until_parked();
21047
21048 // Correct range is renamed, as `surrounding_word` is used to find it.
21049 cx.assert_editor_state(indoc! {"
21050 struct FooRenamedˇ {}
21051 "});
21052}
21053
21054#[gpui::test]
21055async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
21056 init_test(cx, |_| {});
21057 let mut cx = EditorTestContext::new(cx).await;
21058
21059 let language = Arc::new(
21060 Language::new(
21061 LanguageConfig::default(),
21062 Some(tree_sitter_html::LANGUAGE.into()),
21063 )
21064 .with_brackets_query(
21065 r#"
21066 ("<" @open "/>" @close)
21067 ("</" @open ">" @close)
21068 ("<" @open ">" @close)
21069 ("\"" @open "\"" @close)
21070 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
21071 "#,
21072 )
21073 .unwrap(),
21074 );
21075 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21076
21077 cx.set_state(indoc! {"
21078 <span>ˇ</span>
21079 "});
21080 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21081 cx.assert_editor_state(indoc! {"
21082 <span>
21083 ˇ
21084 </span>
21085 "});
21086
21087 cx.set_state(indoc! {"
21088 <span><span></span>ˇ</span>
21089 "});
21090 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21091 cx.assert_editor_state(indoc! {"
21092 <span><span></span>
21093 ˇ</span>
21094 "});
21095
21096 cx.set_state(indoc! {"
21097 <span>ˇ
21098 </span>
21099 "});
21100 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21101 cx.assert_editor_state(indoc! {"
21102 <span>
21103 ˇ
21104 </span>
21105 "});
21106}
21107
21108#[gpui::test(iterations = 10)]
21109async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
21110 init_test(cx, |_| {});
21111
21112 let fs = FakeFs::new(cx.executor());
21113 fs.insert_tree(
21114 path!("/dir"),
21115 json!({
21116 "a.ts": "a",
21117 }),
21118 )
21119 .await;
21120
21121 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
21122 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21123 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21124
21125 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21126 language_registry.add(Arc::new(Language::new(
21127 LanguageConfig {
21128 name: "TypeScript".into(),
21129 matcher: LanguageMatcher {
21130 path_suffixes: vec!["ts".to_string()],
21131 ..Default::default()
21132 },
21133 ..Default::default()
21134 },
21135 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
21136 )));
21137 let mut fake_language_servers = language_registry.register_fake_lsp(
21138 "TypeScript",
21139 FakeLspAdapter {
21140 capabilities: lsp::ServerCapabilities {
21141 code_lens_provider: Some(lsp::CodeLensOptions {
21142 resolve_provider: Some(true),
21143 }),
21144 execute_command_provider: Some(lsp::ExecuteCommandOptions {
21145 commands: vec!["_the/command".to_string()],
21146 ..lsp::ExecuteCommandOptions::default()
21147 }),
21148 ..lsp::ServerCapabilities::default()
21149 },
21150 ..FakeLspAdapter::default()
21151 },
21152 );
21153
21154 let (buffer, _handle) = project
21155 .update(cx, |p, cx| {
21156 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
21157 })
21158 .await
21159 .unwrap();
21160 cx.executor().run_until_parked();
21161
21162 let fake_server = fake_language_servers.next().await.unwrap();
21163
21164 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
21165 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
21166 drop(buffer_snapshot);
21167 let actions = cx
21168 .update_window(*workspace, |_, window, cx| {
21169 project.code_actions(&buffer, anchor..anchor, window, cx)
21170 })
21171 .unwrap();
21172
21173 fake_server
21174 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21175 Ok(Some(vec![
21176 lsp::CodeLens {
21177 range: lsp::Range::default(),
21178 command: Some(lsp::Command {
21179 title: "Code lens command".to_owned(),
21180 command: "_the/command".to_owned(),
21181 arguments: None,
21182 }),
21183 data: None,
21184 },
21185 lsp::CodeLens {
21186 range: lsp::Range::default(),
21187 command: Some(lsp::Command {
21188 title: "Command not in capabilities".to_owned(),
21189 command: "not in capabilities".to_owned(),
21190 arguments: None,
21191 }),
21192 data: None,
21193 },
21194 lsp::CodeLens {
21195 range: lsp::Range {
21196 start: lsp::Position {
21197 line: 1,
21198 character: 1,
21199 },
21200 end: lsp::Position {
21201 line: 1,
21202 character: 1,
21203 },
21204 },
21205 command: Some(lsp::Command {
21206 title: "Command not in range".to_owned(),
21207 command: "_the/command".to_owned(),
21208 arguments: None,
21209 }),
21210 data: None,
21211 },
21212 ]))
21213 })
21214 .next()
21215 .await;
21216
21217 let actions = actions.await.unwrap();
21218 assert_eq!(
21219 actions.len(),
21220 1,
21221 "Should have only one valid action for the 0..0 range"
21222 );
21223 let action = actions[0].clone();
21224 let apply = project.update(cx, |project, cx| {
21225 project.apply_code_action(buffer.clone(), action, true, cx)
21226 });
21227
21228 // Resolving the code action does not populate its edits. In absence of
21229 // edits, we must execute the given command.
21230 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
21231 |mut lens, _| async move {
21232 let lens_command = lens.command.as_mut().expect("should have a command");
21233 assert_eq!(lens_command.title, "Code lens command");
21234 lens_command.arguments = Some(vec![json!("the-argument")]);
21235 Ok(lens)
21236 },
21237 );
21238
21239 // While executing the command, the language server sends the editor
21240 // a `workspaceEdit` request.
21241 fake_server
21242 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
21243 let fake = fake_server.clone();
21244 move |params, _| {
21245 assert_eq!(params.command, "_the/command");
21246 let fake = fake.clone();
21247 async move {
21248 fake.server
21249 .request::<lsp::request::ApplyWorkspaceEdit>(
21250 lsp::ApplyWorkspaceEditParams {
21251 label: None,
21252 edit: lsp::WorkspaceEdit {
21253 changes: Some(
21254 [(
21255 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
21256 vec![lsp::TextEdit {
21257 range: lsp::Range::new(
21258 lsp::Position::new(0, 0),
21259 lsp::Position::new(0, 0),
21260 ),
21261 new_text: "X".into(),
21262 }],
21263 )]
21264 .into_iter()
21265 .collect(),
21266 ),
21267 ..Default::default()
21268 },
21269 },
21270 )
21271 .await
21272 .into_response()
21273 .unwrap();
21274 Ok(Some(json!(null)))
21275 }
21276 }
21277 })
21278 .next()
21279 .await;
21280
21281 // Applying the code lens command returns a project transaction containing the edits
21282 // sent by the language server in its `workspaceEdit` request.
21283 let transaction = apply.await.unwrap();
21284 assert!(transaction.0.contains_key(&buffer));
21285 buffer.update(cx, |buffer, cx| {
21286 assert_eq!(buffer.text(), "Xa");
21287 buffer.undo(cx);
21288 assert_eq!(buffer.text(), "a");
21289 });
21290}
21291
21292#[gpui::test]
21293async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
21294 init_test(cx, |_| {});
21295
21296 let fs = FakeFs::new(cx.executor());
21297 let main_text = r#"fn main() {
21298println!("1");
21299println!("2");
21300println!("3");
21301println!("4");
21302println!("5");
21303}"#;
21304 let lib_text = "mod foo {}";
21305 fs.insert_tree(
21306 path!("/a"),
21307 json!({
21308 "lib.rs": lib_text,
21309 "main.rs": main_text,
21310 }),
21311 )
21312 .await;
21313
21314 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21315 let (workspace, cx) =
21316 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21317 let worktree_id = workspace.update(cx, |workspace, cx| {
21318 workspace.project().update(cx, |project, cx| {
21319 project.worktrees(cx).next().unwrap().read(cx).id()
21320 })
21321 });
21322
21323 let expected_ranges = vec![
21324 Point::new(0, 0)..Point::new(0, 0),
21325 Point::new(1, 0)..Point::new(1, 1),
21326 Point::new(2, 0)..Point::new(2, 2),
21327 Point::new(3, 0)..Point::new(3, 3),
21328 ];
21329
21330 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21331 let editor_1 = workspace
21332 .update_in(cx, |workspace, window, cx| {
21333 workspace.open_path(
21334 (worktree_id, "main.rs"),
21335 Some(pane_1.downgrade()),
21336 true,
21337 window,
21338 cx,
21339 )
21340 })
21341 .unwrap()
21342 .await
21343 .downcast::<Editor>()
21344 .unwrap();
21345 pane_1.update(cx, |pane, cx| {
21346 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21347 open_editor.update(cx, |editor, cx| {
21348 assert_eq!(
21349 editor.display_text(cx),
21350 main_text,
21351 "Original main.rs text on initial open",
21352 );
21353 assert_eq!(
21354 editor
21355 .selections
21356 .all::<Point>(cx)
21357 .into_iter()
21358 .map(|s| s.range())
21359 .collect::<Vec<_>>(),
21360 vec![Point::zero()..Point::zero()],
21361 "Default selections on initial open",
21362 );
21363 })
21364 });
21365 editor_1.update_in(cx, |editor, window, cx| {
21366 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21367 s.select_ranges(expected_ranges.clone());
21368 });
21369 });
21370
21371 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
21372 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
21373 });
21374 let editor_2 = workspace
21375 .update_in(cx, |workspace, window, cx| {
21376 workspace.open_path(
21377 (worktree_id, "main.rs"),
21378 Some(pane_2.downgrade()),
21379 true,
21380 window,
21381 cx,
21382 )
21383 })
21384 .unwrap()
21385 .await
21386 .downcast::<Editor>()
21387 .unwrap();
21388 pane_2.update(cx, |pane, cx| {
21389 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21390 open_editor.update(cx, |editor, cx| {
21391 assert_eq!(
21392 editor.display_text(cx),
21393 main_text,
21394 "Original main.rs text on initial open in another panel",
21395 );
21396 assert_eq!(
21397 editor
21398 .selections
21399 .all::<Point>(cx)
21400 .into_iter()
21401 .map(|s| s.range())
21402 .collect::<Vec<_>>(),
21403 vec![Point::zero()..Point::zero()],
21404 "Default selections on initial open in another panel",
21405 );
21406 })
21407 });
21408
21409 editor_2.update_in(cx, |editor, window, cx| {
21410 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
21411 });
21412
21413 let _other_editor_1 = workspace
21414 .update_in(cx, |workspace, window, cx| {
21415 workspace.open_path(
21416 (worktree_id, "lib.rs"),
21417 Some(pane_1.downgrade()),
21418 true,
21419 window,
21420 cx,
21421 )
21422 })
21423 .unwrap()
21424 .await
21425 .downcast::<Editor>()
21426 .unwrap();
21427 pane_1
21428 .update_in(cx, |pane, window, cx| {
21429 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
21430 })
21431 .await
21432 .unwrap();
21433 drop(editor_1);
21434 pane_1.update(cx, |pane, cx| {
21435 pane.active_item()
21436 .unwrap()
21437 .downcast::<Editor>()
21438 .unwrap()
21439 .update(cx, |editor, cx| {
21440 assert_eq!(
21441 editor.display_text(cx),
21442 lib_text,
21443 "Other file should be open and active",
21444 );
21445 });
21446 assert_eq!(pane.items().count(), 1, "No other editors should be open");
21447 });
21448
21449 let _other_editor_2 = workspace
21450 .update_in(cx, |workspace, window, cx| {
21451 workspace.open_path(
21452 (worktree_id, "lib.rs"),
21453 Some(pane_2.downgrade()),
21454 true,
21455 window,
21456 cx,
21457 )
21458 })
21459 .unwrap()
21460 .await
21461 .downcast::<Editor>()
21462 .unwrap();
21463 pane_2
21464 .update_in(cx, |pane, window, cx| {
21465 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
21466 })
21467 .await
21468 .unwrap();
21469 drop(editor_2);
21470 pane_2.update(cx, |pane, cx| {
21471 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21472 open_editor.update(cx, |editor, cx| {
21473 assert_eq!(
21474 editor.display_text(cx),
21475 lib_text,
21476 "Other file should be open and active in another panel too",
21477 );
21478 });
21479 assert_eq!(
21480 pane.items().count(),
21481 1,
21482 "No other editors should be open in another pane",
21483 );
21484 });
21485
21486 let _editor_1_reopened = workspace
21487 .update_in(cx, |workspace, window, cx| {
21488 workspace.open_path(
21489 (worktree_id, "main.rs"),
21490 Some(pane_1.downgrade()),
21491 true,
21492 window,
21493 cx,
21494 )
21495 })
21496 .unwrap()
21497 .await
21498 .downcast::<Editor>()
21499 .unwrap();
21500 let _editor_2_reopened = workspace
21501 .update_in(cx, |workspace, window, cx| {
21502 workspace.open_path(
21503 (worktree_id, "main.rs"),
21504 Some(pane_2.downgrade()),
21505 true,
21506 window,
21507 cx,
21508 )
21509 })
21510 .unwrap()
21511 .await
21512 .downcast::<Editor>()
21513 .unwrap();
21514 pane_1.update(cx, |pane, cx| {
21515 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21516 open_editor.update(cx, |editor, cx| {
21517 assert_eq!(
21518 editor.display_text(cx),
21519 main_text,
21520 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
21521 );
21522 assert_eq!(
21523 editor
21524 .selections
21525 .all::<Point>(cx)
21526 .into_iter()
21527 .map(|s| s.range())
21528 .collect::<Vec<_>>(),
21529 expected_ranges,
21530 "Previous editor in the 1st panel had selections and should get them restored on reopen",
21531 );
21532 })
21533 });
21534 pane_2.update(cx, |pane, cx| {
21535 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21536 open_editor.update(cx, |editor, cx| {
21537 assert_eq!(
21538 editor.display_text(cx),
21539 r#"fn main() {
21540⋯rintln!("1");
21541⋯intln!("2");
21542⋯ntln!("3");
21543println!("4");
21544println!("5");
21545}"#,
21546 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
21547 );
21548 assert_eq!(
21549 editor
21550 .selections
21551 .all::<Point>(cx)
21552 .into_iter()
21553 .map(|s| s.range())
21554 .collect::<Vec<_>>(),
21555 vec![Point::zero()..Point::zero()],
21556 "Previous editor in the 2nd pane had no selections changed hence should restore none",
21557 );
21558 })
21559 });
21560}
21561
21562#[gpui::test]
21563async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
21564 init_test(cx, |_| {});
21565
21566 let fs = FakeFs::new(cx.executor());
21567 let main_text = r#"fn main() {
21568println!("1");
21569println!("2");
21570println!("3");
21571println!("4");
21572println!("5");
21573}"#;
21574 let lib_text = "mod foo {}";
21575 fs.insert_tree(
21576 path!("/a"),
21577 json!({
21578 "lib.rs": lib_text,
21579 "main.rs": main_text,
21580 }),
21581 )
21582 .await;
21583
21584 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21585 let (workspace, cx) =
21586 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21587 let worktree_id = workspace.update(cx, |workspace, cx| {
21588 workspace.project().update(cx, |project, cx| {
21589 project.worktrees(cx).next().unwrap().read(cx).id()
21590 })
21591 });
21592
21593 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21594 let editor = workspace
21595 .update_in(cx, |workspace, window, cx| {
21596 workspace.open_path(
21597 (worktree_id, "main.rs"),
21598 Some(pane.downgrade()),
21599 true,
21600 window,
21601 cx,
21602 )
21603 })
21604 .unwrap()
21605 .await
21606 .downcast::<Editor>()
21607 .unwrap();
21608 pane.update(cx, |pane, cx| {
21609 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21610 open_editor.update(cx, |editor, cx| {
21611 assert_eq!(
21612 editor.display_text(cx),
21613 main_text,
21614 "Original main.rs text on initial open",
21615 );
21616 })
21617 });
21618 editor.update_in(cx, |editor, window, cx| {
21619 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
21620 });
21621
21622 cx.update_global(|store: &mut SettingsStore, cx| {
21623 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21624 s.restore_on_file_reopen = Some(false);
21625 });
21626 });
21627 editor.update_in(cx, |editor, window, cx| {
21628 editor.fold_ranges(
21629 vec![
21630 Point::new(1, 0)..Point::new(1, 1),
21631 Point::new(2, 0)..Point::new(2, 2),
21632 Point::new(3, 0)..Point::new(3, 3),
21633 ],
21634 false,
21635 window,
21636 cx,
21637 );
21638 });
21639 pane.update_in(cx, |pane, window, cx| {
21640 pane.close_all_items(&CloseAllItems::default(), window, cx)
21641 })
21642 .await
21643 .unwrap();
21644 pane.update(cx, |pane, _| {
21645 assert!(pane.active_item().is_none());
21646 });
21647 cx.update_global(|store: &mut SettingsStore, cx| {
21648 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21649 s.restore_on_file_reopen = Some(true);
21650 });
21651 });
21652
21653 let _editor_reopened = workspace
21654 .update_in(cx, |workspace, window, cx| {
21655 workspace.open_path(
21656 (worktree_id, "main.rs"),
21657 Some(pane.downgrade()),
21658 true,
21659 window,
21660 cx,
21661 )
21662 })
21663 .unwrap()
21664 .await
21665 .downcast::<Editor>()
21666 .unwrap();
21667 pane.update(cx, |pane, cx| {
21668 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21669 open_editor.update(cx, |editor, cx| {
21670 assert_eq!(
21671 editor.display_text(cx),
21672 main_text,
21673 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
21674 );
21675 })
21676 });
21677}
21678
21679#[gpui::test]
21680async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
21681 struct EmptyModalView {
21682 focus_handle: gpui::FocusHandle,
21683 }
21684 impl EventEmitter<DismissEvent> for EmptyModalView {}
21685 impl Render for EmptyModalView {
21686 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
21687 div()
21688 }
21689 }
21690 impl Focusable for EmptyModalView {
21691 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
21692 self.focus_handle.clone()
21693 }
21694 }
21695 impl workspace::ModalView for EmptyModalView {}
21696 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
21697 EmptyModalView {
21698 focus_handle: cx.focus_handle(),
21699 }
21700 }
21701
21702 init_test(cx, |_| {});
21703
21704 let fs = FakeFs::new(cx.executor());
21705 let project = Project::test(fs, [], cx).await;
21706 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21707 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
21708 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21709 let editor = cx.new_window_entity(|window, cx| {
21710 Editor::new(
21711 EditorMode::full(),
21712 buffer,
21713 Some(project.clone()),
21714 window,
21715 cx,
21716 )
21717 });
21718 workspace
21719 .update(cx, |workspace, window, cx| {
21720 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
21721 })
21722 .unwrap();
21723 editor.update_in(cx, |editor, window, cx| {
21724 editor.open_context_menu(&OpenContextMenu, window, cx);
21725 assert!(editor.mouse_context_menu.is_some());
21726 });
21727 workspace
21728 .update(cx, |workspace, window, cx| {
21729 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
21730 })
21731 .unwrap();
21732 cx.read(|cx| {
21733 assert!(editor.read(cx).mouse_context_menu.is_none());
21734 });
21735}
21736
21737#[gpui::test]
21738async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
21739 init_test(cx, |_| {});
21740
21741 let fs = FakeFs::new(cx.executor());
21742 fs.insert_file(path!("/file.html"), Default::default())
21743 .await;
21744
21745 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
21746
21747 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21748 let html_language = Arc::new(Language::new(
21749 LanguageConfig {
21750 name: "HTML".into(),
21751 matcher: LanguageMatcher {
21752 path_suffixes: vec!["html".to_string()],
21753 ..LanguageMatcher::default()
21754 },
21755 brackets: BracketPairConfig {
21756 pairs: vec![BracketPair {
21757 start: "<".into(),
21758 end: ">".into(),
21759 close: true,
21760 ..Default::default()
21761 }],
21762 ..Default::default()
21763 },
21764 ..Default::default()
21765 },
21766 Some(tree_sitter_html::LANGUAGE.into()),
21767 ));
21768 language_registry.add(html_language);
21769 let mut fake_servers = language_registry.register_fake_lsp(
21770 "HTML",
21771 FakeLspAdapter {
21772 capabilities: lsp::ServerCapabilities {
21773 completion_provider: Some(lsp::CompletionOptions {
21774 resolve_provider: Some(true),
21775 ..Default::default()
21776 }),
21777 ..Default::default()
21778 },
21779 ..Default::default()
21780 },
21781 );
21782
21783 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21784 let cx = &mut VisualTestContext::from_window(*workspace, cx);
21785
21786 let worktree_id = workspace
21787 .update(cx, |workspace, _window, cx| {
21788 workspace.project().update(cx, |project, cx| {
21789 project.worktrees(cx).next().unwrap().read(cx).id()
21790 })
21791 })
21792 .unwrap();
21793 project
21794 .update(cx, |project, cx| {
21795 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
21796 })
21797 .await
21798 .unwrap();
21799 let editor = workspace
21800 .update(cx, |workspace, window, cx| {
21801 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
21802 })
21803 .unwrap()
21804 .await
21805 .unwrap()
21806 .downcast::<Editor>()
21807 .unwrap();
21808
21809 let fake_server = fake_servers.next().await.unwrap();
21810 editor.update_in(cx, |editor, window, cx| {
21811 editor.set_text("<ad></ad>", window, cx);
21812 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21813 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
21814 });
21815 let Some((buffer, _)) = editor
21816 .buffer
21817 .read(cx)
21818 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
21819 else {
21820 panic!("Failed to get buffer for selection position");
21821 };
21822 let buffer = buffer.read(cx);
21823 let buffer_id = buffer.remote_id();
21824 let opening_range =
21825 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
21826 let closing_range =
21827 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
21828 let mut linked_ranges = HashMap::default();
21829 linked_ranges.insert(
21830 buffer_id,
21831 vec![(opening_range.clone(), vec![closing_range.clone()])],
21832 );
21833 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
21834 });
21835 let mut completion_handle =
21836 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
21837 Ok(Some(lsp::CompletionResponse::Array(vec![
21838 lsp::CompletionItem {
21839 label: "head".to_string(),
21840 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21841 lsp::InsertReplaceEdit {
21842 new_text: "head".to_string(),
21843 insert: lsp::Range::new(
21844 lsp::Position::new(0, 1),
21845 lsp::Position::new(0, 3),
21846 ),
21847 replace: lsp::Range::new(
21848 lsp::Position::new(0, 1),
21849 lsp::Position::new(0, 3),
21850 ),
21851 },
21852 )),
21853 ..Default::default()
21854 },
21855 ])))
21856 });
21857 editor.update_in(cx, |editor, window, cx| {
21858 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
21859 });
21860 cx.run_until_parked();
21861 completion_handle.next().await.unwrap();
21862 editor.update(cx, |editor, _| {
21863 assert!(
21864 editor.context_menu_visible(),
21865 "Completion menu should be visible"
21866 );
21867 });
21868 editor.update_in(cx, |editor, window, cx| {
21869 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
21870 });
21871 cx.executor().run_until_parked();
21872 editor.update(cx, |editor, cx| {
21873 assert_eq!(editor.text(cx), "<head></head>");
21874 });
21875}
21876
21877#[gpui::test]
21878async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
21879 init_test(cx, |_| {});
21880
21881 let fs = FakeFs::new(cx.executor());
21882 fs.insert_tree(
21883 path!("/root"),
21884 json!({
21885 "a": {
21886 "main.rs": "fn main() {}",
21887 },
21888 "foo": {
21889 "bar": {
21890 "external_file.rs": "pub mod external {}",
21891 }
21892 }
21893 }),
21894 )
21895 .await;
21896
21897 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
21898 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21899 language_registry.add(rust_lang());
21900 let _fake_servers = language_registry.register_fake_lsp(
21901 "Rust",
21902 FakeLspAdapter {
21903 ..FakeLspAdapter::default()
21904 },
21905 );
21906 let (workspace, cx) =
21907 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21908 let worktree_id = workspace.update(cx, |workspace, cx| {
21909 workspace.project().update(cx, |project, cx| {
21910 project.worktrees(cx).next().unwrap().read(cx).id()
21911 })
21912 });
21913
21914 let assert_language_servers_count =
21915 |expected: usize, context: &str, cx: &mut VisualTestContext| {
21916 project.update(cx, |project, cx| {
21917 let current = project
21918 .lsp_store()
21919 .read(cx)
21920 .as_local()
21921 .unwrap()
21922 .language_servers
21923 .len();
21924 assert_eq!(expected, current, "{context}");
21925 });
21926 };
21927
21928 assert_language_servers_count(
21929 0,
21930 "No servers should be running before any file is open",
21931 cx,
21932 );
21933 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21934 let main_editor = workspace
21935 .update_in(cx, |workspace, window, cx| {
21936 workspace.open_path(
21937 (worktree_id, "main.rs"),
21938 Some(pane.downgrade()),
21939 true,
21940 window,
21941 cx,
21942 )
21943 })
21944 .unwrap()
21945 .await
21946 .downcast::<Editor>()
21947 .unwrap();
21948 pane.update(cx, |pane, cx| {
21949 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21950 open_editor.update(cx, |editor, cx| {
21951 assert_eq!(
21952 editor.display_text(cx),
21953 "fn main() {}",
21954 "Original main.rs text on initial open",
21955 );
21956 });
21957 assert_eq!(open_editor, main_editor);
21958 });
21959 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
21960
21961 let external_editor = workspace
21962 .update_in(cx, |workspace, window, cx| {
21963 workspace.open_abs_path(
21964 PathBuf::from("/root/foo/bar/external_file.rs"),
21965 OpenOptions::default(),
21966 window,
21967 cx,
21968 )
21969 })
21970 .await
21971 .expect("opening external file")
21972 .downcast::<Editor>()
21973 .expect("downcasted external file's open element to editor");
21974 pane.update(cx, |pane, cx| {
21975 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21976 open_editor.update(cx, |editor, cx| {
21977 assert_eq!(
21978 editor.display_text(cx),
21979 "pub mod external {}",
21980 "External file is open now",
21981 );
21982 });
21983 assert_eq!(open_editor, external_editor);
21984 });
21985 assert_language_servers_count(
21986 1,
21987 "Second, external, *.rs file should join the existing server",
21988 cx,
21989 );
21990
21991 pane.update_in(cx, |pane, window, cx| {
21992 pane.close_active_item(&CloseActiveItem::default(), window, cx)
21993 })
21994 .await
21995 .unwrap();
21996 pane.update_in(cx, |pane, window, cx| {
21997 pane.navigate_backward(window, cx);
21998 });
21999 cx.run_until_parked();
22000 pane.update(cx, |pane, cx| {
22001 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22002 open_editor.update(cx, |editor, cx| {
22003 assert_eq!(
22004 editor.display_text(cx),
22005 "pub mod external {}",
22006 "External file is open now",
22007 );
22008 });
22009 });
22010 assert_language_servers_count(
22011 1,
22012 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
22013 cx,
22014 );
22015
22016 cx.update(|_, cx| {
22017 workspace::reload(&workspace::Reload::default(), cx);
22018 });
22019 assert_language_servers_count(
22020 1,
22021 "After reloading the worktree with local and external files opened, only one project should be started",
22022 cx,
22023 );
22024}
22025
22026#[gpui::test]
22027async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
22028 init_test(cx, |_| {});
22029
22030 let mut cx = EditorTestContext::new(cx).await;
22031 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22032 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22033
22034 // test cursor move to start of each line on tab
22035 // for `if`, `elif`, `else`, `while`, `with` and `for`
22036 cx.set_state(indoc! {"
22037 def main():
22038 ˇ for item in items:
22039 ˇ while item.active:
22040 ˇ if item.value > 10:
22041 ˇ continue
22042 ˇ elif item.value < 0:
22043 ˇ break
22044 ˇ else:
22045 ˇ with item.context() as ctx:
22046 ˇ yield count
22047 ˇ else:
22048 ˇ log('while else')
22049 ˇ else:
22050 ˇ log('for else')
22051 "});
22052 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22053 cx.assert_editor_state(indoc! {"
22054 def main():
22055 ˇfor item in items:
22056 ˇwhile item.active:
22057 ˇif item.value > 10:
22058 ˇcontinue
22059 ˇelif item.value < 0:
22060 ˇbreak
22061 ˇelse:
22062 ˇwith item.context() as ctx:
22063 ˇyield count
22064 ˇelse:
22065 ˇlog('while else')
22066 ˇelse:
22067 ˇlog('for else')
22068 "});
22069 // test relative indent is preserved when tab
22070 // for `if`, `elif`, `else`, `while`, `with` and `for`
22071 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22072 cx.assert_editor_state(indoc! {"
22073 def main():
22074 ˇfor item in items:
22075 ˇwhile item.active:
22076 ˇif item.value > 10:
22077 ˇcontinue
22078 ˇelif item.value < 0:
22079 ˇbreak
22080 ˇelse:
22081 ˇwith item.context() as ctx:
22082 ˇyield count
22083 ˇelse:
22084 ˇlog('while else')
22085 ˇelse:
22086 ˇlog('for else')
22087 "});
22088
22089 // test cursor move to start of each line on tab
22090 // for `try`, `except`, `else`, `finally`, `match` and `def`
22091 cx.set_state(indoc! {"
22092 def main():
22093 ˇ try:
22094 ˇ fetch()
22095 ˇ except ValueError:
22096 ˇ handle_error()
22097 ˇ else:
22098 ˇ match value:
22099 ˇ case _:
22100 ˇ finally:
22101 ˇ def status():
22102 ˇ return 0
22103 "});
22104 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22105 cx.assert_editor_state(indoc! {"
22106 def main():
22107 ˇtry:
22108 ˇfetch()
22109 ˇexcept ValueError:
22110 ˇhandle_error()
22111 ˇelse:
22112 ˇmatch value:
22113 ˇcase _:
22114 ˇfinally:
22115 ˇdef status():
22116 ˇreturn 0
22117 "});
22118 // test relative indent is preserved when tab
22119 // for `try`, `except`, `else`, `finally`, `match` and `def`
22120 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22121 cx.assert_editor_state(indoc! {"
22122 def main():
22123 ˇtry:
22124 ˇfetch()
22125 ˇexcept ValueError:
22126 ˇhandle_error()
22127 ˇelse:
22128 ˇmatch value:
22129 ˇcase _:
22130 ˇfinally:
22131 ˇdef status():
22132 ˇreturn 0
22133 "});
22134}
22135
22136#[gpui::test]
22137async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
22138 init_test(cx, |_| {});
22139
22140 let mut cx = EditorTestContext::new(cx).await;
22141 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22142 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22143
22144 // test `else` auto outdents when typed inside `if` block
22145 cx.set_state(indoc! {"
22146 def main():
22147 if i == 2:
22148 return
22149 ˇ
22150 "});
22151 cx.update_editor(|editor, window, cx| {
22152 editor.handle_input("else:", window, cx);
22153 });
22154 cx.assert_editor_state(indoc! {"
22155 def main():
22156 if i == 2:
22157 return
22158 else:ˇ
22159 "});
22160
22161 // test `except` auto outdents when typed inside `try` block
22162 cx.set_state(indoc! {"
22163 def main():
22164 try:
22165 i = 2
22166 ˇ
22167 "});
22168 cx.update_editor(|editor, window, cx| {
22169 editor.handle_input("except:", window, cx);
22170 });
22171 cx.assert_editor_state(indoc! {"
22172 def main():
22173 try:
22174 i = 2
22175 except:ˇ
22176 "});
22177
22178 // test `else` auto outdents when typed inside `except` block
22179 cx.set_state(indoc! {"
22180 def main():
22181 try:
22182 i = 2
22183 except:
22184 j = 2
22185 ˇ
22186 "});
22187 cx.update_editor(|editor, window, cx| {
22188 editor.handle_input("else:", window, cx);
22189 });
22190 cx.assert_editor_state(indoc! {"
22191 def main():
22192 try:
22193 i = 2
22194 except:
22195 j = 2
22196 else:ˇ
22197 "});
22198
22199 // test `finally` auto outdents when typed inside `else` block
22200 cx.set_state(indoc! {"
22201 def main():
22202 try:
22203 i = 2
22204 except:
22205 j = 2
22206 else:
22207 k = 2
22208 ˇ
22209 "});
22210 cx.update_editor(|editor, window, cx| {
22211 editor.handle_input("finally:", window, cx);
22212 });
22213 cx.assert_editor_state(indoc! {"
22214 def main():
22215 try:
22216 i = 2
22217 except:
22218 j = 2
22219 else:
22220 k = 2
22221 finally:ˇ
22222 "});
22223
22224 // test `else` does not outdents when typed inside `except` block right after for block
22225 cx.set_state(indoc! {"
22226 def main():
22227 try:
22228 i = 2
22229 except:
22230 for i in range(n):
22231 pass
22232 ˇ
22233 "});
22234 cx.update_editor(|editor, window, cx| {
22235 editor.handle_input("else:", window, cx);
22236 });
22237 cx.assert_editor_state(indoc! {"
22238 def main():
22239 try:
22240 i = 2
22241 except:
22242 for i in range(n):
22243 pass
22244 else:ˇ
22245 "});
22246
22247 // test `finally` auto outdents when typed inside `else` block right after for block
22248 cx.set_state(indoc! {"
22249 def main():
22250 try:
22251 i = 2
22252 except:
22253 j = 2
22254 else:
22255 for i in range(n):
22256 pass
22257 ˇ
22258 "});
22259 cx.update_editor(|editor, window, cx| {
22260 editor.handle_input("finally:", window, cx);
22261 });
22262 cx.assert_editor_state(indoc! {"
22263 def main():
22264 try:
22265 i = 2
22266 except:
22267 j = 2
22268 else:
22269 for i in range(n):
22270 pass
22271 finally:ˇ
22272 "});
22273
22274 // test `except` outdents to inner "try" block
22275 cx.set_state(indoc! {"
22276 def main():
22277 try:
22278 i = 2
22279 if i == 2:
22280 try:
22281 i = 3
22282 ˇ
22283 "});
22284 cx.update_editor(|editor, window, cx| {
22285 editor.handle_input("except:", window, cx);
22286 });
22287 cx.assert_editor_state(indoc! {"
22288 def main():
22289 try:
22290 i = 2
22291 if i == 2:
22292 try:
22293 i = 3
22294 except:ˇ
22295 "});
22296
22297 // test `except` outdents to outer "try" block
22298 cx.set_state(indoc! {"
22299 def main():
22300 try:
22301 i = 2
22302 if i == 2:
22303 try:
22304 i = 3
22305 ˇ
22306 "});
22307 cx.update_editor(|editor, window, cx| {
22308 editor.handle_input("except:", window, cx);
22309 });
22310 cx.assert_editor_state(indoc! {"
22311 def main():
22312 try:
22313 i = 2
22314 if i == 2:
22315 try:
22316 i = 3
22317 except:ˇ
22318 "});
22319
22320 // test `else` stays at correct indent when typed after `for` block
22321 cx.set_state(indoc! {"
22322 def main():
22323 for i in range(10):
22324 if i == 3:
22325 break
22326 ˇ
22327 "});
22328 cx.update_editor(|editor, window, cx| {
22329 editor.handle_input("else:", window, cx);
22330 });
22331 cx.assert_editor_state(indoc! {"
22332 def main():
22333 for i in range(10):
22334 if i == 3:
22335 break
22336 else:ˇ
22337 "});
22338
22339 // test does not outdent on typing after line with square brackets
22340 cx.set_state(indoc! {"
22341 def f() -> list[str]:
22342 ˇ
22343 "});
22344 cx.update_editor(|editor, window, cx| {
22345 editor.handle_input("a", window, cx);
22346 });
22347 cx.assert_editor_state(indoc! {"
22348 def f() -> list[str]:
22349 aˇ
22350 "});
22351
22352 // test does not outdent on typing : after case keyword
22353 cx.set_state(indoc! {"
22354 match 1:
22355 caseˇ
22356 "});
22357 cx.update_editor(|editor, window, cx| {
22358 editor.handle_input(":", window, cx);
22359 });
22360 cx.assert_editor_state(indoc! {"
22361 match 1:
22362 case:ˇ
22363 "});
22364}
22365
22366#[gpui::test]
22367async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
22368 init_test(cx, |_| {});
22369 update_test_language_settings(cx, |settings| {
22370 settings.defaults.extend_comment_on_newline = Some(false);
22371 });
22372 let mut cx = EditorTestContext::new(cx).await;
22373 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22374 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22375
22376 // test correct indent after newline on comment
22377 cx.set_state(indoc! {"
22378 # COMMENT:ˇ
22379 "});
22380 cx.update_editor(|editor, window, cx| {
22381 editor.newline(&Newline, window, cx);
22382 });
22383 cx.assert_editor_state(indoc! {"
22384 # COMMENT:
22385 ˇ
22386 "});
22387
22388 // test correct indent after newline in brackets
22389 cx.set_state(indoc! {"
22390 {ˇ}
22391 "});
22392 cx.update_editor(|editor, window, cx| {
22393 editor.newline(&Newline, window, cx);
22394 });
22395 cx.run_until_parked();
22396 cx.assert_editor_state(indoc! {"
22397 {
22398 ˇ
22399 }
22400 "});
22401
22402 cx.set_state(indoc! {"
22403 (ˇ)
22404 "});
22405 cx.update_editor(|editor, window, cx| {
22406 editor.newline(&Newline, window, cx);
22407 });
22408 cx.run_until_parked();
22409 cx.assert_editor_state(indoc! {"
22410 (
22411 ˇ
22412 )
22413 "});
22414
22415 // do not indent after empty lists or dictionaries
22416 cx.set_state(indoc! {"
22417 a = []ˇ
22418 "});
22419 cx.update_editor(|editor, window, cx| {
22420 editor.newline(&Newline, window, cx);
22421 });
22422 cx.run_until_parked();
22423 cx.assert_editor_state(indoc! {"
22424 a = []
22425 ˇ
22426 "});
22427}
22428
22429fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
22430 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
22431 point..point
22432}
22433
22434#[track_caller]
22435fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
22436 let (text, ranges) = marked_text_ranges(marked_text, true);
22437 assert_eq!(editor.text(cx), text);
22438 assert_eq!(
22439 editor.selections.ranges(cx),
22440 ranges,
22441 "Assert selections are {}",
22442 marked_text
22443 );
22444}
22445
22446pub fn handle_signature_help_request(
22447 cx: &mut EditorLspTestContext,
22448 mocked_response: lsp::SignatureHelp,
22449) -> impl Future<Output = ()> + use<> {
22450 let mut request =
22451 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
22452 let mocked_response = mocked_response.clone();
22453 async move { Ok(Some(mocked_response)) }
22454 });
22455
22456 async move {
22457 request.next().await;
22458 }
22459}
22460
22461#[track_caller]
22462pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
22463 cx.update_editor(|editor, _, _| {
22464 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
22465 let entries = menu.entries.borrow();
22466 let entries = entries
22467 .iter()
22468 .map(|entry| entry.string.as_str())
22469 .collect::<Vec<_>>();
22470 assert_eq!(entries, expected);
22471 } else {
22472 panic!("Expected completions menu");
22473 }
22474 });
22475}
22476
22477/// Handle completion request passing a marked string specifying where the completion
22478/// should be triggered from using '|' character, what range should be replaced, and what completions
22479/// should be returned using '<' and '>' to delimit the range.
22480///
22481/// Also see `handle_completion_request_with_insert_and_replace`.
22482#[track_caller]
22483pub fn handle_completion_request(
22484 marked_string: &str,
22485 completions: Vec<&'static str>,
22486 is_incomplete: bool,
22487 counter: Arc<AtomicUsize>,
22488 cx: &mut EditorLspTestContext,
22489) -> impl Future<Output = ()> {
22490 let complete_from_marker: TextRangeMarker = '|'.into();
22491 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22492 let (_, mut marked_ranges) = marked_text_ranges_by(
22493 marked_string,
22494 vec![complete_from_marker.clone(), replace_range_marker.clone()],
22495 );
22496
22497 let complete_from_position =
22498 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22499 let replace_range =
22500 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22501
22502 let mut request =
22503 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22504 let completions = completions.clone();
22505 counter.fetch_add(1, atomic::Ordering::Release);
22506 async move {
22507 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22508 assert_eq!(
22509 params.text_document_position.position,
22510 complete_from_position
22511 );
22512 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
22513 is_incomplete: is_incomplete,
22514 item_defaults: None,
22515 items: completions
22516 .iter()
22517 .map(|completion_text| lsp::CompletionItem {
22518 label: completion_text.to_string(),
22519 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
22520 range: replace_range,
22521 new_text: completion_text.to_string(),
22522 })),
22523 ..Default::default()
22524 })
22525 .collect(),
22526 })))
22527 }
22528 });
22529
22530 async move {
22531 request.next().await;
22532 }
22533}
22534
22535/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
22536/// given instead, which also contains an `insert` range.
22537///
22538/// This function uses markers to define ranges:
22539/// - `|` marks the cursor position
22540/// - `<>` marks the replace range
22541/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
22542pub fn handle_completion_request_with_insert_and_replace(
22543 cx: &mut EditorLspTestContext,
22544 marked_string: &str,
22545 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
22546 counter: Arc<AtomicUsize>,
22547) -> impl Future<Output = ()> {
22548 let complete_from_marker: TextRangeMarker = '|'.into();
22549 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22550 let insert_range_marker: TextRangeMarker = ('{', '}').into();
22551
22552 let (_, mut marked_ranges) = marked_text_ranges_by(
22553 marked_string,
22554 vec![
22555 complete_from_marker.clone(),
22556 replace_range_marker.clone(),
22557 insert_range_marker.clone(),
22558 ],
22559 );
22560
22561 let complete_from_position =
22562 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22563 let replace_range =
22564 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22565
22566 let insert_range = match marked_ranges.remove(&insert_range_marker) {
22567 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
22568 _ => lsp::Range {
22569 start: replace_range.start,
22570 end: complete_from_position,
22571 },
22572 };
22573
22574 let mut request =
22575 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22576 let completions = completions.clone();
22577 counter.fetch_add(1, atomic::Ordering::Release);
22578 async move {
22579 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22580 assert_eq!(
22581 params.text_document_position.position, complete_from_position,
22582 "marker `|` position doesn't match",
22583 );
22584 Ok(Some(lsp::CompletionResponse::Array(
22585 completions
22586 .iter()
22587 .map(|(label, new_text)| lsp::CompletionItem {
22588 label: label.to_string(),
22589 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22590 lsp::InsertReplaceEdit {
22591 insert: insert_range,
22592 replace: replace_range,
22593 new_text: new_text.to_string(),
22594 },
22595 )),
22596 ..Default::default()
22597 })
22598 .collect(),
22599 )))
22600 }
22601 });
22602
22603 async move {
22604 request.next().await;
22605 }
22606}
22607
22608fn handle_resolve_completion_request(
22609 cx: &mut EditorLspTestContext,
22610 edits: Option<Vec<(&'static str, &'static str)>>,
22611) -> impl Future<Output = ()> {
22612 let edits = edits.map(|edits| {
22613 edits
22614 .iter()
22615 .map(|(marked_string, new_text)| {
22616 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
22617 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
22618 lsp::TextEdit::new(replace_range, new_text.to_string())
22619 })
22620 .collect::<Vec<_>>()
22621 });
22622
22623 let mut request =
22624 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
22625 let edits = edits.clone();
22626 async move {
22627 Ok(lsp::CompletionItem {
22628 additional_text_edits: edits,
22629 ..Default::default()
22630 })
22631 }
22632 });
22633
22634 async move {
22635 request.next().await;
22636 }
22637}
22638
22639pub(crate) fn update_test_language_settings(
22640 cx: &mut TestAppContext,
22641 f: impl Fn(&mut AllLanguageSettingsContent),
22642) {
22643 cx.update(|cx| {
22644 SettingsStore::update_global(cx, |store, cx| {
22645 store.update_user_settings::<AllLanguageSettings>(cx, f);
22646 });
22647 });
22648}
22649
22650pub(crate) fn update_test_project_settings(
22651 cx: &mut TestAppContext,
22652 f: impl Fn(&mut ProjectSettings),
22653) {
22654 cx.update(|cx| {
22655 SettingsStore::update_global(cx, |store, cx| {
22656 store.update_user_settings::<ProjectSettings>(cx, f);
22657 });
22658 });
22659}
22660
22661pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
22662 cx.update(|cx| {
22663 assets::Assets.load_test_fonts(cx);
22664 let store = SettingsStore::test(cx);
22665 cx.set_global(store);
22666 theme::init(theme::LoadThemes::JustBase, cx);
22667 release_channel::init(SemanticVersion::default(), cx);
22668 client::init_settings(cx);
22669 language::init(cx);
22670 Project::init_settings(cx);
22671 workspace::init_settings(cx);
22672 crate::init(cx);
22673 });
22674
22675 update_test_language_settings(cx, f);
22676}
22677
22678#[track_caller]
22679fn assert_hunk_revert(
22680 not_reverted_text_with_selections: &str,
22681 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
22682 expected_reverted_text_with_selections: &str,
22683 base_text: &str,
22684 cx: &mut EditorLspTestContext,
22685) {
22686 cx.set_state(not_reverted_text_with_selections);
22687 cx.set_head_text(base_text);
22688 cx.executor().run_until_parked();
22689
22690 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
22691 let snapshot = editor.snapshot(window, cx);
22692 let reverted_hunk_statuses = snapshot
22693 .buffer_snapshot
22694 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
22695 .map(|hunk| hunk.status().kind)
22696 .collect::<Vec<_>>();
22697
22698 editor.git_restore(&Default::default(), window, cx);
22699 reverted_hunk_statuses
22700 });
22701 cx.executor().run_until_parked();
22702 cx.assert_editor_state(expected_reverted_text_with_selections);
22703 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
22704}
22705
22706#[gpui::test(iterations = 10)]
22707async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
22708 init_test(cx, |_| {});
22709
22710 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
22711 let counter = diagnostic_requests.clone();
22712
22713 let fs = FakeFs::new(cx.executor());
22714 fs.insert_tree(
22715 path!("/a"),
22716 json!({
22717 "first.rs": "fn main() { let a = 5; }",
22718 "second.rs": "// Test file",
22719 }),
22720 )
22721 .await;
22722
22723 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22724 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22725 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22726
22727 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22728 language_registry.add(rust_lang());
22729 let mut fake_servers = language_registry.register_fake_lsp(
22730 "Rust",
22731 FakeLspAdapter {
22732 capabilities: lsp::ServerCapabilities {
22733 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
22734 lsp::DiagnosticOptions {
22735 identifier: None,
22736 inter_file_dependencies: true,
22737 workspace_diagnostics: true,
22738 work_done_progress_options: Default::default(),
22739 },
22740 )),
22741 ..Default::default()
22742 },
22743 ..Default::default()
22744 },
22745 );
22746
22747 let editor = workspace
22748 .update(cx, |workspace, window, cx| {
22749 workspace.open_abs_path(
22750 PathBuf::from(path!("/a/first.rs")),
22751 OpenOptions::default(),
22752 window,
22753 cx,
22754 )
22755 })
22756 .unwrap()
22757 .await
22758 .unwrap()
22759 .downcast::<Editor>()
22760 .unwrap();
22761 let fake_server = fake_servers.next().await.unwrap();
22762 let server_id = fake_server.server.server_id();
22763 let mut first_request = fake_server
22764 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
22765 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
22766 let result_id = Some(new_result_id.to_string());
22767 assert_eq!(
22768 params.text_document.uri,
22769 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
22770 );
22771 async move {
22772 Ok(lsp::DocumentDiagnosticReportResult::Report(
22773 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
22774 related_documents: None,
22775 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
22776 items: Vec::new(),
22777 result_id,
22778 },
22779 }),
22780 ))
22781 }
22782 });
22783
22784 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
22785 project.update(cx, |project, cx| {
22786 let buffer_id = editor
22787 .read(cx)
22788 .buffer()
22789 .read(cx)
22790 .as_singleton()
22791 .expect("created a singleton buffer")
22792 .read(cx)
22793 .remote_id();
22794 let buffer_result_id = project
22795 .lsp_store()
22796 .read(cx)
22797 .result_id(server_id, buffer_id, cx);
22798 assert_eq!(expected, buffer_result_id);
22799 });
22800 };
22801
22802 ensure_result_id(None, cx);
22803 cx.executor().advance_clock(Duration::from_millis(60));
22804 cx.executor().run_until_parked();
22805 assert_eq!(
22806 diagnostic_requests.load(atomic::Ordering::Acquire),
22807 1,
22808 "Opening file should trigger diagnostic request"
22809 );
22810 first_request
22811 .next()
22812 .await
22813 .expect("should have sent the first diagnostics pull request");
22814 ensure_result_id(Some("1".to_string()), cx);
22815
22816 // Editing should trigger diagnostics
22817 editor.update_in(cx, |editor, window, cx| {
22818 editor.handle_input("2", window, cx)
22819 });
22820 cx.executor().advance_clock(Duration::from_millis(60));
22821 cx.executor().run_until_parked();
22822 assert_eq!(
22823 diagnostic_requests.load(atomic::Ordering::Acquire),
22824 2,
22825 "Editing should trigger diagnostic request"
22826 );
22827 ensure_result_id(Some("2".to_string()), cx);
22828
22829 // Moving cursor should not trigger diagnostic request
22830 editor.update_in(cx, |editor, window, cx| {
22831 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22832 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
22833 });
22834 });
22835 cx.executor().advance_clock(Duration::from_millis(60));
22836 cx.executor().run_until_parked();
22837 assert_eq!(
22838 diagnostic_requests.load(atomic::Ordering::Acquire),
22839 2,
22840 "Cursor movement should not trigger diagnostic request"
22841 );
22842 ensure_result_id(Some("2".to_string()), cx);
22843 // Multiple rapid edits should be debounced
22844 for _ in 0..5 {
22845 editor.update_in(cx, |editor, window, cx| {
22846 editor.handle_input("x", window, cx)
22847 });
22848 }
22849 cx.executor().advance_clock(Duration::from_millis(60));
22850 cx.executor().run_until_parked();
22851
22852 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
22853 assert!(
22854 final_requests <= 4,
22855 "Multiple rapid edits should be debounced (got {final_requests} requests)",
22856 );
22857 ensure_result_id(Some(final_requests.to_string()), cx);
22858}
22859
22860#[gpui::test]
22861async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
22862 // Regression test for issue #11671
22863 // Previously, adding a cursor after moving multiple cursors would reset
22864 // the cursor count instead of adding to the existing cursors.
22865 init_test(cx, |_| {});
22866 let mut cx = EditorTestContext::new(cx).await;
22867
22868 // Create a simple buffer with cursor at start
22869 cx.set_state(indoc! {"
22870 ˇaaaa
22871 bbbb
22872 cccc
22873 dddd
22874 eeee
22875 ffff
22876 gggg
22877 hhhh"});
22878
22879 // Add 2 cursors below (so we have 3 total)
22880 cx.update_editor(|editor, window, cx| {
22881 editor.add_selection_below(&Default::default(), window, cx);
22882 editor.add_selection_below(&Default::default(), window, cx);
22883 });
22884
22885 // Verify we have 3 cursors
22886 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
22887 assert_eq!(
22888 initial_count, 3,
22889 "Should have 3 cursors after adding 2 below"
22890 );
22891
22892 // Move down one line
22893 cx.update_editor(|editor, window, cx| {
22894 editor.move_down(&MoveDown, window, cx);
22895 });
22896
22897 // Add another cursor below
22898 cx.update_editor(|editor, window, cx| {
22899 editor.add_selection_below(&Default::default(), window, cx);
22900 });
22901
22902 // Should now have 4 cursors (3 original + 1 new)
22903 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
22904 assert_eq!(
22905 final_count, 4,
22906 "Should have 4 cursors after moving and adding another"
22907 );
22908}
22909
22910#[gpui::test(iterations = 10)]
22911async fn test_document_colors(cx: &mut TestAppContext) {
22912 let expected_color = Rgba {
22913 r: 0.33,
22914 g: 0.33,
22915 b: 0.33,
22916 a: 0.33,
22917 };
22918
22919 init_test(cx, |_| {});
22920
22921 let fs = FakeFs::new(cx.executor());
22922 fs.insert_tree(
22923 path!("/a"),
22924 json!({
22925 "first.rs": "fn main() { let a = 5; }",
22926 }),
22927 )
22928 .await;
22929
22930 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22931 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22932 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22933
22934 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22935 language_registry.add(rust_lang());
22936 let mut fake_servers = language_registry.register_fake_lsp(
22937 "Rust",
22938 FakeLspAdapter {
22939 capabilities: lsp::ServerCapabilities {
22940 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
22941 ..lsp::ServerCapabilities::default()
22942 },
22943 name: "rust-analyzer",
22944 ..FakeLspAdapter::default()
22945 },
22946 );
22947 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
22948 "Rust",
22949 FakeLspAdapter {
22950 capabilities: lsp::ServerCapabilities {
22951 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
22952 ..lsp::ServerCapabilities::default()
22953 },
22954 name: "not-rust-analyzer",
22955 ..FakeLspAdapter::default()
22956 },
22957 );
22958
22959 let editor = workspace
22960 .update(cx, |workspace, window, cx| {
22961 workspace.open_abs_path(
22962 PathBuf::from(path!("/a/first.rs")),
22963 OpenOptions::default(),
22964 window,
22965 cx,
22966 )
22967 })
22968 .unwrap()
22969 .await
22970 .unwrap()
22971 .downcast::<Editor>()
22972 .unwrap();
22973 let fake_language_server = fake_servers.next().await.unwrap();
22974 let fake_language_server_without_capabilities =
22975 fake_servers_without_capabilities.next().await.unwrap();
22976 let requests_made = Arc::new(AtomicUsize::new(0));
22977 let closure_requests_made = Arc::clone(&requests_made);
22978 let mut color_request_handle = fake_language_server
22979 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
22980 let requests_made = Arc::clone(&closure_requests_made);
22981 async move {
22982 assert_eq!(
22983 params.text_document.uri,
22984 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
22985 );
22986 requests_made.fetch_add(1, atomic::Ordering::Release);
22987 Ok(vec![
22988 lsp::ColorInformation {
22989 range: lsp::Range {
22990 start: lsp::Position {
22991 line: 0,
22992 character: 0,
22993 },
22994 end: lsp::Position {
22995 line: 0,
22996 character: 1,
22997 },
22998 },
22999 color: lsp::Color {
23000 red: 0.33,
23001 green: 0.33,
23002 blue: 0.33,
23003 alpha: 0.33,
23004 },
23005 },
23006 lsp::ColorInformation {
23007 range: lsp::Range {
23008 start: lsp::Position {
23009 line: 0,
23010 character: 0,
23011 },
23012 end: lsp::Position {
23013 line: 0,
23014 character: 1,
23015 },
23016 },
23017 color: lsp::Color {
23018 red: 0.33,
23019 green: 0.33,
23020 blue: 0.33,
23021 alpha: 0.33,
23022 },
23023 },
23024 ])
23025 }
23026 });
23027
23028 let _handle = fake_language_server_without_capabilities
23029 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
23030 panic!("Should not be called");
23031 });
23032 cx.executor().advance_clock(Duration::from_millis(100));
23033 color_request_handle.next().await.unwrap();
23034 cx.run_until_parked();
23035 assert_eq!(
23036 1,
23037 requests_made.load(atomic::Ordering::Acquire),
23038 "Should query for colors once per editor open"
23039 );
23040 editor.update_in(cx, |editor, _, cx| {
23041 assert_eq!(
23042 vec![expected_color],
23043 extract_color_inlays(editor, cx),
23044 "Should have an initial inlay"
23045 );
23046 });
23047
23048 // opening another file in a split should not influence the LSP query counter
23049 workspace
23050 .update(cx, |workspace, window, cx| {
23051 assert_eq!(
23052 workspace.panes().len(),
23053 1,
23054 "Should have one pane with one editor"
23055 );
23056 workspace.move_item_to_pane_in_direction(
23057 &MoveItemToPaneInDirection {
23058 direction: SplitDirection::Right,
23059 focus: false,
23060 clone: true,
23061 },
23062 window,
23063 cx,
23064 );
23065 })
23066 .unwrap();
23067 cx.run_until_parked();
23068 workspace
23069 .update(cx, |workspace, _, cx| {
23070 let panes = workspace.panes();
23071 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
23072 for pane in panes {
23073 let editor = pane
23074 .read(cx)
23075 .active_item()
23076 .and_then(|item| item.downcast::<Editor>())
23077 .expect("Should have opened an editor in each split");
23078 let editor_file = editor
23079 .read(cx)
23080 .buffer()
23081 .read(cx)
23082 .as_singleton()
23083 .expect("test deals with singleton buffers")
23084 .read(cx)
23085 .file()
23086 .expect("test buffese should have a file")
23087 .path();
23088 assert_eq!(
23089 editor_file.as_ref(),
23090 Path::new("first.rs"),
23091 "Both editors should be opened for the same file"
23092 )
23093 }
23094 })
23095 .unwrap();
23096
23097 cx.executor().advance_clock(Duration::from_millis(500));
23098 let save = editor.update_in(cx, |editor, window, cx| {
23099 editor.move_to_end(&MoveToEnd, window, cx);
23100 editor.handle_input("dirty", window, cx);
23101 editor.save(
23102 SaveOptions {
23103 format: true,
23104 autosave: true,
23105 },
23106 project.clone(),
23107 window,
23108 cx,
23109 )
23110 });
23111 save.await.unwrap();
23112
23113 color_request_handle.next().await.unwrap();
23114 cx.run_until_parked();
23115 assert_eq!(
23116 3,
23117 requests_made.load(atomic::Ordering::Acquire),
23118 "Should query for colors once per save and once per formatting after save"
23119 );
23120
23121 drop(editor);
23122 let close = workspace
23123 .update(cx, |workspace, window, cx| {
23124 workspace.active_pane().update(cx, |pane, cx| {
23125 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23126 })
23127 })
23128 .unwrap();
23129 close.await.unwrap();
23130 let close = workspace
23131 .update(cx, |workspace, window, cx| {
23132 workspace.active_pane().update(cx, |pane, cx| {
23133 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23134 })
23135 })
23136 .unwrap();
23137 close.await.unwrap();
23138 assert_eq!(
23139 3,
23140 requests_made.load(atomic::Ordering::Acquire),
23141 "After saving and closing all editors, no extra requests should be made"
23142 );
23143 workspace
23144 .update(cx, |workspace, _, cx| {
23145 assert!(
23146 workspace.active_item(cx).is_none(),
23147 "Should close all editors"
23148 )
23149 })
23150 .unwrap();
23151
23152 workspace
23153 .update(cx, |workspace, window, cx| {
23154 workspace.active_pane().update(cx, |pane, cx| {
23155 pane.navigate_backward(window, cx);
23156 })
23157 })
23158 .unwrap();
23159 cx.executor().advance_clock(Duration::from_millis(100));
23160 cx.run_until_parked();
23161 let editor = workspace
23162 .update(cx, |workspace, _, cx| {
23163 workspace
23164 .active_item(cx)
23165 .expect("Should have reopened the editor again after navigating back")
23166 .downcast::<Editor>()
23167 .expect("Should be an editor")
23168 })
23169 .unwrap();
23170 color_request_handle.next().await.unwrap();
23171 assert_eq!(
23172 3,
23173 requests_made.load(atomic::Ordering::Acquire),
23174 "Cache should be reused on buffer close and reopen"
23175 );
23176 editor.update(cx, |editor, cx| {
23177 assert_eq!(
23178 vec![expected_color],
23179 extract_color_inlays(editor, cx),
23180 "Should have an initial inlay"
23181 );
23182 });
23183}
23184
23185#[gpui::test]
23186async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
23187 init_test(cx, |_| {});
23188 let (editor, cx) = cx.add_window_view(Editor::single_line);
23189 editor.update_in(cx, |editor, window, cx| {
23190 editor.set_text("oops\n\nwow\n", window, cx)
23191 });
23192 cx.run_until_parked();
23193 editor.update(cx, |editor, cx| {
23194 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
23195 });
23196 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
23197 cx.run_until_parked();
23198 editor.update(cx, |editor, cx| {
23199 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
23200 });
23201}
23202
23203#[track_caller]
23204fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
23205 editor
23206 .all_inlays(cx)
23207 .into_iter()
23208 .filter_map(|inlay| inlay.get_color())
23209 .map(Rgba::from)
23210 .collect()
23211}