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]
3084async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3085 init_test(cx, |settings| {
3086 settings.defaults.tab_size = NonZeroU32::new(4)
3087 });
3088
3089 let lua_language = Arc::new(Language::new(
3090 LanguageConfig {
3091 line_comments: vec!["--".into()],
3092 block_comment: Some(("--[[".into(), "]]".into())),
3093 ..LanguageConfig::default()
3094 },
3095 None,
3096 ));
3097
3098 let mut cx = EditorTestContext::new(cx).await;
3099 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3100
3101 // Line with line comment should extend
3102 cx.set_state(indoc! {"
3103 --ˇ
3104 "});
3105 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3106 cx.assert_editor_state(indoc! {"
3107 --
3108 --ˇ
3109 "});
3110
3111 // Line with block comment that matches line comment should not extend
3112 cx.set_state(indoc! {"
3113 --[[ˇ
3114 "});
3115 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3116 cx.assert_editor_state(indoc! {"
3117 --[[
3118 ˇ
3119 "});
3120}
3121
3122#[gpui::test]
3123fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3124 init_test(cx, |_| {});
3125
3126 let editor = cx.add_window(|window, cx| {
3127 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3128 let mut editor = build_editor(buffer.clone(), window, cx);
3129 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3130 s.select_ranges([3..4, 11..12, 19..20])
3131 });
3132 editor
3133 });
3134
3135 _ = editor.update(cx, |editor, window, cx| {
3136 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3137 editor.buffer.update(cx, |buffer, cx| {
3138 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3139 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3140 });
3141 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3142
3143 editor.insert("Z", window, cx);
3144 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3145
3146 // The selections are moved after the inserted characters
3147 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3148 });
3149}
3150
3151#[gpui::test]
3152async fn test_tab(cx: &mut TestAppContext) {
3153 init_test(cx, |settings| {
3154 settings.defaults.tab_size = NonZeroU32::new(3)
3155 });
3156
3157 let mut cx = EditorTestContext::new(cx).await;
3158 cx.set_state(indoc! {"
3159 ˇabˇc
3160 ˇ🏀ˇ🏀ˇefg
3161 dˇ
3162 "});
3163 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3164 cx.assert_editor_state(indoc! {"
3165 ˇab ˇc
3166 ˇ🏀 ˇ🏀 ˇefg
3167 d ˇ
3168 "});
3169
3170 cx.set_state(indoc! {"
3171 a
3172 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3173 "});
3174 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3175 cx.assert_editor_state(indoc! {"
3176 a
3177 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3178 "});
3179}
3180
3181#[gpui::test]
3182async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3183 init_test(cx, |_| {});
3184
3185 let mut cx = EditorTestContext::new(cx).await;
3186 let language = Arc::new(
3187 Language::new(
3188 LanguageConfig::default(),
3189 Some(tree_sitter_rust::LANGUAGE.into()),
3190 )
3191 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3192 .unwrap(),
3193 );
3194 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3195
3196 // test when all cursors are not at suggested indent
3197 // then simply move to their suggested indent location
3198 cx.set_state(indoc! {"
3199 const a: B = (
3200 c(
3201 ˇ
3202 ˇ )
3203 );
3204 "});
3205 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3206 cx.assert_editor_state(indoc! {"
3207 const a: B = (
3208 c(
3209 ˇ
3210 ˇ)
3211 );
3212 "});
3213
3214 // test cursor already at suggested indent not moving when
3215 // other cursors are yet to reach their suggested indents
3216 cx.set_state(indoc! {"
3217 ˇ
3218 const a: B = (
3219 c(
3220 d(
3221 ˇ
3222 )
3223 ˇ
3224 ˇ )
3225 );
3226 "});
3227 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3228 cx.assert_editor_state(indoc! {"
3229 ˇ
3230 const a: B = (
3231 c(
3232 d(
3233 ˇ
3234 )
3235 ˇ
3236 ˇ)
3237 );
3238 "});
3239 // test when all cursors are at suggested indent then tab is inserted
3240 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3241 cx.assert_editor_state(indoc! {"
3242 ˇ
3243 const a: B = (
3244 c(
3245 d(
3246 ˇ
3247 )
3248 ˇ
3249 ˇ)
3250 );
3251 "});
3252
3253 // test when current indent is less than suggested indent,
3254 // we adjust line to match suggested indent and move cursor to it
3255 //
3256 // when no other cursor is at word boundary, all of them should move
3257 cx.set_state(indoc! {"
3258 const a: B = (
3259 c(
3260 d(
3261 ˇ
3262 ˇ )
3263 ˇ )
3264 );
3265 "});
3266 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3267 cx.assert_editor_state(indoc! {"
3268 const a: B = (
3269 c(
3270 d(
3271 ˇ
3272 ˇ)
3273 ˇ)
3274 );
3275 "});
3276
3277 // test when current indent is less than suggested indent,
3278 // we adjust line to match suggested indent and move cursor to it
3279 //
3280 // when some other cursor is at word boundary, it should not move
3281 cx.set_state(indoc! {"
3282 const a: B = (
3283 c(
3284 d(
3285 ˇ
3286 ˇ )
3287 ˇ)
3288 );
3289 "});
3290 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3291 cx.assert_editor_state(indoc! {"
3292 const a: B = (
3293 c(
3294 d(
3295 ˇ
3296 ˇ)
3297 ˇ)
3298 );
3299 "});
3300
3301 // test when current indent is more than suggested indent,
3302 // we just move cursor to current indent instead of suggested indent
3303 //
3304 // when no other cursor is at word boundary, all of them should move
3305 cx.set_state(indoc! {"
3306 const a: B = (
3307 c(
3308 d(
3309 ˇ
3310 ˇ )
3311 ˇ )
3312 );
3313 "});
3314 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3315 cx.assert_editor_state(indoc! {"
3316 const a: B = (
3317 c(
3318 d(
3319 ˇ
3320 ˇ)
3321 ˇ)
3322 );
3323 "});
3324 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3325 cx.assert_editor_state(indoc! {"
3326 const a: B = (
3327 c(
3328 d(
3329 ˇ
3330 ˇ)
3331 ˇ)
3332 );
3333 "});
3334
3335 // test when current indent is more than suggested indent,
3336 // we just move cursor to current indent instead of suggested indent
3337 //
3338 // when some other cursor is at word boundary, it doesn't move
3339 cx.set_state(indoc! {"
3340 const a: B = (
3341 c(
3342 d(
3343 ˇ
3344 ˇ )
3345 ˇ)
3346 );
3347 "});
3348 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3349 cx.assert_editor_state(indoc! {"
3350 const a: B = (
3351 c(
3352 d(
3353 ˇ
3354 ˇ)
3355 ˇ)
3356 );
3357 "});
3358
3359 // handle auto-indent when there are multiple cursors on the same line
3360 cx.set_state(indoc! {"
3361 const a: B = (
3362 c(
3363 ˇ ˇ
3364 ˇ )
3365 );
3366 "});
3367 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3368 cx.assert_editor_state(indoc! {"
3369 const a: B = (
3370 c(
3371 ˇ
3372 ˇ)
3373 );
3374 "});
3375}
3376
3377#[gpui::test]
3378async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3379 init_test(cx, |settings| {
3380 settings.defaults.tab_size = NonZeroU32::new(3)
3381 });
3382
3383 let mut cx = EditorTestContext::new(cx).await;
3384 cx.set_state(indoc! {"
3385 ˇ
3386 \t ˇ
3387 \t ˇ
3388 \t ˇ
3389 \t \t\t \t \t\t \t\t \t \t ˇ
3390 "});
3391
3392 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3393 cx.assert_editor_state(indoc! {"
3394 ˇ
3395 \t ˇ
3396 \t ˇ
3397 \t ˇ
3398 \t \t\t \t \t\t \t\t \t \t ˇ
3399 "});
3400}
3401
3402#[gpui::test]
3403async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3404 init_test(cx, |settings| {
3405 settings.defaults.tab_size = NonZeroU32::new(4)
3406 });
3407
3408 let language = Arc::new(
3409 Language::new(
3410 LanguageConfig::default(),
3411 Some(tree_sitter_rust::LANGUAGE.into()),
3412 )
3413 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3414 .unwrap(),
3415 );
3416
3417 let mut cx = EditorTestContext::new(cx).await;
3418 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3419 cx.set_state(indoc! {"
3420 fn a() {
3421 if b {
3422 \t ˇc
3423 }
3424 }
3425 "});
3426
3427 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3428 cx.assert_editor_state(indoc! {"
3429 fn a() {
3430 if b {
3431 ˇc
3432 }
3433 }
3434 "});
3435}
3436
3437#[gpui::test]
3438async fn test_indent_outdent(cx: &mut TestAppContext) {
3439 init_test(cx, |settings| {
3440 settings.defaults.tab_size = NonZeroU32::new(4);
3441 });
3442
3443 let mut cx = EditorTestContext::new(cx).await;
3444
3445 cx.set_state(indoc! {"
3446 «oneˇ» «twoˇ»
3447 three
3448 four
3449 "});
3450 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3451 cx.assert_editor_state(indoc! {"
3452 «oneˇ» «twoˇ»
3453 three
3454 four
3455 "});
3456
3457 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3458 cx.assert_editor_state(indoc! {"
3459 «oneˇ» «twoˇ»
3460 three
3461 four
3462 "});
3463
3464 // select across line ending
3465 cx.set_state(indoc! {"
3466 one two
3467 t«hree
3468 ˇ» four
3469 "});
3470 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3471 cx.assert_editor_state(indoc! {"
3472 one two
3473 t«hree
3474 ˇ» four
3475 "});
3476
3477 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3478 cx.assert_editor_state(indoc! {"
3479 one two
3480 t«hree
3481 ˇ» four
3482 "});
3483
3484 // Ensure that indenting/outdenting works when the cursor is at column 0.
3485 cx.set_state(indoc! {"
3486 one two
3487 ˇthree
3488 four
3489 "});
3490 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3491 cx.assert_editor_state(indoc! {"
3492 one two
3493 ˇthree
3494 four
3495 "});
3496
3497 cx.set_state(indoc! {"
3498 one two
3499 ˇ three
3500 four
3501 "});
3502 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3503 cx.assert_editor_state(indoc! {"
3504 one two
3505 ˇthree
3506 four
3507 "});
3508}
3509
3510#[gpui::test]
3511async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3512 // This is a regression test for issue #33761
3513 init_test(cx, |_| {});
3514
3515 let mut cx = EditorTestContext::new(cx).await;
3516 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3517 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3518
3519 cx.set_state(
3520 r#"ˇ# ingress:
3521ˇ# api:
3522ˇ# enabled: false
3523ˇ# pathType: Prefix
3524ˇ# console:
3525ˇ# enabled: false
3526ˇ# pathType: Prefix
3527"#,
3528 );
3529
3530 // Press tab to indent all lines
3531 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3532
3533 cx.assert_editor_state(
3534 r#" ˇ# ingress:
3535 ˇ# api:
3536 ˇ# enabled: false
3537 ˇ# pathType: Prefix
3538 ˇ# console:
3539 ˇ# enabled: false
3540 ˇ# pathType: Prefix
3541"#,
3542 );
3543}
3544
3545#[gpui::test]
3546async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3547 // This is a test to make sure our fix for issue #33761 didn't break anything
3548 init_test(cx, |_| {});
3549
3550 let mut cx = EditorTestContext::new(cx).await;
3551 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3552 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3553
3554 cx.set_state(
3555 r#"ˇingress:
3556ˇ api:
3557ˇ enabled: false
3558ˇ pathType: Prefix
3559"#,
3560 );
3561
3562 // Press tab to indent all lines
3563 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3564
3565 cx.assert_editor_state(
3566 r#"ˇingress:
3567 ˇapi:
3568 ˇenabled: false
3569 ˇpathType: Prefix
3570"#,
3571 );
3572}
3573
3574#[gpui::test]
3575async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3576 init_test(cx, |settings| {
3577 settings.defaults.hard_tabs = Some(true);
3578 });
3579
3580 let mut cx = EditorTestContext::new(cx).await;
3581
3582 // select two ranges on one line
3583 cx.set_state(indoc! {"
3584 «oneˇ» «twoˇ»
3585 three
3586 four
3587 "});
3588 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3589 cx.assert_editor_state(indoc! {"
3590 \t«oneˇ» «twoˇ»
3591 three
3592 four
3593 "});
3594 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3595 cx.assert_editor_state(indoc! {"
3596 \t\t«oneˇ» «twoˇ»
3597 three
3598 four
3599 "});
3600 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3601 cx.assert_editor_state(indoc! {"
3602 \t«oneˇ» «twoˇ»
3603 three
3604 four
3605 "});
3606 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3607 cx.assert_editor_state(indoc! {"
3608 «oneˇ» «twoˇ»
3609 three
3610 four
3611 "});
3612
3613 // select across a line ending
3614 cx.set_state(indoc! {"
3615 one two
3616 t«hree
3617 ˇ»four
3618 "});
3619 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3620 cx.assert_editor_state(indoc! {"
3621 one two
3622 \tt«hree
3623 ˇ»four
3624 "});
3625 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3626 cx.assert_editor_state(indoc! {"
3627 one two
3628 \t\tt«hree
3629 ˇ»four
3630 "});
3631 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3632 cx.assert_editor_state(indoc! {"
3633 one two
3634 \tt«hree
3635 ˇ»four
3636 "});
3637 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3638 cx.assert_editor_state(indoc! {"
3639 one two
3640 t«hree
3641 ˇ»four
3642 "});
3643
3644 // Ensure that indenting/outdenting works when the cursor is at column 0.
3645 cx.set_state(indoc! {"
3646 one two
3647 ˇthree
3648 four
3649 "});
3650 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3651 cx.assert_editor_state(indoc! {"
3652 one two
3653 ˇthree
3654 four
3655 "});
3656 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3657 cx.assert_editor_state(indoc! {"
3658 one two
3659 \tˇthree
3660 four
3661 "});
3662 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3663 cx.assert_editor_state(indoc! {"
3664 one two
3665 ˇthree
3666 four
3667 "});
3668}
3669
3670#[gpui::test]
3671fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3672 init_test(cx, |settings| {
3673 settings.languages.0.extend([
3674 (
3675 "TOML".into(),
3676 LanguageSettingsContent {
3677 tab_size: NonZeroU32::new(2),
3678 ..Default::default()
3679 },
3680 ),
3681 (
3682 "Rust".into(),
3683 LanguageSettingsContent {
3684 tab_size: NonZeroU32::new(4),
3685 ..Default::default()
3686 },
3687 ),
3688 ]);
3689 });
3690
3691 let toml_language = Arc::new(Language::new(
3692 LanguageConfig {
3693 name: "TOML".into(),
3694 ..Default::default()
3695 },
3696 None,
3697 ));
3698 let rust_language = Arc::new(Language::new(
3699 LanguageConfig {
3700 name: "Rust".into(),
3701 ..Default::default()
3702 },
3703 None,
3704 ));
3705
3706 let toml_buffer =
3707 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3708 let rust_buffer =
3709 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3710 let multibuffer = cx.new(|cx| {
3711 let mut multibuffer = MultiBuffer::new(ReadWrite);
3712 multibuffer.push_excerpts(
3713 toml_buffer.clone(),
3714 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3715 cx,
3716 );
3717 multibuffer.push_excerpts(
3718 rust_buffer.clone(),
3719 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3720 cx,
3721 );
3722 multibuffer
3723 });
3724
3725 cx.add_window(|window, cx| {
3726 let mut editor = build_editor(multibuffer, window, cx);
3727
3728 assert_eq!(
3729 editor.text(cx),
3730 indoc! {"
3731 a = 1
3732 b = 2
3733
3734 const c: usize = 3;
3735 "}
3736 );
3737
3738 select_ranges(
3739 &mut editor,
3740 indoc! {"
3741 «aˇ» = 1
3742 b = 2
3743
3744 «const c:ˇ» usize = 3;
3745 "},
3746 window,
3747 cx,
3748 );
3749
3750 editor.tab(&Tab, window, cx);
3751 assert_text_with_selections(
3752 &mut editor,
3753 indoc! {"
3754 «aˇ» = 1
3755 b = 2
3756
3757 «const c:ˇ» usize = 3;
3758 "},
3759 cx,
3760 );
3761 editor.backtab(&Backtab, window, cx);
3762 assert_text_with_selections(
3763 &mut editor,
3764 indoc! {"
3765 «aˇ» = 1
3766 b = 2
3767
3768 «const c:ˇ» usize = 3;
3769 "},
3770 cx,
3771 );
3772
3773 editor
3774 });
3775}
3776
3777#[gpui::test]
3778async fn test_backspace(cx: &mut TestAppContext) {
3779 init_test(cx, |_| {});
3780
3781 let mut cx = EditorTestContext::new(cx).await;
3782
3783 // Basic backspace
3784 cx.set_state(indoc! {"
3785 onˇe two three
3786 fou«rˇ» five six
3787 seven «ˇeight nine
3788 »ten
3789 "});
3790 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3791 cx.assert_editor_state(indoc! {"
3792 oˇe two three
3793 fouˇ five six
3794 seven ˇten
3795 "});
3796
3797 // Test backspace inside and around indents
3798 cx.set_state(indoc! {"
3799 zero
3800 ˇone
3801 ˇtwo
3802 ˇ ˇ ˇ three
3803 ˇ ˇ four
3804 "});
3805 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3806 cx.assert_editor_state(indoc! {"
3807 zero
3808 ˇone
3809 ˇtwo
3810 ˇ threeˇ four
3811 "});
3812}
3813
3814#[gpui::test]
3815async fn test_delete(cx: &mut TestAppContext) {
3816 init_test(cx, |_| {});
3817
3818 let mut cx = EditorTestContext::new(cx).await;
3819 cx.set_state(indoc! {"
3820 onˇe two three
3821 fou«rˇ» five six
3822 seven «ˇeight nine
3823 »ten
3824 "});
3825 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3826 cx.assert_editor_state(indoc! {"
3827 onˇ two three
3828 fouˇ five six
3829 seven ˇten
3830 "});
3831}
3832
3833#[gpui::test]
3834fn test_delete_line(cx: &mut TestAppContext) {
3835 init_test(cx, |_| {});
3836
3837 let editor = cx.add_window(|window, cx| {
3838 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3839 build_editor(buffer, window, cx)
3840 });
3841 _ = editor.update(cx, |editor, window, cx| {
3842 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3843 s.select_display_ranges([
3844 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3845 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3846 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3847 ])
3848 });
3849 editor.delete_line(&DeleteLine, window, cx);
3850 assert_eq!(editor.display_text(cx), "ghi");
3851 assert_eq!(
3852 editor.selections.display_ranges(cx),
3853 vec![
3854 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3855 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3856 ]
3857 );
3858 });
3859
3860 let editor = cx.add_window(|window, cx| {
3861 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3862 build_editor(buffer, window, cx)
3863 });
3864 _ = editor.update(cx, |editor, window, cx| {
3865 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3866 s.select_display_ranges([
3867 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3868 ])
3869 });
3870 editor.delete_line(&DeleteLine, window, cx);
3871 assert_eq!(editor.display_text(cx), "ghi\n");
3872 assert_eq!(
3873 editor.selections.display_ranges(cx),
3874 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3875 );
3876 });
3877}
3878
3879#[gpui::test]
3880fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3881 init_test(cx, |_| {});
3882
3883 cx.add_window(|window, cx| {
3884 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3885 let mut editor = build_editor(buffer.clone(), window, cx);
3886 let buffer = buffer.read(cx).as_singleton().unwrap();
3887
3888 assert_eq!(
3889 editor.selections.ranges::<Point>(cx),
3890 &[Point::new(0, 0)..Point::new(0, 0)]
3891 );
3892
3893 // When on single line, replace newline at end by space
3894 editor.join_lines(&JoinLines, window, cx);
3895 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3896 assert_eq!(
3897 editor.selections.ranges::<Point>(cx),
3898 &[Point::new(0, 3)..Point::new(0, 3)]
3899 );
3900
3901 // When multiple lines are selected, remove newlines that are spanned by the selection
3902 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3903 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3904 });
3905 editor.join_lines(&JoinLines, window, cx);
3906 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3907 assert_eq!(
3908 editor.selections.ranges::<Point>(cx),
3909 &[Point::new(0, 11)..Point::new(0, 11)]
3910 );
3911
3912 // Undo should be transactional
3913 editor.undo(&Undo, window, cx);
3914 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3915 assert_eq!(
3916 editor.selections.ranges::<Point>(cx),
3917 &[Point::new(0, 5)..Point::new(2, 2)]
3918 );
3919
3920 // When joining an empty line don't insert a space
3921 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3922 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3923 });
3924 editor.join_lines(&JoinLines, window, cx);
3925 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3926 assert_eq!(
3927 editor.selections.ranges::<Point>(cx),
3928 [Point::new(2, 3)..Point::new(2, 3)]
3929 );
3930
3931 // We can remove trailing newlines
3932 editor.join_lines(&JoinLines, window, cx);
3933 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3934 assert_eq!(
3935 editor.selections.ranges::<Point>(cx),
3936 [Point::new(2, 3)..Point::new(2, 3)]
3937 );
3938
3939 // We don't blow up on the last line
3940 editor.join_lines(&JoinLines, window, cx);
3941 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3942 assert_eq!(
3943 editor.selections.ranges::<Point>(cx),
3944 [Point::new(2, 3)..Point::new(2, 3)]
3945 );
3946
3947 // reset to test indentation
3948 editor.buffer.update(cx, |buffer, cx| {
3949 buffer.edit(
3950 [
3951 (Point::new(1, 0)..Point::new(1, 2), " "),
3952 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3953 ],
3954 None,
3955 cx,
3956 )
3957 });
3958
3959 // We remove any leading spaces
3960 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3961 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3962 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3963 });
3964 editor.join_lines(&JoinLines, window, cx);
3965 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3966
3967 // We don't insert a space for a line containing only spaces
3968 editor.join_lines(&JoinLines, window, cx);
3969 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3970
3971 // We ignore any leading tabs
3972 editor.join_lines(&JoinLines, window, cx);
3973 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3974
3975 editor
3976 });
3977}
3978
3979#[gpui::test]
3980fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3981 init_test(cx, |_| {});
3982
3983 cx.add_window(|window, cx| {
3984 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3985 let mut editor = build_editor(buffer.clone(), window, cx);
3986 let buffer = buffer.read(cx).as_singleton().unwrap();
3987
3988 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3989 s.select_ranges([
3990 Point::new(0, 2)..Point::new(1, 1),
3991 Point::new(1, 2)..Point::new(1, 2),
3992 Point::new(3, 1)..Point::new(3, 2),
3993 ])
3994 });
3995
3996 editor.join_lines(&JoinLines, window, cx);
3997 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3998
3999 assert_eq!(
4000 editor.selections.ranges::<Point>(cx),
4001 [
4002 Point::new(0, 7)..Point::new(0, 7),
4003 Point::new(1, 3)..Point::new(1, 3)
4004 ]
4005 );
4006 editor
4007 });
4008}
4009
4010#[gpui::test]
4011async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4012 init_test(cx, |_| {});
4013
4014 let mut cx = EditorTestContext::new(cx).await;
4015
4016 let diff_base = r#"
4017 Line 0
4018 Line 1
4019 Line 2
4020 Line 3
4021 "#
4022 .unindent();
4023
4024 cx.set_state(
4025 &r#"
4026 ˇLine 0
4027 Line 1
4028 Line 2
4029 Line 3
4030 "#
4031 .unindent(),
4032 );
4033
4034 cx.set_head_text(&diff_base);
4035 executor.run_until_parked();
4036
4037 // Join lines
4038 cx.update_editor(|editor, window, cx| {
4039 editor.join_lines(&JoinLines, window, cx);
4040 });
4041 executor.run_until_parked();
4042
4043 cx.assert_editor_state(
4044 &r#"
4045 Line 0ˇ Line 1
4046 Line 2
4047 Line 3
4048 "#
4049 .unindent(),
4050 );
4051 // Join again
4052 cx.update_editor(|editor, window, cx| {
4053 editor.join_lines(&JoinLines, window, cx);
4054 });
4055 executor.run_until_parked();
4056
4057 cx.assert_editor_state(
4058 &r#"
4059 Line 0 Line 1ˇ Line 2
4060 Line 3
4061 "#
4062 .unindent(),
4063 );
4064}
4065
4066#[gpui::test]
4067async fn test_custom_newlines_cause_no_false_positive_diffs(
4068 executor: BackgroundExecutor,
4069 cx: &mut TestAppContext,
4070) {
4071 init_test(cx, |_| {});
4072 let mut cx = EditorTestContext::new(cx).await;
4073 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4074 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4075 executor.run_until_parked();
4076
4077 cx.update_editor(|editor, window, cx| {
4078 let snapshot = editor.snapshot(window, cx);
4079 assert_eq!(
4080 snapshot
4081 .buffer_snapshot
4082 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
4083 .collect::<Vec<_>>(),
4084 Vec::new(),
4085 "Should not have any diffs for files with custom newlines"
4086 );
4087 });
4088}
4089
4090#[gpui::test]
4091async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4092 init_test(cx, |_| {});
4093
4094 let mut cx = EditorTestContext::new(cx).await;
4095
4096 // Test sort_lines_case_insensitive()
4097 cx.set_state(indoc! {"
4098 «z
4099 y
4100 x
4101 Z
4102 Y
4103 Xˇ»
4104 "});
4105 cx.update_editor(|e, window, cx| {
4106 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4107 });
4108 cx.assert_editor_state(indoc! {"
4109 «x
4110 X
4111 y
4112 Y
4113 z
4114 Zˇ»
4115 "});
4116
4117 // Test sort_lines_by_length()
4118 //
4119 // Demonstrates:
4120 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4121 // - sort is stable
4122 cx.set_state(indoc! {"
4123 «123
4124 æ
4125 12
4126 ∞
4127 1
4128 æˇ»
4129 "});
4130 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4131 cx.assert_editor_state(indoc! {"
4132 «æ
4133 ∞
4134 1
4135 æ
4136 12
4137 123ˇ»
4138 "});
4139
4140 // Test reverse_lines()
4141 cx.set_state(indoc! {"
4142 «5
4143 4
4144 3
4145 2
4146 1ˇ»
4147 "});
4148 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4149 cx.assert_editor_state(indoc! {"
4150 «1
4151 2
4152 3
4153 4
4154 5ˇ»
4155 "});
4156
4157 // Skip testing shuffle_line()
4158
4159 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4160 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4161
4162 // Don't manipulate when cursor is on single line, but expand the selection
4163 cx.set_state(indoc! {"
4164 ddˇdd
4165 ccc
4166 bb
4167 a
4168 "});
4169 cx.update_editor(|e, window, cx| {
4170 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4171 });
4172 cx.assert_editor_state(indoc! {"
4173 «ddddˇ»
4174 ccc
4175 bb
4176 a
4177 "});
4178
4179 // Basic manipulate case
4180 // Start selection moves to column 0
4181 // End of selection shrinks to fit shorter line
4182 cx.set_state(indoc! {"
4183 dd«d
4184 ccc
4185 bb
4186 aaaaaˇ»
4187 "});
4188 cx.update_editor(|e, window, cx| {
4189 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4190 });
4191 cx.assert_editor_state(indoc! {"
4192 «aaaaa
4193 bb
4194 ccc
4195 dddˇ»
4196 "});
4197
4198 // Manipulate case with newlines
4199 cx.set_state(indoc! {"
4200 dd«d
4201 ccc
4202
4203 bb
4204 aaaaa
4205
4206 ˇ»
4207 "});
4208 cx.update_editor(|e, window, cx| {
4209 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4210 });
4211 cx.assert_editor_state(indoc! {"
4212 «
4213
4214 aaaaa
4215 bb
4216 ccc
4217 dddˇ»
4218
4219 "});
4220
4221 // Adding new line
4222 cx.set_state(indoc! {"
4223 aa«a
4224 bbˇ»b
4225 "});
4226 cx.update_editor(|e, window, cx| {
4227 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4228 });
4229 cx.assert_editor_state(indoc! {"
4230 «aaa
4231 bbb
4232 added_lineˇ»
4233 "});
4234
4235 // Removing line
4236 cx.set_state(indoc! {"
4237 aa«a
4238 bbbˇ»
4239 "});
4240 cx.update_editor(|e, window, cx| {
4241 e.manipulate_immutable_lines(window, cx, |lines| {
4242 lines.pop();
4243 })
4244 });
4245 cx.assert_editor_state(indoc! {"
4246 «aaaˇ»
4247 "});
4248
4249 // Removing all lines
4250 cx.set_state(indoc! {"
4251 aa«a
4252 bbbˇ»
4253 "});
4254 cx.update_editor(|e, window, cx| {
4255 e.manipulate_immutable_lines(window, cx, |lines| {
4256 lines.drain(..);
4257 })
4258 });
4259 cx.assert_editor_state(indoc! {"
4260 ˇ
4261 "});
4262}
4263
4264#[gpui::test]
4265async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4266 init_test(cx, |_| {});
4267
4268 let mut cx = EditorTestContext::new(cx).await;
4269
4270 // Consider continuous selection as single selection
4271 cx.set_state(indoc! {"
4272 Aaa«aa
4273 cˇ»c«c
4274 bb
4275 aaaˇ»aa
4276 "});
4277 cx.update_editor(|e, window, cx| {
4278 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4279 });
4280 cx.assert_editor_state(indoc! {"
4281 «Aaaaa
4282 ccc
4283 bb
4284 aaaaaˇ»
4285 "});
4286
4287 cx.set_state(indoc! {"
4288 Aaa«aa
4289 cˇ»c«c
4290 bb
4291 aaaˇ»aa
4292 "});
4293 cx.update_editor(|e, window, cx| {
4294 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4295 });
4296 cx.assert_editor_state(indoc! {"
4297 «Aaaaa
4298 ccc
4299 bbˇ»
4300 "});
4301
4302 // Consider non continuous selection as distinct dedup operations
4303 cx.set_state(indoc! {"
4304 «aaaaa
4305 bb
4306 aaaaa
4307 aaaaaˇ»
4308
4309 aaa«aaˇ»
4310 "});
4311 cx.update_editor(|e, window, cx| {
4312 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4313 });
4314 cx.assert_editor_state(indoc! {"
4315 «aaaaa
4316 bbˇ»
4317
4318 «aaaaaˇ»
4319 "});
4320}
4321
4322#[gpui::test]
4323async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4324 init_test(cx, |_| {});
4325
4326 let mut cx = EditorTestContext::new(cx).await;
4327
4328 cx.set_state(indoc! {"
4329 «Aaa
4330 aAa
4331 Aaaˇ»
4332 "});
4333 cx.update_editor(|e, window, cx| {
4334 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4335 });
4336 cx.assert_editor_state(indoc! {"
4337 «Aaa
4338 aAaˇ»
4339 "});
4340
4341 cx.set_state(indoc! {"
4342 «Aaa
4343 aAa
4344 aaAˇ»
4345 "});
4346 cx.update_editor(|e, window, cx| {
4347 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4348 });
4349 cx.assert_editor_state(indoc! {"
4350 «Aaaˇ»
4351 "});
4352}
4353
4354#[gpui::test]
4355async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4356 init_test(cx, |_| {});
4357
4358 let mut cx = EditorTestContext::new(cx).await;
4359
4360 // Manipulate with multiple selections on a single line
4361 cx.set_state(indoc! {"
4362 dd«dd
4363 cˇ»c«c
4364 bb
4365 aaaˇ»aa
4366 "});
4367 cx.update_editor(|e, window, cx| {
4368 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4369 });
4370 cx.assert_editor_state(indoc! {"
4371 «aaaaa
4372 bb
4373 ccc
4374 ddddˇ»
4375 "});
4376
4377 // Manipulate with multiple disjoin selections
4378 cx.set_state(indoc! {"
4379 5«
4380 4
4381 3
4382 2
4383 1ˇ»
4384
4385 dd«dd
4386 ccc
4387 bb
4388 aaaˇ»aa
4389 "});
4390 cx.update_editor(|e, window, cx| {
4391 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4392 });
4393 cx.assert_editor_state(indoc! {"
4394 «1
4395 2
4396 3
4397 4
4398 5ˇ»
4399
4400 «aaaaa
4401 bb
4402 ccc
4403 ddddˇ»
4404 "});
4405
4406 // Adding lines on each selection
4407 cx.set_state(indoc! {"
4408 2«
4409 1ˇ»
4410
4411 bb«bb
4412 aaaˇ»aa
4413 "});
4414 cx.update_editor(|e, window, cx| {
4415 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4416 });
4417 cx.assert_editor_state(indoc! {"
4418 «2
4419 1
4420 added lineˇ»
4421
4422 «bbbb
4423 aaaaa
4424 added lineˇ»
4425 "});
4426
4427 // Removing lines on each selection
4428 cx.set_state(indoc! {"
4429 2«
4430 1ˇ»
4431
4432 bb«bb
4433 aaaˇ»aa
4434 "});
4435 cx.update_editor(|e, window, cx| {
4436 e.manipulate_immutable_lines(window, cx, |lines| {
4437 lines.pop();
4438 })
4439 });
4440 cx.assert_editor_state(indoc! {"
4441 «2ˇ»
4442
4443 «bbbbˇ»
4444 "});
4445}
4446
4447#[gpui::test]
4448async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4449 init_test(cx, |settings| {
4450 settings.defaults.tab_size = NonZeroU32::new(3)
4451 });
4452
4453 let mut cx = EditorTestContext::new(cx).await;
4454
4455 // MULTI SELECTION
4456 // Ln.1 "«" tests empty lines
4457 // Ln.9 tests just leading whitespace
4458 cx.set_state(indoc! {"
4459 «
4460 abc // No indentationˇ»
4461 «\tabc // 1 tabˇ»
4462 \t\tabc « ˇ» // 2 tabs
4463 \t ab«c // Tab followed by space
4464 \tabc // Space followed by tab (3 spaces should be the result)
4465 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4466 abˇ»ˇc ˇ ˇ // Already space indented«
4467 \t
4468 \tabc\tdef // Only the leading tab is manipulatedˇ»
4469 "});
4470 cx.update_editor(|e, window, cx| {
4471 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4472 });
4473 cx.assert_editor_state(
4474 indoc! {"
4475 «
4476 abc // No indentation
4477 abc // 1 tab
4478 abc // 2 tabs
4479 abc // Tab followed by space
4480 abc // Space followed by tab (3 spaces should be the result)
4481 abc // Mixed indentation (tab conversion depends on the column)
4482 abc // Already space indented
4483 ·
4484 abc\tdef // Only the leading tab is manipulatedˇ»
4485 "}
4486 .replace("·", "")
4487 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4488 );
4489
4490 // Test on just a few lines, the others should remain unchanged
4491 // Only lines (3, 5, 10, 11) should change
4492 cx.set_state(
4493 indoc! {"
4494 ·
4495 abc // No indentation
4496 \tabcˇ // 1 tab
4497 \t\tabc // 2 tabs
4498 \t abcˇ // Tab followed by space
4499 \tabc // Space followed by tab (3 spaces should be the result)
4500 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4501 abc // Already space indented
4502 «\t
4503 \tabc\tdef // Only the leading tab is manipulatedˇ»
4504 "}
4505 .replace("·", "")
4506 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4507 );
4508 cx.update_editor(|e, window, cx| {
4509 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4510 });
4511 cx.assert_editor_state(
4512 indoc! {"
4513 ·
4514 abc // No indentation
4515 « abc // 1 tabˇ»
4516 \t\tabc // 2 tabs
4517 « abc // Tab followed by spaceˇ»
4518 \tabc // Space followed by tab (3 spaces should be the result)
4519 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4520 abc // Already space indented
4521 « ·
4522 abc\tdef // Only the leading tab is manipulatedˇ»
4523 "}
4524 .replace("·", "")
4525 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4526 );
4527
4528 // SINGLE SELECTION
4529 // Ln.1 "«" tests empty lines
4530 // Ln.9 tests just leading whitespace
4531 cx.set_state(indoc! {"
4532 «
4533 abc // No indentation
4534 \tabc // 1 tab
4535 \t\tabc // 2 tabs
4536 \t abc // Tab followed by space
4537 \tabc // Space followed by tab (3 spaces should be the result)
4538 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4539 abc // Already space indented
4540 \t
4541 \tabc\tdef // Only the leading tab is manipulatedˇ»
4542 "});
4543 cx.update_editor(|e, window, cx| {
4544 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4545 });
4546 cx.assert_editor_state(
4547 indoc! {"
4548 «
4549 abc // No indentation
4550 abc // 1 tab
4551 abc // 2 tabs
4552 abc // Tab followed by space
4553 abc // Space followed by tab (3 spaces should be the result)
4554 abc // Mixed indentation (tab conversion depends on the column)
4555 abc // Already space indented
4556 ·
4557 abc\tdef // Only the leading tab is manipulatedˇ»
4558 "}
4559 .replace("·", "")
4560 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4561 );
4562}
4563
4564#[gpui::test]
4565async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
4566 init_test(cx, |settings| {
4567 settings.defaults.tab_size = NonZeroU32::new(3)
4568 });
4569
4570 let mut cx = EditorTestContext::new(cx).await;
4571
4572 // MULTI SELECTION
4573 // Ln.1 "«" tests empty lines
4574 // Ln.11 tests just leading whitespace
4575 cx.set_state(indoc! {"
4576 «
4577 abˇ»ˇc // No indentation
4578 abc ˇ ˇ // 1 space (< 3 so dont convert)
4579 abc « // 2 spaces (< 3 so dont convert)
4580 abc // 3 spaces (convert)
4581 abc ˇ» // 5 spaces (1 tab + 2 spaces)
4582 «\tˇ»\t«\tˇ»abc // Already tab indented
4583 «\t abc // Tab followed by space
4584 \tabc // Space followed by tab (should be consumed due to tab)
4585 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4586 \tˇ» «\t
4587 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
4588 "});
4589 cx.update_editor(|e, window, cx| {
4590 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4591 });
4592 cx.assert_editor_state(indoc! {"
4593 «
4594 abc // No indentation
4595 abc // 1 space (< 3 so dont convert)
4596 abc // 2 spaces (< 3 so dont convert)
4597 \tabc // 3 spaces (convert)
4598 \t abc // 5 spaces (1 tab + 2 spaces)
4599 \t\t\tabc // Already tab indented
4600 \t abc // Tab followed by space
4601 \tabc // Space followed by tab (should be consumed due to tab)
4602 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4603 \t\t\t
4604 \tabc \t // Only the leading spaces should be convertedˇ»
4605 "});
4606
4607 // Test on just a few lines, the other should remain unchanged
4608 // Only lines (4, 8, 11, 12) should change
4609 cx.set_state(
4610 indoc! {"
4611 ·
4612 abc // No indentation
4613 abc // 1 space (< 3 so dont convert)
4614 abc // 2 spaces (< 3 so dont convert)
4615 « abc // 3 spaces (convert)ˇ»
4616 abc // 5 spaces (1 tab + 2 spaces)
4617 \t\t\tabc // Already tab indented
4618 \t abc // Tab followed by space
4619 \tabc ˇ // Space followed by tab (should be consumed due to tab)
4620 \t\t \tabc // Mixed indentation
4621 \t \t \t \tabc // Mixed indentation
4622 \t \tˇ
4623 « abc \t // Only the leading spaces should be convertedˇ»
4624 "}
4625 .replace("·", "")
4626 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4627 );
4628 cx.update_editor(|e, window, cx| {
4629 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4630 });
4631 cx.assert_editor_state(
4632 indoc! {"
4633 ·
4634 abc // No indentation
4635 abc // 1 space (< 3 so dont convert)
4636 abc // 2 spaces (< 3 so dont convert)
4637 «\tabc // 3 spaces (convert)ˇ»
4638 abc // 5 spaces (1 tab + 2 spaces)
4639 \t\t\tabc // Already tab indented
4640 \t abc // Tab followed by space
4641 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
4642 \t\t \tabc // Mixed indentation
4643 \t \t \t \tabc // Mixed indentation
4644 «\t\t\t
4645 \tabc \t // Only the leading spaces should be convertedˇ»
4646 "}
4647 .replace("·", "")
4648 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4649 );
4650
4651 // SINGLE SELECTION
4652 // Ln.1 "«" tests empty lines
4653 // Ln.11 tests just leading whitespace
4654 cx.set_state(indoc! {"
4655 «
4656 abc // No indentation
4657 abc // 1 space (< 3 so dont convert)
4658 abc // 2 spaces (< 3 so dont convert)
4659 abc // 3 spaces (convert)
4660 abc // 5 spaces (1 tab + 2 spaces)
4661 \t\t\tabc // Already tab indented
4662 \t abc // Tab followed by space
4663 \tabc // Space followed by tab (should be consumed due to tab)
4664 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4665 \t \t
4666 abc \t // Only the leading spaces should be convertedˇ»
4667 "});
4668 cx.update_editor(|e, window, cx| {
4669 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
4670 });
4671 cx.assert_editor_state(indoc! {"
4672 «
4673 abc // No indentation
4674 abc // 1 space (< 3 so dont convert)
4675 abc // 2 spaces (< 3 so dont convert)
4676 \tabc // 3 spaces (convert)
4677 \t abc // 5 spaces (1 tab + 2 spaces)
4678 \t\t\tabc // Already tab indented
4679 \t abc // Tab followed by space
4680 \tabc // Space followed by tab (should be consumed due to tab)
4681 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
4682 \t\t\t
4683 \tabc \t // Only the leading spaces should be convertedˇ»
4684 "});
4685}
4686
4687#[gpui::test]
4688async fn test_toggle_case(cx: &mut TestAppContext) {
4689 init_test(cx, |_| {});
4690
4691 let mut cx = EditorTestContext::new(cx).await;
4692
4693 // If all lower case -> upper case
4694 cx.set_state(indoc! {"
4695 «hello worldˇ»
4696 "});
4697 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4698 cx.assert_editor_state(indoc! {"
4699 «HELLO WORLDˇ»
4700 "});
4701
4702 // If all upper case -> lower case
4703 cx.set_state(indoc! {"
4704 «HELLO WORLDˇ»
4705 "});
4706 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4707 cx.assert_editor_state(indoc! {"
4708 «hello worldˇ»
4709 "});
4710
4711 // If any upper case characters are identified -> lower case
4712 // This matches JetBrains IDEs
4713 cx.set_state(indoc! {"
4714 «hEllo worldˇ»
4715 "});
4716 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4717 cx.assert_editor_state(indoc! {"
4718 «hello worldˇ»
4719 "});
4720}
4721
4722#[gpui::test]
4723async fn test_manipulate_text(cx: &mut TestAppContext) {
4724 init_test(cx, |_| {});
4725
4726 let mut cx = EditorTestContext::new(cx).await;
4727
4728 // Test convert_to_upper_case()
4729 cx.set_state(indoc! {"
4730 «hello worldˇ»
4731 "});
4732 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4733 cx.assert_editor_state(indoc! {"
4734 «HELLO WORLDˇ»
4735 "});
4736
4737 // Test convert_to_lower_case()
4738 cx.set_state(indoc! {"
4739 «HELLO WORLDˇ»
4740 "});
4741 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4742 cx.assert_editor_state(indoc! {"
4743 «hello worldˇ»
4744 "});
4745
4746 // Test multiple line, single selection case
4747 cx.set_state(indoc! {"
4748 «The quick brown
4749 fox jumps over
4750 the lazy dogˇ»
4751 "});
4752 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4753 cx.assert_editor_state(indoc! {"
4754 «The Quick Brown
4755 Fox Jumps Over
4756 The Lazy Dogˇ»
4757 "});
4758
4759 // Test multiple line, single selection case
4760 cx.set_state(indoc! {"
4761 «The quick brown
4762 fox jumps over
4763 the lazy dogˇ»
4764 "});
4765 cx.update_editor(|e, window, cx| {
4766 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4767 });
4768 cx.assert_editor_state(indoc! {"
4769 «TheQuickBrown
4770 FoxJumpsOver
4771 TheLazyDogˇ»
4772 "});
4773
4774 // From here on out, test more complex cases of manipulate_text()
4775
4776 // Test no selection case - should affect words cursors are in
4777 // Cursor at beginning, middle, and end of word
4778 cx.set_state(indoc! {"
4779 ˇhello big beauˇtiful worldˇ
4780 "});
4781 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4782 cx.assert_editor_state(indoc! {"
4783 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4784 "});
4785
4786 // Test multiple selections on a single line and across multiple lines
4787 cx.set_state(indoc! {"
4788 «Theˇ» quick «brown
4789 foxˇ» jumps «overˇ»
4790 the «lazyˇ» dog
4791 "});
4792 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4793 cx.assert_editor_state(indoc! {"
4794 «THEˇ» quick «BROWN
4795 FOXˇ» jumps «OVERˇ»
4796 the «LAZYˇ» dog
4797 "});
4798
4799 // Test case where text length grows
4800 cx.set_state(indoc! {"
4801 «tschüߡ»
4802 "});
4803 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4804 cx.assert_editor_state(indoc! {"
4805 «TSCHÜSSˇ»
4806 "});
4807
4808 // Test to make sure we don't crash when text shrinks
4809 cx.set_state(indoc! {"
4810 aaa_bbbˇ
4811 "});
4812 cx.update_editor(|e, window, cx| {
4813 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4814 });
4815 cx.assert_editor_state(indoc! {"
4816 «aaaBbbˇ»
4817 "});
4818
4819 // Test to make sure we all aware of the fact that each word can grow and shrink
4820 // Final selections should be aware of this fact
4821 cx.set_state(indoc! {"
4822 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4823 "});
4824 cx.update_editor(|e, window, cx| {
4825 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4826 });
4827 cx.assert_editor_state(indoc! {"
4828 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4829 "});
4830
4831 cx.set_state(indoc! {"
4832 «hElLo, WoRld!ˇ»
4833 "});
4834 cx.update_editor(|e, window, cx| {
4835 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4836 });
4837 cx.assert_editor_state(indoc! {"
4838 «HeLlO, wOrLD!ˇ»
4839 "});
4840}
4841
4842#[gpui::test]
4843fn test_duplicate_line(cx: &mut TestAppContext) {
4844 init_test(cx, |_| {});
4845
4846 let editor = cx.add_window(|window, cx| {
4847 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4848 build_editor(buffer, window, cx)
4849 });
4850 _ = editor.update(cx, |editor, window, cx| {
4851 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4852 s.select_display_ranges([
4853 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4854 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4855 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4856 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4857 ])
4858 });
4859 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4860 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4861 assert_eq!(
4862 editor.selections.display_ranges(cx),
4863 vec![
4864 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4865 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4866 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4867 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4868 ]
4869 );
4870 });
4871
4872 let editor = cx.add_window(|window, cx| {
4873 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4874 build_editor(buffer, window, cx)
4875 });
4876 _ = editor.update(cx, |editor, window, cx| {
4877 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4878 s.select_display_ranges([
4879 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4880 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4881 ])
4882 });
4883 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4884 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4885 assert_eq!(
4886 editor.selections.display_ranges(cx),
4887 vec![
4888 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4889 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4890 ]
4891 );
4892 });
4893
4894 // With `move_upwards` the selections stay in place, except for
4895 // the lines inserted above them
4896 let editor = cx.add_window(|window, cx| {
4897 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4898 build_editor(buffer, window, cx)
4899 });
4900 _ = editor.update(cx, |editor, window, cx| {
4901 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4902 s.select_display_ranges([
4903 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4904 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4905 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4906 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4907 ])
4908 });
4909 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4910 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4911 assert_eq!(
4912 editor.selections.display_ranges(cx),
4913 vec![
4914 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4915 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4916 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4917 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4918 ]
4919 );
4920 });
4921
4922 let editor = cx.add_window(|window, cx| {
4923 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4924 build_editor(buffer, window, cx)
4925 });
4926 _ = editor.update(cx, |editor, window, cx| {
4927 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4928 s.select_display_ranges([
4929 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4930 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4931 ])
4932 });
4933 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4934 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4935 assert_eq!(
4936 editor.selections.display_ranges(cx),
4937 vec![
4938 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4939 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4940 ]
4941 );
4942 });
4943
4944 let editor = cx.add_window(|window, cx| {
4945 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4946 build_editor(buffer, window, cx)
4947 });
4948 _ = editor.update(cx, |editor, window, cx| {
4949 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4950 s.select_display_ranges([
4951 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4952 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4953 ])
4954 });
4955 editor.duplicate_selection(&DuplicateSelection, window, cx);
4956 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4957 assert_eq!(
4958 editor.selections.display_ranges(cx),
4959 vec![
4960 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4961 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4962 ]
4963 );
4964 });
4965}
4966
4967#[gpui::test]
4968fn test_move_line_up_down(cx: &mut TestAppContext) {
4969 init_test(cx, |_| {});
4970
4971 let editor = cx.add_window(|window, cx| {
4972 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4973 build_editor(buffer, window, cx)
4974 });
4975 _ = editor.update(cx, |editor, window, cx| {
4976 editor.fold_creases(
4977 vec![
4978 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4979 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4980 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4981 ],
4982 true,
4983 window,
4984 cx,
4985 );
4986 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4987 s.select_display_ranges([
4988 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4989 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4990 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4991 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4992 ])
4993 });
4994 assert_eq!(
4995 editor.display_text(cx),
4996 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4997 );
4998
4999 editor.move_line_up(&MoveLineUp, window, cx);
5000 assert_eq!(
5001 editor.display_text(cx),
5002 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5003 );
5004 assert_eq!(
5005 editor.selections.display_ranges(cx),
5006 vec![
5007 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5008 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5009 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5010 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5011 ]
5012 );
5013 });
5014
5015 _ = editor.update(cx, |editor, window, cx| {
5016 editor.move_line_down(&MoveLineDown, window, cx);
5017 assert_eq!(
5018 editor.display_text(cx),
5019 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5020 );
5021 assert_eq!(
5022 editor.selections.display_ranges(cx),
5023 vec![
5024 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5025 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5026 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5027 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5028 ]
5029 );
5030 });
5031
5032 _ = editor.update(cx, |editor, window, cx| {
5033 editor.move_line_down(&MoveLineDown, window, cx);
5034 assert_eq!(
5035 editor.display_text(cx),
5036 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5037 );
5038 assert_eq!(
5039 editor.selections.display_ranges(cx),
5040 vec![
5041 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5042 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5043 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5044 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5045 ]
5046 );
5047 });
5048
5049 _ = editor.update(cx, |editor, window, cx| {
5050 editor.move_line_up(&MoveLineUp, window, cx);
5051 assert_eq!(
5052 editor.display_text(cx),
5053 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5054 );
5055 assert_eq!(
5056 editor.selections.display_ranges(cx),
5057 vec![
5058 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5059 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5060 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5061 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5062 ]
5063 );
5064 });
5065}
5066
5067#[gpui::test]
5068fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5069 init_test(cx, |_| {});
5070
5071 let editor = cx.add_window(|window, cx| {
5072 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5073 build_editor(buffer, window, cx)
5074 });
5075 _ = editor.update(cx, |editor, window, cx| {
5076 let snapshot = editor.buffer.read(cx).snapshot(cx);
5077 editor.insert_blocks(
5078 [BlockProperties {
5079 style: BlockStyle::Fixed,
5080 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5081 height: Some(1),
5082 render: Arc::new(|_| div().into_any()),
5083 priority: 0,
5084 }],
5085 Some(Autoscroll::fit()),
5086 cx,
5087 );
5088 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5089 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5090 });
5091 editor.move_line_down(&MoveLineDown, window, cx);
5092 });
5093}
5094
5095#[gpui::test]
5096async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5097 init_test(cx, |_| {});
5098
5099 let mut cx = EditorTestContext::new(cx).await;
5100 cx.set_state(
5101 &"
5102 ˇzero
5103 one
5104 two
5105 three
5106 four
5107 five
5108 "
5109 .unindent(),
5110 );
5111
5112 // Create a four-line block that replaces three lines of text.
5113 cx.update_editor(|editor, window, cx| {
5114 let snapshot = editor.snapshot(window, cx);
5115 let snapshot = &snapshot.buffer_snapshot;
5116 let placement = BlockPlacement::Replace(
5117 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5118 );
5119 editor.insert_blocks(
5120 [BlockProperties {
5121 placement,
5122 height: Some(4),
5123 style: BlockStyle::Sticky,
5124 render: Arc::new(|_| gpui::div().into_any_element()),
5125 priority: 0,
5126 }],
5127 None,
5128 cx,
5129 );
5130 });
5131
5132 // Move down so that the cursor touches the block.
5133 cx.update_editor(|editor, window, cx| {
5134 editor.move_down(&Default::default(), window, cx);
5135 });
5136 cx.assert_editor_state(
5137 &"
5138 zero
5139 «one
5140 two
5141 threeˇ»
5142 four
5143 five
5144 "
5145 .unindent(),
5146 );
5147
5148 // Move down past the block.
5149 cx.update_editor(|editor, window, cx| {
5150 editor.move_down(&Default::default(), window, cx);
5151 });
5152 cx.assert_editor_state(
5153 &"
5154 zero
5155 one
5156 two
5157 three
5158 ˇfour
5159 five
5160 "
5161 .unindent(),
5162 );
5163}
5164
5165#[gpui::test]
5166fn test_transpose(cx: &mut TestAppContext) {
5167 init_test(cx, |_| {});
5168
5169 _ = cx.add_window(|window, cx| {
5170 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5171 editor.set_style(EditorStyle::default(), window, cx);
5172 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5173 s.select_ranges([1..1])
5174 });
5175 editor.transpose(&Default::default(), window, cx);
5176 assert_eq!(editor.text(cx), "bac");
5177 assert_eq!(editor.selections.ranges(cx), [2..2]);
5178
5179 editor.transpose(&Default::default(), window, cx);
5180 assert_eq!(editor.text(cx), "bca");
5181 assert_eq!(editor.selections.ranges(cx), [3..3]);
5182
5183 editor.transpose(&Default::default(), window, cx);
5184 assert_eq!(editor.text(cx), "bac");
5185 assert_eq!(editor.selections.ranges(cx), [3..3]);
5186
5187 editor
5188 });
5189
5190 _ = cx.add_window(|window, cx| {
5191 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5192 editor.set_style(EditorStyle::default(), window, cx);
5193 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5194 s.select_ranges([3..3])
5195 });
5196 editor.transpose(&Default::default(), window, cx);
5197 assert_eq!(editor.text(cx), "acb\nde");
5198 assert_eq!(editor.selections.ranges(cx), [3..3]);
5199
5200 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5201 s.select_ranges([4..4])
5202 });
5203 editor.transpose(&Default::default(), window, cx);
5204 assert_eq!(editor.text(cx), "acbd\ne");
5205 assert_eq!(editor.selections.ranges(cx), [5..5]);
5206
5207 editor.transpose(&Default::default(), window, cx);
5208 assert_eq!(editor.text(cx), "acbde\n");
5209 assert_eq!(editor.selections.ranges(cx), [6..6]);
5210
5211 editor.transpose(&Default::default(), window, cx);
5212 assert_eq!(editor.text(cx), "acbd\ne");
5213 assert_eq!(editor.selections.ranges(cx), [6..6]);
5214
5215 editor
5216 });
5217
5218 _ = cx.add_window(|window, cx| {
5219 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5220 editor.set_style(EditorStyle::default(), window, cx);
5221 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5222 s.select_ranges([1..1, 2..2, 4..4])
5223 });
5224 editor.transpose(&Default::default(), window, cx);
5225 assert_eq!(editor.text(cx), "bacd\ne");
5226 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5227
5228 editor.transpose(&Default::default(), window, cx);
5229 assert_eq!(editor.text(cx), "bcade\n");
5230 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5231
5232 editor.transpose(&Default::default(), window, cx);
5233 assert_eq!(editor.text(cx), "bcda\ne");
5234 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5235
5236 editor.transpose(&Default::default(), window, cx);
5237 assert_eq!(editor.text(cx), "bcade\n");
5238 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5239
5240 editor.transpose(&Default::default(), window, cx);
5241 assert_eq!(editor.text(cx), "bcaed\n");
5242 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5243
5244 editor
5245 });
5246
5247 _ = cx.add_window(|window, cx| {
5248 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5249 editor.set_style(EditorStyle::default(), window, cx);
5250 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5251 s.select_ranges([4..4])
5252 });
5253 editor.transpose(&Default::default(), window, cx);
5254 assert_eq!(editor.text(cx), "🏀🍐✋");
5255 assert_eq!(editor.selections.ranges(cx), [8..8]);
5256
5257 editor.transpose(&Default::default(), window, cx);
5258 assert_eq!(editor.text(cx), "🏀✋🍐");
5259 assert_eq!(editor.selections.ranges(cx), [11..11]);
5260
5261 editor.transpose(&Default::default(), window, cx);
5262 assert_eq!(editor.text(cx), "🏀🍐✋");
5263 assert_eq!(editor.selections.ranges(cx), [11..11]);
5264
5265 editor
5266 });
5267}
5268
5269#[gpui::test]
5270async fn test_rewrap(cx: &mut TestAppContext) {
5271 init_test(cx, |settings| {
5272 settings.languages.0.extend([
5273 (
5274 "Markdown".into(),
5275 LanguageSettingsContent {
5276 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5277 preferred_line_length: Some(40),
5278 ..Default::default()
5279 },
5280 ),
5281 (
5282 "Plain Text".into(),
5283 LanguageSettingsContent {
5284 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5285 preferred_line_length: Some(40),
5286 ..Default::default()
5287 },
5288 ),
5289 (
5290 "C++".into(),
5291 LanguageSettingsContent {
5292 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5293 preferred_line_length: Some(40),
5294 ..Default::default()
5295 },
5296 ),
5297 (
5298 "Python".into(),
5299 LanguageSettingsContent {
5300 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5301 preferred_line_length: Some(40),
5302 ..Default::default()
5303 },
5304 ),
5305 (
5306 "Rust".into(),
5307 LanguageSettingsContent {
5308 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5309 preferred_line_length: Some(40),
5310 ..Default::default()
5311 },
5312 ),
5313 ])
5314 });
5315
5316 let mut cx = EditorTestContext::new(cx).await;
5317
5318 let cpp_language = Arc::new(Language::new(
5319 LanguageConfig {
5320 name: "C++".into(),
5321 line_comments: vec!["// ".into()],
5322 ..LanguageConfig::default()
5323 },
5324 None,
5325 ));
5326 let python_language = Arc::new(Language::new(
5327 LanguageConfig {
5328 name: "Python".into(),
5329 line_comments: vec!["# ".into()],
5330 ..LanguageConfig::default()
5331 },
5332 None,
5333 ));
5334 let markdown_language = Arc::new(Language::new(
5335 LanguageConfig {
5336 name: "Markdown".into(),
5337 rewrap_prefixes: vec![
5338 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5339 regex::Regex::new("[-*+]\\s+").unwrap(),
5340 ],
5341 ..LanguageConfig::default()
5342 },
5343 None,
5344 ));
5345 let rust_language = Arc::new(Language::new(
5346 LanguageConfig {
5347 name: "Rust".into(),
5348 line_comments: vec!["// ".into(), "/// ".into()],
5349 ..LanguageConfig::default()
5350 },
5351 Some(tree_sitter_rust::LANGUAGE.into()),
5352 ));
5353
5354 let plaintext_language = Arc::new(Language::new(
5355 LanguageConfig {
5356 name: "Plain Text".into(),
5357 ..LanguageConfig::default()
5358 },
5359 None,
5360 ));
5361
5362 // Test basic rewrapping of a long line with a cursor
5363 assert_rewrap(
5364 indoc! {"
5365 // ˇThis is a long comment that needs to be wrapped.
5366 "},
5367 indoc! {"
5368 // ˇThis is a long comment that needs to
5369 // be wrapped.
5370 "},
5371 cpp_language.clone(),
5372 &mut cx,
5373 );
5374
5375 // Test rewrapping a full selection
5376 assert_rewrap(
5377 indoc! {"
5378 «// This selected long comment needs to be wrapped.ˇ»"
5379 },
5380 indoc! {"
5381 «// This selected long comment needs to
5382 // be wrapped.ˇ»"
5383 },
5384 cpp_language.clone(),
5385 &mut cx,
5386 );
5387
5388 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5389 assert_rewrap(
5390 indoc! {"
5391 // ˇThis is the first line.
5392 // Thisˇ is the second line.
5393 // This is the thirdˇ line, all part of one paragraph.
5394 "},
5395 indoc! {"
5396 // ˇThis is the first line. Thisˇ is the
5397 // second line. This is the thirdˇ line,
5398 // all part of one paragraph.
5399 "},
5400 cpp_language.clone(),
5401 &mut cx,
5402 );
5403
5404 // Test multiple cursors in different paragraphs trigger separate rewraps
5405 assert_rewrap(
5406 indoc! {"
5407 // ˇThis is the first paragraph, first line.
5408 // ˇThis is the first paragraph, second line.
5409
5410 // ˇThis is the second paragraph, first line.
5411 // ˇThis is the second paragraph, second line.
5412 "},
5413 indoc! {"
5414 // ˇThis is the first paragraph, first
5415 // line. ˇThis is the first paragraph,
5416 // second line.
5417
5418 // ˇThis is the second paragraph, first
5419 // line. ˇThis is the second paragraph,
5420 // second line.
5421 "},
5422 cpp_language.clone(),
5423 &mut cx,
5424 );
5425
5426 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
5427 assert_rewrap(
5428 indoc! {"
5429 «// A regular long long comment to be wrapped.
5430 /// A documentation long comment to be wrapped.ˇ»
5431 "},
5432 indoc! {"
5433 «// A regular long long comment to be
5434 // wrapped.
5435 /// A documentation long comment to be
5436 /// wrapped.ˇ»
5437 "},
5438 rust_language.clone(),
5439 &mut cx,
5440 );
5441
5442 // Test that change in indentation level trigger seperate rewraps
5443 assert_rewrap(
5444 indoc! {"
5445 fn foo() {
5446 «// This is a long comment at the base indent.
5447 // This is a long comment at the next indent.ˇ»
5448 }
5449 "},
5450 indoc! {"
5451 fn foo() {
5452 «// This is a long comment at the
5453 // base indent.
5454 // This is a long comment at the
5455 // next indent.ˇ»
5456 }
5457 "},
5458 rust_language.clone(),
5459 &mut cx,
5460 );
5461
5462 // Test that different comment prefix characters (e.g., '#') are handled correctly
5463 assert_rewrap(
5464 indoc! {"
5465 # ˇThis is a long comment using a pound sign.
5466 "},
5467 indoc! {"
5468 # ˇThis is a long comment using a pound
5469 # sign.
5470 "},
5471 python_language.clone(),
5472 &mut cx,
5473 );
5474
5475 // Test rewrapping only affects comments, not code even when selected
5476 assert_rewrap(
5477 indoc! {"
5478 «/// This doc comment is long and should be wrapped.
5479 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5480 "},
5481 indoc! {"
5482 «/// This doc comment is long and should
5483 /// be wrapped.
5484 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
5485 "},
5486 rust_language.clone(),
5487 &mut cx,
5488 );
5489
5490 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
5491 assert_rewrap(
5492 indoc! {"
5493 # Header
5494
5495 A long long long line of markdown text to wrap.ˇ
5496 "},
5497 indoc! {"
5498 # Header
5499
5500 A long long long line of markdown text
5501 to wrap.ˇ
5502 "},
5503 markdown_language.clone(),
5504 &mut cx,
5505 );
5506
5507 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
5508 assert_rewrap(
5509 indoc! {"
5510 «1. This is a numbered list item that is very long and needs to be wrapped properly.
5511 2. This is a numbered list item that is very long and needs to be wrapped properly.
5512 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
5513 "},
5514 indoc! {"
5515 «1. This is a numbered list item that is
5516 very long and needs to be wrapped
5517 properly.
5518 2. This is a numbered list item that is
5519 very long and needs to be wrapped
5520 properly.
5521 - This is an unordered list item that is
5522 also very long and should not merge
5523 with the numbered item.ˇ»
5524 "},
5525 markdown_language.clone(),
5526 &mut cx,
5527 );
5528
5529 // Test that rewrapping add indents for rewrapping boundary if not exists already.
5530 assert_rewrap(
5531 indoc! {"
5532 «1. This is a numbered list item that is
5533 very long and needs to be wrapped
5534 properly.
5535 2. This is a numbered list item that is
5536 very long and needs to be wrapped
5537 properly.
5538 - This is an unordered list item that is
5539 also very long and should not merge with
5540 the numbered item.ˇ»
5541 "},
5542 indoc! {"
5543 «1. This is a numbered list item that is
5544 very long and needs to be wrapped
5545 properly.
5546 2. This is a numbered list item that is
5547 very long and needs to be wrapped
5548 properly.
5549 - This is an unordered list item that is
5550 also very long and should not merge
5551 with the numbered item.ˇ»
5552 "},
5553 markdown_language.clone(),
5554 &mut cx,
5555 );
5556
5557 // Test that rewrapping maintain indents even when they already exists.
5558 assert_rewrap(
5559 indoc! {"
5560 «1. This is a numbered list
5561 item that is very long and needs to be wrapped properly.
5562 2. This is a numbered list
5563 item that is very long and needs to be wrapped properly.
5564 - This is an unordered list item that is also very long and
5565 should not merge with the numbered item.ˇ»
5566 "},
5567 indoc! {"
5568 «1. This is a numbered list item that is
5569 very long and needs to be wrapped
5570 properly.
5571 2. This is a numbered list item that is
5572 very long and needs to be wrapped
5573 properly.
5574 - This is an unordered list item that is
5575 also very long and should not merge
5576 with the numbered item.ˇ»
5577 "},
5578 markdown_language.clone(),
5579 &mut cx,
5580 );
5581
5582 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
5583 assert_rewrap(
5584 indoc! {"
5585 ˇThis is a very long line of plain text that will be wrapped.
5586 "},
5587 indoc! {"
5588 ˇThis is a very long line of plain text
5589 that will be wrapped.
5590 "},
5591 plaintext_language.clone(),
5592 &mut cx,
5593 );
5594
5595 // Test that non-commented code acts as a paragraph boundary within a selection
5596 assert_rewrap(
5597 indoc! {"
5598 «// This is the first long comment block to be wrapped.
5599 fn my_func(a: u32);
5600 // This is the second long comment block to be wrapped.ˇ»
5601 "},
5602 indoc! {"
5603 «// This is the first long comment block
5604 // to be wrapped.
5605 fn my_func(a: u32);
5606 // This is the second long comment block
5607 // to be wrapped.ˇ»
5608 "},
5609 rust_language.clone(),
5610 &mut cx,
5611 );
5612
5613 // Test rewrapping multiple selections, including ones with blank lines or tabs
5614 assert_rewrap(
5615 indoc! {"
5616 «ˇThis is a very long line that will be wrapped.
5617
5618 This is another paragraph in the same selection.»
5619
5620 «\tThis is a very long indented line that will be wrapped.ˇ»
5621 "},
5622 indoc! {"
5623 «ˇThis is a very long line that will be
5624 wrapped.
5625
5626 This is another paragraph in the same
5627 selection.»
5628
5629 «\tThis is a very long indented line
5630 \tthat will be wrapped.ˇ»
5631 "},
5632 plaintext_language.clone(),
5633 &mut cx,
5634 );
5635
5636 // Test that an empty comment line acts as a paragraph boundary
5637 assert_rewrap(
5638 indoc! {"
5639 // ˇThis is a long comment that will be wrapped.
5640 //
5641 // And this is another long comment that will also be wrapped.ˇ
5642 "},
5643 indoc! {"
5644 // ˇThis is a long comment that will be
5645 // wrapped.
5646 //
5647 // And this is another long comment that
5648 // will also be wrapped.ˇ
5649 "},
5650 cpp_language,
5651 &mut cx,
5652 );
5653
5654 #[track_caller]
5655 fn assert_rewrap(
5656 unwrapped_text: &str,
5657 wrapped_text: &str,
5658 language: Arc<Language>,
5659 cx: &mut EditorTestContext,
5660 ) {
5661 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5662 cx.set_state(unwrapped_text);
5663 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5664 cx.assert_editor_state(wrapped_text);
5665 }
5666}
5667
5668#[gpui::test]
5669async fn test_hard_wrap(cx: &mut TestAppContext) {
5670 init_test(cx, |_| {});
5671 let mut cx = EditorTestContext::new(cx).await;
5672
5673 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5674 cx.update_editor(|editor, _, cx| {
5675 editor.set_hard_wrap(Some(14), cx);
5676 });
5677
5678 cx.set_state(indoc!(
5679 "
5680 one two three ˇ
5681 "
5682 ));
5683 cx.simulate_input("four");
5684 cx.run_until_parked();
5685
5686 cx.assert_editor_state(indoc!(
5687 "
5688 one two three
5689 fourˇ
5690 "
5691 ));
5692
5693 cx.update_editor(|editor, window, cx| {
5694 editor.newline(&Default::default(), window, cx);
5695 });
5696 cx.run_until_parked();
5697 cx.assert_editor_state(indoc!(
5698 "
5699 one two three
5700 four
5701 ˇ
5702 "
5703 ));
5704
5705 cx.simulate_input("five");
5706 cx.run_until_parked();
5707 cx.assert_editor_state(indoc!(
5708 "
5709 one two three
5710 four
5711 fiveˇ
5712 "
5713 ));
5714
5715 cx.update_editor(|editor, window, cx| {
5716 editor.newline(&Default::default(), window, cx);
5717 });
5718 cx.run_until_parked();
5719 cx.simulate_input("# ");
5720 cx.run_until_parked();
5721 cx.assert_editor_state(indoc!(
5722 "
5723 one two three
5724 four
5725 five
5726 # ˇ
5727 "
5728 ));
5729
5730 cx.update_editor(|editor, window, cx| {
5731 editor.newline(&Default::default(), window, cx);
5732 });
5733 cx.run_until_parked();
5734 cx.assert_editor_state(indoc!(
5735 "
5736 one two three
5737 four
5738 five
5739 #\x20
5740 #ˇ
5741 "
5742 ));
5743
5744 cx.simulate_input(" 6");
5745 cx.run_until_parked();
5746 cx.assert_editor_state(indoc!(
5747 "
5748 one two three
5749 four
5750 five
5751 #
5752 # 6ˇ
5753 "
5754 ));
5755}
5756
5757#[gpui::test]
5758async fn test_clipboard(cx: &mut TestAppContext) {
5759 init_test(cx, |_| {});
5760
5761 let mut cx = EditorTestContext::new(cx).await;
5762
5763 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5764 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5765 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5766
5767 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5768 cx.set_state("two ˇfour ˇsix ˇ");
5769 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5770 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5771
5772 // Paste again but with only two cursors. Since the number of cursors doesn't
5773 // match the number of slices in the clipboard, the entire clipboard text
5774 // is pasted at each cursor.
5775 cx.set_state("ˇtwo one✅ four three six five ˇ");
5776 cx.update_editor(|e, window, cx| {
5777 e.handle_input("( ", window, cx);
5778 e.paste(&Paste, window, cx);
5779 e.handle_input(") ", window, cx);
5780 });
5781 cx.assert_editor_state(
5782 &([
5783 "( one✅ ",
5784 "three ",
5785 "five ) ˇtwo one✅ four three six five ( one✅ ",
5786 "three ",
5787 "five ) ˇ",
5788 ]
5789 .join("\n")),
5790 );
5791
5792 // Cut with three selections, one of which is full-line.
5793 cx.set_state(indoc! {"
5794 1«2ˇ»3
5795 4ˇ567
5796 «8ˇ»9"});
5797 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5798 cx.assert_editor_state(indoc! {"
5799 1ˇ3
5800 ˇ9"});
5801
5802 // Paste with three selections, noticing how the copied selection that was full-line
5803 // gets inserted before the second cursor.
5804 cx.set_state(indoc! {"
5805 1ˇ3
5806 9ˇ
5807 «oˇ»ne"});
5808 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5809 cx.assert_editor_state(indoc! {"
5810 12ˇ3
5811 4567
5812 9ˇ
5813 8ˇne"});
5814
5815 // Copy with a single cursor only, which writes the whole line into the clipboard.
5816 cx.set_state(indoc! {"
5817 The quick brown
5818 fox juˇmps over
5819 the lazy dog"});
5820 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5821 assert_eq!(
5822 cx.read_from_clipboard()
5823 .and_then(|item| item.text().as_deref().map(str::to_string)),
5824 Some("fox jumps over\n".to_string())
5825 );
5826
5827 // Paste with three selections, noticing how the copied full-line selection is inserted
5828 // before the empty selections but replaces the selection that is non-empty.
5829 cx.set_state(indoc! {"
5830 Tˇhe quick brown
5831 «foˇ»x jumps over
5832 tˇhe lazy dog"});
5833 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5834 cx.assert_editor_state(indoc! {"
5835 fox jumps over
5836 Tˇhe quick brown
5837 fox jumps over
5838 ˇx jumps over
5839 fox jumps over
5840 tˇhe lazy dog"});
5841}
5842
5843#[gpui::test]
5844async fn test_copy_trim(cx: &mut TestAppContext) {
5845 init_test(cx, |_| {});
5846
5847 let mut cx = EditorTestContext::new(cx).await;
5848 cx.set_state(
5849 r#" «for selection in selections.iter() {
5850 let mut start = selection.start;
5851 let mut end = selection.end;
5852 let is_entire_line = selection.is_empty();
5853 if is_entire_line {
5854 start = Point::new(start.row, 0);ˇ»
5855 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5856 }
5857 "#,
5858 );
5859 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5860 assert_eq!(
5861 cx.read_from_clipboard()
5862 .and_then(|item| item.text().as_deref().map(str::to_string)),
5863 Some(
5864 "for selection in selections.iter() {
5865 let mut start = selection.start;
5866 let mut end = selection.end;
5867 let is_entire_line = selection.is_empty();
5868 if is_entire_line {
5869 start = Point::new(start.row, 0);"
5870 .to_string()
5871 ),
5872 "Regular copying preserves all indentation selected",
5873 );
5874 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5875 assert_eq!(
5876 cx.read_from_clipboard()
5877 .and_then(|item| item.text().as_deref().map(str::to_string)),
5878 Some(
5879 "for selection in selections.iter() {
5880let mut start = selection.start;
5881let mut end = selection.end;
5882let is_entire_line = selection.is_empty();
5883if is_entire_line {
5884 start = Point::new(start.row, 0);"
5885 .to_string()
5886 ),
5887 "Copying with stripping should strip all leading whitespaces"
5888 );
5889
5890 cx.set_state(
5891 r#" « for selection in selections.iter() {
5892 let mut start = selection.start;
5893 let mut end = selection.end;
5894 let is_entire_line = selection.is_empty();
5895 if is_entire_line {
5896 start = Point::new(start.row, 0);ˇ»
5897 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5898 }
5899 "#,
5900 );
5901 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5902 assert_eq!(
5903 cx.read_from_clipboard()
5904 .and_then(|item| item.text().as_deref().map(str::to_string)),
5905 Some(
5906 " for selection in selections.iter() {
5907 let mut start = selection.start;
5908 let mut end = selection.end;
5909 let is_entire_line = selection.is_empty();
5910 if is_entire_line {
5911 start = Point::new(start.row, 0);"
5912 .to_string()
5913 ),
5914 "Regular copying preserves all indentation selected",
5915 );
5916 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5917 assert_eq!(
5918 cx.read_from_clipboard()
5919 .and_then(|item| item.text().as_deref().map(str::to_string)),
5920 Some(
5921 "for selection in selections.iter() {
5922let mut start = selection.start;
5923let mut end = selection.end;
5924let is_entire_line = selection.is_empty();
5925if is_entire_line {
5926 start = Point::new(start.row, 0);"
5927 .to_string()
5928 ),
5929 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5930 );
5931
5932 cx.set_state(
5933 r#" «ˇ for selection in selections.iter() {
5934 let mut start = selection.start;
5935 let mut end = selection.end;
5936 let is_entire_line = selection.is_empty();
5937 if is_entire_line {
5938 start = Point::new(start.row, 0);»
5939 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5940 }
5941 "#,
5942 );
5943 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5944 assert_eq!(
5945 cx.read_from_clipboard()
5946 .and_then(|item| item.text().as_deref().map(str::to_string)),
5947 Some(
5948 " for selection in selections.iter() {
5949 let mut start = selection.start;
5950 let mut end = selection.end;
5951 let is_entire_line = selection.is_empty();
5952 if is_entire_line {
5953 start = Point::new(start.row, 0);"
5954 .to_string()
5955 ),
5956 "Regular copying for reverse selection works the same",
5957 );
5958 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5959 assert_eq!(
5960 cx.read_from_clipboard()
5961 .and_then(|item| item.text().as_deref().map(str::to_string)),
5962 Some(
5963 "for selection in selections.iter() {
5964let mut start = selection.start;
5965let mut end = selection.end;
5966let is_entire_line = selection.is_empty();
5967if is_entire_line {
5968 start = Point::new(start.row, 0);"
5969 .to_string()
5970 ),
5971 "Copying with stripping for reverse selection works the same"
5972 );
5973
5974 cx.set_state(
5975 r#" for selection «in selections.iter() {
5976 let mut start = selection.start;
5977 let mut end = selection.end;
5978 let is_entire_line = selection.is_empty();
5979 if is_entire_line {
5980 start = Point::new(start.row, 0);ˇ»
5981 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5982 }
5983 "#,
5984 );
5985 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5986 assert_eq!(
5987 cx.read_from_clipboard()
5988 .and_then(|item| item.text().as_deref().map(str::to_string)),
5989 Some(
5990 "in selections.iter() {
5991 let mut start = selection.start;
5992 let mut end = selection.end;
5993 let is_entire_line = selection.is_empty();
5994 if is_entire_line {
5995 start = Point::new(start.row, 0);"
5996 .to_string()
5997 ),
5998 "When selecting past the indent, the copying works as usual",
5999 );
6000 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6001 assert_eq!(
6002 cx.read_from_clipboard()
6003 .and_then(|item| item.text().as_deref().map(str::to_string)),
6004 Some(
6005 "in selections.iter() {
6006 let mut start = selection.start;
6007 let mut end = selection.end;
6008 let is_entire_line = selection.is_empty();
6009 if is_entire_line {
6010 start = Point::new(start.row, 0);"
6011 .to_string()
6012 ),
6013 "When selecting past the indent, nothing is trimmed"
6014 );
6015
6016 cx.set_state(
6017 r#" «for selection in selections.iter() {
6018 let mut start = selection.start;
6019
6020 let mut end = selection.end;
6021 let is_entire_line = selection.is_empty();
6022 if is_entire_line {
6023 start = Point::new(start.row, 0);
6024ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
6025 }
6026 "#,
6027 );
6028 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6029 assert_eq!(
6030 cx.read_from_clipboard()
6031 .and_then(|item| item.text().as_deref().map(str::to_string)),
6032 Some(
6033 "for selection in selections.iter() {
6034let mut start = selection.start;
6035
6036let mut end = selection.end;
6037let is_entire_line = selection.is_empty();
6038if is_entire_line {
6039 start = Point::new(start.row, 0);
6040"
6041 .to_string()
6042 ),
6043 "Copying with stripping should ignore empty lines"
6044 );
6045}
6046
6047#[gpui::test]
6048async fn test_paste_multiline(cx: &mut TestAppContext) {
6049 init_test(cx, |_| {});
6050
6051 let mut cx = EditorTestContext::new(cx).await;
6052 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6053
6054 // Cut an indented block, without the leading whitespace.
6055 cx.set_state(indoc! {"
6056 const a: B = (
6057 c(),
6058 «d(
6059 e,
6060 f
6061 )ˇ»
6062 );
6063 "});
6064 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6065 cx.assert_editor_state(indoc! {"
6066 const a: B = (
6067 c(),
6068 ˇ
6069 );
6070 "});
6071
6072 // Paste it at the same position.
6073 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6074 cx.assert_editor_state(indoc! {"
6075 const a: B = (
6076 c(),
6077 d(
6078 e,
6079 f
6080 )ˇ
6081 );
6082 "});
6083
6084 // Paste it at a line with a lower indent level.
6085 cx.set_state(indoc! {"
6086 ˇ
6087 const a: B = (
6088 c(),
6089 );
6090 "});
6091 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6092 cx.assert_editor_state(indoc! {"
6093 d(
6094 e,
6095 f
6096 )ˇ
6097 const a: B = (
6098 c(),
6099 );
6100 "});
6101
6102 // Cut an indented block, with the leading whitespace.
6103 cx.set_state(indoc! {"
6104 const a: B = (
6105 c(),
6106 « d(
6107 e,
6108 f
6109 )
6110 ˇ»);
6111 "});
6112 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6113 cx.assert_editor_state(indoc! {"
6114 const a: B = (
6115 c(),
6116 ˇ);
6117 "});
6118
6119 // Paste it at the same position.
6120 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6121 cx.assert_editor_state(indoc! {"
6122 const a: B = (
6123 c(),
6124 d(
6125 e,
6126 f
6127 )
6128 ˇ);
6129 "});
6130
6131 // Paste it at a line with a higher indent level.
6132 cx.set_state(indoc! {"
6133 const a: B = (
6134 c(),
6135 d(
6136 e,
6137 fˇ
6138 )
6139 );
6140 "});
6141 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6142 cx.assert_editor_state(indoc! {"
6143 const a: B = (
6144 c(),
6145 d(
6146 e,
6147 f d(
6148 e,
6149 f
6150 )
6151 ˇ
6152 )
6153 );
6154 "});
6155
6156 // Copy an indented block, starting mid-line
6157 cx.set_state(indoc! {"
6158 const a: B = (
6159 c(),
6160 somethin«g(
6161 e,
6162 f
6163 )ˇ»
6164 );
6165 "});
6166 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6167
6168 // Paste it on a line with a lower indent level
6169 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
6170 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6171 cx.assert_editor_state(indoc! {"
6172 const a: B = (
6173 c(),
6174 something(
6175 e,
6176 f
6177 )
6178 );
6179 g(
6180 e,
6181 f
6182 )ˇ"});
6183}
6184
6185#[gpui::test]
6186async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
6187 init_test(cx, |_| {});
6188
6189 cx.write_to_clipboard(ClipboardItem::new_string(
6190 " d(\n e\n );\n".into(),
6191 ));
6192
6193 let mut cx = EditorTestContext::new(cx).await;
6194 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6195
6196 cx.set_state(indoc! {"
6197 fn a() {
6198 b();
6199 if c() {
6200 ˇ
6201 }
6202 }
6203 "});
6204
6205 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6206 cx.assert_editor_state(indoc! {"
6207 fn a() {
6208 b();
6209 if c() {
6210 d(
6211 e
6212 );
6213 ˇ
6214 }
6215 }
6216 "});
6217
6218 cx.set_state(indoc! {"
6219 fn a() {
6220 b();
6221 ˇ
6222 }
6223 "});
6224
6225 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6226 cx.assert_editor_state(indoc! {"
6227 fn a() {
6228 b();
6229 d(
6230 e
6231 );
6232 ˇ
6233 }
6234 "});
6235}
6236
6237#[gpui::test]
6238fn test_select_all(cx: &mut TestAppContext) {
6239 init_test(cx, |_| {});
6240
6241 let editor = cx.add_window(|window, cx| {
6242 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
6243 build_editor(buffer, window, cx)
6244 });
6245 _ = editor.update(cx, |editor, window, cx| {
6246 editor.select_all(&SelectAll, window, cx);
6247 assert_eq!(
6248 editor.selections.display_ranges(cx),
6249 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
6250 );
6251 });
6252}
6253
6254#[gpui::test]
6255fn test_select_line(cx: &mut TestAppContext) {
6256 init_test(cx, |_| {});
6257
6258 let editor = cx.add_window(|window, cx| {
6259 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
6260 build_editor(buffer, window, cx)
6261 });
6262 _ = editor.update(cx, |editor, window, cx| {
6263 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6264 s.select_display_ranges([
6265 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6266 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6267 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6268 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
6269 ])
6270 });
6271 editor.select_line(&SelectLine, window, cx);
6272 assert_eq!(
6273 editor.selections.display_ranges(cx),
6274 vec![
6275 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
6276 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
6277 ]
6278 );
6279 });
6280
6281 _ = editor.update(cx, |editor, window, cx| {
6282 editor.select_line(&SelectLine, window, cx);
6283 assert_eq!(
6284 editor.selections.display_ranges(cx),
6285 vec![
6286 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
6287 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
6288 ]
6289 );
6290 });
6291
6292 _ = editor.update(cx, |editor, window, cx| {
6293 editor.select_line(&SelectLine, window, cx);
6294 assert_eq!(
6295 editor.selections.display_ranges(cx),
6296 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
6297 );
6298 });
6299}
6300
6301#[gpui::test]
6302async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
6303 init_test(cx, |_| {});
6304 let mut cx = EditorTestContext::new(cx).await;
6305
6306 #[track_caller]
6307 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
6308 cx.set_state(initial_state);
6309 cx.update_editor(|e, window, cx| {
6310 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
6311 });
6312 cx.assert_editor_state(expected_state);
6313 }
6314
6315 // Selection starts and ends at the middle of lines, left-to-right
6316 test(
6317 &mut cx,
6318 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
6319 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6320 );
6321 // Same thing, right-to-left
6322 test(
6323 &mut cx,
6324 "aa\nb«b\ncc\ndd\neˇ»e\nff",
6325 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
6326 );
6327
6328 // Whole buffer, left-to-right, last line *doesn't* end with newline
6329 test(
6330 &mut cx,
6331 "«ˇaa\nbb\ncc\ndd\nee\nff»",
6332 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6333 );
6334 // Same thing, right-to-left
6335 test(
6336 &mut cx,
6337 "«aa\nbb\ncc\ndd\nee\nffˇ»",
6338 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
6339 );
6340
6341 // Whole buffer, left-to-right, last line ends with newline
6342 test(
6343 &mut cx,
6344 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
6345 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6346 );
6347 // Same thing, right-to-left
6348 test(
6349 &mut cx,
6350 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
6351 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
6352 );
6353
6354 // Starts at the end of a line, ends at the start of another
6355 test(
6356 &mut cx,
6357 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
6358 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
6359 );
6360}
6361
6362#[gpui::test]
6363async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
6364 init_test(cx, |_| {});
6365
6366 let editor = cx.add_window(|window, cx| {
6367 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
6368 build_editor(buffer, window, cx)
6369 });
6370
6371 // setup
6372 _ = editor.update(cx, |editor, window, cx| {
6373 editor.fold_creases(
6374 vec![
6375 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
6376 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
6377 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
6378 ],
6379 true,
6380 window,
6381 cx,
6382 );
6383 assert_eq!(
6384 editor.display_text(cx),
6385 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6386 );
6387 });
6388
6389 _ = editor.update(cx, |editor, window, cx| {
6390 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6391 s.select_display_ranges([
6392 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
6393 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
6394 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
6395 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
6396 ])
6397 });
6398 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6399 assert_eq!(
6400 editor.display_text(cx),
6401 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
6402 );
6403 });
6404 EditorTestContext::for_editor(editor, cx)
6405 .await
6406 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
6407
6408 _ = editor.update(cx, |editor, window, cx| {
6409 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6410 s.select_display_ranges([
6411 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
6412 ])
6413 });
6414 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
6415 assert_eq!(
6416 editor.display_text(cx),
6417 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
6418 );
6419 assert_eq!(
6420 editor.selections.display_ranges(cx),
6421 [
6422 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
6423 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
6424 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
6425 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
6426 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
6427 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
6428 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
6429 ]
6430 );
6431 });
6432 EditorTestContext::for_editor(editor, cx)
6433 .await
6434 .assert_editor_state(
6435 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
6436 );
6437}
6438
6439#[gpui::test]
6440async fn test_add_selection_above_below(cx: &mut TestAppContext) {
6441 init_test(cx, |_| {});
6442
6443 let mut cx = EditorTestContext::new(cx).await;
6444
6445 cx.set_state(indoc!(
6446 r#"abc
6447 defˇghi
6448
6449 jk
6450 nlmo
6451 "#
6452 ));
6453
6454 cx.update_editor(|editor, window, cx| {
6455 editor.add_selection_above(&Default::default(), window, cx);
6456 });
6457
6458 cx.assert_editor_state(indoc!(
6459 r#"abcˇ
6460 defˇghi
6461
6462 jk
6463 nlmo
6464 "#
6465 ));
6466
6467 cx.update_editor(|editor, window, cx| {
6468 editor.add_selection_above(&Default::default(), window, cx);
6469 });
6470
6471 cx.assert_editor_state(indoc!(
6472 r#"abcˇ
6473 defˇghi
6474
6475 jk
6476 nlmo
6477 "#
6478 ));
6479
6480 cx.update_editor(|editor, window, cx| {
6481 editor.add_selection_below(&Default::default(), window, cx);
6482 });
6483
6484 cx.assert_editor_state(indoc!(
6485 r#"abc
6486 defˇghi
6487
6488 jk
6489 nlmo
6490 "#
6491 ));
6492
6493 cx.update_editor(|editor, window, cx| {
6494 editor.undo_selection(&Default::default(), window, cx);
6495 });
6496
6497 cx.assert_editor_state(indoc!(
6498 r#"abcˇ
6499 defˇghi
6500
6501 jk
6502 nlmo
6503 "#
6504 ));
6505
6506 cx.update_editor(|editor, window, cx| {
6507 editor.redo_selection(&Default::default(), window, cx);
6508 });
6509
6510 cx.assert_editor_state(indoc!(
6511 r#"abc
6512 defˇghi
6513
6514 jk
6515 nlmo
6516 "#
6517 ));
6518
6519 cx.update_editor(|editor, window, cx| {
6520 editor.add_selection_below(&Default::default(), window, cx);
6521 });
6522
6523 cx.assert_editor_state(indoc!(
6524 r#"abc
6525 defˇghi
6526 ˇ
6527 jk
6528 nlmo
6529 "#
6530 ));
6531
6532 cx.update_editor(|editor, window, cx| {
6533 editor.add_selection_below(&Default::default(), window, cx);
6534 });
6535
6536 cx.assert_editor_state(indoc!(
6537 r#"abc
6538 defˇghi
6539 ˇ
6540 jkˇ
6541 nlmo
6542 "#
6543 ));
6544
6545 cx.update_editor(|editor, window, cx| {
6546 editor.add_selection_below(&Default::default(), window, cx);
6547 });
6548
6549 cx.assert_editor_state(indoc!(
6550 r#"abc
6551 defˇghi
6552 ˇ
6553 jkˇ
6554 nlmˇo
6555 "#
6556 ));
6557
6558 cx.update_editor(|editor, window, cx| {
6559 editor.add_selection_below(&Default::default(), window, cx);
6560 });
6561
6562 cx.assert_editor_state(indoc!(
6563 r#"abc
6564 defˇghi
6565 ˇ
6566 jkˇ
6567 nlmˇo
6568 ˇ"#
6569 ));
6570
6571 // change selections
6572 cx.set_state(indoc!(
6573 r#"abc
6574 def«ˇg»hi
6575
6576 jk
6577 nlmo
6578 "#
6579 ));
6580
6581 cx.update_editor(|editor, window, cx| {
6582 editor.add_selection_below(&Default::default(), window, cx);
6583 });
6584
6585 cx.assert_editor_state(indoc!(
6586 r#"abc
6587 def«ˇg»hi
6588
6589 jk
6590 nlm«ˇo»
6591 "#
6592 ));
6593
6594 cx.update_editor(|editor, window, cx| {
6595 editor.add_selection_below(&Default::default(), window, cx);
6596 });
6597
6598 cx.assert_editor_state(indoc!(
6599 r#"abc
6600 def«ˇg»hi
6601
6602 jk
6603 nlm«ˇo»
6604 "#
6605 ));
6606
6607 cx.update_editor(|editor, window, cx| {
6608 editor.add_selection_above(&Default::default(), window, cx);
6609 });
6610
6611 cx.assert_editor_state(indoc!(
6612 r#"abc
6613 def«ˇg»hi
6614
6615 jk
6616 nlmo
6617 "#
6618 ));
6619
6620 cx.update_editor(|editor, window, cx| {
6621 editor.add_selection_above(&Default::default(), window, cx);
6622 });
6623
6624 cx.assert_editor_state(indoc!(
6625 r#"abc
6626 def«ˇg»hi
6627
6628 jk
6629 nlmo
6630 "#
6631 ));
6632
6633 // Change selections again
6634 cx.set_state(indoc!(
6635 r#"a«bc
6636 defgˇ»hi
6637
6638 jk
6639 nlmo
6640 "#
6641 ));
6642
6643 cx.update_editor(|editor, window, cx| {
6644 editor.add_selection_below(&Default::default(), window, cx);
6645 });
6646
6647 cx.assert_editor_state(indoc!(
6648 r#"a«bcˇ»
6649 d«efgˇ»hi
6650
6651 j«kˇ»
6652 nlmo
6653 "#
6654 ));
6655
6656 cx.update_editor(|editor, window, cx| {
6657 editor.add_selection_below(&Default::default(), window, cx);
6658 });
6659 cx.assert_editor_state(indoc!(
6660 r#"a«bcˇ»
6661 d«efgˇ»hi
6662
6663 j«kˇ»
6664 n«lmoˇ»
6665 "#
6666 ));
6667 cx.update_editor(|editor, window, cx| {
6668 editor.add_selection_above(&Default::default(), window, cx);
6669 });
6670
6671 cx.assert_editor_state(indoc!(
6672 r#"a«bcˇ»
6673 d«efgˇ»hi
6674
6675 j«kˇ»
6676 nlmo
6677 "#
6678 ));
6679
6680 // Change selections again
6681 cx.set_state(indoc!(
6682 r#"abc
6683 d«ˇefghi
6684
6685 jk
6686 nlm»o
6687 "#
6688 ));
6689
6690 cx.update_editor(|editor, window, cx| {
6691 editor.add_selection_above(&Default::default(), window, cx);
6692 });
6693
6694 cx.assert_editor_state(indoc!(
6695 r#"a«ˇbc»
6696 d«ˇef»ghi
6697
6698 j«ˇk»
6699 n«ˇlm»o
6700 "#
6701 ));
6702
6703 cx.update_editor(|editor, window, cx| {
6704 editor.add_selection_below(&Default::default(), window, cx);
6705 });
6706
6707 cx.assert_editor_state(indoc!(
6708 r#"abc
6709 d«ˇef»ghi
6710
6711 j«ˇk»
6712 n«ˇlm»o
6713 "#
6714 ));
6715}
6716
6717#[gpui::test]
6718async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
6719 init_test(cx, |_| {});
6720 let mut cx = EditorTestContext::new(cx).await;
6721
6722 cx.set_state(indoc!(
6723 r#"line onˇe
6724 liˇne two
6725 line three
6726 line four"#
6727 ));
6728
6729 cx.update_editor(|editor, window, cx| {
6730 editor.add_selection_below(&Default::default(), window, cx);
6731 });
6732
6733 // test multiple cursors expand in the same direction
6734 cx.assert_editor_state(indoc!(
6735 r#"line onˇe
6736 liˇne twˇo
6737 liˇne three
6738 line four"#
6739 ));
6740
6741 cx.update_editor(|editor, window, cx| {
6742 editor.add_selection_below(&Default::default(), window, cx);
6743 });
6744
6745 cx.update_editor(|editor, window, cx| {
6746 editor.add_selection_below(&Default::default(), window, cx);
6747 });
6748
6749 // test multiple cursors expand below overflow
6750 cx.assert_editor_state(indoc!(
6751 r#"line onˇe
6752 liˇne twˇo
6753 liˇne thˇree
6754 liˇne foˇur"#
6755 ));
6756
6757 cx.update_editor(|editor, window, cx| {
6758 editor.add_selection_above(&Default::default(), window, cx);
6759 });
6760
6761 // test multiple cursors retrieves back correctly
6762 cx.assert_editor_state(indoc!(
6763 r#"line onˇe
6764 liˇne twˇo
6765 liˇne thˇree
6766 line four"#
6767 ));
6768
6769 cx.update_editor(|editor, window, cx| {
6770 editor.add_selection_above(&Default::default(), window, cx);
6771 });
6772
6773 cx.update_editor(|editor, window, cx| {
6774 editor.add_selection_above(&Default::default(), window, cx);
6775 });
6776
6777 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
6778 cx.assert_editor_state(indoc!(
6779 r#"liˇne onˇe
6780 liˇne two
6781 line three
6782 line four"#
6783 ));
6784
6785 cx.update_editor(|editor, window, cx| {
6786 editor.undo_selection(&Default::default(), window, cx);
6787 });
6788
6789 // test undo
6790 cx.assert_editor_state(indoc!(
6791 r#"line onˇe
6792 liˇne twˇo
6793 line three
6794 line four"#
6795 ));
6796
6797 cx.update_editor(|editor, window, cx| {
6798 editor.redo_selection(&Default::default(), window, cx);
6799 });
6800
6801 // test redo
6802 cx.assert_editor_state(indoc!(
6803 r#"liˇne onˇe
6804 liˇne two
6805 line three
6806 line four"#
6807 ));
6808
6809 cx.set_state(indoc!(
6810 r#"abcd
6811 ef«ghˇ»
6812 ijkl
6813 «mˇ»nop"#
6814 ));
6815
6816 cx.update_editor(|editor, window, cx| {
6817 editor.add_selection_above(&Default::default(), window, cx);
6818 });
6819
6820 // test multiple selections expand in the same direction
6821 cx.assert_editor_state(indoc!(
6822 r#"ab«cdˇ»
6823 ef«ghˇ»
6824 «iˇ»jkl
6825 «mˇ»nop"#
6826 ));
6827
6828 cx.update_editor(|editor, window, cx| {
6829 editor.add_selection_above(&Default::default(), window, cx);
6830 });
6831
6832 // test multiple selection upward overflow
6833 cx.assert_editor_state(indoc!(
6834 r#"ab«cdˇ»
6835 «eˇ»f«ghˇ»
6836 «iˇ»jkl
6837 «mˇ»nop"#
6838 ));
6839
6840 cx.update_editor(|editor, window, cx| {
6841 editor.add_selection_below(&Default::default(), window, cx);
6842 });
6843
6844 // test multiple selection retrieves back correctly
6845 cx.assert_editor_state(indoc!(
6846 r#"abcd
6847 ef«ghˇ»
6848 «iˇ»jkl
6849 «mˇ»nop"#
6850 ));
6851
6852 cx.update_editor(|editor, window, cx| {
6853 editor.add_selection_below(&Default::default(), window, cx);
6854 });
6855
6856 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
6857 cx.assert_editor_state(indoc!(
6858 r#"abcd
6859 ef«ghˇ»
6860 ij«klˇ»
6861 «mˇ»nop"#
6862 ));
6863
6864 cx.update_editor(|editor, window, cx| {
6865 editor.undo_selection(&Default::default(), window, cx);
6866 });
6867
6868 // test undo
6869 cx.assert_editor_state(indoc!(
6870 r#"abcd
6871 ef«ghˇ»
6872 «iˇ»jkl
6873 «mˇ»nop"#
6874 ));
6875
6876 cx.update_editor(|editor, window, cx| {
6877 editor.redo_selection(&Default::default(), window, cx);
6878 });
6879
6880 // test redo
6881 cx.assert_editor_state(indoc!(
6882 r#"abcd
6883 ef«ghˇ»
6884 ij«klˇ»
6885 «mˇ»nop"#
6886 ));
6887}
6888
6889#[gpui::test]
6890async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
6891 init_test(cx, |_| {});
6892 let mut cx = EditorTestContext::new(cx).await;
6893
6894 cx.set_state(indoc!(
6895 r#"line onˇe
6896 liˇne two
6897 line three
6898 line four"#
6899 ));
6900
6901 cx.update_editor(|editor, window, cx| {
6902 editor.add_selection_below(&Default::default(), window, cx);
6903 editor.add_selection_below(&Default::default(), window, cx);
6904 editor.add_selection_below(&Default::default(), window, cx);
6905 });
6906
6907 // initial state with two multi cursor groups
6908 cx.assert_editor_state(indoc!(
6909 r#"line onˇe
6910 liˇne twˇo
6911 liˇne thˇree
6912 liˇne foˇur"#
6913 ));
6914
6915 // add single cursor in middle - simulate opt click
6916 cx.update_editor(|editor, window, cx| {
6917 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
6918 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6919 editor.end_selection(window, cx);
6920 });
6921
6922 cx.assert_editor_state(indoc!(
6923 r#"line onˇe
6924 liˇne twˇo
6925 liˇneˇ thˇree
6926 liˇne foˇur"#
6927 ));
6928
6929 cx.update_editor(|editor, window, cx| {
6930 editor.add_selection_above(&Default::default(), window, cx);
6931 });
6932
6933 // test new added selection expands above and existing selection shrinks
6934 cx.assert_editor_state(indoc!(
6935 r#"line onˇe
6936 liˇneˇ twˇo
6937 liˇneˇ thˇree
6938 line four"#
6939 ));
6940
6941 cx.update_editor(|editor, window, cx| {
6942 editor.add_selection_above(&Default::default(), window, cx);
6943 });
6944
6945 // test new added selection expands above and existing selection shrinks
6946 cx.assert_editor_state(indoc!(
6947 r#"lineˇ onˇe
6948 liˇneˇ twˇo
6949 lineˇ three
6950 line four"#
6951 ));
6952
6953 // intial state with two selection groups
6954 cx.set_state(indoc!(
6955 r#"abcd
6956 ef«ghˇ»
6957 ijkl
6958 «mˇ»nop"#
6959 ));
6960
6961 cx.update_editor(|editor, window, cx| {
6962 editor.add_selection_above(&Default::default(), window, cx);
6963 editor.add_selection_above(&Default::default(), window, cx);
6964 });
6965
6966 cx.assert_editor_state(indoc!(
6967 r#"ab«cdˇ»
6968 «eˇ»f«ghˇ»
6969 «iˇ»jkl
6970 «mˇ»nop"#
6971 ));
6972
6973 // add single selection in middle - simulate opt drag
6974 cx.update_editor(|editor, window, cx| {
6975 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
6976 editor.begin_selection(new_cursor_point, true, 1, window, cx);
6977 editor.update_selection(
6978 DisplayPoint::new(DisplayRow(2), 4),
6979 0,
6980 gpui::Point::<f32>::default(),
6981 window,
6982 cx,
6983 );
6984 editor.end_selection(window, cx);
6985 });
6986
6987 cx.assert_editor_state(indoc!(
6988 r#"ab«cdˇ»
6989 «eˇ»f«ghˇ»
6990 «iˇ»jk«lˇ»
6991 «mˇ»nop"#
6992 ));
6993
6994 cx.update_editor(|editor, window, cx| {
6995 editor.add_selection_below(&Default::default(), window, cx);
6996 });
6997
6998 // test new added selection expands below, others shrinks from above
6999 cx.assert_editor_state(indoc!(
7000 r#"abcd
7001 ef«ghˇ»
7002 «iˇ»jk«lˇ»
7003 «mˇ»no«pˇ»"#
7004 ));
7005}
7006
7007#[gpui::test]
7008async fn test_select_next(cx: &mut TestAppContext) {
7009 init_test(cx, |_| {});
7010
7011 let mut cx = EditorTestContext::new(cx).await;
7012 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7013
7014 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7015 .unwrap();
7016 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7017
7018 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7019 .unwrap();
7020 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7021
7022 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7023 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7024
7025 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7026 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
7027
7028 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7029 .unwrap();
7030 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7031
7032 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7033 .unwrap();
7034 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7035
7036 // Test selection direction should be preserved
7037 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7038
7039 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7040 .unwrap();
7041 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
7042}
7043
7044#[gpui::test]
7045async fn test_select_all_matches(cx: &mut TestAppContext) {
7046 init_test(cx, |_| {});
7047
7048 let mut cx = EditorTestContext::new(cx).await;
7049
7050 // Test caret-only selections
7051 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7052 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7053 .unwrap();
7054 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7055
7056 // Test left-to-right selections
7057 cx.set_state("abc\n«abcˇ»\nabc");
7058 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7059 .unwrap();
7060 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
7061
7062 // Test right-to-left selections
7063 cx.set_state("abc\n«ˇabc»\nabc");
7064 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7065 .unwrap();
7066 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
7067
7068 // Test selecting whitespace with caret selection
7069 cx.set_state("abc\nˇ abc\nabc");
7070 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7071 .unwrap();
7072 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7073
7074 // Test selecting whitespace with left-to-right selection
7075 cx.set_state("abc\n«ˇ »abc\nabc");
7076 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7077 .unwrap();
7078 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
7079
7080 // Test no matches with right-to-left selection
7081 cx.set_state("abc\n« ˇ»abc\nabc");
7082 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7083 .unwrap();
7084 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
7085
7086 // Test with a single word and clip_at_line_ends=true (#29823)
7087 cx.set_state("aˇbc");
7088 cx.update_editor(|e, window, cx| {
7089 e.set_clip_at_line_ends(true, cx);
7090 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
7091 e.set_clip_at_line_ends(false, cx);
7092 });
7093 cx.assert_editor_state("«abcˇ»");
7094}
7095
7096#[gpui::test]
7097async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
7098 init_test(cx, |_| {});
7099
7100 let mut cx = EditorTestContext::new(cx).await;
7101
7102 let large_body_1 = "\nd".repeat(200);
7103 let large_body_2 = "\ne".repeat(200);
7104
7105 cx.set_state(&format!(
7106 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
7107 ));
7108 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
7109 let scroll_position = editor.scroll_position(cx);
7110 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
7111 scroll_position
7112 });
7113
7114 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
7115 .unwrap();
7116 cx.assert_editor_state(&format!(
7117 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
7118 ));
7119 let scroll_position_after_selection =
7120 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
7121 assert_eq!(
7122 initial_scroll_position, scroll_position_after_selection,
7123 "Scroll position should not change after selecting all matches"
7124 );
7125}
7126
7127#[gpui::test]
7128async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
7129 init_test(cx, |_| {});
7130
7131 let mut cx = EditorLspTestContext::new_rust(
7132 lsp::ServerCapabilities {
7133 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7134 ..Default::default()
7135 },
7136 cx,
7137 )
7138 .await;
7139
7140 cx.set_state(indoc! {"
7141 line 1
7142 line 2
7143 linˇe 3
7144 line 4
7145 line 5
7146 "});
7147
7148 // Make an edit
7149 cx.update_editor(|editor, window, cx| {
7150 editor.handle_input("X", window, cx);
7151 });
7152
7153 // Move cursor to a different position
7154 cx.update_editor(|editor, window, cx| {
7155 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7156 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
7157 });
7158 });
7159
7160 cx.assert_editor_state(indoc! {"
7161 line 1
7162 line 2
7163 linXe 3
7164 line 4
7165 liˇne 5
7166 "});
7167
7168 cx.lsp
7169 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
7170 Ok(Some(vec![lsp::TextEdit::new(
7171 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
7172 "PREFIX ".to_string(),
7173 )]))
7174 });
7175
7176 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
7177 .unwrap()
7178 .await
7179 .unwrap();
7180
7181 cx.assert_editor_state(indoc! {"
7182 PREFIX line 1
7183 line 2
7184 linXe 3
7185 line 4
7186 liˇne 5
7187 "});
7188
7189 // Undo formatting
7190 cx.update_editor(|editor, window, cx| {
7191 editor.undo(&Default::default(), window, cx);
7192 });
7193
7194 // Verify cursor moved back to position after edit
7195 cx.assert_editor_state(indoc! {"
7196 line 1
7197 line 2
7198 linXˇe 3
7199 line 4
7200 line 5
7201 "});
7202}
7203
7204#[gpui::test]
7205async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) {
7206 init_test(cx, |_| {});
7207
7208 let mut cx = EditorTestContext::new(cx).await;
7209
7210 let provider = cx.new(|_| FakeInlineCompletionProvider::default());
7211 cx.update_editor(|editor, window, cx| {
7212 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
7213 });
7214
7215 cx.set_state(indoc! {"
7216 line 1
7217 line 2
7218 linˇe 3
7219 line 4
7220 line 5
7221 line 6
7222 line 7
7223 line 8
7224 line 9
7225 line 10
7226 "});
7227
7228 let snapshot = cx.buffer_snapshot();
7229 let edit_position = snapshot.anchor_after(Point::new(2, 4));
7230
7231 cx.update(|_, cx| {
7232 provider.update(cx, |provider, _| {
7233 provider.set_inline_completion(Some(inline_completion::InlineCompletion {
7234 id: None,
7235 edits: vec![(edit_position..edit_position, "X".into())],
7236 edit_preview: None,
7237 }))
7238 })
7239 });
7240
7241 cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
7242 cx.update_editor(|editor, window, cx| {
7243 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
7244 });
7245
7246 cx.assert_editor_state(indoc! {"
7247 line 1
7248 line 2
7249 lineXˇ 3
7250 line 4
7251 line 5
7252 line 6
7253 line 7
7254 line 8
7255 line 9
7256 line 10
7257 "});
7258
7259 cx.update_editor(|editor, window, cx| {
7260 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7261 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
7262 });
7263 });
7264
7265 cx.assert_editor_state(indoc! {"
7266 line 1
7267 line 2
7268 lineX 3
7269 line 4
7270 line 5
7271 line 6
7272 line 7
7273 line 8
7274 line 9
7275 liˇne 10
7276 "});
7277
7278 cx.update_editor(|editor, window, cx| {
7279 editor.undo(&Default::default(), window, cx);
7280 });
7281
7282 cx.assert_editor_state(indoc! {"
7283 line 1
7284 line 2
7285 lineˇ 3
7286 line 4
7287 line 5
7288 line 6
7289 line 7
7290 line 8
7291 line 9
7292 line 10
7293 "});
7294}
7295
7296#[gpui::test]
7297async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
7298 init_test(cx, |_| {});
7299
7300 let mut cx = EditorTestContext::new(cx).await;
7301 cx.set_state(
7302 r#"let foo = 2;
7303lˇet foo = 2;
7304let fooˇ = 2;
7305let foo = 2;
7306let foo = ˇ2;"#,
7307 );
7308
7309 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7310 .unwrap();
7311 cx.assert_editor_state(
7312 r#"let foo = 2;
7313«letˇ» foo = 2;
7314let «fooˇ» = 2;
7315let foo = 2;
7316let foo = «2ˇ»;"#,
7317 );
7318
7319 // noop for multiple selections with different contents
7320 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7321 .unwrap();
7322 cx.assert_editor_state(
7323 r#"let foo = 2;
7324«letˇ» foo = 2;
7325let «fooˇ» = 2;
7326let foo = 2;
7327let foo = «2ˇ»;"#,
7328 );
7329
7330 // Test last selection direction should be preserved
7331 cx.set_state(
7332 r#"let foo = 2;
7333let foo = 2;
7334let «fooˇ» = 2;
7335let «ˇfoo» = 2;
7336let foo = 2;"#,
7337 );
7338
7339 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
7340 .unwrap();
7341 cx.assert_editor_state(
7342 r#"let foo = 2;
7343let foo = 2;
7344let «fooˇ» = 2;
7345let «ˇfoo» = 2;
7346let «ˇfoo» = 2;"#,
7347 );
7348}
7349
7350#[gpui::test]
7351async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
7352 init_test(cx, |_| {});
7353
7354 let mut cx =
7355 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
7356
7357 cx.assert_editor_state(indoc! {"
7358 ˇbbb
7359 ccc
7360
7361 bbb
7362 ccc
7363 "});
7364 cx.dispatch_action(SelectPrevious::default());
7365 cx.assert_editor_state(indoc! {"
7366 «bbbˇ»
7367 ccc
7368
7369 bbb
7370 ccc
7371 "});
7372 cx.dispatch_action(SelectPrevious::default());
7373 cx.assert_editor_state(indoc! {"
7374 «bbbˇ»
7375 ccc
7376
7377 «bbbˇ»
7378 ccc
7379 "});
7380}
7381
7382#[gpui::test]
7383async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
7384 init_test(cx, |_| {});
7385
7386 let mut cx = EditorTestContext::new(cx).await;
7387 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
7388
7389 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7390 .unwrap();
7391 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7392
7393 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7394 .unwrap();
7395 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7396
7397 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7398 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
7399
7400 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7401 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
7402
7403 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7404 .unwrap();
7405 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
7406
7407 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7408 .unwrap();
7409 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
7410}
7411
7412#[gpui::test]
7413async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
7414 init_test(cx, |_| {});
7415
7416 let mut cx = EditorTestContext::new(cx).await;
7417 cx.set_state("aˇ");
7418
7419 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7420 .unwrap();
7421 cx.assert_editor_state("«aˇ»");
7422 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7423 .unwrap();
7424 cx.assert_editor_state("«aˇ»");
7425}
7426
7427#[gpui::test]
7428async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
7429 init_test(cx, |_| {});
7430
7431 let mut cx = EditorTestContext::new(cx).await;
7432 cx.set_state(
7433 r#"let foo = 2;
7434lˇet foo = 2;
7435let fooˇ = 2;
7436let foo = 2;
7437let foo = ˇ2;"#,
7438 );
7439
7440 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7441 .unwrap();
7442 cx.assert_editor_state(
7443 r#"let foo = 2;
7444«letˇ» foo = 2;
7445let «fooˇ» = 2;
7446let foo = 2;
7447let foo = «2ˇ»;"#,
7448 );
7449
7450 // noop for multiple selections with different contents
7451 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7452 .unwrap();
7453 cx.assert_editor_state(
7454 r#"let foo = 2;
7455«letˇ» foo = 2;
7456let «fooˇ» = 2;
7457let foo = 2;
7458let foo = «2ˇ»;"#,
7459 );
7460}
7461
7462#[gpui::test]
7463async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
7464 init_test(cx, |_| {});
7465
7466 let mut cx = EditorTestContext::new(cx).await;
7467 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
7468
7469 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7470 .unwrap();
7471 // selection direction is preserved
7472 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7473
7474 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7475 .unwrap();
7476 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7477
7478 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
7479 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
7480
7481 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
7482 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
7483
7484 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7485 .unwrap();
7486 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
7487
7488 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
7489 .unwrap();
7490 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
7491}
7492
7493#[gpui::test]
7494async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
7495 init_test(cx, |_| {});
7496
7497 let language = Arc::new(Language::new(
7498 LanguageConfig::default(),
7499 Some(tree_sitter_rust::LANGUAGE.into()),
7500 ));
7501
7502 let text = r#"
7503 use mod1::mod2::{mod3, mod4};
7504
7505 fn fn_1(param1: bool, param2: &str) {
7506 let var1 = "text";
7507 }
7508 "#
7509 .unindent();
7510
7511 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7512 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7513 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7514
7515 editor
7516 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7517 .await;
7518
7519 editor.update_in(cx, |editor, window, cx| {
7520 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7521 s.select_display_ranges([
7522 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
7523 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
7524 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
7525 ]);
7526 });
7527 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7528 });
7529 editor.update(cx, |editor, cx| {
7530 assert_text_with_selections(
7531 editor,
7532 indoc! {r#"
7533 use mod1::mod2::{mod3, «mod4ˇ»};
7534
7535 fn fn_1«ˇ(param1: bool, param2: &str)» {
7536 let var1 = "«ˇtext»";
7537 }
7538 "#},
7539 cx,
7540 );
7541 });
7542
7543 editor.update_in(cx, |editor, window, cx| {
7544 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7545 });
7546 editor.update(cx, |editor, cx| {
7547 assert_text_with_selections(
7548 editor,
7549 indoc! {r#"
7550 use mod1::mod2::«{mod3, mod4}ˇ»;
7551
7552 «ˇfn fn_1(param1: bool, param2: &str) {
7553 let var1 = "text";
7554 }»
7555 "#},
7556 cx,
7557 );
7558 });
7559
7560 editor.update_in(cx, |editor, window, cx| {
7561 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7562 });
7563 assert_eq!(
7564 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7565 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7566 );
7567
7568 // Trying to expand the selected syntax node one more time has no effect.
7569 editor.update_in(cx, |editor, window, cx| {
7570 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7571 });
7572 assert_eq!(
7573 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
7574 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
7575 );
7576
7577 editor.update_in(cx, |editor, window, cx| {
7578 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7579 });
7580 editor.update(cx, |editor, cx| {
7581 assert_text_with_selections(
7582 editor,
7583 indoc! {r#"
7584 use mod1::mod2::«{mod3, mod4}ˇ»;
7585
7586 «ˇfn fn_1(param1: bool, param2: &str) {
7587 let var1 = "text";
7588 }»
7589 "#},
7590 cx,
7591 );
7592 });
7593
7594 editor.update_in(cx, |editor, window, cx| {
7595 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7596 });
7597 editor.update(cx, |editor, cx| {
7598 assert_text_with_selections(
7599 editor,
7600 indoc! {r#"
7601 use mod1::mod2::{mod3, «mod4ˇ»};
7602
7603 fn fn_1«ˇ(param1: bool, param2: &str)» {
7604 let var1 = "«ˇtext»";
7605 }
7606 "#},
7607 cx,
7608 );
7609 });
7610
7611 editor.update_in(cx, |editor, window, cx| {
7612 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7613 });
7614 editor.update(cx, |editor, cx| {
7615 assert_text_with_selections(
7616 editor,
7617 indoc! {r#"
7618 use mod1::mod2::{mod3, mo«ˇ»d4};
7619
7620 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7621 let var1 = "te«ˇ»xt";
7622 }
7623 "#},
7624 cx,
7625 );
7626 });
7627
7628 // Trying to shrink the selected syntax node one more time has no effect.
7629 editor.update_in(cx, |editor, window, cx| {
7630 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
7631 });
7632 editor.update_in(cx, |editor, _, cx| {
7633 assert_text_with_selections(
7634 editor,
7635 indoc! {r#"
7636 use mod1::mod2::{mod3, mo«ˇ»d4};
7637
7638 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
7639 let var1 = "te«ˇ»xt";
7640 }
7641 "#},
7642 cx,
7643 );
7644 });
7645
7646 // Ensure that we keep expanding the selection if the larger selection starts or ends within
7647 // a fold.
7648 editor.update_in(cx, |editor, window, cx| {
7649 editor.fold_creases(
7650 vec![
7651 Crease::simple(
7652 Point::new(0, 21)..Point::new(0, 24),
7653 FoldPlaceholder::test(),
7654 ),
7655 Crease::simple(
7656 Point::new(3, 20)..Point::new(3, 22),
7657 FoldPlaceholder::test(),
7658 ),
7659 ],
7660 true,
7661 window,
7662 cx,
7663 );
7664 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7665 });
7666 editor.update(cx, |editor, cx| {
7667 assert_text_with_selections(
7668 editor,
7669 indoc! {r#"
7670 use mod1::mod2::«{mod3, mod4}ˇ»;
7671
7672 fn fn_1«ˇ(param1: bool, param2: &str)» {
7673 let var1 = "«ˇtext»";
7674 }
7675 "#},
7676 cx,
7677 );
7678 });
7679}
7680
7681#[gpui::test]
7682async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
7683 init_test(cx, |_| {});
7684
7685 let language = Arc::new(Language::new(
7686 LanguageConfig::default(),
7687 Some(tree_sitter_rust::LANGUAGE.into()),
7688 ));
7689
7690 let text = "let a = 2;";
7691
7692 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7693 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7694 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7695
7696 editor
7697 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7698 .await;
7699
7700 // Test case 1: Cursor at end of word
7701 editor.update_in(cx, |editor, window, cx| {
7702 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7703 s.select_display_ranges([
7704 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
7705 ]);
7706 });
7707 });
7708 editor.update(cx, |editor, cx| {
7709 assert_text_with_selections(editor, "let aˇ = 2;", cx);
7710 });
7711 editor.update_in(cx, |editor, window, cx| {
7712 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7713 });
7714 editor.update(cx, |editor, cx| {
7715 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
7716 });
7717 editor.update_in(cx, |editor, window, cx| {
7718 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7719 });
7720 editor.update(cx, |editor, cx| {
7721 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7722 });
7723
7724 // Test case 2: Cursor at end of statement
7725 editor.update_in(cx, |editor, window, cx| {
7726 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7727 s.select_display_ranges([
7728 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
7729 ]);
7730 });
7731 });
7732 editor.update(cx, |editor, cx| {
7733 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
7734 });
7735 editor.update_in(cx, |editor, window, cx| {
7736 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7737 });
7738 editor.update(cx, |editor, cx| {
7739 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
7740 });
7741}
7742
7743#[gpui::test]
7744async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
7745 init_test(cx, |_| {});
7746
7747 let language = Arc::new(Language::new(
7748 LanguageConfig::default(),
7749 Some(tree_sitter_rust::LANGUAGE.into()),
7750 ));
7751
7752 let text = r#"
7753 use mod1::mod2::{mod3, mod4};
7754
7755 fn fn_1(param1: bool, param2: &str) {
7756 let var1 = "hello world";
7757 }
7758 "#
7759 .unindent();
7760
7761 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7762 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7763 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7764
7765 editor
7766 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7767 .await;
7768
7769 // Test 1: Cursor on a letter of a string word
7770 editor.update_in(cx, |editor, window, cx| {
7771 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7772 s.select_display_ranges([
7773 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
7774 ]);
7775 });
7776 });
7777 editor.update_in(cx, |editor, window, cx| {
7778 assert_text_with_selections(
7779 editor,
7780 indoc! {r#"
7781 use mod1::mod2::{mod3, mod4};
7782
7783 fn fn_1(param1: bool, param2: &str) {
7784 let var1 = "hˇello world";
7785 }
7786 "#},
7787 cx,
7788 );
7789 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7790 assert_text_with_selections(
7791 editor,
7792 indoc! {r#"
7793 use mod1::mod2::{mod3, mod4};
7794
7795 fn fn_1(param1: bool, param2: &str) {
7796 let var1 = "«ˇhello» world";
7797 }
7798 "#},
7799 cx,
7800 );
7801 });
7802
7803 // Test 2: Partial selection within a word
7804 editor.update_in(cx, |editor, window, cx| {
7805 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7806 s.select_display_ranges([
7807 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7808 ]);
7809 });
7810 });
7811 editor.update_in(cx, |editor, window, cx| {
7812 assert_text_with_selections(
7813 editor,
7814 indoc! {r#"
7815 use mod1::mod2::{mod3, mod4};
7816
7817 fn fn_1(param1: bool, param2: &str) {
7818 let var1 = "h«elˇ»lo world";
7819 }
7820 "#},
7821 cx,
7822 );
7823 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7824 assert_text_with_selections(
7825 editor,
7826 indoc! {r#"
7827 use mod1::mod2::{mod3, mod4};
7828
7829 fn fn_1(param1: bool, param2: &str) {
7830 let var1 = "«ˇhello» world";
7831 }
7832 "#},
7833 cx,
7834 );
7835 });
7836
7837 // Test 3: Complete word already selected
7838 editor.update_in(cx, |editor, window, cx| {
7839 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7840 s.select_display_ranges([
7841 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7842 ]);
7843 });
7844 });
7845 editor.update_in(cx, |editor, window, cx| {
7846 assert_text_with_selections(
7847 editor,
7848 indoc! {r#"
7849 use mod1::mod2::{mod3, mod4};
7850
7851 fn fn_1(param1: bool, param2: &str) {
7852 let var1 = "«helloˇ» world";
7853 }
7854 "#},
7855 cx,
7856 );
7857 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7858 assert_text_with_selections(
7859 editor,
7860 indoc! {r#"
7861 use mod1::mod2::{mod3, mod4};
7862
7863 fn fn_1(param1: bool, param2: &str) {
7864 let var1 = "«hello worldˇ»";
7865 }
7866 "#},
7867 cx,
7868 );
7869 });
7870
7871 // Test 4: Selection spanning across words
7872 editor.update_in(cx, |editor, window, cx| {
7873 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7874 s.select_display_ranges([
7875 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7876 ]);
7877 });
7878 });
7879 editor.update_in(cx, |editor, window, cx| {
7880 assert_text_with_selections(
7881 editor,
7882 indoc! {r#"
7883 use mod1::mod2::{mod3, mod4};
7884
7885 fn fn_1(param1: bool, param2: &str) {
7886 let var1 = "hel«lo woˇ»rld";
7887 }
7888 "#},
7889 cx,
7890 );
7891 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7892 assert_text_with_selections(
7893 editor,
7894 indoc! {r#"
7895 use mod1::mod2::{mod3, mod4};
7896
7897 fn fn_1(param1: bool, param2: &str) {
7898 let var1 = "«ˇhello world»";
7899 }
7900 "#},
7901 cx,
7902 );
7903 });
7904
7905 // Test 5: Expansion beyond string
7906 editor.update_in(cx, |editor, window, cx| {
7907 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7908 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7909 assert_text_with_selections(
7910 editor,
7911 indoc! {r#"
7912 use mod1::mod2::{mod3, mod4};
7913
7914 fn fn_1(param1: bool, param2: &str) {
7915 «ˇlet var1 = "hello world";»
7916 }
7917 "#},
7918 cx,
7919 );
7920 });
7921}
7922
7923#[gpui::test]
7924async fn test_fold_function_bodies(cx: &mut TestAppContext) {
7925 init_test(cx, |_| {});
7926
7927 let base_text = r#"
7928 impl A {
7929 // this is an uncommitted comment
7930
7931 fn b() {
7932 c();
7933 }
7934
7935 // this is another uncommitted comment
7936
7937 fn d() {
7938 // e
7939 // f
7940 }
7941 }
7942
7943 fn g() {
7944 // h
7945 }
7946 "#
7947 .unindent();
7948
7949 let text = r#"
7950 ˇimpl A {
7951
7952 fn b() {
7953 c();
7954 }
7955
7956 fn d() {
7957 // e
7958 // f
7959 }
7960 }
7961
7962 fn g() {
7963 // h
7964 }
7965 "#
7966 .unindent();
7967
7968 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7969 cx.set_state(&text);
7970 cx.set_head_text(&base_text);
7971 cx.update_editor(|editor, window, cx| {
7972 editor.expand_all_diff_hunks(&Default::default(), window, cx);
7973 });
7974
7975 cx.assert_state_with_diff(
7976 "
7977 ˇimpl A {
7978 - // this is an uncommitted comment
7979
7980 fn b() {
7981 c();
7982 }
7983
7984 - // this is another uncommitted comment
7985 -
7986 fn d() {
7987 // e
7988 // f
7989 }
7990 }
7991
7992 fn g() {
7993 // h
7994 }
7995 "
7996 .unindent(),
7997 );
7998
7999 let expected_display_text = "
8000 impl A {
8001 // this is an uncommitted comment
8002
8003 fn b() {
8004 ⋯
8005 }
8006
8007 // this is another uncommitted comment
8008
8009 fn d() {
8010 ⋯
8011 }
8012 }
8013
8014 fn g() {
8015 ⋯
8016 }
8017 "
8018 .unindent();
8019
8020 cx.update_editor(|editor, window, cx| {
8021 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
8022 assert_eq!(editor.display_text(cx), expected_display_text);
8023 });
8024}
8025
8026#[gpui::test]
8027async fn test_autoindent(cx: &mut TestAppContext) {
8028 init_test(cx, |_| {});
8029
8030 let language = Arc::new(
8031 Language::new(
8032 LanguageConfig {
8033 brackets: BracketPairConfig {
8034 pairs: vec![
8035 BracketPair {
8036 start: "{".to_string(),
8037 end: "}".to_string(),
8038 close: false,
8039 surround: false,
8040 newline: true,
8041 },
8042 BracketPair {
8043 start: "(".to_string(),
8044 end: ")".to_string(),
8045 close: false,
8046 surround: false,
8047 newline: true,
8048 },
8049 ],
8050 ..Default::default()
8051 },
8052 ..Default::default()
8053 },
8054 Some(tree_sitter_rust::LANGUAGE.into()),
8055 )
8056 .with_indents_query(
8057 r#"
8058 (_ "(" ")" @end) @indent
8059 (_ "{" "}" @end) @indent
8060 "#,
8061 )
8062 .unwrap(),
8063 );
8064
8065 let text = "fn a() {}";
8066
8067 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8068 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8069 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8070 editor
8071 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8072 .await;
8073
8074 editor.update_in(cx, |editor, window, cx| {
8075 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8076 s.select_ranges([5..5, 8..8, 9..9])
8077 });
8078 editor.newline(&Newline, window, cx);
8079 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
8080 assert_eq!(
8081 editor.selections.ranges(cx),
8082 &[
8083 Point::new(1, 4)..Point::new(1, 4),
8084 Point::new(3, 4)..Point::new(3, 4),
8085 Point::new(5, 0)..Point::new(5, 0)
8086 ]
8087 );
8088 });
8089}
8090
8091#[gpui::test]
8092async fn test_autoindent_selections(cx: &mut TestAppContext) {
8093 init_test(cx, |_| {});
8094
8095 {
8096 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
8097 cx.set_state(indoc! {"
8098 impl A {
8099
8100 fn b() {}
8101
8102 «fn c() {
8103
8104 }ˇ»
8105 }
8106 "});
8107
8108 cx.update_editor(|editor, window, cx| {
8109 editor.autoindent(&Default::default(), window, cx);
8110 });
8111
8112 cx.assert_editor_state(indoc! {"
8113 impl A {
8114
8115 fn b() {}
8116
8117 «fn c() {
8118
8119 }ˇ»
8120 }
8121 "});
8122 }
8123
8124 {
8125 let mut cx = EditorTestContext::new_multibuffer(
8126 cx,
8127 [indoc! { "
8128 impl A {
8129 «
8130 // a
8131 fn b(){}
8132 »
8133 «
8134 }
8135 fn c(){}
8136 »
8137 "}],
8138 );
8139
8140 let buffer = cx.update_editor(|editor, _, cx| {
8141 let buffer = editor.buffer().update(cx, |buffer, _| {
8142 buffer.all_buffers().iter().next().unwrap().clone()
8143 });
8144 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
8145 buffer
8146 });
8147
8148 cx.run_until_parked();
8149 cx.update_editor(|editor, window, cx| {
8150 editor.select_all(&Default::default(), window, cx);
8151 editor.autoindent(&Default::default(), window, cx)
8152 });
8153 cx.run_until_parked();
8154
8155 cx.update(|_, cx| {
8156 assert_eq!(
8157 buffer.read(cx).text(),
8158 indoc! { "
8159 impl A {
8160
8161 // a
8162 fn b(){}
8163
8164
8165 }
8166 fn c(){}
8167
8168 " }
8169 )
8170 });
8171 }
8172}
8173
8174#[gpui::test]
8175async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
8176 init_test(cx, |_| {});
8177
8178 let mut cx = EditorTestContext::new(cx).await;
8179
8180 let language = Arc::new(Language::new(
8181 LanguageConfig {
8182 brackets: BracketPairConfig {
8183 pairs: vec![
8184 BracketPair {
8185 start: "{".to_string(),
8186 end: "}".to_string(),
8187 close: true,
8188 surround: true,
8189 newline: true,
8190 },
8191 BracketPair {
8192 start: "(".to_string(),
8193 end: ")".to_string(),
8194 close: true,
8195 surround: true,
8196 newline: true,
8197 },
8198 BracketPair {
8199 start: "/*".to_string(),
8200 end: " */".to_string(),
8201 close: true,
8202 surround: true,
8203 newline: true,
8204 },
8205 BracketPair {
8206 start: "[".to_string(),
8207 end: "]".to_string(),
8208 close: false,
8209 surround: false,
8210 newline: true,
8211 },
8212 BracketPair {
8213 start: "\"".to_string(),
8214 end: "\"".to_string(),
8215 close: true,
8216 surround: true,
8217 newline: false,
8218 },
8219 BracketPair {
8220 start: "<".to_string(),
8221 end: ">".to_string(),
8222 close: false,
8223 surround: true,
8224 newline: true,
8225 },
8226 ],
8227 ..Default::default()
8228 },
8229 autoclose_before: "})]".to_string(),
8230 ..Default::default()
8231 },
8232 Some(tree_sitter_rust::LANGUAGE.into()),
8233 ));
8234
8235 cx.language_registry().add(language.clone());
8236 cx.update_buffer(|buffer, cx| {
8237 buffer.set_language(Some(language), cx);
8238 });
8239
8240 cx.set_state(
8241 &r#"
8242 🏀ˇ
8243 εˇ
8244 ❤️ˇ
8245 "#
8246 .unindent(),
8247 );
8248
8249 // autoclose multiple nested brackets at multiple cursors
8250 cx.update_editor(|editor, window, cx| {
8251 editor.handle_input("{", window, cx);
8252 editor.handle_input("{", window, cx);
8253 editor.handle_input("{", window, cx);
8254 });
8255 cx.assert_editor_state(
8256 &"
8257 🏀{{{ˇ}}}
8258 ε{{{ˇ}}}
8259 ❤️{{{ˇ}}}
8260 "
8261 .unindent(),
8262 );
8263
8264 // insert a different closing bracket
8265 cx.update_editor(|editor, window, cx| {
8266 editor.handle_input(")", window, cx);
8267 });
8268 cx.assert_editor_state(
8269 &"
8270 🏀{{{)ˇ}}}
8271 ε{{{)ˇ}}}
8272 ❤️{{{)ˇ}}}
8273 "
8274 .unindent(),
8275 );
8276
8277 // skip over the auto-closed brackets when typing a closing bracket
8278 cx.update_editor(|editor, window, cx| {
8279 editor.move_right(&MoveRight, window, cx);
8280 editor.handle_input("}", window, cx);
8281 editor.handle_input("}", window, cx);
8282 editor.handle_input("}", window, cx);
8283 });
8284 cx.assert_editor_state(
8285 &"
8286 🏀{{{)}}}}ˇ
8287 ε{{{)}}}}ˇ
8288 ❤️{{{)}}}}ˇ
8289 "
8290 .unindent(),
8291 );
8292
8293 // autoclose multi-character pairs
8294 cx.set_state(
8295 &"
8296 ˇ
8297 ˇ
8298 "
8299 .unindent(),
8300 );
8301 cx.update_editor(|editor, window, cx| {
8302 editor.handle_input("/", window, cx);
8303 editor.handle_input("*", window, cx);
8304 });
8305 cx.assert_editor_state(
8306 &"
8307 /*ˇ */
8308 /*ˇ */
8309 "
8310 .unindent(),
8311 );
8312
8313 // one cursor autocloses a multi-character pair, one cursor
8314 // does not autoclose.
8315 cx.set_state(
8316 &"
8317 /ˇ
8318 ˇ
8319 "
8320 .unindent(),
8321 );
8322 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
8323 cx.assert_editor_state(
8324 &"
8325 /*ˇ */
8326 *ˇ
8327 "
8328 .unindent(),
8329 );
8330
8331 // Don't autoclose if the next character isn't whitespace and isn't
8332 // listed in the language's "autoclose_before" section.
8333 cx.set_state("ˇa b");
8334 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8335 cx.assert_editor_state("{ˇa b");
8336
8337 // Don't autoclose if `close` is false for the bracket pair
8338 cx.set_state("ˇ");
8339 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
8340 cx.assert_editor_state("[ˇ");
8341
8342 // Surround with brackets if text is selected
8343 cx.set_state("«aˇ» b");
8344 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8345 cx.assert_editor_state("{«aˇ»} b");
8346
8347 // Autoclose when not immediately after a word character
8348 cx.set_state("a ˇ");
8349 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8350 cx.assert_editor_state("a \"ˇ\"");
8351
8352 // Autoclose pair where the start and end characters are the same
8353 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8354 cx.assert_editor_state("a \"\"ˇ");
8355
8356 // Don't autoclose when immediately after a word character
8357 cx.set_state("aˇ");
8358 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8359 cx.assert_editor_state("a\"ˇ");
8360
8361 // Do autoclose when after a non-word character
8362 cx.set_state("{ˇ");
8363 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
8364 cx.assert_editor_state("{\"ˇ\"");
8365
8366 // Non identical pairs autoclose regardless of preceding character
8367 cx.set_state("aˇ");
8368 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
8369 cx.assert_editor_state("a{ˇ}");
8370
8371 // Don't autoclose pair if autoclose is disabled
8372 cx.set_state("ˇ");
8373 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8374 cx.assert_editor_state("<ˇ");
8375
8376 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
8377 cx.set_state("«aˇ» b");
8378 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
8379 cx.assert_editor_state("<«aˇ»> b");
8380}
8381
8382#[gpui::test]
8383async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
8384 init_test(cx, |settings| {
8385 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8386 });
8387
8388 let mut cx = EditorTestContext::new(cx).await;
8389
8390 let language = Arc::new(Language::new(
8391 LanguageConfig {
8392 brackets: BracketPairConfig {
8393 pairs: vec![
8394 BracketPair {
8395 start: "{".to_string(),
8396 end: "}".to_string(),
8397 close: true,
8398 surround: true,
8399 newline: true,
8400 },
8401 BracketPair {
8402 start: "(".to_string(),
8403 end: ")".to_string(),
8404 close: true,
8405 surround: true,
8406 newline: true,
8407 },
8408 BracketPair {
8409 start: "[".to_string(),
8410 end: "]".to_string(),
8411 close: false,
8412 surround: false,
8413 newline: true,
8414 },
8415 ],
8416 ..Default::default()
8417 },
8418 autoclose_before: "})]".to_string(),
8419 ..Default::default()
8420 },
8421 Some(tree_sitter_rust::LANGUAGE.into()),
8422 ));
8423
8424 cx.language_registry().add(language.clone());
8425 cx.update_buffer(|buffer, cx| {
8426 buffer.set_language(Some(language), cx);
8427 });
8428
8429 cx.set_state(
8430 &"
8431 ˇ
8432 ˇ
8433 ˇ
8434 "
8435 .unindent(),
8436 );
8437
8438 // ensure only matching closing brackets are skipped over
8439 cx.update_editor(|editor, window, cx| {
8440 editor.handle_input("}", window, cx);
8441 editor.move_left(&MoveLeft, window, cx);
8442 editor.handle_input(")", window, cx);
8443 editor.move_left(&MoveLeft, window, cx);
8444 });
8445 cx.assert_editor_state(
8446 &"
8447 ˇ)}
8448 ˇ)}
8449 ˇ)}
8450 "
8451 .unindent(),
8452 );
8453
8454 // skip-over closing brackets at multiple cursors
8455 cx.update_editor(|editor, window, cx| {
8456 editor.handle_input(")", window, cx);
8457 editor.handle_input("}", window, cx);
8458 });
8459 cx.assert_editor_state(
8460 &"
8461 )}ˇ
8462 )}ˇ
8463 )}ˇ
8464 "
8465 .unindent(),
8466 );
8467
8468 // ignore non-close brackets
8469 cx.update_editor(|editor, window, cx| {
8470 editor.handle_input("]", window, cx);
8471 editor.move_left(&MoveLeft, window, cx);
8472 editor.handle_input("]", window, cx);
8473 });
8474 cx.assert_editor_state(
8475 &"
8476 )}]ˇ]
8477 )}]ˇ]
8478 )}]ˇ]
8479 "
8480 .unindent(),
8481 );
8482}
8483
8484#[gpui::test]
8485async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
8486 init_test(cx, |_| {});
8487
8488 let mut cx = EditorTestContext::new(cx).await;
8489
8490 let html_language = Arc::new(
8491 Language::new(
8492 LanguageConfig {
8493 name: "HTML".into(),
8494 brackets: BracketPairConfig {
8495 pairs: vec![
8496 BracketPair {
8497 start: "<".into(),
8498 end: ">".into(),
8499 close: true,
8500 ..Default::default()
8501 },
8502 BracketPair {
8503 start: "{".into(),
8504 end: "}".into(),
8505 close: true,
8506 ..Default::default()
8507 },
8508 BracketPair {
8509 start: "(".into(),
8510 end: ")".into(),
8511 close: true,
8512 ..Default::default()
8513 },
8514 ],
8515 ..Default::default()
8516 },
8517 autoclose_before: "})]>".into(),
8518 ..Default::default()
8519 },
8520 Some(tree_sitter_html::LANGUAGE.into()),
8521 )
8522 .with_injection_query(
8523 r#"
8524 (script_element
8525 (raw_text) @injection.content
8526 (#set! injection.language "javascript"))
8527 "#,
8528 )
8529 .unwrap(),
8530 );
8531
8532 let javascript_language = Arc::new(Language::new(
8533 LanguageConfig {
8534 name: "JavaScript".into(),
8535 brackets: BracketPairConfig {
8536 pairs: vec![
8537 BracketPair {
8538 start: "/*".into(),
8539 end: " */".into(),
8540 close: true,
8541 ..Default::default()
8542 },
8543 BracketPair {
8544 start: "{".into(),
8545 end: "}".into(),
8546 close: true,
8547 ..Default::default()
8548 },
8549 BracketPair {
8550 start: "(".into(),
8551 end: ")".into(),
8552 close: true,
8553 ..Default::default()
8554 },
8555 ],
8556 ..Default::default()
8557 },
8558 autoclose_before: "})]>".into(),
8559 ..Default::default()
8560 },
8561 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8562 ));
8563
8564 cx.language_registry().add(html_language.clone());
8565 cx.language_registry().add(javascript_language.clone());
8566
8567 cx.update_buffer(|buffer, cx| {
8568 buffer.set_language(Some(html_language), cx);
8569 });
8570
8571 cx.set_state(
8572 &r#"
8573 <body>ˇ
8574 <script>
8575 var x = 1;ˇ
8576 </script>
8577 </body>ˇ
8578 "#
8579 .unindent(),
8580 );
8581
8582 // Precondition: different languages are active at different locations.
8583 cx.update_editor(|editor, window, cx| {
8584 let snapshot = editor.snapshot(window, cx);
8585 let cursors = editor.selections.ranges::<usize>(cx);
8586 let languages = cursors
8587 .iter()
8588 .map(|c| snapshot.language_at(c.start).unwrap().name())
8589 .collect::<Vec<_>>();
8590 assert_eq!(
8591 languages,
8592 &["HTML".into(), "JavaScript".into(), "HTML".into()]
8593 );
8594 });
8595
8596 // Angle brackets autoclose in HTML, but not JavaScript.
8597 cx.update_editor(|editor, window, cx| {
8598 editor.handle_input("<", window, cx);
8599 editor.handle_input("a", window, cx);
8600 });
8601 cx.assert_editor_state(
8602 &r#"
8603 <body><aˇ>
8604 <script>
8605 var x = 1;<aˇ
8606 </script>
8607 </body><aˇ>
8608 "#
8609 .unindent(),
8610 );
8611
8612 // Curly braces and parens autoclose in both HTML and JavaScript.
8613 cx.update_editor(|editor, window, cx| {
8614 editor.handle_input(" b=", window, cx);
8615 editor.handle_input("{", window, cx);
8616 editor.handle_input("c", window, cx);
8617 editor.handle_input("(", window, cx);
8618 });
8619 cx.assert_editor_state(
8620 &r#"
8621 <body><a b={c(ˇ)}>
8622 <script>
8623 var x = 1;<a b={c(ˇ)}
8624 </script>
8625 </body><a b={c(ˇ)}>
8626 "#
8627 .unindent(),
8628 );
8629
8630 // Brackets that were already autoclosed are skipped.
8631 cx.update_editor(|editor, window, cx| {
8632 editor.handle_input(")", window, cx);
8633 editor.handle_input("d", window, cx);
8634 editor.handle_input("}", window, cx);
8635 });
8636 cx.assert_editor_state(
8637 &r#"
8638 <body><a b={c()d}ˇ>
8639 <script>
8640 var x = 1;<a b={c()d}ˇ
8641 </script>
8642 </body><a b={c()d}ˇ>
8643 "#
8644 .unindent(),
8645 );
8646 cx.update_editor(|editor, window, cx| {
8647 editor.handle_input(">", window, cx);
8648 });
8649 cx.assert_editor_state(
8650 &r#"
8651 <body><a b={c()d}>ˇ
8652 <script>
8653 var x = 1;<a b={c()d}>ˇ
8654 </script>
8655 </body><a b={c()d}>ˇ
8656 "#
8657 .unindent(),
8658 );
8659
8660 // Reset
8661 cx.set_state(
8662 &r#"
8663 <body>ˇ
8664 <script>
8665 var x = 1;ˇ
8666 </script>
8667 </body>ˇ
8668 "#
8669 .unindent(),
8670 );
8671
8672 cx.update_editor(|editor, window, cx| {
8673 editor.handle_input("<", window, cx);
8674 });
8675 cx.assert_editor_state(
8676 &r#"
8677 <body><ˇ>
8678 <script>
8679 var x = 1;<ˇ
8680 </script>
8681 </body><ˇ>
8682 "#
8683 .unindent(),
8684 );
8685
8686 // When backspacing, the closing angle brackets are removed.
8687 cx.update_editor(|editor, window, cx| {
8688 editor.backspace(&Backspace, window, cx);
8689 });
8690 cx.assert_editor_state(
8691 &r#"
8692 <body>ˇ
8693 <script>
8694 var x = 1;ˇ
8695 </script>
8696 </body>ˇ
8697 "#
8698 .unindent(),
8699 );
8700
8701 // Block comments autoclose in JavaScript, but not HTML.
8702 cx.update_editor(|editor, window, cx| {
8703 editor.handle_input("/", window, cx);
8704 editor.handle_input("*", window, cx);
8705 });
8706 cx.assert_editor_state(
8707 &r#"
8708 <body>/*ˇ
8709 <script>
8710 var x = 1;/*ˇ */
8711 </script>
8712 </body>/*ˇ
8713 "#
8714 .unindent(),
8715 );
8716}
8717
8718#[gpui::test]
8719async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
8720 init_test(cx, |_| {});
8721
8722 let mut cx = EditorTestContext::new(cx).await;
8723
8724 let rust_language = Arc::new(
8725 Language::new(
8726 LanguageConfig {
8727 name: "Rust".into(),
8728 brackets: serde_json::from_value(json!([
8729 { "start": "{", "end": "}", "close": true, "newline": true },
8730 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
8731 ]))
8732 .unwrap(),
8733 autoclose_before: "})]>".into(),
8734 ..Default::default()
8735 },
8736 Some(tree_sitter_rust::LANGUAGE.into()),
8737 )
8738 .with_override_query("(string_literal) @string")
8739 .unwrap(),
8740 );
8741
8742 cx.language_registry().add(rust_language.clone());
8743 cx.update_buffer(|buffer, cx| {
8744 buffer.set_language(Some(rust_language), cx);
8745 });
8746
8747 cx.set_state(
8748 &r#"
8749 let x = ˇ
8750 "#
8751 .unindent(),
8752 );
8753
8754 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
8755 cx.update_editor(|editor, window, cx| {
8756 editor.handle_input("\"", window, cx);
8757 });
8758 cx.assert_editor_state(
8759 &r#"
8760 let x = "ˇ"
8761 "#
8762 .unindent(),
8763 );
8764
8765 // Inserting another quotation mark. The cursor moves across the existing
8766 // automatically-inserted quotation mark.
8767 cx.update_editor(|editor, window, cx| {
8768 editor.handle_input("\"", window, cx);
8769 });
8770 cx.assert_editor_state(
8771 &r#"
8772 let x = ""ˇ
8773 "#
8774 .unindent(),
8775 );
8776
8777 // Reset
8778 cx.set_state(
8779 &r#"
8780 let x = ˇ
8781 "#
8782 .unindent(),
8783 );
8784
8785 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
8786 cx.update_editor(|editor, window, cx| {
8787 editor.handle_input("\"", window, cx);
8788 editor.handle_input(" ", window, cx);
8789 editor.move_left(&Default::default(), window, cx);
8790 editor.handle_input("\\", window, cx);
8791 editor.handle_input("\"", window, cx);
8792 });
8793 cx.assert_editor_state(
8794 &r#"
8795 let x = "\"ˇ "
8796 "#
8797 .unindent(),
8798 );
8799
8800 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8801 // mark. Nothing is inserted.
8802 cx.update_editor(|editor, window, cx| {
8803 editor.move_right(&Default::default(), window, cx);
8804 editor.handle_input("\"", window, cx);
8805 });
8806 cx.assert_editor_state(
8807 &r#"
8808 let x = "\" "ˇ
8809 "#
8810 .unindent(),
8811 );
8812}
8813
8814#[gpui::test]
8815async fn test_surround_with_pair(cx: &mut TestAppContext) {
8816 init_test(cx, |_| {});
8817
8818 let language = Arc::new(Language::new(
8819 LanguageConfig {
8820 brackets: BracketPairConfig {
8821 pairs: vec![
8822 BracketPair {
8823 start: "{".to_string(),
8824 end: "}".to_string(),
8825 close: true,
8826 surround: true,
8827 newline: true,
8828 },
8829 BracketPair {
8830 start: "/* ".to_string(),
8831 end: "*/".to_string(),
8832 close: true,
8833 surround: true,
8834 ..Default::default()
8835 },
8836 ],
8837 ..Default::default()
8838 },
8839 ..Default::default()
8840 },
8841 Some(tree_sitter_rust::LANGUAGE.into()),
8842 ));
8843
8844 let text = r#"
8845 a
8846 b
8847 c
8848 "#
8849 .unindent();
8850
8851 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8852 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8853 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8854 editor
8855 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8856 .await;
8857
8858 editor.update_in(cx, |editor, window, cx| {
8859 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8860 s.select_display_ranges([
8861 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8862 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8863 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8864 ])
8865 });
8866
8867 editor.handle_input("{", window, cx);
8868 editor.handle_input("{", window, cx);
8869 editor.handle_input("{", window, cx);
8870 assert_eq!(
8871 editor.text(cx),
8872 "
8873 {{{a}}}
8874 {{{b}}}
8875 {{{c}}}
8876 "
8877 .unindent()
8878 );
8879 assert_eq!(
8880 editor.selections.display_ranges(cx),
8881 [
8882 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
8883 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
8884 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
8885 ]
8886 );
8887
8888 editor.undo(&Undo, window, cx);
8889 editor.undo(&Undo, window, cx);
8890 editor.undo(&Undo, window, cx);
8891 assert_eq!(
8892 editor.text(cx),
8893 "
8894 a
8895 b
8896 c
8897 "
8898 .unindent()
8899 );
8900 assert_eq!(
8901 editor.selections.display_ranges(cx),
8902 [
8903 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8904 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8905 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8906 ]
8907 );
8908
8909 // Ensure inserting the first character of a multi-byte bracket pair
8910 // doesn't surround the selections with the bracket.
8911 editor.handle_input("/", window, cx);
8912 assert_eq!(
8913 editor.text(cx),
8914 "
8915 /
8916 /
8917 /
8918 "
8919 .unindent()
8920 );
8921 assert_eq!(
8922 editor.selections.display_ranges(cx),
8923 [
8924 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8925 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8926 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8927 ]
8928 );
8929
8930 editor.undo(&Undo, window, cx);
8931 assert_eq!(
8932 editor.text(cx),
8933 "
8934 a
8935 b
8936 c
8937 "
8938 .unindent()
8939 );
8940 assert_eq!(
8941 editor.selections.display_ranges(cx),
8942 [
8943 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8944 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8945 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8946 ]
8947 );
8948
8949 // Ensure inserting the last character of a multi-byte bracket pair
8950 // doesn't surround the selections with the bracket.
8951 editor.handle_input("*", window, cx);
8952 assert_eq!(
8953 editor.text(cx),
8954 "
8955 *
8956 *
8957 *
8958 "
8959 .unindent()
8960 );
8961 assert_eq!(
8962 editor.selections.display_ranges(cx),
8963 [
8964 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8965 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8966 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8967 ]
8968 );
8969 });
8970}
8971
8972#[gpui::test]
8973async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
8974 init_test(cx, |_| {});
8975
8976 let language = Arc::new(Language::new(
8977 LanguageConfig {
8978 brackets: BracketPairConfig {
8979 pairs: vec![BracketPair {
8980 start: "{".to_string(),
8981 end: "}".to_string(),
8982 close: true,
8983 surround: true,
8984 newline: true,
8985 }],
8986 ..Default::default()
8987 },
8988 autoclose_before: "}".to_string(),
8989 ..Default::default()
8990 },
8991 Some(tree_sitter_rust::LANGUAGE.into()),
8992 ));
8993
8994 let text = r#"
8995 a
8996 b
8997 c
8998 "#
8999 .unindent();
9000
9001 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9002 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9003 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9004 editor
9005 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9006 .await;
9007
9008 editor.update_in(cx, |editor, window, cx| {
9009 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9010 s.select_ranges([
9011 Point::new(0, 1)..Point::new(0, 1),
9012 Point::new(1, 1)..Point::new(1, 1),
9013 Point::new(2, 1)..Point::new(2, 1),
9014 ])
9015 });
9016
9017 editor.handle_input("{", window, cx);
9018 editor.handle_input("{", window, cx);
9019 editor.handle_input("_", window, cx);
9020 assert_eq!(
9021 editor.text(cx),
9022 "
9023 a{{_}}
9024 b{{_}}
9025 c{{_}}
9026 "
9027 .unindent()
9028 );
9029 assert_eq!(
9030 editor.selections.ranges::<Point>(cx),
9031 [
9032 Point::new(0, 4)..Point::new(0, 4),
9033 Point::new(1, 4)..Point::new(1, 4),
9034 Point::new(2, 4)..Point::new(2, 4)
9035 ]
9036 );
9037
9038 editor.backspace(&Default::default(), window, cx);
9039 editor.backspace(&Default::default(), window, cx);
9040 assert_eq!(
9041 editor.text(cx),
9042 "
9043 a{}
9044 b{}
9045 c{}
9046 "
9047 .unindent()
9048 );
9049 assert_eq!(
9050 editor.selections.ranges::<Point>(cx),
9051 [
9052 Point::new(0, 2)..Point::new(0, 2),
9053 Point::new(1, 2)..Point::new(1, 2),
9054 Point::new(2, 2)..Point::new(2, 2)
9055 ]
9056 );
9057
9058 editor.delete_to_previous_word_start(&Default::default(), window, cx);
9059 assert_eq!(
9060 editor.text(cx),
9061 "
9062 a
9063 b
9064 c
9065 "
9066 .unindent()
9067 );
9068 assert_eq!(
9069 editor.selections.ranges::<Point>(cx),
9070 [
9071 Point::new(0, 1)..Point::new(0, 1),
9072 Point::new(1, 1)..Point::new(1, 1),
9073 Point::new(2, 1)..Point::new(2, 1)
9074 ]
9075 );
9076 });
9077}
9078
9079#[gpui::test]
9080async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
9081 init_test(cx, |settings| {
9082 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9083 });
9084
9085 let mut cx = EditorTestContext::new(cx).await;
9086
9087 let language = Arc::new(Language::new(
9088 LanguageConfig {
9089 brackets: BracketPairConfig {
9090 pairs: vec![
9091 BracketPair {
9092 start: "{".to_string(),
9093 end: "}".to_string(),
9094 close: true,
9095 surround: true,
9096 newline: true,
9097 },
9098 BracketPair {
9099 start: "(".to_string(),
9100 end: ")".to_string(),
9101 close: true,
9102 surround: true,
9103 newline: true,
9104 },
9105 BracketPair {
9106 start: "[".to_string(),
9107 end: "]".to_string(),
9108 close: false,
9109 surround: true,
9110 newline: true,
9111 },
9112 ],
9113 ..Default::default()
9114 },
9115 autoclose_before: "})]".to_string(),
9116 ..Default::default()
9117 },
9118 Some(tree_sitter_rust::LANGUAGE.into()),
9119 ));
9120
9121 cx.language_registry().add(language.clone());
9122 cx.update_buffer(|buffer, cx| {
9123 buffer.set_language(Some(language), cx);
9124 });
9125
9126 cx.set_state(
9127 &"
9128 {(ˇ)}
9129 [[ˇ]]
9130 {(ˇ)}
9131 "
9132 .unindent(),
9133 );
9134
9135 cx.update_editor(|editor, window, cx| {
9136 editor.backspace(&Default::default(), window, cx);
9137 editor.backspace(&Default::default(), window, cx);
9138 });
9139
9140 cx.assert_editor_state(
9141 &"
9142 ˇ
9143 ˇ]]
9144 ˇ
9145 "
9146 .unindent(),
9147 );
9148
9149 cx.update_editor(|editor, window, cx| {
9150 editor.handle_input("{", window, cx);
9151 editor.handle_input("{", window, cx);
9152 editor.move_right(&MoveRight, window, cx);
9153 editor.move_right(&MoveRight, window, cx);
9154 editor.move_left(&MoveLeft, window, cx);
9155 editor.move_left(&MoveLeft, window, cx);
9156 editor.backspace(&Default::default(), window, cx);
9157 });
9158
9159 cx.assert_editor_state(
9160 &"
9161 {ˇ}
9162 {ˇ}]]
9163 {ˇ}
9164 "
9165 .unindent(),
9166 );
9167
9168 cx.update_editor(|editor, window, cx| {
9169 editor.backspace(&Default::default(), window, cx);
9170 });
9171
9172 cx.assert_editor_state(
9173 &"
9174 ˇ
9175 ˇ]]
9176 ˇ
9177 "
9178 .unindent(),
9179 );
9180}
9181
9182#[gpui::test]
9183async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
9184 init_test(cx, |_| {});
9185
9186 let language = Arc::new(Language::new(
9187 LanguageConfig::default(),
9188 Some(tree_sitter_rust::LANGUAGE.into()),
9189 ));
9190
9191 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
9192 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9193 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9194 editor
9195 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9196 .await;
9197
9198 editor.update_in(cx, |editor, window, cx| {
9199 editor.set_auto_replace_emoji_shortcode(true);
9200
9201 editor.handle_input("Hello ", window, cx);
9202 editor.handle_input(":wave", window, cx);
9203 assert_eq!(editor.text(cx), "Hello :wave".unindent());
9204
9205 editor.handle_input(":", window, cx);
9206 assert_eq!(editor.text(cx), "Hello 👋".unindent());
9207
9208 editor.handle_input(" :smile", window, cx);
9209 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
9210
9211 editor.handle_input(":", window, cx);
9212 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
9213
9214 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
9215 editor.handle_input(":wave", window, cx);
9216 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
9217
9218 editor.handle_input(":", window, cx);
9219 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
9220
9221 editor.handle_input(":1", window, cx);
9222 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
9223
9224 editor.handle_input(":", window, cx);
9225 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
9226
9227 // Ensure shortcode does not get replaced when it is part of a word
9228 editor.handle_input(" Test:wave", window, cx);
9229 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
9230
9231 editor.handle_input(":", window, cx);
9232 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
9233
9234 editor.set_auto_replace_emoji_shortcode(false);
9235
9236 // Ensure shortcode does not get replaced when auto replace is off
9237 editor.handle_input(" :wave", window, cx);
9238 assert_eq!(
9239 editor.text(cx),
9240 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
9241 );
9242
9243 editor.handle_input(":", window, cx);
9244 assert_eq!(
9245 editor.text(cx),
9246 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
9247 );
9248 });
9249}
9250
9251#[gpui::test]
9252async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
9253 init_test(cx, |_| {});
9254
9255 let (text, insertion_ranges) = marked_text_ranges(
9256 indoc! {"
9257 ˇ
9258 "},
9259 false,
9260 );
9261
9262 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
9263 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9264
9265 _ = editor.update_in(cx, |editor, window, cx| {
9266 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
9267
9268 editor
9269 .insert_snippet(&insertion_ranges, snippet, window, cx)
9270 .unwrap();
9271
9272 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
9273 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
9274 assert_eq!(editor.text(cx), expected_text);
9275 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
9276 }
9277
9278 assert(
9279 editor,
9280 cx,
9281 indoc! {"
9282 type «» =•
9283 "},
9284 );
9285
9286 assert!(editor.context_menu_visible(), "There should be a matches");
9287 });
9288}
9289
9290#[gpui::test]
9291async fn test_snippets(cx: &mut TestAppContext) {
9292 init_test(cx, |_| {});
9293
9294 let mut cx = EditorTestContext::new(cx).await;
9295
9296 cx.set_state(indoc! {"
9297 a.ˇ b
9298 a.ˇ b
9299 a.ˇ b
9300 "});
9301
9302 cx.update_editor(|editor, window, cx| {
9303 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
9304 let insertion_ranges = editor
9305 .selections
9306 .all(cx)
9307 .iter()
9308 .map(|s| s.range().clone())
9309 .collect::<Vec<_>>();
9310 editor
9311 .insert_snippet(&insertion_ranges, snippet, window, cx)
9312 .unwrap();
9313 });
9314
9315 cx.assert_editor_state(indoc! {"
9316 a.f(«oneˇ», two, «threeˇ») b
9317 a.f(«oneˇ», two, «threeˇ») b
9318 a.f(«oneˇ», two, «threeˇ») b
9319 "});
9320
9321 // Can't move earlier than the first tab stop
9322 cx.update_editor(|editor, window, cx| {
9323 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9324 });
9325 cx.assert_editor_state(indoc! {"
9326 a.f(«oneˇ», two, «threeˇ») b
9327 a.f(«oneˇ», two, «threeˇ») b
9328 a.f(«oneˇ», two, «threeˇ») b
9329 "});
9330
9331 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9332 cx.assert_editor_state(indoc! {"
9333 a.f(one, «twoˇ», three) b
9334 a.f(one, «twoˇ», three) b
9335 a.f(one, «twoˇ», three) b
9336 "});
9337
9338 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
9339 cx.assert_editor_state(indoc! {"
9340 a.f(«oneˇ», two, «threeˇ») b
9341 a.f(«oneˇ», two, «threeˇ») b
9342 a.f(«oneˇ», two, «threeˇ») b
9343 "});
9344
9345 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9346 cx.assert_editor_state(indoc! {"
9347 a.f(one, «twoˇ», three) b
9348 a.f(one, «twoˇ», three) b
9349 a.f(one, «twoˇ», three) b
9350 "});
9351 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9352 cx.assert_editor_state(indoc! {"
9353 a.f(one, two, three)ˇ b
9354 a.f(one, two, three)ˇ b
9355 a.f(one, two, three)ˇ b
9356 "});
9357
9358 // As soon as the last tab stop is reached, snippet state is gone
9359 cx.update_editor(|editor, window, cx| {
9360 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
9361 });
9362 cx.assert_editor_state(indoc! {"
9363 a.f(one, two, three)ˇ b
9364 a.f(one, two, three)ˇ b
9365 a.f(one, two, three)ˇ b
9366 "});
9367}
9368
9369#[gpui::test]
9370async fn test_snippet_indentation(cx: &mut TestAppContext) {
9371 init_test(cx, |_| {});
9372
9373 let mut cx = EditorTestContext::new(cx).await;
9374
9375 cx.update_editor(|editor, window, cx| {
9376 let snippet = Snippet::parse(indoc! {"
9377 /*
9378 * Multiline comment with leading indentation
9379 *
9380 * $1
9381 */
9382 $0"})
9383 .unwrap();
9384 let insertion_ranges = editor
9385 .selections
9386 .all(cx)
9387 .iter()
9388 .map(|s| s.range().clone())
9389 .collect::<Vec<_>>();
9390 editor
9391 .insert_snippet(&insertion_ranges, snippet, window, cx)
9392 .unwrap();
9393 });
9394
9395 cx.assert_editor_state(indoc! {"
9396 /*
9397 * Multiline comment with leading indentation
9398 *
9399 * ˇ
9400 */
9401 "});
9402
9403 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
9404 cx.assert_editor_state(indoc! {"
9405 /*
9406 * Multiline comment with leading indentation
9407 *
9408 *•
9409 */
9410 ˇ"});
9411}
9412
9413#[gpui::test]
9414async fn test_document_format_during_save(cx: &mut TestAppContext) {
9415 init_test(cx, |_| {});
9416
9417 let fs = FakeFs::new(cx.executor());
9418 fs.insert_file(path!("/file.rs"), Default::default()).await;
9419
9420 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
9421
9422 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9423 language_registry.add(rust_lang());
9424 let mut fake_servers = language_registry.register_fake_lsp(
9425 "Rust",
9426 FakeLspAdapter {
9427 capabilities: lsp::ServerCapabilities {
9428 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9429 ..Default::default()
9430 },
9431 ..Default::default()
9432 },
9433 );
9434
9435 let buffer = project
9436 .update(cx, |project, cx| {
9437 project.open_local_buffer(path!("/file.rs"), cx)
9438 })
9439 .await
9440 .unwrap();
9441
9442 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9443 let (editor, cx) = cx.add_window_view(|window, cx| {
9444 build_editor_with_project(project.clone(), buffer, window, cx)
9445 });
9446 editor.update_in(cx, |editor, window, cx| {
9447 editor.set_text("one\ntwo\nthree\n", window, cx)
9448 });
9449 assert!(cx.read(|cx| editor.is_dirty(cx)));
9450
9451 cx.executor().start_waiting();
9452 let fake_server = fake_servers.next().await.unwrap();
9453
9454 {
9455 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9456 move |params, _| async move {
9457 assert_eq!(
9458 params.text_document.uri,
9459 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9460 );
9461 assert_eq!(params.options.tab_size, 4);
9462 Ok(Some(vec![lsp::TextEdit::new(
9463 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9464 ", ".to_string(),
9465 )]))
9466 },
9467 );
9468 let save = editor
9469 .update_in(cx, |editor, window, cx| {
9470 editor.save(
9471 SaveOptions {
9472 format: true,
9473 autosave: false,
9474 },
9475 project.clone(),
9476 window,
9477 cx,
9478 )
9479 })
9480 .unwrap();
9481 cx.executor().start_waiting();
9482 save.await;
9483
9484 assert_eq!(
9485 editor.update(cx, |editor, cx| editor.text(cx)),
9486 "one, two\nthree\n"
9487 );
9488 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9489 }
9490
9491 {
9492 editor.update_in(cx, |editor, window, cx| {
9493 editor.set_text("one\ntwo\nthree\n", window, cx)
9494 });
9495 assert!(cx.read(|cx| editor.is_dirty(cx)));
9496
9497 // Ensure we can still save even if formatting hangs.
9498 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9499 move |params, _| async move {
9500 assert_eq!(
9501 params.text_document.uri,
9502 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9503 );
9504 futures::future::pending::<()>().await;
9505 unreachable!()
9506 },
9507 );
9508 let save = editor
9509 .update_in(cx, |editor, window, cx| {
9510 editor.save(
9511 SaveOptions {
9512 format: true,
9513 autosave: false,
9514 },
9515 project.clone(),
9516 window,
9517 cx,
9518 )
9519 })
9520 .unwrap();
9521 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9522 cx.executor().start_waiting();
9523 save.await;
9524 assert_eq!(
9525 editor.update(cx, |editor, cx| editor.text(cx)),
9526 "one\ntwo\nthree\n"
9527 );
9528 }
9529
9530 // Set rust language override and assert overridden tabsize is sent to language server
9531 update_test_language_settings(cx, |settings| {
9532 settings.languages.0.insert(
9533 "Rust".into(),
9534 LanguageSettingsContent {
9535 tab_size: NonZeroU32::new(8),
9536 ..Default::default()
9537 },
9538 );
9539 });
9540
9541 {
9542 editor.update_in(cx, |editor, window, cx| {
9543 editor.set_text("somehting_new\n", window, cx)
9544 });
9545 assert!(cx.read(|cx| editor.is_dirty(cx)));
9546 let _formatting_request_signal = fake_server
9547 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9548 assert_eq!(
9549 params.text_document.uri,
9550 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9551 );
9552 assert_eq!(params.options.tab_size, 8);
9553 Ok(Some(vec![]))
9554 });
9555 let save = editor
9556 .update_in(cx, |editor, window, cx| {
9557 editor.save(
9558 SaveOptions {
9559 format: true,
9560 autosave: false,
9561 },
9562 project.clone(),
9563 window,
9564 cx,
9565 )
9566 })
9567 .unwrap();
9568 cx.executor().start_waiting();
9569 save.await;
9570 }
9571}
9572
9573#[gpui::test]
9574async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
9575 init_test(cx, |settings| {
9576 settings.defaults.ensure_final_newline_on_save = Some(false);
9577 });
9578
9579 let fs = FakeFs::new(cx.executor());
9580 fs.insert_file(path!("/file.txt"), "foo".into()).await;
9581
9582 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
9583
9584 let buffer = project
9585 .update(cx, |project, cx| {
9586 project.open_local_buffer(path!("/file.txt"), cx)
9587 })
9588 .await
9589 .unwrap();
9590
9591 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9592 let (editor, cx) = cx.add_window_view(|window, cx| {
9593 build_editor_with_project(project.clone(), buffer, window, cx)
9594 });
9595 editor.update_in(cx, |editor, window, cx| {
9596 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9597 s.select_ranges([0..0])
9598 });
9599 });
9600 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9601
9602 editor.update_in(cx, |editor, window, cx| {
9603 editor.handle_input("\n", window, cx)
9604 });
9605 cx.run_until_parked();
9606 save(&editor, &project, cx).await;
9607 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9608
9609 editor.update_in(cx, |editor, window, cx| {
9610 editor.undo(&Default::default(), window, cx);
9611 });
9612 save(&editor, &project, cx).await;
9613 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9614
9615 editor.update_in(cx, |editor, window, cx| {
9616 editor.redo(&Default::default(), window, cx);
9617 });
9618 cx.run_until_parked();
9619 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
9620
9621 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
9622 let save = editor
9623 .update_in(cx, |editor, window, cx| {
9624 editor.save(
9625 SaveOptions {
9626 format: true,
9627 autosave: false,
9628 },
9629 project.clone(),
9630 window,
9631 cx,
9632 )
9633 })
9634 .unwrap();
9635 cx.executor().start_waiting();
9636 save.await;
9637 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9638 }
9639}
9640
9641#[gpui::test]
9642async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
9643 init_test(cx, |_| {});
9644
9645 let cols = 4;
9646 let rows = 10;
9647 let sample_text_1 = sample_text(rows, cols, 'a');
9648 assert_eq!(
9649 sample_text_1,
9650 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
9651 );
9652 let sample_text_2 = sample_text(rows, cols, 'l');
9653 assert_eq!(
9654 sample_text_2,
9655 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
9656 );
9657 let sample_text_3 = sample_text(rows, cols, 'v');
9658 assert_eq!(
9659 sample_text_3,
9660 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
9661 );
9662
9663 let fs = FakeFs::new(cx.executor());
9664 fs.insert_tree(
9665 path!("/a"),
9666 json!({
9667 "main.rs": sample_text_1,
9668 "other.rs": sample_text_2,
9669 "lib.rs": sample_text_3,
9670 }),
9671 )
9672 .await;
9673
9674 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
9675 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9676 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9677
9678 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9679 language_registry.add(rust_lang());
9680 let mut fake_servers = language_registry.register_fake_lsp(
9681 "Rust",
9682 FakeLspAdapter {
9683 capabilities: lsp::ServerCapabilities {
9684 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9685 ..Default::default()
9686 },
9687 ..Default::default()
9688 },
9689 );
9690
9691 let worktree = project.update(cx, |project, cx| {
9692 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
9693 assert_eq!(worktrees.len(), 1);
9694 worktrees.pop().unwrap()
9695 });
9696 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9697
9698 let buffer_1 = project
9699 .update(cx, |project, cx| {
9700 project.open_buffer((worktree_id, "main.rs"), cx)
9701 })
9702 .await
9703 .unwrap();
9704 let buffer_2 = project
9705 .update(cx, |project, cx| {
9706 project.open_buffer((worktree_id, "other.rs"), cx)
9707 })
9708 .await
9709 .unwrap();
9710 let buffer_3 = project
9711 .update(cx, |project, cx| {
9712 project.open_buffer((worktree_id, "lib.rs"), cx)
9713 })
9714 .await
9715 .unwrap();
9716
9717 let multi_buffer = cx.new(|cx| {
9718 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9719 multi_buffer.push_excerpts(
9720 buffer_1.clone(),
9721 [
9722 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9723 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9724 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9725 ],
9726 cx,
9727 );
9728 multi_buffer.push_excerpts(
9729 buffer_2.clone(),
9730 [
9731 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9732 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9733 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9734 ],
9735 cx,
9736 );
9737 multi_buffer.push_excerpts(
9738 buffer_3.clone(),
9739 [
9740 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
9741 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
9742 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
9743 ],
9744 cx,
9745 );
9746 multi_buffer
9747 });
9748 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
9749 Editor::new(
9750 EditorMode::full(),
9751 multi_buffer,
9752 Some(project.clone()),
9753 window,
9754 cx,
9755 )
9756 });
9757
9758 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9759 editor.change_selections(
9760 SelectionEffects::scroll(Autoscroll::Next),
9761 window,
9762 cx,
9763 |s| s.select_ranges(Some(1..2)),
9764 );
9765 editor.insert("|one|two|three|", window, cx);
9766 });
9767 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9768 multi_buffer_editor.update_in(cx, |editor, window, cx| {
9769 editor.change_selections(
9770 SelectionEffects::scroll(Autoscroll::Next),
9771 window,
9772 cx,
9773 |s| s.select_ranges(Some(60..70)),
9774 );
9775 editor.insert("|four|five|six|", window, cx);
9776 });
9777 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
9778
9779 // First two buffers should be edited, but not the third one.
9780 assert_eq!(
9781 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9782 "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}",
9783 );
9784 buffer_1.update(cx, |buffer, _| {
9785 assert!(buffer.is_dirty());
9786 assert_eq!(
9787 buffer.text(),
9788 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
9789 )
9790 });
9791 buffer_2.update(cx, |buffer, _| {
9792 assert!(buffer.is_dirty());
9793 assert_eq!(
9794 buffer.text(),
9795 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
9796 )
9797 });
9798 buffer_3.update(cx, |buffer, _| {
9799 assert!(!buffer.is_dirty());
9800 assert_eq!(buffer.text(), sample_text_3,)
9801 });
9802 cx.executor().run_until_parked();
9803
9804 cx.executor().start_waiting();
9805 let save = multi_buffer_editor
9806 .update_in(cx, |editor, window, cx| {
9807 editor.save(
9808 SaveOptions {
9809 format: true,
9810 autosave: false,
9811 },
9812 project.clone(),
9813 window,
9814 cx,
9815 )
9816 })
9817 .unwrap();
9818
9819 let fake_server = fake_servers.next().await.unwrap();
9820 fake_server
9821 .server
9822 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
9823 Ok(Some(vec![lsp::TextEdit::new(
9824 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9825 format!("[{} formatted]", params.text_document.uri),
9826 )]))
9827 })
9828 .detach();
9829 save.await;
9830
9831 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
9832 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
9833 assert_eq!(
9834 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
9835 uri!(
9836 "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}"
9837 ),
9838 );
9839 buffer_1.update(cx, |buffer, _| {
9840 assert!(!buffer.is_dirty());
9841 assert_eq!(
9842 buffer.text(),
9843 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
9844 )
9845 });
9846 buffer_2.update(cx, |buffer, _| {
9847 assert!(!buffer.is_dirty());
9848 assert_eq!(
9849 buffer.text(),
9850 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
9851 )
9852 });
9853 buffer_3.update(cx, |buffer, _| {
9854 assert!(!buffer.is_dirty());
9855 assert_eq!(buffer.text(), sample_text_3,)
9856 });
9857}
9858
9859#[gpui::test]
9860async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
9861 init_test(cx, |_| {});
9862
9863 let fs = FakeFs::new(cx.executor());
9864 fs.insert_tree(
9865 path!("/dir"),
9866 json!({
9867 "file1.rs": "fn main() { println!(\"hello\"); }",
9868 "file2.rs": "fn test() { println!(\"test\"); }",
9869 "file3.rs": "fn other() { println!(\"other\"); }\n",
9870 }),
9871 )
9872 .await;
9873
9874 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
9875 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9876 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9877
9878 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9879 language_registry.add(rust_lang());
9880
9881 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
9882 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9883
9884 // Open three buffers
9885 let buffer_1 = project
9886 .update(cx, |project, cx| {
9887 project.open_buffer((worktree_id, "file1.rs"), cx)
9888 })
9889 .await
9890 .unwrap();
9891 let buffer_2 = project
9892 .update(cx, |project, cx| {
9893 project.open_buffer((worktree_id, "file2.rs"), cx)
9894 })
9895 .await
9896 .unwrap();
9897 let buffer_3 = project
9898 .update(cx, |project, cx| {
9899 project.open_buffer((worktree_id, "file3.rs"), cx)
9900 })
9901 .await
9902 .unwrap();
9903
9904 // Create a multi-buffer with all three buffers
9905 let multi_buffer = cx.new(|cx| {
9906 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9907 multi_buffer.push_excerpts(
9908 buffer_1.clone(),
9909 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9910 cx,
9911 );
9912 multi_buffer.push_excerpts(
9913 buffer_2.clone(),
9914 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9915 cx,
9916 );
9917 multi_buffer.push_excerpts(
9918 buffer_3.clone(),
9919 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9920 cx,
9921 );
9922 multi_buffer
9923 });
9924
9925 let editor = cx.new_window_entity(|window, cx| {
9926 Editor::new(
9927 EditorMode::full(),
9928 multi_buffer,
9929 Some(project.clone()),
9930 window,
9931 cx,
9932 )
9933 });
9934
9935 // Edit only the first buffer
9936 editor.update_in(cx, |editor, window, cx| {
9937 editor.change_selections(
9938 SelectionEffects::scroll(Autoscroll::Next),
9939 window,
9940 cx,
9941 |s| s.select_ranges(Some(10..10)),
9942 );
9943 editor.insert("// edited", window, cx);
9944 });
9945
9946 // Verify that only buffer 1 is dirty
9947 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
9948 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9949 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9950
9951 // Get write counts after file creation (files were created with initial content)
9952 // We expect each file to have been written once during creation
9953 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
9954 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
9955 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
9956
9957 // Perform autosave
9958 let save_task = editor.update_in(cx, |editor, window, cx| {
9959 editor.save(
9960 SaveOptions {
9961 format: true,
9962 autosave: true,
9963 },
9964 project.clone(),
9965 window,
9966 cx,
9967 )
9968 });
9969 save_task.await.unwrap();
9970
9971 // Only the dirty buffer should have been saved
9972 assert_eq!(
9973 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9974 1,
9975 "Buffer 1 was dirty, so it should have been written once during autosave"
9976 );
9977 assert_eq!(
9978 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9979 0,
9980 "Buffer 2 was clean, so it should not have been written during autosave"
9981 );
9982 assert_eq!(
9983 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9984 0,
9985 "Buffer 3 was clean, so it should not have been written during autosave"
9986 );
9987
9988 // Verify buffer states after autosave
9989 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9990 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9991 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9992
9993 // Now perform a manual save (format = true)
9994 let save_task = editor.update_in(cx, |editor, window, cx| {
9995 editor.save(
9996 SaveOptions {
9997 format: true,
9998 autosave: false,
9999 },
10000 project.clone(),
10001 window,
10002 cx,
10003 )
10004 });
10005 save_task.await.unwrap();
10006
10007 // During manual save, clean buffers don't get written to disk
10008 // They just get did_save called for language server notifications
10009 assert_eq!(
10010 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
10011 1,
10012 "Buffer 1 should only have been written once total (during autosave, not manual save)"
10013 );
10014 assert_eq!(
10015 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
10016 0,
10017 "Buffer 2 should not have been written at all"
10018 );
10019 assert_eq!(
10020 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
10021 0,
10022 "Buffer 3 should not have been written at all"
10023 );
10024}
10025
10026async fn setup_range_format_test(
10027 cx: &mut TestAppContext,
10028) -> (
10029 Entity<Project>,
10030 Entity<Editor>,
10031 &mut gpui::VisualTestContext,
10032 lsp::FakeLanguageServer,
10033) {
10034 init_test(cx, |_| {});
10035
10036 let fs = FakeFs::new(cx.executor());
10037 fs.insert_file(path!("/file.rs"), Default::default()).await;
10038
10039 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10040
10041 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10042 language_registry.add(rust_lang());
10043 let mut fake_servers = language_registry.register_fake_lsp(
10044 "Rust",
10045 FakeLspAdapter {
10046 capabilities: lsp::ServerCapabilities {
10047 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
10048 ..lsp::ServerCapabilities::default()
10049 },
10050 ..FakeLspAdapter::default()
10051 },
10052 );
10053
10054 let buffer = project
10055 .update(cx, |project, cx| {
10056 project.open_local_buffer(path!("/file.rs"), cx)
10057 })
10058 .await
10059 .unwrap();
10060
10061 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10062 let (editor, cx) = cx.add_window_view(|window, cx| {
10063 build_editor_with_project(project.clone(), buffer, window, cx)
10064 });
10065
10066 cx.executor().start_waiting();
10067 let fake_server = fake_servers.next().await.unwrap();
10068
10069 (project, editor, cx, fake_server)
10070}
10071
10072#[gpui::test]
10073async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
10074 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10075
10076 editor.update_in(cx, |editor, window, cx| {
10077 editor.set_text("one\ntwo\nthree\n", window, cx)
10078 });
10079 assert!(cx.read(|cx| editor.is_dirty(cx)));
10080
10081 let save = editor
10082 .update_in(cx, |editor, window, cx| {
10083 editor.save(
10084 SaveOptions {
10085 format: true,
10086 autosave: false,
10087 },
10088 project.clone(),
10089 window,
10090 cx,
10091 )
10092 })
10093 .unwrap();
10094 fake_server
10095 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10096 assert_eq!(
10097 params.text_document.uri,
10098 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10099 );
10100 assert_eq!(params.options.tab_size, 4);
10101 Ok(Some(vec![lsp::TextEdit::new(
10102 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10103 ", ".to_string(),
10104 )]))
10105 })
10106 .next()
10107 .await;
10108 cx.executor().start_waiting();
10109 save.await;
10110 assert_eq!(
10111 editor.update(cx, |editor, cx| editor.text(cx)),
10112 "one, two\nthree\n"
10113 );
10114 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10115}
10116
10117#[gpui::test]
10118async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
10119 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10120
10121 editor.update_in(cx, |editor, window, cx| {
10122 editor.set_text("one\ntwo\nthree\n", window, cx)
10123 });
10124 assert!(cx.read(|cx| editor.is_dirty(cx)));
10125
10126 // Test that save still works when formatting hangs
10127 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
10128 move |params, _| async move {
10129 assert_eq!(
10130 params.text_document.uri,
10131 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10132 );
10133 futures::future::pending::<()>().await;
10134 unreachable!()
10135 },
10136 );
10137 let save = editor
10138 .update_in(cx, |editor, window, cx| {
10139 editor.save(
10140 SaveOptions {
10141 format: true,
10142 autosave: false,
10143 },
10144 project.clone(),
10145 window,
10146 cx,
10147 )
10148 })
10149 .unwrap();
10150 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10151 cx.executor().start_waiting();
10152 save.await;
10153 assert_eq!(
10154 editor.update(cx, |editor, cx| editor.text(cx)),
10155 "one\ntwo\nthree\n"
10156 );
10157 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10158}
10159
10160#[gpui::test]
10161async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
10162 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10163
10164 // Buffer starts clean, no formatting should be requested
10165 let save = editor
10166 .update_in(cx, |editor, window, cx| {
10167 editor.save(
10168 SaveOptions {
10169 format: false,
10170 autosave: false,
10171 },
10172 project.clone(),
10173 window,
10174 cx,
10175 )
10176 })
10177 .unwrap();
10178 let _pending_format_request = fake_server
10179 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
10180 panic!("Should not be invoked");
10181 })
10182 .next();
10183 cx.executor().start_waiting();
10184 save.await;
10185 cx.run_until_parked();
10186}
10187
10188#[gpui::test]
10189async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
10190 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
10191
10192 // Set Rust language override and assert overridden tabsize is sent to language server
10193 update_test_language_settings(cx, |settings| {
10194 settings.languages.0.insert(
10195 "Rust".into(),
10196 LanguageSettingsContent {
10197 tab_size: NonZeroU32::new(8),
10198 ..Default::default()
10199 },
10200 );
10201 });
10202
10203 editor.update_in(cx, |editor, window, cx| {
10204 editor.set_text("something_new\n", window, cx)
10205 });
10206 assert!(cx.read(|cx| editor.is_dirty(cx)));
10207 let save = editor
10208 .update_in(cx, |editor, window, cx| {
10209 editor.save(
10210 SaveOptions {
10211 format: true,
10212 autosave: false,
10213 },
10214 project.clone(),
10215 window,
10216 cx,
10217 )
10218 })
10219 .unwrap();
10220 fake_server
10221 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
10222 assert_eq!(
10223 params.text_document.uri,
10224 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10225 );
10226 assert_eq!(params.options.tab_size, 8);
10227 Ok(Some(Vec::new()))
10228 })
10229 .next()
10230 .await;
10231 save.await;
10232}
10233
10234#[gpui::test]
10235async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
10236 init_test(cx, |settings| {
10237 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
10238 Formatter::LanguageServer { name: None },
10239 )))
10240 });
10241
10242 let fs = FakeFs::new(cx.executor());
10243 fs.insert_file(path!("/file.rs"), Default::default()).await;
10244
10245 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10246
10247 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10248 language_registry.add(Arc::new(Language::new(
10249 LanguageConfig {
10250 name: "Rust".into(),
10251 matcher: LanguageMatcher {
10252 path_suffixes: vec!["rs".to_string()],
10253 ..Default::default()
10254 },
10255 ..LanguageConfig::default()
10256 },
10257 Some(tree_sitter_rust::LANGUAGE.into()),
10258 )));
10259 update_test_language_settings(cx, |settings| {
10260 // Enable Prettier formatting for the same buffer, and ensure
10261 // LSP is called instead of Prettier.
10262 settings.defaults.prettier = Some(PrettierSettings {
10263 allowed: true,
10264 ..PrettierSettings::default()
10265 });
10266 });
10267 let mut fake_servers = language_registry.register_fake_lsp(
10268 "Rust",
10269 FakeLspAdapter {
10270 capabilities: lsp::ServerCapabilities {
10271 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10272 ..Default::default()
10273 },
10274 ..Default::default()
10275 },
10276 );
10277
10278 let buffer = project
10279 .update(cx, |project, cx| {
10280 project.open_local_buffer(path!("/file.rs"), cx)
10281 })
10282 .await
10283 .unwrap();
10284
10285 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10286 let (editor, cx) = cx.add_window_view(|window, cx| {
10287 build_editor_with_project(project.clone(), buffer, window, cx)
10288 });
10289 editor.update_in(cx, |editor, window, cx| {
10290 editor.set_text("one\ntwo\nthree\n", window, cx)
10291 });
10292
10293 cx.executor().start_waiting();
10294 let fake_server = fake_servers.next().await.unwrap();
10295
10296 let format = editor
10297 .update_in(cx, |editor, window, cx| {
10298 editor.perform_format(
10299 project.clone(),
10300 FormatTrigger::Manual,
10301 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10302 window,
10303 cx,
10304 )
10305 })
10306 .unwrap();
10307 fake_server
10308 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10309 assert_eq!(
10310 params.text_document.uri,
10311 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10312 );
10313 assert_eq!(params.options.tab_size, 4);
10314 Ok(Some(vec![lsp::TextEdit::new(
10315 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10316 ", ".to_string(),
10317 )]))
10318 })
10319 .next()
10320 .await;
10321 cx.executor().start_waiting();
10322 format.await;
10323 assert_eq!(
10324 editor.update(cx, |editor, cx| editor.text(cx)),
10325 "one, two\nthree\n"
10326 );
10327
10328 editor.update_in(cx, |editor, window, cx| {
10329 editor.set_text("one\ntwo\nthree\n", window, cx)
10330 });
10331 // Ensure we don't lock if formatting hangs.
10332 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10333 move |params, _| async move {
10334 assert_eq!(
10335 params.text_document.uri,
10336 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
10337 );
10338 futures::future::pending::<()>().await;
10339 unreachable!()
10340 },
10341 );
10342 let format = editor
10343 .update_in(cx, |editor, window, cx| {
10344 editor.perform_format(
10345 project,
10346 FormatTrigger::Manual,
10347 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10348 window,
10349 cx,
10350 )
10351 })
10352 .unwrap();
10353 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10354 cx.executor().start_waiting();
10355 format.await;
10356 assert_eq!(
10357 editor.update(cx, |editor, cx| editor.text(cx)),
10358 "one\ntwo\nthree\n"
10359 );
10360}
10361
10362#[gpui::test]
10363async fn test_multiple_formatters(cx: &mut TestAppContext) {
10364 init_test(cx, |settings| {
10365 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
10366 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10367 Formatter::LanguageServer { name: None },
10368 Formatter::CodeActions(
10369 [
10370 ("code-action-1".into(), true),
10371 ("code-action-2".into(), true),
10372 ]
10373 .into_iter()
10374 .collect(),
10375 ),
10376 ])))
10377 });
10378
10379 let fs = FakeFs::new(cx.executor());
10380 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
10381 .await;
10382
10383 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10384 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10385 language_registry.add(rust_lang());
10386
10387 let mut fake_servers = language_registry.register_fake_lsp(
10388 "Rust",
10389 FakeLspAdapter {
10390 capabilities: lsp::ServerCapabilities {
10391 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10392 execute_command_provider: Some(lsp::ExecuteCommandOptions {
10393 commands: vec!["the-command-for-code-action-1".into()],
10394 ..Default::default()
10395 }),
10396 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10397 ..Default::default()
10398 },
10399 ..Default::default()
10400 },
10401 );
10402
10403 let buffer = project
10404 .update(cx, |project, cx| {
10405 project.open_local_buffer(path!("/file.rs"), cx)
10406 })
10407 .await
10408 .unwrap();
10409
10410 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10411 let (editor, cx) = cx.add_window_view(|window, cx| {
10412 build_editor_with_project(project.clone(), buffer, window, cx)
10413 });
10414
10415 cx.executor().start_waiting();
10416
10417 let fake_server = fake_servers.next().await.unwrap();
10418 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10419 move |_params, _| async move {
10420 Ok(Some(vec![lsp::TextEdit::new(
10421 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10422 "applied-formatting\n".to_string(),
10423 )]))
10424 },
10425 );
10426 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10427 move |params, _| async move {
10428 assert_eq!(
10429 params.context.only,
10430 Some(vec!["code-action-1".into(), "code-action-2".into()])
10431 );
10432 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
10433 Ok(Some(vec![
10434 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10435 kind: Some("code-action-1".into()),
10436 edit: Some(lsp::WorkspaceEdit::new(
10437 [(
10438 uri.clone(),
10439 vec![lsp::TextEdit::new(
10440 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10441 "applied-code-action-1-edit\n".to_string(),
10442 )],
10443 )]
10444 .into_iter()
10445 .collect(),
10446 )),
10447 command: Some(lsp::Command {
10448 command: "the-command-for-code-action-1".into(),
10449 ..Default::default()
10450 }),
10451 ..Default::default()
10452 }),
10453 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
10454 kind: Some("code-action-2".into()),
10455 edit: Some(lsp::WorkspaceEdit::new(
10456 [(
10457 uri.clone(),
10458 vec![lsp::TextEdit::new(
10459 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
10460 "applied-code-action-2-edit\n".to_string(),
10461 )],
10462 )]
10463 .into_iter()
10464 .collect(),
10465 )),
10466 ..Default::default()
10467 }),
10468 ]))
10469 },
10470 );
10471
10472 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
10473 move |params, _| async move { Ok(params) }
10474 });
10475
10476 let command_lock = Arc::new(futures::lock::Mutex::new(()));
10477 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
10478 let fake = fake_server.clone();
10479 let lock = command_lock.clone();
10480 move |params, _| {
10481 assert_eq!(params.command, "the-command-for-code-action-1");
10482 let fake = fake.clone();
10483 let lock = lock.clone();
10484 async move {
10485 lock.lock().await;
10486 fake.server
10487 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
10488 label: None,
10489 edit: lsp::WorkspaceEdit {
10490 changes: Some(
10491 [(
10492 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
10493 vec![lsp::TextEdit {
10494 range: lsp::Range::new(
10495 lsp::Position::new(0, 0),
10496 lsp::Position::new(0, 0),
10497 ),
10498 new_text: "applied-code-action-1-command\n".into(),
10499 }],
10500 )]
10501 .into_iter()
10502 .collect(),
10503 ),
10504 ..Default::default()
10505 },
10506 })
10507 .await
10508 .into_response()
10509 .unwrap();
10510 Ok(Some(json!(null)))
10511 }
10512 }
10513 });
10514
10515 cx.executor().start_waiting();
10516 editor
10517 .update_in(cx, |editor, window, cx| {
10518 editor.perform_format(
10519 project.clone(),
10520 FormatTrigger::Manual,
10521 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10522 window,
10523 cx,
10524 )
10525 })
10526 .unwrap()
10527 .await;
10528 editor.update(cx, |editor, cx| {
10529 assert_eq!(
10530 editor.text(cx),
10531 r#"
10532 applied-code-action-2-edit
10533 applied-code-action-1-command
10534 applied-code-action-1-edit
10535 applied-formatting
10536 one
10537 two
10538 three
10539 "#
10540 .unindent()
10541 );
10542 });
10543
10544 editor.update_in(cx, |editor, window, cx| {
10545 editor.undo(&Default::default(), window, cx);
10546 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10547 });
10548
10549 // Perform a manual edit while waiting for an LSP command
10550 // that's being run as part of a formatting code action.
10551 let lock_guard = command_lock.lock().await;
10552 let format = editor
10553 .update_in(cx, |editor, window, cx| {
10554 editor.perform_format(
10555 project.clone(),
10556 FormatTrigger::Manual,
10557 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
10558 window,
10559 cx,
10560 )
10561 })
10562 .unwrap();
10563 cx.run_until_parked();
10564 editor.update(cx, |editor, cx| {
10565 assert_eq!(
10566 editor.text(cx),
10567 r#"
10568 applied-code-action-1-edit
10569 applied-formatting
10570 one
10571 two
10572 three
10573 "#
10574 .unindent()
10575 );
10576
10577 editor.buffer.update(cx, |buffer, cx| {
10578 let ix = buffer.len(cx);
10579 buffer.edit([(ix..ix, "edited\n")], None, cx);
10580 });
10581 });
10582
10583 // Allow the LSP command to proceed. Because the buffer was edited,
10584 // the second code action will not be run.
10585 drop(lock_guard);
10586 format.await;
10587 editor.update_in(cx, |editor, window, cx| {
10588 assert_eq!(
10589 editor.text(cx),
10590 r#"
10591 applied-code-action-1-command
10592 applied-code-action-1-edit
10593 applied-formatting
10594 one
10595 two
10596 three
10597 edited
10598 "#
10599 .unindent()
10600 );
10601
10602 // The manual edit is undone first, because it is the last thing the user did
10603 // (even though the command completed afterwards).
10604 editor.undo(&Default::default(), window, cx);
10605 assert_eq!(
10606 editor.text(cx),
10607 r#"
10608 applied-code-action-1-command
10609 applied-code-action-1-edit
10610 applied-formatting
10611 one
10612 two
10613 three
10614 "#
10615 .unindent()
10616 );
10617
10618 // All the formatting (including the command, which completed after the manual edit)
10619 // is undone together.
10620 editor.undo(&Default::default(), window, cx);
10621 assert_eq!(editor.text(cx), "one \ntwo \nthree");
10622 });
10623}
10624
10625#[gpui::test]
10626async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
10627 init_test(cx, |settings| {
10628 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
10629 Formatter::LanguageServer { name: None },
10630 ])))
10631 });
10632
10633 let fs = FakeFs::new(cx.executor());
10634 fs.insert_file(path!("/file.ts"), Default::default()).await;
10635
10636 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
10637
10638 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10639 language_registry.add(Arc::new(Language::new(
10640 LanguageConfig {
10641 name: "TypeScript".into(),
10642 matcher: LanguageMatcher {
10643 path_suffixes: vec!["ts".to_string()],
10644 ..Default::default()
10645 },
10646 ..LanguageConfig::default()
10647 },
10648 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10649 )));
10650 update_test_language_settings(cx, |settings| {
10651 settings.defaults.prettier = Some(PrettierSettings {
10652 allowed: true,
10653 ..PrettierSettings::default()
10654 });
10655 });
10656 let mut fake_servers = language_registry.register_fake_lsp(
10657 "TypeScript",
10658 FakeLspAdapter {
10659 capabilities: lsp::ServerCapabilities {
10660 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
10661 ..Default::default()
10662 },
10663 ..Default::default()
10664 },
10665 );
10666
10667 let buffer = project
10668 .update(cx, |project, cx| {
10669 project.open_local_buffer(path!("/file.ts"), cx)
10670 })
10671 .await
10672 .unwrap();
10673
10674 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10675 let (editor, cx) = cx.add_window_view(|window, cx| {
10676 build_editor_with_project(project.clone(), buffer, window, cx)
10677 });
10678 editor.update_in(cx, |editor, window, cx| {
10679 editor.set_text(
10680 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10681 window,
10682 cx,
10683 )
10684 });
10685
10686 cx.executor().start_waiting();
10687 let fake_server = fake_servers.next().await.unwrap();
10688
10689 let format = editor
10690 .update_in(cx, |editor, window, cx| {
10691 editor.perform_code_action_kind(
10692 project.clone(),
10693 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10694 window,
10695 cx,
10696 )
10697 })
10698 .unwrap();
10699 fake_server
10700 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
10701 assert_eq!(
10702 params.text_document.uri,
10703 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10704 );
10705 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
10706 lsp::CodeAction {
10707 title: "Organize Imports".to_string(),
10708 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
10709 edit: Some(lsp::WorkspaceEdit {
10710 changes: Some(
10711 [(
10712 params.text_document.uri.clone(),
10713 vec![lsp::TextEdit::new(
10714 lsp::Range::new(
10715 lsp::Position::new(1, 0),
10716 lsp::Position::new(2, 0),
10717 ),
10718 "".to_string(),
10719 )],
10720 )]
10721 .into_iter()
10722 .collect(),
10723 ),
10724 ..Default::default()
10725 }),
10726 ..Default::default()
10727 },
10728 )]))
10729 })
10730 .next()
10731 .await;
10732 cx.executor().start_waiting();
10733 format.await;
10734 assert_eq!(
10735 editor.update(cx, |editor, cx| editor.text(cx)),
10736 "import { a } from 'module';\n\nconst x = a;\n"
10737 );
10738
10739 editor.update_in(cx, |editor, window, cx| {
10740 editor.set_text(
10741 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
10742 window,
10743 cx,
10744 )
10745 });
10746 // Ensure we don't lock if code action hangs.
10747 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
10748 move |params, _| async move {
10749 assert_eq!(
10750 params.text_document.uri,
10751 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
10752 );
10753 futures::future::pending::<()>().await;
10754 unreachable!()
10755 },
10756 );
10757 let format = editor
10758 .update_in(cx, |editor, window, cx| {
10759 editor.perform_code_action_kind(
10760 project,
10761 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
10762 window,
10763 cx,
10764 )
10765 })
10766 .unwrap();
10767 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
10768 cx.executor().start_waiting();
10769 format.await;
10770 assert_eq!(
10771 editor.update(cx, |editor, cx| editor.text(cx)),
10772 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
10773 );
10774}
10775
10776#[gpui::test]
10777async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
10778 init_test(cx, |_| {});
10779
10780 let mut cx = EditorLspTestContext::new_rust(
10781 lsp::ServerCapabilities {
10782 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10783 ..Default::default()
10784 },
10785 cx,
10786 )
10787 .await;
10788
10789 cx.set_state(indoc! {"
10790 one.twoˇ
10791 "});
10792
10793 // The format request takes a long time. When it completes, it inserts
10794 // a newline and an indent before the `.`
10795 cx.lsp
10796 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
10797 let executor = cx.background_executor().clone();
10798 async move {
10799 executor.timer(Duration::from_millis(100)).await;
10800 Ok(Some(vec![lsp::TextEdit {
10801 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
10802 new_text: "\n ".into(),
10803 }]))
10804 }
10805 });
10806
10807 // Submit a format request.
10808 let format_1 = cx
10809 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10810 .unwrap();
10811 cx.executor().run_until_parked();
10812
10813 // Submit a second format request.
10814 let format_2 = cx
10815 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10816 .unwrap();
10817 cx.executor().run_until_parked();
10818
10819 // Wait for both format requests to complete
10820 cx.executor().advance_clock(Duration::from_millis(200));
10821 cx.executor().start_waiting();
10822 format_1.await.unwrap();
10823 cx.executor().start_waiting();
10824 format_2.await.unwrap();
10825
10826 // The formatting edits only happens once.
10827 cx.assert_editor_state(indoc! {"
10828 one
10829 .twoˇ
10830 "});
10831}
10832
10833#[gpui::test]
10834async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
10835 init_test(cx, |settings| {
10836 settings.defaults.formatter = Some(SelectedFormatter::Auto)
10837 });
10838
10839 let mut cx = EditorLspTestContext::new_rust(
10840 lsp::ServerCapabilities {
10841 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10842 ..Default::default()
10843 },
10844 cx,
10845 )
10846 .await;
10847
10848 // Set up a buffer white some trailing whitespace and no trailing newline.
10849 cx.set_state(
10850 &[
10851 "one ", //
10852 "twoˇ", //
10853 "three ", //
10854 "four", //
10855 ]
10856 .join("\n"),
10857 );
10858
10859 // Submit a format request.
10860 let format = cx
10861 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
10862 .unwrap();
10863
10864 // Record which buffer changes have been sent to the language server
10865 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
10866 cx.lsp
10867 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
10868 let buffer_changes = buffer_changes.clone();
10869 move |params, _| {
10870 buffer_changes.lock().extend(
10871 params
10872 .content_changes
10873 .into_iter()
10874 .map(|e| (e.range.unwrap(), e.text)),
10875 );
10876 }
10877 });
10878
10879 // Handle formatting requests to the language server.
10880 cx.lsp
10881 .set_request_handler::<lsp::request::Formatting, _, _>({
10882 let buffer_changes = buffer_changes.clone();
10883 move |_, _| {
10884 // When formatting is requested, trailing whitespace has already been stripped,
10885 // and the trailing newline has already been added.
10886 assert_eq!(
10887 &buffer_changes.lock()[1..],
10888 &[
10889 (
10890 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
10891 "".into()
10892 ),
10893 (
10894 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
10895 "".into()
10896 ),
10897 (
10898 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
10899 "\n".into()
10900 ),
10901 ]
10902 );
10903
10904 // Insert blank lines between each line of the buffer.
10905 async move {
10906 Ok(Some(vec![
10907 lsp::TextEdit {
10908 range: lsp::Range::new(
10909 lsp::Position::new(1, 0),
10910 lsp::Position::new(1, 0),
10911 ),
10912 new_text: "\n".into(),
10913 },
10914 lsp::TextEdit {
10915 range: lsp::Range::new(
10916 lsp::Position::new(2, 0),
10917 lsp::Position::new(2, 0),
10918 ),
10919 new_text: "\n".into(),
10920 },
10921 ]))
10922 }
10923 }
10924 });
10925
10926 // After formatting the buffer, the trailing whitespace is stripped,
10927 // a newline is appended, and the edits provided by the language server
10928 // have been applied.
10929 format.await.unwrap();
10930 cx.assert_editor_state(
10931 &[
10932 "one", //
10933 "", //
10934 "twoˇ", //
10935 "", //
10936 "three", //
10937 "four", //
10938 "", //
10939 ]
10940 .join("\n"),
10941 );
10942
10943 // Undoing the formatting undoes the trailing whitespace removal, the
10944 // trailing newline, and the LSP edits.
10945 cx.update_buffer(|buffer, cx| buffer.undo(cx));
10946 cx.assert_editor_state(
10947 &[
10948 "one ", //
10949 "twoˇ", //
10950 "three ", //
10951 "four", //
10952 ]
10953 .join("\n"),
10954 );
10955}
10956
10957#[gpui::test]
10958async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
10959 cx: &mut TestAppContext,
10960) {
10961 init_test(cx, |_| {});
10962
10963 cx.update(|cx| {
10964 cx.update_global::<SettingsStore, _>(|settings, cx| {
10965 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10966 settings.auto_signature_help = Some(true);
10967 });
10968 });
10969 });
10970
10971 let mut cx = EditorLspTestContext::new_rust(
10972 lsp::ServerCapabilities {
10973 signature_help_provider: Some(lsp::SignatureHelpOptions {
10974 ..Default::default()
10975 }),
10976 ..Default::default()
10977 },
10978 cx,
10979 )
10980 .await;
10981
10982 let language = Language::new(
10983 LanguageConfig {
10984 name: "Rust".into(),
10985 brackets: BracketPairConfig {
10986 pairs: vec![
10987 BracketPair {
10988 start: "{".to_string(),
10989 end: "}".to_string(),
10990 close: true,
10991 surround: true,
10992 newline: true,
10993 },
10994 BracketPair {
10995 start: "(".to_string(),
10996 end: ")".to_string(),
10997 close: true,
10998 surround: true,
10999 newline: true,
11000 },
11001 BracketPair {
11002 start: "/*".to_string(),
11003 end: " */".to_string(),
11004 close: true,
11005 surround: true,
11006 newline: true,
11007 },
11008 BracketPair {
11009 start: "[".to_string(),
11010 end: "]".to_string(),
11011 close: false,
11012 surround: false,
11013 newline: true,
11014 },
11015 BracketPair {
11016 start: "\"".to_string(),
11017 end: "\"".to_string(),
11018 close: true,
11019 surround: true,
11020 newline: false,
11021 },
11022 BracketPair {
11023 start: "<".to_string(),
11024 end: ">".to_string(),
11025 close: false,
11026 surround: true,
11027 newline: true,
11028 },
11029 ],
11030 ..Default::default()
11031 },
11032 autoclose_before: "})]".to_string(),
11033 ..Default::default()
11034 },
11035 Some(tree_sitter_rust::LANGUAGE.into()),
11036 );
11037 let language = Arc::new(language);
11038
11039 cx.language_registry().add(language.clone());
11040 cx.update_buffer(|buffer, cx| {
11041 buffer.set_language(Some(language), cx);
11042 });
11043
11044 cx.set_state(
11045 &r#"
11046 fn main() {
11047 sampleˇ
11048 }
11049 "#
11050 .unindent(),
11051 );
11052
11053 cx.update_editor(|editor, window, cx| {
11054 editor.handle_input("(", window, cx);
11055 });
11056 cx.assert_editor_state(
11057 &"
11058 fn main() {
11059 sample(ˇ)
11060 }
11061 "
11062 .unindent(),
11063 );
11064
11065 let mocked_response = lsp::SignatureHelp {
11066 signatures: vec![lsp::SignatureInformation {
11067 label: "fn sample(param1: u8, param2: u8)".to_string(),
11068 documentation: None,
11069 parameters: Some(vec![
11070 lsp::ParameterInformation {
11071 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11072 documentation: None,
11073 },
11074 lsp::ParameterInformation {
11075 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11076 documentation: None,
11077 },
11078 ]),
11079 active_parameter: None,
11080 }],
11081 active_signature: Some(0),
11082 active_parameter: Some(0),
11083 };
11084 handle_signature_help_request(&mut cx, mocked_response).await;
11085
11086 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11087 .await;
11088
11089 cx.editor(|editor, _, _| {
11090 let signature_help_state = editor.signature_help_state.popover().cloned();
11091 let signature = signature_help_state.unwrap();
11092 assert_eq!(
11093 signature.signatures[signature.current_signature].label,
11094 "fn sample(param1: u8, param2: u8)"
11095 );
11096 });
11097}
11098
11099#[gpui::test]
11100async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
11101 init_test(cx, |_| {});
11102
11103 cx.update(|cx| {
11104 cx.update_global::<SettingsStore, _>(|settings, cx| {
11105 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11106 settings.auto_signature_help = Some(false);
11107 settings.show_signature_help_after_edits = Some(false);
11108 });
11109 });
11110 });
11111
11112 let mut cx = EditorLspTestContext::new_rust(
11113 lsp::ServerCapabilities {
11114 signature_help_provider: Some(lsp::SignatureHelpOptions {
11115 ..Default::default()
11116 }),
11117 ..Default::default()
11118 },
11119 cx,
11120 )
11121 .await;
11122
11123 let language = Language::new(
11124 LanguageConfig {
11125 name: "Rust".into(),
11126 brackets: BracketPairConfig {
11127 pairs: vec![
11128 BracketPair {
11129 start: "{".to_string(),
11130 end: "}".to_string(),
11131 close: true,
11132 surround: true,
11133 newline: true,
11134 },
11135 BracketPair {
11136 start: "(".to_string(),
11137 end: ")".to_string(),
11138 close: true,
11139 surround: true,
11140 newline: true,
11141 },
11142 BracketPair {
11143 start: "/*".to_string(),
11144 end: " */".to_string(),
11145 close: true,
11146 surround: true,
11147 newline: true,
11148 },
11149 BracketPair {
11150 start: "[".to_string(),
11151 end: "]".to_string(),
11152 close: false,
11153 surround: false,
11154 newline: true,
11155 },
11156 BracketPair {
11157 start: "\"".to_string(),
11158 end: "\"".to_string(),
11159 close: true,
11160 surround: true,
11161 newline: false,
11162 },
11163 BracketPair {
11164 start: "<".to_string(),
11165 end: ">".to_string(),
11166 close: false,
11167 surround: true,
11168 newline: true,
11169 },
11170 ],
11171 ..Default::default()
11172 },
11173 autoclose_before: "})]".to_string(),
11174 ..Default::default()
11175 },
11176 Some(tree_sitter_rust::LANGUAGE.into()),
11177 );
11178 let language = Arc::new(language);
11179
11180 cx.language_registry().add(language.clone());
11181 cx.update_buffer(|buffer, cx| {
11182 buffer.set_language(Some(language), cx);
11183 });
11184
11185 // Ensure that signature_help is not called when no signature help is enabled.
11186 cx.set_state(
11187 &r#"
11188 fn main() {
11189 sampleˇ
11190 }
11191 "#
11192 .unindent(),
11193 );
11194 cx.update_editor(|editor, window, cx| {
11195 editor.handle_input("(", window, cx);
11196 });
11197 cx.assert_editor_state(
11198 &"
11199 fn main() {
11200 sample(ˇ)
11201 }
11202 "
11203 .unindent(),
11204 );
11205 cx.editor(|editor, _, _| {
11206 assert!(editor.signature_help_state.task().is_none());
11207 });
11208
11209 let mocked_response = lsp::SignatureHelp {
11210 signatures: vec![lsp::SignatureInformation {
11211 label: "fn sample(param1: u8, param2: u8)".to_string(),
11212 documentation: None,
11213 parameters: Some(vec![
11214 lsp::ParameterInformation {
11215 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11216 documentation: None,
11217 },
11218 lsp::ParameterInformation {
11219 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11220 documentation: None,
11221 },
11222 ]),
11223 active_parameter: None,
11224 }],
11225 active_signature: Some(0),
11226 active_parameter: Some(0),
11227 };
11228
11229 // Ensure that signature_help is called when enabled afte edits
11230 cx.update(|_, cx| {
11231 cx.update_global::<SettingsStore, _>(|settings, cx| {
11232 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11233 settings.auto_signature_help = Some(false);
11234 settings.show_signature_help_after_edits = Some(true);
11235 });
11236 });
11237 });
11238 cx.set_state(
11239 &r#"
11240 fn main() {
11241 sampleˇ
11242 }
11243 "#
11244 .unindent(),
11245 );
11246 cx.update_editor(|editor, window, cx| {
11247 editor.handle_input("(", window, cx);
11248 });
11249 cx.assert_editor_state(
11250 &"
11251 fn main() {
11252 sample(ˇ)
11253 }
11254 "
11255 .unindent(),
11256 );
11257 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11258 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11259 .await;
11260 cx.update_editor(|editor, _, _| {
11261 let signature_help_state = editor.signature_help_state.popover().cloned();
11262 assert!(signature_help_state.is_some());
11263 let signature = signature_help_state.unwrap();
11264 assert_eq!(
11265 signature.signatures[signature.current_signature].label,
11266 "fn sample(param1: u8, param2: u8)"
11267 );
11268 editor.signature_help_state = SignatureHelpState::default();
11269 });
11270
11271 // Ensure that signature_help is called when auto signature help override is enabled
11272 cx.update(|_, cx| {
11273 cx.update_global::<SettingsStore, _>(|settings, cx| {
11274 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11275 settings.auto_signature_help = Some(true);
11276 settings.show_signature_help_after_edits = Some(false);
11277 });
11278 });
11279 });
11280 cx.set_state(
11281 &r#"
11282 fn main() {
11283 sampleˇ
11284 }
11285 "#
11286 .unindent(),
11287 );
11288 cx.update_editor(|editor, window, cx| {
11289 editor.handle_input("(", window, cx);
11290 });
11291 cx.assert_editor_state(
11292 &"
11293 fn main() {
11294 sample(ˇ)
11295 }
11296 "
11297 .unindent(),
11298 );
11299 handle_signature_help_request(&mut cx, mocked_response).await;
11300 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11301 .await;
11302 cx.editor(|editor, _, _| {
11303 let signature_help_state = editor.signature_help_state.popover().cloned();
11304 assert!(signature_help_state.is_some());
11305 let signature = signature_help_state.unwrap();
11306 assert_eq!(
11307 signature.signatures[signature.current_signature].label,
11308 "fn sample(param1: u8, param2: u8)"
11309 );
11310 });
11311}
11312
11313#[gpui::test]
11314async fn test_signature_help(cx: &mut TestAppContext) {
11315 init_test(cx, |_| {});
11316 cx.update(|cx| {
11317 cx.update_global::<SettingsStore, _>(|settings, cx| {
11318 settings.update_user_settings::<EditorSettings>(cx, |settings| {
11319 settings.auto_signature_help = Some(true);
11320 });
11321 });
11322 });
11323
11324 let mut cx = EditorLspTestContext::new_rust(
11325 lsp::ServerCapabilities {
11326 signature_help_provider: Some(lsp::SignatureHelpOptions {
11327 ..Default::default()
11328 }),
11329 ..Default::default()
11330 },
11331 cx,
11332 )
11333 .await;
11334
11335 // A test that directly calls `show_signature_help`
11336 cx.update_editor(|editor, window, cx| {
11337 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11338 });
11339
11340 let mocked_response = lsp::SignatureHelp {
11341 signatures: vec![lsp::SignatureInformation {
11342 label: "fn sample(param1: u8, param2: u8)".to_string(),
11343 documentation: None,
11344 parameters: Some(vec![
11345 lsp::ParameterInformation {
11346 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11347 documentation: None,
11348 },
11349 lsp::ParameterInformation {
11350 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11351 documentation: None,
11352 },
11353 ]),
11354 active_parameter: None,
11355 }],
11356 active_signature: Some(0),
11357 active_parameter: Some(0),
11358 };
11359 handle_signature_help_request(&mut cx, mocked_response).await;
11360
11361 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11362 .await;
11363
11364 cx.editor(|editor, _, _| {
11365 let signature_help_state = editor.signature_help_state.popover().cloned();
11366 assert!(signature_help_state.is_some());
11367 let signature = signature_help_state.unwrap();
11368 assert_eq!(
11369 signature.signatures[signature.current_signature].label,
11370 "fn sample(param1: u8, param2: u8)"
11371 );
11372 });
11373
11374 // When exiting outside from inside the brackets, `signature_help` is closed.
11375 cx.set_state(indoc! {"
11376 fn main() {
11377 sample(ˇ);
11378 }
11379
11380 fn sample(param1: u8, param2: u8) {}
11381 "});
11382
11383 cx.update_editor(|editor, window, cx| {
11384 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11385 s.select_ranges([0..0])
11386 });
11387 });
11388
11389 let mocked_response = lsp::SignatureHelp {
11390 signatures: Vec::new(),
11391 active_signature: None,
11392 active_parameter: None,
11393 };
11394 handle_signature_help_request(&mut cx, mocked_response).await;
11395
11396 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11397 .await;
11398
11399 cx.editor(|editor, _, _| {
11400 assert!(!editor.signature_help_state.is_shown());
11401 });
11402
11403 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
11404 cx.set_state(indoc! {"
11405 fn main() {
11406 sample(ˇ);
11407 }
11408
11409 fn sample(param1: u8, param2: u8) {}
11410 "});
11411
11412 let mocked_response = lsp::SignatureHelp {
11413 signatures: vec![lsp::SignatureInformation {
11414 label: "fn sample(param1: u8, param2: u8)".to_string(),
11415 documentation: None,
11416 parameters: Some(vec![
11417 lsp::ParameterInformation {
11418 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11419 documentation: None,
11420 },
11421 lsp::ParameterInformation {
11422 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11423 documentation: None,
11424 },
11425 ]),
11426 active_parameter: None,
11427 }],
11428 active_signature: Some(0),
11429 active_parameter: Some(0),
11430 };
11431 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11432 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11433 .await;
11434 cx.editor(|editor, _, _| {
11435 assert!(editor.signature_help_state.is_shown());
11436 });
11437
11438 // Restore the popover with more parameter input
11439 cx.set_state(indoc! {"
11440 fn main() {
11441 sample(param1, param2ˇ);
11442 }
11443
11444 fn sample(param1: u8, param2: u8) {}
11445 "});
11446
11447 let mocked_response = lsp::SignatureHelp {
11448 signatures: vec![lsp::SignatureInformation {
11449 label: "fn sample(param1: u8, param2: u8)".to_string(),
11450 documentation: None,
11451 parameters: Some(vec![
11452 lsp::ParameterInformation {
11453 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11454 documentation: None,
11455 },
11456 lsp::ParameterInformation {
11457 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11458 documentation: None,
11459 },
11460 ]),
11461 active_parameter: None,
11462 }],
11463 active_signature: Some(0),
11464 active_parameter: Some(1),
11465 };
11466 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11467 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11468 .await;
11469
11470 // When selecting a range, the popover is gone.
11471 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
11472 cx.update_editor(|editor, window, cx| {
11473 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11474 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11475 })
11476 });
11477 cx.assert_editor_state(indoc! {"
11478 fn main() {
11479 sample(param1, «ˇparam2»);
11480 }
11481
11482 fn sample(param1: u8, param2: u8) {}
11483 "});
11484 cx.editor(|editor, _, _| {
11485 assert!(!editor.signature_help_state.is_shown());
11486 });
11487
11488 // When unselecting again, the popover is back if within the brackets.
11489 cx.update_editor(|editor, window, cx| {
11490 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11491 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11492 })
11493 });
11494 cx.assert_editor_state(indoc! {"
11495 fn main() {
11496 sample(param1, ˇparam2);
11497 }
11498
11499 fn sample(param1: u8, param2: u8) {}
11500 "});
11501 handle_signature_help_request(&mut cx, mocked_response).await;
11502 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11503 .await;
11504 cx.editor(|editor, _, _| {
11505 assert!(editor.signature_help_state.is_shown());
11506 });
11507
11508 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
11509 cx.update_editor(|editor, window, cx| {
11510 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11511 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
11512 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11513 })
11514 });
11515 cx.assert_editor_state(indoc! {"
11516 fn main() {
11517 sample(param1, ˇparam2);
11518 }
11519
11520 fn sample(param1: u8, param2: u8) {}
11521 "});
11522
11523 let mocked_response = lsp::SignatureHelp {
11524 signatures: vec![lsp::SignatureInformation {
11525 label: "fn sample(param1: u8, param2: u8)".to_string(),
11526 documentation: None,
11527 parameters: Some(vec![
11528 lsp::ParameterInformation {
11529 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
11530 documentation: None,
11531 },
11532 lsp::ParameterInformation {
11533 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
11534 documentation: None,
11535 },
11536 ]),
11537 active_parameter: None,
11538 }],
11539 active_signature: Some(0),
11540 active_parameter: Some(1),
11541 };
11542 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
11543 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11544 .await;
11545 cx.update_editor(|editor, _, cx| {
11546 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
11547 });
11548 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
11549 .await;
11550 cx.update_editor(|editor, window, cx| {
11551 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11552 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
11553 })
11554 });
11555 cx.assert_editor_state(indoc! {"
11556 fn main() {
11557 sample(param1, «ˇparam2»);
11558 }
11559
11560 fn sample(param1: u8, param2: u8) {}
11561 "});
11562 cx.update_editor(|editor, window, cx| {
11563 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11564 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
11565 })
11566 });
11567 cx.assert_editor_state(indoc! {"
11568 fn main() {
11569 sample(param1, ˇparam2);
11570 }
11571
11572 fn sample(param1: u8, param2: u8) {}
11573 "});
11574 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
11575 .await;
11576}
11577
11578#[gpui::test]
11579async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
11580 init_test(cx, |_| {});
11581
11582 let mut cx = EditorLspTestContext::new_rust(
11583 lsp::ServerCapabilities {
11584 signature_help_provider: Some(lsp::SignatureHelpOptions {
11585 ..Default::default()
11586 }),
11587 ..Default::default()
11588 },
11589 cx,
11590 )
11591 .await;
11592
11593 cx.set_state(indoc! {"
11594 fn main() {
11595 overloadedˇ
11596 }
11597 "});
11598
11599 cx.update_editor(|editor, window, cx| {
11600 editor.handle_input("(", window, cx);
11601 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11602 });
11603
11604 // Mock response with 3 signatures
11605 let mocked_response = lsp::SignatureHelp {
11606 signatures: vec![
11607 lsp::SignatureInformation {
11608 label: "fn overloaded(x: i32)".to_string(),
11609 documentation: None,
11610 parameters: Some(vec![lsp::ParameterInformation {
11611 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11612 documentation: None,
11613 }]),
11614 active_parameter: None,
11615 },
11616 lsp::SignatureInformation {
11617 label: "fn overloaded(x: i32, y: i32)".to_string(),
11618 documentation: None,
11619 parameters: Some(vec![
11620 lsp::ParameterInformation {
11621 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11622 documentation: None,
11623 },
11624 lsp::ParameterInformation {
11625 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11626 documentation: None,
11627 },
11628 ]),
11629 active_parameter: None,
11630 },
11631 lsp::SignatureInformation {
11632 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
11633 documentation: None,
11634 parameters: Some(vec![
11635 lsp::ParameterInformation {
11636 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
11637 documentation: None,
11638 },
11639 lsp::ParameterInformation {
11640 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
11641 documentation: None,
11642 },
11643 lsp::ParameterInformation {
11644 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
11645 documentation: None,
11646 },
11647 ]),
11648 active_parameter: None,
11649 },
11650 ],
11651 active_signature: Some(1),
11652 active_parameter: Some(0),
11653 };
11654 handle_signature_help_request(&mut cx, mocked_response).await;
11655
11656 cx.condition(|editor, _| editor.signature_help_state.is_shown())
11657 .await;
11658
11659 // Verify we have multiple signatures and the right one is selected
11660 cx.editor(|editor, _, _| {
11661 let popover = editor.signature_help_state.popover().cloned().unwrap();
11662 assert_eq!(popover.signatures.len(), 3);
11663 // active_signature was 1, so that should be the current
11664 assert_eq!(popover.current_signature, 1);
11665 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
11666 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
11667 assert_eq!(
11668 popover.signatures[2].label,
11669 "fn overloaded(x: i32, y: i32, z: i32)"
11670 );
11671 });
11672
11673 // Test navigation functionality
11674 cx.update_editor(|editor, window, cx| {
11675 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11676 });
11677
11678 cx.editor(|editor, _, _| {
11679 let popover = editor.signature_help_state.popover().cloned().unwrap();
11680 assert_eq!(popover.current_signature, 2);
11681 });
11682
11683 // Test wrap around
11684 cx.update_editor(|editor, window, cx| {
11685 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
11686 });
11687
11688 cx.editor(|editor, _, _| {
11689 let popover = editor.signature_help_state.popover().cloned().unwrap();
11690 assert_eq!(popover.current_signature, 0);
11691 });
11692
11693 // Test previous navigation
11694 cx.update_editor(|editor, window, cx| {
11695 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
11696 });
11697
11698 cx.editor(|editor, _, _| {
11699 let popover = editor.signature_help_state.popover().cloned().unwrap();
11700 assert_eq!(popover.current_signature, 2);
11701 });
11702}
11703
11704#[gpui::test]
11705async fn test_completion_mode(cx: &mut TestAppContext) {
11706 init_test(cx, |_| {});
11707 let mut cx = EditorLspTestContext::new_rust(
11708 lsp::ServerCapabilities {
11709 completion_provider: Some(lsp::CompletionOptions {
11710 resolve_provider: Some(true),
11711 ..Default::default()
11712 }),
11713 ..Default::default()
11714 },
11715 cx,
11716 )
11717 .await;
11718
11719 struct Run {
11720 run_description: &'static str,
11721 initial_state: String,
11722 buffer_marked_text: String,
11723 completion_label: &'static str,
11724 completion_text: &'static str,
11725 expected_with_insert_mode: String,
11726 expected_with_replace_mode: String,
11727 expected_with_replace_subsequence_mode: String,
11728 expected_with_replace_suffix_mode: String,
11729 }
11730
11731 let runs = [
11732 Run {
11733 run_description: "Start of word matches completion text",
11734 initial_state: "before ediˇ after".into(),
11735 buffer_marked_text: "before <edi|> after".into(),
11736 completion_label: "editor",
11737 completion_text: "editor",
11738 expected_with_insert_mode: "before editorˇ after".into(),
11739 expected_with_replace_mode: "before editorˇ after".into(),
11740 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11741 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11742 },
11743 Run {
11744 run_description: "Accept same text at the middle of the word",
11745 initial_state: "before ediˇtor after".into(),
11746 buffer_marked_text: "before <edi|tor> after".into(),
11747 completion_label: "editor",
11748 completion_text: "editor",
11749 expected_with_insert_mode: "before editorˇtor after".into(),
11750 expected_with_replace_mode: "before editorˇ after".into(),
11751 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11752 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11753 },
11754 Run {
11755 run_description: "End of word matches completion text -- cursor at end",
11756 initial_state: "before torˇ after".into(),
11757 buffer_marked_text: "before <tor|> after".into(),
11758 completion_label: "editor",
11759 completion_text: "editor",
11760 expected_with_insert_mode: "before editorˇ after".into(),
11761 expected_with_replace_mode: "before editorˇ after".into(),
11762 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11763 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11764 },
11765 Run {
11766 run_description: "End of word matches completion text -- cursor at start",
11767 initial_state: "before ˇtor after".into(),
11768 buffer_marked_text: "before <|tor> after".into(),
11769 completion_label: "editor",
11770 completion_text: "editor",
11771 expected_with_insert_mode: "before editorˇtor after".into(),
11772 expected_with_replace_mode: "before editorˇ after".into(),
11773 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11774 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11775 },
11776 Run {
11777 run_description: "Prepend text containing whitespace",
11778 initial_state: "pˇfield: bool".into(),
11779 buffer_marked_text: "<p|field>: bool".into(),
11780 completion_label: "pub ",
11781 completion_text: "pub ",
11782 expected_with_insert_mode: "pub ˇfield: bool".into(),
11783 expected_with_replace_mode: "pub ˇ: bool".into(),
11784 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
11785 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
11786 },
11787 Run {
11788 run_description: "Add element to start of list",
11789 initial_state: "[element_ˇelement_2]".into(),
11790 buffer_marked_text: "[<element_|element_2>]".into(),
11791 completion_label: "element_1",
11792 completion_text: "element_1",
11793 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
11794 expected_with_replace_mode: "[element_1ˇ]".into(),
11795 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
11796 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
11797 },
11798 Run {
11799 run_description: "Add element to start of list -- first and second elements are equal",
11800 initial_state: "[elˇelement]".into(),
11801 buffer_marked_text: "[<el|element>]".into(),
11802 completion_label: "element",
11803 completion_text: "element",
11804 expected_with_insert_mode: "[elementˇelement]".into(),
11805 expected_with_replace_mode: "[elementˇ]".into(),
11806 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
11807 expected_with_replace_suffix_mode: "[elementˇ]".into(),
11808 },
11809 Run {
11810 run_description: "Ends with matching suffix",
11811 initial_state: "SubˇError".into(),
11812 buffer_marked_text: "<Sub|Error>".into(),
11813 completion_label: "SubscriptionError",
11814 completion_text: "SubscriptionError",
11815 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
11816 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11817 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11818 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
11819 },
11820 Run {
11821 run_description: "Suffix is a subsequence -- contiguous",
11822 initial_state: "SubˇErr".into(),
11823 buffer_marked_text: "<Sub|Err>".into(),
11824 completion_label: "SubscriptionError",
11825 completion_text: "SubscriptionError",
11826 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
11827 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11828 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11829 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
11830 },
11831 Run {
11832 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
11833 initial_state: "Suˇscrirr".into(),
11834 buffer_marked_text: "<Su|scrirr>".into(),
11835 completion_label: "SubscriptionError",
11836 completion_text: "SubscriptionError",
11837 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
11838 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
11839 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
11840 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
11841 },
11842 Run {
11843 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
11844 initial_state: "foo(indˇix)".into(),
11845 buffer_marked_text: "foo(<ind|ix>)".into(),
11846 completion_label: "node_index",
11847 completion_text: "node_index",
11848 expected_with_insert_mode: "foo(node_indexˇix)".into(),
11849 expected_with_replace_mode: "foo(node_indexˇ)".into(),
11850 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
11851 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
11852 },
11853 Run {
11854 run_description: "Replace range ends before cursor - should extend to cursor",
11855 initial_state: "before editˇo after".into(),
11856 buffer_marked_text: "before <{ed}>it|o after".into(),
11857 completion_label: "editor",
11858 completion_text: "editor",
11859 expected_with_insert_mode: "before editorˇo after".into(),
11860 expected_with_replace_mode: "before editorˇo after".into(),
11861 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
11862 expected_with_replace_suffix_mode: "before editorˇo after".into(),
11863 },
11864 Run {
11865 run_description: "Uses label for suffix matching",
11866 initial_state: "before ediˇtor after".into(),
11867 buffer_marked_text: "before <edi|tor> after".into(),
11868 completion_label: "editor",
11869 completion_text: "editor()",
11870 expected_with_insert_mode: "before editor()ˇtor after".into(),
11871 expected_with_replace_mode: "before editor()ˇ after".into(),
11872 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
11873 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
11874 },
11875 Run {
11876 run_description: "Case insensitive subsequence and suffix matching",
11877 initial_state: "before EDiˇtoR after".into(),
11878 buffer_marked_text: "before <EDi|toR> after".into(),
11879 completion_label: "editor",
11880 completion_text: "editor",
11881 expected_with_insert_mode: "before editorˇtoR after".into(),
11882 expected_with_replace_mode: "before editorˇ after".into(),
11883 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
11884 expected_with_replace_suffix_mode: "before editorˇ after".into(),
11885 },
11886 ];
11887
11888 for run in runs {
11889 let run_variations = [
11890 (LspInsertMode::Insert, run.expected_with_insert_mode),
11891 (LspInsertMode::Replace, run.expected_with_replace_mode),
11892 (
11893 LspInsertMode::ReplaceSubsequence,
11894 run.expected_with_replace_subsequence_mode,
11895 ),
11896 (
11897 LspInsertMode::ReplaceSuffix,
11898 run.expected_with_replace_suffix_mode,
11899 ),
11900 ];
11901
11902 for (lsp_insert_mode, expected_text) in run_variations {
11903 eprintln!(
11904 "run = {:?}, mode = {lsp_insert_mode:.?}",
11905 run.run_description,
11906 );
11907
11908 update_test_language_settings(&mut cx, |settings| {
11909 settings.defaults.completions = Some(CompletionSettings {
11910 lsp_insert_mode,
11911 words: WordsCompletionMode::Disabled,
11912 lsp: true,
11913 lsp_fetch_timeout_ms: 0,
11914 });
11915 });
11916
11917 cx.set_state(&run.initial_state);
11918 cx.update_editor(|editor, window, cx| {
11919 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11920 });
11921
11922 let counter = Arc::new(AtomicUsize::new(0));
11923 handle_completion_request_with_insert_and_replace(
11924 &mut cx,
11925 &run.buffer_marked_text,
11926 vec![(run.completion_label, run.completion_text)],
11927 counter.clone(),
11928 )
11929 .await;
11930 cx.condition(|editor, _| editor.context_menu_visible())
11931 .await;
11932 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11933
11934 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11935 editor
11936 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11937 .unwrap()
11938 });
11939 cx.assert_editor_state(&expected_text);
11940 handle_resolve_completion_request(&mut cx, None).await;
11941 apply_additional_edits.await.unwrap();
11942 }
11943 }
11944}
11945
11946#[gpui::test]
11947async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
11948 init_test(cx, |_| {});
11949 let mut cx = EditorLspTestContext::new_rust(
11950 lsp::ServerCapabilities {
11951 completion_provider: Some(lsp::CompletionOptions {
11952 resolve_provider: Some(true),
11953 ..Default::default()
11954 }),
11955 ..Default::default()
11956 },
11957 cx,
11958 )
11959 .await;
11960
11961 let initial_state = "SubˇError";
11962 let buffer_marked_text = "<Sub|Error>";
11963 let completion_text = "SubscriptionError";
11964 let expected_with_insert_mode = "SubscriptionErrorˇError";
11965 let expected_with_replace_mode = "SubscriptionErrorˇ";
11966
11967 update_test_language_settings(&mut cx, |settings| {
11968 settings.defaults.completions = Some(CompletionSettings {
11969 words: WordsCompletionMode::Disabled,
11970 // set the opposite here to ensure that the action is overriding the default behavior
11971 lsp_insert_mode: LspInsertMode::Insert,
11972 lsp: true,
11973 lsp_fetch_timeout_ms: 0,
11974 });
11975 });
11976
11977 cx.set_state(initial_state);
11978 cx.update_editor(|editor, window, cx| {
11979 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11980 });
11981
11982 let counter = Arc::new(AtomicUsize::new(0));
11983 handle_completion_request_with_insert_and_replace(
11984 &mut cx,
11985 &buffer_marked_text,
11986 vec![(completion_text, completion_text)],
11987 counter.clone(),
11988 )
11989 .await;
11990 cx.condition(|editor, _| editor.context_menu_visible())
11991 .await;
11992 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11993
11994 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11995 editor
11996 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11997 .unwrap()
11998 });
11999 cx.assert_editor_state(&expected_with_replace_mode);
12000 handle_resolve_completion_request(&mut cx, None).await;
12001 apply_additional_edits.await.unwrap();
12002
12003 update_test_language_settings(&mut cx, |settings| {
12004 settings.defaults.completions = Some(CompletionSettings {
12005 words: WordsCompletionMode::Disabled,
12006 // set the opposite here to ensure that the action is overriding the default behavior
12007 lsp_insert_mode: LspInsertMode::Replace,
12008 lsp: true,
12009 lsp_fetch_timeout_ms: 0,
12010 });
12011 });
12012
12013 cx.set_state(initial_state);
12014 cx.update_editor(|editor, window, cx| {
12015 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12016 });
12017 handle_completion_request_with_insert_and_replace(
12018 &mut cx,
12019 &buffer_marked_text,
12020 vec![(completion_text, completion_text)],
12021 counter.clone(),
12022 )
12023 .await;
12024 cx.condition(|editor, _| editor.context_menu_visible())
12025 .await;
12026 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12027
12028 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12029 editor
12030 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
12031 .unwrap()
12032 });
12033 cx.assert_editor_state(&expected_with_insert_mode);
12034 handle_resolve_completion_request(&mut cx, None).await;
12035 apply_additional_edits.await.unwrap();
12036}
12037
12038#[gpui::test]
12039async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
12040 init_test(cx, |_| {});
12041 let mut cx = EditorLspTestContext::new_rust(
12042 lsp::ServerCapabilities {
12043 completion_provider: Some(lsp::CompletionOptions {
12044 resolve_provider: Some(true),
12045 ..Default::default()
12046 }),
12047 ..Default::default()
12048 },
12049 cx,
12050 )
12051 .await;
12052
12053 // scenario: surrounding text matches completion text
12054 let completion_text = "to_offset";
12055 let initial_state = indoc! {"
12056 1. buf.to_offˇsuffix
12057 2. buf.to_offˇsuf
12058 3. buf.to_offˇfix
12059 4. buf.to_offˇ
12060 5. into_offˇensive
12061 6. ˇsuffix
12062 7. let ˇ //
12063 8. aaˇzz
12064 9. buf.to_off«zzzzzˇ»suffix
12065 10. buf.«ˇzzzzz»suffix
12066 11. to_off«ˇzzzzz»
12067
12068 buf.to_offˇsuffix // newest cursor
12069 "};
12070 let completion_marked_buffer = indoc! {"
12071 1. buf.to_offsuffix
12072 2. buf.to_offsuf
12073 3. buf.to_offfix
12074 4. buf.to_off
12075 5. into_offensive
12076 6. suffix
12077 7. let //
12078 8. aazz
12079 9. buf.to_offzzzzzsuffix
12080 10. buf.zzzzzsuffix
12081 11. to_offzzzzz
12082
12083 buf.<to_off|suffix> // newest cursor
12084 "};
12085 let expected = indoc! {"
12086 1. buf.to_offsetˇ
12087 2. buf.to_offsetˇsuf
12088 3. buf.to_offsetˇfix
12089 4. buf.to_offsetˇ
12090 5. into_offsetˇensive
12091 6. to_offsetˇsuffix
12092 7. let to_offsetˇ //
12093 8. aato_offsetˇzz
12094 9. buf.to_offsetˇ
12095 10. buf.to_offsetˇsuffix
12096 11. to_offsetˇ
12097
12098 buf.to_offsetˇ // newest cursor
12099 "};
12100 cx.set_state(initial_state);
12101 cx.update_editor(|editor, window, cx| {
12102 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12103 });
12104 handle_completion_request_with_insert_and_replace(
12105 &mut cx,
12106 completion_marked_buffer,
12107 vec![(completion_text, completion_text)],
12108 Arc::new(AtomicUsize::new(0)),
12109 )
12110 .await;
12111 cx.condition(|editor, _| editor.context_menu_visible())
12112 .await;
12113 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12114 editor
12115 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12116 .unwrap()
12117 });
12118 cx.assert_editor_state(expected);
12119 handle_resolve_completion_request(&mut cx, None).await;
12120 apply_additional_edits.await.unwrap();
12121
12122 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
12123 let completion_text = "foo_and_bar";
12124 let initial_state = indoc! {"
12125 1. ooanbˇ
12126 2. zooanbˇ
12127 3. ooanbˇz
12128 4. zooanbˇz
12129 5. ooanˇ
12130 6. oanbˇ
12131
12132 ooanbˇ
12133 "};
12134 let completion_marked_buffer = indoc! {"
12135 1. ooanb
12136 2. zooanb
12137 3. ooanbz
12138 4. zooanbz
12139 5. ooan
12140 6. oanb
12141
12142 <ooanb|>
12143 "};
12144 let expected = indoc! {"
12145 1. foo_and_barˇ
12146 2. zfoo_and_barˇ
12147 3. foo_and_barˇz
12148 4. zfoo_and_barˇz
12149 5. ooanfoo_and_barˇ
12150 6. oanbfoo_and_barˇ
12151
12152 foo_and_barˇ
12153 "};
12154 cx.set_state(initial_state);
12155 cx.update_editor(|editor, window, cx| {
12156 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12157 });
12158 handle_completion_request_with_insert_and_replace(
12159 &mut cx,
12160 completion_marked_buffer,
12161 vec![(completion_text, completion_text)],
12162 Arc::new(AtomicUsize::new(0)),
12163 )
12164 .await;
12165 cx.condition(|editor, _| editor.context_menu_visible())
12166 .await;
12167 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12168 editor
12169 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12170 .unwrap()
12171 });
12172 cx.assert_editor_state(expected);
12173 handle_resolve_completion_request(&mut cx, None).await;
12174 apply_additional_edits.await.unwrap();
12175
12176 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
12177 // (expects the same as if it was inserted at the end)
12178 let completion_text = "foo_and_bar";
12179 let initial_state = indoc! {"
12180 1. ooˇanb
12181 2. zooˇanb
12182 3. ooˇanbz
12183 4. zooˇanbz
12184
12185 ooˇanb
12186 "};
12187 let completion_marked_buffer = indoc! {"
12188 1. ooanb
12189 2. zooanb
12190 3. ooanbz
12191 4. zooanbz
12192
12193 <oo|anb>
12194 "};
12195 let expected = indoc! {"
12196 1. foo_and_barˇ
12197 2. zfoo_and_barˇ
12198 3. foo_and_barˇz
12199 4. zfoo_and_barˇz
12200
12201 foo_and_barˇ
12202 "};
12203 cx.set_state(initial_state);
12204 cx.update_editor(|editor, window, cx| {
12205 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12206 });
12207 handle_completion_request_with_insert_and_replace(
12208 &mut cx,
12209 completion_marked_buffer,
12210 vec![(completion_text, completion_text)],
12211 Arc::new(AtomicUsize::new(0)),
12212 )
12213 .await;
12214 cx.condition(|editor, _| editor.context_menu_visible())
12215 .await;
12216 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12217 editor
12218 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12219 .unwrap()
12220 });
12221 cx.assert_editor_state(expected);
12222 handle_resolve_completion_request(&mut cx, None).await;
12223 apply_additional_edits.await.unwrap();
12224}
12225
12226// This used to crash
12227#[gpui::test]
12228async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
12229 init_test(cx, |_| {});
12230
12231 let buffer_text = indoc! {"
12232 fn main() {
12233 10.satu;
12234
12235 //
12236 // separate cursors so they open in different excerpts (manually reproducible)
12237 //
12238
12239 10.satu20;
12240 }
12241 "};
12242 let multibuffer_text_with_selections = indoc! {"
12243 fn main() {
12244 10.satuˇ;
12245
12246 //
12247
12248 //
12249
12250 10.satuˇ20;
12251 }
12252 "};
12253 let expected_multibuffer = indoc! {"
12254 fn main() {
12255 10.saturating_sub()ˇ;
12256
12257 //
12258
12259 //
12260
12261 10.saturating_sub()ˇ;
12262 }
12263 "};
12264
12265 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
12266 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
12267
12268 let fs = FakeFs::new(cx.executor());
12269 fs.insert_tree(
12270 path!("/a"),
12271 json!({
12272 "main.rs": buffer_text,
12273 }),
12274 )
12275 .await;
12276
12277 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12278 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12279 language_registry.add(rust_lang());
12280 let mut fake_servers = language_registry.register_fake_lsp(
12281 "Rust",
12282 FakeLspAdapter {
12283 capabilities: lsp::ServerCapabilities {
12284 completion_provider: Some(lsp::CompletionOptions {
12285 resolve_provider: None,
12286 ..lsp::CompletionOptions::default()
12287 }),
12288 ..lsp::ServerCapabilities::default()
12289 },
12290 ..FakeLspAdapter::default()
12291 },
12292 );
12293 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12294 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12295 let buffer = project
12296 .update(cx, |project, cx| {
12297 project.open_local_buffer(path!("/a/main.rs"), cx)
12298 })
12299 .await
12300 .unwrap();
12301
12302 let multi_buffer = cx.new(|cx| {
12303 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
12304 multi_buffer.push_excerpts(
12305 buffer.clone(),
12306 [ExcerptRange::new(0..first_excerpt_end)],
12307 cx,
12308 );
12309 multi_buffer.push_excerpts(
12310 buffer.clone(),
12311 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
12312 cx,
12313 );
12314 multi_buffer
12315 });
12316
12317 let editor = workspace
12318 .update(cx, |_, window, cx| {
12319 cx.new(|cx| {
12320 Editor::new(
12321 EditorMode::Full {
12322 scale_ui_elements_with_buffer_font_size: false,
12323 show_active_line_background: false,
12324 sized_by_content: false,
12325 },
12326 multi_buffer.clone(),
12327 Some(project.clone()),
12328 window,
12329 cx,
12330 )
12331 })
12332 })
12333 .unwrap();
12334
12335 let pane = workspace
12336 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12337 .unwrap();
12338 pane.update_in(cx, |pane, window, cx| {
12339 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
12340 });
12341
12342 let fake_server = fake_servers.next().await.unwrap();
12343
12344 editor.update_in(cx, |editor, window, cx| {
12345 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12346 s.select_ranges([
12347 Point::new(1, 11)..Point::new(1, 11),
12348 Point::new(7, 11)..Point::new(7, 11),
12349 ])
12350 });
12351
12352 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
12353 });
12354
12355 editor.update_in(cx, |editor, window, cx| {
12356 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12357 });
12358
12359 fake_server
12360 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
12361 let completion_item = lsp::CompletionItem {
12362 label: "saturating_sub()".into(),
12363 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
12364 lsp::InsertReplaceEdit {
12365 new_text: "saturating_sub()".to_owned(),
12366 insert: lsp::Range::new(
12367 lsp::Position::new(7, 7),
12368 lsp::Position::new(7, 11),
12369 ),
12370 replace: lsp::Range::new(
12371 lsp::Position::new(7, 7),
12372 lsp::Position::new(7, 13),
12373 ),
12374 },
12375 )),
12376 ..lsp::CompletionItem::default()
12377 };
12378
12379 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
12380 })
12381 .next()
12382 .await
12383 .unwrap();
12384
12385 cx.condition(&editor, |editor, _| editor.context_menu_visible())
12386 .await;
12387
12388 editor
12389 .update_in(cx, |editor, window, cx| {
12390 editor
12391 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
12392 .unwrap()
12393 })
12394 .await
12395 .unwrap();
12396
12397 editor.update(cx, |editor, cx| {
12398 assert_text_with_selections(editor, expected_multibuffer, cx);
12399 })
12400}
12401
12402#[gpui::test]
12403async fn test_completion(cx: &mut TestAppContext) {
12404 init_test(cx, |_| {});
12405
12406 let mut cx = EditorLspTestContext::new_rust(
12407 lsp::ServerCapabilities {
12408 completion_provider: Some(lsp::CompletionOptions {
12409 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12410 resolve_provider: Some(true),
12411 ..Default::default()
12412 }),
12413 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12414 ..Default::default()
12415 },
12416 cx,
12417 )
12418 .await;
12419 let counter = Arc::new(AtomicUsize::new(0));
12420
12421 cx.set_state(indoc! {"
12422 oneˇ
12423 two
12424 three
12425 "});
12426 cx.simulate_keystroke(".");
12427 handle_completion_request(
12428 indoc! {"
12429 one.|<>
12430 two
12431 three
12432 "},
12433 vec!["first_completion", "second_completion"],
12434 true,
12435 counter.clone(),
12436 &mut cx,
12437 )
12438 .await;
12439 cx.condition(|editor, _| editor.context_menu_visible())
12440 .await;
12441 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12442
12443 let _handler = handle_signature_help_request(
12444 &mut cx,
12445 lsp::SignatureHelp {
12446 signatures: vec![lsp::SignatureInformation {
12447 label: "test signature".to_string(),
12448 documentation: None,
12449 parameters: Some(vec![lsp::ParameterInformation {
12450 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
12451 documentation: None,
12452 }]),
12453 active_parameter: None,
12454 }],
12455 active_signature: None,
12456 active_parameter: None,
12457 },
12458 );
12459 cx.update_editor(|editor, window, cx| {
12460 assert!(
12461 !editor.signature_help_state.is_shown(),
12462 "No signature help was called for"
12463 );
12464 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12465 });
12466 cx.run_until_parked();
12467 cx.update_editor(|editor, _, _| {
12468 assert!(
12469 !editor.signature_help_state.is_shown(),
12470 "No signature help should be shown when completions menu is open"
12471 );
12472 });
12473
12474 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12475 editor.context_menu_next(&Default::default(), window, cx);
12476 editor
12477 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12478 .unwrap()
12479 });
12480 cx.assert_editor_state(indoc! {"
12481 one.second_completionˇ
12482 two
12483 three
12484 "});
12485
12486 handle_resolve_completion_request(
12487 &mut cx,
12488 Some(vec![
12489 (
12490 //This overlaps with the primary completion edit which is
12491 //misbehavior from the LSP spec, test that we filter it out
12492 indoc! {"
12493 one.second_ˇcompletion
12494 two
12495 threeˇ
12496 "},
12497 "overlapping additional edit",
12498 ),
12499 (
12500 indoc! {"
12501 one.second_completion
12502 two
12503 threeˇ
12504 "},
12505 "\nadditional edit",
12506 ),
12507 ]),
12508 )
12509 .await;
12510 apply_additional_edits.await.unwrap();
12511 cx.assert_editor_state(indoc! {"
12512 one.second_completionˇ
12513 two
12514 three
12515 additional edit
12516 "});
12517
12518 cx.set_state(indoc! {"
12519 one.second_completion
12520 twoˇ
12521 threeˇ
12522 additional edit
12523 "});
12524 cx.simulate_keystroke(" ");
12525 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12526 cx.simulate_keystroke("s");
12527 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12528
12529 cx.assert_editor_state(indoc! {"
12530 one.second_completion
12531 two sˇ
12532 three sˇ
12533 additional edit
12534 "});
12535 handle_completion_request(
12536 indoc! {"
12537 one.second_completion
12538 two s
12539 three <s|>
12540 additional edit
12541 "},
12542 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12543 true,
12544 counter.clone(),
12545 &mut cx,
12546 )
12547 .await;
12548 cx.condition(|editor, _| editor.context_menu_visible())
12549 .await;
12550 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12551
12552 cx.simulate_keystroke("i");
12553
12554 handle_completion_request(
12555 indoc! {"
12556 one.second_completion
12557 two si
12558 three <si|>
12559 additional edit
12560 "},
12561 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
12562 true,
12563 counter.clone(),
12564 &mut cx,
12565 )
12566 .await;
12567 cx.condition(|editor, _| editor.context_menu_visible())
12568 .await;
12569 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12570
12571 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12572 editor
12573 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12574 .unwrap()
12575 });
12576 cx.assert_editor_state(indoc! {"
12577 one.second_completion
12578 two sixth_completionˇ
12579 three sixth_completionˇ
12580 additional edit
12581 "});
12582
12583 apply_additional_edits.await.unwrap();
12584
12585 update_test_language_settings(&mut cx, |settings| {
12586 settings.defaults.show_completions_on_input = Some(false);
12587 });
12588 cx.set_state("editorˇ");
12589 cx.simulate_keystroke(".");
12590 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12591 cx.simulate_keystrokes("c l o");
12592 cx.assert_editor_state("editor.cloˇ");
12593 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
12594 cx.update_editor(|editor, window, cx| {
12595 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
12596 });
12597 handle_completion_request(
12598 "editor.<clo|>",
12599 vec!["close", "clobber"],
12600 true,
12601 counter.clone(),
12602 &mut cx,
12603 )
12604 .await;
12605 cx.condition(|editor, _| editor.context_menu_visible())
12606 .await;
12607 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
12608
12609 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12610 editor
12611 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12612 .unwrap()
12613 });
12614 cx.assert_editor_state("editor.clobberˇ");
12615 handle_resolve_completion_request(&mut cx, None).await;
12616 apply_additional_edits.await.unwrap();
12617}
12618
12619#[gpui::test]
12620async fn test_completion_reuse(cx: &mut TestAppContext) {
12621 init_test(cx, |_| {});
12622
12623 let mut cx = EditorLspTestContext::new_rust(
12624 lsp::ServerCapabilities {
12625 completion_provider: Some(lsp::CompletionOptions {
12626 trigger_characters: Some(vec![".".to_string()]),
12627 ..Default::default()
12628 }),
12629 ..Default::default()
12630 },
12631 cx,
12632 )
12633 .await;
12634
12635 let counter = Arc::new(AtomicUsize::new(0));
12636 cx.set_state("objˇ");
12637 cx.simulate_keystroke(".");
12638
12639 // Initial completion request returns complete results
12640 let is_incomplete = false;
12641 handle_completion_request(
12642 "obj.|<>",
12643 vec!["a", "ab", "abc"],
12644 is_incomplete,
12645 counter.clone(),
12646 &mut cx,
12647 )
12648 .await;
12649 cx.run_until_parked();
12650 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12651 cx.assert_editor_state("obj.ˇ");
12652 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12653
12654 // Type "a" - filters existing completions
12655 cx.simulate_keystroke("a");
12656 cx.run_until_parked();
12657 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12658 cx.assert_editor_state("obj.aˇ");
12659 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12660
12661 // Type "b" - filters existing completions
12662 cx.simulate_keystroke("b");
12663 cx.run_until_parked();
12664 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12665 cx.assert_editor_state("obj.abˇ");
12666 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12667
12668 // Type "c" - filters existing completions
12669 cx.simulate_keystroke("c");
12670 cx.run_until_parked();
12671 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12672 cx.assert_editor_state("obj.abcˇ");
12673 check_displayed_completions(vec!["abc"], &mut cx);
12674
12675 // Backspace to delete "c" - filters existing completions
12676 cx.update_editor(|editor, window, cx| {
12677 editor.backspace(&Backspace, window, cx);
12678 });
12679 cx.run_until_parked();
12680 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12681 cx.assert_editor_state("obj.abˇ");
12682 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12683
12684 // Moving cursor to the left dismisses menu.
12685 cx.update_editor(|editor, window, cx| {
12686 editor.move_left(&MoveLeft, window, cx);
12687 });
12688 cx.run_until_parked();
12689 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12690 cx.assert_editor_state("obj.aˇb");
12691 cx.update_editor(|editor, _, _| {
12692 assert_eq!(editor.context_menu_visible(), false);
12693 });
12694
12695 // Type "b" - new request
12696 cx.simulate_keystroke("b");
12697 let is_incomplete = false;
12698 handle_completion_request(
12699 "obj.<ab|>a",
12700 vec!["ab", "abc"],
12701 is_incomplete,
12702 counter.clone(),
12703 &mut cx,
12704 )
12705 .await;
12706 cx.run_until_parked();
12707 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
12708 cx.assert_editor_state("obj.abˇb");
12709 check_displayed_completions(vec!["ab", "abc"], &mut cx);
12710
12711 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
12712 cx.update_editor(|editor, window, cx| {
12713 editor.backspace(&Backspace, window, cx);
12714 });
12715 let is_incomplete = false;
12716 handle_completion_request(
12717 "obj.<a|>b",
12718 vec!["a", "ab", "abc"],
12719 is_incomplete,
12720 counter.clone(),
12721 &mut cx,
12722 )
12723 .await;
12724 cx.run_until_parked();
12725 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12726 cx.assert_editor_state("obj.aˇb");
12727 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
12728
12729 // Backspace to delete "a" - dismisses menu.
12730 cx.update_editor(|editor, window, cx| {
12731 editor.backspace(&Backspace, window, cx);
12732 });
12733 cx.run_until_parked();
12734 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
12735 cx.assert_editor_state("obj.ˇb");
12736 cx.update_editor(|editor, _, _| {
12737 assert_eq!(editor.context_menu_visible(), false);
12738 });
12739}
12740
12741#[gpui::test]
12742async fn test_word_completion(cx: &mut TestAppContext) {
12743 let lsp_fetch_timeout_ms = 10;
12744 init_test(cx, |language_settings| {
12745 language_settings.defaults.completions = Some(CompletionSettings {
12746 words: WordsCompletionMode::Fallback,
12747 lsp: true,
12748 lsp_fetch_timeout_ms: 10,
12749 lsp_insert_mode: LspInsertMode::Insert,
12750 });
12751 });
12752
12753 let mut cx = EditorLspTestContext::new_rust(
12754 lsp::ServerCapabilities {
12755 completion_provider: Some(lsp::CompletionOptions {
12756 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12757 ..lsp::CompletionOptions::default()
12758 }),
12759 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12760 ..lsp::ServerCapabilities::default()
12761 },
12762 cx,
12763 )
12764 .await;
12765
12766 let throttle_completions = Arc::new(AtomicBool::new(false));
12767
12768 let lsp_throttle_completions = throttle_completions.clone();
12769 let _completion_requests_handler =
12770 cx.lsp
12771 .server
12772 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
12773 let lsp_throttle_completions = lsp_throttle_completions.clone();
12774 let cx = cx.clone();
12775 async move {
12776 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
12777 cx.background_executor()
12778 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
12779 .await;
12780 }
12781 Ok(Some(lsp::CompletionResponse::Array(vec![
12782 lsp::CompletionItem {
12783 label: "first".into(),
12784 ..lsp::CompletionItem::default()
12785 },
12786 lsp::CompletionItem {
12787 label: "last".into(),
12788 ..lsp::CompletionItem::default()
12789 },
12790 ])))
12791 }
12792 });
12793
12794 cx.set_state(indoc! {"
12795 oneˇ
12796 two
12797 three
12798 "});
12799 cx.simulate_keystroke(".");
12800 cx.executor().run_until_parked();
12801 cx.condition(|editor, _| editor.context_menu_visible())
12802 .await;
12803 cx.update_editor(|editor, window, cx| {
12804 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12805 {
12806 assert_eq!(
12807 completion_menu_entries(&menu),
12808 &["first", "last"],
12809 "When LSP server is fast to reply, no fallback word completions are used"
12810 );
12811 } else {
12812 panic!("expected completion menu to be open");
12813 }
12814 editor.cancel(&Cancel, window, cx);
12815 });
12816 cx.executor().run_until_parked();
12817 cx.condition(|editor, _| !editor.context_menu_visible())
12818 .await;
12819
12820 throttle_completions.store(true, atomic::Ordering::Release);
12821 cx.simulate_keystroke(".");
12822 cx.executor()
12823 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
12824 cx.executor().run_until_parked();
12825 cx.condition(|editor, _| editor.context_menu_visible())
12826 .await;
12827 cx.update_editor(|editor, _, _| {
12828 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12829 {
12830 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
12831 "When LSP server is slow, document words can be shown instead, if configured accordingly");
12832 } else {
12833 panic!("expected completion menu to be open");
12834 }
12835 });
12836}
12837
12838#[gpui::test]
12839async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
12840 init_test(cx, |language_settings| {
12841 language_settings.defaults.completions = Some(CompletionSettings {
12842 words: WordsCompletionMode::Enabled,
12843 lsp: true,
12844 lsp_fetch_timeout_ms: 0,
12845 lsp_insert_mode: LspInsertMode::Insert,
12846 });
12847 });
12848
12849 let mut cx = EditorLspTestContext::new_rust(
12850 lsp::ServerCapabilities {
12851 completion_provider: Some(lsp::CompletionOptions {
12852 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12853 ..lsp::CompletionOptions::default()
12854 }),
12855 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12856 ..lsp::ServerCapabilities::default()
12857 },
12858 cx,
12859 )
12860 .await;
12861
12862 let _completion_requests_handler =
12863 cx.lsp
12864 .server
12865 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12866 Ok(Some(lsp::CompletionResponse::Array(vec![
12867 lsp::CompletionItem {
12868 label: "first".into(),
12869 ..lsp::CompletionItem::default()
12870 },
12871 lsp::CompletionItem {
12872 label: "last".into(),
12873 ..lsp::CompletionItem::default()
12874 },
12875 ])))
12876 });
12877
12878 cx.set_state(indoc! {"ˇ
12879 first
12880 last
12881 second
12882 "});
12883 cx.simulate_keystroke(".");
12884 cx.executor().run_until_parked();
12885 cx.condition(|editor, _| editor.context_menu_visible())
12886 .await;
12887 cx.update_editor(|editor, _, _| {
12888 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12889 {
12890 assert_eq!(
12891 completion_menu_entries(&menu),
12892 &["first", "last", "second"],
12893 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
12894 );
12895 } else {
12896 panic!("expected completion menu to be open");
12897 }
12898 });
12899}
12900
12901#[gpui::test]
12902async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
12903 init_test(cx, |language_settings| {
12904 language_settings.defaults.completions = Some(CompletionSettings {
12905 words: WordsCompletionMode::Disabled,
12906 lsp: true,
12907 lsp_fetch_timeout_ms: 0,
12908 lsp_insert_mode: LspInsertMode::Insert,
12909 });
12910 });
12911
12912 let mut cx = EditorLspTestContext::new_rust(
12913 lsp::ServerCapabilities {
12914 completion_provider: Some(lsp::CompletionOptions {
12915 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
12916 ..lsp::CompletionOptions::default()
12917 }),
12918 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
12919 ..lsp::ServerCapabilities::default()
12920 },
12921 cx,
12922 )
12923 .await;
12924
12925 let _completion_requests_handler =
12926 cx.lsp
12927 .server
12928 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
12929 panic!("LSP completions should not be queried when dealing with word completions")
12930 });
12931
12932 cx.set_state(indoc! {"ˇ
12933 first
12934 last
12935 second
12936 "});
12937 cx.update_editor(|editor, window, cx| {
12938 editor.show_word_completions(&ShowWordCompletions, window, cx);
12939 });
12940 cx.executor().run_until_parked();
12941 cx.condition(|editor, _| editor.context_menu_visible())
12942 .await;
12943 cx.update_editor(|editor, _, _| {
12944 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12945 {
12946 assert_eq!(
12947 completion_menu_entries(&menu),
12948 &["first", "last", "second"],
12949 "`ShowWordCompletions` action should show word completions"
12950 );
12951 } else {
12952 panic!("expected completion menu to be open");
12953 }
12954 });
12955
12956 cx.simulate_keystroke("l");
12957 cx.executor().run_until_parked();
12958 cx.condition(|editor, _| editor.context_menu_visible())
12959 .await;
12960 cx.update_editor(|editor, _, _| {
12961 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
12962 {
12963 assert_eq!(
12964 completion_menu_entries(&menu),
12965 &["last"],
12966 "After showing word completions, further editing should filter them and not query the LSP"
12967 );
12968 } else {
12969 panic!("expected completion menu to be open");
12970 }
12971 });
12972}
12973
12974#[gpui::test]
12975async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
12976 init_test(cx, |language_settings| {
12977 language_settings.defaults.completions = Some(CompletionSettings {
12978 words: WordsCompletionMode::Fallback,
12979 lsp: false,
12980 lsp_fetch_timeout_ms: 0,
12981 lsp_insert_mode: LspInsertMode::Insert,
12982 });
12983 });
12984
12985 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
12986
12987 cx.set_state(indoc! {"ˇ
12988 0_usize
12989 let
12990 33
12991 4.5f32
12992 "});
12993 cx.update_editor(|editor, window, cx| {
12994 editor.show_completions(&ShowCompletions::default(), window, cx);
12995 });
12996 cx.executor().run_until_parked();
12997 cx.condition(|editor, _| editor.context_menu_visible())
12998 .await;
12999 cx.update_editor(|editor, window, cx| {
13000 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13001 {
13002 assert_eq!(
13003 completion_menu_entries(&menu),
13004 &["let"],
13005 "With no digits in the completion query, no digits should be in the word completions"
13006 );
13007 } else {
13008 panic!("expected completion menu to be open");
13009 }
13010 editor.cancel(&Cancel, window, cx);
13011 });
13012
13013 cx.set_state(indoc! {"3ˇ
13014 0_usize
13015 let
13016 3
13017 33.35f32
13018 "});
13019 cx.update_editor(|editor, window, cx| {
13020 editor.show_completions(&ShowCompletions::default(), window, cx);
13021 });
13022 cx.executor().run_until_parked();
13023 cx.condition(|editor, _| editor.context_menu_visible())
13024 .await;
13025 cx.update_editor(|editor, _, _| {
13026 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13027 {
13028 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
13029 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
13030 } else {
13031 panic!("expected completion menu to be open");
13032 }
13033 });
13034}
13035
13036fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
13037 let position = || lsp::Position {
13038 line: params.text_document_position.position.line,
13039 character: params.text_document_position.position.character,
13040 };
13041 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13042 range: lsp::Range {
13043 start: position(),
13044 end: position(),
13045 },
13046 new_text: text.to_string(),
13047 }))
13048}
13049
13050#[gpui::test]
13051async fn test_multiline_completion(cx: &mut TestAppContext) {
13052 init_test(cx, |_| {});
13053
13054 let fs = FakeFs::new(cx.executor());
13055 fs.insert_tree(
13056 path!("/a"),
13057 json!({
13058 "main.ts": "a",
13059 }),
13060 )
13061 .await;
13062
13063 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13064 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13065 let typescript_language = Arc::new(Language::new(
13066 LanguageConfig {
13067 name: "TypeScript".into(),
13068 matcher: LanguageMatcher {
13069 path_suffixes: vec!["ts".to_string()],
13070 ..LanguageMatcher::default()
13071 },
13072 line_comments: vec!["// ".into()],
13073 ..LanguageConfig::default()
13074 },
13075 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13076 ));
13077 language_registry.add(typescript_language.clone());
13078 let mut fake_servers = language_registry.register_fake_lsp(
13079 "TypeScript",
13080 FakeLspAdapter {
13081 capabilities: lsp::ServerCapabilities {
13082 completion_provider: Some(lsp::CompletionOptions {
13083 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13084 ..lsp::CompletionOptions::default()
13085 }),
13086 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13087 ..lsp::ServerCapabilities::default()
13088 },
13089 // Emulate vtsls label generation
13090 label_for_completion: Some(Box::new(|item, _| {
13091 let text = if let Some(description) = item
13092 .label_details
13093 .as_ref()
13094 .and_then(|label_details| label_details.description.as_ref())
13095 {
13096 format!("{} {}", item.label, description)
13097 } else if let Some(detail) = &item.detail {
13098 format!("{} {}", item.label, detail)
13099 } else {
13100 item.label.clone()
13101 };
13102 let len = text.len();
13103 Some(language::CodeLabel {
13104 text,
13105 runs: Vec::new(),
13106 filter_range: 0..len,
13107 })
13108 })),
13109 ..FakeLspAdapter::default()
13110 },
13111 );
13112 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13113 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13114 let worktree_id = workspace
13115 .update(cx, |workspace, _window, cx| {
13116 workspace.project().update(cx, |project, cx| {
13117 project.worktrees(cx).next().unwrap().read(cx).id()
13118 })
13119 })
13120 .unwrap();
13121 let _buffer = project
13122 .update(cx, |project, cx| {
13123 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
13124 })
13125 .await
13126 .unwrap();
13127 let editor = workspace
13128 .update(cx, |workspace, window, cx| {
13129 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
13130 })
13131 .unwrap()
13132 .await
13133 .unwrap()
13134 .downcast::<Editor>()
13135 .unwrap();
13136 let fake_server = fake_servers.next().await.unwrap();
13137
13138 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
13139 let multiline_label_2 = "a\nb\nc\n";
13140 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
13141 let multiline_description = "d\ne\nf\n";
13142 let multiline_detail_2 = "g\nh\ni\n";
13143
13144 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
13145 move |params, _| async move {
13146 Ok(Some(lsp::CompletionResponse::Array(vec![
13147 lsp::CompletionItem {
13148 label: multiline_label.to_string(),
13149 text_edit: gen_text_edit(¶ms, "new_text_1"),
13150 ..lsp::CompletionItem::default()
13151 },
13152 lsp::CompletionItem {
13153 label: "single line label 1".to_string(),
13154 detail: Some(multiline_detail.to_string()),
13155 text_edit: gen_text_edit(¶ms, "new_text_2"),
13156 ..lsp::CompletionItem::default()
13157 },
13158 lsp::CompletionItem {
13159 label: "single line label 2".to_string(),
13160 label_details: Some(lsp::CompletionItemLabelDetails {
13161 description: Some(multiline_description.to_string()),
13162 detail: None,
13163 }),
13164 text_edit: gen_text_edit(¶ms, "new_text_2"),
13165 ..lsp::CompletionItem::default()
13166 },
13167 lsp::CompletionItem {
13168 label: multiline_label_2.to_string(),
13169 detail: Some(multiline_detail_2.to_string()),
13170 text_edit: gen_text_edit(¶ms, "new_text_3"),
13171 ..lsp::CompletionItem::default()
13172 },
13173 lsp::CompletionItem {
13174 label: "Label with many spaces and \t but without newlines".to_string(),
13175 detail: Some(
13176 "Details with many spaces and \t but without newlines".to_string(),
13177 ),
13178 text_edit: gen_text_edit(¶ms, "new_text_4"),
13179 ..lsp::CompletionItem::default()
13180 },
13181 ])))
13182 },
13183 );
13184
13185 editor.update_in(cx, |editor, window, cx| {
13186 cx.focus_self(window);
13187 editor.move_to_end(&MoveToEnd, window, cx);
13188 editor.handle_input(".", window, cx);
13189 });
13190 cx.run_until_parked();
13191 completion_handle.next().await.unwrap();
13192
13193 editor.update(cx, |editor, _| {
13194 assert!(editor.context_menu_visible());
13195 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13196 {
13197 let completion_labels = menu
13198 .completions
13199 .borrow()
13200 .iter()
13201 .map(|c| c.label.text.clone())
13202 .collect::<Vec<_>>();
13203 assert_eq!(
13204 completion_labels,
13205 &[
13206 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
13207 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
13208 "single line label 2 d e f ",
13209 "a b c g h i ",
13210 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
13211 ],
13212 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
13213 );
13214
13215 for completion in menu
13216 .completions
13217 .borrow()
13218 .iter() {
13219 assert_eq!(
13220 completion.label.filter_range,
13221 0..completion.label.text.len(),
13222 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
13223 );
13224 }
13225 } else {
13226 panic!("expected completion menu to be open");
13227 }
13228 });
13229}
13230
13231#[gpui::test]
13232async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
13233 init_test(cx, |_| {});
13234 let mut cx = EditorLspTestContext::new_rust(
13235 lsp::ServerCapabilities {
13236 completion_provider: Some(lsp::CompletionOptions {
13237 trigger_characters: Some(vec![".".to_string()]),
13238 ..Default::default()
13239 }),
13240 ..Default::default()
13241 },
13242 cx,
13243 )
13244 .await;
13245 cx.lsp
13246 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13247 Ok(Some(lsp::CompletionResponse::Array(vec![
13248 lsp::CompletionItem {
13249 label: "first".into(),
13250 ..Default::default()
13251 },
13252 lsp::CompletionItem {
13253 label: "last".into(),
13254 ..Default::default()
13255 },
13256 ])))
13257 });
13258 cx.set_state("variableˇ");
13259 cx.simulate_keystroke(".");
13260 cx.executor().run_until_parked();
13261
13262 cx.update_editor(|editor, _, _| {
13263 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13264 {
13265 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
13266 } else {
13267 panic!("expected completion menu to be open");
13268 }
13269 });
13270
13271 cx.update_editor(|editor, window, cx| {
13272 editor.move_page_down(&MovePageDown::default(), window, cx);
13273 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13274 {
13275 assert!(
13276 menu.selected_item == 1,
13277 "expected PageDown to select the last item from the context menu"
13278 );
13279 } else {
13280 panic!("expected completion menu to stay open after PageDown");
13281 }
13282 });
13283
13284 cx.update_editor(|editor, window, cx| {
13285 editor.move_page_up(&MovePageUp::default(), window, cx);
13286 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13287 {
13288 assert!(
13289 menu.selected_item == 0,
13290 "expected PageUp to select the first item from the context menu"
13291 );
13292 } else {
13293 panic!("expected completion menu to stay open after PageUp");
13294 }
13295 });
13296}
13297
13298#[gpui::test]
13299async fn test_as_is_completions(cx: &mut TestAppContext) {
13300 init_test(cx, |_| {});
13301 let mut cx = EditorLspTestContext::new_rust(
13302 lsp::ServerCapabilities {
13303 completion_provider: Some(lsp::CompletionOptions {
13304 ..Default::default()
13305 }),
13306 ..Default::default()
13307 },
13308 cx,
13309 )
13310 .await;
13311 cx.lsp
13312 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13313 Ok(Some(lsp::CompletionResponse::Array(vec![
13314 lsp::CompletionItem {
13315 label: "unsafe".into(),
13316 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13317 range: lsp::Range {
13318 start: lsp::Position {
13319 line: 1,
13320 character: 2,
13321 },
13322 end: lsp::Position {
13323 line: 1,
13324 character: 3,
13325 },
13326 },
13327 new_text: "unsafe".to_string(),
13328 })),
13329 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
13330 ..Default::default()
13331 },
13332 ])))
13333 });
13334 cx.set_state("fn a() {}\n nˇ");
13335 cx.executor().run_until_parked();
13336 cx.update_editor(|editor, window, cx| {
13337 editor.show_completions(
13338 &ShowCompletions {
13339 trigger: Some("\n".into()),
13340 },
13341 window,
13342 cx,
13343 );
13344 });
13345 cx.executor().run_until_parked();
13346
13347 cx.update_editor(|editor, window, cx| {
13348 editor.confirm_completion(&Default::default(), window, cx)
13349 });
13350 cx.executor().run_until_parked();
13351 cx.assert_editor_state("fn a() {}\n unsafeˇ");
13352}
13353
13354#[gpui::test]
13355async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
13356 init_test(cx, |_| {});
13357
13358 let mut cx = EditorLspTestContext::new_rust(
13359 lsp::ServerCapabilities {
13360 completion_provider: Some(lsp::CompletionOptions {
13361 trigger_characters: Some(vec![".".to_string()]),
13362 resolve_provider: Some(true),
13363 ..Default::default()
13364 }),
13365 ..Default::default()
13366 },
13367 cx,
13368 )
13369 .await;
13370
13371 cx.set_state("fn main() { let a = 2ˇ; }");
13372 cx.simulate_keystroke(".");
13373 let completion_item = lsp::CompletionItem {
13374 label: "Some".into(),
13375 kind: Some(lsp::CompletionItemKind::SNIPPET),
13376 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13377 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13378 kind: lsp::MarkupKind::Markdown,
13379 value: "```rust\nSome(2)\n```".to_string(),
13380 })),
13381 deprecated: Some(false),
13382 sort_text: Some("Some".to_string()),
13383 filter_text: Some("Some".to_string()),
13384 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13385 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13386 range: lsp::Range {
13387 start: lsp::Position {
13388 line: 0,
13389 character: 22,
13390 },
13391 end: lsp::Position {
13392 line: 0,
13393 character: 22,
13394 },
13395 },
13396 new_text: "Some(2)".to_string(),
13397 })),
13398 additional_text_edits: Some(vec![lsp::TextEdit {
13399 range: lsp::Range {
13400 start: lsp::Position {
13401 line: 0,
13402 character: 20,
13403 },
13404 end: lsp::Position {
13405 line: 0,
13406 character: 22,
13407 },
13408 },
13409 new_text: "".to_string(),
13410 }]),
13411 ..Default::default()
13412 };
13413
13414 let closure_completion_item = completion_item.clone();
13415 let counter = Arc::new(AtomicUsize::new(0));
13416 let counter_clone = counter.clone();
13417 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13418 let task_completion_item = closure_completion_item.clone();
13419 counter_clone.fetch_add(1, atomic::Ordering::Release);
13420 async move {
13421 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13422 is_incomplete: true,
13423 item_defaults: None,
13424 items: vec![task_completion_item],
13425 })))
13426 }
13427 });
13428
13429 cx.condition(|editor, _| editor.context_menu_visible())
13430 .await;
13431 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
13432 assert!(request.next().await.is_some());
13433 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13434
13435 cx.simulate_keystrokes("S o m");
13436 cx.condition(|editor, _| editor.context_menu_visible())
13437 .await;
13438 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
13439 assert!(request.next().await.is_some());
13440 assert!(request.next().await.is_some());
13441 assert!(request.next().await.is_some());
13442 request.close();
13443 assert!(request.next().await.is_none());
13444 assert_eq!(
13445 counter.load(atomic::Ordering::Acquire),
13446 4,
13447 "With the completions menu open, only one LSP request should happen per input"
13448 );
13449}
13450
13451#[gpui::test]
13452async fn test_toggle_comment(cx: &mut TestAppContext) {
13453 init_test(cx, |_| {});
13454 let mut cx = EditorTestContext::new(cx).await;
13455 let language = Arc::new(Language::new(
13456 LanguageConfig {
13457 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13458 ..Default::default()
13459 },
13460 Some(tree_sitter_rust::LANGUAGE.into()),
13461 ));
13462 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13463
13464 // If multiple selections intersect a line, the line is only toggled once.
13465 cx.set_state(indoc! {"
13466 fn a() {
13467 «//b();
13468 ˇ»// «c();
13469 //ˇ» d();
13470 }
13471 "});
13472
13473 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13474
13475 cx.assert_editor_state(indoc! {"
13476 fn a() {
13477 «b();
13478 c();
13479 ˇ» d();
13480 }
13481 "});
13482
13483 // The comment prefix is inserted at the same column for every line in a
13484 // selection.
13485 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13486
13487 cx.assert_editor_state(indoc! {"
13488 fn a() {
13489 // «b();
13490 // c();
13491 ˇ»// d();
13492 }
13493 "});
13494
13495 // If a selection ends at the beginning of a line, that line is not toggled.
13496 cx.set_selections_state(indoc! {"
13497 fn a() {
13498 // b();
13499 «// c();
13500 ˇ» // d();
13501 }
13502 "});
13503
13504 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13505
13506 cx.assert_editor_state(indoc! {"
13507 fn a() {
13508 // b();
13509 «c();
13510 ˇ» // d();
13511 }
13512 "});
13513
13514 // If a selection span a single line and is empty, the line is toggled.
13515 cx.set_state(indoc! {"
13516 fn a() {
13517 a();
13518 b();
13519 ˇ
13520 }
13521 "});
13522
13523 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13524
13525 cx.assert_editor_state(indoc! {"
13526 fn a() {
13527 a();
13528 b();
13529 //•ˇ
13530 }
13531 "});
13532
13533 // If a selection span multiple lines, empty lines are not toggled.
13534 cx.set_state(indoc! {"
13535 fn a() {
13536 «a();
13537
13538 c();ˇ»
13539 }
13540 "});
13541
13542 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13543
13544 cx.assert_editor_state(indoc! {"
13545 fn a() {
13546 // «a();
13547
13548 // c();ˇ»
13549 }
13550 "});
13551
13552 // If a selection includes multiple comment prefixes, all lines are uncommented.
13553 cx.set_state(indoc! {"
13554 fn a() {
13555 «// a();
13556 /// b();
13557 //! c();ˇ»
13558 }
13559 "});
13560
13561 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
13562
13563 cx.assert_editor_state(indoc! {"
13564 fn a() {
13565 «a();
13566 b();
13567 c();ˇ»
13568 }
13569 "});
13570}
13571
13572#[gpui::test]
13573async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
13574 init_test(cx, |_| {});
13575 let mut cx = EditorTestContext::new(cx).await;
13576 let language = Arc::new(Language::new(
13577 LanguageConfig {
13578 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
13579 ..Default::default()
13580 },
13581 Some(tree_sitter_rust::LANGUAGE.into()),
13582 ));
13583 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
13584
13585 let toggle_comments = &ToggleComments {
13586 advance_downwards: false,
13587 ignore_indent: true,
13588 };
13589
13590 // If multiple selections intersect a line, the line is only toggled once.
13591 cx.set_state(indoc! {"
13592 fn a() {
13593 // «b();
13594 // c();
13595 // ˇ» d();
13596 }
13597 "});
13598
13599 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13600
13601 cx.assert_editor_state(indoc! {"
13602 fn a() {
13603 «b();
13604 c();
13605 ˇ» d();
13606 }
13607 "});
13608
13609 // The comment prefix is inserted at the beginning of each line
13610 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13611
13612 cx.assert_editor_state(indoc! {"
13613 fn a() {
13614 // «b();
13615 // c();
13616 // ˇ» d();
13617 }
13618 "});
13619
13620 // If a selection ends at the beginning of a line, that line is not toggled.
13621 cx.set_selections_state(indoc! {"
13622 fn a() {
13623 // b();
13624 // «c();
13625 ˇ»// d();
13626 }
13627 "});
13628
13629 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13630
13631 cx.assert_editor_state(indoc! {"
13632 fn a() {
13633 // b();
13634 «c();
13635 ˇ»// d();
13636 }
13637 "});
13638
13639 // If a selection span a single line and is empty, the line is toggled.
13640 cx.set_state(indoc! {"
13641 fn a() {
13642 a();
13643 b();
13644 ˇ
13645 }
13646 "});
13647
13648 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13649
13650 cx.assert_editor_state(indoc! {"
13651 fn a() {
13652 a();
13653 b();
13654 //ˇ
13655 }
13656 "});
13657
13658 // If a selection span multiple lines, empty lines are not toggled.
13659 cx.set_state(indoc! {"
13660 fn a() {
13661 «a();
13662
13663 c();ˇ»
13664 }
13665 "});
13666
13667 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13668
13669 cx.assert_editor_state(indoc! {"
13670 fn a() {
13671 // «a();
13672
13673 // c();ˇ»
13674 }
13675 "});
13676
13677 // If a selection includes multiple comment prefixes, all lines are uncommented.
13678 cx.set_state(indoc! {"
13679 fn a() {
13680 // «a();
13681 /// b();
13682 //! c();ˇ»
13683 }
13684 "});
13685
13686 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
13687
13688 cx.assert_editor_state(indoc! {"
13689 fn a() {
13690 «a();
13691 b();
13692 c();ˇ»
13693 }
13694 "});
13695}
13696
13697#[gpui::test]
13698async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
13699 init_test(cx, |_| {});
13700
13701 let language = Arc::new(Language::new(
13702 LanguageConfig {
13703 line_comments: vec!["// ".into()],
13704 ..Default::default()
13705 },
13706 Some(tree_sitter_rust::LANGUAGE.into()),
13707 ));
13708
13709 let mut cx = EditorTestContext::new(cx).await;
13710
13711 cx.language_registry().add(language.clone());
13712 cx.update_buffer(|buffer, cx| {
13713 buffer.set_language(Some(language), cx);
13714 });
13715
13716 let toggle_comments = &ToggleComments {
13717 advance_downwards: true,
13718 ignore_indent: false,
13719 };
13720
13721 // Single cursor on one line -> advance
13722 // Cursor moves horizontally 3 characters as well on non-blank line
13723 cx.set_state(indoc!(
13724 "fn a() {
13725 ˇdog();
13726 cat();
13727 }"
13728 ));
13729 cx.update_editor(|editor, window, cx| {
13730 editor.toggle_comments(toggle_comments, window, cx);
13731 });
13732 cx.assert_editor_state(indoc!(
13733 "fn a() {
13734 // dog();
13735 catˇ();
13736 }"
13737 ));
13738
13739 // Single selection on one line -> don't advance
13740 cx.set_state(indoc!(
13741 "fn a() {
13742 «dog()ˇ»;
13743 cat();
13744 }"
13745 ));
13746 cx.update_editor(|editor, window, cx| {
13747 editor.toggle_comments(toggle_comments, window, cx);
13748 });
13749 cx.assert_editor_state(indoc!(
13750 "fn a() {
13751 // «dog()ˇ»;
13752 cat();
13753 }"
13754 ));
13755
13756 // Multiple cursors on one line -> advance
13757 cx.set_state(indoc!(
13758 "fn a() {
13759 ˇdˇog();
13760 cat();
13761 }"
13762 ));
13763 cx.update_editor(|editor, window, cx| {
13764 editor.toggle_comments(toggle_comments, window, cx);
13765 });
13766 cx.assert_editor_state(indoc!(
13767 "fn a() {
13768 // dog();
13769 catˇ(ˇ);
13770 }"
13771 ));
13772
13773 // Multiple cursors on one line, with selection -> don't advance
13774 cx.set_state(indoc!(
13775 "fn a() {
13776 ˇdˇog«()ˇ»;
13777 cat();
13778 }"
13779 ));
13780 cx.update_editor(|editor, window, cx| {
13781 editor.toggle_comments(toggle_comments, window, cx);
13782 });
13783 cx.assert_editor_state(indoc!(
13784 "fn a() {
13785 // ˇdˇog«()ˇ»;
13786 cat();
13787 }"
13788 ));
13789
13790 // Single cursor on one line -> advance
13791 // Cursor moves to column 0 on blank line
13792 cx.set_state(indoc!(
13793 "fn a() {
13794 ˇdog();
13795
13796 cat();
13797 }"
13798 ));
13799 cx.update_editor(|editor, window, cx| {
13800 editor.toggle_comments(toggle_comments, window, cx);
13801 });
13802 cx.assert_editor_state(indoc!(
13803 "fn a() {
13804 // dog();
13805 ˇ
13806 cat();
13807 }"
13808 ));
13809
13810 // Single cursor on one line -> advance
13811 // Cursor starts and ends at column 0
13812 cx.set_state(indoc!(
13813 "fn a() {
13814 ˇ dog();
13815 cat();
13816 }"
13817 ));
13818 cx.update_editor(|editor, window, cx| {
13819 editor.toggle_comments(toggle_comments, window, cx);
13820 });
13821 cx.assert_editor_state(indoc!(
13822 "fn a() {
13823 // dog();
13824 ˇ cat();
13825 }"
13826 ));
13827}
13828
13829#[gpui::test]
13830async fn test_toggle_block_comment(cx: &mut TestAppContext) {
13831 init_test(cx, |_| {});
13832
13833 let mut cx = EditorTestContext::new(cx).await;
13834
13835 let html_language = Arc::new(
13836 Language::new(
13837 LanguageConfig {
13838 name: "HTML".into(),
13839 block_comment: Some(("<!-- ".into(), " -->".into())),
13840 ..Default::default()
13841 },
13842 Some(tree_sitter_html::LANGUAGE.into()),
13843 )
13844 .with_injection_query(
13845 r#"
13846 (script_element
13847 (raw_text) @injection.content
13848 (#set! injection.language "javascript"))
13849 "#,
13850 )
13851 .unwrap(),
13852 );
13853
13854 let javascript_language = Arc::new(Language::new(
13855 LanguageConfig {
13856 name: "JavaScript".into(),
13857 line_comments: vec!["// ".into()],
13858 ..Default::default()
13859 },
13860 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13861 ));
13862
13863 cx.language_registry().add(html_language.clone());
13864 cx.language_registry().add(javascript_language.clone());
13865 cx.update_buffer(|buffer, cx| {
13866 buffer.set_language(Some(html_language), cx);
13867 });
13868
13869 // Toggle comments for empty selections
13870 cx.set_state(
13871 &r#"
13872 <p>A</p>ˇ
13873 <p>B</p>ˇ
13874 <p>C</p>ˇ
13875 "#
13876 .unindent(),
13877 );
13878 cx.update_editor(|editor, window, cx| {
13879 editor.toggle_comments(&ToggleComments::default(), window, cx)
13880 });
13881 cx.assert_editor_state(
13882 &r#"
13883 <!-- <p>A</p>ˇ -->
13884 <!-- <p>B</p>ˇ -->
13885 <!-- <p>C</p>ˇ -->
13886 "#
13887 .unindent(),
13888 );
13889 cx.update_editor(|editor, window, cx| {
13890 editor.toggle_comments(&ToggleComments::default(), window, cx)
13891 });
13892 cx.assert_editor_state(
13893 &r#"
13894 <p>A</p>ˇ
13895 <p>B</p>ˇ
13896 <p>C</p>ˇ
13897 "#
13898 .unindent(),
13899 );
13900
13901 // Toggle comments for mixture of empty and non-empty selections, where
13902 // multiple selections occupy a given line.
13903 cx.set_state(
13904 &r#"
13905 <p>A«</p>
13906 <p>ˇ»B</p>ˇ
13907 <p>C«</p>
13908 <p>ˇ»D</p>ˇ
13909 "#
13910 .unindent(),
13911 );
13912
13913 cx.update_editor(|editor, window, cx| {
13914 editor.toggle_comments(&ToggleComments::default(), window, cx)
13915 });
13916 cx.assert_editor_state(
13917 &r#"
13918 <!-- <p>A«</p>
13919 <p>ˇ»B</p>ˇ -->
13920 <!-- <p>C«</p>
13921 <p>ˇ»D</p>ˇ -->
13922 "#
13923 .unindent(),
13924 );
13925 cx.update_editor(|editor, window, cx| {
13926 editor.toggle_comments(&ToggleComments::default(), window, cx)
13927 });
13928 cx.assert_editor_state(
13929 &r#"
13930 <p>A«</p>
13931 <p>ˇ»B</p>ˇ
13932 <p>C«</p>
13933 <p>ˇ»D</p>ˇ
13934 "#
13935 .unindent(),
13936 );
13937
13938 // Toggle comments when different languages are active for different
13939 // selections.
13940 cx.set_state(
13941 &r#"
13942 ˇ<script>
13943 ˇvar x = new Y();
13944 ˇ</script>
13945 "#
13946 .unindent(),
13947 );
13948 cx.executor().run_until_parked();
13949 cx.update_editor(|editor, window, cx| {
13950 editor.toggle_comments(&ToggleComments::default(), window, cx)
13951 });
13952 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
13953 // Uncommenting and commenting from this position brings in even more wrong artifacts.
13954 cx.assert_editor_state(
13955 &r#"
13956 <!-- ˇ<script> -->
13957 // ˇvar x = new Y();
13958 <!-- ˇ</script> -->
13959 "#
13960 .unindent(),
13961 );
13962}
13963
13964#[gpui::test]
13965fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
13966 init_test(cx, |_| {});
13967
13968 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
13969 let multibuffer = cx.new(|cx| {
13970 let mut multibuffer = MultiBuffer::new(ReadWrite);
13971 multibuffer.push_excerpts(
13972 buffer.clone(),
13973 [
13974 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
13975 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
13976 ],
13977 cx,
13978 );
13979 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
13980 multibuffer
13981 });
13982
13983 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
13984 editor.update_in(cx, |editor, window, cx| {
13985 assert_eq!(editor.text(cx), "aaaa\nbbbb");
13986 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13987 s.select_ranges([
13988 Point::new(0, 0)..Point::new(0, 0),
13989 Point::new(1, 0)..Point::new(1, 0),
13990 ])
13991 });
13992
13993 editor.handle_input("X", window, cx);
13994 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
13995 assert_eq!(
13996 editor.selections.ranges(cx),
13997 [
13998 Point::new(0, 1)..Point::new(0, 1),
13999 Point::new(1, 1)..Point::new(1, 1),
14000 ]
14001 );
14002
14003 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
14004 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14005 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
14006 });
14007 editor.backspace(&Default::default(), window, cx);
14008 assert_eq!(editor.text(cx), "Xa\nbbb");
14009 assert_eq!(
14010 editor.selections.ranges(cx),
14011 [Point::new(1, 0)..Point::new(1, 0)]
14012 );
14013
14014 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14015 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
14016 });
14017 editor.backspace(&Default::default(), window, cx);
14018 assert_eq!(editor.text(cx), "X\nbb");
14019 assert_eq!(
14020 editor.selections.ranges(cx),
14021 [Point::new(0, 1)..Point::new(0, 1)]
14022 );
14023 });
14024}
14025
14026#[gpui::test]
14027fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
14028 init_test(cx, |_| {});
14029
14030 let markers = vec![('[', ']').into(), ('(', ')').into()];
14031 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
14032 indoc! {"
14033 [aaaa
14034 (bbbb]
14035 cccc)",
14036 },
14037 markers.clone(),
14038 );
14039 let excerpt_ranges = markers.into_iter().map(|marker| {
14040 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
14041 ExcerptRange::new(context.clone())
14042 });
14043 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
14044 let multibuffer = cx.new(|cx| {
14045 let mut multibuffer = MultiBuffer::new(ReadWrite);
14046 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
14047 multibuffer
14048 });
14049
14050 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
14051 editor.update_in(cx, |editor, window, cx| {
14052 let (expected_text, selection_ranges) = marked_text_ranges(
14053 indoc! {"
14054 aaaa
14055 bˇbbb
14056 bˇbbˇb
14057 cccc"
14058 },
14059 true,
14060 );
14061 assert_eq!(editor.text(cx), expected_text);
14062 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14063 s.select_ranges(selection_ranges)
14064 });
14065
14066 editor.handle_input("X", window, cx);
14067
14068 let (expected_text, expected_selections) = marked_text_ranges(
14069 indoc! {"
14070 aaaa
14071 bXˇbbXb
14072 bXˇbbXˇb
14073 cccc"
14074 },
14075 false,
14076 );
14077 assert_eq!(editor.text(cx), expected_text);
14078 assert_eq!(editor.selections.ranges(cx), expected_selections);
14079
14080 editor.newline(&Newline, window, cx);
14081 let (expected_text, expected_selections) = marked_text_ranges(
14082 indoc! {"
14083 aaaa
14084 bX
14085 ˇbbX
14086 b
14087 bX
14088 ˇbbX
14089 ˇb
14090 cccc"
14091 },
14092 false,
14093 );
14094 assert_eq!(editor.text(cx), expected_text);
14095 assert_eq!(editor.selections.ranges(cx), expected_selections);
14096 });
14097}
14098
14099#[gpui::test]
14100fn test_refresh_selections(cx: &mut TestAppContext) {
14101 init_test(cx, |_| {});
14102
14103 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14104 let mut excerpt1_id = None;
14105 let multibuffer = cx.new(|cx| {
14106 let mut multibuffer = MultiBuffer::new(ReadWrite);
14107 excerpt1_id = multibuffer
14108 .push_excerpts(
14109 buffer.clone(),
14110 [
14111 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14112 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14113 ],
14114 cx,
14115 )
14116 .into_iter()
14117 .next();
14118 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14119 multibuffer
14120 });
14121
14122 let editor = cx.add_window(|window, cx| {
14123 let mut editor = build_editor(multibuffer.clone(), window, cx);
14124 let snapshot = editor.snapshot(window, cx);
14125 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14126 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
14127 });
14128 editor.begin_selection(
14129 Point::new(2, 1).to_display_point(&snapshot),
14130 true,
14131 1,
14132 window,
14133 cx,
14134 );
14135 assert_eq!(
14136 editor.selections.ranges(cx),
14137 [
14138 Point::new(1, 3)..Point::new(1, 3),
14139 Point::new(2, 1)..Point::new(2, 1),
14140 ]
14141 );
14142 editor
14143 });
14144
14145 // Refreshing selections is a no-op when excerpts haven't changed.
14146 _ = editor.update(cx, |editor, window, cx| {
14147 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14148 assert_eq!(
14149 editor.selections.ranges(cx),
14150 [
14151 Point::new(1, 3)..Point::new(1, 3),
14152 Point::new(2, 1)..Point::new(2, 1),
14153 ]
14154 );
14155 });
14156
14157 multibuffer.update(cx, |multibuffer, cx| {
14158 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14159 });
14160 _ = editor.update(cx, |editor, window, cx| {
14161 // Removing an excerpt causes the first selection to become degenerate.
14162 assert_eq!(
14163 editor.selections.ranges(cx),
14164 [
14165 Point::new(0, 0)..Point::new(0, 0),
14166 Point::new(0, 1)..Point::new(0, 1)
14167 ]
14168 );
14169
14170 // Refreshing selections will relocate the first selection to the original buffer
14171 // location.
14172 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14173 assert_eq!(
14174 editor.selections.ranges(cx),
14175 [
14176 Point::new(0, 1)..Point::new(0, 1),
14177 Point::new(0, 3)..Point::new(0, 3)
14178 ]
14179 );
14180 assert!(editor.selections.pending_anchor().is_some());
14181 });
14182}
14183
14184#[gpui::test]
14185fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
14186 init_test(cx, |_| {});
14187
14188 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
14189 let mut excerpt1_id = None;
14190 let multibuffer = cx.new(|cx| {
14191 let mut multibuffer = MultiBuffer::new(ReadWrite);
14192 excerpt1_id = multibuffer
14193 .push_excerpts(
14194 buffer.clone(),
14195 [
14196 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
14197 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
14198 ],
14199 cx,
14200 )
14201 .into_iter()
14202 .next();
14203 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
14204 multibuffer
14205 });
14206
14207 let editor = cx.add_window(|window, cx| {
14208 let mut editor = build_editor(multibuffer.clone(), window, cx);
14209 let snapshot = editor.snapshot(window, cx);
14210 editor.begin_selection(
14211 Point::new(1, 3).to_display_point(&snapshot),
14212 false,
14213 1,
14214 window,
14215 cx,
14216 );
14217 assert_eq!(
14218 editor.selections.ranges(cx),
14219 [Point::new(1, 3)..Point::new(1, 3)]
14220 );
14221 editor
14222 });
14223
14224 multibuffer.update(cx, |multibuffer, cx| {
14225 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
14226 });
14227 _ = editor.update(cx, |editor, window, cx| {
14228 assert_eq!(
14229 editor.selections.ranges(cx),
14230 [Point::new(0, 0)..Point::new(0, 0)]
14231 );
14232
14233 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
14234 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
14235 assert_eq!(
14236 editor.selections.ranges(cx),
14237 [Point::new(0, 3)..Point::new(0, 3)]
14238 );
14239 assert!(editor.selections.pending_anchor().is_some());
14240 });
14241}
14242
14243#[gpui::test]
14244async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
14245 init_test(cx, |_| {});
14246
14247 let language = Arc::new(
14248 Language::new(
14249 LanguageConfig {
14250 brackets: BracketPairConfig {
14251 pairs: vec![
14252 BracketPair {
14253 start: "{".to_string(),
14254 end: "}".to_string(),
14255 close: true,
14256 surround: true,
14257 newline: true,
14258 },
14259 BracketPair {
14260 start: "/* ".to_string(),
14261 end: " */".to_string(),
14262 close: true,
14263 surround: true,
14264 newline: true,
14265 },
14266 ],
14267 ..Default::default()
14268 },
14269 ..Default::default()
14270 },
14271 Some(tree_sitter_rust::LANGUAGE.into()),
14272 )
14273 .with_indents_query("")
14274 .unwrap(),
14275 );
14276
14277 let text = concat!(
14278 "{ }\n", //
14279 " x\n", //
14280 " /* */\n", //
14281 "x\n", //
14282 "{{} }\n", //
14283 );
14284
14285 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
14286 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14287 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14288 editor
14289 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
14290 .await;
14291
14292 editor.update_in(cx, |editor, window, cx| {
14293 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14294 s.select_display_ranges([
14295 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
14296 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
14297 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
14298 ])
14299 });
14300 editor.newline(&Newline, window, cx);
14301
14302 assert_eq!(
14303 editor.buffer().read(cx).read(cx).text(),
14304 concat!(
14305 "{ \n", // Suppress rustfmt
14306 "\n", //
14307 "}\n", //
14308 " x\n", //
14309 " /* \n", //
14310 " \n", //
14311 " */\n", //
14312 "x\n", //
14313 "{{} \n", //
14314 "}\n", //
14315 )
14316 );
14317 });
14318}
14319
14320#[gpui::test]
14321fn test_highlighted_ranges(cx: &mut TestAppContext) {
14322 init_test(cx, |_| {});
14323
14324 let editor = cx.add_window(|window, cx| {
14325 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
14326 build_editor(buffer.clone(), window, cx)
14327 });
14328
14329 _ = editor.update(cx, |editor, window, cx| {
14330 struct Type1;
14331 struct Type2;
14332
14333 let buffer = editor.buffer.read(cx).snapshot(cx);
14334
14335 let anchor_range =
14336 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
14337
14338 editor.highlight_background::<Type1>(
14339 &[
14340 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
14341 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
14342 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
14343 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
14344 ],
14345 |_| Hsla::red(),
14346 cx,
14347 );
14348 editor.highlight_background::<Type2>(
14349 &[
14350 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
14351 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
14352 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
14353 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
14354 ],
14355 |_| Hsla::green(),
14356 cx,
14357 );
14358
14359 let snapshot = editor.snapshot(window, cx);
14360 let mut highlighted_ranges = editor.background_highlights_in_range(
14361 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
14362 &snapshot,
14363 cx.theme(),
14364 );
14365 // Enforce a consistent ordering based on color without relying on the ordering of the
14366 // highlight's `TypeId` which is non-executor.
14367 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
14368 assert_eq!(
14369 highlighted_ranges,
14370 &[
14371 (
14372 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
14373 Hsla::red(),
14374 ),
14375 (
14376 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14377 Hsla::red(),
14378 ),
14379 (
14380 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
14381 Hsla::green(),
14382 ),
14383 (
14384 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
14385 Hsla::green(),
14386 ),
14387 ]
14388 );
14389 assert_eq!(
14390 editor.background_highlights_in_range(
14391 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
14392 &snapshot,
14393 cx.theme(),
14394 ),
14395 &[(
14396 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
14397 Hsla::red(),
14398 )]
14399 );
14400 });
14401}
14402
14403#[gpui::test]
14404async fn test_following(cx: &mut TestAppContext) {
14405 init_test(cx, |_| {});
14406
14407 let fs = FakeFs::new(cx.executor());
14408 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14409
14410 let buffer = project.update(cx, |project, cx| {
14411 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
14412 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
14413 });
14414 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
14415 let follower = cx.update(|cx| {
14416 cx.open_window(
14417 WindowOptions {
14418 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
14419 gpui::Point::new(px(0.), px(0.)),
14420 gpui::Point::new(px(10.), px(80.)),
14421 ))),
14422 ..Default::default()
14423 },
14424 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
14425 )
14426 .unwrap()
14427 });
14428
14429 let is_still_following = Rc::new(RefCell::new(true));
14430 let follower_edit_event_count = Rc::new(RefCell::new(0));
14431 let pending_update = Rc::new(RefCell::new(None));
14432 let leader_entity = leader.root(cx).unwrap();
14433 let follower_entity = follower.root(cx).unwrap();
14434 _ = follower.update(cx, {
14435 let update = pending_update.clone();
14436 let is_still_following = is_still_following.clone();
14437 let follower_edit_event_count = follower_edit_event_count.clone();
14438 |_, window, cx| {
14439 cx.subscribe_in(
14440 &leader_entity,
14441 window,
14442 move |_, leader, event, window, cx| {
14443 leader.read(cx).add_event_to_update_proto(
14444 event,
14445 &mut update.borrow_mut(),
14446 window,
14447 cx,
14448 );
14449 },
14450 )
14451 .detach();
14452
14453 cx.subscribe_in(
14454 &follower_entity,
14455 window,
14456 move |_, _, event: &EditorEvent, _window, _cx| {
14457 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
14458 *is_still_following.borrow_mut() = false;
14459 }
14460
14461 if let EditorEvent::BufferEdited = event {
14462 *follower_edit_event_count.borrow_mut() += 1;
14463 }
14464 },
14465 )
14466 .detach();
14467 }
14468 });
14469
14470 // Update the selections only
14471 _ = leader.update(cx, |leader, window, cx| {
14472 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14473 s.select_ranges([1..1])
14474 });
14475 });
14476 follower
14477 .update(cx, |follower, window, cx| {
14478 follower.apply_update_proto(
14479 &project,
14480 pending_update.borrow_mut().take().unwrap(),
14481 window,
14482 cx,
14483 )
14484 })
14485 .unwrap()
14486 .await
14487 .unwrap();
14488 _ = follower.update(cx, |follower, _, cx| {
14489 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
14490 });
14491 assert!(*is_still_following.borrow());
14492 assert_eq!(*follower_edit_event_count.borrow(), 0);
14493
14494 // Update the scroll position only
14495 _ = leader.update(cx, |leader, window, cx| {
14496 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14497 });
14498 follower
14499 .update(cx, |follower, window, cx| {
14500 follower.apply_update_proto(
14501 &project,
14502 pending_update.borrow_mut().take().unwrap(),
14503 window,
14504 cx,
14505 )
14506 })
14507 .unwrap()
14508 .await
14509 .unwrap();
14510 assert_eq!(
14511 follower
14512 .update(cx, |follower, _, cx| follower.scroll_position(cx))
14513 .unwrap(),
14514 gpui::Point::new(1.5, 3.5)
14515 );
14516 assert!(*is_still_following.borrow());
14517 assert_eq!(*follower_edit_event_count.borrow(), 0);
14518
14519 // Update the selections and scroll position. The follower's scroll position is updated
14520 // via autoscroll, not via the leader's exact scroll position.
14521 _ = leader.update(cx, |leader, window, cx| {
14522 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14523 s.select_ranges([0..0])
14524 });
14525 leader.request_autoscroll(Autoscroll::newest(), cx);
14526 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
14527 });
14528 follower
14529 .update(cx, |follower, window, cx| {
14530 follower.apply_update_proto(
14531 &project,
14532 pending_update.borrow_mut().take().unwrap(),
14533 window,
14534 cx,
14535 )
14536 })
14537 .unwrap()
14538 .await
14539 .unwrap();
14540 _ = follower.update(cx, |follower, _, cx| {
14541 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
14542 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
14543 });
14544 assert!(*is_still_following.borrow());
14545
14546 // Creating a pending selection that precedes another selection
14547 _ = leader.update(cx, |leader, window, cx| {
14548 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14549 s.select_ranges([1..1])
14550 });
14551 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
14552 });
14553 follower
14554 .update(cx, |follower, window, cx| {
14555 follower.apply_update_proto(
14556 &project,
14557 pending_update.borrow_mut().take().unwrap(),
14558 window,
14559 cx,
14560 )
14561 })
14562 .unwrap()
14563 .await
14564 .unwrap();
14565 _ = follower.update(cx, |follower, _, cx| {
14566 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
14567 });
14568 assert!(*is_still_following.borrow());
14569
14570 // Extend the pending selection so that it surrounds another selection
14571 _ = leader.update(cx, |leader, window, cx| {
14572 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
14573 });
14574 follower
14575 .update(cx, |follower, window, cx| {
14576 follower.apply_update_proto(
14577 &project,
14578 pending_update.borrow_mut().take().unwrap(),
14579 window,
14580 cx,
14581 )
14582 })
14583 .unwrap()
14584 .await
14585 .unwrap();
14586 _ = follower.update(cx, |follower, _, cx| {
14587 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
14588 });
14589
14590 // Scrolling locally breaks the follow
14591 _ = follower.update(cx, |follower, window, cx| {
14592 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
14593 follower.set_scroll_anchor(
14594 ScrollAnchor {
14595 anchor: top_anchor,
14596 offset: gpui::Point::new(0.0, 0.5),
14597 },
14598 window,
14599 cx,
14600 );
14601 });
14602 assert!(!(*is_still_following.borrow()));
14603}
14604
14605#[gpui::test]
14606async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
14607 init_test(cx, |_| {});
14608
14609 let fs = FakeFs::new(cx.executor());
14610 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
14611 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14612 let pane = workspace
14613 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14614 .unwrap();
14615
14616 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14617
14618 let leader = pane.update_in(cx, |_, window, cx| {
14619 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
14620 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
14621 });
14622
14623 // Start following the editor when it has no excerpts.
14624 let mut state_message =
14625 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14626 let workspace_entity = workspace.root(cx).unwrap();
14627 let follower_1 = cx
14628 .update_window(*workspace.deref(), |_, window, cx| {
14629 Editor::from_state_proto(
14630 workspace_entity,
14631 ViewId {
14632 creator: CollaboratorId::PeerId(PeerId::default()),
14633 id: 0,
14634 },
14635 &mut state_message,
14636 window,
14637 cx,
14638 )
14639 })
14640 .unwrap()
14641 .unwrap()
14642 .await
14643 .unwrap();
14644
14645 let update_message = Rc::new(RefCell::new(None));
14646 follower_1.update_in(cx, {
14647 let update = update_message.clone();
14648 |_, window, cx| {
14649 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
14650 leader.read(cx).add_event_to_update_proto(
14651 event,
14652 &mut update.borrow_mut(),
14653 window,
14654 cx,
14655 );
14656 })
14657 .detach();
14658 }
14659 });
14660
14661 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
14662 (
14663 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
14664 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
14665 )
14666 });
14667
14668 // Insert some excerpts.
14669 leader.update(cx, |leader, cx| {
14670 leader.buffer.update(cx, |multibuffer, cx| {
14671 multibuffer.set_excerpts_for_path(
14672 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
14673 buffer_1.clone(),
14674 vec![
14675 Point::row_range(0..3),
14676 Point::row_range(1..6),
14677 Point::row_range(12..15),
14678 ],
14679 0,
14680 cx,
14681 );
14682 multibuffer.set_excerpts_for_path(
14683 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
14684 buffer_2.clone(),
14685 vec![Point::row_range(0..6), Point::row_range(8..12)],
14686 0,
14687 cx,
14688 );
14689 });
14690 });
14691
14692 // Apply the update of adding the excerpts.
14693 follower_1
14694 .update_in(cx, |follower, window, cx| {
14695 follower.apply_update_proto(
14696 &project,
14697 update_message.borrow().clone().unwrap(),
14698 window,
14699 cx,
14700 )
14701 })
14702 .await
14703 .unwrap();
14704 assert_eq!(
14705 follower_1.update(cx, |editor, cx| editor.text(cx)),
14706 leader.update(cx, |editor, cx| editor.text(cx))
14707 );
14708 update_message.borrow_mut().take();
14709
14710 // Start following separately after it already has excerpts.
14711 let mut state_message =
14712 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
14713 let workspace_entity = workspace.root(cx).unwrap();
14714 let follower_2 = cx
14715 .update_window(*workspace.deref(), |_, window, cx| {
14716 Editor::from_state_proto(
14717 workspace_entity,
14718 ViewId {
14719 creator: CollaboratorId::PeerId(PeerId::default()),
14720 id: 0,
14721 },
14722 &mut state_message,
14723 window,
14724 cx,
14725 )
14726 })
14727 .unwrap()
14728 .unwrap()
14729 .await
14730 .unwrap();
14731 assert_eq!(
14732 follower_2.update(cx, |editor, cx| editor.text(cx)),
14733 leader.update(cx, |editor, cx| editor.text(cx))
14734 );
14735
14736 // Remove some excerpts.
14737 leader.update(cx, |leader, cx| {
14738 leader.buffer.update(cx, |multibuffer, cx| {
14739 let excerpt_ids = multibuffer.excerpt_ids();
14740 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
14741 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
14742 });
14743 });
14744
14745 // Apply the update of removing the excerpts.
14746 follower_1
14747 .update_in(cx, |follower, window, cx| {
14748 follower.apply_update_proto(
14749 &project,
14750 update_message.borrow().clone().unwrap(),
14751 window,
14752 cx,
14753 )
14754 })
14755 .await
14756 .unwrap();
14757 follower_2
14758 .update_in(cx, |follower, window, cx| {
14759 follower.apply_update_proto(
14760 &project,
14761 update_message.borrow().clone().unwrap(),
14762 window,
14763 cx,
14764 )
14765 })
14766 .await
14767 .unwrap();
14768 update_message.borrow_mut().take();
14769 assert_eq!(
14770 follower_1.update(cx, |editor, cx| editor.text(cx)),
14771 leader.update(cx, |editor, cx| editor.text(cx))
14772 );
14773}
14774
14775#[gpui::test]
14776async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14777 init_test(cx, |_| {});
14778
14779 let mut cx = EditorTestContext::new(cx).await;
14780 let lsp_store =
14781 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
14782
14783 cx.set_state(indoc! {"
14784 ˇfn func(abc def: i32) -> u32 {
14785 }
14786 "});
14787
14788 cx.update(|_, cx| {
14789 lsp_store.update(cx, |lsp_store, cx| {
14790 lsp_store
14791 .update_diagnostics(
14792 LanguageServerId(0),
14793 lsp::PublishDiagnosticsParams {
14794 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
14795 version: None,
14796 diagnostics: vec![
14797 lsp::Diagnostic {
14798 range: lsp::Range::new(
14799 lsp::Position::new(0, 11),
14800 lsp::Position::new(0, 12),
14801 ),
14802 severity: Some(lsp::DiagnosticSeverity::ERROR),
14803 ..Default::default()
14804 },
14805 lsp::Diagnostic {
14806 range: lsp::Range::new(
14807 lsp::Position::new(0, 12),
14808 lsp::Position::new(0, 15),
14809 ),
14810 severity: Some(lsp::DiagnosticSeverity::ERROR),
14811 ..Default::default()
14812 },
14813 lsp::Diagnostic {
14814 range: lsp::Range::new(
14815 lsp::Position::new(0, 25),
14816 lsp::Position::new(0, 28),
14817 ),
14818 severity: Some(lsp::DiagnosticSeverity::ERROR),
14819 ..Default::default()
14820 },
14821 ],
14822 },
14823 None,
14824 DiagnosticSourceKind::Pushed,
14825 &[],
14826 cx,
14827 )
14828 .unwrap()
14829 });
14830 });
14831
14832 executor.run_until_parked();
14833
14834 cx.update_editor(|editor, window, cx| {
14835 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14836 });
14837
14838 cx.assert_editor_state(indoc! {"
14839 fn func(abc def: i32) -> ˇu32 {
14840 }
14841 "});
14842
14843 cx.update_editor(|editor, window, cx| {
14844 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14845 });
14846
14847 cx.assert_editor_state(indoc! {"
14848 fn func(abc ˇdef: i32) -> u32 {
14849 }
14850 "});
14851
14852 cx.update_editor(|editor, window, cx| {
14853 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14854 });
14855
14856 cx.assert_editor_state(indoc! {"
14857 fn func(abcˇ def: i32) -> u32 {
14858 }
14859 "});
14860
14861 cx.update_editor(|editor, window, cx| {
14862 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
14863 });
14864
14865 cx.assert_editor_state(indoc! {"
14866 fn func(abc def: i32) -> ˇu32 {
14867 }
14868 "});
14869}
14870
14871#[gpui::test]
14872async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14873 init_test(cx, |_| {});
14874
14875 let mut cx = EditorTestContext::new(cx).await;
14876
14877 let diff_base = r#"
14878 use some::mod;
14879
14880 const A: u32 = 42;
14881
14882 fn main() {
14883 println!("hello");
14884
14885 println!("world");
14886 }
14887 "#
14888 .unindent();
14889
14890 // Edits are modified, removed, modified, added
14891 cx.set_state(
14892 &r#"
14893 use some::modified;
14894
14895 ˇ
14896 fn main() {
14897 println!("hello there");
14898
14899 println!("around the");
14900 println!("world");
14901 }
14902 "#
14903 .unindent(),
14904 );
14905
14906 cx.set_head_text(&diff_base);
14907 executor.run_until_parked();
14908
14909 cx.update_editor(|editor, window, cx| {
14910 //Wrap around the bottom of the buffer
14911 for _ in 0..3 {
14912 editor.go_to_next_hunk(&GoToHunk, window, cx);
14913 }
14914 });
14915
14916 cx.assert_editor_state(
14917 &r#"
14918 ˇuse some::modified;
14919
14920
14921 fn main() {
14922 println!("hello there");
14923
14924 println!("around the");
14925 println!("world");
14926 }
14927 "#
14928 .unindent(),
14929 );
14930
14931 cx.update_editor(|editor, window, cx| {
14932 //Wrap around the top of the buffer
14933 for _ in 0..2 {
14934 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14935 }
14936 });
14937
14938 cx.assert_editor_state(
14939 &r#"
14940 use some::modified;
14941
14942
14943 fn main() {
14944 ˇ println!("hello there");
14945
14946 println!("around the");
14947 println!("world");
14948 }
14949 "#
14950 .unindent(),
14951 );
14952
14953 cx.update_editor(|editor, window, cx| {
14954 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14955 });
14956
14957 cx.assert_editor_state(
14958 &r#"
14959 use some::modified;
14960
14961 ˇ
14962 fn main() {
14963 println!("hello there");
14964
14965 println!("around the");
14966 println!("world");
14967 }
14968 "#
14969 .unindent(),
14970 );
14971
14972 cx.update_editor(|editor, window, cx| {
14973 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14974 });
14975
14976 cx.assert_editor_state(
14977 &r#"
14978 ˇuse some::modified;
14979
14980
14981 fn main() {
14982 println!("hello there");
14983
14984 println!("around the");
14985 println!("world");
14986 }
14987 "#
14988 .unindent(),
14989 );
14990
14991 cx.update_editor(|editor, window, cx| {
14992 for _ in 0..2 {
14993 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
14994 }
14995 });
14996
14997 cx.assert_editor_state(
14998 &r#"
14999 use some::modified;
15000
15001
15002 fn main() {
15003 ˇ println!("hello there");
15004
15005 println!("around the");
15006 println!("world");
15007 }
15008 "#
15009 .unindent(),
15010 );
15011
15012 cx.update_editor(|editor, window, cx| {
15013 editor.fold(&Fold, window, cx);
15014 });
15015
15016 cx.update_editor(|editor, window, cx| {
15017 editor.go_to_next_hunk(&GoToHunk, window, cx);
15018 });
15019
15020 cx.assert_editor_state(
15021 &r#"
15022 ˇuse some::modified;
15023
15024
15025 fn main() {
15026 println!("hello there");
15027
15028 println!("around the");
15029 println!("world");
15030 }
15031 "#
15032 .unindent(),
15033 );
15034}
15035
15036#[test]
15037fn test_split_words() {
15038 fn split(text: &str) -> Vec<&str> {
15039 split_words(text).collect()
15040 }
15041
15042 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
15043 assert_eq!(split("hello_world"), &["hello_", "world"]);
15044 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
15045 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
15046 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
15047 assert_eq!(split("helloworld"), &["helloworld"]);
15048
15049 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
15050}
15051
15052#[gpui::test]
15053async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
15054 init_test(cx, |_| {});
15055
15056 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
15057 let mut assert = |before, after| {
15058 let _state_context = cx.set_state(before);
15059 cx.run_until_parked();
15060 cx.update_editor(|editor, window, cx| {
15061 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
15062 });
15063 cx.run_until_parked();
15064 cx.assert_editor_state(after);
15065 };
15066
15067 // Outside bracket jumps to outside of matching bracket
15068 assert("console.logˇ(var);", "console.log(var)ˇ;");
15069 assert("console.log(var)ˇ;", "console.logˇ(var);");
15070
15071 // Inside bracket jumps to inside of matching bracket
15072 assert("console.log(ˇvar);", "console.log(varˇ);");
15073 assert("console.log(varˇ);", "console.log(ˇvar);");
15074
15075 // When outside a bracket and inside, favor jumping to the inside bracket
15076 assert(
15077 "console.log('foo', [1, 2, 3]ˇ);",
15078 "console.log(ˇ'foo', [1, 2, 3]);",
15079 );
15080 assert(
15081 "console.log(ˇ'foo', [1, 2, 3]);",
15082 "console.log('foo', [1, 2, 3]ˇ);",
15083 );
15084
15085 // Bias forward if two options are equally likely
15086 assert(
15087 "let result = curried_fun()ˇ();",
15088 "let result = curried_fun()()ˇ;",
15089 );
15090
15091 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
15092 assert(
15093 indoc! {"
15094 function test() {
15095 console.log('test')ˇ
15096 }"},
15097 indoc! {"
15098 function test() {
15099 console.logˇ('test')
15100 }"},
15101 );
15102}
15103
15104#[gpui::test]
15105async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
15106 init_test(cx, |_| {});
15107
15108 let fs = FakeFs::new(cx.executor());
15109 fs.insert_tree(
15110 path!("/a"),
15111 json!({
15112 "main.rs": "fn main() { let a = 5; }",
15113 "other.rs": "// Test file",
15114 }),
15115 )
15116 .await;
15117 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15118
15119 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15120 language_registry.add(Arc::new(Language::new(
15121 LanguageConfig {
15122 name: "Rust".into(),
15123 matcher: LanguageMatcher {
15124 path_suffixes: vec!["rs".to_string()],
15125 ..Default::default()
15126 },
15127 brackets: BracketPairConfig {
15128 pairs: vec![BracketPair {
15129 start: "{".to_string(),
15130 end: "}".to_string(),
15131 close: true,
15132 surround: true,
15133 newline: true,
15134 }],
15135 disabled_scopes_by_bracket_ix: Vec::new(),
15136 },
15137 ..Default::default()
15138 },
15139 Some(tree_sitter_rust::LANGUAGE.into()),
15140 )));
15141 let mut fake_servers = language_registry.register_fake_lsp(
15142 "Rust",
15143 FakeLspAdapter {
15144 capabilities: lsp::ServerCapabilities {
15145 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15146 first_trigger_character: "{".to_string(),
15147 more_trigger_character: None,
15148 }),
15149 ..Default::default()
15150 },
15151 ..Default::default()
15152 },
15153 );
15154
15155 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15156
15157 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15158
15159 let worktree_id = workspace
15160 .update(cx, |workspace, _, cx| {
15161 workspace.project().update(cx, |project, cx| {
15162 project.worktrees(cx).next().unwrap().read(cx).id()
15163 })
15164 })
15165 .unwrap();
15166
15167 let buffer = project
15168 .update(cx, |project, cx| {
15169 project.open_local_buffer(path!("/a/main.rs"), cx)
15170 })
15171 .await
15172 .unwrap();
15173 let editor_handle = workspace
15174 .update(cx, |workspace, window, cx| {
15175 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
15176 })
15177 .unwrap()
15178 .await
15179 .unwrap()
15180 .downcast::<Editor>()
15181 .unwrap();
15182
15183 cx.executor().start_waiting();
15184 let fake_server = fake_servers.next().await.unwrap();
15185
15186 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
15187 |params, _| async move {
15188 assert_eq!(
15189 params.text_document_position.text_document.uri,
15190 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
15191 );
15192 assert_eq!(
15193 params.text_document_position.position,
15194 lsp::Position::new(0, 21),
15195 );
15196
15197 Ok(Some(vec![lsp::TextEdit {
15198 new_text: "]".to_string(),
15199 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15200 }]))
15201 },
15202 );
15203
15204 editor_handle.update_in(cx, |editor, window, cx| {
15205 window.focus(&editor.focus_handle(cx));
15206 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15207 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
15208 });
15209 editor.handle_input("{", window, cx);
15210 });
15211
15212 cx.executor().run_until_parked();
15213
15214 buffer.update(cx, |buffer, _| {
15215 assert_eq!(
15216 buffer.text(),
15217 "fn main() { let a = {5}; }",
15218 "No extra braces from on type formatting should appear in the buffer"
15219 )
15220 });
15221}
15222
15223#[gpui::test(iterations = 20, seeds(31))]
15224async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
15225 init_test(cx, |_| {});
15226
15227 let mut cx = EditorLspTestContext::new_rust(
15228 lsp::ServerCapabilities {
15229 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
15230 first_trigger_character: ".".to_string(),
15231 more_trigger_character: None,
15232 }),
15233 ..Default::default()
15234 },
15235 cx,
15236 )
15237 .await;
15238
15239 cx.update_buffer(|buffer, _| {
15240 // This causes autoindent to be async.
15241 buffer.set_sync_parse_timeout(Duration::ZERO)
15242 });
15243
15244 cx.set_state("fn c() {\n d()ˇ\n}\n");
15245 cx.simulate_keystroke("\n");
15246 cx.run_until_parked();
15247
15248 let buffer_cloned =
15249 cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap().clone());
15250 let mut request =
15251 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
15252 let buffer_cloned = buffer_cloned.clone();
15253 async move {
15254 buffer_cloned.update(&mut cx, |buffer, _| {
15255 assert_eq!(
15256 buffer.text(),
15257 "fn c() {\n d()\n .\n}\n",
15258 "OnTypeFormatting should triggered after autoindent applied"
15259 )
15260 })?;
15261
15262 Ok(Some(vec![]))
15263 }
15264 });
15265
15266 cx.simulate_keystroke(".");
15267 cx.run_until_parked();
15268
15269 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
15270 assert!(request.next().await.is_some());
15271 request.close();
15272 assert!(request.next().await.is_none());
15273}
15274
15275#[gpui::test]
15276async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
15277 init_test(cx, |_| {});
15278
15279 let fs = FakeFs::new(cx.executor());
15280 fs.insert_tree(
15281 path!("/a"),
15282 json!({
15283 "main.rs": "fn main() { let a = 5; }",
15284 "other.rs": "// Test file",
15285 }),
15286 )
15287 .await;
15288
15289 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15290
15291 let server_restarts = Arc::new(AtomicUsize::new(0));
15292 let closure_restarts = Arc::clone(&server_restarts);
15293 let language_server_name = "test language server";
15294 let language_name: LanguageName = "Rust".into();
15295
15296 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15297 language_registry.add(Arc::new(Language::new(
15298 LanguageConfig {
15299 name: language_name.clone(),
15300 matcher: LanguageMatcher {
15301 path_suffixes: vec!["rs".to_string()],
15302 ..Default::default()
15303 },
15304 ..Default::default()
15305 },
15306 Some(tree_sitter_rust::LANGUAGE.into()),
15307 )));
15308 let mut fake_servers = language_registry.register_fake_lsp(
15309 "Rust",
15310 FakeLspAdapter {
15311 name: language_server_name,
15312 initialization_options: Some(json!({
15313 "testOptionValue": true
15314 })),
15315 initializer: Some(Box::new(move |fake_server| {
15316 let task_restarts = Arc::clone(&closure_restarts);
15317 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
15318 task_restarts.fetch_add(1, atomic::Ordering::Release);
15319 futures::future::ready(Ok(()))
15320 });
15321 })),
15322 ..Default::default()
15323 },
15324 );
15325
15326 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15327 let _buffer = project
15328 .update(cx, |project, cx| {
15329 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
15330 })
15331 .await
15332 .unwrap();
15333 let _fake_server = fake_servers.next().await.unwrap();
15334 update_test_language_settings(cx, |language_settings| {
15335 language_settings.languages.0.insert(
15336 language_name.clone(),
15337 LanguageSettingsContent {
15338 tab_size: NonZeroU32::new(8),
15339 ..Default::default()
15340 },
15341 );
15342 });
15343 cx.executor().run_until_parked();
15344 assert_eq!(
15345 server_restarts.load(atomic::Ordering::Acquire),
15346 0,
15347 "Should not restart LSP server on an unrelated change"
15348 );
15349
15350 update_test_project_settings(cx, |project_settings| {
15351 project_settings.lsp.insert(
15352 "Some other server name".into(),
15353 LspSettings {
15354 binary: None,
15355 settings: None,
15356 initialization_options: Some(json!({
15357 "some other init value": false
15358 })),
15359 enable_lsp_tasks: false,
15360 },
15361 );
15362 });
15363 cx.executor().run_until_parked();
15364 assert_eq!(
15365 server_restarts.load(atomic::Ordering::Acquire),
15366 0,
15367 "Should not restart LSP server on an unrelated LSP settings change"
15368 );
15369
15370 update_test_project_settings(cx, |project_settings| {
15371 project_settings.lsp.insert(
15372 language_server_name.into(),
15373 LspSettings {
15374 binary: None,
15375 settings: None,
15376 initialization_options: Some(json!({
15377 "anotherInitValue": false
15378 })),
15379 enable_lsp_tasks: false,
15380 },
15381 );
15382 });
15383 cx.executor().run_until_parked();
15384 assert_eq!(
15385 server_restarts.load(atomic::Ordering::Acquire),
15386 1,
15387 "Should restart LSP server on a related LSP settings change"
15388 );
15389
15390 update_test_project_settings(cx, |project_settings| {
15391 project_settings.lsp.insert(
15392 language_server_name.into(),
15393 LspSettings {
15394 binary: None,
15395 settings: None,
15396 initialization_options: Some(json!({
15397 "anotherInitValue": false
15398 })),
15399 enable_lsp_tasks: false,
15400 },
15401 );
15402 });
15403 cx.executor().run_until_parked();
15404 assert_eq!(
15405 server_restarts.load(atomic::Ordering::Acquire),
15406 1,
15407 "Should not restart LSP server on a related LSP settings change that is the same"
15408 );
15409
15410 update_test_project_settings(cx, |project_settings| {
15411 project_settings.lsp.insert(
15412 language_server_name.into(),
15413 LspSettings {
15414 binary: None,
15415 settings: None,
15416 initialization_options: None,
15417 enable_lsp_tasks: false,
15418 },
15419 );
15420 });
15421 cx.executor().run_until_parked();
15422 assert_eq!(
15423 server_restarts.load(atomic::Ordering::Acquire),
15424 2,
15425 "Should restart LSP server on another related LSP settings change"
15426 );
15427}
15428
15429#[gpui::test]
15430async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
15431 init_test(cx, |_| {});
15432
15433 let mut cx = EditorLspTestContext::new_rust(
15434 lsp::ServerCapabilities {
15435 completion_provider: Some(lsp::CompletionOptions {
15436 trigger_characters: Some(vec![".".to_string()]),
15437 resolve_provider: Some(true),
15438 ..Default::default()
15439 }),
15440 ..Default::default()
15441 },
15442 cx,
15443 )
15444 .await;
15445
15446 cx.set_state("fn main() { let a = 2ˇ; }");
15447 cx.simulate_keystroke(".");
15448 let completion_item = lsp::CompletionItem {
15449 label: "some".into(),
15450 kind: Some(lsp::CompletionItemKind::SNIPPET),
15451 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15452 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15453 kind: lsp::MarkupKind::Markdown,
15454 value: "```rust\nSome(2)\n```".to_string(),
15455 })),
15456 deprecated: Some(false),
15457 sort_text: Some("fffffff2".to_string()),
15458 filter_text: Some("some".to_string()),
15459 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15460 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15461 range: lsp::Range {
15462 start: lsp::Position {
15463 line: 0,
15464 character: 22,
15465 },
15466 end: lsp::Position {
15467 line: 0,
15468 character: 22,
15469 },
15470 },
15471 new_text: "Some(2)".to_string(),
15472 })),
15473 additional_text_edits: Some(vec![lsp::TextEdit {
15474 range: lsp::Range {
15475 start: lsp::Position {
15476 line: 0,
15477 character: 20,
15478 },
15479 end: lsp::Position {
15480 line: 0,
15481 character: 22,
15482 },
15483 },
15484 new_text: "".to_string(),
15485 }]),
15486 ..Default::default()
15487 };
15488
15489 let closure_completion_item = completion_item.clone();
15490 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15491 let task_completion_item = closure_completion_item.clone();
15492 async move {
15493 Ok(Some(lsp::CompletionResponse::Array(vec![
15494 task_completion_item,
15495 ])))
15496 }
15497 });
15498
15499 request.next().await;
15500
15501 cx.condition(|editor, _| editor.context_menu_visible())
15502 .await;
15503 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15504 editor
15505 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15506 .unwrap()
15507 });
15508 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
15509
15510 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
15511 let task_completion_item = completion_item.clone();
15512 async move { Ok(task_completion_item) }
15513 })
15514 .next()
15515 .await
15516 .unwrap();
15517 apply_additional_edits.await.unwrap();
15518 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
15519}
15520
15521#[gpui::test]
15522async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
15523 init_test(cx, |_| {});
15524
15525 let mut cx = EditorLspTestContext::new_rust(
15526 lsp::ServerCapabilities {
15527 completion_provider: Some(lsp::CompletionOptions {
15528 trigger_characters: Some(vec![".".to_string()]),
15529 resolve_provider: Some(true),
15530 ..Default::default()
15531 }),
15532 ..Default::default()
15533 },
15534 cx,
15535 )
15536 .await;
15537
15538 cx.set_state("fn main() { let a = 2ˇ; }");
15539 cx.simulate_keystroke(".");
15540
15541 let item1 = lsp::CompletionItem {
15542 label: "method id()".to_string(),
15543 filter_text: Some("id".to_string()),
15544 detail: None,
15545 documentation: None,
15546 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15547 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15548 new_text: ".id".to_string(),
15549 })),
15550 ..lsp::CompletionItem::default()
15551 };
15552
15553 let item2 = lsp::CompletionItem {
15554 label: "other".to_string(),
15555 filter_text: Some("other".to_string()),
15556 detail: None,
15557 documentation: None,
15558 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15559 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15560 new_text: ".other".to_string(),
15561 })),
15562 ..lsp::CompletionItem::default()
15563 };
15564
15565 let item1 = item1.clone();
15566 cx.set_request_handler::<lsp::request::Completion, _, _>({
15567 let item1 = item1.clone();
15568 move |_, _, _| {
15569 let item1 = item1.clone();
15570 let item2 = item2.clone();
15571 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
15572 }
15573 })
15574 .next()
15575 .await;
15576
15577 cx.condition(|editor, _| editor.context_menu_visible())
15578 .await;
15579 cx.update_editor(|editor, _, _| {
15580 let context_menu = editor.context_menu.borrow_mut();
15581 let context_menu = context_menu
15582 .as_ref()
15583 .expect("Should have the context menu deployed");
15584 match context_menu {
15585 CodeContextMenu::Completions(completions_menu) => {
15586 let completions = completions_menu.completions.borrow_mut();
15587 assert_eq!(
15588 completions
15589 .iter()
15590 .map(|completion| &completion.label.text)
15591 .collect::<Vec<_>>(),
15592 vec!["method id()", "other"]
15593 )
15594 }
15595 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15596 }
15597 });
15598
15599 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
15600 let item1 = item1.clone();
15601 move |_, item_to_resolve, _| {
15602 let item1 = item1.clone();
15603 async move {
15604 if item1 == item_to_resolve {
15605 Ok(lsp::CompletionItem {
15606 label: "method id()".to_string(),
15607 filter_text: Some("id".to_string()),
15608 detail: Some("Now resolved!".to_string()),
15609 documentation: Some(lsp::Documentation::String("Docs".to_string())),
15610 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15611 range: lsp::Range::new(
15612 lsp::Position::new(0, 22),
15613 lsp::Position::new(0, 22),
15614 ),
15615 new_text: ".id".to_string(),
15616 })),
15617 ..lsp::CompletionItem::default()
15618 })
15619 } else {
15620 Ok(item_to_resolve)
15621 }
15622 }
15623 }
15624 })
15625 .next()
15626 .await
15627 .unwrap();
15628 cx.run_until_parked();
15629
15630 cx.update_editor(|editor, window, cx| {
15631 editor.context_menu_next(&Default::default(), window, cx);
15632 });
15633
15634 cx.update_editor(|editor, _, _| {
15635 let context_menu = editor.context_menu.borrow_mut();
15636 let context_menu = context_menu
15637 .as_ref()
15638 .expect("Should have the context menu deployed");
15639 match context_menu {
15640 CodeContextMenu::Completions(completions_menu) => {
15641 let completions = completions_menu.completions.borrow_mut();
15642 assert_eq!(
15643 completions
15644 .iter()
15645 .map(|completion| &completion.label.text)
15646 .collect::<Vec<_>>(),
15647 vec!["method id() Now resolved!", "other"],
15648 "Should update first completion label, but not second as the filter text did not match."
15649 );
15650 }
15651 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15652 }
15653 });
15654}
15655
15656#[gpui::test]
15657async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
15658 init_test(cx, |_| {});
15659 let mut cx = EditorLspTestContext::new_rust(
15660 lsp::ServerCapabilities {
15661 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
15662 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
15663 completion_provider: Some(lsp::CompletionOptions {
15664 resolve_provider: Some(true),
15665 ..Default::default()
15666 }),
15667 ..Default::default()
15668 },
15669 cx,
15670 )
15671 .await;
15672 cx.set_state(indoc! {"
15673 struct TestStruct {
15674 field: i32
15675 }
15676
15677 fn mainˇ() {
15678 let unused_var = 42;
15679 let test_struct = TestStruct { field: 42 };
15680 }
15681 "});
15682 let symbol_range = cx.lsp_range(indoc! {"
15683 struct TestStruct {
15684 field: i32
15685 }
15686
15687 «fn main»() {
15688 let unused_var = 42;
15689 let test_struct = TestStruct { field: 42 };
15690 }
15691 "});
15692 let mut hover_requests =
15693 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
15694 Ok(Some(lsp::Hover {
15695 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
15696 kind: lsp::MarkupKind::Markdown,
15697 value: "Function documentation".to_string(),
15698 }),
15699 range: Some(symbol_range),
15700 }))
15701 });
15702
15703 // Case 1: Test that code action menu hide hover popover
15704 cx.dispatch_action(Hover);
15705 hover_requests.next().await;
15706 cx.condition(|editor, _| editor.hover_state.visible()).await;
15707 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
15708 move |_, _, _| async move {
15709 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
15710 lsp::CodeAction {
15711 title: "Remove unused variable".to_string(),
15712 kind: Some(CodeActionKind::QUICKFIX),
15713 edit: Some(lsp::WorkspaceEdit {
15714 changes: Some(
15715 [(
15716 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
15717 vec![lsp::TextEdit {
15718 range: lsp::Range::new(
15719 lsp::Position::new(5, 4),
15720 lsp::Position::new(5, 27),
15721 ),
15722 new_text: "".to_string(),
15723 }],
15724 )]
15725 .into_iter()
15726 .collect(),
15727 ),
15728 ..Default::default()
15729 }),
15730 ..Default::default()
15731 },
15732 )]))
15733 },
15734 );
15735 cx.update_editor(|editor, window, cx| {
15736 editor.toggle_code_actions(
15737 &ToggleCodeActions {
15738 deployed_from: None,
15739 quick_launch: false,
15740 },
15741 window,
15742 cx,
15743 );
15744 });
15745 code_action_requests.next().await;
15746 cx.run_until_parked();
15747 cx.condition(|editor, _| editor.context_menu_visible())
15748 .await;
15749 cx.update_editor(|editor, _, _| {
15750 assert!(
15751 !editor.hover_state.visible(),
15752 "Hover popover should be hidden when code action menu is shown"
15753 );
15754 // Hide code actions
15755 editor.context_menu.take();
15756 });
15757
15758 // Case 2: Test that code completions hide hover popover
15759 cx.dispatch_action(Hover);
15760 hover_requests.next().await;
15761 cx.condition(|editor, _| editor.hover_state.visible()).await;
15762 let counter = Arc::new(AtomicUsize::new(0));
15763 let mut completion_requests =
15764 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15765 let counter = counter.clone();
15766 async move {
15767 counter.fetch_add(1, atomic::Ordering::Release);
15768 Ok(Some(lsp::CompletionResponse::Array(vec![
15769 lsp::CompletionItem {
15770 label: "main".into(),
15771 kind: Some(lsp::CompletionItemKind::FUNCTION),
15772 detail: Some("() -> ()".to_string()),
15773 ..Default::default()
15774 },
15775 lsp::CompletionItem {
15776 label: "TestStruct".into(),
15777 kind: Some(lsp::CompletionItemKind::STRUCT),
15778 detail: Some("struct TestStruct".to_string()),
15779 ..Default::default()
15780 },
15781 ])))
15782 }
15783 });
15784 cx.update_editor(|editor, window, cx| {
15785 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15786 });
15787 completion_requests.next().await;
15788 cx.condition(|editor, _| editor.context_menu_visible())
15789 .await;
15790 cx.update_editor(|editor, _, _| {
15791 assert!(
15792 !editor.hover_state.visible(),
15793 "Hover popover should be hidden when completion menu is shown"
15794 );
15795 });
15796}
15797
15798#[gpui::test]
15799async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
15800 init_test(cx, |_| {});
15801
15802 let mut cx = EditorLspTestContext::new_rust(
15803 lsp::ServerCapabilities {
15804 completion_provider: Some(lsp::CompletionOptions {
15805 trigger_characters: Some(vec![".".to_string()]),
15806 resolve_provider: Some(true),
15807 ..Default::default()
15808 }),
15809 ..Default::default()
15810 },
15811 cx,
15812 )
15813 .await;
15814
15815 cx.set_state("fn main() { let a = 2ˇ; }");
15816 cx.simulate_keystroke(".");
15817
15818 let unresolved_item_1 = lsp::CompletionItem {
15819 label: "id".to_string(),
15820 filter_text: Some("id".to_string()),
15821 detail: None,
15822 documentation: None,
15823 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15824 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15825 new_text: ".id".to_string(),
15826 })),
15827 ..lsp::CompletionItem::default()
15828 };
15829 let resolved_item_1 = lsp::CompletionItem {
15830 additional_text_edits: Some(vec![lsp::TextEdit {
15831 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15832 new_text: "!!".to_string(),
15833 }]),
15834 ..unresolved_item_1.clone()
15835 };
15836 let unresolved_item_2 = lsp::CompletionItem {
15837 label: "other".to_string(),
15838 filter_text: Some("other".to_string()),
15839 detail: None,
15840 documentation: None,
15841 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15842 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
15843 new_text: ".other".to_string(),
15844 })),
15845 ..lsp::CompletionItem::default()
15846 };
15847 let resolved_item_2 = lsp::CompletionItem {
15848 additional_text_edits: Some(vec![lsp::TextEdit {
15849 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
15850 new_text: "??".to_string(),
15851 }]),
15852 ..unresolved_item_2.clone()
15853 };
15854
15855 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
15856 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
15857 cx.lsp
15858 .server
15859 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
15860 let unresolved_item_1 = unresolved_item_1.clone();
15861 let resolved_item_1 = resolved_item_1.clone();
15862 let unresolved_item_2 = unresolved_item_2.clone();
15863 let resolved_item_2 = resolved_item_2.clone();
15864 let resolve_requests_1 = resolve_requests_1.clone();
15865 let resolve_requests_2 = resolve_requests_2.clone();
15866 move |unresolved_request, _| {
15867 let unresolved_item_1 = unresolved_item_1.clone();
15868 let resolved_item_1 = resolved_item_1.clone();
15869 let unresolved_item_2 = unresolved_item_2.clone();
15870 let resolved_item_2 = resolved_item_2.clone();
15871 let resolve_requests_1 = resolve_requests_1.clone();
15872 let resolve_requests_2 = resolve_requests_2.clone();
15873 async move {
15874 if unresolved_request == unresolved_item_1 {
15875 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
15876 Ok(resolved_item_1.clone())
15877 } else if unresolved_request == unresolved_item_2 {
15878 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
15879 Ok(resolved_item_2.clone())
15880 } else {
15881 panic!("Unexpected completion item {unresolved_request:?}")
15882 }
15883 }
15884 }
15885 })
15886 .detach();
15887
15888 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15889 let unresolved_item_1 = unresolved_item_1.clone();
15890 let unresolved_item_2 = unresolved_item_2.clone();
15891 async move {
15892 Ok(Some(lsp::CompletionResponse::Array(vec![
15893 unresolved_item_1,
15894 unresolved_item_2,
15895 ])))
15896 }
15897 })
15898 .next()
15899 .await;
15900
15901 cx.condition(|editor, _| editor.context_menu_visible())
15902 .await;
15903 cx.update_editor(|editor, _, _| {
15904 let context_menu = editor.context_menu.borrow_mut();
15905 let context_menu = context_menu
15906 .as_ref()
15907 .expect("Should have the context menu deployed");
15908 match context_menu {
15909 CodeContextMenu::Completions(completions_menu) => {
15910 let completions = completions_menu.completions.borrow_mut();
15911 assert_eq!(
15912 completions
15913 .iter()
15914 .map(|completion| &completion.label.text)
15915 .collect::<Vec<_>>(),
15916 vec!["id", "other"]
15917 )
15918 }
15919 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
15920 }
15921 });
15922 cx.run_until_parked();
15923
15924 cx.update_editor(|editor, window, cx| {
15925 editor.context_menu_next(&ContextMenuNext, window, cx);
15926 });
15927 cx.run_until_parked();
15928 cx.update_editor(|editor, window, cx| {
15929 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
15930 });
15931 cx.run_until_parked();
15932 cx.update_editor(|editor, window, cx| {
15933 editor.context_menu_next(&ContextMenuNext, window, cx);
15934 });
15935 cx.run_until_parked();
15936 cx.update_editor(|editor, window, cx| {
15937 editor
15938 .compose_completion(&ComposeCompletion::default(), window, cx)
15939 .expect("No task returned")
15940 })
15941 .await
15942 .expect("Completion failed");
15943 cx.run_until_parked();
15944
15945 cx.update_editor(|editor, _, cx| {
15946 assert_eq!(
15947 resolve_requests_1.load(atomic::Ordering::Acquire),
15948 1,
15949 "Should always resolve once despite multiple selections"
15950 );
15951 assert_eq!(
15952 resolve_requests_2.load(atomic::Ordering::Acquire),
15953 1,
15954 "Should always resolve once after multiple selections and applying the completion"
15955 );
15956 assert_eq!(
15957 editor.text(cx),
15958 "fn main() { let a = ??.other; }",
15959 "Should use resolved data when applying the completion"
15960 );
15961 });
15962}
15963
15964#[gpui::test]
15965async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
15966 init_test(cx, |_| {});
15967
15968 let item_0 = lsp::CompletionItem {
15969 label: "abs".into(),
15970 insert_text: Some("abs".into()),
15971 data: Some(json!({ "very": "special"})),
15972 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
15973 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
15974 lsp::InsertReplaceEdit {
15975 new_text: "abs".to_string(),
15976 insert: lsp::Range::default(),
15977 replace: lsp::Range::default(),
15978 },
15979 )),
15980 ..lsp::CompletionItem::default()
15981 };
15982 let items = iter::once(item_0.clone())
15983 .chain((11..51).map(|i| lsp::CompletionItem {
15984 label: format!("item_{}", i),
15985 insert_text: Some(format!("item_{}", i)),
15986 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15987 ..lsp::CompletionItem::default()
15988 }))
15989 .collect::<Vec<_>>();
15990
15991 let default_commit_characters = vec!["?".to_string()];
15992 let default_data = json!({ "default": "data"});
15993 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
15994 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
15995 let default_edit_range = lsp::Range {
15996 start: lsp::Position {
15997 line: 0,
15998 character: 5,
15999 },
16000 end: lsp::Position {
16001 line: 0,
16002 character: 5,
16003 },
16004 };
16005
16006 let mut cx = EditorLspTestContext::new_rust(
16007 lsp::ServerCapabilities {
16008 completion_provider: Some(lsp::CompletionOptions {
16009 trigger_characters: Some(vec![".".to_string()]),
16010 resolve_provider: Some(true),
16011 ..Default::default()
16012 }),
16013 ..Default::default()
16014 },
16015 cx,
16016 )
16017 .await;
16018
16019 cx.set_state("fn main() { let a = 2ˇ; }");
16020 cx.simulate_keystroke(".");
16021
16022 let completion_data = default_data.clone();
16023 let completion_characters = default_commit_characters.clone();
16024 let completion_items = items.clone();
16025 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16026 let default_data = completion_data.clone();
16027 let default_commit_characters = completion_characters.clone();
16028 let items = completion_items.clone();
16029 async move {
16030 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16031 items,
16032 item_defaults: Some(lsp::CompletionListItemDefaults {
16033 data: Some(default_data.clone()),
16034 commit_characters: Some(default_commit_characters.clone()),
16035 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
16036 default_edit_range,
16037 )),
16038 insert_text_format: Some(default_insert_text_format),
16039 insert_text_mode: Some(default_insert_text_mode),
16040 }),
16041 ..lsp::CompletionList::default()
16042 })))
16043 }
16044 })
16045 .next()
16046 .await;
16047
16048 let resolved_items = Arc::new(Mutex::new(Vec::new()));
16049 cx.lsp
16050 .server
16051 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
16052 let closure_resolved_items = resolved_items.clone();
16053 move |item_to_resolve, _| {
16054 let closure_resolved_items = closure_resolved_items.clone();
16055 async move {
16056 closure_resolved_items.lock().push(item_to_resolve.clone());
16057 Ok(item_to_resolve)
16058 }
16059 }
16060 })
16061 .detach();
16062
16063 cx.condition(|editor, _| editor.context_menu_visible())
16064 .await;
16065 cx.run_until_parked();
16066 cx.update_editor(|editor, _, _| {
16067 let menu = editor.context_menu.borrow_mut();
16068 match menu.as_ref().expect("should have the completions menu") {
16069 CodeContextMenu::Completions(completions_menu) => {
16070 assert_eq!(
16071 completions_menu
16072 .entries
16073 .borrow()
16074 .iter()
16075 .map(|mat| mat.string.clone())
16076 .collect::<Vec<String>>(),
16077 items
16078 .iter()
16079 .map(|completion| completion.label.clone())
16080 .collect::<Vec<String>>()
16081 );
16082 }
16083 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
16084 }
16085 });
16086 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
16087 // with 4 from the end.
16088 assert_eq!(
16089 *resolved_items.lock(),
16090 [&items[0..16], &items[items.len() - 4..items.len()]]
16091 .concat()
16092 .iter()
16093 .cloned()
16094 .map(|mut item| {
16095 if item.data.is_none() {
16096 item.data = Some(default_data.clone());
16097 }
16098 item
16099 })
16100 .collect::<Vec<lsp::CompletionItem>>(),
16101 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
16102 );
16103 resolved_items.lock().clear();
16104
16105 cx.update_editor(|editor, window, cx| {
16106 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
16107 });
16108 cx.run_until_parked();
16109 // Completions that have already been resolved are skipped.
16110 assert_eq!(
16111 *resolved_items.lock(),
16112 items[items.len() - 17..items.len() - 4]
16113 .iter()
16114 .cloned()
16115 .map(|mut item| {
16116 if item.data.is_none() {
16117 item.data = Some(default_data.clone());
16118 }
16119 item
16120 })
16121 .collect::<Vec<lsp::CompletionItem>>()
16122 );
16123 resolved_items.lock().clear();
16124}
16125
16126#[gpui::test]
16127async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
16128 init_test(cx, |_| {});
16129
16130 let mut cx = EditorLspTestContext::new(
16131 Language::new(
16132 LanguageConfig {
16133 matcher: LanguageMatcher {
16134 path_suffixes: vec!["jsx".into()],
16135 ..Default::default()
16136 },
16137 overrides: [(
16138 "element".into(),
16139 LanguageConfigOverride {
16140 completion_query_characters: Override::Set(['-'].into_iter().collect()),
16141 ..Default::default()
16142 },
16143 )]
16144 .into_iter()
16145 .collect(),
16146 ..Default::default()
16147 },
16148 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16149 )
16150 .with_override_query("(jsx_self_closing_element) @element")
16151 .unwrap(),
16152 lsp::ServerCapabilities {
16153 completion_provider: Some(lsp::CompletionOptions {
16154 trigger_characters: Some(vec![":".to_string()]),
16155 ..Default::default()
16156 }),
16157 ..Default::default()
16158 },
16159 cx,
16160 )
16161 .await;
16162
16163 cx.lsp
16164 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16165 Ok(Some(lsp::CompletionResponse::Array(vec![
16166 lsp::CompletionItem {
16167 label: "bg-blue".into(),
16168 ..Default::default()
16169 },
16170 lsp::CompletionItem {
16171 label: "bg-red".into(),
16172 ..Default::default()
16173 },
16174 lsp::CompletionItem {
16175 label: "bg-yellow".into(),
16176 ..Default::default()
16177 },
16178 ])))
16179 });
16180
16181 cx.set_state(r#"<p class="bgˇ" />"#);
16182
16183 // Trigger completion when typing a dash, because the dash is an extra
16184 // word character in the 'element' scope, which contains the cursor.
16185 cx.simulate_keystroke("-");
16186 cx.executor().run_until_parked();
16187 cx.update_editor(|editor, _, _| {
16188 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16189 {
16190 assert_eq!(
16191 completion_menu_entries(&menu),
16192 &["bg-blue", "bg-red", "bg-yellow"]
16193 );
16194 } else {
16195 panic!("expected completion menu to be open");
16196 }
16197 });
16198
16199 cx.simulate_keystroke("l");
16200 cx.executor().run_until_parked();
16201 cx.update_editor(|editor, _, _| {
16202 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16203 {
16204 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
16205 } else {
16206 panic!("expected completion menu to be open");
16207 }
16208 });
16209
16210 // When filtering completions, consider the character after the '-' to
16211 // be the start of a subword.
16212 cx.set_state(r#"<p class="yelˇ" />"#);
16213 cx.simulate_keystroke("l");
16214 cx.executor().run_until_parked();
16215 cx.update_editor(|editor, _, _| {
16216 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16217 {
16218 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
16219 } else {
16220 panic!("expected completion menu to be open");
16221 }
16222 });
16223}
16224
16225fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
16226 let entries = menu.entries.borrow();
16227 entries.iter().map(|mat| mat.string.clone()).collect()
16228}
16229
16230#[gpui::test]
16231async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
16232 init_test(cx, |settings| {
16233 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
16234 Formatter::Prettier,
16235 )))
16236 });
16237
16238 let fs = FakeFs::new(cx.executor());
16239 fs.insert_file(path!("/file.ts"), Default::default()).await;
16240
16241 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
16242 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16243
16244 language_registry.add(Arc::new(Language::new(
16245 LanguageConfig {
16246 name: "TypeScript".into(),
16247 matcher: LanguageMatcher {
16248 path_suffixes: vec!["ts".to_string()],
16249 ..Default::default()
16250 },
16251 ..Default::default()
16252 },
16253 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
16254 )));
16255 update_test_language_settings(cx, |settings| {
16256 settings.defaults.prettier = Some(PrettierSettings {
16257 allowed: true,
16258 ..PrettierSettings::default()
16259 });
16260 });
16261
16262 let test_plugin = "test_plugin";
16263 let _ = language_registry.register_fake_lsp(
16264 "TypeScript",
16265 FakeLspAdapter {
16266 prettier_plugins: vec![test_plugin],
16267 ..Default::default()
16268 },
16269 );
16270
16271 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
16272 let buffer = project
16273 .update(cx, |project, cx| {
16274 project.open_local_buffer(path!("/file.ts"), cx)
16275 })
16276 .await
16277 .unwrap();
16278
16279 let buffer_text = "one\ntwo\nthree\n";
16280 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16281 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16282 editor.update_in(cx, |editor, window, cx| {
16283 editor.set_text(buffer_text, window, cx)
16284 });
16285
16286 editor
16287 .update_in(cx, |editor, window, cx| {
16288 editor.perform_format(
16289 project.clone(),
16290 FormatTrigger::Manual,
16291 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16292 window,
16293 cx,
16294 )
16295 })
16296 .unwrap()
16297 .await;
16298 assert_eq!(
16299 editor.update(cx, |editor, cx| editor.text(cx)),
16300 buffer_text.to_string() + prettier_format_suffix,
16301 "Test prettier formatting was not applied to the original buffer text",
16302 );
16303
16304 update_test_language_settings(cx, |settings| {
16305 settings.defaults.formatter = Some(SelectedFormatter::Auto)
16306 });
16307 let format = editor.update_in(cx, |editor, window, cx| {
16308 editor.perform_format(
16309 project.clone(),
16310 FormatTrigger::Manual,
16311 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
16312 window,
16313 cx,
16314 )
16315 });
16316 format.await.unwrap();
16317 assert_eq!(
16318 editor.update(cx, |editor, cx| editor.text(cx)),
16319 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
16320 "Autoformatting (via test prettier) was not applied to the original buffer text",
16321 );
16322}
16323
16324#[gpui::test]
16325async fn test_addition_reverts(cx: &mut TestAppContext) {
16326 init_test(cx, |_| {});
16327 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16328 let base_text = indoc! {r#"
16329 struct Row;
16330 struct Row1;
16331 struct Row2;
16332
16333 struct Row4;
16334 struct Row5;
16335 struct Row6;
16336
16337 struct Row8;
16338 struct Row9;
16339 struct Row10;"#};
16340
16341 // When addition hunks are not adjacent to carets, no hunk revert is performed
16342 assert_hunk_revert(
16343 indoc! {r#"struct Row;
16344 struct Row1;
16345 struct Row1.1;
16346 struct Row1.2;
16347 struct Row2;ˇ
16348
16349 struct Row4;
16350 struct Row5;
16351 struct Row6;
16352
16353 struct Row8;
16354 ˇstruct Row9;
16355 struct Row9.1;
16356 struct Row9.2;
16357 struct Row9.3;
16358 struct Row10;"#},
16359 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16360 indoc! {r#"struct Row;
16361 struct Row1;
16362 struct Row1.1;
16363 struct Row1.2;
16364 struct Row2;ˇ
16365
16366 struct Row4;
16367 struct Row5;
16368 struct Row6;
16369
16370 struct Row8;
16371 ˇstruct Row9;
16372 struct Row9.1;
16373 struct Row9.2;
16374 struct Row9.3;
16375 struct Row10;"#},
16376 base_text,
16377 &mut cx,
16378 );
16379 // Same for selections
16380 assert_hunk_revert(
16381 indoc! {r#"struct Row;
16382 struct Row1;
16383 struct Row2;
16384 struct Row2.1;
16385 struct Row2.2;
16386 «ˇ
16387 struct Row4;
16388 struct» Row5;
16389 «struct Row6;
16390 ˇ»
16391 struct Row9.1;
16392 struct Row9.2;
16393 struct Row9.3;
16394 struct Row8;
16395 struct Row9;
16396 struct Row10;"#},
16397 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
16398 indoc! {r#"struct Row;
16399 struct Row1;
16400 struct Row2;
16401 struct Row2.1;
16402 struct Row2.2;
16403 «ˇ
16404 struct Row4;
16405 struct» Row5;
16406 «struct Row6;
16407 ˇ»
16408 struct Row9.1;
16409 struct Row9.2;
16410 struct Row9.3;
16411 struct Row8;
16412 struct Row9;
16413 struct Row10;"#},
16414 base_text,
16415 &mut cx,
16416 );
16417
16418 // When carets and selections intersect the addition hunks, those are reverted.
16419 // Adjacent carets got merged.
16420 assert_hunk_revert(
16421 indoc! {r#"struct Row;
16422 ˇ// something on the top
16423 struct Row1;
16424 struct Row2;
16425 struct Roˇw3.1;
16426 struct Row2.2;
16427 struct Row2.3;ˇ
16428
16429 struct Row4;
16430 struct ˇRow5.1;
16431 struct Row5.2;
16432 struct «Rowˇ»5.3;
16433 struct Row5;
16434 struct Row6;
16435 ˇ
16436 struct Row9.1;
16437 struct «Rowˇ»9.2;
16438 struct «ˇRow»9.3;
16439 struct Row8;
16440 struct Row9;
16441 «ˇ// something on bottom»
16442 struct Row10;"#},
16443 vec![
16444 DiffHunkStatusKind::Added,
16445 DiffHunkStatusKind::Added,
16446 DiffHunkStatusKind::Added,
16447 DiffHunkStatusKind::Added,
16448 DiffHunkStatusKind::Added,
16449 ],
16450 indoc! {r#"struct Row;
16451 ˇstruct Row1;
16452 struct Row2;
16453 ˇ
16454 struct Row4;
16455 ˇstruct Row5;
16456 struct Row6;
16457 ˇ
16458 ˇstruct Row8;
16459 struct Row9;
16460 ˇstruct Row10;"#},
16461 base_text,
16462 &mut cx,
16463 );
16464}
16465
16466#[gpui::test]
16467async fn test_modification_reverts(cx: &mut TestAppContext) {
16468 init_test(cx, |_| {});
16469 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16470 let base_text = indoc! {r#"
16471 struct Row;
16472 struct Row1;
16473 struct Row2;
16474
16475 struct Row4;
16476 struct Row5;
16477 struct Row6;
16478
16479 struct Row8;
16480 struct Row9;
16481 struct Row10;"#};
16482
16483 // Modification hunks behave the same as the addition ones.
16484 assert_hunk_revert(
16485 indoc! {r#"struct Row;
16486 struct Row1;
16487 struct Row33;
16488 ˇ
16489 struct Row4;
16490 struct Row5;
16491 struct Row6;
16492 ˇ
16493 struct Row99;
16494 struct Row9;
16495 struct Row10;"#},
16496 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16497 indoc! {r#"struct Row;
16498 struct Row1;
16499 struct Row33;
16500 ˇ
16501 struct Row4;
16502 struct Row5;
16503 struct Row6;
16504 ˇ
16505 struct Row99;
16506 struct Row9;
16507 struct Row10;"#},
16508 base_text,
16509 &mut cx,
16510 );
16511 assert_hunk_revert(
16512 indoc! {r#"struct Row;
16513 struct Row1;
16514 struct Row33;
16515 «ˇ
16516 struct Row4;
16517 struct» Row5;
16518 «struct Row6;
16519 ˇ»
16520 struct Row99;
16521 struct Row9;
16522 struct Row10;"#},
16523 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
16524 indoc! {r#"struct Row;
16525 struct Row1;
16526 struct Row33;
16527 «ˇ
16528 struct Row4;
16529 struct» Row5;
16530 «struct Row6;
16531 ˇ»
16532 struct Row99;
16533 struct Row9;
16534 struct Row10;"#},
16535 base_text,
16536 &mut cx,
16537 );
16538
16539 assert_hunk_revert(
16540 indoc! {r#"ˇstruct Row1.1;
16541 struct Row1;
16542 «ˇstr»uct Row22;
16543
16544 struct ˇRow44;
16545 struct Row5;
16546 struct «Rˇ»ow66;ˇ
16547
16548 «struˇ»ct Row88;
16549 struct Row9;
16550 struct Row1011;ˇ"#},
16551 vec![
16552 DiffHunkStatusKind::Modified,
16553 DiffHunkStatusKind::Modified,
16554 DiffHunkStatusKind::Modified,
16555 DiffHunkStatusKind::Modified,
16556 DiffHunkStatusKind::Modified,
16557 DiffHunkStatusKind::Modified,
16558 ],
16559 indoc! {r#"struct Row;
16560 ˇstruct Row1;
16561 struct Row2;
16562 ˇ
16563 struct Row4;
16564 ˇstruct Row5;
16565 struct Row6;
16566 ˇ
16567 struct Row8;
16568 ˇstruct Row9;
16569 struct Row10;ˇ"#},
16570 base_text,
16571 &mut cx,
16572 );
16573}
16574
16575#[gpui::test]
16576async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
16577 init_test(cx, |_| {});
16578 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16579 let base_text = indoc! {r#"
16580 one
16581
16582 two
16583 three
16584 "#};
16585
16586 cx.set_head_text(base_text);
16587 cx.set_state("\nˇ\n");
16588 cx.executor().run_until_parked();
16589 cx.update_editor(|editor, _window, cx| {
16590 editor.expand_selected_diff_hunks(cx);
16591 });
16592 cx.executor().run_until_parked();
16593 cx.update_editor(|editor, window, cx| {
16594 editor.backspace(&Default::default(), window, cx);
16595 });
16596 cx.run_until_parked();
16597 cx.assert_state_with_diff(
16598 indoc! {r#"
16599
16600 - two
16601 - threeˇ
16602 +
16603 "#}
16604 .to_string(),
16605 );
16606}
16607
16608#[gpui::test]
16609async fn test_deletion_reverts(cx: &mut TestAppContext) {
16610 init_test(cx, |_| {});
16611 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16612 let base_text = indoc! {r#"struct Row;
16613struct Row1;
16614struct Row2;
16615
16616struct Row4;
16617struct Row5;
16618struct Row6;
16619
16620struct Row8;
16621struct Row9;
16622struct Row10;"#};
16623
16624 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
16625 assert_hunk_revert(
16626 indoc! {r#"struct Row;
16627 struct Row2;
16628
16629 ˇstruct Row4;
16630 struct Row5;
16631 struct Row6;
16632 ˇ
16633 struct Row8;
16634 struct Row10;"#},
16635 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16636 indoc! {r#"struct Row;
16637 struct Row2;
16638
16639 ˇstruct Row4;
16640 struct Row5;
16641 struct Row6;
16642 ˇ
16643 struct Row8;
16644 struct Row10;"#},
16645 base_text,
16646 &mut cx,
16647 );
16648 assert_hunk_revert(
16649 indoc! {r#"struct Row;
16650 struct Row2;
16651
16652 «ˇstruct Row4;
16653 struct» Row5;
16654 «struct Row6;
16655 ˇ»
16656 struct Row8;
16657 struct Row10;"#},
16658 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16659 indoc! {r#"struct Row;
16660 struct Row2;
16661
16662 «ˇstruct Row4;
16663 struct» Row5;
16664 «struct Row6;
16665 ˇ»
16666 struct Row8;
16667 struct Row10;"#},
16668 base_text,
16669 &mut cx,
16670 );
16671
16672 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
16673 assert_hunk_revert(
16674 indoc! {r#"struct Row;
16675 ˇstruct Row2;
16676
16677 struct Row4;
16678 struct Row5;
16679 struct Row6;
16680
16681 struct Row8;ˇ
16682 struct Row10;"#},
16683 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
16684 indoc! {r#"struct Row;
16685 struct Row1;
16686 ˇstruct Row2;
16687
16688 struct Row4;
16689 struct Row5;
16690 struct Row6;
16691
16692 struct Row8;ˇ
16693 struct Row9;
16694 struct Row10;"#},
16695 base_text,
16696 &mut cx,
16697 );
16698 assert_hunk_revert(
16699 indoc! {r#"struct Row;
16700 struct Row2«ˇ;
16701 struct Row4;
16702 struct» Row5;
16703 «struct Row6;
16704
16705 struct Row8;ˇ»
16706 struct Row10;"#},
16707 vec![
16708 DiffHunkStatusKind::Deleted,
16709 DiffHunkStatusKind::Deleted,
16710 DiffHunkStatusKind::Deleted,
16711 ],
16712 indoc! {r#"struct Row;
16713 struct Row1;
16714 struct Row2«ˇ;
16715
16716 struct Row4;
16717 struct» Row5;
16718 «struct Row6;
16719
16720 struct Row8;ˇ»
16721 struct Row9;
16722 struct Row10;"#},
16723 base_text,
16724 &mut cx,
16725 );
16726}
16727
16728#[gpui::test]
16729async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
16730 init_test(cx, |_| {});
16731
16732 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
16733 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
16734 let base_text_3 =
16735 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
16736
16737 let text_1 = edit_first_char_of_every_line(base_text_1);
16738 let text_2 = edit_first_char_of_every_line(base_text_2);
16739 let text_3 = edit_first_char_of_every_line(base_text_3);
16740
16741 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
16742 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
16743 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
16744
16745 let multibuffer = cx.new(|cx| {
16746 let mut multibuffer = MultiBuffer::new(ReadWrite);
16747 multibuffer.push_excerpts(
16748 buffer_1.clone(),
16749 [
16750 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16751 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16752 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16753 ],
16754 cx,
16755 );
16756 multibuffer.push_excerpts(
16757 buffer_2.clone(),
16758 [
16759 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16760 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16761 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16762 ],
16763 cx,
16764 );
16765 multibuffer.push_excerpts(
16766 buffer_3.clone(),
16767 [
16768 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16769 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16770 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16771 ],
16772 cx,
16773 );
16774 multibuffer
16775 });
16776
16777 let fs = FakeFs::new(cx.executor());
16778 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
16779 let (editor, cx) = cx
16780 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
16781 editor.update_in(cx, |editor, _window, cx| {
16782 for (buffer, diff_base) in [
16783 (buffer_1.clone(), base_text_1),
16784 (buffer_2.clone(), base_text_2),
16785 (buffer_3.clone(), base_text_3),
16786 ] {
16787 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
16788 editor
16789 .buffer
16790 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
16791 }
16792 });
16793 cx.executor().run_until_parked();
16794
16795 editor.update_in(cx, |editor, window, cx| {
16796 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}");
16797 editor.select_all(&SelectAll, window, cx);
16798 editor.git_restore(&Default::default(), window, cx);
16799 });
16800 cx.executor().run_until_parked();
16801
16802 // When all ranges are selected, all buffer hunks are reverted.
16803 editor.update(cx, |editor, cx| {
16804 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");
16805 });
16806 buffer_1.update(cx, |buffer, _| {
16807 assert_eq!(buffer.text(), base_text_1);
16808 });
16809 buffer_2.update(cx, |buffer, _| {
16810 assert_eq!(buffer.text(), base_text_2);
16811 });
16812 buffer_3.update(cx, |buffer, _| {
16813 assert_eq!(buffer.text(), base_text_3);
16814 });
16815
16816 editor.update_in(cx, |editor, window, cx| {
16817 editor.undo(&Default::default(), window, cx);
16818 });
16819
16820 editor.update_in(cx, |editor, window, cx| {
16821 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16822 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
16823 });
16824 editor.git_restore(&Default::default(), window, cx);
16825 });
16826
16827 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
16828 // but not affect buffer_2 and its related excerpts.
16829 editor.update(cx, |editor, cx| {
16830 assert_eq!(
16831 editor.text(cx),
16832 "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}"
16833 );
16834 });
16835 buffer_1.update(cx, |buffer, _| {
16836 assert_eq!(buffer.text(), base_text_1);
16837 });
16838 buffer_2.update(cx, |buffer, _| {
16839 assert_eq!(
16840 buffer.text(),
16841 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
16842 );
16843 });
16844 buffer_3.update(cx, |buffer, _| {
16845 assert_eq!(
16846 buffer.text(),
16847 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
16848 );
16849 });
16850
16851 fn edit_first_char_of_every_line(text: &str) -> String {
16852 text.split('\n')
16853 .map(|line| format!("X{}", &line[1..]))
16854 .collect::<Vec<_>>()
16855 .join("\n")
16856 }
16857}
16858
16859#[gpui::test]
16860async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
16861 init_test(cx, |_| {});
16862
16863 let cols = 4;
16864 let rows = 10;
16865 let sample_text_1 = sample_text(rows, cols, 'a');
16866 assert_eq!(
16867 sample_text_1,
16868 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
16869 );
16870 let sample_text_2 = sample_text(rows, cols, 'l');
16871 assert_eq!(
16872 sample_text_2,
16873 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
16874 );
16875 let sample_text_3 = sample_text(rows, cols, 'v');
16876 assert_eq!(
16877 sample_text_3,
16878 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
16879 );
16880
16881 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
16882 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
16883 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
16884
16885 let multi_buffer = cx.new(|cx| {
16886 let mut multibuffer = MultiBuffer::new(ReadWrite);
16887 multibuffer.push_excerpts(
16888 buffer_1.clone(),
16889 [
16890 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16891 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16892 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16893 ],
16894 cx,
16895 );
16896 multibuffer.push_excerpts(
16897 buffer_2.clone(),
16898 [
16899 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16900 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16901 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16902 ],
16903 cx,
16904 );
16905 multibuffer.push_excerpts(
16906 buffer_3.clone(),
16907 [
16908 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16909 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16910 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16911 ],
16912 cx,
16913 );
16914 multibuffer
16915 });
16916
16917 let fs = FakeFs::new(cx.executor());
16918 fs.insert_tree(
16919 "/a",
16920 json!({
16921 "main.rs": sample_text_1,
16922 "other.rs": sample_text_2,
16923 "lib.rs": sample_text_3,
16924 }),
16925 )
16926 .await;
16927 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16928 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16929 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16930 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16931 Editor::new(
16932 EditorMode::full(),
16933 multi_buffer,
16934 Some(project.clone()),
16935 window,
16936 cx,
16937 )
16938 });
16939 let multibuffer_item_id = workspace
16940 .update(cx, |workspace, window, cx| {
16941 assert!(
16942 workspace.active_item(cx).is_none(),
16943 "active item should be None before the first item is added"
16944 );
16945 workspace.add_item_to_active_pane(
16946 Box::new(multi_buffer_editor.clone()),
16947 None,
16948 true,
16949 window,
16950 cx,
16951 );
16952 let active_item = workspace
16953 .active_item(cx)
16954 .expect("should have an active item after adding the multi buffer");
16955 assert!(
16956 !active_item.is_singleton(cx),
16957 "A multi buffer was expected to active after adding"
16958 );
16959 active_item.item_id()
16960 })
16961 .unwrap();
16962 cx.executor().run_until_parked();
16963
16964 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16965 editor.change_selections(
16966 SelectionEffects::scroll(Autoscroll::Next),
16967 window,
16968 cx,
16969 |s| s.select_ranges(Some(1..2)),
16970 );
16971 editor.open_excerpts(&OpenExcerpts, window, cx);
16972 });
16973 cx.executor().run_until_parked();
16974 let first_item_id = workspace
16975 .update(cx, |workspace, window, cx| {
16976 let active_item = workspace
16977 .active_item(cx)
16978 .expect("should have an active item after navigating into the 1st buffer");
16979 let first_item_id = active_item.item_id();
16980 assert_ne!(
16981 first_item_id, multibuffer_item_id,
16982 "Should navigate into the 1st buffer and activate it"
16983 );
16984 assert!(
16985 active_item.is_singleton(cx),
16986 "New active item should be a singleton buffer"
16987 );
16988 assert_eq!(
16989 active_item
16990 .act_as::<Editor>(cx)
16991 .expect("should have navigated into an editor for the 1st buffer")
16992 .read(cx)
16993 .text(cx),
16994 sample_text_1
16995 );
16996
16997 workspace
16998 .go_back(workspace.active_pane().downgrade(), window, cx)
16999 .detach_and_log_err(cx);
17000
17001 first_item_id
17002 })
17003 .unwrap();
17004 cx.executor().run_until_parked();
17005 workspace
17006 .update(cx, |workspace, _, cx| {
17007 let active_item = workspace
17008 .active_item(cx)
17009 .expect("should have an active item after navigating back");
17010 assert_eq!(
17011 active_item.item_id(),
17012 multibuffer_item_id,
17013 "Should navigate back to the multi buffer"
17014 );
17015 assert!(!active_item.is_singleton(cx));
17016 })
17017 .unwrap();
17018
17019 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17020 editor.change_selections(
17021 SelectionEffects::scroll(Autoscroll::Next),
17022 window,
17023 cx,
17024 |s| s.select_ranges(Some(39..40)),
17025 );
17026 editor.open_excerpts(&OpenExcerpts, window, cx);
17027 });
17028 cx.executor().run_until_parked();
17029 let second_item_id = workspace
17030 .update(cx, |workspace, window, cx| {
17031 let active_item = workspace
17032 .active_item(cx)
17033 .expect("should have an active item after navigating into the 2nd buffer");
17034 let second_item_id = active_item.item_id();
17035 assert_ne!(
17036 second_item_id, multibuffer_item_id,
17037 "Should navigate away from the multibuffer"
17038 );
17039 assert_ne!(
17040 second_item_id, first_item_id,
17041 "Should navigate into the 2nd buffer and activate it"
17042 );
17043 assert!(
17044 active_item.is_singleton(cx),
17045 "New active item should be a singleton buffer"
17046 );
17047 assert_eq!(
17048 active_item
17049 .act_as::<Editor>(cx)
17050 .expect("should have navigated into an editor")
17051 .read(cx)
17052 .text(cx),
17053 sample_text_2
17054 );
17055
17056 workspace
17057 .go_back(workspace.active_pane().downgrade(), window, cx)
17058 .detach_and_log_err(cx);
17059
17060 second_item_id
17061 })
17062 .unwrap();
17063 cx.executor().run_until_parked();
17064 workspace
17065 .update(cx, |workspace, _, cx| {
17066 let active_item = workspace
17067 .active_item(cx)
17068 .expect("should have an active item after navigating back from the 2nd buffer");
17069 assert_eq!(
17070 active_item.item_id(),
17071 multibuffer_item_id,
17072 "Should navigate back from the 2nd buffer to the multi buffer"
17073 );
17074 assert!(!active_item.is_singleton(cx));
17075 })
17076 .unwrap();
17077
17078 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17079 editor.change_selections(
17080 SelectionEffects::scroll(Autoscroll::Next),
17081 window,
17082 cx,
17083 |s| s.select_ranges(Some(70..70)),
17084 );
17085 editor.open_excerpts(&OpenExcerpts, window, cx);
17086 });
17087 cx.executor().run_until_parked();
17088 workspace
17089 .update(cx, |workspace, window, cx| {
17090 let active_item = workspace
17091 .active_item(cx)
17092 .expect("should have an active item after navigating into the 3rd buffer");
17093 let third_item_id = active_item.item_id();
17094 assert_ne!(
17095 third_item_id, multibuffer_item_id,
17096 "Should navigate into the 3rd buffer and activate it"
17097 );
17098 assert_ne!(third_item_id, first_item_id);
17099 assert_ne!(third_item_id, second_item_id);
17100 assert!(
17101 active_item.is_singleton(cx),
17102 "New active item should be a singleton buffer"
17103 );
17104 assert_eq!(
17105 active_item
17106 .act_as::<Editor>(cx)
17107 .expect("should have navigated into an editor")
17108 .read(cx)
17109 .text(cx),
17110 sample_text_3
17111 );
17112
17113 workspace
17114 .go_back(workspace.active_pane().downgrade(), window, cx)
17115 .detach_and_log_err(cx);
17116 })
17117 .unwrap();
17118 cx.executor().run_until_parked();
17119 workspace
17120 .update(cx, |workspace, _, cx| {
17121 let active_item = workspace
17122 .active_item(cx)
17123 .expect("should have an active item after navigating back from the 3rd buffer");
17124 assert_eq!(
17125 active_item.item_id(),
17126 multibuffer_item_id,
17127 "Should navigate back from the 3rd buffer to the multi buffer"
17128 );
17129 assert!(!active_item.is_singleton(cx));
17130 })
17131 .unwrap();
17132}
17133
17134#[gpui::test]
17135async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17136 init_test(cx, |_| {});
17137
17138 let mut cx = EditorTestContext::new(cx).await;
17139
17140 let diff_base = r#"
17141 use some::mod;
17142
17143 const A: u32 = 42;
17144
17145 fn main() {
17146 println!("hello");
17147
17148 println!("world");
17149 }
17150 "#
17151 .unindent();
17152
17153 cx.set_state(
17154 &r#"
17155 use some::modified;
17156
17157 ˇ
17158 fn main() {
17159 println!("hello there");
17160
17161 println!("around the");
17162 println!("world");
17163 }
17164 "#
17165 .unindent(),
17166 );
17167
17168 cx.set_head_text(&diff_base);
17169 executor.run_until_parked();
17170
17171 cx.update_editor(|editor, window, cx| {
17172 editor.go_to_next_hunk(&GoToHunk, window, cx);
17173 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17174 });
17175 executor.run_until_parked();
17176 cx.assert_state_with_diff(
17177 r#"
17178 use some::modified;
17179
17180
17181 fn main() {
17182 - println!("hello");
17183 + ˇ println!("hello there");
17184
17185 println!("around the");
17186 println!("world");
17187 }
17188 "#
17189 .unindent(),
17190 );
17191
17192 cx.update_editor(|editor, window, cx| {
17193 for _ in 0..2 {
17194 editor.go_to_next_hunk(&GoToHunk, window, cx);
17195 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17196 }
17197 });
17198 executor.run_until_parked();
17199 cx.assert_state_with_diff(
17200 r#"
17201 - use some::mod;
17202 + ˇuse some::modified;
17203
17204
17205 fn main() {
17206 - println!("hello");
17207 + println!("hello there");
17208
17209 + println!("around the");
17210 println!("world");
17211 }
17212 "#
17213 .unindent(),
17214 );
17215
17216 cx.update_editor(|editor, window, cx| {
17217 editor.go_to_next_hunk(&GoToHunk, window, cx);
17218 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17219 });
17220 executor.run_until_parked();
17221 cx.assert_state_with_diff(
17222 r#"
17223 - use some::mod;
17224 + use some::modified;
17225
17226 - const A: u32 = 42;
17227 ˇ
17228 fn main() {
17229 - println!("hello");
17230 + println!("hello there");
17231
17232 + println!("around the");
17233 println!("world");
17234 }
17235 "#
17236 .unindent(),
17237 );
17238
17239 cx.update_editor(|editor, window, cx| {
17240 editor.cancel(&Cancel, window, cx);
17241 });
17242
17243 cx.assert_state_with_diff(
17244 r#"
17245 use some::modified;
17246
17247 ˇ
17248 fn main() {
17249 println!("hello there");
17250
17251 println!("around the");
17252 println!("world");
17253 }
17254 "#
17255 .unindent(),
17256 );
17257}
17258
17259#[gpui::test]
17260async fn test_diff_base_change_with_expanded_diff_hunks(
17261 executor: BackgroundExecutor,
17262 cx: &mut TestAppContext,
17263) {
17264 init_test(cx, |_| {});
17265
17266 let mut cx = EditorTestContext::new(cx).await;
17267
17268 let diff_base = r#"
17269 use some::mod1;
17270 use some::mod2;
17271
17272 const A: u32 = 42;
17273 const B: u32 = 42;
17274 const C: u32 = 42;
17275
17276 fn main() {
17277 println!("hello");
17278
17279 println!("world");
17280 }
17281 "#
17282 .unindent();
17283
17284 cx.set_state(
17285 &r#"
17286 use some::mod2;
17287
17288 const A: u32 = 42;
17289 const C: u32 = 42;
17290
17291 fn main(ˇ) {
17292 //println!("hello");
17293
17294 println!("world");
17295 //
17296 //
17297 }
17298 "#
17299 .unindent(),
17300 );
17301
17302 cx.set_head_text(&diff_base);
17303 executor.run_until_parked();
17304
17305 cx.update_editor(|editor, window, cx| {
17306 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17307 });
17308 executor.run_until_parked();
17309 cx.assert_state_with_diff(
17310 r#"
17311 - use some::mod1;
17312 use some::mod2;
17313
17314 const A: u32 = 42;
17315 - const B: u32 = 42;
17316 const C: u32 = 42;
17317
17318 fn main(ˇ) {
17319 - println!("hello");
17320 + //println!("hello");
17321
17322 println!("world");
17323 + //
17324 + //
17325 }
17326 "#
17327 .unindent(),
17328 );
17329
17330 cx.set_head_text("new diff base!");
17331 executor.run_until_parked();
17332 cx.assert_state_with_diff(
17333 r#"
17334 - new diff base!
17335 + use some::mod2;
17336 +
17337 + const A: u32 = 42;
17338 + const C: u32 = 42;
17339 +
17340 + fn main(ˇ) {
17341 + //println!("hello");
17342 +
17343 + println!("world");
17344 + //
17345 + //
17346 + }
17347 "#
17348 .unindent(),
17349 );
17350}
17351
17352#[gpui::test]
17353async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
17354 init_test(cx, |_| {});
17355
17356 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17357 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
17358 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17359 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
17360 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
17361 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
17362
17363 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
17364 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
17365 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
17366
17367 let multi_buffer = cx.new(|cx| {
17368 let mut multibuffer = MultiBuffer::new(ReadWrite);
17369 multibuffer.push_excerpts(
17370 buffer_1.clone(),
17371 [
17372 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17373 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17374 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17375 ],
17376 cx,
17377 );
17378 multibuffer.push_excerpts(
17379 buffer_2.clone(),
17380 [
17381 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17382 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17383 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17384 ],
17385 cx,
17386 );
17387 multibuffer.push_excerpts(
17388 buffer_3.clone(),
17389 [
17390 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17391 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17392 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
17393 ],
17394 cx,
17395 );
17396 multibuffer
17397 });
17398
17399 let editor =
17400 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17401 editor
17402 .update(cx, |editor, _window, cx| {
17403 for (buffer, diff_base) in [
17404 (buffer_1.clone(), file_1_old),
17405 (buffer_2.clone(), file_2_old),
17406 (buffer_3.clone(), file_3_old),
17407 ] {
17408 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
17409 editor
17410 .buffer
17411 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
17412 }
17413 })
17414 .unwrap();
17415
17416 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17417 cx.run_until_parked();
17418
17419 cx.assert_editor_state(
17420 &"
17421 ˇaaa
17422 ccc
17423 ddd
17424
17425 ggg
17426 hhh
17427
17428
17429 lll
17430 mmm
17431 NNN
17432
17433 qqq
17434 rrr
17435
17436 uuu
17437 111
17438 222
17439 333
17440
17441 666
17442 777
17443
17444 000
17445 !!!"
17446 .unindent(),
17447 );
17448
17449 cx.update_editor(|editor, window, cx| {
17450 editor.select_all(&SelectAll, window, cx);
17451 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
17452 });
17453 cx.executor().run_until_parked();
17454
17455 cx.assert_state_with_diff(
17456 "
17457 «aaa
17458 - bbb
17459 ccc
17460 ddd
17461
17462 ggg
17463 hhh
17464
17465
17466 lll
17467 mmm
17468 - nnn
17469 + NNN
17470
17471 qqq
17472 rrr
17473
17474 uuu
17475 111
17476 222
17477 333
17478
17479 + 666
17480 777
17481
17482 000
17483 !!!ˇ»"
17484 .unindent(),
17485 );
17486}
17487
17488#[gpui::test]
17489async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
17490 init_test(cx, |_| {});
17491
17492 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
17493 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
17494
17495 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
17496 let multi_buffer = cx.new(|cx| {
17497 let mut multibuffer = MultiBuffer::new(ReadWrite);
17498 multibuffer.push_excerpts(
17499 buffer.clone(),
17500 [
17501 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
17502 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
17503 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
17504 ],
17505 cx,
17506 );
17507 multibuffer
17508 });
17509
17510 let editor =
17511 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
17512 editor
17513 .update(cx, |editor, _window, cx| {
17514 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
17515 editor
17516 .buffer
17517 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
17518 })
17519 .unwrap();
17520
17521 let mut cx = EditorTestContext::for_editor(editor, cx).await;
17522 cx.run_until_parked();
17523
17524 cx.update_editor(|editor, window, cx| {
17525 editor.expand_all_diff_hunks(&Default::default(), window, cx)
17526 });
17527 cx.executor().run_until_parked();
17528
17529 // When the start of a hunk coincides with the start of its excerpt,
17530 // the hunk is expanded. When the start of a a hunk is earlier than
17531 // the start of its excerpt, the hunk is not expanded.
17532 cx.assert_state_with_diff(
17533 "
17534 ˇaaa
17535 - bbb
17536 + BBB
17537
17538 - ddd
17539 - eee
17540 + DDD
17541 + EEE
17542 fff
17543
17544 iii
17545 "
17546 .unindent(),
17547 );
17548}
17549
17550#[gpui::test]
17551async fn test_edits_around_expanded_insertion_hunks(
17552 executor: BackgroundExecutor,
17553 cx: &mut TestAppContext,
17554) {
17555 init_test(cx, |_| {});
17556
17557 let mut cx = EditorTestContext::new(cx).await;
17558
17559 let diff_base = r#"
17560 use some::mod1;
17561 use some::mod2;
17562
17563 const A: u32 = 42;
17564
17565 fn main() {
17566 println!("hello");
17567
17568 println!("world");
17569 }
17570 "#
17571 .unindent();
17572 executor.run_until_parked();
17573 cx.set_state(
17574 &r#"
17575 use some::mod1;
17576 use some::mod2;
17577
17578 const A: u32 = 42;
17579 const B: u32 = 42;
17580 const C: u32 = 42;
17581 ˇ
17582
17583 fn main() {
17584 println!("hello");
17585
17586 println!("world");
17587 }
17588 "#
17589 .unindent(),
17590 );
17591
17592 cx.set_head_text(&diff_base);
17593 executor.run_until_parked();
17594
17595 cx.update_editor(|editor, window, cx| {
17596 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17597 });
17598 executor.run_until_parked();
17599
17600 cx.assert_state_with_diff(
17601 r#"
17602 use some::mod1;
17603 use some::mod2;
17604
17605 const A: u32 = 42;
17606 + const B: u32 = 42;
17607 + const C: u32 = 42;
17608 + ˇ
17609
17610 fn main() {
17611 println!("hello");
17612
17613 println!("world");
17614 }
17615 "#
17616 .unindent(),
17617 );
17618
17619 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
17620 executor.run_until_parked();
17621
17622 cx.assert_state_with_diff(
17623 r#"
17624 use some::mod1;
17625 use some::mod2;
17626
17627 const A: u32 = 42;
17628 + const B: u32 = 42;
17629 + const C: u32 = 42;
17630 + const D: u32 = 42;
17631 + ˇ
17632
17633 fn main() {
17634 println!("hello");
17635
17636 println!("world");
17637 }
17638 "#
17639 .unindent(),
17640 );
17641
17642 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
17643 executor.run_until_parked();
17644
17645 cx.assert_state_with_diff(
17646 r#"
17647 use some::mod1;
17648 use some::mod2;
17649
17650 const A: u32 = 42;
17651 + const B: u32 = 42;
17652 + const C: u32 = 42;
17653 + const D: u32 = 42;
17654 + const E: u32 = 42;
17655 + ˇ
17656
17657 fn main() {
17658 println!("hello");
17659
17660 println!("world");
17661 }
17662 "#
17663 .unindent(),
17664 );
17665
17666 cx.update_editor(|editor, window, cx| {
17667 editor.delete_line(&DeleteLine, window, cx);
17668 });
17669 executor.run_until_parked();
17670
17671 cx.assert_state_with_diff(
17672 r#"
17673 use some::mod1;
17674 use some::mod2;
17675
17676 const A: u32 = 42;
17677 + const B: u32 = 42;
17678 + const C: u32 = 42;
17679 + const D: u32 = 42;
17680 + const E: u32 = 42;
17681 ˇ
17682 fn main() {
17683 println!("hello");
17684
17685 println!("world");
17686 }
17687 "#
17688 .unindent(),
17689 );
17690
17691 cx.update_editor(|editor, window, cx| {
17692 editor.move_up(&MoveUp, window, cx);
17693 editor.delete_line(&DeleteLine, window, cx);
17694 editor.move_up(&MoveUp, window, cx);
17695 editor.delete_line(&DeleteLine, window, cx);
17696 editor.move_up(&MoveUp, window, cx);
17697 editor.delete_line(&DeleteLine, window, cx);
17698 });
17699 executor.run_until_parked();
17700 cx.assert_state_with_diff(
17701 r#"
17702 use some::mod1;
17703 use some::mod2;
17704
17705 const A: u32 = 42;
17706 + const B: u32 = 42;
17707 ˇ
17708 fn main() {
17709 println!("hello");
17710
17711 println!("world");
17712 }
17713 "#
17714 .unindent(),
17715 );
17716
17717 cx.update_editor(|editor, window, cx| {
17718 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
17719 editor.delete_line(&DeleteLine, window, cx);
17720 });
17721 executor.run_until_parked();
17722 cx.assert_state_with_diff(
17723 r#"
17724 ˇ
17725 fn main() {
17726 println!("hello");
17727
17728 println!("world");
17729 }
17730 "#
17731 .unindent(),
17732 );
17733}
17734
17735#[gpui::test]
17736async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
17737 init_test(cx, |_| {});
17738
17739 let mut cx = EditorTestContext::new(cx).await;
17740 cx.set_head_text(indoc! { "
17741 one
17742 two
17743 three
17744 four
17745 five
17746 "
17747 });
17748 cx.set_state(indoc! { "
17749 one
17750 ˇthree
17751 five
17752 "});
17753 cx.run_until_parked();
17754 cx.update_editor(|editor, window, cx| {
17755 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17756 });
17757 cx.assert_state_with_diff(
17758 indoc! { "
17759 one
17760 - two
17761 ˇthree
17762 - four
17763 five
17764 "}
17765 .to_string(),
17766 );
17767 cx.update_editor(|editor, window, cx| {
17768 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17769 });
17770
17771 cx.assert_state_with_diff(
17772 indoc! { "
17773 one
17774 ˇthree
17775 five
17776 "}
17777 .to_string(),
17778 );
17779
17780 cx.set_state(indoc! { "
17781 one
17782 ˇTWO
17783 three
17784 four
17785 five
17786 "});
17787 cx.run_until_parked();
17788 cx.update_editor(|editor, window, cx| {
17789 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17790 });
17791
17792 cx.assert_state_with_diff(
17793 indoc! { "
17794 one
17795 - two
17796 + ˇTWO
17797 three
17798 four
17799 five
17800 "}
17801 .to_string(),
17802 );
17803 cx.update_editor(|editor, window, cx| {
17804 editor.move_up(&Default::default(), window, cx);
17805 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
17806 });
17807 cx.assert_state_with_diff(
17808 indoc! { "
17809 one
17810 ˇTWO
17811 three
17812 four
17813 five
17814 "}
17815 .to_string(),
17816 );
17817}
17818
17819#[gpui::test]
17820async fn test_edits_around_expanded_deletion_hunks(
17821 executor: BackgroundExecutor,
17822 cx: &mut TestAppContext,
17823) {
17824 init_test(cx, |_| {});
17825
17826 let mut cx = EditorTestContext::new(cx).await;
17827
17828 let diff_base = r#"
17829 use some::mod1;
17830 use some::mod2;
17831
17832 const A: u32 = 42;
17833 const B: u32 = 42;
17834 const C: u32 = 42;
17835
17836
17837 fn main() {
17838 println!("hello");
17839
17840 println!("world");
17841 }
17842 "#
17843 .unindent();
17844 executor.run_until_parked();
17845 cx.set_state(
17846 &r#"
17847 use some::mod1;
17848 use some::mod2;
17849
17850 ˇconst B: u32 = 42;
17851 const C: u32 = 42;
17852
17853
17854 fn main() {
17855 println!("hello");
17856
17857 println!("world");
17858 }
17859 "#
17860 .unindent(),
17861 );
17862
17863 cx.set_head_text(&diff_base);
17864 executor.run_until_parked();
17865
17866 cx.update_editor(|editor, window, cx| {
17867 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17868 });
17869 executor.run_until_parked();
17870
17871 cx.assert_state_with_diff(
17872 r#"
17873 use some::mod1;
17874 use some::mod2;
17875
17876 - const A: u32 = 42;
17877 ˇconst B: u32 = 42;
17878 const C: u32 = 42;
17879
17880
17881 fn main() {
17882 println!("hello");
17883
17884 println!("world");
17885 }
17886 "#
17887 .unindent(),
17888 );
17889
17890 cx.update_editor(|editor, window, cx| {
17891 editor.delete_line(&DeleteLine, window, cx);
17892 });
17893 executor.run_until_parked();
17894 cx.assert_state_with_diff(
17895 r#"
17896 use some::mod1;
17897 use some::mod2;
17898
17899 - const A: u32 = 42;
17900 - const B: u32 = 42;
17901 ˇconst C: u32 = 42;
17902
17903
17904 fn main() {
17905 println!("hello");
17906
17907 println!("world");
17908 }
17909 "#
17910 .unindent(),
17911 );
17912
17913 cx.update_editor(|editor, window, cx| {
17914 editor.delete_line(&DeleteLine, window, cx);
17915 });
17916 executor.run_until_parked();
17917 cx.assert_state_with_diff(
17918 r#"
17919 use some::mod1;
17920 use some::mod2;
17921
17922 - const A: u32 = 42;
17923 - const B: u32 = 42;
17924 - const C: u32 = 42;
17925 ˇ
17926
17927 fn main() {
17928 println!("hello");
17929
17930 println!("world");
17931 }
17932 "#
17933 .unindent(),
17934 );
17935
17936 cx.update_editor(|editor, window, cx| {
17937 editor.handle_input("replacement", window, cx);
17938 });
17939 executor.run_until_parked();
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 -
17949 + replacementˇ
17950
17951 fn main() {
17952 println!("hello");
17953
17954 println!("world");
17955 }
17956 "#
17957 .unindent(),
17958 );
17959}
17960
17961#[gpui::test]
17962async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17963 init_test(cx, |_| {});
17964
17965 let mut cx = EditorTestContext::new(cx).await;
17966
17967 let base_text = r#"
17968 one
17969 two
17970 three
17971 four
17972 five
17973 "#
17974 .unindent();
17975 executor.run_until_parked();
17976 cx.set_state(
17977 &r#"
17978 one
17979 two
17980 fˇour
17981 five
17982 "#
17983 .unindent(),
17984 );
17985
17986 cx.set_head_text(&base_text);
17987 executor.run_until_parked();
17988
17989 cx.update_editor(|editor, window, cx| {
17990 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17991 });
17992 executor.run_until_parked();
17993
17994 cx.assert_state_with_diff(
17995 r#"
17996 one
17997 two
17998 - three
17999 fˇour
18000 five
18001 "#
18002 .unindent(),
18003 );
18004
18005 cx.update_editor(|editor, window, cx| {
18006 editor.backspace(&Backspace, window, cx);
18007 editor.backspace(&Backspace, window, cx);
18008 });
18009 executor.run_until_parked();
18010 cx.assert_state_with_diff(
18011 r#"
18012 one
18013 two
18014 - threeˇ
18015 - four
18016 + our
18017 five
18018 "#
18019 .unindent(),
18020 );
18021}
18022
18023#[gpui::test]
18024async fn test_edit_after_expanded_modification_hunk(
18025 executor: BackgroundExecutor,
18026 cx: &mut TestAppContext,
18027) {
18028 init_test(cx, |_| {});
18029
18030 let mut cx = EditorTestContext::new(cx).await;
18031
18032 let diff_base = r#"
18033 use some::mod1;
18034 use some::mod2;
18035
18036 const A: u32 = 42;
18037 const B: u32 = 42;
18038 const C: u32 = 42;
18039 const D: u32 = 42;
18040
18041
18042 fn main() {
18043 println!("hello");
18044
18045 println!("world");
18046 }"#
18047 .unindent();
18048
18049 cx.set_state(
18050 &r#"
18051 use some::mod1;
18052 use some::mod2;
18053
18054 const A: u32 = 42;
18055 const B: u32 = 42;
18056 const C: u32 = 43ˇ
18057 const D: u32 = 42;
18058
18059
18060 fn main() {
18061 println!("hello");
18062
18063 println!("world");
18064 }"#
18065 .unindent(),
18066 );
18067
18068 cx.set_head_text(&diff_base);
18069 executor.run_until_parked();
18070 cx.update_editor(|editor, window, cx| {
18071 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18072 });
18073 executor.run_until_parked();
18074
18075 cx.assert_state_with_diff(
18076 r#"
18077 use some::mod1;
18078 use some::mod2;
18079
18080 const A: u32 = 42;
18081 const B: u32 = 42;
18082 - const C: u32 = 42;
18083 + const C: u32 = 43ˇ
18084 const D: u32 = 42;
18085
18086
18087 fn main() {
18088 println!("hello");
18089
18090 println!("world");
18091 }"#
18092 .unindent(),
18093 );
18094
18095 cx.update_editor(|editor, window, cx| {
18096 editor.handle_input("\nnew_line\n", window, cx);
18097 });
18098 executor.run_until_parked();
18099
18100 cx.assert_state_with_diff(
18101 r#"
18102 use some::mod1;
18103 use some::mod2;
18104
18105 const A: u32 = 42;
18106 const B: u32 = 42;
18107 - const C: u32 = 42;
18108 + const C: u32 = 43
18109 + new_line
18110 + ˇ
18111 const D: u32 = 42;
18112
18113
18114 fn main() {
18115 println!("hello");
18116
18117 println!("world");
18118 }"#
18119 .unindent(),
18120 );
18121}
18122
18123#[gpui::test]
18124async fn test_stage_and_unstage_added_file_hunk(
18125 executor: BackgroundExecutor,
18126 cx: &mut TestAppContext,
18127) {
18128 init_test(cx, |_| {});
18129
18130 let mut cx = EditorTestContext::new(cx).await;
18131 cx.update_editor(|editor, _, cx| {
18132 editor.set_expand_all_diff_hunks(cx);
18133 });
18134
18135 let working_copy = r#"
18136 ˇfn main() {
18137 println!("hello, world!");
18138 }
18139 "#
18140 .unindent();
18141
18142 cx.set_state(&working_copy);
18143 executor.run_until_parked();
18144
18145 cx.assert_state_with_diff(
18146 r#"
18147 + ˇfn main() {
18148 + println!("hello, world!");
18149 + }
18150 "#
18151 .unindent(),
18152 );
18153 cx.assert_index_text(None);
18154
18155 cx.update_editor(|editor, window, cx| {
18156 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18157 });
18158 executor.run_until_parked();
18159 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
18160 cx.assert_state_with_diff(
18161 r#"
18162 + ˇfn main() {
18163 + println!("hello, world!");
18164 + }
18165 "#
18166 .unindent(),
18167 );
18168
18169 cx.update_editor(|editor, window, cx| {
18170 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
18171 });
18172 executor.run_until_parked();
18173 cx.assert_index_text(None);
18174}
18175
18176async fn setup_indent_guides_editor(
18177 text: &str,
18178 cx: &mut TestAppContext,
18179) -> (BufferId, EditorTestContext) {
18180 init_test(cx, |_| {});
18181
18182 let mut cx = EditorTestContext::new(cx).await;
18183
18184 let buffer_id = cx.update_editor(|editor, window, cx| {
18185 editor.set_text(text, window, cx);
18186 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
18187
18188 buffer_ids[0]
18189 });
18190
18191 (buffer_id, cx)
18192}
18193
18194fn assert_indent_guides(
18195 range: Range<u32>,
18196 expected: Vec<IndentGuide>,
18197 active_indices: Option<Vec<usize>>,
18198 cx: &mut EditorTestContext,
18199) {
18200 let indent_guides = cx.update_editor(|editor, window, cx| {
18201 let snapshot = editor.snapshot(window, cx).display_snapshot;
18202 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
18203 editor,
18204 MultiBufferRow(range.start)..MultiBufferRow(range.end),
18205 true,
18206 &snapshot,
18207 cx,
18208 );
18209
18210 indent_guides.sort_by(|a, b| {
18211 a.depth.cmp(&b.depth).then(
18212 a.start_row
18213 .cmp(&b.start_row)
18214 .then(a.end_row.cmp(&b.end_row)),
18215 )
18216 });
18217 indent_guides
18218 });
18219
18220 if let Some(expected) = active_indices {
18221 let active_indices = cx.update_editor(|editor, window, cx| {
18222 let snapshot = editor.snapshot(window, cx).display_snapshot;
18223 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
18224 });
18225
18226 assert_eq!(
18227 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
18228 expected,
18229 "Active indent guide indices do not match"
18230 );
18231 }
18232
18233 assert_eq!(indent_guides, expected, "Indent guides do not match");
18234}
18235
18236fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
18237 IndentGuide {
18238 buffer_id,
18239 start_row: MultiBufferRow(start_row),
18240 end_row: MultiBufferRow(end_row),
18241 depth,
18242 tab_size: 4,
18243 settings: IndentGuideSettings {
18244 enabled: true,
18245 line_width: 1,
18246 active_line_width: 1,
18247 ..Default::default()
18248 },
18249 }
18250}
18251
18252#[gpui::test]
18253async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
18254 let (buffer_id, mut cx) = setup_indent_guides_editor(
18255 &"
18256 fn main() {
18257 let a = 1;
18258 }"
18259 .unindent(),
18260 cx,
18261 )
18262 .await;
18263
18264 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18265}
18266
18267#[gpui::test]
18268async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
18269 let (buffer_id, mut cx) = setup_indent_guides_editor(
18270 &"
18271 fn main() {
18272 let a = 1;
18273 let b = 2;
18274 }"
18275 .unindent(),
18276 cx,
18277 )
18278 .await;
18279
18280 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
18281}
18282
18283#[gpui::test]
18284async fn test_indent_guide_nested(cx: &mut TestAppContext) {
18285 let (buffer_id, mut cx) = setup_indent_guides_editor(
18286 &"
18287 fn main() {
18288 let a = 1;
18289 if a == 3 {
18290 let b = 2;
18291 } else {
18292 let c = 3;
18293 }
18294 }"
18295 .unindent(),
18296 cx,
18297 )
18298 .await;
18299
18300 assert_indent_guides(
18301 0..8,
18302 vec![
18303 indent_guide(buffer_id, 1, 6, 0),
18304 indent_guide(buffer_id, 3, 3, 1),
18305 indent_guide(buffer_id, 5, 5, 1),
18306 ],
18307 None,
18308 &mut cx,
18309 );
18310}
18311
18312#[gpui::test]
18313async fn test_indent_guide_tab(cx: &mut TestAppContext) {
18314 let (buffer_id, mut cx) = setup_indent_guides_editor(
18315 &"
18316 fn main() {
18317 let a = 1;
18318 let b = 2;
18319 let c = 3;
18320 }"
18321 .unindent(),
18322 cx,
18323 )
18324 .await;
18325
18326 assert_indent_guides(
18327 0..5,
18328 vec![
18329 indent_guide(buffer_id, 1, 3, 0),
18330 indent_guide(buffer_id, 2, 2, 1),
18331 ],
18332 None,
18333 &mut cx,
18334 );
18335}
18336
18337#[gpui::test]
18338async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
18339 let (buffer_id, mut cx) = setup_indent_guides_editor(
18340 &"
18341 fn main() {
18342 let a = 1;
18343
18344 let c = 3;
18345 }"
18346 .unindent(),
18347 cx,
18348 )
18349 .await;
18350
18351 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
18352}
18353
18354#[gpui::test]
18355async fn test_indent_guide_complex(cx: &mut TestAppContext) {
18356 let (buffer_id, mut cx) = setup_indent_guides_editor(
18357 &"
18358 fn main() {
18359 let a = 1;
18360
18361 let c = 3;
18362
18363 if a == 3 {
18364 let b = 2;
18365 } else {
18366 let c = 3;
18367 }
18368 }"
18369 .unindent(),
18370 cx,
18371 )
18372 .await;
18373
18374 assert_indent_guides(
18375 0..11,
18376 vec![
18377 indent_guide(buffer_id, 1, 9, 0),
18378 indent_guide(buffer_id, 6, 6, 1),
18379 indent_guide(buffer_id, 8, 8, 1),
18380 ],
18381 None,
18382 &mut cx,
18383 );
18384}
18385
18386#[gpui::test]
18387async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
18388 let (buffer_id, mut cx) = setup_indent_guides_editor(
18389 &"
18390 fn main() {
18391 let a = 1;
18392
18393 let c = 3;
18394
18395 if a == 3 {
18396 let b = 2;
18397 } else {
18398 let c = 3;
18399 }
18400 }"
18401 .unindent(),
18402 cx,
18403 )
18404 .await;
18405
18406 assert_indent_guides(
18407 1..11,
18408 vec![
18409 indent_guide(buffer_id, 1, 9, 0),
18410 indent_guide(buffer_id, 6, 6, 1),
18411 indent_guide(buffer_id, 8, 8, 1),
18412 ],
18413 None,
18414 &mut cx,
18415 );
18416}
18417
18418#[gpui::test]
18419async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
18420 let (buffer_id, mut cx) = setup_indent_guides_editor(
18421 &"
18422 fn main() {
18423 let a = 1;
18424
18425 let c = 3;
18426
18427 if a == 3 {
18428 let b = 2;
18429 } else {
18430 let c = 3;
18431 }
18432 }"
18433 .unindent(),
18434 cx,
18435 )
18436 .await;
18437
18438 assert_indent_guides(
18439 1..10,
18440 vec![
18441 indent_guide(buffer_id, 1, 9, 0),
18442 indent_guide(buffer_id, 6, 6, 1),
18443 indent_guide(buffer_id, 8, 8, 1),
18444 ],
18445 None,
18446 &mut cx,
18447 );
18448}
18449
18450#[gpui::test]
18451async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
18452 let (buffer_id, mut cx) = setup_indent_guides_editor(
18453 &"
18454 fn main() {
18455 if a {
18456 b(
18457 c,
18458 d,
18459 )
18460 } else {
18461 e(
18462 f
18463 )
18464 }
18465 }"
18466 .unindent(),
18467 cx,
18468 )
18469 .await;
18470
18471 assert_indent_guides(
18472 0..11,
18473 vec![
18474 indent_guide(buffer_id, 1, 10, 0),
18475 indent_guide(buffer_id, 2, 5, 1),
18476 indent_guide(buffer_id, 7, 9, 1),
18477 indent_guide(buffer_id, 3, 4, 2),
18478 indent_guide(buffer_id, 8, 8, 2),
18479 ],
18480 None,
18481 &mut cx,
18482 );
18483
18484 cx.update_editor(|editor, window, cx| {
18485 editor.fold_at(MultiBufferRow(2), window, cx);
18486 assert_eq!(
18487 editor.display_text(cx),
18488 "
18489 fn main() {
18490 if a {
18491 b(⋯
18492 )
18493 } else {
18494 e(
18495 f
18496 )
18497 }
18498 }"
18499 .unindent()
18500 );
18501 });
18502
18503 assert_indent_guides(
18504 0..11,
18505 vec![
18506 indent_guide(buffer_id, 1, 10, 0),
18507 indent_guide(buffer_id, 2, 5, 1),
18508 indent_guide(buffer_id, 7, 9, 1),
18509 indent_guide(buffer_id, 8, 8, 2),
18510 ],
18511 None,
18512 &mut cx,
18513 );
18514}
18515
18516#[gpui::test]
18517async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
18518 let (buffer_id, mut cx) = setup_indent_guides_editor(
18519 &"
18520 block1
18521 block2
18522 block3
18523 block4
18524 block2
18525 block1
18526 block1"
18527 .unindent(),
18528 cx,
18529 )
18530 .await;
18531
18532 assert_indent_guides(
18533 1..10,
18534 vec![
18535 indent_guide(buffer_id, 1, 4, 0),
18536 indent_guide(buffer_id, 2, 3, 1),
18537 indent_guide(buffer_id, 3, 3, 2),
18538 ],
18539 None,
18540 &mut cx,
18541 );
18542}
18543
18544#[gpui::test]
18545async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
18546 let (buffer_id, mut cx) = setup_indent_guides_editor(
18547 &"
18548 block1
18549 block2
18550 block3
18551
18552 block1
18553 block1"
18554 .unindent(),
18555 cx,
18556 )
18557 .await;
18558
18559 assert_indent_guides(
18560 0..6,
18561 vec![
18562 indent_guide(buffer_id, 1, 2, 0),
18563 indent_guide(buffer_id, 2, 2, 1),
18564 ],
18565 None,
18566 &mut cx,
18567 );
18568}
18569
18570#[gpui::test]
18571async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
18572 let (buffer_id, mut cx) = setup_indent_guides_editor(
18573 &"
18574 function component() {
18575 \treturn (
18576 \t\t\t
18577 \t\t<div>
18578 \t\t\t<abc></abc>
18579 \t\t</div>
18580 \t)
18581 }"
18582 .unindent(),
18583 cx,
18584 )
18585 .await;
18586
18587 assert_indent_guides(
18588 0..8,
18589 vec![
18590 indent_guide(buffer_id, 1, 6, 0),
18591 indent_guide(buffer_id, 2, 5, 1),
18592 indent_guide(buffer_id, 4, 4, 2),
18593 ],
18594 None,
18595 &mut cx,
18596 );
18597}
18598
18599#[gpui::test]
18600async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
18601 let (buffer_id, mut cx) = setup_indent_guides_editor(
18602 &"
18603 function component() {
18604 \treturn (
18605 \t
18606 \t\t<div>
18607 \t\t\t<abc></abc>
18608 \t\t</div>
18609 \t)
18610 }"
18611 .unindent(),
18612 cx,
18613 )
18614 .await;
18615
18616 assert_indent_guides(
18617 0..8,
18618 vec![
18619 indent_guide(buffer_id, 1, 6, 0),
18620 indent_guide(buffer_id, 2, 5, 1),
18621 indent_guide(buffer_id, 4, 4, 2),
18622 ],
18623 None,
18624 &mut cx,
18625 );
18626}
18627
18628#[gpui::test]
18629async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
18630 let (buffer_id, mut cx) = setup_indent_guides_editor(
18631 &"
18632 block1
18633
18634
18635
18636 block2
18637 "
18638 .unindent(),
18639 cx,
18640 )
18641 .await;
18642
18643 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
18644}
18645
18646#[gpui::test]
18647async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
18648 let (buffer_id, mut cx) = setup_indent_guides_editor(
18649 &"
18650 def a:
18651 \tb = 3
18652 \tif True:
18653 \t\tc = 4
18654 \t\td = 5
18655 \tprint(b)
18656 "
18657 .unindent(),
18658 cx,
18659 )
18660 .await;
18661
18662 assert_indent_guides(
18663 0..6,
18664 vec![
18665 indent_guide(buffer_id, 1, 5, 0),
18666 indent_guide(buffer_id, 3, 4, 1),
18667 ],
18668 None,
18669 &mut cx,
18670 );
18671}
18672
18673#[gpui::test]
18674async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
18675 let (buffer_id, mut cx) = setup_indent_guides_editor(
18676 &"
18677 fn main() {
18678 let a = 1;
18679 }"
18680 .unindent(),
18681 cx,
18682 )
18683 .await;
18684
18685 cx.update_editor(|editor, window, cx| {
18686 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18687 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18688 });
18689 });
18690
18691 assert_indent_guides(
18692 0..3,
18693 vec![indent_guide(buffer_id, 1, 1, 0)],
18694 Some(vec![0]),
18695 &mut cx,
18696 );
18697}
18698
18699#[gpui::test]
18700async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
18701 let (buffer_id, mut cx) = setup_indent_guides_editor(
18702 &"
18703 fn main() {
18704 if 1 == 2 {
18705 let a = 1;
18706 }
18707 }"
18708 .unindent(),
18709 cx,
18710 )
18711 .await;
18712
18713 cx.update_editor(|editor, window, cx| {
18714 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18715 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18716 });
18717 });
18718
18719 assert_indent_guides(
18720 0..4,
18721 vec![
18722 indent_guide(buffer_id, 1, 3, 0),
18723 indent_guide(buffer_id, 2, 2, 1),
18724 ],
18725 Some(vec![1]),
18726 &mut cx,
18727 );
18728
18729 cx.update_editor(|editor, window, cx| {
18730 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18731 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18732 });
18733 });
18734
18735 assert_indent_guides(
18736 0..4,
18737 vec![
18738 indent_guide(buffer_id, 1, 3, 0),
18739 indent_guide(buffer_id, 2, 2, 1),
18740 ],
18741 Some(vec![1]),
18742 &mut cx,
18743 );
18744
18745 cx.update_editor(|editor, window, cx| {
18746 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18747 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
18748 });
18749 });
18750
18751 assert_indent_guides(
18752 0..4,
18753 vec![
18754 indent_guide(buffer_id, 1, 3, 0),
18755 indent_guide(buffer_id, 2, 2, 1),
18756 ],
18757 Some(vec![0]),
18758 &mut cx,
18759 );
18760}
18761
18762#[gpui::test]
18763async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
18764 let (buffer_id, mut cx) = setup_indent_guides_editor(
18765 &"
18766 fn main() {
18767 let a = 1;
18768
18769 let b = 2;
18770 }"
18771 .unindent(),
18772 cx,
18773 )
18774 .await;
18775
18776 cx.update_editor(|editor, window, cx| {
18777 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18778 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
18779 });
18780 });
18781
18782 assert_indent_guides(
18783 0..5,
18784 vec![indent_guide(buffer_id, 1, 3, 0)],
18785 Some(vec![0]),
18786 &mut cx,
18787 );
18788}
18789
18790#[gpui::test]
18791async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
18792 let (buffer_id, mut cx) = setup_indent_guides_editor(
18793 &"
18794 def m:
18795 a = 1
18796 pass"
18797 .unindent(),
18798 cx,
18799 )
18800 .await;
18801
18802 cx.update_editor(|editor, window, cx| {
18803 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18804 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
18805 });
18806 });
18807
18808 assert_indent_guides(
18809 0..3,
18810 vec![indent_guide(buffer_id, 1, 2, 0)],
18811 Some(vec![0]),
18812 &mut cx,
18813 );
18814}
18815
18816#[gpui::test]
18817async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
18818 init_test(cx, |_| {});
18819 let mut cx = EditorTestContext::new(cx).await;
18820 let text = indoc! {
18821 "
18822 impl A {
18823 fn b() {
18824 0;
18825 3;
18826 5;
18827 6;
18828 7;
18829 }
18830 }
18831 "
18832 };
18833 let base_text = indoc! {
18834 "
18835 impl A {
18836 fn b() {
18837 0;
18838 1;
18839 2;
18840 3;
18841 4;
18842 }
18843 fn c() {
18844 5;
18845 6;
18846 7;
18847 }
18848 }
18849 "
18850 };
18851
18852 cx.update_editor(|editor, window, cx| {
18853 editor.set_text(text, window, cx);
18854
18855 editor.buffer().update(cx, |multibuffer, cx| {
18856 let buffer = multibuffer.as_singleton().unwrap();
18857 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
18858
18859 multibuffer.set_all_diff_hunks_expanded(cx);
18860 multibuffer.add_diff(diff, cx);
18861
18862 buffer.read(cx).remote_id()
18863 })
18864 });
18865 cx.run_until_parked();
18866
18867 cx.assert_state_with_diff(
18868 indoc! { "
18869 impl A {
18870 fn b() {
18871 0;
18872 - 1;
18873 - 2;
18874 3;
18875 - 4;
18876 - }
18877 - fn c() {
18878 5;
18879 6;
18880 7;
18881 }
18882 }
18883 ˇ"
18884 }
18885 .to_string(),
18886 );
18887
18888 let mut actual_guides = cx.update_editor(|editor, window, cx| {
18889 editor
18890 .snapshot(window, cx)
18891 .buffer_snapshot
18892 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
18893 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
18894 .collect::<Vec<_>>()
18895 });
18896 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
18897 assert_eq!(
18898 actual_guides,
18899 vec![
18900 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
18901 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
18902 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
18903 ]
18904 );
18905}
18906
18907#[gpui::test]
18908async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18909 init_test(cx, |_| {});
18910 let mut cx = EditorTestContext::new(cx).await;
18911
18912 let diff_base = r#"
18913 a
18914 b
18915 c
18916 "#
18917 .unindent();
18918
18919 cx.set_state(
18920 &r#"
18921 ˇA
18922 b
18923 C
18924 "#
18925 .unindent(),
18926 );
18927 cx.set_head_text(&diff_base);
18928 cx.update_editor(|editor, window, cx| {
18929 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
18930 });
18931 executor.run_until_parked();
18932
18933 let both_hunks_expanded = r#"
18934 - a
18935 + ˇA
18936 b
18937 - c
18938 + C
18939 "#
18940 .unindent();
18941
18942 cx.assert_state_with_diff(both_hunks_expanded.clone());
18943
18944 let hunk_ranges = cx.update_editor(|editor, window, cx| {
18945 let snapshot = editor.snapshot(window, cx);
18946 let hunks = editor
18947 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
18948 .collect::<Vec<_>>();
18949 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
18950 let buffer_id = hunks[0].buffer_id;
18951 hunks
18952 .into_iter()
18953 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
18954 .collect::<Vec<_>>()
18955 });
18956 assert_eq!(hunk_ranges.len(), 2);
18957
18958 cx.update_editor(|editor, _, cx| {
18959 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18960 });
18961 executor.run_until_parked();
18962
18963 let second_hunk_expanded = r#"
18964 ˇA
18965 b
18966 - c
18967 + C
18968 "#
18969 .unindent();
18970
18971 cx.assert_state_with_diff(second_hunk_expanded);
18972
18973 cx.update_editor(|editor, _, cx| {
18974 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
18975 });
18976 executor.run_until_parked();
18977
18978 cx.assert_state_with_diff(both_hunks_expanded.clone());
18979
18980 cx.update_editor(|editor, _, cx| {
18981 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18982 });
18983 executor.run_until_parked();
18984
18985 let first_hunk_expanded = r#"
18986 - a
18987 + ˇA
18988 b
18989 C
18990 "#
18991 .unindent();
18992
18993 cx.assert_state_with_diff(first_hunk_expanded);
18994
18995 cx.update_editor(|editor, _, cx| {
18996 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
18997 });
18998 executor.run_until_parked();
18999
19000 cx.assert_state_with_diff(both_hunks_expanded);
19001
19002 cx.set_state(
19003 &r#"
19004 ˇA
19005 b
19006 "#
19007 .unindent(),
19008 );
19009 cx.run_until_parked();
19010
19011 // TODO this cursor position seems bad
19012 cx.assert_state_with_diff(
19013 r#"
19014 - ˇa
19015 + A
19016 b
19017 "#
19018 .unindent(),
19019 );
19020
19021 cx.update_editor(|editor, window, cx| {
19022 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19023 });
19024
19025 cx.assert_state_with_diff(
19026 r#"
19027 - ˇa
19028 + A
19029 b
19030 - c
19031 "#
19032 .unindent(),
19033 );
19034
19035 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19036 let snapshot = editor.snapshot(window, cx);
19037 let hunks = editor
19038 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19039 .collect::<Vec<_>>();
19040 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19041 let buffer_id = hunks[0].buffer_id;
19042 hunks
19043 .into_iter()
19044 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19045 .collect::<Vec<_>>()
19046 });
19047 assert_eq!(hunk_ranges.len(), 2);
19048
19049 cx.update_editor(|editor, _, cx| {
19050 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
19051 });
19052 executor.run_until_parked();
19053
19054 cx.assert_state_with_diff(
19055 r#"
19056 - ˇa
19057 + A
19058 b
19059 "#
19060 .unindent(),
19061 );
19062}
19063
19064#[gpui::test]
19065async fn test_toggle_deletion_hunk_at_start_of_file(
19066 executor: BackgroundExecutor,
19067 cx: &mut TestAppContext,
19068) {
19069 init_test(cx, |_| {});
19070 let mut cx = EditorTestContext::new(cx).await;
19071
19072 let diff_base = r#"
19073 a
19074 b
19075 c
19076 "#
19077 .unindent();
19078
19079 cx.set_state(
19080 &r#"
19081 ˇb
19082 c
19083 "#
19084 .unindent(),
19085 );
19086 cx.set_head_text(&diff_base);
19087 cx.update_editor(|editor, window, cx| {
19088 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19089 });
19090 executor.run_until_parked();
19091
19092 let hunk_expanded = r#"
19093 - a
19094 ˇb
19095 c
19096 "#
19097 .unindent();
19098
19099 cx.assert_state_with_diff(hunk_expanded.clone());
19100
19101 let hunk_ranges = cx.update_editor(|editor, window, cx| {
19102 let snapshot = editor.snapshot(window, cx);
19103 let hunks = editor
19104 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19105 .collect::<Vec<_>>();
19106 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
19107 let buffer_id = hunks[0].buffer_id;
19108 hunks
19109 .into_iter()
19110 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
19111 .collect::<Vec<_>>()
19112 });
19113 assert_eq!(hunk_ranges.len(), 1);
19114
19115 cx.update_editor(|editor, _, cx| {
19116 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19117 });
19118 executor.run_until_parked();
19119
19120 let hunk_collapsed = r#"
19121 ˇb
19122 c
19123 "#
19124 .unindent();
19125
19126 cx.assert_state_with_diff(hunk_collapsed);
19127
19128 cx.update_editor(|editor, _, cx| {
19129 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
19130 });
19131 executor.run_until_parked();
19132
19133 cx.assert_state_with_diff(hunk_expanded.clone());
19134}
19135
19136#[gpui::test]
19137async fn test_display_diff_hunks(cx: &mut TestAppContext) {
19138 init_test(cx, |_| {});
19139
19140 let fs = FakeFs::new(cx.executor());
19141 fs.insert_tree(
19142 path!("/test"),
19143 json!({
19144 ".git": {},
19145 "file-1": "ONE\n",
19146 "file-2": "TWO\n",
19147 "file-3": "THREE\n",
19148 }),
19149 )
19150 .await;
19151
19152 fs.set_head_for_repo(
19153 path!("/test/.git").as_ref(),
19154 &[
19155 ("file-1".into(), "one\n".into()),
19156 ("file-2".into(), "two\n".into()),
19157 ("file-3".into(), "three\n".into()),
19158 ],
19159 "deadbeef",
19160 );
19161
19162 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
19163 let mut buffers = vec![];
19164 for i in 1..=3 {
19165 let buffer = project
19166 .update(cx, |project, cx| {
19167 let path = format!(path!("/test/file-{}"), i);
19168 project.open_local_buffer(path, cx)
19169 })
19170 .await
19171 .unwrap();
19172 buffers.push(buffer);
19173 }
19174
19175 let multibuffer = cx.new(|cx| {
19176 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
19177 multibuffer.set_all_diff_hunks_expanded(cx);
19178 for buffer in &buffers {
19179 let snapshot = buffer.read(cx).snapshot();
19180 multibuffer.set_excerpts_for_path(
19181 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
19182 buffer.clone(),
19183 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
19184 DEFAULT_MULTIBUFFER_CONTEXT,
19185 cx,
19186 );
19187 }
19188 multibuffer
19189 });
19190
19191 let editor = cx.add_window(|window, cx| {
19192 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
19193 });
19194 cx.run_until_parked();
19195
19196 let snapshot = editor
19197 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19198 .unwrap();
19199 let hunks = snapshot
19200 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
19201 .map(|hunk| match hunk {
19202 DisplayDiffHunk::Unfolded {
19203 display_row_range, ..
19204 } => display_row_range,
19205 DisplayDiffHunk::Folded { .. } => unreachable!(),
19206 })
19207 .collect::<Vec<_>>();
19208 assert_eq!(
19209 hunks,
19210 [
19211 DisplayRow(2)..DisplayRow(4),
19212 DisplayRow(7)..DisplayRow(9),
19213 DisplayRow(12)..DisplayRow(14),
19214 ]
19215 );
19216}
19217
19218#[gpui::test]
19219async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
19220 init_test(cx, |_| {});
19221
19222 let mut cx = EditorTestContext::new(cx).await;
19223 cx.set_head_text(indoc! { "
19224 one
19225 two
19226 three
19227 four
19228 five
19229 "
19230 });
19231 cx.set_index_text(indoc! { "
19232 one
19233 two
19234 three
19235 four
19236 five
19237 "
19238 });
19239 cx.set_state(indoc! {"
19240 one
19241 TWO
19242 ˇTHREE
19243 FOUR
19244 five
19245 "});
19246 cx.run_until_parked();
19247 cx.update_editor(|editor, window, cx| {
19248 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19249 });
19250 cx.run_until_parked();
19251 cx.assert_index_text(Some(indoc! {"
19252 one
19253 TWO
19254 THREE
19255 FOUR
19256 five
19257 "}));
19258 cx.set_state(indoc! { "
19259 one
19260 TWO
19261 ˇTHREE-HUNDRED
19262 FOUR
19263 five
19264 "});
19265 cx.run_until_parked();
19266 cx.update_editor(|editor, window, cx| {
19267 let snapshot = editor.snapshot(window, cx);
19268 let hunks = editor
19269 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
19270 .collect::<Vec<_>>();
19271 assert_eq!(hunks.len(), 1);
19272 assert_eq!(
19273 hunks[0].status(),
19274 DiffHunkStatus {
19275 kind: DiffHunkStatusKind::Modified,
19276 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
19277 }
19278 );
19279
19280 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19281 });
19282 cx.run_until_parked();
19283 cx.assert_index_text(Some(indoc! {"
19284 one
19285 TWO
19286 THREE-HUNDRED
19287 FOUR
19288 five
19289 "}));
19290}
19291
19292#[gpui::test]
19293fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
19294 init_test(cx, |_| {});
19295
19296 let editor = cx.add_window(|window, cx| {
19297 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
19298 build_editor(buffer, window, cx)
19299 });
19300
19301 let render_args = Arc::new(Mutex::new(None));
19302 let snapshot = editor
19303 .update(cx, |editor, window, cx| {
19304 let snapshot = editor.buffer().read(cx).snapshot(cx);
19305 let range =
19306 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
19307
19308 struct RenderArgs {
19309 row: MultiBufferRow,
19310 folded: bool,
19311 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
19312 }
19313
19314 let crease = Crease::inline(
19315 range,
19316 FoldPlaceholder::test(),
19317 {
19318 let toggle_callback = render_args.clone();
19319 move |row, folded, callback, _window, _cx| {
19320 *toggle_callback.lock() = Some(RenderArgs {
19321 row,
19322 folded,
19323 callback,
19324 });
19325 div()
19326 }
19327 },
19328 |_row, _folded, _window, _cx| div(),
19329 );
19330
19331 editor.insert_creases(Some(crease), cx);
19332 let snapshot = editor.snapshot(window, cx);
19333 let _div = snapshot.render_crease_toggle(
19334 MultiBufferRow(1),
19335 false,
19336 cx.entity().clone(),
19337 window,
19338 cx,
19339 );
19340 snapshot
19341 })
19342 .unwrap();
19343
19344 let render_args = render_args.lock().take().unwrap();
19345 assert_eq!(render_args.row, MultiBufferRow(1));
19346 assert!(!render_args.folded);
19347 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19348
19349 cx.update_window(*editor, |_, window, cx| {
19350 (render_args.callback)(true, window, cx)
19351 })
19352 .unwrap();
19353 let snapshot = editor
19354 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19355 .unwrap();
19356 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
19357
19358 cx.update_window(*editor, |_, window, cx| {
19359 (render_args.callback)(false, window, cx)
19360 })
19361 .unwrap();
19362 let snapshot = editor
19363 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
19364 .unwrap();
19365 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
19366}
19367
19368#[gpui::test]
19369async fn test_input_text(cx: &mut TestAppContext) {
19370 init_test(cx, |_| {});
19371 let mut cx = EditorTestContext::new(cx).await;
19372
19373 cx.set_state(
19374 &r#"ˇone
19375 two
19376
19377 three
19378 fourˇ
19379 five
19380
19381 siˇx"#
19382 .unindent(),
19383 );
19384
19385 cx.dispatch_action(HandleInput(String::new()));
19386 cx.assert_editor_state(
19387 &r#"ˇone
19388 two
19389
19390 three
19391 fourˇ
19392 five
19393
19394 siˇx"#
19395 .unindent(),
19396 );
19397
19398 cx.dispatch_action(HandleInput("AAAA".to_string()));
19399 cx.assert_editor_state(
19400 &r#"AAAAˇone
19401 two
19402
19403 three
19404 fourAAAAˇ
19405 five
19406
19407 siAAAAˇx"#
19408 .unindent(),
19409 );
19410}
19411
19412#[gpui::test]
19413async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
19414 init_test(cx, |_| {});
19415
19416 let mut cx = EditorTestContext::new(cx).await;
19417 cx.set_state(
19418 r#"let foo = 1;
19419let foo = 2;
19420let foo = 3;
19421let fooˇ = 4;
19422let foo = 5;
19423let foo = 6;
19424let foo = 7;
19425let foo = 8;
19426let foo = 9;
19427let foo = 10;
19428let foo = 11;
19429let foo = 12;
19430let foo = 13;
19431let foo = 14;
19432let foo = 15;"#,
19433 );
19434
19435 cx.update_editor(|e, window, cx| {
19436 assert_eq!(
19437 e.next_scroll_position,
19438 NextScrollCursorCenterTopBottom::Center,
19439 "Default next scroll direction is center",
19440 );
19441
19442 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19443 assert_eq!(
19444 e.next_scroll_position,
19445 NextScrollCursorCenterTopBottom::Top,
19446 "After center, next scroll direction should be top",
19447 );
19448
19449 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19450 assert_eq!(
19451 e.next_scroll_position,
19452 NextScrollCursorCenterTopBottom::Bottom,
19453 "After top, next scroll direction should be bottom",
19454 );
19455
19456 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19457 assert_eq!(
19458 e.next_scroll_position,
19459 NextScrollCursorCenterTopBottom::Center,
19460 "After bottom, scrolling should start over",
19461 );
19462
19463 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
19464 assert_eq!(
19465 e.next_scroll_position,
19466 NextScrollCursorCenterTopBottom::Top,
19467 "Scrolling continues if retriggered fast enough"
19468 );
19469 });
19470
19471 cx.executor()
19472 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
19473 cx.executor().run_until_parked();
19474 cx.update_editor(|e, _, _| {
19475 assert_eq!(
19476 e.next_scroll_position,
19477 NextScrollCursorCenterTopBottom::Center,
19478 "If scrolling is not triggered fast enough, it should reset"
19479 );
19480 });
19481}
19482
19483#[gpui::test]
19484async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
19485 init_test(cx, |_| {});
19486 let mut cx = EditorLspTestContext::new_rust(
19487 lsp::ServerCapabilities {
19488 definition_provider: Some(lsp::OneOf::Left(true)),
19489 references_provider: Some(lsp::OneOf::Left(true)),
19490 ..lsp::ServerCapabilities::default()
19491 },
19492 cx,
19493 )
19494 .await;
19495
19496 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
19497 let go_to_definition = cx
19498 .lsp
19499 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19500 move |params, _| async move {
19501 if empty_go_to_definition {
19502 Ok(None)
19503 } else {
19504 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
19505 uri: params.text_document_position_params.text_document.uri,
19506 range: lsp::Range::new(
19507 lsp::Position::new(4, 3),
19508 lsp::Position::new(4, 6),
19509 ),
19510 })))
19511 }
19512 },
19513 );
19514 let references = cx
19515 .lsp
19516 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
19517 Ok(Some(vec![lsp::Location {
19518 uri: params.text_document_position.text_document.uri,
19519 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
19520 }]))
19521 });
19522 (go_to_definition, references)
19523 };
19524
19525 cx.set_state(
19526 &r#"fn one() {
19527 let mut a = ˇtwo();
19528 }
19529
19530 fn two() {}"#
19531 .unindent(),
19532 );
19533 set_up_lsp_handlers(false, &mut cx);
19534 let navigated = cx
19535 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19536 .await
19537 .expect("Failed to navigate to definition");
19538 assert_eq!(
19539 navigated,
19540 Navigated::Yes,
19541 "Should have navigated to definition from the GetDefinition response"
19542 );
19543 cx.assert_editor_state(
19544 &r#"fn one() {
19545 let mut a = two();
19546 }
19547
19548 fn «twoˇ»() {}"#
19549 .unindent(),
19550 );
19551
19552 let editors = cx.update_workspace(|workspace, _, cx| {
19553 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19554 });
19555 cx.update_editor(|_, _, test_editor_cx| {
19556 assert_eq!(
19557 editors.len(),
19558 1,
19559 "Initially, only one, test, editor should be open in the workspace"
19560 );
19561 assert_eq!(
19562 test_editor_cx.entity(),
19563 editors.last().expect("Asserted len is 1").clone()
19564 );
19565 });
19566
19567 set_up_lsp_handlers(true, &mut cx);
19568 let navigated = cx
19569 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19570 .await
19571 .expect("Failed to navigate to lookup references");
19572 assert_eq!(
19573 navigated,
19574 Navigated::Yes,
19575 "Should have navigated to references as a fallback after empty GoToDefinition response"
19576 );
19577 // We should not change the selections in the existing file,
19578 // if opening another milti buffer with the references
19579 cx.assert_editor_state(
19580 &r#"fn one() {
19581 let mut a = two();
19582 }
19583
19584 fn «twoˇ»() {}"#
19585 .unindent(),
19586 );
19587 let editors = cx.update_workspace(|workspace, _, cx| {
19588 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19589 });
19590 cx.update_editor(|_, _, test_editor_cx| {
19591 assert_eq!(
19592 editors.len(),
19593 2,
19594 "After falling back to references search, we open a new editor with the results"
19595 );
19596 let references_fallback_text = editors
19597 .into_iter()
19598 .find(|new_editor| *new_editor != test_editor_cx.entity())
19599 .expect("Should have one non-test editor now")
19600 .read(test_editor_cx)
19601 .text(test_editor_cx);
19602 assert_eq!(
19603 references_fallback_text, "fn one() {\n let mut a = two();\n}",
19604 "Should use the range from the references response and not the GoToDefinition one"
19605 );
19606 });
19607}
19608
19609#[gpui::test]
19610async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
19611 init_test(cx, |_| {});
19612 cx.update(|cx| {
19613 let mut editor_settings = EditorSettings::get_global(cx).clone();
19614 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
19615 EditorSettings::override_global(editor_settings, cx);
19616 });
19617 let mut cx = EditorLspTestContext::new_rust(
19618 lsp::ServerCapabilities {
19619 definition_provider: Some(lsp::OneOf::Left(true)),
19620 references_provider: Some(lsp::OneOf::Left(true)),
19621 ..lsp::ServerCapabilities::default()
19622 },
19623 cx,
19624 )
19625 .await;
19626 let original_state = r#"fn one() {
19627 let mut a = ˇtwo();
19628 }
19629
19630 fn two() {}"#
19631 .unindent();
19632 cx.set_state(&original_state);
19633
19634 let mut go_to_definition = cx
19635 .lsp
19636 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
19637 move |_, _| async move { Ok(None) },
19638 );
19639 let _references = cx
19640 .lsp
19641 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
19642 panic!("Should not call for references with no go to definition fallback")
19643 });
19644
19645 let navigated = cx
19646 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
19647 .await
19648 .expect("Failed to navigate to lookup references");
19649 go_to_definition
19650 .next()
19651 .await
19652 .expect("Should have called the go_to_definition handler");
19653
19654 assert_eq!(
19655 navigated,
19656 Navigated::No,
19657 "Should have navigated to references as a fallback after empty GoToDefinition response"
19658 );
19659 cx.assert_editor_state(&original_state);
19660 let editors = cx.update_workspace(|workspace, _, cx| {
19661 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
19662 });
19663 cx.update_editor(|_, _, _| {
19664 assert_eq!(
19665 editors.len(),
19666 1,
19667 "After unsuccessful fallback, no other editor should have been opened"
19668 );
19669 });
19670}
19671
19672#[gpui::test]
19673async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
19674 init_test(cx, |_| {});
19675
19676 let language = Arc::new(Language::new(
19677 LanguageConfig::default(),
19678 Some(tree_sitter_rust::LANGUAGE.into()),
19679 ));
19680
19681 let text = r#"
19682 #[cfg(test)]
19683 mod tests() {
19684 #[test]
19685 fn runnable_1() {
19686 let a = 1;
19687 }
19688
19689 #[test]
19690 fn runnable_2() {
19691 let a = 1;
19692 let b = 2;
19693 }
19694 }
19695 "#
19696 .unindent();
19697
19698 let fs = FakeFs::new(cx.executor());
19699 fs.insert_file("/file.rs", Default::default()).await;
19700
19701 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19702 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19703 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19704 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
19705 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
19706
19707 let editor = cx.new_window_entity(|window, cx| {
19708 Editor::new(
19709 EditorMode::full(),
19710 multi_buffer,
19711 Some(project.clone()),
19712 window,
19713 cx,
19714 )
19715 });
19716
19717 editor.update_in(cx, |editor, window, cx| {
19718 let snapshot = editor.buffer().read(cx).snapshot(cx);
19719 editor.tasks.insert(
19720 (buffer.read(cx).remote_id(), 3),
19721 RunnableTasks {
19722 templates: vec![],
19723 offset: snapshot.anchor_before(43),
19724 column: 0,
19725 extra_variables: HashMap::default(),
19726 context_range: BufferOffset(43)..BufferOffset(85),
19727 },
19728 );
19729 editor.tasks.insert(
19730 (buffer.read(cx).remote_id(), 8),
19731 RunnableTasks {
19732 templates: vec![],
19733 offset: snapshot.anchor_before(86),
19734 column: 0,
19735 extra_variables: HashMap::default(),
19736 context_range: BufferOffset(86)..BufferOffset(191),
19737 },
19738 );
19739
19740 // Test finding task when cursor is inside function body
19741 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19742 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
19743 });
19744 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19745 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
19746
19747 // Test finding task when cursor is on function name
19748 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19749 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
19750 });
19751 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
19752 assert_eq!(row, 8, "Should find task when cursor is on function name");
19753 });
19754}
19755
19756#[gpui::test]
19757async fn test_folding_buffers(cx: &mut TestAppContext) {
19758 init_test(cx, |_| {});
19759
19760 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
19761 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
19762 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
19763
19764 let fs = FakeFs::new(cx.executor());
19765 fs.insert_tree(
19766 path!("/a"),
19767 json!({
19768 "first.rs": sample_text_1,
19769 "second.rs": sample_text_2,
19770 "third.rs": sample_text_3,
19771 }),
19772 )
19773 .await;
19774 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19775 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19776 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19777 let worktree = project.update(cx, |project, cx| {
19778 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
19779 assert_eq!(worktrees.len(), 1);
19780 worktrees.pop().unwrap()
19781 });
19782 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
19783
19784 let buffer_1 = project
19785 .update(cx, |project, cx| {
19786 project.open_buffer((worktree_id, "first.rs"), cx)
19787 })
19788 .await
19789 .unwrap();
19790 let buffer_2 = project
19791 .update(cx, |project, cx| {
19792 project.open_buffer((worktree_id, "second.rs"), cx)
19793 })
19794 .await
19795 .unwrap();
19796 let buffer_3 = project
19797 .update(cx, |project, cx| {
19798 project.open_buffer((worktree_id, "third.rs"), cx)
19799 })
19800 .await
19801 .unwrap();
19802
19803 let multi_buffer = cx.new(|cx| {
19804 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19805 multi_buffer.push_excerpts(
19806 buffer_1.clone(),
19807 [
19808 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19809 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19810 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19811 ],
19812 cx,
19813 );
19814 multi_buffer.push_excerpts(
19815 buffer_2.clone(),
19816 [
19817 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19818 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19819 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19820 ],
19821 cx,
19822 );
19823 multi_buffer.push_excerpts(
19824 buffer_3.clone(),
19825 [
19826 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19827 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19828 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19829 ],
19830 cx,
19831 );
19832 multi_buffer
19833 });
19834 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19835 Editor::new(
19836 EditorMode::full(),
19837 multi_buffer.clone(),
19838 Some(project.clone()),
19839 window,
19840 cx,
19841 )
19842 });
19843
19844 assert_eq!(
19845 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19846 "\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",
19847 );
19848
19849 multi_buffer_editor.update(cx, |editor, cx| {
19850 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
19851 });
19852 assert_eq!(
19853 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19854 "\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",
19855 "After folding the first buffer, its text should not be displayed"
19856 );
19857
19858 multi_buffer_editor.update(cx, |editor, cx| {
19859 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
19860 });
19861 assert_eq!(
19862 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19863 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
19864 "After folding the second buffer, its text should not be displayed"
19865 );
19866
19867 multi_buffer_editor.update(cx, |editor, cx| {
19868 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
19869 });
19870 assert_eq!(
19871 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19872 "\n\n\n\n\n",
19873 "After folding the third buffer, its text should not be displayed"
19874 );
19875
19876 // Emulate selection inside the fold logic, that should work
19877 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19878 editor
19879 .snapshot(window, cx)
19880 .next_line_boundary(Point::new(0, 4));
19881 });
19882
19883 multi_buffer_editor.update(cx, |editor, cx| {
19884 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
19885 });
19886 assert_eq!(
19887 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19888 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19889 "After unfolding the second buffer, its text should be displayed"
19890 );
19891
19892 // Typing inside of buffer 1 causes that buffer to be unfolded.
19893 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19894 assert_eq!(
19895 multi_buffer
19896 .read(cx)
19897 .snapshot(cx)
19898 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
19899 .collect::<String>(),
19900 "bbbb"
19901 );
19902 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
19903 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
19904 });
19905 editor.handle_input("B", window, cx);
19906 });
19907
19908 assert_eq!(
19909 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19910 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
19911 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
19912 );
19913
19914 multi_buffer_editor.update(cx, |editor, cx| {
19915 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
19916 });
19917 assert_eq!(
19918 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
19919 "\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",
19920 "After unfolding the all buffers, all original text should be displayed"
19921 );
19922}
19923
19924#[gpui::test]
19925async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
19926 init_test(cx, |_| {});
19927
19928 let sample_text_1 = "1111\n2222\n3333".to_string();
19929 let sample_text_2 = "4444\n5555\n6666".to_string();
19930 let sample_text_3 = "7777\n8888\n9999".to_string();
19931
19932 let fs = FakeFs::new(cx.executor());
19933 fs.insert_tree(
19934 path!("/a"),
19935 json!({
19936 "first.rs": sample_text_1,
19937 "second.rs": sample_text_2,
19938 "third.rs": sample_text_3,
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, "first.rs"), cx)
19955 })
19956 .await
19957 .unwrap();
19958 let buffer_2 = project
19959 .update(cx, |project, cx| {
19960 project.open_buffer((worktree_id, "second.rs"), cx)
19961 })
19962 .await
19963 .unwrap();
19964 let buffer_3 = project
19965 .update(cx, |project, cx| {
19966 project.open_buffer((worktree_id, "third.rs"), cx)
19967 })
19968 .await
19969 .unwrap();
19970
19971 let multi_buffer = cx.new(|cx| {
19972 let mut multi_buffer = MultiBuffer::new(ReadWrite);
19973 multi_buffer.push_excerpts(
19974 buffer_1.clone(),
19975 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19976 cx,
19977 );
19978 multi_buffer.push_excerpts(
19979 buffer_2.clone(),
19980 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19981 cx,
19982 );
19983 multi_buffer.push_excerpts(
19984 buffer_3.clone(),
19985 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
19986 cx,
19987 );
19988 multi_buffer
19989 });
19990
19991 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19992 Editor::new(
19993 EditorMode::full(),
19994 multi_buffer,
19995 Some(project.clone()),
19996 window,
19997 cx,
19998 )
19999 });
20000
20001 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
20002 assert_eq!(
20003 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20004 full_text,
20005 );
20006
20007 multi_buffer_editor.update(cx, |editor, cx| {
20008 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
20009 });
20010 assert_eq!(
20011 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20012 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
20013 "After folding the first buffer, its text should not be displayed"
20014 );
20015
20016 multi_buffer_editor.update(cx, |editor, cx| {
20017 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
20018 });
20019
20020 assert_eq!(
20021 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20022 "\n\n\n\n\n\n7777\n8888\n9999",
20023 "After folding the second buffer, its text should not be displayed"
20024 );
20025
20026 multi_buffer_editor.update(cx, |editor, cx| {
20027 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
20028 });
20029 assert_eq!(
20030 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20031 "\n\n\n\n\n",
20032 "After folding the third buffer, its text should not be displayed"
20033 );
20034
20035 multi_buffer_editor.update(cx, |editor, cx| {
20036 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
20037 });
20038 assert_eq!(
20039 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20040 "\n\n\n\n4444\n5555\n6666\n\n",
20041 "After unfolding the second buffer, its text should be displayed"
20042 );
20043
20044 multi_buffer_editor.update(cx, |editor, cx| {
20045 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
20046 });
20047 assert_eq!(
20048 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20049 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
20050 "After unfolding the first buffer, its text should be displayed"
20051 );
20052
20053 multi_buffer_editor.update(cx, |editor, cx| {
20054 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
20055 });
20056 assert_eq!(
20057 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20058 full_text,
20059 "After unfolding all buffers, all original text should be displayed"
20060 );
20061}
20062
20063#[gpui::test]
20064async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
20065 init_test(cx, |_| {});
20066
20067 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
20068
20069 let fs = FakeFs::new(cx.executor());
20070 fs.insert_tree(
20071 path!("/a"),
20072 json!({
20073 "main.rs": sample_text,
20074 }),
20075 )
20076 .await;
20077 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20078 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20079 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20080 let worktree = project.update(cx, |project, cx| {
20081 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
20082 assert_eq!(worktrees.len(), 1);
20083 worktrees.pop().unwrap()
20084 });
20085 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
20086
20087 let buffer_1 = project
20088 .update(cx, |project, cx| {
20089 project.open_buffer((worktree_id, "main.rs"), cx)
20090 })
20091 .await
20092 .unwrap();
20093
20094 let multi_buffer = cx.new(|cx| {
20095 let mut multi_buffer = MultiBuffer::new(ReadWrite);
20096 multi_buffer.push_excerpts(
20097 buffer_1.clone(),
20098 [ExcerptRange::new(
20099 Point::new(0, 0)
20100 ..Point::new(
20101 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
20102 0,
20103 ),
20104 )],
20105 cx,
20106 );
20107 multi_buffer
20108 });
20109 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20110 Editor::new(
20111 EditorMode::full(),
20112 multi_buffer,
20113 Some(project.clone()),
20114 window,
20115 cx,
20116 )
20117 });
20118
20119 let selection_range = Point::new(1, 0)..Point::new(2, 0);
20120 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20121 enum TestHighlight {}
20122 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
20123 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
20124 editor.highlight_text::<TestHighlight>(
20125 vec![highlight_range.clone()],
20126 HighlightStyle::color(Hsla::green()),
20127 cx,
20128 );
20129 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20130 s.select_ranges(Some(highlight_range))
20131 });
20132 });
20133
20134 let full_text = format!("\n\n{sample_text}");
20135 assert_eq!(
20136 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
20137 full_text,
20138 );
20139}
20140
20141#[gpui::test]
20142async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
20143 init_test(cx, |_| {});
20144 cx.update(|cx| {
20145 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
20146 "keymaps/default-linux.json",
20147 cx,
20148 )
20149 .unwrap();
20150 cx.bind_keys(default_key_bindings);
20151 });
20152
20153 let (editor, cx) = cx.add_window_view(|window, cx| {
20154 let multi_buffer = MultiBuffer::build_multi(
20155 [
20156 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
20157 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
20158 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
20159 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
20160 ],
20161 cx,
20162 );
20163 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
20164
20165 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
20166 // fold all but the second buffer, so that we test navigating between two
20167 // adjacent folded buffers, as well as folded buffers at the start and
20168 // end the multibuffer
20169 editor.fold_buffer(buffer_ids[0], cx);
20170 editor.fold_buffer(buffer_ids[2], cx);
20171 editor.fold_buffer(buffer_ids[3], cx);
20172
20173 editor
20174 });
20175 cx.simulate_resize(size(px(1000.), px(1000.)));
20176
20177 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
20178 cx.assert_excerpts_with_selections(indoc! {"
20179 [EXCERPT]
20180 ˇ[FOLDED]
20181 [EXCERPT]
20182 a1
20183 b1
20184 [EXCERPT]
20185 [FOLDED]
20186 [EXCERPT]
20187 [FOLDED]
20188 "
20189 });
20190 cx.simulate_keystroke("down");
20191 cx.assert_excerpts_with_selections(indoc! {"
20192 [EXCERPT]
20193 [FOLDED]
20194 [EXCERPT]
20195 ˇa1
20196 b1
20197 [EXCERPT]
20198 [FOLDED]
20199 [EXCERPT]
20200 [FOLDED]
20201 "
20202 });
20203 cx.simulate_keystroke("down");
20204 cx.assert_excerpts_with_selections(indoc! {"
20205 [EXCERPT]
20206 [FOLDED]
20207 [EXCERPT]
20208 a1
20209 ˇb1
20210 [EXCERPT]
20211 [FOLDED]
20212 [EXCERPT]
20213 [FOLDED]
20214 "
20215 });
20216 cx.simulate_keystroke("down");
20217 cx.assert_excerpts_with_selections(indoc! {"
20218 [EXCERPT]
20219 [FOLDED]
20220 [EXCERPT]
20221 a1
20222 b1
20223 ˇ[EXCERPT]
20224 [FOLDED]
20225 [EXCERPT]
20226 [FOLDED]
20227 "
20228 });
20229 cx.simulate_keystroke("down");
20230 cx.assert_excerpts_with_selections(indoc! {"
20231 [EXCERPT]
20232 [FOLDED]
20233 [EXCERPT]
20234 a1
20235 b1
20236 [EXCERPT]
20237 ˇ[FOLDED]
20238 [EXCERPT]
20239 [FOLDED]
20240 "
20241 });
20242 for _ in 0..5 {
20243 cx.simulate_keystroke("down");
20244 cx.assert_excerpts_with_selections(indoc! {"
20245 [EXCERPT]
20246 [FOLDED]
20247 [EXCERPT]
20248 a1
20249 b1
20250 [EXCERPT]
20251 [FOLDED]
20252 [EXCERPT]
20253 ˇ[FOLDED]
20254 "
20255 });
20256 }
20257
20258 cx.simulate_keystroke("up");
20259 cx.assert_excerpts_with_selections(indoc! {"
20260 [EXCERPT]
20261 [FOLDED]
20262 [EXCERPT]
20263 a1
20264 b1
20265 [EXCERPT]
20266 ˇ[FOLDED]
20267 [EXCERPT]
20268 [FOLDED]
20269 "
20270 });
20271 cx.simulate_keystroke("up");
20272 cx.assert_excerpts_with_selections(indoc! {"
20273 [EXCERPT]
20274 [FOLDED]
20275 [EXCERPT]
20276 a1
20277 b1
20278 ˇ[EXCERPT]
20279 [FOLDED]
20280 [EXCERPT]
20281 [FOLDED]
20282 "
20283 });
20284 cx.simulate_keystroke("up");
20285 cx.assert_excerpts_with_selections(indoc! {"
20286 [EXCERPT]
20287 [FOLDED]
20288 [EXCERPT]
20289 a1
20290 ˇb1
20291 [EXCERPT]
20292 [FOLDED]
20293 [EXCERPT]
20294 [FOLDED]
20295 "
20296 });
20297 cx.simulate_keystroke("up");
20298 cx.assert_excerpts_with_selections(indoc! {"
20299 [EXCERPT]
20300 [FOLDED]
20301 [EXCERPT]
20302 ˇa1
20303 b1
20304 [EXCERPT]
20305 [FOLDED]
20306 [EXCERPT]
20307 [FOLDED]
20308 "
20309 });
20310 for _ in 0..5 {
20311 cx.simulate_keystroke("up");
20312 cx.assert_excerpts_with_selections(indoc! {"
20313 [EXCERPT]
20314 ˇ[FOLDED]
20315 [EXCERPT]
20316 a1
20317 b1
20318 [EXCERPT]
20319 [FOLDED]
20320 [EXCERPT]
20321 [FOLDED]
20322 "
20323 });
20324 }
20325}
20326
20327#[gpui::test]
20328async fn test_inline_completion_text(cx: &mut TestAppContext) {
20329 init_test(cx, |_| {});
20330
20331 // Simple insertion
20332 assert_highlighted_edits(
20333 "Hello, world!",
20334 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
20335 true,
20336 cx,
20337 |highlighted_edits, cx| {
20338 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
20339 assert_eq!(highlighted_edits.highlights.len(), 1);
20340 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
20341 assert_eq!(
20342 highlighted_edits.highlights[0].1.background_color,
20343 Some(cx.theme().status().created_background)
20344 );
20345 },
20346 )
20347 .await;
20348
20349 // Replacement
20350 assert_highlighted_edits(
20351 "This is a test.",
20352 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
20353 false,
20354 cx,
20355 |highlighted_edits, cx| {
20356 assert_eq!(highlighted_edits.text, "That is a test.");
20357 assert_eq!(highlighted_edits.highlights.len(), 1);
20358 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
20359 assert_eq!(
20360 highlighted_edits.highlights[0].1.background_color,
20361 Some(cx.theme().status().created_background)
20362 );
20363 },
20364 )
20365 .await;
20366
20367 // Multiple edits
20368 assert_highlighted_edits(
20369 "Hello, world!",
20370 vec![
20371 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
20372 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
20373 ],
20374 false,
20375 cx,
20376 |highlighted_edits, cx| {
20377 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
20378 assert_eq!(highlighted_edits.highlights.len(), 2);
20379 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
20380 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
20381 assert_eq!(
20382 highlighted_edits.highlights[0].1.background_color,
20383 Some(cx.theme().status().created_background)
20384 );
20385 assert_eq!(
20386 highlighted_edits.highlights[1].1.background_color,
20387 Some(cx.theme().status().created_background)
20388 );
20389 },
20390 )
20391 .await;
20392
20393 // Multiple lines with edits
20394 assert_highlighted_edits(
20395 "First line\nSecond line\nThird line\nFourth line",
20396 vec![
20397 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
20398 (
20399 Point::new(2, 0)..Point::new(2, 10),
20400 "New third line".to_string(),
20401 ),
20402 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
20403 ],
20404 false,
20405 cx,
20406 |highlighted_edits, cx| {
20407 assert_eq!(
20408 highlighted_edits.text,
20409 "Second modified\nNew third line\nFourth updated line"
20410 );
20411 assert_eq!(highlighted_edits.highlights.len(), 3);
20412 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
20413 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
20414 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
20415 for highlight in &highlighted_edits.highlights {
20416 assert_eq!(
20417 highlight.1.background_color,
20418 Some(cx.theme().status().created_background)
20419 );
20420 }
20421 },
20422 )
20423 .await;
20424}
20425
20426#[gpui::test]
20427async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
20428 init_test(cx, |_| {});
20429
20430 // Deletion
20431 assert_highlighted_edits(
20432 "Hello, world!",
20433 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
20434 true,
20435 cx,
20436 |highlighted_edits, cx| {
20437 assert_eq!(highlighted_edits.text, "Hello, world!");
20438 assert_eq!(highlighted_edits.highlights.len(), 1);
20439 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
20440 assert_eq!(
20441 highlighted_edits.highlights[0].1.background_color,
20442 Some(cx.theme().status().deleted_background)
20443 );
20444 },
20445 )
20446 .await;
20447
20448 // Insertion
20449 assert_highlighted_edits(
20450 "Hello, world!",
20451 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
20452 true,
20453 cx,
20454 |highlighted_edits, cx| {
20455 assert_eq!(highlighted_edits.highlights.len(), 1);
20456 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
20457 assert_eq!(
20458 highlighted_edits.highlights[0].1.background_color,
20459 Some(cx.theme().status().created_background)
20460 );
20461 },
20462 )
20463 .await;
20464}
20465
20466async fn assert_highlighted_edits(
20467 text: &str,
20468 edits: Vec<(Range<Point>, String)>,
20469 include_deletions: bool,
20470 cx: &mut TestAppContext,
20471 assertion_fn: impl Fn(HighlightedText, &App),
20472) {
20473 let window = cx.add_window(|window, cx| {
20474 let buffer = MultiBuffer::build_simple(text, cx);
20475 Editor::new(EditorMode::full(), buffer, None, window, cx)
20476 });
20477 let cx = &mut VisualTestContext::from_window(*window, cx);
20478
20479 let (buffer, snapshot) = window
20480 .update(cx, |editor, _window, cx| {
20481 (
20482 editor.buffer().clone(),
20483 editor.buffer().read(cx).snapshot(cx),
20484 )
20485 })
20486 .unwrap();
20487
20488 let edits = edits
20489 .into_iter()
20490 .map(|(range, edit)| {
20491 (
20492 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
20493 edit,
20494 )
20495 })
20496 .collect::<Vec<_>>();
20497
20498 let text_anchor_edits = edits
20499 .clone()
20500 .into_iter()
20501 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
20502 .collect::<Vec<_>>();
20503
20504 let edit_preview = window
20505 .update(cx, |_, _window, cx| {
20506 buffer
20507 .read(cx)
20508 .as_singleton()
20509 .unwrap()
20510 .read(cx)
20511 .preview_edits(text_anchor_edits.into(), cx)
20512 })
20513 .unwrap()
20514 .await;
20515
20516 cx.update(|_window, cx| {
20517 let highlighted_edits = inline_completion_edit_text(
20518 &snapshot.as_singleton().unwrap().2,
20519 &edits,
20520 &edit_preview,
20521 include_deletions,
20522 cx,
20523 );
20524 assertion_fn(highlighted_edits, cx)
20525 });
20526}
20527
20528#[track_caller]
20529fn assert_breakpoint(
20530 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
20531 path: &Arc<Path>,
20532 expected: Vec<(u32, Breakpoint)>,
20533) {
20534 if expected.len() == 0usize {
20535 assert!(!breakpoints.contains_key(path), "{}", path.display());
20536 } else {
20537 let mut breakpoint = breakpoints
20538 .get(path)
20539 .unwrap()
20540 .into_iter()
20541 .map(|breakpoint| {
20542 (
20543 breakpoint.row,
20544 Breakpoint {
20545 message: breakpoint.message.clone(),
20546 state: breakpoint.state,
20547 condition: breakpoint.condition.clone(),
20548 hit_condition: breakpoint.hit_condition.clone(),
20549 },
20550 )
20551 })
20552 .collect::<Vec<_>>();
20553
20554 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
20555
20556 assert_eq!(expected, breakpoint);
20557 }
20558}
20559
20560fn add_log_breakpoint_at_cursor(
20561 editor: &mut Editor,
20562 log_message: &str,
20563 window: &mut Window,
20564 cx: &mut Context<Editor>,
20565) {
20566 let (anchor, bp) = editor
20567 .breakpoints_at_cursors(window, cx)
20568 .first()
20569 .and_then(|(anchor, bp)| {
20570 if let Some(bp) = bp {
20571 Some((*anchor, bp.clone()))
20572 } else {
20573 None
20574 }
20575 })
20576 .unwrap_or_else(|| {
20577 let cursor_position: Point = editor.selections.newest(cx).head();
20578
20579 let breakpoint_position = editor
20580 .snapshot(window, cx)
20581 .display_snapshot
20582 .buffer_snapshot
20583 .anchor_before(Point::new(cursor_position.row, 0));
20584
20585 (breakpoint_position, Breakpoint::new_log(&log_message))
20586 });
20587
20588 editor.edit_breakpoint_at_anchor(
20589 anchor,
20590 bp,
20591 BreakpointEditAction::EditLogMessage(log_message.into()),
20592 cx,
20593 );
20594}
20595
20596#[gpui::test]
20597async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
20598 init_test(cx, |_| {});
20599
20600 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20601 let fs = FakeFs::new(cx.executor());
20602 fs.insert_tree(
20603 path!("/a"),
20604 json!({
20605 "main.rs": sample_text,
20606 }),
20607 )
20608 .await;
20609 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20610 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20611 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20612
20613 let fs = FakeFs::new(cx.executor());
20614 fs.insert_tree(
20615 path!("/a"),
20616 json!({
20617 "main.rs": sample_text,
20618 }),
20619 )
20620 .await;
20621 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20622 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20623 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20624 let worktree_id = workspace
20625 .update(cx, |workspace, _window, cx| {
20626 workspace.project().update(cx, |project, cx| {
20627 project.worktrees(cx).next().unwrap().read(cx).id()
20628 })
20629 })
20630 .unwrap();
20631
20632 let buffer = project
20633 .update(cx, |project, cx| {
20634 project.open_buffer((worktree_id, "main.rs"), cx)
20635 })
20636 .await
20637 .unwrap();
20638
20639 let (editor, cx) = cx.add_window_view(|window, cx| {
20640 Editor::new(
20641 EditorMode::full(),
20642 MultiBuffer::build_from_buffer(buffer, cx),
20643 Some(project.clone()),
20644 window,
20645 cx,
20646 )
20647 });
20648
20649 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20650 let abs_path = project.read_with(cx, |project, cx| {
20651 project
20652 .absolute_path(&project_path, cx)
20653 .map(|path_buf| Arc::from(path_buf.to_owned()))
20654 .unwrap()
20655 });
20656
20657 // assert we can add breakpoint on the first line
20658 editor.update_in(cx, |editor, window, cx| {
20659 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20660 editor.move_to_end(&MoveToEnd, window, cx);
20661 editor.toggle_breakpoint(&actions::ToggleBreakpoint, 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_eq!(1, breakpoints.len());
20675 assert_breakpoint(
20676 &breakpoints,
20677 &abs_path,
20678 vec![
20679 (0, Breakpoint::new_standard()),
20680 (3, Breakpoint::new_standard()),
20681 ],
20682 );
20683
20684 editor.update_in(cx, |editor, window, cx| {
20685 editor.move_to_beginning(&MoveToBeginning, window, cx);
20686 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20687 });
20688
20689 let breakpoints = editor.update(cx, |editor, cx| {
20690 editor
20691 .breakpoint_store()
20692 .as_ref()
20693 .unwrap()
20694 .read(cx)
20695 .all_source_breakpoints(cx)
20696 .clone()
20697 });
20698
20699 assert_eq!(1, breakpoints.len());
20700 assert_breakpoint(
20701 &breakpoints,
20702 &abs_path,
20703 vec![(3, Breakpoint::new_standard())],
20704 );
20705
20706 editor.update_in(cx, |editor, window, cx| {
20707 editor.move_to_end(&MoveToEnd, window, cx);
20708 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20709 });
20710
20711 let breakpoints = editor.update(cx, |editor, cx| {
20712 editor
20713 .breakpoint_store()
20714 .as_ref()
20715 .unwrap()
20716 .read(cx)
20717 .all_source_breakpoints(cx)
20718 .clone()
20719 });
20720
20721 assert_eq!(0, breakpoints.len());
20722 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20723}
20724
20725#[gpui::test]
20726async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
20727 init_test(cx, |_| {});
20728
20729 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20730
20731 let fs = FakeFs::new(cx.executor());
20732 fs.insert_tree(
20733 path!("/a"),
20734 json!({
20735 "main.rs": sample_text,
20736 }),
20737 )
20738 .await;
20739 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20740 let (workspace, cx) =
20741 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20742
20743 let worktree_id = workspace.update(cx, |workspace, cx| {
20744 workspace.project().update(cx, |project, cx| {
20745 project.worktrees(cx).next().unwrap().read(cx).id()
20746 })
20747 });
20748
20749 let buffer = project
20750 .update(cx, |project, cx| {
20751 project.open_buffer((worktree_id, "main.rs"), cx)
20752 })
20753 .await
20754 .unwrap();
20755
20756 let (editor, cx) = cx.add_window_view(|window, cx| {
20757 Editor::new(
20758 EditorMode::full(),
20759 MultiBuffer::build_from_buffer(buffer, cx),
20760 Some(project.clone()),
20761 window,
20762 cx,
20763 )
20764 });
20765
20766 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20767 let abs_path = project.read_with(cx, |project, cx| {
20768 project
20769 .absolute_path(&project_path, cx)
20770 .map(|path_buf| Arc::from(path_buf.to_owned()))
20771 .unwrap()
20772 });
20773
20774 editor.update_in(cx, |editor, window, cx| {
20775 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20776 });
20777
20778 let breakpoints = editor.update(cx, |editor, cx| {
20779 editor
20780 .breakpoint_store()
20781 .as_ref()
20782 .unwrap()
20783 .read(cx)
20784 .all_source_breakpoints(cx)
20785 .clone()
20786 });
20787
20788 assert_breakpoint(
20789 &breakpoints,
20790 &abs_path,
20791 vec![(0, Breakpoint::new_log("hello world"))],
20792 );
20793
20794 // Removing a log message from a log breakpoint should remove it
20795 editor.update_in(cx, |editor, window, cx| {
20796 add_log_breakpoint_at_cursor(editor, "", window, cx);
20797 });
20798
20799 let breakpoints = editor.update(cx, |editor, cx| {
20800 editor
20801 .breakpoint_store()
20802 .as_ref()
20803 .unwrap()
20804 .read(cx)
20805 .all_source_breakpoints(cx)
20806 .clone()
20807 });
20808
20809 assert_breakpoint(&breakpoints, &abs_path, vec![]);
20810
20811 editor.update_in(cx, |editor, window, cx| {
20812 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20813 editor.move_to_end(&MoveToEnd, window, cx);
20814 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20815 // Not adding a log message to a standard breakpoint shouldn't remove it
20816 add_log_breakpoint_at_cursor(editor, "", window, cx);
20817 });
20818
20819 let breakpoints = editor.update(cx, |editor, cx| {
20820 editor
20821 .breakpoint_store()
20822 .as_ref()
20823 .unwrap()
20824 .read(cx)
20825 .all_source_breakpoints(cx)
20826 .clone()
20827 });
20828
20829 assert_breakpoint(
20830 &breakpoints,
20831 &abs_path,
20832 vec![
20833 (0, Breakpoint::new_standard()),
20834 (3, Breakpoint::new_standard()),
20835 ],
20836 );
20837
20838 editor.update_in(cx, |editor, window, cx| {
20839 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
20840 });
20841
20842 let breakpoints = editor.update(cx, |editor, cx| {
20843 editor
20844 .breakpoint_store()
20845 .as_ref()
20846 .unwrap()
20847 .read(cx)
20848 .all_source_breakpoints(cx)
20849 .clone()
20850 });
20851
20852 assert_breakpoint(
20853 &breakpoints,
20854 &abs_path,
20855 vec![
20856 (0, Breakpoint::new_standard()),
20857 (3, Breakpoint::new_log("hello world")),
20858 ],
20859 );
20860
20861 editor.update_in(cx, |editor, window, cx| {
20862 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
20863 });
20864
20865 let breakpoints = editor.update(cx, |editor, cx| {
20866 editor
20867 .breakpoint_store()
20868 .as_ref()
20869 .unwrap()
20870 .read(cx)
20871 .all_source_breakpoints(cx)
20872 .clone()
20873 });
20874
20875 assert_breakpoint(
20876 &breakpoints,
20877 &abs_path,
20878 vec![
20879 (0, Breakpoint::new_standard()),
20880 (3, Breakpoint::new_log("hello Earth!!")),
20881 ],
20882 );
20883}
20884
20885/// This also tests that Editor::breakpoint_at_cursor_head is working properly
20886/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
20887/// or when breakpoints were placed out of order. This tests for a regression too
20888#[gpui::test]
20889async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
20890 init_test(cx, |_| {});
20891
20892 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
20893 let fs = FakeFs::new(cx.executor());
20894 fs.insert_tree(
20895 path!("/a"),
20896 json!({
20897 "main.rs": sample_text,
20898 }),
20899 )
20900 .await;
20901 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20902 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20903 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20904
20905 let fs = FakeFs::new(cx.executor());
20906 fs.insert_tree(
20907 path!("/a"),
20908 json!({
20909 "main.rs": sample_text,
20910 }),
20911 )
20912 .await;
20913 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20914 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20915 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20916 let worktree_id = workspace
20917 .update(cx, |workspace, _window, cx| {
20918 workspace.project().update(cx, |project, cx| {
20919 project.worktrees(cx).next().unwrap().read(cx).id()
20920 })
20921 })
20922 .unwrap();
20923
20924 let buffer = project
20925 .update(cx, |project, cx| {
20926 project.open_buffer((worktree_id, "main.rs"), cx)
20927 })
20928 .await
20929 .unwrap();
20930
20931 let (editor, cx) = cx.add_window_view(|window, cx| {
20932 Editor::new(
20933 EditorMode::full(),
20934 MultiBuffer::build_from_buffer(buffer, cx),
20935 Some(project.clone()),
20936 window,
20937 cx,
20938 )
20939 });
20940
20941 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
20942 let abs_path = project.read_with(cx, |project, cx| {
20943 project
20944 .absolute_path(&project_path, cx)
20945 .map(|path_buf| Arc::from(path_buf.to_owned()))
20946 .unwrap()
20947 });
20948
20949 // assert we can add breakpoint on the first line
20950 editor.update_in(cx, |editor, window, cx| {
20951 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20952 editor.move_to_end(&MoveToEnd, window, cx);
20953 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20954 editor.move_up(&MoveUp, window, cx);
20955 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
20956 });
20957
20958 let breakpoints = editor.update(cx, |editor, cx| {
20959 editor
20960 .breakpoint_store()
20961 .as_ref()
20962 .unwrap()
20963 .read(cx)
20964 .all_source_breakpoints(cx)
20965 .clone()
20966 });
20967
20968 assert_eq!(1, breakpoints.len());
20969 assert_breakpoint(
20970 &breakpoints,
20971 &abs_path,
20972 vec![
20973 (0, Breakpoint::new_standard()),
20974 (2, Breakpoint::new_standard()),
20975 (3, Breakpoint::new_standard()),
20976 ],
20977 );
20978
20979 editor.update_in(cx, |editor, window, cx| {
20980 editor.move_to_beginning(&MoveToBeginning, window, cx);
20981 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20982 editor.move_to_end(&MoveToEnd, window, cx);
20983 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20984 // Disabling a breakpoint that doesn't exist should do nothing
20985 editor.move_up(&MoveUp, window, cx);
20986 editor.move_up(&MoveUp, window, cx);
20987 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
20988 });
20989
20990 let breakpoints = editor.update(cx, |editor, cx| {
20991 editor
20992 .breakpoint_store()
20993 .as_ref()
20994 .unwrap()
20995 .read(cx)
20996 .all_source_breakpoints(cx)
20997 .clone()
20998 });
20999
21000 let disable_breakpoint = {
21001 let mut bp = Breakpoint::new_standard();
21002 bp.state = BreakpointState::Disabled;
21003 bp
21004 };
21005
21006 assert_eq!(1, breakpoints.len());
21007 assert_breakpoint(
21008 &breakpoints,
21009 &abs_path,
21010 vec![
21011 (0, disable_breakpoint.clone()),
21012 (2, Breakpoint::new_standard()),
21013 (3, disable_breakpoint.clone()),
21014 ],
21015 );
21016
21017 editor.update_in(cx, |editor, window, cx| {
21018 editor.move_to_beginning(&MoveToBeginning, window, cx);
21019 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
21020 editor.move_to_end(&MoveToEnd, window, cx);
21021 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
21022 editor.move_up(&MoveUp, window, cx);
21023 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
21024 });
21025
21026 let breakpoints = editor.update(cx, |editor, cx| {
21027 editor
21028 .breakpoint_store()
21029 .as_ref()
21030 .unwrap()
21031 .read(cx)
21032 .all_source_breakpoints(cx)
21033 .clone()
21034 });
21035
21036 assert_eq!(1, breakpoints.len());
21037 assert_breakpoint(
21038 &breakpoints,
21039 &abs_path,
21040 vec![
21041 (0, Breakpoint::new_standard()),
21042 (2, disable_breakpoint),
21043 (3, Breakpoint::new_standard()),
21044 ],
21045 );
21046}
21047
21048#[gpui::test]
21049async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
21050 init_test(cx, |_| {});
21051 let capabilities = lsp::ServerCapabilities {
21052 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
21053 prepare_provider: Some(true),
21054 work_done_progress_options: Default::default(),
21055 })),
21056 ..Default::default()
21057 };
21058 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21059
21060 cx.set_state(indoc! {"
21061 struct Fˇoo {}
21062 "});
21063
21064 cx.update_editor(|editor, _, cx| {
21065 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21066 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21067 editor.highlight_background::<DocumentHighlightRead>(
21068 &[highlight_range],
21069 |theme| theme.colors().editor_document_highlight_read_background,
21070 cx,
21071 );
21072 });
21073
21074 let mut prepare_rename_handler = cx
21075 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
21076 move |_, _, _| async move {
21077 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
21078 start: lsp::Position {
21079 line: 0,
21080 character: 7,
21081 },
21082 end: lsp::Position {
21083 line: 0,
21084 character: 10,
21085 },
21086 })))
21087 },
21088 );
21089 let prepare_rename_task = cx
21090 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21091 .expect("Prepare rename was not started");
21092 prepare_rename_handler.next().await.unwrap();
21093 prepare_rename_task.await.expect("Prepare rename failed");
21094
21095 let mut rename_handler =
21096 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21097 let edit = lsp::TextEdit {
21098 range: lsp::Range {
21099 start: lsp::Position {
21100 line: 0,
21101 character: 7,
21102 },
21103 end: lsp::Position {
21104 line: 0,
21105 character: 10,
21106 },
21107 },
21108 new_text: "FooRenamed".to_string(),
21109 };
21110 Ok(Some(lsp::WorkspaceEdit::new(
21111 // Specify the same edit twice
21112 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
21113 )))
21114 });
21115 let rename_task = cx
21116 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21117 .expect("Confirm rename was not started");
21118 rename_handler.next().await.unwrap();
21119 rename_task.await.expect("Confirm rename failed");
21120 cx.run_until_parked();
21121
21122 // Despite two edits, only one is actually applied as those are identical
21123 cx.assert_editor_state(indoc! {"
21124 struct FooRenamedˇ {}
21125 "});
21126}
21127
21128#[gpui::test]
21129async fn test_rename_without_prepare(cx: &mut TestAppContext) {
21130 init_test(cx, |_| {});
21131 // These capabilities indicate that the server does not support prepare rename.
21132 let capabilities = lsp::ServerCapabilities {
21133 rename_provider: Some(lsp::OneOf::Left(true)),
21134 ..Default::default()
21135 };
21136 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
21137
21138 cx.set_state(indoc! {"
21139 struct Fˇoo {}
21140 "});
21141
21142 cx.update_editor(|editor, _window, cx| {
21143 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
21144 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
21145 editor.highlight_background::<DocumentHighlightRead>(
21146 &[highlight_range],
21147 |theme| theme.colors().editor_document_highlight_read_background,
21148 cx,
21149 );
21150 });
21151
21152 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
21153 .expect("Prepare rename was not started")
21154 .await
21155 .expect("Prepare rename failed");
21156
21157 let mut rename_handler =
21158 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
21159 let edit = lsp::TextEdit {
21160 range: lsp::Range {
21161 start: lsp::Position {
21162 line: 0,
21163 character: 7,
21164 },
21165 end: lsp::Position {
21166 line: 0,
21167 character: 10,
21168 },
21169 },
21170 new_text: "FooRenamed".to_string(),
21171 };
21172 Ok(Some(lsp::WorkspaceEdit::new(
21173 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
21174 )))
21175 });
21176 let rename_task = cx
21177 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
21178 .expect("Confirm rename was not started");
21179 rename_handler.next().await.unwrap();
21180 rename_task.await.expect("Confirm rename failed");
21181 cx.run_until_parked();
21182
21183 // Correct range is renamed, as `surrounding_word` is used to find it.
21184 cx.assert_editor_state(indoc! {"
21185 struct FooRenamedˇ {}
21186 "});
21187}
21188
21189#[gpui::test]
21190async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
21191 init_test(cx, |_| {});
21192 let mut cx = EditorTestContext::new(cx).await;
21193
21194 let language = Arc::new(
21195 Language::new(
21196 LanguageConfig::default(),
21197 Some(tree_sitter_html::LANGUAGE.into()),
21198 )
21199 .with_brackets_query(
21200 r#"
21201 ("<" @open "/>" @close)
21202 ("</" @open ">" @close)
21203 ("<" @open ">" @close)
21204 ("\"" @open "\"" @close)
21205 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
21206 "#,
21207 )
21208 .unwrap(),
21209 );
21210 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21211
21212 cx.set_state(indoc! {"
21213 <span>ˇ</span>
21214 "});
21215 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21216 cx.assert_editor_state(indoc! {"
21217 <span>
21218 ˇ
21219 </span>
21220 "});
21221
21222 cx.set_state(indoc! {"
21223 <span><span></span>ˇ</span>
21224 "});
21225 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21226 cx.assert_editor_state(indoc! {"
21227 <span><span></span>
21228 ˇ</span>
21229 "});
21230
21231 cx.set_state(indoc! {"
21232 <span>ˇ
21233 </span>
21234 "});
21235 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
21236 cx.assert_editor_state(indoc! {"
21237 <span>
21238 ˇ
21239 </span>
21240 "});
21241}
21242
21243#[gpui::test(iterations = 10)]
21244async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
21245 init_test(cx, |_| {});
21246
21247 let fs = FakeFs::new(cx.executor());
21248 fs.insert_tree(
21249 path!("/dir"),
21250 json!({
21251 "a.ts": "a",
21252 }),
21253 )
21254 .await;
21255
21256 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
21257 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21258 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21259
21260 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21261 language_registry.add(Arc::new(Language::new(
21262 LanguageConfig {
21263 name: "TypeScript".into(),
21264 matcher: LanguageMatcher {
21265 path_suffixes: vec!["ts".to_string()],
21266 ..Default::default()
21267 },
21268 ..Default::default()
21269 },
21270 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
21271 )));
21272 let mut fake_language_servers = language_registry.register_fake_lsp(
21273 "TypeScript",
21274 FakeLspAdapter {
21275 capabilities: lsp::ServerCapabilities {
21276 code_lens_provider: Some(lsp::CodeLensOptions {
21277 resolve_provider: Some(true),
21278 }),
21279 execute_command_provider: Some(lsp::ExecuteCommandOptions {
21280 commands: vec!["_the/command".to_string()],
21281 ..lsp::ExecuteCommandOptions::default()
21282 }),
21283 ..lsp::ServerCapabilities::default()
21284 },
21285 ..FakeLspAdapter::default()
21286 },
21287 );
21288
21289 let editor = workspace
21290 .update(cx, |workspace, window, cx| {
21291 workspace.open_abs_path(
21292 PathBuf::from(path!("/dir/a.ts")),
21293 OpenOptions::default(),
21294 window,
21295 cx,
21296 )
21297 })
21298 .unwrap()
21299 .await
21300 .unwrap()
21301 .downcast::<Editor>()
21302 .unwrap();
21303 cx.executor().run_until_parked();
21304
21305 let fake_server = fake_language_servers.next().await.unwrap();
21306
21307 let buffer = editor.update(cx, |editor, cx| {
21308 editor
21309 .buffer()
21310 .read(cx)
21311 .as_singleton()
21312 .expect("have opened a single file by path")
21313 });
21314
21315 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
21316 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
21317 drop(buffer_snapshot);
21318 let actions = cx
21319 .update_window(*workspace, |_, window, cx| {
21320 project.code_actions(&buffer, anchor..anchor, window, cx)
21321 })
21322 .unwrap();
21323
21324 fake_server
21325 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21326 Ok(Some(vec![
21327 lsp::CodeLens {
21328 range: lsp::Range::default(),
21329 command: Some(lsp::Command {
21330 title: "Code lens command".to_owned(),
21331 command: "_the/command".to_owned(),
21332 arguments: None,
21333 }),
21334 data: None,
21335 },
21336 lsp::CodeLens {
21337 range: lsp::Range::default(),
21338 command: Some(lsp::Command {
21339 title: "Command not in capabilities".to_owned(),
21340 command: "not in capabilities".to_owned(),
21341 arguments: None,
21342 }),
21343 data: None,
21344 },
21345 lsp::CodeLens {
21346 range: lsp::Range {
21347 start: lsp::Position {
21348 line: 1,
21349 character: 1,
21350 },
21351 end: lsp::Position {
21352 line: 1,
21353 character: 1,
21354 },
21355 },
21356 command: Some(lsp::Command {
21357 title: "Command not in range".to_owned(),
21358 command: "_the/command".to_owned(),
21359 arguments: None,
21360 }),
21361 data: None,
21362 },
21363 ]))
21364 })
21365 .next()
21366 .await;
21367
21368 let actions = actions.await.unwrap();
21369 assert_eq!(
21370 actions.len(),
21371 1,
21372 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
21373 );
21374 let action = actions[0].clone();
21375 let apply = project.update(cx, |project, cx| {
21376 project.apply_code_action(buffer.clone(), action, true, cx)
21377 });
21378
21379 // Resolving the code action does not populate its edits. In absence of
21380 // edits, we must execute the given command.
21381 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
21382 |mut lens, _| async move {
21383 let lens_command = lens.command.as_mut().expect("should have a command");
21384 assert_eq!(lens_command.title, "Code lens command");
21385 lens_command.arguments = Some(vec![json!("the-argument")]);
21386 Ok(lens)
21387 },
21388 );
21389
21390 // While executing the command, the language server sends the editor
21391 // a `workspaceEdit` request.
21392 fake_server
21393 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
21394 let fake = fake_server.clone();
21395 move |params, _| {
21396 assert_eq!(params.command, "_the/command");
21397 let fake = fake.clone();
21398 async move {
21399 fake.server
21400 .request::<lsp::request::ApplyWorkspaceEdit>(
21401 lsp::ApplyWorkspaceEditParams {
21402 label: None,
21403 edit: lsp::WorkspaceEdit {
21404 changes: Some(
21405 [(
21406 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
21407 vec![lsp::TextEdit {
21408 range: lsp::Range::new(
21409 lsp::Position::new(0, 0),
21410 lsp::Position::new(0, 0),
21411 ),
21412 new_text: "X".into(),
21413 }],
21414 )]
21415 .into_iter()
21416 .collect(),
21417 ),
21418 ..lsp::WorkspaceEdit::default()
21419 },
21420 },
21421 )
21422 .await
21423 .into_response()
21424 .unwrap();
21425 Ok(Some(json!(null)))
21426 }
21427 }
21428 })
21429 .next()
21430 .await;
21431
21432 // Applying the code lens command returns a project transaction containing the edits
21433 // sent by the language server in its `workspaceEdit` request.
21434 let transaction = apply.await.unwrap();
21435 assert!(transaction.0.contains_key(&buffer));
21436 buffer.update(cx, |buffer, cx| {
21437 assert_eq!(buffer.text(), "Xa");
21438 buffer.undo(cx);
21439 assert_eq!(buffer.text(), "a");
21440 });
21441
21442 let actions_after_edits = cx
21443 .update_window(*workspace, |_, window, cx| {
21444 project.code_actions(&buffer, anchor..anchor, window, cx)
21445 })
21446 .unwrap()
21447 .await
21448 .unwrap();
21449 assert_eq!(
21450 actions, actions_after_edits,
21451 "For the same selection, same code lens actions should be returned"
21452 );
21453
21454 let _responses =
21455 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
21456 panic!("No more code lens requests are expected");
21457 });
21458 editor.update_in(cx, |editor, window, cx| {
21459 editor.select_all(&SelectAll, window, cx);
21460 });
21461 cx.executor().run_until_parked();
21462 let new_actions = cx
21463 .update_window(*workspace, |_, window, cx| {
21464 project.code_actions(&buffer, anchor..anchor, window, cx)
21465 })
21466 .unwrap()
21467 .await
21468 .unwrap();
21469 assert_eq!(
21470 actions, new_actions,
21471 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
21472 );
21473}
21474
21475#[gpui::test]
21476async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
21477 init_test(cx, |_| {});
21478
21479 let fs = FakeFs::new(cx.executor());
21480 let main_text = r#"fn main() {
21481println!("1");
21482println!("2");
21483println!("3");
21484println!("4");
21485println!("5");
21486}"#;
21487 let lib_text = "mod foo {}";
21488 fs.insert_tree(
21489 path!("/a"),
21490 json!({
21491 "lib.rs": lib_text,
21492 "main.rs": main_text,
21493 }),
21494 )
21495 .await;
21496
21497 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21498 let (workspace, cx) =
21499 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21500 let worktree_id = workspace.update(cx, |workspace, cx| {
21501 workspace.project().update(cx, |project, cx| {
21502 project.worktrees(cx).next().unwrap().read(cx).id()
21503 })
21504 });
21505
21506 let expected_ranges = vec![
21507 Point::new(0, 0)..Point::new(0, 0),
21508 Point::new(1, 0)..Point::new(1, 1),
21509 Point::new(2, 0)..Point::new(2, 2),
21510 Point::new(3, 0)..Point::new(3, 3),
21511 ];
21512
21513 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21514 let editor_1 = workspace
21515 .update_in(cx, |workspace, window, cx| {
21516 workspace.open_path(
21517 (worktree_id, "main.rs"),
21518 Some(pane_1.downgrade()),
21519 true,
21520 window,
21521 cx,
21522 )
21523 })
21524 .unwrap()
21525 .await
21526 .downcast::<Editor>()
21527 .unwrap();
21528 pane_1.update(cx, |pane, cx| {
21529 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21530 open_editor.update(cx, |editor, cx| {
21531 assert_eq!(
21532 editor.display_text(cx),
21533 main_text,
21534 "Original main.rs text on initial open",
21535 );
21536 assert_eq!(
21537 editor
21538 .selections
21539 .all::<Point>(cx)
21540 .into_iter()
21541 .map(|s| s.range())
21542 .collect::<Vec<_>>(),
21543 vec![Point::zero()..Point::zero()],
21544 "Default selections on initial open",
21545 );
21546 })
21547 });
21548 editor_1.update_in(cx, |editor, window, cx| {
21549 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21550 s.select_ranges(expected_ranges.clone());
21551 });
21552 });
21553
21554 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
21555 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
21556 });
21557 let editor_2 = workspace
21558 .update_in(cx, |workspace, window, cx| {
21559 workspace.open_path(
21560 (worktree_id, "main.rs"),
21561 Some(pane_2.downgrade()),
21562 true,
21563 window,
21564 cx,
21565 )
21566 })
21567 .unwrap()
21568 .await
21569 .downcast::<Editor>()
21570 .unwrap();
21571 pane_2.update(cx, |pane, cx| {
21572 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21573 open_editor.update(cx, |editor, cx| {
21574 assert_eq!(
21575 editor.display_text(cx),
21576 main_text,
21577 "Original main.rs text on initial open in another panel",
21578 );
21579 assert_eq!(
21580 editor
21581 .selections
21582 .all::<Point>(cx)
21583 .into_iter()
21584 .map(|s| s.range())
21585 .collect::<Vec<_>>(),
21586 vec![Point::zero()..Point::zero()],
21587 "Default selections on initial open in another panel",
21588 );
21589 })
21590 });
21591
21592 editor_2.update_in(cx, |editor, window, cx| {
21593 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
21594 });
21595
21596 let _other_editor_1 = workspace
21597 .update_in(cx, |workspace, window, cx| {
21598 workspace.open_path(
21599 (worktree_id, "lib.rs"),
21600 Some(pane_1.downgrade()),
21601 true,
21602 window,
21603 cx,
21604 )
21605 })
21606 .unwrap()
21607 .await
21608 .downcast::<Editor>()
21609 .unwrap();
21610 pane_1
21611 .update_in(cx, |pane, window, cx| {
21612 pane.close_inactive_items(&CloseInactiveItems::default(), None, window, cx)
21613 })
21614 .await
21615 .unwrap();
21616 drop(editor_1);
21617 pane_1.update(cx, |pane, cx| {
21618 pane.active_item()
21619 .unwrap()
21620 .downcast::<Editor>()
21621 .unwrap()
21622 .update(cx, |editor, cx| {
21623 assert_eq!(
21624 editor.display_text(cx),
21625 lib_text,
21626 "Other file should be open and active",
21627 );
21628 });
21629 assert_eq!(pane.items().count(), 1, "No other editors should be open");
21630 });
21631
21632 let _other_editor_2 = workspace
21633 .update_in(cx, |workspace, window, cx| {
21634 workspace.open_path(
21635 (worktree_id, "lib.rs"),
21636 Some(pane_2.downgrade()),
21637 true,
21638 window,
21639 cx,
21640 )
21641 })
21642 .unwrap()
21643 .await
21644 .downcast::<Editor>()
21645 .unwrap();
21646 pane_2
21647 .update_in(cx, |pane, window, cx| {
21648 pane.close_inactive_items(&CloseInactiveItems::default(), None, window, cx)
21649 })
21650 .await
21651 .unwrap();
21652 drop(editor_2);
21653 pane_2.update(cx, |pane, cx| {
21654 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21655 open_editor.update(cx, |editor, cx| {
21656 assert_eq!(
21657 editor.display_text(cx),
21658 lib_text,
21659 "Other file should be open and active in another panel too",
21660 );
21661 });
21662 assert_eq!(
21663 pane.items().count(),
21664 1,
21665 "No other editors should be open in another pane",
21666 );
21667 });
21668
21669 let _editor_1_reopened = workspace
21670 .update_in(cx, |workspace, window, cx| {
21671 workspace.open_path(
21672 (worktree_id, "main.rs"),
21673 Some(pane_1.downgrade()),
21674 true,
21675 window,
21676 cx,
21677 )
21678 })
21679 .unwrap()
21680 .await
21681 .downcast::<Editor>()
21682 .unwrap();
21683 let _editor_2_reopened = workspace
21684 .update_in(cx, |workspace, window, cx| {
21685 workspace.open_path(
21686 (worktree_id, "main.rs"),
21687 Some(pane_2.downgrade()),
21688 true,
21689 window,
21690 cx,
21691 )
21692 })
21693 .unwrap()
21694 .await
21695 .downcast::<Editor>()
21696 .unwrap();
21697 pane_1.update(cx, |pane, cx| {
21698 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21699 open_editor.update(cx, |editor, cx| {
21700 assert_eq!(
21701 editor.display_text(cx),
21702 main_text,
21703 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
21704 );
21705 assert_eq!(
21706 editor
21707 .selections
21708 .all::<Point>(cx)
21709 .into_iter()
21710 .map(|s| s.range())
21711 .collect::<Vec<_>>(),
21712 expected_ranges,
21713 "Previous editor in the 1st panel had selections and should get them restored on reopen",
21714 );
21715 })
21716 });
21717 pane_2.update(cx, |pane, cx| {
21718 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21719 open_editor.update(cx, |editor, cx| {
21720 assert_eq!(
21721 editor.display_text(cx),
21722 r#"fn main() {
21723⋯rintln!("1");
21724⋯intln!("2");
21725⋯ntln!("3");
21726println!("4");
21727println!("5");
21728}"#,
21729 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
21730 );
21731 assert_eq!(
21732 editor
21733 .selections
21734 .all::<Point>(cx)
21735 .into_iter()
21736 .map(|s| s.range())
21737 .collect::<Vec<_>>(),
21738 vec![Point::zero()..Point::zero()],
21739 "Previous editor in the 2nd pane had no selections changed hence should restore none",
21740 );
21741 })
21742 });
21743}
21744
21745#[gpui::test]
21746async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
21747 init_test(cx, |_| {});
21748
21749 let fs = FakeFs::new(cx.executor());
21750 let main_text = r#"fn main() {
21751println!("1");
21752println!("2");
21753println!("3");
21754println!("4");
21755println!("5");
21756}"#;
21757 let lib_text = "mod foo {}";
21758 fs.insert_tree(
21759 path!("/a"),
21760 json!({
21761 "lib.rs": lib_text,
21762 "main.rs": main_text,
21763 }),
21764 )
21765 .await;
21766
21767 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21768 let (workspace, cx) =
21769 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
21770 let worktree_id = workspace.update(cx, |workspace, cx| {
21771 workspace.project().update(cx, |project, cx| {
21772 project.worktrees(cx).next().unwrap().read(cx).id()
21773 })
21774 });
21775
21776 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
21777 let editor = workspace
21778 .update_in(cx, |workspace, window, cx| {
21779 workspace.open_path(
21780 (worktree_id, "main.rs"),
21781 Some(pane.downgrade()),
21782 true,
21783 window,
21784 cx,
21785 )
21786 })
21787 .unwrap()
21788 .await
21789 .downcast::<Editor>()
21790 .unwrap();
21791 pane.update(cx, |pane, cx| {
21792 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21793 open_editor.update(cx, |editor, cx| {
21794 assert_eq!(
21795 editor.display_text(cx),
21796 main_text,
21797 "Original main.rs text on initial open",
21798 );
21799 })
21800 });
21801 editor.update_in(cx, |editor, window, cx| {
21802 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
21803 });
21804
21805 cx.update_global(|store: &mut SettingsStore, cx| {
21806 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21807 s.restore_on_file_reopen = Some(false);
21808 });
21809 });
21810 editor.update_in(cx, |editor, window, cx| {
21811 editor.fold_ranges(
21812 vec![
21813 Point::new(1, 0)..Point::new(1, 1),
21814 Point::new(2, 0)..Point::new(2, 2),
21815 Point::new(3, 0)..Point::new(3, 3),
21816 ],
21817 false,
21818 window,
21819 cx,
21820 );
21821 });
21822 pane.update_in(cx, |pane, window, cx| {
21823 pane.close_all_items(&CloseAllItems::default(), window, cx)
21824 })
21825 .await
21826 .unwrap();
21827 pane.update(cx, |pane, _| {
21828 assert!(pane.active_item().is_none());
21829 });
21830 cx.update_global(|store: &mut SettingsStore, cx| {
21831 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
21832 s.restore_on_file_reopen = Some(true);
21833 });
21834 });
21835
21836 let _editor_reopened = workspace
21837 .update_in(cx, |workspace, window, cx| {
21838 workspace.open_path(
21839 (worktree_id, "main.rs"),
21840 Some(pane.downgrade()),
21841 true,
21842 window,
21843 cx,
21844 )
21845 })
21846 .unwrap()
21847 .await
21848 .downcast::<Editor>()
21849 .unwrap();
21850 pane.update(cx, |pane, cx| {
21851 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
21852 open_editor.update(cx, |editor, cx| {
21853 assert_eq!(
21854 editor.display_text(cx),
21855 main_text,
21856 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
21857 );
21858 })
21859 });
21860}
21861
21862#[gpui::test]
21863async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
21864 struct EmptyModalView {
21865 focus_handle: gpui::FocusHandle,
21866 }
21867 impl EventEmitter<DismissEvent> for EmptyModalView {}
21868 impl Render for EmptyModalView {
21869 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
21870 div()
21871 }
21872 }
21873 impl Focusable for EmptyModalView {
21874 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
21875 self.focus_handle.clone()
21876 }
21877 }
21878 impl workspace::ModalView for EmptyModalView {}
21879 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
21880 EmptyModalView {
21881 focus_handle: cx.focus_handle(),
21882 }
21883 }
21884
21885 init_test(cx, |_| {});
21886
21887 let fs = FakeFs::new(cx.executor());
21888 let project = Project::test(fs, [], cx).await;
21889 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21890 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
21891 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21892 let editor = cx.new_window_entity(|window, cx| {
21893 Editor::new(
21894 EditorMode::full(),
21895 buffer,
21896 Some(project.clone()),
21897 window,
21898 cx,
21899 )
21900 });
21901 workspace
21902 .update(cx, |workspace, window, cx| {
21903 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
21904 })
21905 .unwrap();
21906 editor.update_in(cx, |editor, window, cx| {
21907 editor.open_context_menu(&OpenContextMenu, window, cx);
21908 assert!(editor.mouse_context_menu.is_some());
21909 });
21910 workspace
21911 .update(cx, |workspace, window, cx| {
21912 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
21913 })
21914 .unwrap();
21915 cx.read(|cx| {
21916 assert!(editor.read(cx).mouse_context_menu.is_none());
21917 });
21918}
21919
21920#[gpui::test]
21921async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
21922 init_test(cx, |_| {});
21923
21924 let fs = FakeFs::new(cx.executor());
21925 fs.insert_file(path!("/file.html"), Default::default())
21926 .await;
21927
21928 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
21929
21930 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
21931 let html_language = Arc::new(Language::new(
21932 LanguageConfig {
21933 name: "HTML".into(),
21934 matcher: LanguageMatcher {
21935 path_suffixes: vec!["html".to_string()],
21936 ..LanguageMatcher::default()
21937 },
21938 brackets: BracketPairConfig {
21939 pairs: vec![BracketPair {
21940 start: "<".into(),
21941 end: ">".into(),
21942 close: true,
21943 ..Default::default()
21944 }],
21945 ..Default::default()
21946 },
21947 ..Default::default()
21948 },
21949 Some(tree_sitter_html::LANGUAGE.into()),
21950 ));
21951 language_registry.add(html_language);
21952 let mut fake_servers = language_registry.register_fake_lsp(
21953 "HTML",
21954 FakeLspAdapter {
21955 capabilities: lsp::ServerCapabilities {
21956 completion_provider: Some(lsp::CompletionOptions {
21957 resolve_provider: Some(true),
21958 ..Default::default()
21959 }),
21960 ..Default::default()
21961 },
21962 ..Default::default()
21963 },
21964 );
21965
21966 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21967 let cx = &mut VisualTestContext::from_window(*workspace, cx);
21968
21969 let worktree_id = workspace
21970 .update(cx, |workspace, _window, cx| {
21971 workspace.project().update(cx, |project, cx| {
21972 project.worktrees(cx).next().unwrap().read(cx).id()
21973 })
21974 })
21975 .unwrap();
21976 project
21977 .update(cx, |project, cx| {
21978 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
21979 })
21980 .await
21981 .unwrap();
21982 let editor = workspace
21983 .update(cx, |workspace, window, cx| {
21984 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
21985 })
21986 .unwrap()
21987 .await
21988 .unwrap()
21989 .downcast::<Editor>()
21990 .unwrap();
21991
21992 let fake_server = fake_servers.next().await.unwrap();
21993 editor.update_in(cx, |editor, window, cx| {
21994 editor.set_text("<ad></ad>", window, cx);
21995 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21996 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
21997 });
21998 let Some((buffer, _)) = editor
21999 .buffer
22000 .read(cx)
22001 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
22002 else {
22003 panic!("Failed to get buffer for selection position");
22004 };
22005 let buffer = buffer.read(cx);
22006 let buffer_id = buffer.remote_id();
22007 let opening_range =
22008 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
22009 let closing_range =
22010 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
22011 let mut linked_ranges = HashMap::default();
22012 linked_ranges.insert(
22013 buffer_id,
22014 vec![(opening_range.clone(), vec![closing_range.clone()])],
22015 );
22016 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
22017 });
22018 let mut completion_handle =
22019 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
22020 Ok(Some(lsp::CompletionResponse::Array(vec![
22021 lsp::CompletionItem {
22022 label: "head".to_string(),
22023 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22024 lsp::InsertReplaceEdit {
22025 new_text: "head".to_string(),
22026 insert: lsp::Range::new(
22027 lsp::Position::new(0, 1),
22028 lsp::Position::new(0, 3),
22029 ),
22030 replace: lsp::Range::new(
22031 lsp::Position::new(0, 1),
22032 lsp::Position::new(0, 3),
22033 ),
22034 },
22035 )),
22036 ..Default::default()
22037 },
22038 ])))
22039 });
22040 editor.update_in(cx, |editor, window, cx| {
22041 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
22042 });
22043 cx.run_until_parked();
22044 completion_handle.next().await.unwrap();
22045 editor.update(cx, |editor, _| {
22046 assert!(
22047 editor.context_menu_visible(),
22048 "Completion menu should be visible"
22049 );
22050 });
22051 editor.update_in(cx, |editor, window, cx| {
22052 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
22053 });
22054 cx.executor().run_until_parked();
22055 editor.update(cx, |editor, cx| {
22056 assert_eq!(editor.text(cx), "<head></head>");
22057 });
22058}
22059
22060#[gpui::test]
22061async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
22062 init_test(cx, |_| {});
22063
22064 let fs = FakeFs::new(cx.executor());
22065 fs.insert_tree(
22066 path!("/root"),
22067 json!({
22068 "a": {
22069 "main.rs": "fn main() {}",
22070 },
22071 "foo": {
22072 "bar": {
22073 "external_file.rs": "pub mod external {}",
22074 }
22075 }
22076 }),
22077 )
22078 .await;
22079
22080 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
22081 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22082 language_registry.add(rust_lang());
22083 let _fake_servers = language_registry.register_fake_lsp(
22084 "Rust",
22085 FakeLspAdapter {
22086 ..FakeLspAdapter::default()
22087 },
22088 );
22089 let (workspace, cx) =
22090 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22091 let worktree_id = workspace.update(cx, |workspace, cx| {
22092 workspace.project().update(cx, |project, cx| {
22093 project.worktrees(cx).next().unwrap().read(cx).id()
22094 })
22095 });
22096
22097 let assert_language_servers_count =
22098 |expected: usize, context: &str, cx: &mut VisualTestContext| {
22099 project.update(cx, |project, cx| {
22100 let current = project
22101 .lsp_store()
22102 .read(cx)
22103 .as_local()
22104 .unwrap()
22105 .language_servers
22106 .len();
22107 assert_eq!(expected, current, "{context}");
22108 });
22109 };
22110
22111 assert_language_servers_count(
22112 0,
22113 "No servers should be running before any file is open",
22114 cx,
22115 );
22116 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
22117 let main_editor = workspace
22118 .update_in(cx, |workspace, window, cx| {
22119 workspace.open_path(
22120 (worktree_id, "main.rs"),
22121 Some(pane.downgrade()),
22122 true,
22123 window,
22124 cx,
22125 )
22126 })
22127 .unwrap()
22128 .await
22129 .downcast::<Editor>()
22130 .unwrap();
22131 pane.update(cx, |pane, cx| {
22132 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22133 open_editor.update(cx, |editor, cx| {
22134 assert_eq!(
22135 editor.display_text(cx),
22136 "fn main() {}",
22137 "Original main.rs text on initial open",
22138 );
22139 });
22140 assert_eq!(open_editor, main_editor);
22141 });
22142 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
22143
22144 let external_editor = workspace
22145 .update_in(cx, |workspace, window, cx| {
22146 workspace.open_abs_path(
22147 PathBuf::from("/root/foo/bar/external_file.rs"),
22148 OpenOptions::default(),
22149 window,
22150 cx,
22151 )
22152 })
22153 .await
22154 .expect("opening external file")
22155 .downcast::<Editor>()
22156 .expect("downcasted external file's open element to editor");
22157 pane.update(cx, |pane, cx| {
22158 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22159 open_editor.update(cx, |editor, cx| {
22160 assert_eq!(
22161 editor.display_text(cx),
22162 "pub mod external {}",
22163 "External file is open now",
22164 );
22165 });
22166 assert_eq!(open_editor, external_editor);
22167 });
22168 assert_language_servers_count(
22169 1,
22170 "Second, external, *.rs file should join the existing server",
22171 cx,
22172 );
22173
22174 pane.update_in(cx, |pane, window, cx| {
22175 pane.close_active_item(&CloseActiveItem::default(), window, cx)
22176 })
22177 .await
22178 .unwrap();
22179 pane.update_in(cx, |pane, window, cx| {
22180 pane.navigate_backward(window, cx);
22181 });
22182 cx.run_until_parked();
22183 pane.update(cx, |pane, cx| {
22184 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
22185 open_editor.update(cx, |editor, cx| {
22186 assert_eq!(
22187 editor.display_text(cx),
22188 "pub mod external {}",
22189 "External file is open now",
22190 );
22191 });
22192 });
22193 assert_language_servers_count(
22194 1,
22195 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
22196 cx,
22197 );
22198
22199 cx.update(|_, cx| {
22200 workspace::reload(&workspace::Reload::default(), cx);
22201 });
22202 assert_language_servers_count(
22203 1,
22204 "After reloading the worktree with local and external files opened, only one project should be started",
22205 cx,
22206 );
22207}
22208
22209#[gpui::test]
22210async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
22211 init_test(cx, |_| {});
22212
22213 let mut cx = EditorTestContext::new(cx).await;
22214 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22215 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22216
22217 // test cursor move to start of each line on tab
22218 // for `if`, `elif`, `else`, `while`, `with` and `for`
22219 cx.set_state(indoc! {"
22220 def main():
22221 ˇ for item in items:
22222 ˇ while item.active:
22223 ˇ if item.value > 10:
22224 ˇ continue
22225 ˇ elif item.value < 0:
22226 ˇ break
22227 ˇ else:
22228 ˇ with item.context() as ctx:
22229 ˇ yield count
22230 ˇ else:
22231 ˇ log('while else')
22232 ˇ else:
22233 ˇ log('for else')
22234 "});
22235 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22236 cx.assert_editor_state(indoc! {"
22237 def main():
22238 ˇfor item in items:
22239 ˇwhile item.active:
22240 ˇif item.value > 10:
22241 ˇcontinue
22242 ˇelif item.value < 0:
22243 ˇbreak
22244 ˇelse:
22245 ˇwith item.context() as ctx:
22246 ˇyield count
22247 ˇelse:
22248 ˇlog('while else')
22249 ˇelse:
22250 ˇlog('for else')
22251 "});
22252 // test relative indent is preserved when tab
22253 // for `if`, `elif`, `else`, `while`, `with` and `for`
22254 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22255 cx.assert_editor_state(indoc! {"
22256 def main():
22257 ˇfor item in items:
22258 ˇwhile item.active:
22259 ˇif item.value > 10:
22260 ˇcontinue
22261 ˇelif item.value < 0:
22262 ˇbreak
22263 ˇelse:
22264 ˇwith item.context() as ctx:
22265 ˇyield count
22266 ˇelse:
22267 ˇlog('while else')
22268 ˇelse:
22269 ˇlog('for else')
22270 "});
22271
22272 // test cursor move to start of each line on tab
22273 // for `try`, `except`, `else`, `finally`, `match` and `def`
22274 cx.set_state(indoc! {"
22275 def main():
22276 ˇ try:
22277 ˇ fetch()
22278 ˇ except ValueError:
22279 ˇ handle_error()
22280 ˇ else:
22281 ˇ match value:
22282 ˇ case _:
22283 ˇ finally:
22284 ˇ def status():
22285 ˇ return 0
22286 "});
22287 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22288 cx.assert_editor_state(indoc! {"
22289 def main():
22290 ˇtry:
22291 ˇfetch()
22292 ˇexcept ValueError:
22293 ˇhandle_error()
22294 ˇelse:
22295 ˇmatch value:
22296 ˇcase _:
22297 ˇfinally:
22298 ˇdef status():
22299 ˇreturn 0
22300 "});
22301 // test relative indent is preserved when tab
22302 // for `try`, `except`, `else`, `finally`, `match` and `def`
22303 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
22304 cx.assert_editor_state(indoc! {"
22305 def main():
22306 ˇtry:
22307 ˇfetch()
22308 ˇexcept ValueError:
22309 ˇhandle_error()
22310 ˇelse:
22311 ˇmatch value:
22312 ˇcase _:
22313 ˇfinally:
22314 ˇdef status():
22315 ˇreturn 0
22316 "});
22317}
22318
22319#[gpui::test]
22320async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
22321 init_test(cx, |_| {});
22322
22323 let mut cx = EditorTestContext::new(cx).await;
22324 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22325 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22326
22327 // test `else` auto outdents when typed inside `if` block
22328 cx.set_state(indoc! {"
22329 def main():
22330 if i == 2:
22331 return
22332 ˇ
22333 "});
22334 cx.update_editor(|editor, window, cx| {
22335 editor.handle_input("else:", window, cx);
22336 });
22337 cx.assert_editor_state(indoc! {"
22338 def main():
22339 if i == 2:
22340 return
22341 else:ˇ
22342 "});
22343
22344 // test `except` auto outdents when typed inside `try` block
22345 cx.set_state(indoc! {"
22346 def main():
22347 try:
22348 i = 2
22349 ˇ
22350 "});
22351 cx.update_editor(|editor, window, cx| {
22352 editor.handle_input("except:", window, cx);
22353 });
22354 cx.assert_editor_state(indoc! {"
22355 def main():
22356 try:
22357 i = 2
22358 except:ˇ
22359 "});
22360
22361 // test `else` auto outdents when typed inside `except` block
22362 cx.set_state(indoc! {"
22363 def main():
22364 try:
22365 i = 2
22366 except:
22367 j = 2
22368 ˇ
22369 "});
22370 cx.update_editor(|editor, window, cx| {
22371 editor.handle_input("else:", window, cx);
22372 });
22373 cx.assert_editor_state(indoc! {"
22374 def main():
22375 try:
22376 i = 2
22377 except:
22378 j = 2
22379 else:ˇ
22380 "});
22381
22382 // test `finally` auto outdents when typed inside `else` block
22383 cx.set_state(indoc! {"
22384 def main():
22385 try:
22386 i = 2
22387 except:
22388 j = 2
22389 else:
22390 k = 2
22391 ˇ
22392 "});
22393 cx.update_editor(|editor, window, cx| {
22394 editor.handle_input("finally:", window, cx);
22395 });
22396 cx.assert_editor_state(indoc! {"
22397 def main():
22398 try:
22399 i = 2
22400 except:
22401 j = 2
22402 else:
22403 k = 2
22404 finally:ˇ
22405 "});
22406
22407 // test `else` does not outdents when typed inside `except` block right after for block
22408 cx.set_state(indoc! {"
22409 def main():
22410 try:
22411 i = 2
22412 except:
22413 for i in range(n):
22414 pass
22415 ˇ
22416 "});
22417 cx.update_editor(|editor, window, cx| {
22418 editor.handle_input("else:", window, cx);
22419 });
22420 cx.assert_editor_state(indoc! {"
22421 def main():
22422 try:
22423 i = 2
22424 except:
22425 for i in range(n):
22426 pass
22427 else:ˇ
22428 "});
22429
22430 // test `finally` auto outdents when typed inside `else` block right after for block
22431 cx.set_state(indoc! {"
22432 def main():
22433 try:
22434 i = 2
22435 except:
22436 j = 2
22437 else:
22438 for i in range(n):
22439 pass
22440 ˇ
22441 "});
22442 cx.update_editor(|editor, window, cx| {
22443 editor.handle_input("finally:", window, cx);
22444 });
22445 cx.assert_editor_state(indoc! {"
22446 def main():
22447 try:
22448 i = 2
22449 except:
22450 j = 2
22451 else:
22452 for i in range(n):
22453 pass
22454 finally:ˇ
22455 "});
22456
22457 // test `except` outdents to inner "try" block
22458 cx.set_state(indoc! {"
22459 def main():
22460 try:
22461 i = 2
22462 if i == 2:
22463 try:
22464 i = 3
22465 ˇ
22466 "});
22467 cx.update_editor(|editor, window, cx| {
22468 editor.handle_input("except:", window, cx);
22469 });
22470 cx.assert_editor_state(indoc! {"
22471 def main():
22472 try:
22473 i = 2
22474 if i == 2:
22475 try:
22476 i = 3
22477 except:ˇ
22478 "});
22479
22480 // test `except` outdents to outer "try" block
22481 cx.set_state(indoc! {"
22482 def main():
22483 try:
22484 i = 2
22485 if i == 2:
22486 try:
22487 i = 3
22488 ˇ
22489 "});
22490 cx.update_editor(|editor, window, cx| {
22491 editor.handle_input("except:", window, cx);
22492 });
22493 cx.assert_editor_state(indoc! {"
22494 def main():
22495 try:
22496 i = 2
22497 if i == 2:
22498 try:
22499 i = 3
22500 except:ˇ
22501 "});
22502
22503 // test `else` stays at correct indent when typed after `for` block
22504 cx.set_state(indoc! {"
22505 def main():
22506 for i in range(10):
22507 if i == 3:
22508 break
22509 ˇ
22510 "});
22511 cx.update_editor(|editor, window, cx| {
22512 editor.handle_input("else:", window, cx);
22513 });
22514 cx.assert_editor_state(indoc! {"
22515 def main():
22516 for i in range(10):
22517 if i == 3:
22518 break
22519 else:ˇ
22520 "});
22521
22522 // test does not outdent on typing after line with square brackets
22523 cx.set_state(indoc! {"
22524 def f() -> list[str]:
22525 ˇ
22526 "});
22527 cx.update_editor(|editor, window, cx| {
22528 editor.handle_input("a", window, cx);
22529 });
22530 cx.assert_editor_state(indoc! {"
22531 def f() -> list[str]:
22532 aˇ
22533 "});
22534
22535 // test does not outdent on typing : after case keyword
22536 cx.set_state(indoc! {"
22537 match 1:
22538 caseˇ
22539 "});
22540 cx.update_editor(|editor, window, cx| {
22541 editor.handle_input(":", window, cx);
22542 });
22543 cx.assert_editor_state(indoc! {"
22544 match 1:
22545 case:ˇ
22546 "});
22547}
22548
22549#[gpui::test]
22550async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
22551 init_test(cx, |_| {});
22552 update_test_language_settings(cx, |settings| {
22553 settings.defaults.extend_comment_on_newline = Some(false);
22554 });
22555 let mut cx = EditorTestContext::new(cx).await;
22556 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
22557 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
22558
22559 // test correct indent after newline on comment
22560 cx.set_state(indoc! {"
22561 # COMMENT:ˇ
22562 "});
22563 cx.update_editor(|editor, window, cx| {
22564 editor.newline(&Newline, window, cx);
22565 });
22566 cx.assert_editor_state(indoc! {"
22567 # COMMENT:
22568 ˇ
22569 "});
22570
22571 // test correct indent after newline in brackets
22572 cx.set_state(indoc! {"
22573 {ˇ}
22574 "});
22575 cx.update_editor(|editor, window, cx| {
22576 editor.newline(&Newline, window, cx);
22577 });
22578 cx.run_until_parked();
22579 cx.assert_editor_state(indoc! {"
22580 {
22581 ˇ
22582 }
22583 "});
22584
22585 cx.set_state(indoc! {"
22586 (ˇ)
22587 "});
22588 cx.update_editor(|editor, window, cx| {
22589 editor.newline(&Newline, window, cx);
22590 });
22591 cx.run_until_parked();
22592 cx.assert_editor_state(indoc! {"
22593 (
22594 ˇ
22595 )
22596 "});
22597
22598 // do not indent after empty lists or dictionaries
22599 cx.set_state(indoc! {"
22600 a = []ˇ
22601 "});
22602 cx.update_editor(|editor, window, cx| {
22603 editor.newline(&Newline, window, cx);
22604 });
22605 cx.run_until_parked();
22606 cx.assert_editor_state(indoc! {"
22607 a = []
22608 ˇ
22609 "});
22610}
22611
22612fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
22613 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
22614 point..point
22615}
22616
22617#[track_caller]
22618fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
22619 let (text, ranges) = marked_text_ranges(marked_text, true);
22620 assert_eq!(editor.text(cx), text);
22621 assert_eq!(
22622 editor.selections.ranges(cx),
22623 ranges,
22624 "Assert selections are {}",
22625 marked_text
22626 );
22627}
22628
22629pub fn handle_signature_help_request(
22630 cx: &mut EditorLspTestContext,
22631 mocked_response: lsp::SignatureHelp,
22632) -> impl Future<Output = ()> + use<> {
22633 let mut request =
22634 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
22635 let mocked_response = mocked_response.clone();
22636 async move { Ok(Some(mocked_response)) }
22637 });
22638
22639 async move {
22640 request.next().await;
22641 }
22642}
22643
22644#[track_caller]
22645pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
22646 cx.update_editor(|editor, _, _| {
22647 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
22648 let entries = menu.entries.borrow();
22649 let entries = entries
22650 .iter()
22651 .map(|entry| entry.string.as_str())
22652 .collect::<Vec<_>>();
22653 assert_eq!(entries, expected);
22654 } else {
22655 panic!("Expected completions menu");
22656 }
22657 });
22658}
22659
22660/// Handle completion request passing a marked string specifying where the completion
22661/// should be triggered from using '|' character, what range should be replaced, and what completions
22662/// should be returned using '<' and '>' to delimit the range.
22663///
22664/// Also see `handle_completion_request_with_insert_and_replace`.
22665#[track_caller]
22666pub fn handle_completion_request(
22667 marked_string: &str,
22668 completions: Vec<&'static str>,
22669 is_incomplete: bool,
22670 counter: Arc<AtomicUsize>,
22671 cx: &mut EditorLspTestContext,
22672) -> impl Future<Output = ()> {
22673 let complete_from_marker: TextRangeMarker = '|'.into();
22674 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22675 let (_, mut marked_ranges) = marked_text_ranges_by(
22676 marked_string,
22677 vec![complete_from_marker.clone(), replace_range_marker.clone()],
22678 );
22679
22680 let complete_from_position =
22681 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22682 let replace_range =
22683 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22684
22685 let mut request =
22686 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22687 let completions = completions.clone();
22688 counter.fetch_add(1, atomic::Ordering::Release);
22689 async move {
22690 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22691 assert_eq!(
22692 params.text_document_position.position,
22693 complete_from_position
22694 );
22695 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
22696 is_incomplete: is_incomplete,
22697 item_defaults: None,
22698 items: completions
22699 .iter()
22700 .map(|completion_text| lsp::CompletionItem {
22701 label: completion_text.to_string(),
22702 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
22703 range: replace_range,
22704 new_text: completion_text.to_string(),
22705 })),
22706 ..Default::default()
22707 })
22708 .collect(),
22709 })))
22710 }
22711 });
22712
22713 async move {
22714 request.next().await;
22715 }
22716}
22717
22718/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
22719/// given instead, which also contains an `insert` range.
22720///
22721/// This function uses markers to define ranges:
22722/// - `|` marks the cursor position
22723/// - `<>` marks the replace range
22724/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
22725pub fn handle_completion_request_with_insert_and_replace(
22726 cx: &mut EditorLspTestContext,
22727 marked_string: &str,
22728 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
22729 counter: Arc<AtomicUsize>,
22730) -> impl Future<Output = ()> {
22731 let complete_from_marker: TextRangeMarker = '|'.into();
22732 let replace_range_marker: TextRangeMarker = ('<', '>').into();
22733 let insert_range_marker: TextRangeMarker = ('{', '}').into();
22734
22735 let (_, mut marked_ranges) = marked_text_ranges_by(
22736 marked_string,
22737 vec![
22738 complete_from_marker.clone(),
22739 replace_range_marker.clone(),
22740 insert_range_marker.clone(),
22741 ],
22742 );
22743
22744 let complete_from_position =
22745 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
22746 let replace_range =
22747 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
22748
22749 let insert_range = match marked_ranges.remove(&insert_range_marker) {
22750 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
22751 _ => lsp::Range {
22752 start: replace_range.start,
22753 end: complete_from_position,
22754 },
22755 };
22756
22757 let mut request =
22758 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
22759 let completions = completions.clone();
22760 counter.fetch_add(1, atomic::Ordering::Release);
22761 async move {
22762 assert_eq!(params.text_document_position.text_document.uri, url.clone());
22763 assert_eq!(
22764 params.text_document_position.position, complete_from_position,
22765 "marker `|` position doesn't match",
22766 );
22767 Ok(Some(lsp::CompletionResponse::Array(
22768 completions
22769 .iter()
22770 .map(|(label, new_text)| lsp::CompletionItem {
22771 label: label.to_string(),
22772 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
22773 lsp::InsertReplaceEdit {
22774 insert: insert_range,
22775 replace: replace_range,
22776 new_text: new_text.to_string(),
22777 },
22778 )),
22779 ..Default::default()
22780 })
22781 .collect(),
22782 )))
22783 }
22784 });
22785
22786 async move {
22787 request.next().await;
22788 }
22789}
22790
22791fn handle_resolve_completion_request(
22792 cx: &mut EditorLspTestContext,
22793 edits: Option<Vec<(&'static str, &'static str)>>,
22794) -> impl Future<Output = ()> {
22795 let edits = edits.map(|edits| {
22796 edits
22797 .iter()
22798 .map(|(marked_string, new_text)| {
22799 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
22800 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
22801 lsp::TextEdit::new(replace_range, new_text.to_string())
22802 })
22803 .collect::<Vec<_>>()
22804 });
22805
22806 let mut request =
22807 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
22808 let edits = edits.clone();
22809 async move {
22810 Ok(lsp::CompletionItem {
22811 additional_text_edits: edits,
22812 ..Default::default()
22813 })
22814 }
22815 });
22816
22817 async move {
22818 request.next().await;
22819 }
22820}
22821
22822pub(crate) fn update_test_language_settings(
22823 cx: &mut TestAppContext,
22824 f: impl Fn(&mut AllLanguageSettingsContent),
22825) {
22826 cx.update(|cx| {
22827 SettingsStore::update_global(cx, |store, cx| {
22828 store.update_user_settings::<AllLanguageSettings>(cx, f);
22829 });
22830 });
22831}
22832
22833pub(crate) fn update_test_project_settings(
22834 cx: &mut TestAppContext,
22835 f: impl Fn(&mut ProjectSettings),
22836) {
22837 cx.update(|cx| {
22838 SettingsStore::update_global(cx, |store, cx| {
22839 store.update_user_settings::<ProjectSettings>(cx, f);
22840 });
22841 });
22842}
22843
22844pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
22845 cx.update(|cx| {
22846 assets::Assets.load_test_fonts(cx);
22847 let store = SettingsStore::test(cx);
22848 cx.set_global(store);
22849 theme::init(theme::LoadThemes::JustBase, cx);
22850 release_channel::init(SemanticVersion::default(), cx);
22851 client::init_settings(cx);
22852 language::init(cx);
22853 Project::init_settings(cx);
22854 workspace::init_settings(cx);
22855 crate::init(cx);
22856 });
22857 zlog::init_test();
22858 update_test_language_settings(cx, f);
22859}
22860
22861#[track_caller]
22862fn assert_hunk_revert(
22863 not_reverted_text_with_selections: &str,
22864 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
22865 expected_reverted_text_with_selections: &str,
22866 base_text: &str,
22867 cx: &mut EditorLspTestContext,
22868) {
22869 cx.set_state(not_reverted_text_with_selections);
22870 cx.set_head_text(base_text);
22871 cx.executor().run_until_parked();
22872
22873 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
22874 let snapshot = editor.snapshot(window, cx);
22875 let reverted_hunk_statuses = snapshot
22876 .buffer_snapshot
22877 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
22878 .map(|hunk| hunk.status().kind)
22879 .collect::<Vec<_>>();
22880
22881 editor.git_restore(&Default::default(), window, cx);
22882 reverted_hunk_statuses
22883 });
22884 cx.executor().run_until_parked();
22885 cx.assert_editor_state(expected_reverted_text_with_selections);
22886 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
22887}
22888
22889#[gpui::test(iterations = 10)]
22890async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
22891 init_test(cx, |_| {});
22892
22893 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
22894 let counter = diagnostic_requests.clone();
22895
22896 let fs = FakeFs::new(cx.executor());
22897 fs.insert_tree(
22898 path!("/a"),
22899 json!({
22900 "first.rs": "fn main() { let a = 5; }",
22901 "second.rs": "// Test file",
22902 }),
22903 )
22904 .await;
22905
22906 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22907 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22908 let cx = &mut VisualTestContext::from_window(*workspace, cx);
22909
22910 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
22911 language_registry.add(rust_lang());
22912 let mut fake_servers = language_registry.register_fake_lsp(
22913 "Rust",
22914 FakeLspAdapter {
22915 capabilities: lsp::ServerCapabilities {
22916 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
22917 lsp::DiagnosticOptions {
22918 identifier: None,
22919 inter_file_dependencies: true,
22920 workspace_diagnostics: true,
22921 work_done_progress_options: Default::default(),
22922 },
22923 )),
22924 ..Default::default()
22925 },
22926 ..Default::default()
22927 },
22928 );
22929
22930 let editor = workspace
22931 .update(cx, |workspace, window, cx| {
22932 workspace.open_abs_path(
22933 PathBuf::from(path!("/a/first.rs")),
22934 OpenOptions::default(),
22935 window,
22936 cx,
22937 )
22938 })
22939 .unwrap()
22940 .await
22941 .unwrap()
22942 .downcast::<Editor>()
22943 .unwrap();
22944 let fake_server = fake_servers.next().await.unwrap();
22945 let server_id = fake_server.server.server_id();
22946 let mut first_request = fake_server
22947 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
22948 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
22949 let result_id = Some(new_result_id.to_string());
22950 assert_eq!(
22951 params.text_document.uri,
22952 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
22953 );
22954 async move {
22955 Ok(lsp::DocumentDiagnosticReportResult::Report(
22956 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
22957 related_documents: None,
22958 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
22959 items: Vec::new(),
22960 result_id,
22961 },
22962 }),
22963 ))
22964 }
22965 });
22966
22967 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
22968 project.update(cx, |project, cx| {
22969 let buffer_id = editor
22970 .read(cx)
22971 .buffer()
22972 .read(cx)
22973 .as_singleton()
22974 .expect("created a singleton buffer")
22975 .read(cx)
22976 .remote_id();
22977 let buffer_result_id = project
22978 .lsp_store()
22979 .read(cx)
22980 .result_id(server_id, buffer_id, cx);
22981 assert_eq!(expected, buffer_result_id);
22982 });
22983 };
22984
22985 ensure_result_id(None, cx);
22986 cx.executor().advance_clock(Duration::from_millis(60));
22987 cx.executor().run_until_parked();
22988 assert_eq!(
22989 diagnostic_requests.load(atomic::Ordering::Acquire),
22990 1,
22991 "Opening file should trigger diagnostic request"
22992 );
22993 first_request
22994 .next()
22995 .await
22996 .expect("should have sent the first diagnostics pull request");
22997 ensure_result_id(Some("1".to_string()), cx);
22998
22999 // Editing should trigger diagnostics
23000 editor.update_in(cx, |editor, window, cx| {
23001 editor.handle_input("2", window, cx)
23002 });
23003 cx.executor().advance_clock(Duration::from_millis(60));
23004 cx.executor().run_until_parked();
23005 assert_eq!(
23006 diagnostic_requests.load(atomic::Ordering::Acquire),
23007 2,
23008 "Editing should trigger diagnostic request"
23009 );
23010 ensure_result_id(Some("2".to_string()), cx);
23011
23012 // Moving cursor should not trigger diagnostic request
23013 editor.update_in(cx, |editor, window, cx| {
23014 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23015 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
23016 });
23017 });
23018 cx.executor().advance_clock(Duration::from_millis(60));
23019 cx.executor().run_until_parked();
23020 assert_eq!(
23021 diagnostic_requests.load(atomic::Ordering::Acquire),
23022 2,
23023 "Cursor movement should not trigger diagnostic request"
23024 );
23025 ensure_result_id(Some("2".to_string()), cx);
23026 // Multiple rapid edits should be debounced
23027 for _ in 0..5 {
23028 editor.update_in(cx, |editor, window, cx| {
23029 editor.handle_input("x", window, cx)
23030 });
23031 }
23032 cx.executor().advance_clock(Duration::from_millis(60));
23033 cx.executor().run_until_parked();
23034
23035 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
23036 assert!(
23037 final_requests <= 4,
23038 "Multiple rapid edits should be debounced (got {final_requests} requests)",
23039 );
23040 ensure_result_id(Some(final_requests.to_string()), cx);
23041}
23042
23043#[gpui::test]
23044async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
23045 // Regression test for issue #11671
23046 // Previously, adding a cursor after moving multiple cursors would reset
23047 // the cursor count instead of adding to the existing cursors.
23048 init_test(cx, |_| {});
23049 let mut cx = EditorTestContext::new(cx).await;
23050
23051 // Create a simple buffer with cursor at start
23052 cx.set_state(indoc! {"
23053 ˇaaaa
23054 bbbb
23055 cccc
23056 dddd
23057 eeee
23058 ffff
23059 gggg
23060 hhhh"});
23061
23062 // Add 2 cursors below (so we have 3 total)
23063 cx.update_editor(|editor, window, cx| {
23064 editor.add_selection_below(&Default::default(), window, cx);
23065 editor.add_selection_below(&Default::default(), window, cx);
23066 });
23067
23068 // Verify we have 3 cursors
23069 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
23070 assert_eq!(
23071 initial_count, 3,
23072 "Should have 3 cursors after adding 2 below"
23073 );
23074
23075 // Move down one line
23076 cx.update_editor(|editor, window, cx| {
23077 editor.move_down(&MoveDown, window, cx);
23078 });
23079
23080 // Add another cursor below
23081 cx.update_editor(|editor, window, cx| {
23082 editor.add_selection_below(&Default::default(), window, cx);
23083 });
23084
23085 // Should now have 4 cursors (3 original + 1 new)
23086 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
23087 assert_eq!(
23088 final_count, 4,
23089 "Should have 4 cursors after moving and adding another"
23090 );
23091}
23092
23093#[gpui::test(iterations = 10)]
23094async fn test_document_colors(cx: &mut TestAppContext) {
23095 let expected_color = Rgba {
23096 r: 0.33,
23097 g: 0.33,
23098 b: 0.33,
23099 a: 0.33,
23100 };
23101
23102 init_test(cx, |_| {});
23103
23104 let fs = FakeFs::new(cx.executor());
23105 fs.insert_tree(
23106 path!("/a"),
23107 json!({
23108 "first.rs": "fn main() { let a = 5; }",
23109 }),
23110 )
23111 .await;
23112
23113 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23114 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23115 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23116
23117 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23118 language_registry.add(rust_lang());
23119 let mut fake_servers = language_registry.register_fake_lsp(
23120 "Rust",
23121 FakeLspAdapter {
23122 capabilities: lsp::ServerCapabilities {
23123 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
23124 ..lsp::ServerCapabilities::default()
23125 },
23126 name: "rust-analyzer",
23127 ..FakeLspAdapter::default()
23128 },
23129 );
23130 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
23131 "Rust",
23132 FakeLspAdapter {
23133 capabilities: lsp::ServerCapabilities {
23134 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
23135 ..lsp::ServerCapabilities::default()
23136 },
23137 name: "not-rust-analyzer",
23138 ..FakeLspAdapter::default()
23139 },
23140 );
23141
23142 let editor = workspace
23143 .update(cx, |workspace, window, cx| {
23144 workspace.open_abs_path(
23145 PathBuf::from(path!("/a/first.rs")),
23146 OpenOptions::default(),
23147 window,
23148 cx,
23149 )
23150 })
23151 .unwrap()
23152 .await
23153 .unwrap()
23154 .downcast::<Editor>()
23155 .unwrap();
23156 let fake_language_server = fake_servers.next().await.unwrap();
23157 let fake_language_server_without_capabilities =
23158 fake_servers_without_capabilities.next().await.unwrap();
23159 let requests_made = Arc::new(AtomicUsize::new(0));
23160 let closure_requests_made = Arc::clone(&requests_made);
23161 let mut color_request_handle = fake_language_server
23162 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
23163 let requests_made = Arc::clone(&closure_requests_made);
23164 async move {
23165 assert_eq!(
23166 params.text_document.uri,
23167 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
23168 );
23169 requests_made.fetch_add(1, atomic::Ordering::Release);
23170 Ok(vec![
23171 lsp::ColorInformation {
23172 range: lsp::Range {
23173 start: lsp::Position {
23174 line: 0,
23175 character: 0,
23176 },
23177 end: lsp::Position {
23178 line: 0,
23179 character: 1,
23180 },
23181 },
23182 color: lsp::Color {
23183 red: 0.33,
23184 green: 0.33,
23185 blue: 0.33,
23186 alpha: 0.33,
23187 },
23188 },
23189 lsp::ColorInformation {
23190 range: lsp::Range {
23191 start: lsp::Position {
23192 line: 0,
23193 character: 0,
23194 },
23195 end: lsp::Position {
23196 line: 0,
23197 character: 1,
23198 },
23199 },
23200 color: lsp::Color {
23201 red: 0.33,
23202 green: 0.33,
23203 blue: 0.33,
23204 alpha: 0.33,
23205 },
23206 },
23207 ])
23208 }
23209 });
23210
23211 let _handle = fake_language_server_without_capabilities
23212 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
23213 panic!("Should not be called");
23214 });
23215 cx.executor().advance_clock(Duration::from_millis(100));
23216 color_request_handle.next().await.unwrap();
23217 cx.run_until_parked();
23218 assert_eq!(
23219 1,
23220 requests_made.load(atomic::Ordering::Acquire),
23221 "Should query for colors once per editor open"
23222 );
23223 editor.update_in(cx, |editor, _, cx| {
23224 assert_eq!(
23225 vec![expected_color],
23226 extract_color_inlays(editor, cx),
23227 "Should have an initial inlay"
23228 );
23229 });
23230
23231 // opening another file in a split should not influence the LSP query counter
23232 workspace
23233 .update(cx, |workspace, window, cx| {
23234 assert_eq!(
23235 workspace.panes().len(),
23236 1,
23237 "Should have one pane with one editor"
23238 );
23239 workspace.move_item_to_pane_in_direction(
23240 &MoveItemToPaneInDirection {
23241 direction: SplitDirection::Right,
23242 focus: false,
23243 clone: true,
23244 },
23245 window,
23246 cx,
23247 );
23248 })
23249 .unwrap();
23250 cx.run_until_parked();
23251 workspace
23252 .update(cx, |workspace, _, cx| {
23253 let panes = workspace.panes();
23254 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
23255 for pane in panes {
23256 let editor = pane
23257 .read(cx)
23258 .active_item()
23259 .and_then(|item| item.downcast::<Editor>())
23260 .expect("Should have opened an editor in each split");
23261 let editor_file = editor
23262 .read(cx)
23263 .buffer()
23264 .read(cx)
23265 .as_singleton()
23266 .expect("test deals with singleton buffers")
23267 .read(cx)
23268 .file()
23269 .expect("test buffese should have a file")
23270 .path();
23271 assert_eq!(
23272 editor_file.as_ref(),
23273 Path::new("first.rs"),
23274 "Both editors should be opened for the same file"
23275 )
23276 }
23277 })
23278 .unwrap();
23279
23280 cx.executor().advance_clock(Duration::from_millis(500));
23281 let save = editor.update_in(cx, |editor, window, cx| {
23282 editor.move_to_end(&MoveToEnd, window, cx);
23283 editor.handle_input("dirty", window, cx);
23284 editor.save(
23285 SaveOptions {
23286 format: true,
23287 autosave: true,
23288 },
23289 project.clone(),
23290 window,
23291 cx,
23292 )
23293 });
23294 save.await.unwrap();
23295
23296 color_request_handle.next().await.unwrap();
23297 cx.run_until_parked();
23298 assert_eq!(
23299 3,
23300 requests_made.load(atomic::Ordering::Acquire),
23301 "Should query for colors once per save and once per formatting after save"
23302 );
23303
23304 drop(editor);
23305 let close = workspace
23306 .update(cx, |workspace, window, cx| {
23307 workspace.active_pane().update(cx, |pane, cx| {
23308 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23309 })
23310 })
23311 .unwrap();
23312 close.await.unwrap();
23313 let close = workspace
23314 .update(cx, |workspace, window, cx| {
23315 workspace.active_pane().update(cx, |pane, cx| {
23316 pane.close_active_item(&CloseActiveItem::default(), window, cx)
23317 })
23318 })
23319 .unwrap();
23320 close.await.unwrap();
23321 assert_eq!(
23322 3,
23323 requests_made.load(atomic::Ordering::Acquire),
23324 "After saving and closing all editors, no extra requests should be made"
23325 );
23326 workspace
23327 .update(cx, |workspace, _, cx| {
23328 assert!(
23329 workspace.active_item(cx).is_none(),
23330 "Should close all editors"
23331 )
23332 })
23333 .unwrap();
23334
23335 workspace
23336 .update(cx, |workspace, window, cx| {
23337 workspace.active_pane().update(cx, |pane, cx| {
23338 pane.navigate_backward(window, cx);
23339 })
23340 })
23341 .unwrap();
23342 cx.executor().advance_clock(Duration::from_millis(100));
23343 cx.run_until_parked();
23344 let editor = workspace
23345 .update(cx, |workspace, _, cx| {
23346 workspace
23347 .active_item(cx)
23348 .expect("Should have reopened the editor again after navigating back")
23349 .downcast::<Editor>()
23350 .expect("Should be an editor")
23351 })
23352 .unwrap();
23353 color_request_handle.next().await.unwrap();
23354 assert_eq!(
23355 3,
23356 requests_made.load(atomic::Ordering::Acquire),
23357 "Cache should be reused on buffer close and reopen"
23358 );
23359 editor.update(cx, |editor, cx| {
23360 assert_eq!(
23361 vec![expected_color],
23362 extract_color_inlays(editor, cx),
23363 "Should have an initial inlay"
23364 );
23365 });
23366}
23367
23368#[gpui::test]
23369async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
23370 init_test(cx, |_| {});
23371 let (editor, cx) = cx.add_window_view(Editor::single_line);
23372 editor.update_in(cx, |editor, window, cx| {
23373 editor.set_text("oops\n\nwow\n", window, cx)
23374 });
23375 cx.run_until_parked();
23376 editor.update(cx, |editor, cx| {
23377 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
23378 });
23379 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
23380 cx.run_until_parked();
23381 editor.update(cx, |editor, cx| {
23382 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
23383 });
23384}
23385
23386#[track_caller]
23387fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
23388 editor
23389 .all_inlays(cx)
23390 .into_iter()
23391 .filter_map(|inlay| inlay.get_color())
23392 .map(Rgba::from)
23393 .collect()
23394}